mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
feat: enhanced spreading syntax
This commit is contained in:
parent
747d847183
commit
f6c7ac473a
3 changed files with 248 additions and 104 deletions
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue