Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow for a more concise syntax when passing views as props #766

Closed
elliotwaite opened this issue Mar 29, 2023 · 9 comments
Closed
Labels
enhancement New feature or request

Comments

@elliotwaite
Copy link
Contributor

Instead of having to write this:

<Routes>
  <Route path="/" view=|cx| view! { cx, <Home/> }/>
  <Route path="/other" view=|cx| view! { cx, <Other>"children"</Other> }/>
  <Route path="/*any" view=|cx| view! { cx, "just text" }/>
</Routes>

Allow for something like this:

<Routes>
  <Route path="/" view=<Home/>/>
  <Route path="/other" view=<Other>"children"</Other>/>
  <Route path="/*any" view=<>"just text"</>/>
</Routes>

Would it be possible for the view! macro to support this type of syntax?

@gbj gbj added the enhancement New feature or request label Mar 29, 2023
@bram209
Copy link
Contributor

bram209 commented Mar 30, 2023

Would be nice if this was possible (didn't test code)

#[component]
fn ResourceOk<R, RV, IV, Props>(
    cx: Scope,
    resource: ReadSignal<Result<R, String>>,
    resource_view: RV,
) -> impl IntoView
where
    R: 'static,
    RV: FnOnce(Scope, Props) -> IV + 'static,
    Props: From<R>,
    IV: IntoView,
{
    todo!()
}

So you would be able to call it by:

struct User;

#[component]
fn UserProfile(cx: Scope, user: User) -> impl IntoView { }

#[component]
fn SomeComponent(cx: Scope) -> impl IntoView {
    let (name, set_name) = create_signal(cx, Ok(User {}));

    view! { cx,
        <ResourceOk resource=name resource_view=UserProfile  />
    }

Update: added working example

Example code
use leptos::*;
use std::marker::PhantomData;

#[component]
fn ResourceOk<R, RV, IV, Props>(
    cx: Scope,
    resource: ReadSignal<Result<R, String>>,
    view: RV,
    #[prop(default = Default::default())] _m1: PhantomData<IV>,
    #[prop(default = Default::default())] _m2: PhantomData<Props>,
) -> impl IntoView
where
    R: Clone + 'static,
    RV: Fn(Scope, Props) -> IV + Sized + 'static,
    Props: From<R>,
    IV: IntoView,
{
    move || match resource() {
        Err(_) => view! { cx, <></> },
        Ok(r) => view! { cx, <>{view(cx, r.into())}</> },
    }
}

impl From<User> for UserProfileProps {
    fn from(user: User) -> Self {
        UserProfileProps { user }
    }
}

#[derive(Clone)]
struct User {
    name: String,
}

#[component]
fn UserProfile(cx: Scope, user: User) -> impl IntoView {
    view! { cx, <div>"User profile: "{user.name}</div>}
}

#[component]
fn App(cx: Scope) -> impl IntoView {
    let (name, set_name) = create_signal(
        cx,
        Ok(User {
            name: "Bram".to_owned(),
        }),
    );

    view! { cx,
        <>
            <button on:click=move |_| set_name.set(Ok(User { name: "Someone".to_owned()}))>"Change name"</button>
            <button on:click=move |_| set_name.set(Err("Something bad happened".to_owned()))>"Error"</button>
            <ResourceOk resource=name view=UserProfile/>
        </>
    }
}

fn main() {
    mount_to_body(|cx| view! { cx, <App/> })
}

@elliotwaite
Copy link
Contributor Author

elliotwaite commented Mar 31, 2023

Another area this might apply to is when using views in code that's already inside a !view macro. For example, instead of having to write this:

let values = vec![0, 1, 2];
view! { cx,
    <ul>
        {values.into_iter()
            .map(|n| view! { cx, <li>{n}</li>})
            .collect::<Vec<_>>()}
    </ul>
}

Allow for something like this:

let values = vec![0, 1, 2];
view! { cx,
    <ul>
        {values.into_iter()
            .map(|n| <li>{n}</li>) // <-- Using `<li>{n}</li>` directly without the nested `view!` macro.
            .collect::<Vec<_>>()}
    </ul>
}

So maybe instead of this feature only being about passing views as props, it should be about being able to use the HTML-like view syntax anywhere inside a view! macro without having to rewrap those views in nested view! macros.

Taking the above example even further (although this may be going beyond the scope of the original issue), it would also be nice to be able to use iterators as children, so that we could also leave off the .collect::<Vec<_>>() part:

let values = vec![0, 1, 2];
view! { cx,
    <ul>
        {values.into_iter().map(|n| <li>{n}</li>)}
    </ul>
}

Also, I'm very new to Rust, so feel free to let me know if I'm making any incorrect assumptions about what would be possible.

@bram209
Copy link
Contributor

bram209 commented Apr 2, 2023

Another area this might apply to is when using views in code that's already inside a !view macro.

This is not really feasible, considering that the view! macro parses rust code with the syn crate, so it will ask syn to parse a full expression. syn is not aware that it is being used within a leptos context. So unless you fork syn and add a new variant to syn::Expr or write a full rust source code parser, I don't see how this can easily be implemented.

@elliotwaite
Copy link
Contributor Author

@bram209, ah, I see. Thanks for the explanation.

So perhaps the alternative strategy of using custom components, like <For> and <If> (as mentioned in #769), would be the better way of implementing this type of control flow while avoiding nested view! macros.

@vldm
Copy link
Contributor

vldm commented May 19, 2023

I am not sure that having syntax like this is a good idea:

<Routes>
  <Route path="/" view=<Home/>/>
  <Route path="/other" view=<Other>"children"</Other>/>
  <Route path="/*any" view=<>"just text"</>/>
</Routes>

This one makes it incompatible with [HTML spec]https://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#attributes-0.

Unquoted attribute value syntax
The attribute name, followed by zero or more space characters, followed by a single U+003D EQUALS SIGN character, followed by zero or more space characters, followed by the attribute value, which, in addition to the requirements given above for attribute values, must not contain any literal space characters, any U+0022 QUOTATION MARK characters ("), U+0027 APOSTROPHE characters ('), U+003D EQUALS SIGN characters (=), U+003C LESS-THAN SIGN characters (<), U+003E GREATER-THAN SIGN characters (>), or U+0060 GRAVE ACCENT characters (`), and must not be the empty string.

view! { cx,
    <ul>
        {values.into_iter()
            .map(|n| <li>{n}</li>) // <-- Using `<li>{n}</li>` directly without the nested `view!` macro.
            .collect::<Vec<_>>()}
    </ul>
}

This one is probably impossible as @bram209 have mentioned.
Also both of this syntax makes it harder to read by mixing html and rust context.

But what have you considered making both construction more declarative?

For example:

<Routes>
  <Route path="/" > <Home/> </Route>
  <Route path="/other" /> <Other>"children"</Other> </Route>
  <Route path="/*any" />"just text"</Route>
</Routes>

it can become more verbose, because of closed tag, but also more html-like?

For nested routes, we can treat single child as a path to route, force to use special
<Route default>, or to allow nested <Routes>

<Routes>
  <Route path="/" > 
     <Home/>
     <Route path="/*any" />"just text"</Route>
  </Route>
  <Route path="/other" />
    <Route sdefault>
      <Other>"children"</Other>
    </Route>
    <Route path="/*any" />{"just text"}</Route>
  </Route>
</Routes>

with nested

<Routes>
  <Route path="/" > 
     <Home/>
     <Routes>
       <Route path="/*any" />"just text"</Route>
     <Routes/>
  </Route>
</Routes>

Or more complex example with binding conception:

<ul>
   <For each=values bind(n) > // Bind element of iteration to child context 
     <li>{n}</li>
   </For>
</ul>

Bind can be used as a generic way to pass variable from parent component to its childs.

What you guys think about it?

@gbj
Copy link
Collaborator

gbj commented May 21, 2023

We could probably use the new slot approach for nested routes:

<Routes>
  <Route path="/" > 
     <Home/>
     <Route slot:nested path="/*any" />"just text"</Route>
  </Route>
  <Route path="/other" />
    <Route>
      <Other>"children"</Other>
     </Route>
     <Route slot:nested path="/*any" />{"just text"}</Route>
  </Route>
</Routes>

Although I think I actually find that less readable than the view=... version, because it's really hard to see at a glance what's a nested route and what's the view for this route.

@bram209
Copy link
Contributor

bram209 commented May 23, 2023

We could probably use the new slot approach for nested routes:

<Routes>
  <Route path="/" > 
     <Home/>
     <Route slot:nested path="/*any" />"just text"</Route>
  </Route>
  <Route path="/other" />
    <Route>
      <Other>"children"</Other>
     </Route>
     <Route slot:nested path="/*any" />{"just text"}</Route>
  </Route>
</Routes>

Although I think I actually find that less readable than the view=... version, because it's really hard to see at a glance what's a nested route and what's the view for this route.

This looks less readable for me too.

@gbj
Copy link
Collaborator

gbj commented May 27, 2023

Some good news on this one.

First off, something like this already works if you just leave off #[component] on your top-level route view components:

<Routes>
  <Route path="/" view=Home/>
  <Route path="/other" view=Other/>
</Routes >

ie view takes a Fn(Scope) -> impl IntoView so if you leave off the component macro and just make them functions that take Scope you can feed the functions themselves in.

But granted that's not ideal.

Components by default are redefined into functions with two arguments, Scope and their props. But I did a little experimenting and I think I can get the component macro to define no-prop components as one-argument functions (ie ones that just take Scope) without breaking the view, which doesn't actually know how many props a component has. (Because a component with no props could either have no props, or be props with all default values.) This would allow no-prop components to be passed into props that take a function of scope, ie you could collapse every fallback=|cx| view! { cx, <Loading/> } to just fallback=Loading

@gbj
Copy link
Collaborator

gbj commented Jun 5, 2023

So #1144 implements the idea I mentioned, such that you can write things like

view! { cx,
    <Route
        path=""
        view=ContactList
    >
        <Route
            path=":id"
            view=Contact
        />
        <Route
            path="/"
            view=|cx| view! { cx,  <p>"Select a contact."</p> }
        />
    </Route>
}

instead of

view! { cx,
    <Route
        path=""
        view=|cx| view! { cx, <ContactList/> }
    >
        <Route
            path=":id"
            view=|cx| view! { cx, <Contact/> }
        />
        <Route
            path="/"
            view=|cx| view! { cx,  <p>"Select a contact."</p> }
        />
    </Route>
}

Thinking more about the feedback re: parsing from @vldm and thinking about the way it works in React's JSX, I think directly passing additional components as attributes is probably not the right direction. Note that in JSX, you'd do this inside curly braces

<Something prop={<SomethingElse/>}/>

which is the equivalent of

view! { cx,
<Something prop={view! { cx, <SomethingElse/> }}/>

(just that we explicitly invoke a macro rather than automatically transforming the syntax.

I think this probably ends up in a happy medium place, and I'm going to consider this closed once #1144 is merged, unless there are any objections. See also the binding syntax introduced in #1140, which makes it easier to pass children that takes args instead of render props.

@gbj gbj closed this as completed Jun 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants