Merge pull request #232 from gbj/custom-events-in-macro

`leptos_macro` improvements to `class:`, `prop:`, `on:`, `:undelegate…
This commit is contained in:
Greg Johnston 2023-01-04 11:10:19 -05:00 committed by GitHub
commit 3c080e0564
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 155 additions and 24 deletions

View File

@ -85,11 +85,9 @@ macro_rules! generate_event_types {
),* $(,)?} => {
$(
#[doc = "The "]
#[doc = stringify!($event)]
#[doc = " event."]
#[doc = concat!("The `", stringify!($event), "` event, which receives [", stringify!($web_sys_event), "](web_sys::", stringify!($web_sys_event), ") as its argument.")]
#[derive(Copy, Clone)]
#[allow(non_camel_case_types)]
#[derive(Clone, Copy)]
pub struct $event;
impl EventDescriptor for $event {

View File

@ -12,6 +12,7 @@ proc-macro = true
[dependencies]
cfg-if = "1"
doc-comment = "0.3"
itertools = "0.10"
pad-adapter = "0.1"
prettyplease = "0.1"

View File

@ -252,7 +252,7 @@ pub fn view(tokens: TokenStream) -> TokenStream {
/// When you use the component somewhere else, the names of its arguments are the names
/// of the properties you use in the [view](mod@view) macro.
///
/// Every component function should have the return type `-> impl [IntoView](leptos_dom::IntoView)`.
/// Every component function should have the return type `-> impl IntoView`.
///
/// You can add Rust doc comments to component function arguments and the macro will use them to
/// generate documentation for the component.
@ -386,6 +386,51 @@ pub fn view(tokens: TokenStream) -> TokenStream {
/// }
/// }
/// ```
///
/// ## Customizing Properties
/// You can use the `#[prop]` attribute on individual component properties (function arguments) to
/// customize the types that component property can receive. You can use the following attributes:
/// * `#[prop(into)]`: This will call `.into()` on any value passed into the component prop. (For example,
/// you could apply `#[prop(into)]` to a prop that takes [Signal](leptos_reactive::Signal), which would
/// allow users to pass a [ReadSignal](leptos_reactive::ReadSignal) or [RwSignal](leptos_reactive::RwSignal)
/// and automatically convert it.)
/// * `#[prop(optional)]`: If the user does not specify this property when they use the component,
/// it will be set to its default value. If the property type is `Option<T>`, values should be passed
/// as `name=T` and will be received as `Some(T)`.
/// * `#[prop(optional_no_strip)]`: The same as `optional`, but requires values to be passed as `None` or
/// `Some(T)` explicitly. This means that the optional property can be omitted (and be `None`), or explicitly
/// specified as either `None` or `Some(T)`.
/// ```rust
/// # use leptos::*;
///
/// #[component]
/// pub fn MyComponent(
/// cx: Scope,
/// #[prop(into)]
/// name: String,
/// #[prop(optional)]
/// optional_value: Option<i32>,
/// #[prop(optional_no_strip)]
/// optional_no_strip: Option<i32>
/// ) -> impl IntoView {
/// // whatever UI you need
/// }
///
/// #[component]
/// pub fn App(cx: Scope) -> impl IntoView {
/// view! { cx,
/// <MyComponent
/// name="Greg" // automatically converted to String with `.into()`
/// optional_value=42 // received as `Some(42)`
/// optional_no_strip=Some(42) // received as `Some(42)`
/// />
/// <MyComponent
/// name="Bob" // automatically converted to String with `.into()`
/// // optional values can both be omitted, and received as `None`
/// />
/// }
/// }
/// ```
#[proc_macro_error::proc_macro_error]
#[proc_macro_attribute]
pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {

View File

@ -333,6 +333,7 @@ fn attribute_to_tokens_ssr(
.expect("event listener attributes need a value")
.as_ref();
#[allow(unused_variables)]
let (name, is_force_undelegated) = parse_event(name);
let event_type = TYPED_EVENTS
@ -642,22 +643,74 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
.find(|e| **e == name)
.copied()
.unwrap_or("Custom");
let is_custom = event_type == "Custom";
let event_type = event_type
.parse::<TokenStream>()
.expect("couldn't parse event name");
// the `on:event_name` is 1 single name. Ideally, we
// would point `on:` to `.on`, but I don't know how to
// get the "subspan" of a span, so this is good enough
let event_type = if is_custom {
quote! { Custom::new(#name) }
} else {
event_type
};
let event_name_ident = match &node.key {
NodeName::Punctuated(parts) => {
if parts.len() >= 2 {
Some(&parts[1])
} else {
None
}
}
_ => unreachable!(),
};
let undelegated_ident = match &node.key {
NodeName::Punctuated(parts) => parts.last().and_then(|last| {
if last == "undelegated" {
Some(last)
} else {
None
}
}),
_ => unreachable!(),
};
let on = match &node.key {
NodeName::Punctuated(parts) => &parts[0],
_ => unreachable!(),
};
let on = {
let span = on.span();
quote_spanned! {
span => .on
}
};
let event_type = if is_custom {
event_type
} else if let Some(ev_name) = event_name_ident {
let span = ev_name.span();
quote_spanned! {
span => #ev_name
}
} else {
event_type
};
let event_type = if is_force_undelegated {
quote! { ::leptos::ev::undelegated(::leptos::ev::#event_type) }
let undelegated = if let Some(undelegated) = undelegated_ident {
let span = undelegated.span();
quote_spanned! {
span => #undelegated
}
} else {
quote! { undelegated }
};
quote! { ::leptos::ev::#undelegated(::leptos::ev::#event_type) }
} else {
quote! { ::leptos::ev::#event_type }
};
quote! {
.on(#event_type, #handler)
#on(#event_type, #handler)
}
} else if let Some(name) = name.strip_prefix("prop:") {
let value = node
@ -665,8 +718,18 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
.as_ref()
.expect("prop: attributes need a value")
.as_ref();
let prop = match &node.key {
NodeName::Punctuated(parts) => &parts[0],
_ => unreachable!(),
};
let prop = {
let span = prop.span();
quote_spanned! {
span => .prop
}
};
quote! {
.prop(#name, (#cx, #[allow(unused_braces)] #value))
#prop(#name, (#cx, #[allow(unused_braces)] #value))
}
} else if let Some(name) = name.strip_prefix("class:") {
let value = node
@ -674,8 +737,18 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
.as_ref()
.expect("class: attributes need a value")
.as_ref();
let class = match &node.key {
NodeName::Punctuated(parts) => &parts[0],
_ => unreachable!(),
};
let class = {
let span = class.span();
quote_spanned! {
span => .class
}
};
quote! {
.class(#name, (#cx, #[allow(unused_braces)] #value))
#class(#name, (#cx, #[allow(unused_braces)] #value))
}
} else {
let name = name.replacen("attr:", "", 1);
@ -687,8 +760,22 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
}
None => quote_spanned! { span => "" },
};
let attr = match &node.key {
NodeName::Punctuated(parts) => Some(&parts[0]),
_ => None,
};
let attr = if let Some(attr) = attr {
let span = attr.span();
quote_spanned! {
span => .attr
}
} else {
quote! {
.attr(#name, (#cx, #value))
.attr
}
};
quote! {
#attr(#name, (#cx, #value))
}
}
}