Merge branch 'leptos_dom_v2' into leptos_dom_v2_component

This commit is contained in:
Jose Quesada 2022-12-13 07:38:51 -06:00
commit 286c95136f
16 changed files with 375 additions and 571 deletions

View file

@ -11,8 +11,6 @@ use routes::users::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
provide_context(cx, MetaContext::default());
view! {
cx,
<>

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,8 +92,7 @@ where
DynChild::new(move || {
if context.ready() {
child().into_view(cx)
//child().into_view(cx)
child(cx).into_view(cx)
} else {
fallback().into_view(cx)
}
@ -127,12 +126,12 @@ where
else {
let key = cx.current_fragment_key();
cx.register_suspense(context, &key, move || {
render_to_string(move |cx| orig_child())
orig_child().into_view(cx).render_to_string(cx).to_string()
});
// return the fallback for now, wrapped in fragment identifer
div(cx)
.attr("data-fragment", key)
.id(key.to_string())
.child(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);
@ -162,12 +162,12 @@ where
else {
let key = cx.current_fragment_key();
cx.register_suspense(context, &key, move || {
render_to_string(move |cx| orig_child())
orig_child().into_view(cx).render_to_string(cx).to_string()
});
// return the fallback for now, wrapped in fragment identifer
div(cx)
.attr("data-fragment", key)
.id(key.to_string())
.child(move || fallback())
.into_view(cx)
}

View file

@ -1,3 +1,5 @@
use leptos_reactive::Scope;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use std::cell::LazyCell;
@ -32,11 +34,27 @@ impl HydrationCtx {
}
}
pub(crate) fn current_id() -> usize {
unsafe { ID }
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) fn reset_id() {
println!("resetting ID");
unsafe { ID = 0 };
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) fn set_id(cx: Scope) {
let new_id = if let Some(id) = cx.get_hydration_key() {
id + 1
} else {
0
};
println!("setting ID to {new_id}");
unsafe { ID = new_id };
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn stop_hydrating() {
unsafe {

View file

@ -23,15 +23,12 @@ pub fn render_to_string<F, N>(f: F) -> String
where
F: FnOnce(Scope) -> N + 'static,
N: IntoView,
{
let runtime = leptos_reactive::create_runtime();
let view = leptos_reactive::run_scope(runtime, |cx| f(cx).into_view(cx));
HydrationCtx::reset_id();
runtime.dispose();
view.render_to_string().into_owned()
{
let runtime = leptos_reactive::create_runtime();
HydrationCtx::reset_id();
let html = leptos_reactive::run_scope(runtime, |cx| f(cx).into_view(cx).render_to_string(cx));
runtime.dispose();
html.into_owned()
}
/// Renders a function to a stream of HTML strings.
@ -69,7 +66,7 @@ pub fn render_to_stream_with_prefix(
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static
) -> impl Stream<Item = String> {
HydrationCtx::reset_id();
// create the runtime
let runtime = create_runtime();
@ -78,7 +75,7 @@ pub fn render_to_stream_with_prefix(
move |cx| {
// the actual app body/template code
// this does NOT contain any of the data being loaded asynchronously in resources
let shell = view(cx).render_to_string();
let shell = view(cx).render_to_string(cx);
let resources = cx.all_resources();
let pending_resources = serde_json::to_string(&resources).unwrap();
@ -94,40 +91,39 @@ pub fn render_to_stream_with_prefix(
}
});
let fragments = FuturesUnordered::new();
for (fragment_id, fut) in pending_fragments {
fragments.push(async move { (fragment_id, fut.await) })
}
let fragments = FuturesUnordered::new();
for (fragment_id, fut) in pending_fragments {
fragments.push(async move { (fragment_id, fut.await) })
}
// resources and fragments
let resources_and_fragments = futures::stream::select(
// stream data for each Resource as it resolves
serializers.map(|(id, json)| {
let id = serde_json::to_string(&id).unwrap();
format!(
r#"<script>
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
__LEPTOS_RESOURCE_RESOLVERS.get({id})({json:?})
}} else {{
__LEPTOS_RESOLVED_RESOURCES.set({id}, {json:?});
}}
</script>"#,
)
}),
// stream HTML for each <Suspense/> as it resolves
fragments.map(|(fragment_id, html)| {
format!(
r#"
<template id="{fragment_id}">{html}</template>
<script>
var frag = document.querySelector(`[data-fragment-id="{fragment_id}"]`);
var tpl = document.getElementById("{fragment_id}");
if(frag) frag.replaceWith(tpl.content.cloneNode(true));
</script>
"#
)
})
);
// stream HTML for each <Suspense/> as it resolves
let fragments = fragments.map(|(fragment_id, html)| {
format!(
r#"
<template id="{fragment_id}f">{html}</template>
<script>
var frag = document.getElementById("{fragment_id}");
var tpl = document.getElementById("{fragment_id}f");
if(frag) frag.replaceWith(tpl.content.cloneNode(true));
</script>
"#
)
});
// stream data for each Resource as it resolves
let resources =
serializers.map(|(id, json)| {
let id = serde_json::to_string(&id).unwrap();
format!(
r#"<script>
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
__LEPTOS_RESOURCE_RESOLVERS.get({id})({json:?})
}} else {{
__LEPTOS_RESOLVED_RESOURCES.set({id}, {json:?});
}}
</script>"#,
)
});
// HTML for the view function and script to store resources
futures::stream::once(async move {
@ -143,7 +139,10 @@ pub fn render_to_stream_with_prefix(
"#
)
})
.chain(resources_and_fragments)
// TODO these should be combined again in a way that chains them appropriately
// such that individual resources can resolve before all fragments are done
.chain(fragments)
.chain(resources)
// dispose of Scope and Runtime
.chain(futures::stream::once(async move {
disposer.dispose();
@ -154,14 +153,25 @@ pub fn render_to_stream_with_prefix(
impl View {
/// Consumes the node and renders it into an HTML string.
pub fn render_to_string(self) -> Cow<'static, str> {
pub fn render_to_string(self, cx: Scope) -> Cow<'static, str> {
cx.set_hydration_key(HydrationCtx::current_id());
HydrationCtx::set_id(cx);
let s = self.render_to_string_helper();
cx.set_hydration_key(HydrationCtx::current_id());
s
}
pub(crate) fn render_to_string_helper(self) -> Cow<'static, str> {
match self {
View::Text(node) => node.content,
View::Component(node) => {
let content = node
.children
.into_iter()
.map(|node| node.render_to_string())
.map(|node| node.render_to_string_helper())
.join("");
cfg_if! {
if #[cfg(debug_assertions)] {
@ -208,7 +218,7 @@ impl View {
t.content
}
} else {
child.render_to_string()
child.render_to_string_helper()
}
} else {
"".into()
@ -227,7 +237,7 @@ impl View {
.map(|node| {
let id = node.id;
let content = node.child.render_to_string();
let content = node.child.render_to_string_helper();
#[cfg(debug_assertions)]
return format!(
@ -293,7 +303,7 @@ impl View {
let children = el
.children
.into_iter()
.map(|node| node.render_to_string())
.map(|node| node.render_to_string_helper())
.join("");
format!("<{tag_name}{attrs}>{children}</{tag_name}>").into()

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

@ -9,7 +9,7 @@ use std::{
pub struct SharedContext {
pub completed: Vec<web_sys::Element>,
pub events: Vec<()>,
pub context: Option<HydrationContext>,
pub previous_hydration_key: Option<usize>,
pub registry: HashMap<String, web_sys::Element>,
pub pending_resources: HashSet<ResourceId>,
pub resolved_resources: HashMap<ResourceId, String>,
@ -26,7 +26,7 @@ impl PartialEq for SharedContext {
fn eq(&self, other: &Self) -> bool {
self.completed == other.completed
&& self.events == other.events
&& self.context == other.context
&& self.previous_hydration_key == other.previous_hydration_key
&& self.registry == other.registry
&& self.pending_resources == other.pending_resources
&& self.resolved_resources == other.resolved_resources
@ -59,10 +59,7 @@ impl SharedContext {
Self {
completed: Default::default(),
events: Default::default(),
context: Some(HydrationContext {
id: "".into(),
count: -1,
}),
previous_hydration_key: None,
registry,
pending_resources,
resolved_resources,
@ -70,41 +67,11 @@ impl SharedContext {
}
}
pub fn next_hydration_key(&mut self) -> String {
if let Some(context) = &mut self.context {
let k = format!("{}{}", context.id, context.count);
context.count += 1;
k
} else {
self.context = Some(HydrationContext {
id: "0-".into(),
count: 1,
});
"0-0".into()
}
}
pub fn current_fragment_key(&self) -> String {
if let Some(context) = &self.context {
format!("{}{}f", context.id, context.count)
if let Some(id) = &self.previous_hydration_key {
format!("{}f", id)
} else {
"0f".to_string()
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct HydrationContext {
id: String,
count: i32,
}
impl HydrationContext {
pub fn next_hydration_context(&mut self) -> HydrationContext {
self.count += 1;
HydrationContext {
id: format!("{}{}-", self.id, self.count),
count: 0,
}
}
}
}

View file

@ -266,60 +266,56 @@ where
use wasm_bindgen::{JsCast, UnwrapThrowExt};
with_runtime(cx.runtime, |runtime| {
if let Some(ref mut context) = *runtime.shared_context.borrow_mut() {
if let Some(data) = context.resolved_resources.remove(&id) {
// The server already sent us the serialized resource value, so
// deserialize & set it now
context.pending_resources.remove(&id); // no longer pending
r.resolved.set(true);
let mut context = runtime.shared_context.borrow_mut();
if let Some(data) = context.resolved_resources.remove(&id) {
// The server already sent us the serialized resource value, so
// deserialize & set it now
context.pending_resources.remove(&id); // no longer pending
r.resolved.set(true);
let res = T::from_json(&data).expect_throw("could not deserialize Resource JSON");
r.set_value.update(|n| *n = Some(res));
r.set_loading.update(|n| *n = false);
let res = T::from_json(&data).expect_throw("could not deserialize Resource JSON");
r.set_value.update(|n| *n = Some(res));
r.set_loading.update(|n| *n = false);
// for reactivity
r.source.subscribe();
} else if context.pending_resources.remove(&id) {
// We're still waiting for the resource, add a "resolver" closure so
// that it will be set as soon as the server sends the serialized
// value
r.set_loading.update(|n| *n = true);
// for reactivity
r.source.subscribe();
} else if context.pending_resources.remove(&id) {
// We're still waiting for the resource, add a "resolver" closure so
// that it will be set as soon as the server sends the serialized
// value
r.set_loading.update(|n| *n = true);
let resolve = {
let resolved = r.resolved.clone();
let set_value = r.set_value;
let set_loading = r.set_loading;
move |res: String| {
let res =
T::from_json(&res).expect_throw("could not deserialize Resource JSON");
resolved.set(true);
set_value.update(|n| *n = Some(res));
set_loading.update(|n| *n = false);
}
};
let resolve =
wasm_bindgen::closure::Closure::wrap(Box::new(resolve) as Box<dyn Fn(String)>);
let resource_resolvers = js_sys::Reflect::get(
&web_sys::window().unwrap(),
&wasm_bindgen::JsValue::from_str("__LEPTOS_RESOURCE_RESOLVERS"),
)
.expect_throw("no __LEPTOS_RESOURCE_RESOLVERS found in the JS global scope");
let id = serde_json::to_string(&id).expect_throw("could not serialize Resource ID");
_ = js_sys::Reflect::set(
&resource_resolvers,
&wasm_bindgen::JsValue::from_str(&id),
resolve.as_ref().unchecked_ref(),
);
let resolve = {
let resolved = r.resolved.clone();
let set_value = r.set_value;
let set_loading = r.set_loading;
move |res: String| {
let res =
T::from_json(&res).expect_throw("could not deserialize Resource JSON");
resolved.set(true);
set_value.update(|n| *n = Some(res));
set_loading.update(|n| *n = false);
}
};
let resolve =
wasm_bindgen::closure::Closure::wrap(Box::new(resolve) as Box<dyn Fn(String)>);
let resource_resolvers = js_sys::Reflect::get(
&web_sys::window().unwrap(),
&wasm_bindgen::JsValue::from_str("__LEPTOS_RESOURCE_RESOLVERS"),
)
.expect_throw("no __LEPTOS_RESOURCE_RESOLVERS found in the JS global scope");
let id = serde_json::to_string(&id).expect_throw("could not serialize Resource ID");
_ = js_sys::Reflect::set(
&resource_resolvers,
&wasm_bindgen::JsValue::from_str(&id),
resolve.as_ref().unchecked_ref(),
);
// for reactivity
r.source.subscribe()
} else {
// Server didn't mark the resource as pending, so load it on the
// client
r.load(false);
}
// for reactivity
r.source.subscribe()
} else {
r.load(false)
// Server didn't mark the resource as pending, so load it on the client
r.load(false);
}
})
}

View file

@ -192,7 +192,7 @@ impl RuntimeId {
#[derive(Default)]
pub(crate) struct Runtime {
pub shared_context: RefCell<Option<SharedContext>>,
pub shared_context: RefCell<SharedContext>,
pub observer: Cell<Option<EffectId>>,
pub scopes: RefCell<SlotMap<ScopeId, RefCell<Vec<ScopeProperty>>>>,
pub scope_parents: RefCell<SparseSecondaryMap<ScopeId, ScopeId>>,
@ -206,7 +206,6 @@ pub(crate) struct Runtime {
pub effects: RefCell<SlotMap<EffectId, Rc<dyn AnyEffect>>>,
pub effect_sources: RefCell<SecondaryMap<EffectId, RefCell<HashSet<SignalId>>>>,
pub resources: RefCell<SlotMap<ResourceId, AnyResource>>,
pub hydration_id: Cell<usize>,
}
impl Debug for Runtime {
@ -256,37 +255,14 @@ impl Runtime {
.insert(AnyResource::Serializable(state))
}
#[cfg(feature = "hydrate")]
pub fn start_hydration(&self, element: &web_sys::Element) {
use wasm_bindgen::{JsCast, UnwrapThrowExt};
// gather hydratable elements
let mut registry = HashMap::new();
if let Ok(templates) = element.query_selector_all("*[data-hk]") {
for i in 0..templates.length() {
let node = templates
.item(i)
.unwrap_throw() // ok to unwrap; we already have the index, so this can't fail
.unchecked_into::<web_sys::Element>();
let key = node.get_attribute("data-hk").unwrap_throw();
registry.insert(key, node);
}
}
*self.shared_context.borrow_mut() = Some(SharedContext::new_with_registry(registry));
pub fn set_hydration_key(&self, id: usize) {
let mut sc = self.shared_context.borrow_mut();
sc.previous_hydration_key = Some(id);
}
#[cfg(feature = "hydrate")]
pub fn end_hydration(&self) {
if let Some(ref mut sc) = *self.shared_context.borrow_mut() {
sc.context = None;
}
}
pub fn hydration_id(&self) -> usize {
let id = self.hydration_id.get();
self.hydration_id.set(id + 1);
id
pub fn get_hydration_key(&self) -> Option<usize> {
let sc = &self.shared_context.borrow();
sc.previous_hydration_key
}
pub(crate) fn resource<S, T, U>(

View file

@ -1,7 +1,4 @@
use cfg_if::cfg_if;
use crate::{
hydration::SharedContext,
runtime::{with_runtime, RuntimeId},
EffectId, PinnedFuture, ResourceId, SignalId, SuspenseContext,
};
@ -278,182 +275,11 @@ impl ScopeDisposer {
}
impl Scope {
// hydration-specific code
cfg_if! {
if #[cfg(any(feature = "hydrate", doc))] {
/// `hydrate` only: Whether we're currently hydrating the page.
pub fn is_hydrating(&self) -> bool {
with_runtime(self.runtime, |runtime| {
runtime.shared_context.borrow().is_some()
})
}
/// `hydrate` only: Begins the hydration process.
pub fn start_hydration(&self, element: &web_sys::Element) {
with_runtime(self.runtime, |runtime| {
runtime.start_hydration(element);
})
}
/// `hydrate` only: Ends the hydration process.
pub fn end_hydration(&self) {
with_runtime(self.runtime, |runtime| {
runtime.end_hydration();
})
}
/// `hydrate` only: Gets the next element in the hydration queue, either from the
/// server-rendered DOM or from the template.
pub fn get_next_element(&self, template: &web_sys::Element) -> web_sys::Element {
use wasm_bindgen::{JsCast, UnwrapThrowExt};
let cloned_template = |t: &web_sys::Element| {
let t = t
.unchecked_ref::<web_sys::HtmlTemplateElement>()
.content()
.clone_node_with_deep(true)
.expect_throw("(get_next_element) could not clone template")
.unchecked_into::<web_sys::Element>()
.first_element_child()
.expect_throw("(get_next_element) could not get first child of template");
t
};
with_runtime(self.runtime, |runtime| {
if let Some(ref mut shared_context) = &mut *runtime.shared_context.borrow_mut() {
if shared_context.context.is_some() {
let key = shared_context.next_hydration_key();
let node = shared_context.registry.remove(&key);
//log::debug!("(hy) searching for {key}");
if let Some(node) = node {
//log::debug!("(hy) found {key}");
shared_context.completed.push(node.clone());
node
} else {
//log::debug!("(hy) did NOT find {key}");
cloned_template(template)
}
} else {
cloned_template(template)
}
} else {
cloned_template(template)
}
})
}
}
}
/// `hydrate` only: Given the current node, gets the span of the next component that has
/// been marked for hydration, returning its starting node and the set of all its nodes.
#[cfg(any(feature = "csr", feature = "hydrate", doc))]
pub fn get_next_marker(&self, start: &web_sys::Node) -> (web_sys::Node, Vec<web_sys::Node>) {
let mut end = Some(start.clone());
let mut count = 0;
let mut current = Vec::new();
let mut start = start.clone();
with_runtime(self.runtime, |runtime| {
if runtime
.shared_context
.borrow()
.as_ref()
.map(|sc| sc.context.as_ref())
.is_some()
{
while let Some(curr) = end {
start = curr.clone();
if curr.node_type() == 8 {
// COMMENT
let v = curr.node_value();
if v == Some("#".to_string()) {
count += 1;
} else if v == Some("/".to_string()) {
count -= 1;
if count == 0 {
current.push(curr.clone());
return (curr, current);
}
}
}
current.push(curr.clone());
end = curr.next_sibling();
}
}
(start, current)
})
}
/// On either the server side or the browser side, generates the next key in the hydration process.
pub fn next_hydration_key(&self) -> String {
with_runtime(self.runtime, |runtime| {
let mut sc = runtime.shared_context.borrow_mut();
if let Some(ref mut sc) = *sc {
sc.next_hydration_key()
} else {
let mut new_sc = SharedContext::default();
let id = new_sc.next_hydration_key();
*sc = Some(new_sc);
id
}
})
}
/// Runs the given function with the next hydration context.
pub fn with_next_context<T>(&self, f: impl FnOnce() -> T) -> T {
with_runtime(self.runtime, |runtime| {
if runtime
.shared_context
.borrow()
.as_ref()
.and_then(|sc| sc.context.as_ref())
.is_some()
{
let c = {
if let Some(ref mut sc) = *runtime.shared_context.borrow_mut() {
if let Some(ref mut context) = sc.context {
let next = context.next_hydration_context();
Some(std::mem::replace(context, next))
} else {
None
}
} else {
None
}
};
let res = self.untrack(f);
if let Some(ref mut sc) = *runtime.shared_context.borrow_mut() {
sc.context = c;
}
res
} else {
self.untrack(f)
}
})
}
/// Returns IDs for all [Resource](crate::Resource)s found on any scope.
pub fn all_resources(&self) -> Vec<ResourceId> {
with_runtime(self.runtime, |runtime| runtime.all_resources())
}
/// The current key for an HTML fragment created by server-rendering a `<Suspense/>` component.
pub fn current_fragment_key(&self) -> String {
with_runtime(self.runtime, |runtime| {
runtime
.shared_context
.borrow()
.as_ref()
.map(|context| context.current_fragment_key())
.unwrap_or_else(|| String::from("0f"))
})
}
/// Returns IDs for all [Resource](crate::Resource)s found on any scope.
pub fn serialization_resolvers(&self) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
with_runtime(self.runtime, |runtime| runtime.serialization_resolvers())
@ -471,41 +297,52 @@ impl Scope {
use futures::StreamExt;
with_runtime(self.runtime, |runtime| {
if let Some(ref mut shared_context) = *runtime.shared_context.borrow_mut() {
let (tx, mut rx) = futures::channel::mpsc::unbounded();
let mut shared_context = runtime.shared_context.borrow_mut();
let (tx, mut rx) = futures::channel::mpsc::unbounded();
create_isomorphic_effect(*self, move |_| {
let pending = context.pending_resources.try_with(|n| *n).unwrap_or(0);
if pending == 0 {
_ = tx.unbounded_send(());
}
});
create_isomorphic_effect(*self, move |_| {
let pending = context.pending_resources.try_with(|n| *n).unwrap_or(0);
if pending == 0 {
_ = tx.unbounded_send(());
}
});
shared_context.pending_fragments.insert(
key.to_string(),
Box::pin(async move {
rx.next().await;
resolver()
}),
);
}
shared_context.pending_fragments.insert(
key.to_string(),
Box::pin(async move {
rx.next().await;
resolver()
}),
);
})
}
/// The set of all HTML fragments current pending, by their keys (see [Self::current_fragment_key]).
pub fn pending_fragments(&self) -> HashMap<String, Pin<Box<dyn Future<Output = String>>>> {
with_runtime(self.runtime, |runtime| {
if let Some(ref mut shared_context) = *runtime.shared_context.borrow_mut() {
std::mem::take(&mut shared_context.pending_fragments)
} else {
HashMap::new()
}
let mut shared_context = runtime.shared_context.borrow_mut();
std::mem::take(&mut shared_context.pending_fragments)
})
}
/// Returns the hydration key for the next element.
pub fn hydration_id(&self) -> HydrationKey {
with_runtime(self.runtime, |runtime| HydrationKey(runtime.hydration_id()))
/// Sets the latest hydration key.
pub fn set_hydration_key(&self, id: usize) {
with_runtime(self.runtime, |runtime| runtime.set_hydration_key(id))
}
/// Gets the latest hydration key.
pub fn get_hydration_key(&self) -> Option<usize> {
with_runtime(self.runtime, |runtime| runtime.get_hydration_key())
}
/// The current key for an HTML fragment created by server-rendering a `<Suspense/>` component.
pub fn current_fragment_key(&self) -> String {
with_runtime(self.runtime, |runtime| {
runtime
.shared_context
.borrow()
.current_fragment_key()
})
}
}

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