full attribute spreading

This commit is contained in:
Greg Johnston 2024-05-04 09:56:20 -04:00
parent 9ec30d71d2
commit e93a34a2c9
17 changed files with 395 additions and 71 deletions

View File

@ -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! {
<div>
<label>
@ -79,7 +82,7 @@ pub fn fetch_example() -> impl IntoView {
/>
</label>
<Transition fallback=|| view! { <div>"Loading..."</div> }>
<Transition fallback=|| view! { <div>"Loading..."</div> } {..spreadable}>
<ErrorBoundary fallback>
<ul>
{move || Suspend(async move {

View File

@ -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
// <ButtonC on:click=move |_| set_italics.update(|value| *value = !*value)/>
<ButtonC on:click=move |_| set_italics.update(|value| *value = !*value)/>
// Button D gets its setter from context rather than props
<ButtonD/>

View File

@ -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<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| {
// 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<EventHandlerFn> =
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! {
<div {..attrs_only}>
"<div {..attrs_only} />"
</div>
<p>
"You can spread any valid attribute, including a tuple of attributes, with the {..attr} syntax"
</p>
<div {..attrs_only.clone()}>"<div {..attrs_only} />"</div>
<div {..event_handlers_only}>
"<div {..event_handlers_only} />"
</div>
<div {..event_handlers_only.clone()}>"<div {..event_handlers_only} />"</div>
<div {..combined}>
"<div {..combined} />"
</div>
<div {..combined.clone()}>"<div {..combined} />"</div>
<div {..partial_attrs} {..partial_event_handlers}>
<div {..partial_attrs.clone()} {..partial_event_handlers.clone()}>
"<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>
<hr/>
<p>
"The .. is not required to spread; you can pass any valid attribute in a block by itself."
</p>
<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>
}
// 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.
//<div {..mixed} on:click=|_e| { alert("I will never be seen..."); }>
// "with overwritten click handler"
//</div>
}

View File

@ -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! {
<SpreadingExample/>
}
})
mount::mount_to_body(SpreadingExample)
}

View File

@ -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<Chil, Fal, Rndr> AddAnyAttr<Rndr> for ErrorBoundaryView<Chil, Fal, Rndr>
where
Chil: RenderHtml<Rndr>,
Fal: RenderHtml<Rndr> + Send,
Rndr: Renderer,
{
type Output<SomeNewAttr: Attribute<Rndr>> =
ErrorBoundaryView<Chil::Output<SomeNewAttr>, Fal, Rndr>;
fn add_any_attr<NewAttr: Attribute<Rndr>>(
self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml<Rndr>,
{
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<Chil, Fal, Rndr> RenderHtml<Rndr> for ErrorBoundaryView<Chil, Fal, Rndr>
where
Chil: RenderHtml<Rndr>,

View File

@ -55,7 +55,7 @@ pub fn For<Rndr, IF, I, T, EF, N, KF, K>(
) -> impl IntoView
where
IF: Fn() -> I + Send + 'static,
I: IntoIterator<Item = T> + Send,
I: IntoIterator<Item = T> + Send + 'static,
EF: Fn(T) -> N + Send + Clone + 'static,
N: IntoView + 'static,
KF: Fn(&T) -> K + Send + Clone + 'static,

View File

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

View File

@ -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<const TRANSITION: bool, Fal, Chil> {
impl<const TRANSITION: bool, Fal, Chil, Rndr> Render<Rndr>
for SuspenseBoundary<TRANSITION, Fal, Chil>
where
Fal: Render<Rndr> + 'static,
Chil: Render<Rndr> + 'static,
Fal: Render<Rndr> + Send + 'static,
Chil: Render<Rndr> + Send + 'static,
Rndr: Renderer + 'static,
{
type State =
@ -100,6 +103,40 @@ where
fn rebuild(self, _state: &mut Self::State) {}
}
impl<const TRANSITION: bool, Fal, Chil, Rndr> AddAnyAttr<Rndr>
for SuspenseBoundary<TRANSITION, Fal, Chil>
where
Fal: RenderHtml<Rndr> + Send + 'static,
Chil: RenderHtml<Rndr> + Send + 'static,
Rndr: Renderer + 'static,
{
type Output<SomeNewAttr: Attribute<Rndr>> = SuspenseBoundary<
TRANSITION,
Fal,
Chil::Output<SomeNewAttr::CloneableOwned>,
>;
fn add_any_attr<NewAttr: Attribute<Rndr>>(
self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml<Rndr>,
{
let attr = attr.into_cloneable_owned();
let SuspenseBoundary {
none_pending,
fallback,
children,
} = self;
SuspenseBoundary {
none_pending,
fallback,
children: children.add_any_attr(attr),
}
}
}
impl<const TRANSITION: bool, Fal, Chil, Rndr> RenderHtml<Rndr>
for SuspenseBoundary<TRANSITION, Fal, Chil>
where
@ -341,6 +378,39 @@ where
}
}
impl<Fut, Rndr> AddAnyAttr<Rndr> for Suspend<Fut>
where
Fut: Future + Send + 'static,
Fut::Output: AddAnyAttr<Rndr>,
Rndr: Renderer + 'static,
{
type Output<SomeNewAttr: Attribute<Rndr>> = Suspend<
Pin<
Box<
dyn Future<
Output = <Fut::Output as AddAnyAttr<Rndr>>::Output<
SomeNewAttr::CloneableOwned,
>,
> + Send,
>,
>,
>;
fn add_any_attr<NewAttr: Attribute<Rndr>>(
self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml<Rndr>,
{
let attr = attr.into_cloneable_owned();
Suspend(Box::pin(async move {
let this = self.0.await;
this.add_any_attr(attr)
}))
}
}
impl<Fut, Rndr> RenderHtml<Rndr> for Suspend<Fut>
where
Fut: Future + Send + 'static,

View File

@ -102,6 +102,37 @@ pub(crate) fn component_to_tokens(
})
.collect::<Vec<_>>();
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)*
}
};

View File

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

View File

@ -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},
};
}

View File

@ -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<R> IntoClass<R> 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
}
}

View File

@ -376,6 +376,8 @@ mod stable {
R: DomRenderer,
{
type State = RenderEffectState<C::State>;
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<R> IntoClass<R> for (&'static str, $sig<bool>)
@ -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<C::State>;
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<R> IntoClass<R> for (&'static str, $sig<bool>)
@ -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
}
}
};
}

View File

@ -90,6 +90,8 @@ mod stable {
R: DomRenderer,
{
type State = RenderEffect<V::State>;
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<V::State>;
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
}
}
};
}

View File

@ -147,7 +147,7 @@ where
impl<F, V, R> RenderHtml<R> for F
where
F: ReactiveFunction<Output = V>,
V: RenderHtml<R>,
V: RenderHtml<R> + 'static,
V::State: 'static,
R: Renderer + 'static,
@ -203,22 +203,21 @@ where
impl<F, V, R> AddAnyAttr<R> for F
where
F: ReactiveFunction<Output = V>,
V: AddAnyAttr<R>,
V: RenderHtml<R> + 'static,
R: Renderer + 'static,
{
type Output<SomeNewAttr: Attribute<R>> =
SharedReactiveFunction<V::Output<SomeNewAttr>>;
Box<dyn FnMut() -> V::Output<SomeNewAttr::CloneableOwned> + Send>;
fn add_any_attr<NewAttr: Attribute<R>>(
self,
mut self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml<R>,
{
/*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<V, R> AddAnyAttr<R> for $sig<V>
where
V: RenderHtml<R> + Clone + Send + Sync + 'static,
V::State: 'static,
R: Renderer + 'static,
{
type Output<SomeNewAttr: Attribute<R>> = $sig<V>;
fn add_any_attr<NewAttr: Attribute<R>>(
mut self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml<R>,
{
todo!()
}
}
impl<V, R> RenderHtml<R> for $sig<V>
where
V: RenderHtml<R> + Clone + Send + Sync + 'static,
@ -496,6 +516,8 @@ mod stable {
R: Renderer,
{
type State = RenderEffectState<V::State>;
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<V, R> Render<R> for $sig<V>
where
V: Render<R> + Clone + 'static,
V: Render<R> + Send + Sync + Clone + 'static,
V::State: 'static,
R: Renderer,
@ -553,6 +583,25 @@ mod stable {
}
}
impl<V, R> AddAnyAttr<R> for $sig<V>
where
V: RenderHtml<R> + Clone + Send + Sync + 'static,
V::State: 'static,
R: Renderer + 'static,
{
type Output<SomeNewAttr: Attribute<R>> = $sig<V>;
fn add_any_attr<NewAttr: Attribute<R>>(
mut self,
attr: NewAttr,
) -> Self::Output<NewAttr>
where
Self::Output<NewAttr>: RenderHtml<R>,
{
todo!()
}
}
impl<V, R> RenderHtml<R> for $sig<V>
where
V: RenderHtml<R> + Clone + Send + Sync + 'static,
@ -609,6 +658,8 @@ mod stable {
R: Renderer,
{
type State = RenderEffectState<V::State>;
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
}
}
};
}

View File

@ -93,6 +93,8 @@ mod stable {
R: DomRenderer,
{
type State = RenderEffect<V::State>;
type Cloneable = Self;
type CloneableOwned = Self;
fn hydrate<const FROM_SERVER: bool>(
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<V, R> IntoProperty<R> for $sig<V>
where
V: IntoProperty<R> + Clone + 'static,
V: IntoProperty<R> + Send + Sync + Clone + 'static,
V::State: 'static,
R: DomRenderer,
{
type State = RenderEffect<V::State>;
type Cloneable = Self;
type CloneableOwned = Self;
fn hydrate<const FROM_SERVER: bool>(
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
}
}
};
}

View File

@ -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<R: Renderer>: Sized {
/// The “view state” for this type, which can be retained between updates.
///