feat: pass components with no props directly into the view as a function that takes only `Scope` (#1144)

This commit is contained in:
Greg Johnston 2023-06-05 20:48:22 -04:00 committed by GitHub
parent 96f961ef54
commit 17adf7cc14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 160 additions and 67 deletions

7
examples/README.md Normal file
View File

@ -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).

View File

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

View File

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

View File

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

View File

@ -18,13 +18,13 @@ pub fn App(cx: Scope) -> impl IntoView {
<main>
<Routes>
// Well 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>

View File

@ -18,18 +18,18 @@ pub fn App(cx: Scope) -> impl IntoView {
<main>
<Routes>
// Well 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>

View File

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

View File

@ -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)
}
}

View File

@ -36,6 +36,5 @@ pub fn TestComponent(
and_another: usize,
) -> impl IntoView {
_ = (key, another, and_another);
todo!()
}

View File

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

View File

@ -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!()
/// }
/// ```
///

View File

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

View File

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