feat: enhanced spreading syntax

This commit is contained in:
Greg Johnston 2024-05-27 15:19:57 -04:00
parent 747d847183
commit f6c7ac473a
3 changed files with 248 additions and 104 deletions

View file

@ -1,10 +1,4 @@
use leptos::{
attr::id,
ev::{self, on},
prelude::*,
// TODO clean up import here
tachys::html::class::class,
};
use leptos::prelude::*;
/// Demonstrates how attributes and event handlers can be spread onto elements.
#[component]
@ -13,21 +7,37 @@ pub fn SpreadingExample() -> impl IntoView {
let _ = window().alert_with_message(msg.as_ref());
}
// TODO support data- attributes better
let attrs_only = class("foo");
let event_handlers_only = on(ev::click, move |_e: ev::MouseEvent| {
// you can easily create sets of spreadable attributes by using the <{..} ___/> syntax
// this is expanded to a tuple of attributes; it has no meaning on its own, but can be spread
// onto an HTML element or component
let attrs_only = view! { <{..} class="foo"/> };
let event_handlers_only = view! { <{..} on:click=move |_| {
alert("event_handlers_only clicked");
});
let combined = (
class("bar"),
on(ev::click, move |_e: ev::MouseEvent| {
alert("combined clicked");
}),
);
let partial_attrs = (id("snood"), class("baz"));
let partial_event_handlers = on(ev::click, move |_e: ev::MouseEvent| {
alert("partial_event_handlers clicked");
});
}/> };
let combined = view! { <{..} class="bar" on:click=move |_| alert("combined clicked") /> };
let partial_attrs =
view! { <{..} id="snood" class="baz" data-foo="bar" /> };
let partial_event_handlers = view! { <{..} on:click=move |_| alert("partial_event_handlers clicked") /> };
let spread_onto_component = view! {
<{..} aria-label="a component with attribute spreading"/>
};
/* with the correct imports, you can use a tuple/builder syntax as well
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");
}),
);
let partial_attrs = (id("snood"), class("baz"));
let partial_event_handlers = on(ev::click, move |_e: ev::MouseEvent| {
alert("partial_event_handlers clicked");
});
*/
view! {
<p>
@ -60,19 +70,23 @@ pub fn SpreadingExample() -> impl IntoView {
<hr/>
// the class:, style:, prop:, and on: syntaxes can be used with components just as they are
// used with elements
//
// to add an HTML attribute that is not a component prop, use attr:
//
// these attributes will be applied to *all* elements returned as part of the component's
// view. to apply attributes to a subset of the component, pass them via a component prop
// attributes that are spread onto a component will be applied to *all* elements returned as part of
// the component's view. to apply attributes to a subset of the component, pass them via a component prop
<ComponentThatTakesSpread
attr:id="foo"
// the class:, style:, prop:, on: syntaxes work just as they do on elements
class:foo=true
style:font-weight="bold"
prop:cool=42
on:click=move |_| alert("clicked ComponentThatTakesSpread")
// props are passed as they usually are on components
some_prop=13
// to pass a plain HTML attribute, prefix it with attr:
attr:id="foo"
// or, if you want to include multiple attributes, rather than prefixing each with
// attr:, you can separate them from component props with the spread {..}
{..} // everything after this is treated as an HTML attribute
title="ooh, a title!"
{..spread_onto_component}
/>
}
// TODO check below
@ -84,7 +98,8 @@ pub fn SpreadingExample() -> impl IntoView {
}
#[component]
pub fn ComponentThatTakesSpread() -> impl IntoView {
pub fn ComponentThatTakesSpread(some_prop: i32) -> impl IntoView {
leptos::logging::log!("some_prop = {some_prop}");
view! {
<button>"<ComponentThatTakesSpread/>"</button>
<p>

View file

@ -1,10 +1,15 @@
use super::{event_to_tokens, fragment_to_tokens, TagType};
use crate::view::{attribute_value, event_type_and_handler};
use crate::view::{
attribute_absolute, attribute_to_tokens, attribute_value,
event_type_and_handler,
};
use proc_macro2::{Ident, TokenStream, TokenTree};
use quote::{format_ident, quote, quote_spanned};
use rstml::node::{NodeAttribute, NodeElement, NodeName, NodeNameFragment};
use rstml::node::{
NodeAttribute, NodeBlock, NodeElement, NodeName, NodeNameFragment,
};
use std::collections::HashMap;
use syn::spanned::Spanned;
use syn::{spanned::Spanned, Expr, ExprRange, RangeLimits, Stmt};
pub(crate) fn component_to_tokens(
node: &NodeElement,
@ -14,6 +19,32 @@ pub(crate) fn component_to_tokens(
#[cfg(debug_assertions)]
let component_name = ident_from_tag_name(node.name());
// an attribute that contains {..} can be used to split props from attributes
// anything before it is a prop, unless it uses the special attribute syntaxes
// (attr:, style:, on:, prop:, etc.)
// anything after it is a plain HTML attribute to be spread onto the prop
let spread_marker = node
.attributes()
.iter()
.position(|node| match node {
NodeAttribute::Block(NodeBlock::ValidBlock(block)) => {
matches!(
block.stmts.first(),
Some(Stmt::Expr(
Expr::Range(ExprRange {
start: None,
limits: RangeLimits::HalfOpen(_),
end: None,
..
}),
_,
))
)
}
_ => false,
})
.unwrap_or_else(|| node.attributes().len());
let attrs = node.attributes().iter().filter_map(|node| {
if let NodeAttribute::Attribute(node) = node {
Some(node)
@ -48,18 +79,21 @@ pub(crate) fn component_to_tokens(
let props = attrs
.clone()
.filter(|attr| {
let attr_key = attr.key.to_string();
!attr_key.starts_with("let:")
&& !attr_key.starts_with("clone:")
&& !attr_key.starts_with("class:")
&& !attr_key.starts_with("style:")
&& !attr_key.starts_with("attr:")
&& !attr_key.starts_with("prop:")
&& !attr_key.starts_with("on:")
&& !attr_key.starts_with("use:")
.enumerate()
.filter(|(idx, attr)| {
idx < &spread_marker && {
let attr_key = attr.key.to_string();
!attr_key.starts_with("let:")
&& !attr_key.starts_with("clone:")
&& !attr_key.starts_with("class:")
&& !attr_key.starts_with("style:")
&& !attr_key.starts_with("attr:")
&& !attr_key.starts_with("prop:")
&& !attr_key.starts_with("on:")
&& !attr_key.starts_with("use:")
}
})
.map(|attr| {
.map(|(_, attr)| {
let name = &attr.key;
let value = attr
@ -103,7 +137,11 @@ pub(crate) fn component_to_tokens(
let spreads = node
.attributes()
.iter()
.filter_map(|attr| {
.enumerate()
.filter_map(|(idx, attr)| {
if idx == spread_marker {
return None;
}
use rstml::node::NodeBlock;
use syn::{Expr, ExprRange, RangeLimits, Stmt};
@ -118,7 +156,7 @@ pub(crate) fn component_to_tokens(
..
}),
_,
)) => Some(quote! { .add_any_attr(#end) }),
)) => Some(quote! { #end }),
_ => None,
}
} else {
@ -126,59 +164,17 @@ pub(crate) fn component_to_tokens(
};
Some(dotted.unwrap_or_else(|| {
quote! {
.add_any_attr(#[allow(unused_braces)] { #node })
#node
}
}))
} else if let NodeAttribute::Attribute(node) = attr {
// anything that follows the x:y pattern
match &node.key {
NodeName::Punctuated(parts) => {
if parts.len() >= 2 {
let id = &parts[0];
match id {
NodeNameFragment::Ident(id) => {
let value = attribute_value(node);
// ignore `let:`
if id == "let" {
None
}
else if id == "attr" {
let key = &parts[1];
Some(quote! { #key(#value) })
} else if id == "style" || id == "class" {
let key = &node.key.to_string();
let key = key
.replacen("style:", "", 1)
.replacen("class:", "", 1);
Some(quote! { ::leptos::tachys::html::#id::#id((#key, #value)) })
} else if id == "prop" {
let key = &node.key.to_string();
let key = key
.replacen("prop:", "", 1);
Some(quote! { ::leptos::tachys::html::property::#id(#key, #value) })
} else if id == "on" {
let key = &node.key.to_string();
let key = key
.replacen("on:", "", 1);
let (on, ty, handler) = event_type_and_handler(&key, node);
Some(quote! { ::leptos::tachys::html::event::#on(#ty, #handler) })
} else {
proc_macro_error::abort!(id.span(), &format!("`{id}:` syntax is not supported on components"));
}
}
_ => None,
}
} else {
None
}
}
_ => None,
}
attribute_absolute(node, idx >= spread_marker)
} else {
None
}
})
.collect::<Vec<_>>();
let spreads = (!(spreads.is_empty())).then(|| {
quote! {
.add_any_attr((#(#spreads,)*))
@ -292,23 +288,16 @@ pub(crate) fn component_to_tokens(
#[allow(unused_mut)] // used in debug
let mut component = quote_spanned! {node.span()=>
::leptos::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
#name_ref,
#component_props_builder
#(#props)*
#(#slots)*
#children;
#[allow(clippy::let_unit_value, clippy::unit_arg)]
let props = props
#build;
{
#[allow(unreachable_code)]
::leptos::component::component_view(
#[allow(clippy::needless_borrows_for_generic_args)]
#name_ref,
props
#component_props_builder
#(#props)*
#(#slots)*
#children
#build
)
#spreads
}

View file

@ -8,6 +8,7 @@ use proc_macro_error::abort;
use quote::{quote, quote_spanned, ToTokens};
use rstml::node::{
KeyedAttribute, Node, NodeAttribute, NodeBlock, NodeElement, NodeName,
NodeNameFragment,
};
use std::collections::HashMap;
use syn::{
@ -202,6 +203,44 @@ pub(crate) fn element_to_tokens(
} else { */
Some(component_to_tokens(node, global_class))
//}
} else if is_spread_marker(node) {
let mut attributes = Vec::new();
let mut additions = Vec::new();
for node in node.attributes() {
match node {
NodeAttribute::Block(block) => {
if let NodeBlock::ValidBlock(block) = block {
match block.stmts.first() {
Some(Stmt::Expr(
Expr::Range(ExprRange {
start: None,
limits: RangeLimits::HalfOpen(_),
end: Some(end),
..
}),
_,
)) => {
additions.push(quote! { #end });
}
_ => {
additions.push(quote! { #block });
}
}
} else {
additions.push(quote! { #block });
}
}
NodeAttribute::Attribute(node) => {
if let Some(content) = attribute_absolute(node, true) {
attributes.push(content);
}
}
}
}
Some(quote! {
(#(#attributes,)*)
#(.add_any_attr(#additions))*
})
} else {
let tag = name.to_string();
// collect close_tag name to emit semantic information for IDE.
@ -298,6 +337,24 @@ pub(crate) fn element_to_tokens(
}
}
fn is_spread_marker(node: &NodeElement) -> bool {
match node.name() {
NodeName::Block(block) => matches!(
block.stmts.first(),
Some(Stmt::Expr(
Expr::Range(ExprRange {
start: None,
limits: RangeLimits::HalfOpen(_),
end: None,
..
}),
_,
))
),
_ => false,
}
}
fn attribute_to_tokens(
tag_type: TagType,
node: &NodeAttribute,
@ -389,6 +446,89 @@ fn attribute_to_tokens(
}
}
/// Returns attribute values with an absolute path
pub(crate) fn attribute_absolute(
node: &KeyedAttribute,
after_spread: bool,
) -> Option<TokenStream> {
let contains_dash = node.key.to_string().contains('-');
// anything that follows the x:y pattern
match &node.key {
NodeName::Punctuated(parts) if !contains_dash => {
if parts.len() >= 2 {
let id = &parts[0];
match id {
NodeNameFragment::Ident(id) => {
let value = attribute_value(node);
// ignore `let:`
if id == "let" {
None
} else if id == "attr" {
let key = &parts[1];
Some(
quote! { ::leptos::tachys::html::attribute::#key(#value) },
)
} else if id == "style" || id == "class" {
let key = &node.key.to_string();
let key = key
.replacen("style:", "", 1)
.replacen("class:", "", 1);
Some(
quote! { ::leptos::tachys::html::#id::#id((#key, #value)) },
)
} else if id == "prop" {
let key = &node.key.to_string();
let key = key.replacen("prop:", "", 1);
Some(
quote! { ::leptos::tachys::html::property::#id(#key, #value) },
)
} else if id == "on" {
let key = &node.key.to_string();
let key = key.replacen("on:", "", 1);
let (on, ty, handler) =
event_type_and_handler(&key, node);
Some(
quote! { ::leptos::tachys::html::event::#on(#ty, #handler) },
)
} else {
proc_macro_error::abort!(
id.span(),
&format!(
"`{id}:` syntax is not supported on \
components"
)
);
}
}
_ => None,
}
} else {
None
}
}
_ => after_spread.then(|| {
let key = attribute_name(&node.key);
let value = &node.value();
let name = &node.key.to_string();
if name == "class" || name == "style" {
quote! {
::leptos::tachys::html::#key::#key(#value)
}
}
else if name.contains('-') && !name.starts_with("aria-") {
quote! {
::leptos::tachys::html::attribute::custom::custom_attribute(#name, #value)
}
}
else {
quote! {
::leptos::tachys::html::attribute::#key(#value)
}
}
}),
}
}
pub(crate) fn event_to_tokens(
name: &str,
node: &KeyedAttribute,