Properly handle Scope when creating component children

This commit is contained in:
Greg Johnston 2022-12-13 07:42:14 -05:00
parent 8efb28826f
commit 5ca169ac06
9 changed files with 201 additions and 198 deletions

View file

@ -13,7 +13,7 @@ where
/// Will be displayed while resources are pending.
pub fallback: F,
/// Will be displayed once all resources have resolved.
pub children: Box<dyn Fn() -> Fragment>,
pub children: Box<dyn Fn(Scope) -> Fragment>,
}
/// If any [Resource](leptos_reactive::Resource)s are read in the `children` of this
@ -82,7 +82,7 @@ fn render_suspense<'a, F, E>(
cx: Scope,
context: SuspenseContext,
fallback: F,
child: Box<dyn Fn() -> Fragment>,
child: Box<dyn Fn(Scope) -> Fragment>,
) -> impl IntoView
where
F: Fn() -> E + 'static,
@ -92,7 +92,7 @@ where
DynChild::new(move || {
if context.ready() {
child().into_view(cx)
child(cx).into_view(cx)
} else {
fallback().into_view(cx)
}

View file

@ -18,7 +18,7 @@ where
#[builder(default, setter(strip_option, into))]
pub set_pending: Option<SignalSetter<bool>>,
/// Will be displayed once all resources have resolved.
pub children: Box<dyn Fn() -> Fragment>,
pub children: Box<dyn Fn(Scope) -> Fragment>,
}
/// If any [Resource](leptos_reactive::Resource)s are read in the `children` of this
@ -98,7 +98,7 @@ fn render_transition<'a, F, E>(
cx: Scope,
context: SuspenseContext,
fallback: F,
child: Box<dyn Fn() -> Fragment>,
child: Box<dyn Fn(Scope) -> Fragment>,
set_pending: Option<SignalSetter<bool>>,
) -> impl IntoView
where
@ -111,7 +111,7 @@ where
(move || {
if context.ready() {
let current_child = child().into_view(cx);
let current_child = child(cx).into_view(cx);
*prev_child.borrow_mut() = Some(current_child.clone());
if let Some(pending) = &set_pending {
pending.set(false);

View file

@ -358,8 +358,8 @@ fn component_to_tokens(
let children = if node.children.is_empty() {
quote! { }
} else {
let children = fragment_to_tokens(cx, span, &node.children, mode);
quote! { .children(Box::new(move || #children)) }
let children = fragment_to_tokens(&Ident::new("cx", span), span, &node.children, mode);
quote! { .children(Box::new(move |cx| #children)) }
};
let props = node

View file

@ -40,7 +40,7 @@ where
#[builder(default, setter(strip_option))]
pub on_response: Option<Rc<dyn Fn(&web_sys::Response)>>,
/// Component children; should include the HTML of the form elements.
pub children: Box<dyn Fn() -> Fragment>,
pub children: Box<dyn Fn(Scope) -> Fragment>,
}
/// An HTML [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) progressively
@ -139,7 +139,7 @@ where
enctype=enctype
on:submit=on_submit
>
{children}
{move || children(cx)}
</form>
}
})
@ -160,7 +160,7 @@ where
/// manually using [leptos_server::Action::using_server_fn].
pub action: Action<I, Result<O, ServerFnError>>,
/// Component children; should include the HTML of the form elements.
pub children: Box<dyn Fn() -> Fragment>,
pub children: Box<dyn Fn(Scope) -> Fragment>,
}
/// Automatically turns a server [Action](leptos_server::Action) into an HTML
@ -244,7 +244,7 @@ where
/// manually using [leptos_server::Action::using_server_fn].
pub action: MultiAction<I, Result<O, ServerFnError>>,
/// Component children; should include the HTML of the form elements.
pub children: Box<dyn Fn() -> Fragment>,
pub children: Box<dyn Fn(Scope) -> Fragment>,
}
/// Automatically turns a server [MultiAction](leptos_server::MultiAction) into an HTML
@ -289,7 +289,7 @@ where
action=action
on:submit=on_submit
>
{props.children}
{move || (props.children)(cx)}
</form>
}
})

View file

@ -65,7 +65,7 @@ where
#[builder(default, setter(strip_option, into))]
pub class: Option<MaybeSignal<String>>,
/// The nodes or elements to be shown inside the link.
pub children: Box<dyn Fn() -> Fragment>
pub children: Box<dyn Fn(Scope) -> Fragment>
}
/// An HTML [`a`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a)
@ -108,7 +108,7 @@ where
aria-current=move || if is_active.get() { Some("page") } else { None }
class=move || class.as_ref().map(|class| class.get())
>
{props.children}
{move || (props.children)(cx)}
</a>
}
} else {
@ -118,7 +118,7 @@ where
aria-current=move || if is_active() { Some("page") } else { None }
class=move || class.as_ref().map(|class| class.get())
>
{props.children}
{move || (props.children)(cx)}
</a>
}
}

View file

@ -7,29 +7,30 @@ use leptos::*;
/// that child route is displayed. Renders nothing if there is no nested child.
#[component]
pub fn Outlet(cx: Scope) -> impl IntoView {
let route = use_route(cx);
let is_showing = Rc::new(RefCell::new(None));
let (outlet, set_outlet) = create_signal(cx, None);
create_effect(cx, move |_| {
let is_showing_val = { is_showing.borrow().clone() };
let child = route.child();
match (route.child(), &is_showing_val) {
(None, _) => {
set_outlet.set(None);
}
(Some(child), Some(path))
if Some(child.original_path().to_string()) == is_showing_val =>
{
// do nothing: we don't need to rerender the component, because it's the same
}
(Some(child), _) => {
*is_showing.borrow_mut() = Some(child.original_path().to_string());
provide_context(child.cx(), child.clone());
set_outlet.set(Some(child.outlet().into_view(cx)))
}
}
});
Component::new("Outlet", move |cx| {
(move || outlet.get()).into_view(cx)
let route = use_route(cx);
let is_showing = Rc::new(RefCell::new(None));
let (outlet, set_outlet) = create_signal(cx, None);
create_effect(cx, move |_| {
let is_showing_val = { is_showing.borrow().clone() };
let child = route.child();
match (route.child(), &is_showing_val) {
(None, _) => {
set_outlet.set(None);
}
(Some(child), Some(path))
if Some(child.original_path().to_string()) == is_showing_val =>
{
// do nothing: we don't need to rerender the component, because it's the same
}
(Some(child), _) => {
*is_showing.borrow_mut() = Some(child.original_path().to_string());
provide_context(child.cx(), child.clone());
set_outlet.set(Some(child.outlet().into_view(cx)))
}
}
});
move || outlet.get()
})
}

View file

@ -27,19 +27,19 @@ where
pub element: F,
/// `children` may be empty or include nested routes.
#[builder(default, setter(strip_option))]
pub children: Option<Box<dyn Fn() -> Fragment>>,
pub children: Option<Box<dyn Fn(Scope) -> Fragment>>,
}
/// Describes a portion of the nested layout of the app, specifying the route it should match,
/// the element it should display, and data that should be loaded alongside the route.
#[allow(non_snake_case)]
pub fn Route<E, F>(_cx: Scope, props: RouteProps<E, F>) -> RouteDefinition
pub fn Route<E, F>(cx: Scope, props: RouteProps<E, F>) -> RouteDefinition
where
E: IntoView,
F: Fn(Scope) -> E + 'static,
{
let children = props.children
.map(|children| children()
.map(|children| children(cx)
.as_children()
.iter()
.filter_map(|child| child.as_transparent().and_then(|t| t.downcast_ref::<RouteDefinition>()))

View file

@ -31,18 +31,20 @@ pub struct RouterProps {
/// The `<Router/>` should usually wrap your whole page. It can contain
/// any elements, and should include a [Routes](crate::Routes) component somewhere
/// to define and display [Route](crate::Route)s.
pub children: Box<dyn Fn() -> Fragment>,
pub children: Box<dyn Fn(Scope) -> Fragment>,
}
/// Provides for client-side and server-side routing. This should usually be somewhere near
/// the root of the application.
#[allow(non_snake_case)]
pub fn Router(cx: Scope, props: RouterProps) -> impl IntoView {
// create a new RouterContext and provide it to every component beneath the router
let router = RouterContext::new(cx, props.base, props.fallback);
provide_context(cx, router);
Component::new("Router", move |cx| {
// create a new RouterContext and provide it to every component beneath the router
let router = RouterContext::new(cx, props.base, props.fallback);
provide_context(cx, router);
Component::new("Router", move |cx| (props.children)().into_view(cx))
move || (props.children)(cx)
})
}
/// Context type that contains information about the current router state.

View file

@ -21,7 +21,7 @@ use crate::{
pub struct RoutesProps {
#[builder(default, setter(strip_option))]
base: Option<String>,
children: Box<dyn Fn() -> Fragment>,
children: Box<dyn Fn(Scope) -> Fragment>,
}
/// Contains route definitions and manages the actual routing process.
@ -29,137 +29,146 @@ pub struct RoutesProps {
/// You should locate the `<Routes/>` component wherever on the page you want the routes to appear.
#[allow(non_snake_case)]
pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoView {
let router = use_context::<RouterContext>(cx).unwrap_or_else(|| {
log::warn!("<Routes/> component should be nested within a <Router/>.");
panic!()
});
let mut branches = Vec::new();
let children = (props.children)()
.as_children()
.iter()
.filter_map(|child| child.as_transparent().and_then(|t| t.downcast_ref::<RouteDefinition>()))
.cloned()
.collect::<Vec<_>>();
create_branches(
&children,
&props.base.unwrap_or_default(),
&mut Vec::new(),
&mut branches,
);
// whenever path changes, update matches
let matches = create_memo(cx, {
let router = router.clone();
move |_| get_route_matches(branches.clone(), router.pathname().get())
});
// Rebuild the list of nested routes conservatively, and show the root route here
let disposers = RefCell::new(Vec::<ScopeDisposer>::new());
// iterate over the new matches, reusing old routes when they are the same
// and replacing them with new routes when they differ
let next: Rc<RefCell<Vec<RouteContext>>> = Default::default();
let root_equal = Rc::new(Cell::new(true));
let route_states: Memo<RouterState> = create_memo(cx, {
let root_equal = root_equal.clone();
move |prev: Option<&RouterState>| {
root_equal.set(true);
next.borrow_mut().clear();
let next_matches = matches.get();
let prev_matches = prev.as_ref().map(|p| &p.matches);
let prev_routes = prev.as_ref().map(|p| &p.routes);
// are the new route matches the same as the previous route matches so far?
let mut equal = prev_matches
.map(|prev_matches| next_matches.len() == prev_matches.len())
.unwrap_or(false);
for i in 0..next_matches.len() {
let next = next.clone();
let prev_match = prev_matches.and_then(|p| p.get(i));
let next_match = next_matches.get(i).unwrap();
match (prev_routes, prev_match) {
(Some(prev), Some(prev_match))
if next_match.route.key == prev_match.route.key =>
{
let prev_one = { prev.borrow()[i].clone() };
if i >= next.borrow().len() {
next.borrow_mut().push(prev_one);
} else {
*(next.borrow_mut().index_mut(i)) = prev_one;
Component::new("Routes", move |cx| {
let router = use_context::<RouterContext>(cx).unwrap_or_else(|| {
log::warn!("<Routes/> component should be nested within a <Router/>.");
panic!()
});
let mut branches = Vec::new();
let children = (props.children)(cx)
.as_children()
.iter()
.filter_map(|child| child.as_transparent().and_then(|t| t.downcast_ref::<RouteDefinition>()))
.cloned()
.collect::<Vec<_>>();
create_branches(
&children,
&props.base.unwrap_or_default(),
&mut Vec::new(),
&mut branches,
);
// whenever path changes, update matches
let matches = create_memo(cx, {
let router = router.clone();
move |_| get_route_matches(branches.clone(), router.pathname().get())
});
// Rebuild the list of nested routes conservatively, and show the root route here
let disposers = RefCell::new(Vec::<ScopeDisposer>::new());
// iterate over the new matches, reusing old routes when they are the same
// and replacing them with new routes when they differ
let next: Rc<RefCell<Vec<RouteContext>>> = Default::default();
let root_equal = Rc::new(Cell::new(true));
let route_states: Memo<RouterState> = create_memo(cx, {
let root_equal = root_equal.clone();
move |prev: Option<&RouterState>| {
root_equal.set(true);
next.borrow_mut().clear();
let next_matches = matches.get();
let prev_matches = prev.as_ref().map(|p| &p.matches);
let prev_routes = prev.as_ref().map(|p| &p.routes);
// are the new route matches the same as the previous route matches so far?
let mut equal = prev_matches
.map(|prev_matches| next_matches.len() == prev_matches.len())
.unwrap_or(false);
for i in 0..next_matches.len() {
let next = next.clone();
let prev_match = prev_matches.and_then(|p| p.get(i));
let next_match = next_matches.get(i).unwrap();
match (prev_routes, prev_match) {
(Some(prev), Some(prev_match))
if next_match.route.key == prev_match.route.key =>
{
let prev_one = { prev.borrow()[i].clone() };
if i >= next.borrow().len() {
next.borrow_mut().push(prev_one);
} else {
*(next.borrow_mut().index_mut(i)) = prev_one;
}
}
}
_ => {
equal = false;
if i == 0 {
root_equal.set(false);
}
let disposer = cx.child_scope({
let next = next.clone();
let router = Rc::clone(&router.inner);
move |cx| {
_ => {
equal = false;
if i == 0 {
root_equal.set(false);
}
let disposer = cx.child_scope({
let next = next.clone();
let next_ctx = RouteContext::new(
cx,
&RouterContext { inner: router },
{
let next = next.clone();
move || {
if let Some(route_states) =
use_context::<Memo<RouterState>>(cx)
{
route_states.with(|route_states| {
let routes = route_states.routes.borrow();
routes.get(i + 1).cloned()
})
} else {
next.borrow().get(i + 1).cloned()
let router = Rc::clone(&router.inner);
move |cx| {
let next = next.clone();
let next_ctx = RouteContext::new(
cx,
&RouterContext { inner: router },
{
let next = next.clone();
move || {
if let Some(route_states) =
use_context::<Memo<RouterState>>(cx)
{
route_states.with(|route_states| {
let routes = route_states.routes.borrow();
routes.get(i + 1).cloned()
})
} else {
next.borrow().get(i + 1).cloned()
}
}
},
move || matches.with(|m| m.get(i).cloned()),
);
if let Some(next_ctx) = next_ctx {
if next.borrow().len() > i + 1 {
next.borrow_mut()[i] = next_ctx;
} else {
next.borrow_mut().push(next_ctx);
}
},
move || matches.with(|m| m.get(i).cloned()),
);
if let Some(next_ctx) = next_ctx {
if next.borrow().len() > i + 1 {
next.borrow_mut()[i] = next_ctx;
} else {
next.borrow_mut().push(next_ctx);
}
}
});
if disposers.borrow().len() > i + 1 {
let mut disposers = disposers.borrow_mut();
let old_route_disposer = std::mem::replace(&mut disposers[i], disposer);
old_route_disposer.dispose();
} else {
disposers.borrow_mut().push(disposer);
}
});
if disposers.borrow().len() > i + 1 {
let mut disposers = disposers.borrow_mut();
let old_route_disposer = std::mem::replace(&mut disposers[i], disposer);
old_route_disposer.dispose();
} else {
disposers.borrow_mut().push(disposer);
}
}
}
}
if disposers.borrow().len() > next_matches.len() {
let surplus_disposers = disposers.borrow_mut().split_off(next_matches.len() + 1);
for disposer in surplus_disposers {
disposer.dispose();
if disposers.borrow().len() > next_matches.len() {
let surplus_disposers = disposers.borrow_mut().split_off(next_matches.len() + 1);
for disposer in surplus_disposers {
disposer.dispose();
}
}
}
if let Some(prev) = &prev {
if equal {
RouterState {
matches: next_matches.to_vec(),
routes: prev_routes.cloned().unwrap_or_default(),
root: prev.root.clone(),
if let Some(prev) = &prev {
if equal {
RouterState {
matches: next_matches.to_vec(),
routes: prev_routes.cloned().unwrap_or_default(),
root: prev.root.clone(),
}
} else {
let root = next.borrow().get(0).cloned();
RouterState {
matches: next_matches.to_vec(),
routes: Rc::new(RefCell::new(next.borrow().to_vec())),
root,
}
}
} else {
let root = next.borrow().get(0).cloned();
@ -169,37 +178,28 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoView {
root,
}
}
} else {
let root = next.borrow().get(0).cloned();
RouterState {
matches: next_matches.to_vec(),
routes: Rc::new(RefCell::new(next.borrow().to_vec())),
root,
}
});
// show the root route
let root = create_memo(cx, move |prev| {
provide_context(cx, route_states);
route_states.with(|state| {
let root = state.routes.borrow();
let root = root.get(0);
if let Some(route) = root {
provide_context(cx, route.clone());
}
}
}
});
// show the root route
let root = create_memo(cx, move |prev| {
provide_context(cx, route_states);
route_states.with(|state| {
let root = state.routes.borrow();
let root = root.get(0);
if let Some(route) = root {
provide_context(cx, route.clone());
}
if prev.is_none() || !root_equal.get() {
root.as_ref().map(|route| route.outlet().into_view(cx))
} else {
prev.cloned().unwrap()
}
})
});
Component::new("Routes", move |cx| {
(move || root.get()).into_view(cx)
if prev.is_none() || !root_equal.get() {
root.as_ref().map(|route| route.outlet().into_view(cx))
} else {
prev.cloned().unwrap()
}
})
});
move || root.get()
})
}