feat: allow spreading of both attributes and event handlers (#2432)

This commit is contained in:
Lukas Potthast 2024-04-05 20:30:34 +02:00 committed by GitHub
parent fc537c14c4
commit 119c9ea23f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 386 additions and 25 deletions

View file

@ -22,7 +22,7 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Sember Checks - name: Semver Checks
uses: obi1kenobi/cargo-semver-checks-action@v2 uses: obi1kenobi/cargo-semver-checks-action@v2
with: with:
rust-toolchain: nightly-2024-03-31 rust-toolchain: nightly-2024-03-31

View file

@ -75,6 +75,13 @@ check-examples`.
We have a fairly extensive CI setup that runs both lints (like `rustfmt` and `clippy`) We have a fairly extensive CI setup that runs both lints (like `rustfmt` and `clippy`)
and tests on PRs. You can run most of these locally if you have `cargo-make` installed. and tests on PRs. You can run most of these locally if you have `cargo-make` installed.
Note that some of the `rustfmt` settings used require usage of the nightly compiler.
Formatting the code using the stable toolchain may result in a wrong code format and
subsequently CI errors.
Run `cargo +nightly fmt` if you want to keep the stable toolchain active.
You may want to let your IDE automatically use the `+nightly` parameter when a
"format on save" action is used.
If you added an example, make sure to add it to the list in `examples/Makefile.toml`. If you added an example, make sure to add it to the list in `examples/Makefile.toml`.
From the root directory of the repo, run From the root directory of the repo, run

View file

@ -10,7 +10,6 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"counter", "counter",
"counter_isomorphic", "counter_isomorphic",
"counters", "counters",
"counters_stable",
"counter_url_query", "counter_url_query",
"counter_without_macros", "counter_without_macros",
"directives", "directives",
@ -29,6 +28,7 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"server_fns_axum", "server_fns_axum",
"session_auth_axum", "session_auth_axum",
"slots", "slots",
"spread",
"sso_auth_axum", "sso_auth_axum",
"ssr_modes", "ssr_modes",
"ssr_modes_axum", "ssr_modes_axum",

View file

@ -17,10 +17,12 @@ You can also run any of the examples using [`cargo-make`](https://github.com/sag
Follow these steps to get any example up and running. Follow these steps to get any example up and running.
1. `cd` to the example you want to run 1. `cd` to the example you want to run
2. Run `cargo make ci` to setup and test the example 2. Make sure `cargo-make` is installed (for example by running `cargo install cargo-make`)
3. Run `cargo make start` to run the example 3. Make sure `rustup target add wasm32-unknown-unknown` was executed for the currently selected toolchain.
4. Open the client URL in the console output (<http://127.0.0.1:8080> or <http://127.0.0.1:3000> by default) 4. Run `cargo make ci` to setup and test the example
5. Run `cargo make stop` to end any processes started by `cargo make start`. 5. Run `cargo make start` to run the example
6. Open the client URL in the console output (<http://127.0.0.1:8080> or <http://127.0.0.1:3000> by default)
7. Run `cargo make stop` to end any processes started by `cargo make start`.
Here are a few additional notes: Here are a few additional notes:

View file

@ -0,0 +1,14 @@
[package]
name = "spread"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"

View file

@ -0,0 +1,4 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/trunk_server.toml" },
]

13
examples/spread/README.md Normal file
View file

@ -0,0 +1,13 @@
# Leptos Attribute and EventHandler spreading Example
This example creates a simple element in a client side rendered app with Rust and WASM!
Dynamic sets of attributes and event handler are spread onto the element with little effort.
## Getting Started
See the [Examples README](../README.md) for setup and run instructions.
## Quick Start
Run `trunk serve --open` to run this example.

View file

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
</head>
<body></body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly-2024-01-29"

View file

@ -0,0 +1,57 @@
use leptos::*;
/// Demonstrates how attributes and event handlers can be spread onto elements.
#[component]
pub fn SpreadingExample() -> impl IntoView {
fn alert(msg: impl AsRef<str>) {
let _ = window().alert_with_message(msg.as_ref());
}
let attrs_only: Vec<(&'static str, Attribute)> =
vec![("data-foo", "42".into_attribute())];
let event_handlers_only: Vec<EventHandlerFn> =
vec![EventHandlerFn::Click(Box::new(|_e: ev::MouseEvent| {
alert("event_handlers_only clicked");
}))];
let combined: Vec<Binding> = vec![
("data-foo", "123".into_attribute()).into(),
EventHandlerFn::Click(Box::new(|_e: ev::MouseEvent| {
alert("combined clicked");
}))
.into(),
];
let partial_attrs: Vec<(&'static str, Attribute)> =
vec![("data-foo", "11".into_attribute())];
let partial_event_handlers: Vec<EventHandlerFn> =
vec![EventHandlerFn::Click(Box::new(|_e: ev::MouseEvent| {
alert("partial_event_handlers clicked");
}))];
view! {
<div {..attrs_only}>
"<div {..attrs_only} />"
</div>
<div {..event_handlers_only}>
"<div {..event_handlers_only} />"
</div>
<div {..combined}>
"<div {..combined} />"
</div>
<div {..partial_attrs} {..partial_event_handlers}>
"<div {..partial_attrs} {..partial_event_handlers} />"
</div>
// Overwriting an event handler, here on:click, will result in a panic in debug builds. In release builds, the initial handler is kept.
// If spreading is used, prefer manually merging event handlers in the binding list instead.
//<div {..mixed} on:click=|_e| { alert("I will never be seen..."); }>
// "with overwritten click handler"
//</div>
}
}

View file

@ -0,0 +1,12 @@
use leptos::*;
use spread::SpreadingExample;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|| {
view! {
<SpreadingExample/>
}
})
}

View file

@ -27,8 +27,6 @@
//! the code that Leptos generates. //! the code that Leptos generates.
//! - [`counters`](https://github.com/leptos-rs/leptos/tree/main/examples/counters) introduces parent-child //! - [`counters`](https://github.com/leptos-rs/leptos/tree/main/examples/counters) introduces parent-child
//! communication via contexts, and the `<For/>` component for efficient keyed list updates. //! communication via contexts, and the `<For/>` component for efficient keyed list updates.
//! - [`counters_stable`](https://github.com/leptos-rs/leptos/tree/main/examples/counters_stable) adapts the `counters` example
//! to show how to use Leptos with `stable` Rust.
//! - [`error_boundary`](https://github.com/leptos-rs/leptos/tree/main/examples/error_boundary) shows how to use //! - [`error_boundary`](https://github.com/leptos-rs/leptos/tree/main/examples/error_boundary) shows how to use
//! `Result` types to handle errors. //! `Result` types to handle errors.
//! - [`parent_child`](https://github.com/leptos-rs/leptos/tree/main/examples/parent_child) shows four different //! - [`parent_child`](https://github.com/leptos-rs/leptos/tree/main/examples/parent_child) shows four different
@ -39,6 +37,7 @@
//! - [`router`](https://github.com/leptos-rs/leptos/tree/main/examples/router) shows how to use Leptoss nested router //! - [`router`](https://github.com/leptos-rs/leptos/tree/main/examples/router) shows how to use Leptoss nested router
//! to enable client-side navigation and route-specific, reactive data loading. //! to enable client-side navigation and route-specific, reactive data loading.
//! - [`slots`](https://github.com/leptos-rs/leptos/tree/main/examples/slots) shows how to use slots on components. //! - [`slots`](https://github.com/leptos-rs/leptos/tree/main/examples/slots) shows how to use slots on components.
//! - [`spread`](https://github.com/leptos-rs/leptos/tree/main/examples/spread) shows how the spread syntax can be used to spread data and/or event handlers onto elements.
//! - [`counter_isomorphic`](https://github.com/leptos-rs/leptos/tree/main/examples/counter_isomorphic) shows //! - [`counter_isomorphic`](https://github.com/leptos-rs/leptos/tree/main/examples/counter_isomorphic) shows
//! different methods of interaction with a stateful server, including server functions, server actions, forms, //! different methods of interaction with a stateful server, including server functions, server actions, forms,
//! and server-sent events (SSE). //! and server-sent events (SSE).
@ -162,9 +161,11 @@ pub use leptos_dom::{
set_interval_with_handle, set_timeout, set_timeout_with_handle, set_interval_with_handle, set_timeout, set_timeout_with_handle,
window_event_listener, window_event_listener_untyped, window_event_listener, window_event_listener_untyped,
}, },
html, math, mount_to, mount_to_body, nonce, svg, window, Attribute, Class, html,
CollectView, Errors, Fragment, HtmlElement, IntoAttribute, IntoClass, html::Binding,
IntoProperty, IntoStyle, IntoView, NodeRef, Property, View, math, mount_to, mount_to_body, nonce, svg, window, Attribute, Class,
CollectView, Errors, EventHandlerFn, Fragment, HtmlElement, IntoAttribute,
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
}; };
/// Utilities for simple isomorphic logging to the console or terminal. /// Utilities for simple isomorphic logging to the console or terminal.
pub mod logging { pub mod logging {

View file

@ -167,6 +167,86 @@ impl DOMEventResponder for crate::View {
} }
} }
/// A statically typed event handler.
pub enum EventHandlerFn {
/// `keydown` event handler.
Keydown(Box<dyn FnMut(KeyboardEvent)>),
/// `keyup` event handler.
Keyup(Box<dyn FnMut(KeyboardEvent)>),
/// `keypress` event handler.
Keypress(Box<dyn FnMut(KeyboardEvent)>),
/// `click` event handler.
Click(Box<dyn FnMut(MouseEvent)>),
/// `dblclick` event handler.
Dblclick(Box<dyn FnMut(MouseEvent)>),
/// `mousedown` event handler.
Mousedown(Box<dyn FnMut(MouseEvent)>),
/// `mouseup` event handler.
Mouseup(Box<dyn FnMut(MouseEvent)>),
/// `mouseenter` event handler.
Mouseenter(Box<dyn FnMut(MouseEvent)>),
/// `mouseleave` event handler.
Mouseleave(Box<dyn FnMut(MouseEvent)>),
/// `mouseout` event handler.
Mouseout(Box<dyn FnMut(MouseEvent)>),
/// `mouseover` event handler.
Mouseover(Box<dyn FnMut(MouseEvent)>),
/// `mousemove` event handler.
Mousemove(Box<dyn FnMut(MouseEvent)>),
/// `wheel` event handler.
Wheel(Box<dyn FnMut(WheelEvent)>),
/// `touchstart` event handler.
Touchstart(Box<dyn FnMut(TouchEvent)>),
/// `touchend` event handler.
Touchend(Box<dyn FnMut(TouchEvent)>),
/// `touchcancel` event handler.
Touchcancel(Box<dyn FnMut(TouchEvent)>),
/// `touchmove` event handler.
Touchmove(Box<dyn FnMut(TouchEvent)>),
/// `pointerenter` event handler.
Pointerenter(Box<dyn FnMut(PointerEvent)>),
/// `pointerleave` event handler.
Pointerleave(Box<dyn FnMut(PointerEvent)>),
/// `pointerdown` event handler.
Pointerdown(Box<dyn FnMut(PointerEvent)>),
/// `pointerup` event handler.
Pointerup(Box<dyn FnMut(PointerEvent)>),
/// `pointercancel` event handler.
Pointercancel(Box<dyn FnMut(PointerEvent)>),
/// `pointerout` event handler.
Pointerout(Box<dyn FnMut(PointerEvent)>),
/// `pointerover` event handler.
Pointerover(Box<dyn FnMut(PointerEvent)>),
/// `pointermove` event handler.
Pointermove(Box<dyn FnMut(PointerEvent)>),
/// `drag` event handler.
Drag(Box<dyn FnMut(DragEvent)>),
/// `dragend` event handler.
Dragend(Box<dyn FnMut(DragEvent)>),
/// `dragenter` event handler.
Dragenter(Box<dyn FnMut(DragEvent)>),
/// `dragleave` event handler.
Dragleave(Box<dyn FnMut(DragEvent)>),
/// `dragstart` event handler.
Dragstart(Box<dyn FnMut(DragEvent)>),
/// `drop` event handler.
Drop(Box<dyn FnMut(DragEvent)>),
/// `blur` event handler.
Blur(Box<dyn FnMut(FocusEvent)>),
/// `focusout` event handler.
Focusout(Box<dyn FnMut(FocusEvent)>),
/// `focus` event handler.
Focus(Box<dyn FnMut(FocusEvent)>),
/// `focusin` event handler.
Focusin(Box<dyn FnMut(FocusEvent)>),
}
/// Type that can be used to handle DOM events /// Type that can be used to handle DOM events
pub trait EventHandler { pub trait EventHandler {
/// Attaches event listener to any target that can respond to DOM events /// Attaches event listener to any target that can respond to DOM events

View file

@ -63,7 +63,7 @@ cfg_if! {
use crate::{ use crate::{
create_node_ref, create_node_ref,
ev::EventDescriptor, ev::{EventDescriptor, EventHandlerFn},
hydration::HydrationCtx, hydration::HydrationCtx,
macro_helpers::{ macro_helpers::{
Attribute, IntoAttribute, IntoClass, IntoProperty, IntoStyle, Attribute, IntoAttribute, IntoClass, IntoProperty, IntoStyle,
@ -366,6 +366,33 @@ where
} }
} }
/// Bind data through attributes, or behavior through event handlers, to an element.
/// A value of any type able to provide an iterator of bindings (like a: `Vec<Binding>`),
/// can be spread onto an element using the spread syntax `view! { <div {..bindings} /> }`.
pub enum Binding {
/// A statically named attribute.
Attribute {
/// Name of the attribute.
name: &'static str,
/// Value of the attribute, possibly reactive.
value: Attribute,
},
/// A statically typed event handler.
EventHandler(EventHandlerFn),
}
impl From<(&'static str, Attribute)> for Binding {
fn from((name, value): (&'static str, Attribute)) -> Self {
Self::Attribute { name, value }
}
}
impl From<EventHandlerFn> for Binding {
fn from(handler: EventHandlerFn) -> Self {
Self::EventHandler(handler)
}
}
impl<El: ElementDescriptor + 'static> HtmlElement<El> { impl<El: ElementDescriptor + 'static> HtmlElement<El> {
pub(crate) fn new(element: El) -> Self { pub(crate) fn new(element: El) -> Self {
cfg_if! { cfg_if! {
@ -651,7 +678,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
} }
} }
/// Adds multiple attributes to the element /// Adds multiple attributes to the element.
#[track_caller] #[track_caller]
pub fn attrs( pub fn attrs(
mut self, mut self,
@ -663,6 +690,133 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
self self
} }
/// Adds multiple bindings (attributes or event handlers) to the element.
#[track_caller]
pub fn bindings<B: Into<Binding>>(
mut self,
bindings: impl std::iter::IntoIterator<Item = B>,
) -> Self {
for binding in bindings {
self = self.binding(binding.into());
}
self
}
/// Add a single binding (attribute or event handler) to the element.
#[track_caller]
fn binding(self, binding: Binding) -> Self {
match binding {
Binding::Attribute { name, value } => self.attr(name, value),
Binding::EventHandler(handler) => match handler {
EventHandlerFn::Keydown(handler) => {
self.on(crate::events::typed::keydown, handler)
}
EventHandlerFn::Keyup(handler) => {
self.on(crate::events::typed::keyup, handler)
}
EventHandlerFn::Keypress(handler) => {
self.on(crate::events::typed::keypress, handler)
}
EventHandlerFn::Click(handler) => {
self.on(crate::events::typed::click, handler)
}
EventHandlerFn::Dblclick(handler) => {
self.on(crate::events::typed::dblclick, handler)
}
EventHandlerFn::Mousedown(handler) => {
self.on(crate::events::typed::mousedown, handler)
}
EventHandlerFn::Mouseup(handler) => {
self.on(crate::events::typed::mouseup, handler)
}
EventHandlerFn::Mouseenter(handler) => {
self.on(crate::events::typed::mouseenter, handler)
}
EventHandlerFn::Mouseleave(handler) => {
self.on(crate::events::typed::mouseleave, handler)
}
EventHandlerFn::Mouseout(handler) => {
self.on(crate::events::typed::mouseout, handler)
}
EventHandlerFn::Mouseover(handler) => {
self.on(crate::events::typed::mouseover, handler)
}
EventHandlerFn::Mousemove(handler) => {
self.on(crate::events::typed::mousemove, handler)
}
EventHandlerFn::Wheel(handler) => {
self.on(crate::events::typed::wheel, handler)
}
EventHandlerFn::Touchstart(handler) => {
self.on(crate::events::typed::touchstart, handler)
}
EventHandlerFn::Touchend(handler) => {
self.on(crate::events::typed::touchend, handler)
}
EventHandlerFn::Touchcancel(handler) => {
self.on(crate::events::typed::touchcancel, handler)
}
EventHandlerFn::Touchmove(handler) => {
self.on(crate::events::typed::touchmove, handler)
}
EventHandlerFn::Pointerenter(handler) => {
self.on(crate::events::typed::pointerenter, handler)
}
EventHandlerFn::Pointerleave(handler) => {
self.on(crate::events::typed::pointerleave, handler)
}
EventHandlerFn::Pointerdown(handler) => {
self.on(crate::events::typed::pointerdown, handler)
}
EventHandlerFn::Pointerup(handler) => {
self.on(crate::events::typed::pointerup, handler)
}
EventHandlerFn::Pointercancel(handler) => {
self.on(crate::events::typed::pointercancel, handler)
}
EventHandlerFn::Pointerout(handler) => {
self.on(crate::events::typed::pointerout, handler)
}
EventHandlerFn::Pointerover(handler) => {
self.on(crate::events::typed::pointerover, handler)
}
EventHandlerFn::Pointermove(handler) => {
self.on(crate::events::typed::pointermove, handler)
}
EventHandlerFn::Drag(handler) => {
self.on(crate::events::typed::drag, handler)
}
EventHandlerFn::Dragend(handler) => {
self.on(crate::events::typed::dragend, handler)
}
EventHandlerFn::Dragenter(handler) => {
self.on(crate::events::typed::dragenter, handler)
}
EventHandlerFn::Dragleave(handler) => {
self.on(crate::events::typed::dragleave, handler)
}
EventHandlerFn::Dragstart(handler) => {
self.on(crate::events::typed::dragstart, handler)
}
EventHandlerFn::Drop(handler) => {
self.on(crate::events::typed::drop, handler)
}
EventHandlerFn::Blur(handler) => {
self.on(crate::events::typed::blur, handler)
}
EventHandlerFn::Focusout(handler) => {
self.on(crate::events::typed::focusout, handler)
}
EventHandlerFn::Focus(handler) => {
self.on(crate::events::typed::focus, handler)
}
EventHandlerFn::Focusin(handler) => {
self.on(crate::events::typed::focusin, handler)
}
},
}
}
/// Adds a class to an element. /// Adds a class to an element.
/// ///
/// **Note**: In the builder syntax, this will be overwritten by the `class` /// **Note**: In the builder syntax, this will be overwritten by the `class`

View file

@ -36,7 +36,10 @@ pub use directive::*;
pub use events::add_event_helper; pub use events::add_event_helper;
#[cfg(all(target_arch = "wasm32", feature = "web"))] #[cfg(all(target_arch = "wasm32", feature = "web"))]
use events::{add_event_listener, add_event_listener_undelegated}; use events::{add_event_listener, add_event_listener_undelegated};
pub use events::{typed as ev, typed::EventHandler}; pub use events::{
typed as ev,
typed::{EventHandler, EventHandlerFn},
};
pub use html::HtmlElement; pub use html::HtmlElement;
use html::{AnyElement, ElementDescriptor}; use html::{AnyElement, ElementDescriptor};
pub use hydration::{HydrationCtx, HydrationKey}; pub use hydration::{HydrationCtx, HydrationKey};

View file

@ -223,7 +223,7 @@ pub(crate) fn element_to_tokens(
None None
} }
}); });
let spread_attrs = node.attributes().iter().filter_map(|node| { let bindings = node.attributes().iter().filter_map(|node| {
use rstml::node::NodeBlock; use rstml::node::NodeBlock;
use syn::{Expr, ExprRange, RangeLimits, Stmt}; use syn::{Expr, ExprRange, RangeLimits, Stmt};
@ -237,7 +237,9 @@ pub(crate) fn element_to_tokens(
.. ..
}), }),
_, _,
) => Some(quote! { .attrs(#[allow(unused_brace)] {#end}) }), ) => Some(
quote! { .bindings(#[allow(unused_brace)] {#end}) },
),
_ => None, _ => None,
} }
} else { } else {
@ -356,7 +358,7 @@ pub(crate) fn element_to_tokens(
#(#ide_helper_close_tag)* #(#ide_helper_close_tag)*
#name #name
#(#attrs)* #(#attrs)*
#(#spread_attrs)* #(#bindings)*
#(#class_attrs)* #(#class_attrs)*
#(#style_attrs)* #(#style_attrs)*
#global_class_expr #global_class_expr

View file

@ -28,7 +28,6 @@
//! ## Example //! ## Example
//! //!
//! ```rust //! ```rust
//!
//! use leptos::*; //! use leptos::*;
//! use leptos_router::*; //! use leptos_router::*;
//! //!

View file

@ -1,6 +1,9 @@
# Stable options
edition = "2021" edition = "2021"
imports_granularity = "Crate"
max_width = 80 max_width = 80
# Unstable options
imports_granularity = "Crate"
format_strings = true format_strings = true
group_imports = "One" group_imports = "One"
format_code_in_doc_comments = true format_code_in_doc_comments = true