From 119c9ea23fac0bec45eb5c19fd4d458a17624cfb Mon Sep 17 00:00:00 2001 From: Lukas Potthast Date: Fri, 5 Apr 2024 20:30:34 +0200 Subject: [PATCH] feat: allow spreading of both attributes and event handlers (#2432) --- .github/workflows/ci-semver.yml | 2 +- CONTRIBUTING.md | 11 +- README.md | 4 +- examples/Makefile.toml | 2 +- examples/README.md | 14 ++- examples/spread/Cargo.toml | 14 +++ examples/spread/Makefile.toml | 4 + examples/spread/README.md | 13 ++ examples/spread/index.html | 8 ++ examples/spread/public/favicon.ico | Bin 0 -> 15406 bytes examples/spread/rust-toolchain.toml | 2 + examples/spread/src/lib.rs | 57 +++++++++ examples/spread/src/main.rs | 12 ++ leptos/src/lib.rs | 11 +- leptos_dom/src/events/typed.rs | 80 ++++++++++++ leptos_dom/src/html.rs | 158 +++++++++++++++++++++++- leptos_dom/src/lib.rs | 5 +- leptos_macro/src/view/client_builder.rs | 8 +- router/src/lib.rs | 1 - rustfmt.toml | 5 +- 20 files changed, 386 insertions(+), 25 deletions(-) create mode 100644 examples/spread/Cargo.toml create mode 100644 examples/spread/Makefile.toml create mode 100644 examples/spread/README.md create mode 100644 examples/spread/index.html create mode 100644 examples/spread/public/favicon.ico create mode 100644 examples/spread/rust-toolchain.toml create mode 100644 examples/spread/src/lib.rs create mode 100644 examples/spread/src/main.rs diff --git a/.github/workflows/ci-semver.yml b/.github/workflows/ci-semver.yml index 77bc61402..f5d756702 100644 --- a/.github/workflows/ci-semver.yml +++ b/.github/workflows/ci-semver.yml @@ -22,7 +22,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Sember Checks + - name: Semver Checks uses: obi1kenobi/cargo-semver-checks-action@v2 with: rust-toolchain: nightly-2024-03-31 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 126d202f7..d613c5723 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,12 +72,19 @@ check-examples`. ## Before Submitting a PR -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. +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`. -From the root directory of the repo, run +From the root directory of the repo, run - `cargo +nightly fmt` - `cargo +nightly make check` - `cargo +nightly make test` diff --git a/README.md b/README.md index 5c93aa8ae..93e185385 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ The new rendering approach being developed for 0.7 supports “universal renderi ### How is this different from Yew? -Yew is the most-used library for Rust web UI development, but there are several differences between Yew and Leptos, in philosophy, approach, and performance. +Yew is the most-used library for Rust web UI development, but there are several differences between Yew and Leptos, in philosophy, approach, and performance. - **VDOM vs. fine-grained:** Yew is built on the virtual DOM (VDOM) model: state changes cause components to re-render, generating a new virtual DOM tree. Yew diffs this against the previous VDOM, and applies those patches to the actual DOM. Component functions rerun whenever state changes. Leptos takes an entirely different approach. Components run once, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes. - **Performance:** This has huge performance implications: Leptos is simply much faster at both creating and updating the UI than Yew is. @@ -182,4 +182,4 @@ Sycamore and Leptos are both heavily influenced by SolidJS. At this point, Lepto - **Templating DSLs:** Sycamore uses a custom templating language for its views, while Leptos uses a JSX-like template format. - **`'static` signals:** One of Leptos’s main innovations was the creation of `Copy + 'static` signals, which have excellent ergonomics. Sycamore is in the process of adopting the same pattern, but this is not yet released. -- **Perseus vs. server functions:** The Perseus metaframework provides an opinionated way to build Sycamore apps that include server functionality. Leptos instead provides primitives like server functions in the core of the framework. +- **Perseus vs. server functions:** The Perseus metaframework provides an opinionated way to build Sycamore apps that include server functionality. Leptos instead provides primitives like server functions in the core of the framework. diff --git a/examples/Makefile.toml b/examples/Makefile.toml index d717e26f5..8f96ee2be 100644 --- a/examples/Makefile.toml +++ b/examples/Makefile.toml @@ -10,7 +10,6 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [ "counter", "counter_isomorphic", "counters", - "counters_stable", "counter_url_query", "counter_without_macros", "directives", @@ -29,6 +28,7 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [ "server_fns_axum", "session_auth_axum", "slots", + "spread", "sso_auth_axum", "ssr_modes", "ssr_modes_axum", diff --git a/examples/README.md b/examples/README.md index 9bc9c0b90..237827e94 100644 --- a/examples/README.md +++ b/examples/README.md @@ -8,7 +8,7 @@ To the extent that new features have been released or breaking changes have been ## Getting Started -The simplest way to get started with any example is to use the “quick start” command found in the README for each example. Most of the examples use either [`trunk`](https://trunkrs.dev/) (a simple build system and dev server for client-side-rendered apps) or [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) (a build system for server-rendered and client-hydrated apps). +The simplest way to get started with any example is to use the “quick start” command found in the README for each example. Most of the examples use either [`trunk`](https://trunkrs.dev/) (a simple build system and dev server for client-side-rendered apps) or [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) (a build system for server-rendered and client-hydrated apps). ## Using Cargo Make @@ -17,15 +17,17 @@ 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. 1. `cd` to the example you want to run -2. Run `cargo make ci` to setup and test the example -3. Run `cargo make start` to run the example -4. Open the client URL in the console output ( or by default) -5. Run `cargo make stop` to end any processes started by `cargo make start`. +2. Make sure `cargo-make` is installed (for example by running `cargo install cargo-make`) +3. Make sure `rustup target add wasm32-unknown-unknown` was executed for the currently selected toolchain. +4. Run `cargo make ci` to setup and test the example +5. Run `cargo make start` to run the example +6. Open the client URL in the console output ( or by default) +7. Run `cargo make stop` to end any processes started by `cargo make start`. Here are a few additional notes: - Extendable custom task files are located in the [cargo-make](./cargo-make/) directory -- Running a task will automatically install `cargo` dependencies +- Running a task will automatically install `cargo` dependencies - Each `Makefile.toml` file must extend the [cargo-make/main.toml](./cargo-make/main.toml) file - [cargo-make](./cargo-make/) files that end in `*-test.toml` configure web testing strategies - Run `cargo make test-report` to learn which examples have web tests diff --git a/examples/spread/Cargo.toml b/examples/spread/Cargo.toml new file mode 100644 index 000000000..67cba0cdd --- /dev/null +++ b/examples/spread/Cargo.toml @@ -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" diff --git a/examples/spread/Makefile.toml b/examples/spread/Makefile.toml new file mode 100644 index 000000000..bd560af7b --- /dev/null +++ b/examples/spread/Makefile.toml @@ -0,0 +1,4 @@ +extend = [ + { path = "../cargo-make/main.toml" }, + { path = "../cargo-make/trunk_server.toml" }, +] diff --git a/examples/spread/README.md b/examples/spread/README.md new file mode 100644 index 000000000..9a149558c --- /dev/null +++ b/examples/spread/README.md @@ -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. diff --git a/examples/spread/index.html b/examples/spread/index.html new file mode 100644 index 000000000..75fa1f12a --- /dev/null +++ b/examples/spread/index.html @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/examples/spread/public/favicon.ico b/examples/spread/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..2ba8527cb12f5f28f331b8d361eef560492d4c77 GIT binary patch literal 15406 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 k<4FtN!5 impl IntoView { + fn alert(msg: impl AsRef) { + 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 = + vec![EventHandlerFn::Click(Box::new(|_e: ev::MouseEvent| { + alert("event_handlers_only clicked"); + }))]; + + let combined: Vec = 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 = + vec![EventHandlerFn::Click(Box::new(|_e: ev::MouseEvent| { + alert("partial_event_handlers clicked"); + }))]; + + view! { +
+ "
" +
+ +
+ "
" +
+ +
+ "
" +
+ +
+ "
" +
+ + // 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. + //
+ // "with overwritten click handler" + //
+ } +} diff --git a/examples/spread/src/main.rs b/examples/spread/src/main.rs new file mode 100644 index 000000000..afde734c8 --- /dev/null +++ b/examples/spread/src/main.rs @@ -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! { + + } + }) +} diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index 833df76a5..15d027249 100644 --- a/leptos/src/lib.rs +++ b/leptos/src/lib.rs @@ -27,8 +27,6 @@ //! the code that Leptos generates. //! - [`counters`](https://github.com/leptos-rs/leptos/tree/main/examples/counters) introduces parent-child //! communication via contexts, and the `` 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 //! `Result` types to handle errors. //! - [`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 Leptos’s nested router //! 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. +//! - [`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 //! different methods of interaction with a stateful server, including server functions, server actions, forms, //! and server-sent events (SSE). @@ -162,9 +161,11 @@ pub use leptos_dom::{ set_interval_with_handle, set_timeout, set_timeout_with_handle, window_event_listener, window_event_listener_untyped, }, - html, math, mount_to, mount_to_body, nonce, svg, window, Attribute, Class, - CollectView, Errors, Fragment, HtmlElement, IntoAttribute, IntoClass, - IntoProperty, IntoStyle, IntoView, NodeRef, Property, View, + html, + html::Binding, + 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. pub mod logging { diff --git a/leptos_dom/src/events/typed.rs b/leptos_dom/src/events/typed.rs index ad512f35b..c981afbbf 100644 --- a/leptos_dom/src/events/typed.rs +++ b/leptos_dom/src/events/typed.rs @@ -167,6 +167,86 @@ impl DOMEventResponder for crate::View { } } +/// A statically typed event handler. +pub enum EventHandlerFn { + /// `keydown` event handler. + Keydown(Box), + /// `keyup` event handler. + Keyup(Box), + /// `keypress` event handler. + Keypress(Box), + + /// `click` event handler. + Click(Box), + /// `dblclick` event handler. + Dblclick(Box), + /// `mousedown` event handler. + Mousedown(Box), + /// `mouseup` event handler. + Mouseup(Box), + /// `mouseenter` event handler. + Mouseenter(Box), + /// `mouseleave` event handler. + Mouseleave(Box), + /// `mouseout` event handler. + Mouseout(Box), + /// `mouseover` event handler. + Mouseover(Box), + /// `mousemove` event handler. + Mousemove(Box), + + /// `wheel` event handler. + Wheel(Box), + + /// `touchstart` event handler. + Touchstart(Box), + /// `touchend` event handler. + Touchend(Box), + /// `touchcancel` event handler. + Touchcancel(Box), + /// `touchmove` event handler. + Touchmove(Box), + + /// `pointerenter` event handler. + Pointerenter(Box), + /// `pointerleave` event handler. + Pointerleave(Box), + /// `pointerdown` event handler. + Pointerdown(Box), + /// `pointerup` event handler. + Pointerup(Box), + /// `pointercancel` event handler. + Pointercancel(Box), + /// `pointerout` event handler. + Pointerout(Box), + /// `pointerover` event handler. + Pointerover(Box), + /// `pointermove` event handler. + Pointermove(Box), + + /// `drag` event handler. + Drag(Box), + /// `dragend` event handler. + Dragend(Box), + /// `dragenter` event handler. + Dragenter(Box), + /// `dragleave` event handler. + Dragleave(Box), + /// `dragstart` event handler. + Dragstart(Box), + /// `drop` event handler. + Drop(Box), + + /// `blur` event handler. + Blur(Box), + /// `focusout` event handler. + Focusout(Box), + /// `focus` event handler. + Focus(Box), + /// `focusin` event handler. + Focusin(Box), +} + /// Type that can be used to handle DOM events pub trait EventHandler { /// Attaches event listener to any target that can respond to DOM events diff --git a/leptos_dom/src/html.rs b/leptos_dom/src/html.rs index 1a65320b8..242d40c03 100644 --- a/leptos_dom/src/html.rs +++ b/leptos_dom/src/html.rs @@ -63,7 +63,7 @@ cfg_if! { use crate::{ create_node_ref, - ev::EventDescriptor, + ev::{EventDescriptor, EventHandlerFn}, hydration::HydrationCtx, macro_helpers::{ 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`), +/// can be spread onto an element using the spread syntax `view! {
}`. +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 for Binding { + fn from(handler: EventHandlerFn) -> Self { + Self::EventHandler(handler) + } +} + impl HtmlElement { pub(crate) fn new(element: El) -> Self { cfg_if! { @@ -651,7 +678,7 @@ impl HtmlElement { } } - /// Adds multiple attributes to the element + /// Adds multiple attributes to the element. #[track_caller] pub fn attrs( mut self, @@ -663,6 +690,133 @@ impl HtmlElement { self } + /// Adds multiple bindings (attributes or event handlers) to the element. + #[track_caller] + pub fn bindings>( + mut self, + bindings: impl std::iter::IntoIterator, + ) -> 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. /// /// **Note**: In the builder syntax, this will be overwritten by the `class` diff --git a/leptos_dom/src/lib.rs b/leptos_dom/src/lib.rs index 82360522b..20dcc3fca 100644 --- a/leptos_dom/src/lib.rs +++ b/leptos_dom/src/lib.rs @@ -36,7 +36,10 @@ pub use directive::*; pub use events::add_event_helper; #[cfg(all(target_arch = "wasm32", feature = "web"))] 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; use html::{AnyElement, ElementDescriptor}; pub use hydration::{HydrationCtx, HydrationKey}; diff --git a/leptos_macro/src/view/client_builder.rs b/leptos_macro/src/view/client_builder.rs index 15e0151c4..0f49e626f 100644 --- a/leptos_macro/src/view/client_builder.rs +++ b/leptos_macro/src/view/client_builder.rs @@ -223,7 +223,7 @@ pub(crate) fn element_to_tokens( None } }); - let spread_attrs = node.attributes().iter().filter_map(|node| { + let bindings = node.attributes().iter().filter_map(|node| { use rstml::node::NodeBlock; 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, } } else { @@ -356,7 +358,7 @@ pub(crate) fn element_to_tokens( #(#ide_helper_close_tag)* #name #(#attrs)* - #(#spread_attrs)* + #(#bindings)* #(#class_attrs)* #(#style_attrs)* #global_class_expr diff --git a/router/src/lib.rs b/router/src/lib.rs index b77f0e59f..c991ba57b 100644 --- a/router/src/lib.rs +++ b/router/src/lib.rs @@ -28,7 +28,6 @@ //! ## Example //! //! ```rust -//! //! use leptos::*; //! use leptos_router::*; //! diff --git a/rustfmt.toml b/rustfmt.toml index 384bc5c30..1eb778a6a 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,6 +1,9 @@ +# Stable options edition = "2021" -imports_granularity = "Crate" max_width = 80 + +# Unstable options +imports_granularity = "Crate" format_strings = true group_imports = "One" format_code_in_doc_comments = true