mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
Dont shift on normal props updates (#2705)
* don't shift on normal props updates would remove focus from elements and fire other dom mutation events we don't need or want * rename mode -> creation_mode it's not updated when the component finishes hydration * rename node_ref to internal_ref for consistency * fix the bug, should return internal_ref as node_ref gets unset during reconciliation * also no need to shift during hydration props update * debug: encode hydration invariant * fix: next_sibling of descendents not updated * add test case for regression * address review and add one more test
This commit is contained in:
parent
7dc7195da8
commit
cb5a609e08
@ -88,8 +88,10 @@ mod feat_hydration {
|
|||||||
host.clone(),
|
host.clone(),
|
||||||
&mut fragment,
|
&mut fragment,
|
||||||
NodeRef::default(),
|
NodeRef::default(),
|
||||||
props,
|
Rc::clone(&props),
|
||||||
);
|
);
|
||||||
|
#[cfg(debug_assertions)] // Fix trapped next_sibling at the root
|
||||||
|
app.scope.reuse(props, NodeRef::default());
|
||||||
|
|
||||||
// We remove all remaining nodes, this mimics the clear_element behaviour in
|
// We remove all remaining nodes, this mimics the clear_element behaviour in
|
||||||
// mount_with_props.
|
// mount_with_props.
|
||||||
|
|||||||
@ -47,7 +47,7 @@ impl ReconcileTarget for BComp {
|
|||||||
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) -> NodeRef {
|
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) -> NodeRef {
|
||||||
self.scope.shift_node(next_parent.clone(), next_sibling);
|
self.scope.shift_node(next_parent.clone(), next_sibling);
|
||||||
|
|
||||||
self.node_ref.clone()
|
self.internal_ref.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,9 +72,9 @@ impl Reconcilable for VComp {
|
|||||||
|
|
||||||
let scope = mountable.mount(
|
let scope = mountable.mount(
|
||||||
root,
|
root,
|
||||||
internal_ref.clone(),
|
|
||||||
parent_scope,
|
parent_scope,
|
||||||
parent.to_owned(),
|
parent.to_owned(),
|
||||||
|
internal_ref.clone(),
|
||||||
next_sibling,
|
next_sibling,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -158,8 +158,8 @@ mod feat_hydration {
|
|||||||
root.clone(),
|
root.clone(),
|
||||||
parent_scope,
|
parent_scope,
|
||||||
parent.clone(),
|
parent.clone(),
|
||||||
fragment,
|
|
||||||
internal_ref.clone(),
|
internal_ref.clone(),
|
||||||
|
fragment,
|
||||||
);
|
);
|
||||||
|
|
||||||
(
|
(
|
||||||
|
|||||||
@ -174,14 +174,10 @@ impl Reconcilable for VSuspense {
|
|||||||
vfallback.reconcile_node(root, parent_scope, parent, next_sibling, bundle)
|
vfallback.reconcile_node(root, parent_scope, parent, next_sibling, bundle)
|
||||||
}
|
}
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
Fallback::Fragment(fragment) => {
|
Fallback::Fragment(fragment) => match fragment.front().cloned() {
|
||||||
let node_ref = NodeRef::default();
|
Some(m) => NodeRef::new(m),
|
||||||
match fragment.front().cloned() {
|
None => next_sibling,
|
||||||
Some(m) => node_ref.set(Some(m)),
|
},
|
||||||
None => node_ref.link(next_sibling),
|
|
||||||
}
|
|
||||||
node_ref
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Not suspended, just reconcile the children into the DOM
|
// Not suspended, just reconcile the children into the DOM
|
||||||
@ -256,6 +252,8 @@ mod feat_hydration {
|
|||||||
detached_parent.append_child(node).unwrap();
|
detached_parent.append_child(node).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Even if initially suspended, these children correspond to the first non-suspended
|
||||||
|
// content Refer to VSuspense::render_to_string
|
||||||
let (_, children_bundle) =
|
let (_, children_bundle) =
|
||||||
self.children
|
self.children
|
||||||
.hydrate(root, parent_scope, &detached_parent, &mut nodes);
|
.hydrate(root, parent_scope, &detached_parent, &mut nodes);
|
||||||
|
|||||||
@ -40,7 +40,7 @@ where
|
|||||||
type Output = SuspensionResult<Option<Rc<T>>>;
|
type Output = SuspensionResult<Option<Rc<T>>>;
|
||||||
|
|
||||||
fn run(self, ctx: &mut HookContext) -> Self::Output {
|
fn run(self, ctx: &mut HookContext) -> Self::Output {
|
||||||
match ctx.mode {
|
match ctx.creation_mode {
|
||||||
RenderMode::Ssr => feat_ssr::use_prepared_state(self.f, self.deps).run(ctx),
|
RenderMode::Ssr => feat_ssr::use_prepared_state(self.f, self.deps).run(ctx),
|
||||||
_ => feat_hydration::use_prepared_state(self.deps).run(ctx),
|
_ => feat_hydration::use_prepared_state(self.deps).run(ctx),
|
||||||
}
|
}
|
||||||
@ -82,7 +82,7 @@ where
|
|||||||
type Output = SuspensionResult<Option<Rc<T>>>;
|
type Output = SuspensionResult<Option<Rc<T>>>;
|
||||||
|
|
||||||
fn run(self, ctx: &mut HookContext) -> Self::Output {
|
fn run(self, ctx: &mut HookContext) -> Self::Output {
|
||||||
match ctx.mode {
|
match ctx.creation_mode {
|
||||||
RenderMode::Ssr => {
|
RenderMode::Ssr => {
|
||||||
feat_ssr::use_prepared_state_with_suspension(self.f, self.deps).run(ctx)
|
feat_ssr::use_prepared_state_with_suspension(self.f, self.deps).run(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,7 @@ where
|
|||||||
type Output = SuspensionResult<Option<Rc<T>>>;
|
type Output = SuspensionResult<Option<Rc<T>>>;
|
||||||
|
|
||||||
fn run(self, ctx: &mut HookContext) -> Self::Output {
|
fn run(self, ctx: &mut HookContext) -> Self::Output {
|
||||||
match ctx.mode {
|
match ctx.creation_mode {
|
||||||
RenderMode::Ssr => feat_ssr::use_transitive_state(self.f, self.deps).run(ctx),
|
RenderMode::Ssr => feat_ssr::use_transitive_state(self.f, self.deps).run(ctx),
|
||||||
_ => feat_hydration::use_transitive_state(self.deps).run(ctx),
|
_ => feat_hydration::use_transitive_state(self.deps).run(ctx),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -85,7 +85,7 @@ pub struct HookContext {
|
|||||||
pub(crate) scope: AnyScope,
|
pub(crate) scope: AnyScope,
|
||||||
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
|
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
|
||||||
#[cfg(all(feature = "hydration", feature = "ssr"))]
|
#[cfg(all(feature = "hydration", feature = "ssr"))]
|
||||||
mode: RenderMode,
|
creation_mode: RenderMode,
|
||||||
re_render: ReRender,
|
re_render: ReRender,
|
||||||
|
|
||||||
states: Vec<Rc<dyn Any>>,
|
states: Vec<Rc<dyn Any>>,
|
||||||
@ -113,7 +113,7 @@ impl HookContext {
|
|||||||
re_render: ReRender,
|
re_render: ReRender,
|
||||||
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
|
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
|
||||||
#[cfg(all(feature = "hydration", feature = "ssr"))]
|
#[cfg(all(feature = "hydration", feature = "ssr"))]
|
||||||
mode: RenderMode,
|
creation_mode: RenderMode,
|
||||||
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
|
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
prepared_state: Option<&str>,
|
prepared_state: Option<&str>,
|
||||||
@ -124,7 +124,7 @@ impl HookContext {
|
|||||||
|
|
||||||
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
|
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
|
||||||
#[cfg(all(feature = "hydration", feature = "ssr"))]
|
#[cfg(all(feature = "hydration", feature = "ssr"))]
|
||||||
mode,
|
creation_mode,
|
||||||
|
|
||||||
states: Vec::new(),
|
states: Vec::new(),
|
||||||
|
|
||||||
@ -363,7 +363,7 @@ where
|
|||||||
re_render,
|
re_render,
|
||||||
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
|
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
|
||||||
#[cfg(all(feature = "hydration", feature = "ssr"))]
|
#[cfg(all(feature = "hydration", feature = "ssr"))]
|
||||||
ctx.mode(),
|
ctx.creation_mode(),
|
||||||
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
|
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
ctx.prepared_state(),
|
ctx.prepared_state(),
|
||||||
|
|||||||
@ -28,15 +28,15 @@ pub(crate) enum ComponentRenderState {
|
|||||||
root: BSubtree,
|
root: BSubtree,
|
||||||
parent: Element,
|
parent: Element,
|
||||||
next_sibling: NodeRef,
|
next_sibling: NodeRef,
|
||||||
node_ref: NodeRef,
|
internal_ref: NodeRef,
|
||||||
},
|
},
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
Hydration {
|
Hydration {
|
||||||
|
fragment: Fragment,
|
||||||
|
root: BSubtree,
|
||||||
parent: Element,
|
parent: Element,
|
||||||
next_sibling: NodeRef,
|
next_sibling: NodeRef,
|
||||||
node_ref: NodeRef,
|
internal_ref: NodeRef,
|
||||||
root: BSubtree,
|
|
||||||
fragment: Fragment,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
@ -54,14 +54,14 @@ impl std::fmt::Debug for ComponentRenderState {
|
|||||||
root,
|
root,
|
||||||
ref parent,
|
ref parent,
|
||||||
ref next_sibling,
|
ref next_sibling,
|
||||||
ref node_ref,
|
ref internal_ref,
|
||||||
} => f
|
} => f
|
||||||
.debug_struct("ComponentRenderState::Render")
|
.debug_struct("ComponentRenderState::Render")
|
||||||
.field("bundle", bundle)
|
.field("bundle", bundle)
|
||||||
.field("root", root)
|
.field("root", root)
|
||||||
.field("parent", parent)
|
.field("parent", parent)
|
||||||
.field("next_sibling", next_sibling)
|
.field("next_sibling", next_sibling)
|
||||||
.field("node_ref", node_ref)
|
.field("internal_ref", internal_ref)
|
||||||
.finish(),
|
.finish(),
|
||||||
|
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
@ -69,7 +69,7 @@ impl std::fmt::Debug for ComponentRenderState {
|
|||||||
ref fragment,
|
ref fragment,
|
||||||
ref parent,
|
ref parent,
|
||||||
ref next_sibling,
|
ref next_sibling,
|
||||||
ref node_ref,
|
ref internal_ref,
|
||||||
ref root,
|
ref root,
|
||||||
} => f
|
} => f
|
||||||
.debug_struct("ComponentRenderState::Hydration")
|
.debug_struct("ComponentRenderState::Hydration")
|
||||||
@ -77,7 +77,7 @@ impl std::fmt::Debug for ComponentRenderState {
|
|||||||
.field("root", root)
|
.field("root", root)
|
||||||
.field("parent", parent)
|
.field("parent", parent)
|
||||||
.field("next_sibling", next_sibling)
|
.field("next_sibling", next_sibling)
|
||||||
.field("node_ref", node_ref)
|
.field("internal_ref", internal_ref)
|
||||||
.finish(),
|
.finish(),
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
@ -109,7 +109,7 @@ impl ComponentRenderState {
|
|||||||
bundle.shift(&next_parent, next_next_sibling.clone());
|
bundle.shift(&next_parent, next_next_sibling.clone());
|
||||||
|
|
||||||
*parent = next_parent;
|
*parent = next_parent;
|
||||||
*next_sibling = next_next_sibling;
|
next_sibling.link(next_next_sibling);
|
||||||
}
|
}
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
Self::Hydration {
|
Self::Hydration {
|
||||||
@ -121,7 +121,7 @@ impl ComponentRenderState {
|
|||||||
fragment.shift(&next_parent, next_next_sibling.clone());
|
fragment.shift(&next_parent, next_next_sibling.clone());
|
||||||
|
|
||||||
*parent = next_parent;
|
*parent = next_parent;
|
||||||
*next_sibling = next_next_sibling;
|
next_sibling.link(next_next_sibling);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
@ -160,7 +160,7 @@ pub(crate) trait Stateful {
|
|||||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||||
|
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
fn mode(&self) -> RenderMode;
|
fn creation_mode(&self) -> RenderMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<COMP> Stateful for CompStateInner<COMP>
|
impl<COMP> Stateful for CompStateInner<COMP>
|
||||||
@ -184,8 +184,8 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
fn mode(&self) -> RenderMode {
|
fn creation_mode(&self) -> RenderMode {
|
||||||
self.context.mode
|
self.context.creation_mode()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flush_messages(&mut self) -> bool {
|
fn flush_messages(&mut self) -> bool {
|
||||||
@ -246,7 +246,7 @@ impl ComponentState {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
let comp_id = scope.id;
|
let comp_id = scope.id;
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
let mode = {
|
let creation_mode = {
|
||||||
match initial_render_state {
|
match initial_render_state {
|
||||||
ComponentRenderState::Render { .. } => RenderMode::Render,
|
ComponentRenderState::Render { .. } => RenderMode::Render,
|
||||||
ComponentRenderState::Hydration { .. } => RenderMode::Hydration,
|
ComponentRenderState::Hydration { .. } => RenderMode::Hydration,
|
||||||
@ -259,7 +259,7 @@ impl ComponentState {
|
|||||||
scope,
|
scope,
|
||||||
props,
|
props,
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
mode,
|
creation_mode,
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
prepared_state,
|
prepared_state,
|
||||||
};
|
};
|
||||||
@ -344,24 +344,18 @@ impl Runnable for PropsUpdateRunner {
|
|||||||
match state.render_state {
|
match state.render_state {
|
||||||
#[cfg(feature = "csr")]
|
#[cfg(feature = "csr")]
|
||||||
ComponentRenderState::Render {
|
ComponentRenderState::Render {
|
||||||
next_sibling: ref mut current_next_sibling,
|
next_sibling: ref current_next_sibling,
|
||||||
ref parent,
|
|
||||||
ref bundle,
|
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
bundle.shift(parent, next_sibling.clone());
|
current_next_sibling.link(next_sibling);
|
||||||
*current_next_sibling = next_sibling;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
ComponentRenderState::Hydration {
|
ComponentRenderState::Hydration {
|
||||||
next_sibling: ref mut current_next_sibling,
|
next_sibling: ref current_next_sibling,
|
||||||
ref parent,
|
|
||||||
ref fragment,
|
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
fragment.shift(parent, next_sibling.clone());
|
current_next_sibling.link(next_sibling);
|
||||||
*current_next_sibling = next_sibling;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
@ -399,7 +393,7 @@ impl Runnable for PropsUpdateRunner {
|
|||||||
let schedule_render = {
|
let schedule_render = {
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
{
|
{
|
||||||
if state.inner.mode() == RenderMode::Hydration {
|
if state.inner.creation_mode() == RenderMode::Hydration {
|
||||||
should_render_hydration(props, state)
|
should_render_hydration(props, state)
|
||||||
} else {
|
} else {
|
||||||
should_render(props, state)
|
should_render(props, state)
|
||||||
@ -478,13 +472,13 @@ impl Runnable for DestroyRunner {
|
|||||||
ComponentRenderState::Render {
|
ComponentRenderState::Render {
|
||||||
bundle,
|
bundle,
|
||||||
ref parent,
|
ref parent,
|
||||||
ref node_ref,
|
ref internal_ref,
|
||||||
ref root,
|
ref root,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
bundle.detach(root, parent, self.parent_to_detach);
|
bundle.detach(root, parent, self.parent_to_detach);
|
||||||
|
|
||||||
node_ref.set(None);
|
internal_ref.set(None);
|
||||||
}
|
}
|
||||||
// We need to detach the hydrate fragment if the component is not hydrated.
|
// We need to detach the hydrate fragment if the component is not hydrated.
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
@ -492,12 +486,12 @@ impl Runnable for DestroyRunner {
|
|||||||
ref root,
|
ref root,
|
||||||
fragment,
|
fragment,
|
||||||
ref parent,
|
ref parent,
|
||||||
ref node_ref,
|
ref internal_ref,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
fragment.detach(root, parent, self.parent_to_detach);
|
fragment.detach(root, parent, self.parent_to_detach);
|
||||||
|
|
||||||
node_ref.set(None);
|
internal_ref.set(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
@ -593,14 +587,17 @@ impl RenderRunner {
|
|||||||
ref parent,
|
ref parent,
|
||||||
ref root,
|
ref root,
|
||||||
ref next_sibling,
|
ref next_sibling,
|
||||||
ref node_ref,
|
ref internal_ref,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let scope = state.inner.any_scope();
|
let scope = state.inner.any_scope();
|
||||||
|
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
next_sibling.debug_assert_not_trapped();
|
||||||
|
|
||||||
let new_node_ref =
|
let new_node_ref =
|
||||||
bundle.reconcile(root, &scope, parent, next_sibling.clone(), new_root);
|
bundle.reconcile(root, &scope, parent, next_sibling.clone(), new_root);
|
||||||
node_ref.link(new_node_ref);
|
internal_ref.link(new_node_ref);
|
||||||
|
|
||||||
let first_render = !state.has_rendered;
|
let first_render = !state.has_rendered;
|
||||||
state.has_rendered = true;
|
state.has_rendered = true;
|
||||||
@ -619,7 +616,7 @@ impl RenderRunner {
|
|||||||
ComponentRenderState::Hydration {
|
ComponentRenderState::Hydration {
|
||||||
ref mut fragment,
|
ref mut fragment,
|
||||||
ref parent,
|
ref parent,
|
||||||
ref node_ref,
|
ref internal_ref,
|
||||||
ref next_sibling,
|
ref next_sibling,
|
||||||
ref root,
|
ref root,
|
||||||
} => {
|
} => {
|
||||||
@ -644,13 +641,13 @@ impl RenderRunner {
|
|||||||
|
|
||||||
assert!(fragment.is_empty(), "expected end of component, found node");
|
assert!(fragment.is_empty(), "expected end of component, found node");
|
||||||
|
|
||||||
node_ref.link(node);
|
internal_ref.link(node);
|
||||||
|
|
||||||
state.render_state = ComponentRenderState::Render {
|
state.render_state = ComponentRenderState::Render {
|
||||||
root: root.clone(),
|
root: root.clone(),
|
||||||
bundle,
|
bundle,
|
||||||
parent: parent.clone(),
|
parent: parent.clone(),
|
||||||
node_ref: node_ref.clone(),
|
internal_ref: internal_ref.clone(),
|
||||||
next_sibling: next_sibling.clone(),
|
next_sibling: next_sibling.clone(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,7 +74,7 @@ pub struct Context<COMP: BaseComponent> {
|
|||||||
scope: Scope<COMP>,
|
scope: Scope<COMP>,
|
||||||
props: Rc<COMP::Properties>,
|
props: Rc<COMP::Properties>,
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
mode: RenderMode,
|
creation_mode: RenderMode,
|
||||||
|
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
prepared_state: Option<String>,
|
prepared_state: Option<String>,
|
||||||
@ -94,8 +94,8 @@ impl<COMP: BaseComponent> Context<COMP> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
pub(crate) fn mode(&self) -> RenderMode {
|
pub(crate) fn creation_mode(&self) -> RenderMode {
|
||||||
self.mode
|
self.creation_mode
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The component's prepared state
|
/// The component's prepared state
|
||||||
|
|||||||
@ -512,17 +512,19 @@ mod feat_csr {
|
|||||||
root: BSubtree,
|
root: BSubtree,
|
||||||
parent: Element,
|
parent: Element,
|
||||||
next_sibling: NodeRef,
|
next_sibling: NodeRef,
|
||||||
node_ref: NodeRef,
|
internal_ref: NodeRef,
|
||||||
props: Rc<COMP::Properties>,
|
props: Rc<COMP::Properties>,
|
||||||
) {
|
) {
|
||||||
let bundle = Bundle::new();
|
let bundle = Bundle::new();
|
||||||
node_ref.link(next_sibling.clone());
|
internal_ref.link(next_sibling.clone());
|
||||||
|
let stable_next_sibling = NodeRef::default();
|
||||||
|
stable_next_sibling.link(next_sibling);
|
||||||
let state = ComponentRenderState::Render {
|
let state = ComponentRenderState::Render {
|
||||||
bundle,
|
bundle,
|
||||||
root,
|
root,
|
||||||
node_ref,
|
internal_ref,
|
||||||
parent,
|
parent,
|
||||||
next_sibling,
|
next_sibling: stable_next_sibling,
|
||||||
};
|
};
|
||||||
|
|
||||||
scheduler::push_component_create(
|
scheduler::push_component_create(
|
||||||
@ -632,7 +634,7 @@ mod feat_hydration {
|
|||||||
root: BSubtree,
|
root: BSubtree,
|
||||||
parent: Element,
|
parent: Element,
|
||||||
fragment: &mut Fragment,
|
fragment: &mut Fragment,
|
||||||
node_ref: NodeRef,
|
internal_ref: NodeRef,
|
||||||
props: Rc<COMP::Properties>,
|
props: Rc<COMP::Properties>,
|
||||||
) {
|
) {
|
||||||
// This is very helpful to see which component is failing during hydration
|
// This is very helpful to see which component is failing during hydration
|
||||||
@ -647,8 +649,14 @@ mod feat_hydration {
|
|||||||
let collectable = Collectable::for_component::<COMP>();
|
let collectable = Collectable::for_component::<COMP>();
|
||||||
|
|
||||||
let mut fragment = Fragment::collect_between(fragment, &collectable, &parent);
|
let mut fragment = Fragment::collect_between(fragment, &collectable, &parent);
|
||||||
node_ref.set(fragment.front().cloned());
|
match fragment.front().cloned() {
|
||||||
let next_sibling = NodeRef::default();
|
front @ Some(_) => internal_ref.set(front),
|
||||||
|
None =>
|
||||||
|
{
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
internal_ref.link(NodeRef::new_debug_trapped())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let prepared_state = match fragment
|
let prepared_state = match fragment
|
||||||
.back()
|
.back()
|
||||||
@ -664,10 +672,10 @@ mod feat_hydration {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let state = ComponentRenderState::Hydration {
|
let state = ComponentRenderState::Hydration {
|
||||||
root,
|
|
||||||
parent,
|
parent,
|
||||||
node_ref,
|
root,
|
||||||
next_sibling,
|
internal_ref,
|
||||||
|
next_sibling: NodeRef::new_debug_trapped(),
|
||||||
fragment,
|
fragment,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -172,6 +172,43 @@ mod feat_csr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
mod feat_hydration {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
thread_local! {
|
||||||
|
// A special marker element that should not be referenced
|
||||||
|
static TRAP: Node = gloo::utils::document().create_element("div").unwrap().into();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NodeRef {
|
||||||
|
// A new "placeholder" node ref that should not be accessed
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn new_debug_trapped() -> Self {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
Self::new(TRAP.with(|trap| trap.clone()))
|
||||||
|
}
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn debug_assert_not_trapped(&self) {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
TRAP.with(|trap| {
|
||||||
|
assert!(
|
||||||
|
self.get().as_ref() != Some(trap),
|
||||||
|
"should not use a trapped node ref"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Render children into a DOM node that exists outside the hierarchy of the parent
|
/// Render children into a DOM node that exists outside the hierarchy of the parent
|
||||||
/// component.
|
/// component.
|
||||||
/// ## Relevant examples
|
/// ## Relevant examples
|
||||||
|
|||||||
@ -56,7 +56,7 @@ mod feat_csr_ssr {
|
|||||||
use crate::callback::Callback;
|
use crate::callback::Callback;
|
||||||
use crate::html::RenderMode;
|
use crate::html::RenderMode;
|
||||||
|
|
||||||
match ctx.mode() {
|
match ctx.creation_mode() {
|
||||||
RenderMode::Hydration => {
|
RenderMode::Hydration => {
|
||||||
let link = ctx.link().clone();
|
let link = ctx.link().clone();
|
||||||
let (s, handle) = Suspension::new();
|
let (s, handle) = Suspension::new();
|
||||||
|
|||||||
@ -57,9 +57,9 @@ pub(crate) trait Mountable {
|
|||||||
fn mount(
|
fn mount(
|
||||||
self: Box<Self>,
|
self: Box<Self>,
|
||||||
root: &BSubtree,
|
root: &BSubtree,
|
||||||
node_ref: NodeRef,
|
|
||||||
parent_scope: &AnyScope,
|
parent_scope: &AnyScope,
|
||||||
parent: Element,
|
parent: Element,
|
||||||
|
internal_ref: NodeRef,
|
||||||
next_sibling: NodeRef,
|
next_sibling: NodeRef,
|
||||||
) -> Box<dyn Scoped>;
|
) -> Box<dyn Scoped>;
|
||||||
|
|
||||||
@ -80,8 +80,8 @@ pub(crate) trait Mountable {
|
|||||||
root: BSubtree,
|
root: BSubtree,
|
||||||
parent_scope: &AnyScope,
|
parent_scope: &AnyScope,
|
||||||
parent: Element,
|
parent: Element,
|
||||||
|
internal_ref: NodeRef,
|
||||||
fragment: &mut Fragment,
|
fragment: &mut Fragment,
|
||||||
node_ref: NodeRef,
|
|
||||||
) -> Box<dyn Scoped>;
|
) -> Box<dyn Scoped>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,13 +107,13 @@ impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
|
|||||||
fn mount(
|
fn mount(
|
||||||
self: Box<Self>,
|
self: Box<Self>,
|
||||||
root: &BSubtree,
|
root: &BSubtree,
|
||||||
node_ref: NodeRef,
|
|
||||||
parent_scope: &AnyScope,
|
parent_scope: &AnyScope,
|
||||||
parent: Element,
|
parent: Element,
|
||||||
|
internal_ref: NodeRef,
|
||||||
next_sibling: NodeRef,
|
next_sibling: NodeRef,
|
||||||
) -> Box<dyn Scoped> {
|
) -> Box<dyn Scoped> {
|
||||||
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
|
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
|
||||||
scope.mount_in_place(root.clone(), parent, next_sibling, node_ref, self.props);
|
scope.mount_in_place(root.clone(), parent, next_sibling, internal_ref, self.props);
|
||||||
|
|
||||||
Box::new(scope)
|
Box::new(scope)
|
||||||
}
|
}
|
||||||
@ -146,11 +146,11 @@ impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
|
|||||||
root: BSubtree,
|
root: BSubtree,
|
||||||
parent_scope: &AnyScope,
|
parent_scope: &AnyScope,
|
||||||
parent: Element,
|
parent: Element,
|
||||||
|
internal_ref: NodeRef,
|
||||||
fragment: &mut Fragment,
|
fragment: &mut Fragment,
|
||||||
node_ref: NodeRef,
|
|
||||||
) -> Box<dyn Scoped> {
|
) -> Box<dyn Scoped> {
|
||||||
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
|
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
|
||||||
scope.hydrate_in_place(root, parent, fragment, node_ref, self.props);
|
scope.hydrate_in_place(root, parent, fragment, internal_ref, self.props);
|
||||||
|
|
||||||
Box::new(scope)
|
Box::new(scope)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -977,3 +977,54 @@ async fn hydration_props_blocked_until_hydrated() {
|
|||||||
let result = obtain_result_by_id("output");
|
let result = obtain_result_by_id("output");
|
||||||
assert_eq!(result.as_str(), r#"<div>0</div><div>1</div><div>2</div>"#);
|
assert_eq!(result.as_str(), r#"<div>0</div><div>1</div><div>2</div>"#);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
async fn hydrate_empty() {
|
||||||
|
#[function_component]
|
||||||
|
fn Updating() -> Html {
|
||||||
|
let trigger = use_state(|| false);
|
||||||
|
{
|
||||||
|
let trigger = trigger.clone();
|
||||||
|
use_effect_with_deps(
|
||||||
|
move |_| {
|
||||||
|
trigger.set(true);
|
||||||
|
|| {}
|
||||||
|
},
|
||||||
|
(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if *trigger {
|
||||||
|
html! { <div>{"after"}</div> }
|
||||||
|
} else {
|
||||||
|
html! { <div>{"before"}</div> }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[function_component]
|
||||||
|
fn Empty() -> Html {
|
||||||
|
html! { <></> }
|
||||||
|
}
|
||||||
|
#[function_component]
|
||||||
|
fn App() -> Html {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<Updating />
|
||||||
|
<Empty />
|
||||||
|
<Updating />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let s = ServerRenderer::<App>::new().render().await;
|
||||||
|
|
||||||
|
let output_element = gloo::utils::document()
|
||||||
|
.query_selector("#output")
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
output_element.set_inner_html(&s);
|
||||||
|
|
||||||
|
Renderer::<App>::with_root(output_element).hydrate();
|
||||||
|
sleep(Duration::from_millis(50)).await;
|
||||||
|
|
||||||
|
let result = obtain_result_by_id("output");
|
||||||
|
assert_eq!(result.as_str(), r#"<div>after</div><div>after</div>"#);
|
||||||
|
}
|
||||||
|
|||||||
80
packages/yew/tests/layout.rs
Normal file
80
packages/yew/tests/layout.rs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
#![cfg(target_arch = "wasm32")]
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
|
||||||
|
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use common::obtain_result;
|
||||||
|
use gloo::timers::future::sleep;
|
||||||
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
use wasm_bindgen_test::*;
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
async fn change_nested_after_append() {
|
||||||
|
#[function_component]
|
||||||
|
fn Nested() -> Html {
|
||||||
|
let delayed_trigger = use_state(|| true);
|
||||||
|
|
||||||
|
{
|
||||||
|
let delayed_trigger = delayed_trigger.clone();
|
||||||
|
use_effect_with_deps(
|
||||||
|
move |_| {
|
||||||
|
spawn_local(async move {
|
||||||
|
sleep(Duration::from_millis(50)).await;
|
||||||
|
delayed_trigger.set(false);
|
||||||
|
});
|
||||||
|
|| {}
|
||||||
|
},
|
||||||
|
(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if *delayed_trigger {
|
||||||
|
html! { <div>{"failure"}</div> }
|
||||||
|
} else {
|
||||||
|
html! { <><i></i><span id="result">{"success"}</span></> }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
fn Top() -> Html {
|
||||||
|
html! { <Nested /> }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
fn App() -> Html {
|
||||||
|
let show_bottom = use_state_eq(|| false);
|
||||||
|
|
||||||
|
{
|
||||||
|
let show_bottom = show_bottom.clone();
|
||||||
|
|
||||||
|
use_effect_with_deps(
|
||||||
|
move |_| {
|
||||||
|
show_bottom.set(true);
|
||||||
|
|| {}
|
||||||
|
},
|
||||||
|
(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<Top />
|
||||||
|
if *show_bottom {
|
||||||
|
<div>{"<div>Bottom</div>"}</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yew::Renderer::<App>::with_root(gloo_utils::document().get_element_by_id("output").unwrap())
|
||||||
|
.render();
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
|
||||||
|
let result = obtain_result();
|
||||||
|
assert_eq!(result.as_str(), "success");
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user