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
uses: actions/checkout@v4
- name: Sember Checks
- name: Semver Checks
uses: obi1kenobi/cargo-semver-checks-action@v2
with:
rust-toolchain: nightly-2024-03-31

View file

@ -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`

View file

@ -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 Leptoss 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.

View file

@ -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",

View file

@ -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 (<http://127.0.0.1:8080> or <http://127.0.0.1:3000> 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 (<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:
- 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

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.
//! - [`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.
//! - [`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 Leptoss 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 {

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
pub trait EventHandler {
/// Attaches event listener to any target that can respond to DOM events

View file

@ -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<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> {
pub(crate) fn new(element: El) -> Self {
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]
pub fn attrs(
mut self,
@ -663,6 +690,133 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
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.
///
/// **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;
#[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};

View file

@ -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

View file

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

View file

@ -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