From e93a34a2c9e219d963ae70ad711fe511e72ce5a7 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Sat, 4 May 2024 09:56:20 -0400 Subject: [PATCH] full attribute spreading --- examples/fetch/src/lib.rs | 5 +- examples/parent_child/src/lib.rs | 3 +- examples/spread/src/lib.rs | 86 ++++++++++++---------- examples/spread/src/main.rs | 6 +- leptos/src/error_boundary.rs | 41 ++++++++++- leptos/src/for_loop.rs | 2 +- leptos/src/lib.rs | 2 + leptos/src/suspense_component.rs | 74 ++++++++++++++++++- leptos_macro/src/view/component_builder.rs | 32 ++++++++ leptos_macro/src/view/mod.rs | 32 +++++++- tachys/src/lib.rs | 2 +- tachys/src/oco.rs | 14 ++++ tachys/src/reactive_graph/class.rs | 40 ++++++++++ tachys/src/reactive_graph/inner_html.rs | 20 +++++ tachys/src/reactive_graph/mod.rs | 79 +++++++++++++++++--- tachys/src/reactive_graph/property.rs | 22 +++++- tachys/src/view/mod.rs | 6 -- 17 files changed, 395 insertions(+), 71 deletions(-) diff --git a/examples/fetch/src/lib.rs b/examples/fetch/src/lib.rs index c23243bcc..79aa90efa 100644 --- a/examples/fetch/src/lib.rs +++ b/examples/fetch/src/lib.rs @@ -1,4 +1,5 @@ use leptos::prelude::*; +use leptos::tachys::html::style::style; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -65,6 +66,8 @@ pub fn fetch_example() -> impl IntoView { } }; + let spreadable = style(("background-color", "AliceBlue")); + view! {
- "Loading..."
}> + "Loading..." } {..spreadable}>
    {move || Suspend(async move { diff --git a/examples/parent_child/src/lib.rs b/examples/parent_child/src/lib.rs index a90c93ce6..26ce4ecf4 100644 --- a/examples/parent_child/src/lib.rs +++ b/examples/parent_child/src/lib.rs @@ -47,8 +47,7 @@ pub fn App() -> impl IntoView { // Button C: use a regular event listener // setting an event listener on a component like this applies it // to each of the top-level elements the component returns - // TODO WIP - // + // Button D gets its setter from context rather than props diff --git a/examples/spread/src/lib.rs b/examples/spread/src/lib.rs index bdf9f42b9..65a4519c4 100644 --- a/examples/spread/src/lib.rs +++ b/examples/spread/src/lib.rs @@ -1,4 +1,10 @@ -use leptos::*; +use leptos::{ + attr::id, + ev::{self, on}, + prelude::*, + // TODO clean up import here + tachys::html::class::class, +}; /// Demonstrates how attributes and event handlers can be spread onto elements. #[component] @@ -7,51 +13,55 @@ pub fn SpreadingExample() -> impl IntoView { 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| { + // TODO support data- attributes better + let attrs_only = class("foo"); + let event_handlers_only = on(ev::click, move |_e: ev::MouseEvent| { + alert("event_handlers_only clicked"); + }); + let combined = ( + class("bar"), + on(ev::click, move |_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"); - }))]; + }), + ); + let partial_attrs = (id("snood"), class("baz")); + let partial_event_handlers = on(ev::click, move |_e: ev::MouseEvent| { + alert("partial_event_handlers clicked"); + }); view! { -
    - "
    " -
    +

    + "You can spread any valid attribute, including a tuple of attributes, with the {..attr} syntax" +

    +
    "
    "
    -
    - "
    " -
    +
    "
    "
    -
    - "
    " -
    +
    "
    "
    -
    +
    "
    "
    - // 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" - //
    +
    + +

    + "The .. is not required to spread; you can pass any valid attribute in a block by itself." +

    +
    "
    "
    + +
    "
    "
    + +
    "
    "
    + +
    + "
    " +
    } + // TODO check below + // 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 index afde734c8..9bd4aadf1 100644 --- a/examples/spread/src/main.rs +++ b/examples/spread/src/main.rs @@ -4,9 +4,5 @@ use spread::SpreadingExample; pub fn main() { _ = console_log::init_with_level(log::Level::Debug); console_error_panic_hook::set_once(); - mount_to_body(|| { - view! { - - } - }) + mount::mount_to_body(SpreadingExample) } diff --git a/leptos/src/error_boundary.rs b/leptos/src/error_boundary.rs index 7984326cb..7e521e5ff 100644 --- a/leptos/src/error_boundary.rs +++ b/leptos/src/error_boundary.rs @@ -10,10 +10,14 @@ use reactive_graph::{ use rustc_hash::FxHashMap; use std::{marker::PhantomData, sync::Arc}; use tachys::{ + html::attribute::Attribute, hydration::Cursor, renderer::{CastFrom, Renderer}, ssr::StreamBuilder, - view::{Mountable, Position, PositionState, Render, RenderHtml}, + view::{ + add_attr::AddAnyAttr, Mountable, Position, PositionState, Render, + RenderHtml, + }, }; use throw_error::{Error, ErrorHook, ErrorId}; @@ -193,6 +197,41 @@ where } } +impl AddAnyAttr for ErrorBoundaryView +where + Chil: RenderHtml, + Fal: RenderHtml + Send, + Rndr: Renderer, +{ + type Output> = + ErrorBoundaryView, Fal, Rndr>; + + fn add_any_attr>( + self, + attr: NewAttr, + ) -> Self::Output + where + Self::Output: RenderHtml, + { + let ErrorBoundaryView { + boundary_id, + errors_empty, + children, + fallback, + errors, + rndr, + } = self; + ErrorBoundaryView { + boundary_id, + errors_empty, + children: children.add_any_attr(attr), + fallback, + errors, + rndr, + } + } +} + impl RenderHtml for ErrorBoundaryView where Chil: RenderHtml, diff --git a/leptos/src/for_loop.rs b/leptos/src/for_loop.rs index c02ac35f2..049eca1f7 100644 --- a/leptos/src/for_loop.rs +++ b/leptos/src/for_loop.rs @@ -55,7 +55,7 @@ pub fn For( ) -> impl IntoView where IF: Fn() -> I + Send + 'static, - I: IntoIterator + Send, + I: IntoIterator + Send + 'static, EF: Fn(T) -> N + Send + Clone + 'static, N: IntoView + 'static, KF: Fn(&T) -> K + Send + Clone + 'static, diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index 1fd994b26..ea8317d17 100644 --- a/leptos/src/lib.rs +++ b/leptos/src/lib.rs @@ -249,6 +249,8 @@ pub mod context { pub use leptos_server as server; /// HTML element types. pub use tachys::html::element as html; +/// HTML attribute types. +pub use tachys::html::attribute as attr; /// HTML event types. #[doc(no_inline)] pub use tachys::html::event as ev; diff --git a/leptos/src/suspense_component.rs b/leptos/src/suspense_component.rs index a9db10034..a49286206 100644 --- a/leptos/src/suspense_component.rs +++ b/leptos/src/suspense_component.rs @@ -17,15 +17,18 @@ use std::{ cell::RefCell, fmt::Debug, future::{ready, Future, Ready}, + pin::Pin, rc::Rc, }; use tachys::{ either::Either, + html::attribute::Attribute, hydration::Cursor, reactive_graph::RenderEffectState, renderer::{dom::Dom, Renderer}, ssr::StreamBuilder, view::{ + add_attr::AddAnyAttr, any_view::AnyView, either::{EitherKeepAlive, EitherKeepAliveState}, iterators::OptionState, @@ -67,8 +70,8 @@ pub(crate) struct SuspenseBoundary { impl Render for SuspenseBoundary where - Fal: Render + 'static, - Chil: Render + 'static, + Fal: Render + Send + 'static, + Chil: Render + Send + 'static, Rndr: Renderer + 'static, { type State = @@ -100,6 +103,40 @@ where fn rebuild(self, _state: &mut Self::State) {} } +impl AddAnyAttr + for SuspenseBoundary +where + Fal: RenderHtml + Send + 'static, + Chil: RenderHtml + Send + 'static, + Rndr: Renderer + 'static, +{ + type Output> = SuspenseBoundary< + TRANSITION, + Fal, + Chil::Output, + >; + + fn add_any_attr>( + self, + attr: NewAttr, + ) -> Self::Output + where + Self::Output: RenderHtml, + { + let attr = attr.into_cloneable_owned(); + let SuspenseBoundary { + none_pending, + fallback, + children, + } = self; + SuspenseBoundary { + none_pending, + fallback, + children: children.add_any_attr(attr), + } + } +} + impl RenderHtml for SuspenseBoundary where @@ -341,6 +378,39 @@ where } } +impl AddAnyAttr for Suspend +where + Fut: Future + Send + 'static, + Fut::Output: AddAnyAttr, + Rndr: Renderer + 'static, +{ + type Output> = Suspend< + Pin< + Box< + dyn Future< + Output = >::Output< + SomeNewAttr::CloneableOwned, + >, + > + Send, + >, + >, + >; + + fn add_any_attr>( + self, + attr: NewAttr, + ) -> Self::Output + where + Self::Output: RenderHtml, + { + let attr = attr.into_cloneable_owned(); + Suspend(Box::pin(async move { + let this = self.0.await; + this.add_any_attr(attr) + })) + } +} + impl RenderHtml for Suspend where Fut: Future + Send + 'static, diff --git a/leptos_macro/src/view/component_builder.rs b/leptos_macro/src/view/component_builder.rs index b6fcda331..d8571994b 100644 --- a/leptos_macro/src/view/component_builder.rs +++ b/leptos_macro/src/view/component_builder.rs @@ -102,6 +102,37 @@ pub(crate) fn component_to_tokens( }) .collect::>(); + let spreads = node.attributes().iter().filter_map(|attr| { + use rstml::node::NodeBlock; + use syn::{Expr, ExprRange, RangeLimits, Stmt}; + + if let NodeAttribute::Block(block) = attr { + let dotted = if let NodeBlock::ValidBlock(block) = block { + match block.stmts.first() { + Some(Stmt::Expr( + Expr::Range(ExprRange { + start: None, + limits: RangeLimits::HalfOpen(_), + end: Some(end), + .. + }), + _, + )) => Some(quote! { .add_any_attr(#end) }), + _ => None, + } + } else { + None + }; + Some(dotted.unwrap_or_else(|| { + quote! { + .add_any_attr(#[allow(unused_braces)] { #node }) + } + })) + } else { + None + } + }); + /*let directives = attrs .clone() .filter_map(|attr| { @@ -244,6 +275,7 @@ pub(crate) fn component_to_tokens( #name_ref, props ) + #(#spreads)* } }; diff --git a/leptos_macro/src/view/mod.rs b/leptos_macro/src/view/mod.rs index 80c2a32b4..53976de26 100644 --- a/leptos_macro/src/view/mod.rs +++ b/leptos_macro/src/view/mod.rs @@ -6,9 +6,13 @@ use leptos_hot_reload::parsing::is_component_node; use proc_macro2::{Ident, Span, TokenStream, TokenTree}; use proc_macro_error::abort; use quote::{quote, quote_spanned, ToTokens}; -use rstml::node::{KeyedAttribute, Node, NodeAttribute, NodeElement, NodeName}; +use rstml::node::{ + KeyedAttribute, Node, NodeAttribute, NodeBlock, NodeElement, NodeName, +}; use std::collections::HashMap; -use syn::{spanned::Spanned, Expr, ExprPath, Lit, LitStr}; +use syn::{ + spanned::Spanned, Expr, ExprPath, ExprRange, Lit, LitStr, RangeLimits, Stmt, +}; #[derive(Clone, Copy, PartialEq, Eq)] pub(crate) enum TagType { @@ -300,7 +304,29 @@ fn attribute_to_tokens( global_class: Option<&TokenTree>, ) -> TokenStream { match node { - NodeAttribute::Block(_) => todo!(), + NodeAttribute::Block(node) => { + let dotted = if let NodeBlock::ValidBlock(block) = node { + match block.stmts.first() { + Some(Stmt::Expr( + Expr::Range(ExprRange { + start: None, + limits: RangeLimits::HalfOpen(_), + end: Some(end), + .. + }), + _, + )) => Some(quote! { .add_any_attr(#end) }), + _ => None, + } + } else { + None + }; + dotted.unwrap_or_else(|| { + quote! { + .add_any_attr(#[allow(unused_braces)] { #node }) + } + }) + } NodeAttribute::Attribute(node) => { let name = node.key.to_string(); if name == "node_ref" { diff --git a/tachys/src/lib.rs b/tachys/src/lib.rs index d9aab3995..3c708ee65 100644 --- a/tachys/src/lib.rs +++ b/tachys/src/lib.rs @@ -17,7 +17,7 @@ pub mod prelude { node_ref::NodeRefAttribute, }, renderer::{dom::Dom, Renderer}, - view::{Mountable, Render, RenderHtml}, + view::{add_attr::AddAnyAttr, Mountable, Render, RenderHtml}, }; } diff --git a/tachys/src/oco.rs b/tachys/src/oco.rs index cd5a57b49..b2b2e97e8 100644 --- a/tachys/src/oco.rs +++ b/tachys/src/oco.rs @@ -109,6 +109,7 @@ where { type State = (R::Element, Oco<'static, str>); type Cloneable = Self; + type CloneableOwned = Self; fn html_len(&self) -> usize { self.as_str().len() @@ -151,6 +152,12 @@ where self.upgrade_inplace(); self } + + fn into_cloneable_owned(mut self) -> Self::CloneableOwned { + // ensure it's reference-counted + self.upgrade_inplace(); + self + } } impl IntoClass for Oco<'static, str> @@ -159,6 +166,7 @@ where { type State = (R::Element, Self); type Cloneable = Self; + type CloneableOwned = Self; fn html_len(&self) -> usize { self.as_str().len() @@ -193,4 +201,10 @@ where self.upgrade_inplace(); self } + + fn into_cloneable_owned(mut self) -> Self::CloneableOwned { + // ensure it's reference-counted + self.upgrade_inplace(); + self + } } diff --git a/tachys/src/reactive_graph/class.rs b/tachys/src/reactive_graph/class.rs index 660b51149..05abe89c0 100644 --- a/tachys/src/reactive_graph/class.rs +++ b/tachys/src/reactive_graph/class.rs @@ -376,6 +376,8 @@ mod stable { R: DomRenderer, { type State = RenderEffectState; + type Cloneable = Self; + type CloneableOwned = Self; fn html_len(&self) -> usize { 0 @@ -400,6 +402,14 @@ mod stable { fn rebuild(self, _state: &mut Self::State) { // TODO rebuild here? } + + fn into_cloneable(self) -> Self::Cloneable { + self + } + + fn into_cloneable_owned(self) -> Self::CloneableOwned { + self + } } impl IntoClass for (&'static str, $sig) @@ -407,6 +417,8 @@ mod stable { R: DomRenderer, { type State = RenderEffectState<(R::ClassList, bool)>; + type Cloneable = Self; + type CloneableOwned = Self; fn html_len(&self) -> usize { self.0.len() @@ -437,6 +449,14 @@ mod stable { fn rebuild(self, _state: &mut Self::State) { // TODO rebuild here? } + + fn into_cloneable(self) -> Self::Cloneable { + self + } + + fn into_cloneable_owned(self) -> Self::CloneableOwned { + self + } } }; } @@ -450,6 +470,8 @@ mod stable { R: DomRenderer, { type State = RenderEffectState; + type Cloneable = Self; + type CloneableOwned = Self; fn html_len(&self) -> usize { 0 @@ -474,6 +496,14 @@ mod stable { fn rebuild(self, _state: &mut Self::State) { // TODO rebuild here? } + + fn into_cloneable(self) -> Self::Cloneable { + self + } + + fn into_cloneable_owned(self) -> Self::CloneableOwned { + self + } } impl IntoClass for (&'static str, $sig) @@ -481,6 +511,8 @@ mod stable { R: DomRenderer, { type State = RenderEffectState<(R::ClassList, bool)>; + type Cloneable = Self; + type CloneableOwned = Self; fn html_len(&self) -> usize { self.0.len() @@ -511,6 +543,14 @@ mod stable { fn rebuild(self, _state: &mut Self::State) { // TODO rebuild here? } + + fn into_cloneable(self) -> Self::Cloneable { + self + } + + fn into_cloneable_owned(self) -> Self::CloneableOwned { + self + } } }; } diff --git a/tachys/src/reactive_graph/inner_html.rs b/tachys/src/reactive_graph/inner_html.rs index 6a55b2925..6a604a3ac 100644 --- a/tachys/src/reactive_graph/inner_html.rs +++ b/tachys/src/reactive_graph/inner_html.rs @@ -90,6 +90,8 @@ mod stable { R: DomRenderer, { type State = RenderEffect; + type Cloneable = Self; + type CloneableOwned = Self; fn html_len(&self) -> usize { 0 @@ -114,6 +116,14 @@ mod stable { } fn rebuild(self, _state: &mut Self::State) {} + + fn into_cloneable(self) -> Self::Cloneable { + self + } + + fn into_cloneable_owned(self) -> Self::CloneableOwned { + self + } } }; } @@ -127,6 +137,8 @@ mod stable { R: DomRenderer, { type State = RenderEffect; + type Cloneable = Self; + type CloneableOwned = Self; fn html_len(&self) -> usize { 0 @@ -151,6 +163,14 @@ mod stable { } fn rebuild(self, _state: &mut Self::State) {} + + fn into_cloneable(self) -> Self::Cloneable { + self + } + + fn into_cloneable_owned(self) -> Self::CloneableOwned { + self + } } }; } diff --git a/tachys/src/reactive_graph/mod.rs b/tachys/src/reactive_graph/mod.rs index cc6019219..bc983b72d 100644 --- a/tachys/src/reactive_graph/mod.rs +++ b/tachys/src/reactive_graph/mod.rs @@ -147,7 +147,7 @@ where impl RenderHtml for F where F: ReactiveFunction, - V: RenderHtml, + V: RenderHtml + 'static, V::State: 'static, R: Renderer + 'static, @@ -203,22 +203,21 @@ where impl AddAnyAttr for F where F: ReactiveFunction, - V: AddAnyAttr, + V: RenderHtml + 'static, R: Renderer + 'static, { type Output> = - SharedReactiveFunction>; + Box V::Output + Send>; fn add_any_attr>( - self, + mut self, attr: NewAttr, ) -> Self::Output where Self::Output: RenderHtml, { - /*let attr = attr.into_cloneable_owned(); - Arc::new(Mutex::new(move || self.invoke().add_any_attr(attr.clone())));*/ - todo!() + let attr = attr.into_cloneable_owned(); + Box::new(move || self.invoke().add_any_attr(attr.clone())) } } @@ -405,11 +404,13 @@ where mod stable { use super::RenderEffectState; use crate::{ - html::attribute::AttributeValue, + html::attribute::{Attribute, AttributeValue}, hydration::Cursor, renderer::Renderer, ssr::StreamBuilder, - view::{Position, PositionState, Render, RenderHtml}, + view::{ + add_attr::AddAnyAttr, Position, PositionState, Render, RenderHtml, + }, }; use reactive_graph::{ computed::{ArcMemo, Memo}, @@ -440,6 +441,25 @@ mod stable { } } + impl AddAnyAttr for $sig + where + V: RenderHtml + Clone + Send + Sync + 'static, + V::State: 'static, + R: Renderer + 'static, + { + type Output> = $sig; + + fn add_any_attr>( + mut self, + attr: NewAttr, + ) -> Self::Output + where + Self::Output: RenderHtml, + { + todo!() + } + } + impl RenderHtml for $sig where V: RenderHtml + Clone + Send + Sync + 'static, @@ -496,6 +516,8 @@ mod stable { R: Renderer, { type State = RenderEffectState; + type Cloneable = Self; + type CloneableOwned = Self; fn html_len(&self) -> usize { 0 @@ -527,6 +549,14 @@ mod stable { fn rebuild(self, _key: &str, _state: &mut Self::State) { // TODO rebuild } + + fn into_cloneable(self) -> Self::Cloneable { + self + } + + fn into_cloneable_owned(self) -> Self::CloneableOwned { + self + } } }; } @@ -535,7 +565,7 @@ mod stable { ($sig:ident) => { impl Render for $sig where - V: Render + Clone + 'static, + V: Render + Send + Sync + Clone + 'static, V::State: 'static, R: Renderer, @@ -553,6 +583,25 @@ mod stable { } } + impl AddAnyAttr for $sig + where + V: RenderHtml + Clone + Send + Sync + 'static, + V::State: 'static, + R: Renderer + 'static, + { + type Output> = $sig; + + fn add_any_attr>( + mut self, + attr: NewAttr, + ) -> Self::Output + where + Self::Output: RenderHtml, + { + todo!() + } + } + impl RenderHtml for $sig where V: RenderHtml + Clone + Send + Sync + 'static, @@ -609,6 +658,8 @@ mod stable { R: Renderer, { type State = RenderEffectState; + type Cloneable = Self; + type CloneableOwned = Self; fn html_len(&self) -> usize { 0 @@ -640,6 +691,14 @@ mod stable { fn rebuild(self, _key: &str, _state: &mut Self::State) { // TODO rebuild } + + fn into_cloneable(self) -> Self::Cloneable { + self + } + + fn into_cloneable_owned(self) -> Self::CloneableOwned { + self + } } }; } diff --git a/tachys/src/reactive_graph/property.rs b/tachys/src/reactive_graph/property.rs index dc3b13f8e..bca87398c 100644 --- a/tachys/src/reactive_graph/property.rs +++ b/tachys/src/reactive_graph/property.rs @@ -93,6 +93,8 @@ mod stable { R: DomRenderer, { type State = RenderEffect; + type Cloneable = Self; + type CloneableOwned = Self; fn hydrate( self, @@ -113,6 +115,14 @@ mod stable { fn rebuild(self, _state: &mut Self::State, _key: &str) { // TODO rebuild } + + fn into_cloneable(self) -> Self::Cloneable { + self + } + + fn into_cloneable_owned(self) -> Self::CloneableOwned { + self + } } }; } @@ -121,11 +131,13 @@ mod stable { ($sig:ident) => { impl IntoProperty for $sig where - V: IntoProperty + Clone + 'static, + V: IntoProperty + Send + Sync + Clone + 'static, V::State: 'static, R: DomRenderer, { type State = RenderEffect; + type Cloneable = Self; + type CloneableOwned = Self; fn hydrate( self, @@ -146,6 +158,14 @@ mod stable { fn rebuild(self, _state: &mut Self::State, _key: &str) { // TODO rebuild } + + fn into_cloneable(self) -> Self::Cloneable { + self + } + + fn into_cloneable_owned(self) -> Self::CloneableOwned { + self + } } }; } diff --git a/tachys/src/view/mod.rs b/tachys/src/view/mod.rs index 6548ed732..ba6b559cc 100644 --- a/tachys/src/view/mod.rs +++ b/tachys/src/view/mod.rs @@ -20,12 +20,6 @@ pub mod tuples; /// /// It is generic over the renderer itself, as long as that implements the [`Renderer`] /// trait. -#[diagnostic::on_unimplemented( - message = "`Render<{R}>` is not implemented for `{Self}`", - label = "My Label", - note = "Note 1", - note = "Note 2" -)] pub trait Render: Sized { /// The “view state” for this type, which can be retained between updates. ///