feat: pass components with no props directly into the view as a function that takes only `Scope` (#1144)
This commit is contained in:
parent
96f961ef54
commit
17adf7cc14
|
@ -0,0 +1,7 @@
|
|||
# Examples
|
||||
|
||||
The examples in this directory are all built and tested against the current `main` branch.
|
||||
|
||||
To the extent that new features have been released or breaking changes have been made since the previous release, the examples are compatible with the `main` branch and not the current release.
|
||||
|
||||
To see the examples as they were at the time of the `0.3.0` release, [click here](https://github.com/leptos-rs/leptos/tree/v0.3.0/examples).
|
|
@ -22,9 +22,9 @@ pub fn App(cx: Scope) -> impl IntoView {
|
|||
<Nav />
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="users/:id" view=|cx| view! { cx, <User/> }/>
|
||||
<Route path="stories/:id" view=|cx| view! { cx, <Story/> }/>
|
||||
<Route path=":stories?" view=|cx| view! { cx, <Stories/> }/>
|
||||
<Route path="users/:id" view=User/>
|
||||
<Route path="stories/:id" view=Story/>
|
||||
<Route path=":stories?" view=Stories/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
|
|
@ -22,9 +22,9 @@ pub fn App(cx: Scope) -> impl IntoView {
|
|||
<Nav />
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="users/:id" view=|cx| view! { cx, <User/> }/>
|
||||
<Route path="stories/:id" view=|cx| view! { cx, <Story/> }/>
|
||||
<Route path=":stories?" view=|cx| view! { cx, <Stories/> }/>
|
||||
<Route path="users/:id" view=User/>
|
||||
<Route path="stories/:id" view=Story/>
|
||||
<Route path=":stories?" view=Stories/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
|
|
@ -36,15 +36,15 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
|
|||
<ContactRoutes/>
|
||||
<Route
|
||||
path="about"
|
||||
view=move |cx| view! { cx, <About/> }
|
||||
view=About
|
||||
/>
|
||||
<Route
|
||||
path="settings"
|
||||
view=move |cx| view! { cx, <Settings/> }
|
||||
view=Settings
|
||||
/>
|
||||
<Route
|
||||
path="redirect-home"
|
||||
view=move |cx| view! { cx, <Redirect path="/"/> }
|
||||
view=|cx| view! { cx, <Redirect path="/"/> }
|
||||
/>
|
||||
</AnimatedRoutes>
|
||||
</main>
|
||||
|
@ -59,15 +59,15 @@ pub fn ContactRoutes(cx: Scope) -> impl IntoView {
|
|||
view! { cx,
|
||||
<Route
|
||||
path=""
|
||||
view=move |cx| view! { cx, <ContactList/> }
|
||||
view=ContactList
|
||||
>
|
||||
<Route
|
||||
path=":id"
|
||||
view=move |cx| view! { cx, <Contact/> }
|
||||
view=Contact
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
view=move |_| view! { cx, <p>"Select a contact."</p> }
|
||||
view=|cx| view! { cx, <p>"Select a contact."</p> }
|
||||
/>
|
||||
</Route>
|
||||
}
|
||||
|
|
|
@ -18,13 +18,13 @@ pub fn App(cx: Scope) -> impl IntoView {
|
|||
<main>
|
||||
<Routes>
|
||||
// We’ll load the home page with out-of-order streaming and <Suspense/>
|
||||
<Route path="" view=|cx| view! { cx, <HomePage/> }/>
|
||||
<Route path="" view=HomePage/>
|
||||
|
||||
// We'll load the posts with async rendering, so they can set
|
||||
// the title and metadata *after* loading the data
|
||||
<Route
|
||||
path="/post/:id"
|
||||
view=|cx| view! { cx, <Post/> }
|
||||
view=Post
|
||||
ssr=SsrMode::Async
|
||||
/>
|
||||
</Routes>
|
||||
|
|
|
@ -18,18 +18,18 @@ pub fn App(cx: Scope) -> impl IntoView {
|
|||
<main>
|
||||
<Routes>
|
||||
// We’ll load the home page with out-of-order streaming and <Suspense/>
|
||||
<Route path="" view=|cx| view! { cx, <HomePage/> }/>
|
||||
<Route path="" view=HomePage/>
|
||||
|
||||
// We'll load the posts with async rendering, so they can set
|
||||
// the title and metadata *after* loading the data
|
||||
<Route
|
||||
path="/post/:id"
|
||||
view=|cx| view! { cx, <Post/> }
|
||||
view=Post
|
||||
ssr=SsrMode::Async
|
||||
/>
|
||||
<Route
|
||||
path="/post_in_order/:id"
|
||||
view=|cx| view! { cx, <Post/> }
|
||||
view=Post
|
||||
ssr=SsrMode::InOrder
|
||||
/>
|
||||
</Routes>
|
||||
|
|
|
@ -60,7 +60,7 @@ rkyv = ["leptos_reactive/rkyv"]
|
|||
tracing = ["leptos_macro/tracing"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable", "tracing", "template_macro"]
|
||||
denylist = ["stable", "tracing", "template_macro", "rustls", "default-tls", "web-sys", "wasm-bindgen"]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"csr",
|
||||
|
|
|
@ -240,11 +240,84 @@ pub trait Props {
|
|||
fn builder() -> Self::Builder;
|
||||
}
|
||||
|
||||
impl<P, F, R> Component<P> for F where F: FnOnce(::leptos::Scope, P) -> R {}
|
||||
#[doc(hidden)]
|
||||
pub trait PropsOrNoPropsBuilder {
|
||||
type Builder;
|
||||
fn builder_or_not() -> Self::Builder;
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn component_props_builder<P: Props>(
|
||||
_f: &impl Component<P>,
|
||||
) -> <P as Props>::Builder {
|
||||
<P as Props>::builder()
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct EmptyPropsBuilder {}
|
||||
|
||||
impl EmptyPropsBuilder {
|
||||
pub fn build(self) {}
|
||||
}
|
||||
|
||||
impl<P: Props> PropsOrNoPropsBuilder for P {
|
||||
type Builder = <P as Props>::Builder;
|
||||
fn builder_or_not() -> Self::Builder {
|
||||
Self::builder()
|
||||
}
|
||||
}
|
||||
|
||||
impl PropsOrNoPropsBuilder for EmptyPropsBuilder {
|
||||
type Builder = EmptyPropsBuilder;
|
||||
fn builder_or_not() -> Self::Builder {
|
||||
EmptyPropsBuilder {}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, R> Component<EmptyPropsBuilder> for F where
|
||||
F: FnOnce(::leptos::Scope) -> R
|
||||
{
|
||||
}
|
||||
|
||||
impl<P, F, R> Component<P> for F
|
||||
where
|
||||
F: FnOnce(::leptos::Scope, P) -> R,
|
||||
P: Props,
|
||||
{
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn component_props_builder<P: PropsOrNoPropsBuilder>(
|
||||
_f: &impl Component<P>,
|
||||
) -> <P as PropsOrNoPropsBuilder>::Builder {
|
||||
<P as PropsOrNoPropsBuilder>::builder_or_not()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn component_view<P>(
|
||||
f: impl ComponentConstructor<P>,
|
||||
cx: Scope,
|
||||
props: P,
|
||||
) -> View {
|
||||
f.construct(cx, props)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait ComponentConstructor<P> {
|
||||
fn construct(self, cx: Scope, props: P) -> View;
|
||||
}
|
||||
|
||||
impl<Func, V> ComponentConstructor<()> for Func
|
||||
where
|
||||
Func: FnOnce(Scope) -> V,
|
||||
V: IntoView,
|
||||
{
|
||||
fn construct(self, cx: Scope, (): ()) -> View {
|
||||
(self)(cx).into_view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Func, V, P> ComponentConstructor<P> for Func
|
||||
where
|
||||
Func: FnOnce(Scope, P) -> V,
|
||||
V: IntoView,
|
||||
P: PropsOrNoPropsBuilder,
|
||||
{
|
||||
fn construct(self, cx: Scope, props: P) -> View {
|
||||
(self)(cx, props).into_view(cx)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,5 @@ pub fn TestComponent(
|
|||
and_another: usize,
|
||||
) -> impl IntoView {
|
||||
_ = (key, another, and_another);
|
||||
todo!()
|
||||
}
|
||||
|
||||
|
|
|
@ -131,6 +131,8 @@ impl ToTokens for Model {
|
|||
ret,
|
||||
} = self;
|
||||
|
||||
let no_props = props.len() == 1;
|
||||
|
||||
let mut body = body.to_owned();
|
||||
|
||||
// check for components that end ;
|
||||
|
@ -213,6 +215,42 @@ impl ToTokens for Model {
|
|||
}
|
||||
};
|
||||
|
||||
let props_arg = if no_props {
|
||||
quote! {}
|
||||
} else {
|
||||
quote! {
|
||||
props: #props_name #generics
|
||||
}
|
||||
};
|
||||
|
||||
let destructure_props = if no_props {
|
||||
quote! {}
|
||||
} else {
|
||||
quote! {
|
||||
let #props_name {
|
||||
#prop_names
|
||||
} = props;
|
||||
}
|
||||
};
|
||||
|
||||
let into_view = if no_props {
|
||||
quote! {
|
||||
impl #generics ::leptos::IntoView for #props_name #generics #where_clause {
|
||||
fn into_view(self, cx: ::leptos::Scope) -> ::leptos::View {
|
||||
#name(cx).into_view(cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
impl #generics ::leptos::IntoView for #props_name #generics #where_clause {
|
||||
fn into_view(self, cx: ::leptos::Scope) -> ::leptos::View {
|
||||
#name(cx, self).into_view(cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let output = quote! {
|
||||
#[doc = #builder_name_doc]
|
||||
#[doc = ""]
|
||||
|
@ -231,11 +269,7 @@ impl ToTokens for Model {
|
|||
}
|
||||
}
|
||||
|
||||
impl #generics ::leptos::IntoView for #props_name #generics #where_clause {
|
||||
fn into_view(self, cx: ::leptos::Scope) -> ::leptos::View {
|
||||
#name(cx, self).into_view(cx)
|
||||
}
|
||||
}
|
||||
#into_view
|
||||
|
||||
#docs
|
||||
#component_fn_prop_docs
|
||||
|
@ -244,15 +278,13 @@ impl ToTokens for Model {
|
|||
#vis fn #name #generics (
|
||||
#[allow(unused_variables)]
|
||||
#scope_name: ::leptos::Scope,
|
||||
props: #props_name #generics
|
||||
#props_arg
|
||||
) #ret #(+ #lifetimes)*
|
||||
#where_clause
|
||||
{
|
||||
#body
|
||||
|
||||
let #props_name {
|
||||
#prop_names
|
||||
} = props;
|
||||
#destructure_props
|
||||
|
||||
#tracing_span_expr
|
||||
|
||||
|
|
|
@ -502,15 +502,11 @@ pub fn template(tokens: TokenStream) -> TokenStream {
|
|||
///
|
||||
/// // PascalCase: Generated component will be called MyComponent
|
||||
/// #[component]
|
||||
/// fn MyComponent(cx: Scope) -> impl IntoView {
|
||||
/// todo!()
|
||||
/// }
|
||||
/// fn MyComponent(cx: Scope) -> impl IntoView {}
|
||||
///
|
||||
/// // snake_case: Generated component will be called MySnakeCaseComponent
|
||||
/// #[component]
|
||||
/// fn my_snake_case_component(cx: Scope) -> impl IntoView {
|
||||
/// todo!()
|
||||
/// }
|
||||
/// fn my_snake_case_component(cx: Scope) -> impl IntoView {}
|
||||
/// ```
|
||||
///
|
||||
/// 3. The macro generates a type `ComponentProps` for every `Component` (so, `HomePage` generates `HomePageProps`,
|
||||
|
@ -526,9 +522,7 @@ pub fn template(tokens: TokenStream) -> TokenStream {
|
|||
/// use leptos::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn MyComponent(cx: Scope) -> impl IntoView {
|
||||
/// todo!()
|
||||
/// }
|
||||
/// pub fn MyComponent(cx: Scope) -> impl IntoView {}
|
||||
/// }
|
||||
/// ```
|
||||
/// ```
|
||||
|
@ -542,9 +536,7 @@ pub fn template(tokens: TokenStream) -> TokenStream {
|
|||
/// use leptos::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn my_snake_case_component(cx: Scope) -> impl IntoView {
|
||||
/// todo!()
|
||||
/// }
|
||||
/// pub fn my_snake_case_component(cx: Scope) -> impl IntoView {}
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
|
@ -557,7 +549,6 @@ pub fn template(tokens: TokenStream) -> TokenStream {
|
|||
///
|
||||
/// #[component]
|
||||
/// fn MyComponent<T: Fn() -> HtmlElement<Div>>(cx: Scope, render_prop: T) -> impl IntoView {
|
||||
/// todo!()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
|
@ -571,7 +562,6 @@ pub fn template(tokens: TokenStream) -> TokenStream {
|
|||
/// where
|
||||
/// T: Fn() -> HtmlElement<Div>,
|
||||
/// {
|
||||
/// todo!()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
|
|
|
@ -436,6 +436,7 @@ fn element_to_tokens_ssr(
|
|||
template.push('<');
|
||||
template.push_str(&tag_name);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
stmts_for_ide.save_element_completion(node);
|
||||
|
||||
let mut inner_html = None;
|
||||
|
@ -1671,7 +1672,8 @@ pub(crate) fn component_to_tokens(
|
|||
});
|
||||
|
||||
let mut component = quote! {
|
||||
#name(
|
||||
::leptos::component_view(
|
||||
&#name,
|
||||
#cx,
|
||||
::leptos::component_props_builder(&#name)
|
||||
#(#props)*
|
||||
|
@ -1681,7 +1683,8 @@ pub(crate) fn component_to_tokens(
|
|||
)
|
||||
};
|
||||
|
||||
IdeTagHelper::add_component_completion(&mut component, cx, node);
|
||||
#[cfg(debug_assertions)]
|
||||
IdeTagHelper::add_component_completion(&mut component, node);
|
||||
|
||||
if events.is_empty() {
|
||||
component
|
||||
|
@ -2093,13 +2096,12 @@ impl IdeTagHelper {
|
|||
}
|
||||
}
|
||||
|
||||
/// Add completion to the closing tag of the component.
|
||||
///
|
||||
/// Add completion to close tag of the component definition.
|
||||
/// In order to ensure that generics are passed through correctly in the
|
||||
/// current builder pattern, this clones the whole component constructor,
|
||||
/// but it will never be used.
|
||||
///
|
||||
/// we can emit full copy of open_tag builder (close_tag.props().slots().children().build()),
|
||||
/// but i choose to replace props with unreachable! call,
|
||||
/// so expansion output will be shorter.
|
||||
/// The output is looking like:
|
||||
/// ```no_build
|
||||
/// if false {
|
||||
/// close_tag(cx, unreachable!())
|
||||
|
@ -2108,24 +2110,17 @@ impl IdeTagHelper {
|
|||
/// open_tag(open_tag.props().slots().children().build())
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Because element type can have generics, we cant construct simple statement like
|
||||
/// let _ = #name; or (let _: #name = unreachable!()) both syntax require to know generics params.
|
||||
pub fn add_component_completion(
|
||||
component: &mut TokenStream,
|
||||
cx: &Ident,
|
||||
node: &NodeElement,
|
||||
) {
|
||||
// emit ide helper info
|
||||
if let Some(close_tag) = node.close_tag.as_ref().map(|c| &c.name) {
|
||||
let name = close_tag;
|
||||
if node.close_tag.is_some() {
|
||||
let constructor = component.clone();
|
||||
*component = quote! {
|
||||
if false {
|
||||
#[allow(unreachable_code)]
|
||||
#name(
|
||||
#cx,
|
||||
unreachable!()
|
||||
)
|
||||
#constructor
|
||||
} else {
|
||||
#component
|
||||
}
|
||||
|
|
|
@ -43,8 +43,6 @@ use std::any::{Any, TypeId};
|
|||
/// // consume the provided context of type `ValueSetter` using `use_context`
|
||||
/// // this traverses up the tree of `Scope`s and gets the nearest provided `ValueSetter`
|
||||
/// let set_value = use_context::<ValueSetter>(cx).unwrap().0;
|
||||
///
|
||||
/// todo!()
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
|
@ -107,7 +105,6 @@ where
|
|||
/// // this traverses up the tree of `Scope`s and gets the nearest provided `ValueSetter`
|
||||
/// let set_value = use_context::<ValueSetter>(cx).unwrap().0;
|
||||
///
|
||||
/// todo!()
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
|
|
Loading…
Reference in New Issue