Skip to content

Commit b0dcc42

Browse files
committed
Fix ordering issue with keyed virtual nodes
1 parent 392c870 commit b0dcc42

File tree

15 files changed

+497
-329
lines changed

15 files changed

+497
-329
lines changed

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@ members = [
4040
"examples/game_of_life",
4141
"examples/inner_html",
4242
"examples/js_callback",
43+
"examples/keyed_list",
4344
"examples/large_table",
44-
"examples/minimal",
4545
"examples/minimal_wp",
46+
"examples/minimal",
4647
"examples/mount_point",
4748
"examples/multi_thread",
4849
"examples/nested_list",

yew-macro/src/html_tree/html_component.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ impl ToTokens for HtmlComponent {
155155
};
156156

157157
let key = if let Some(key) = props.key() {
158-
quote_spanned! { key.span() => Some(#key) }
158+
quote_spanned! { key.span() => Some(::yew::virtual_dom::Key::from(#key)) }
159159
} else {
160160
quote! {None }
161161
};

yew-macro/src/html_tree/html_list.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ impl ToTokens for HtmlList {
6060
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
6161
let children = &self.children;
6262
let key = if let Some(key) = &self.key {
63-
quote_spanned! {key.span() => Some(#key)}
63+
quote_spanned! {key.span() => Some(::yew::virtual_dom::Key::from(#key))}
6464
} else {
6565
quote! {None }
6666
};

yew-macro/src/html_tree/html_tag/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ impl ToTokens for HtmlTag {
161161
});
162162
let set_key = key.iter().map(|key| {
163163
quote! {
164-
#vtag.key = Some(#key);
164+
#vtag.key = Some(::yew::virtual_dom::Key::from(#key));
165165
}
166166
});
167167
let listeners = listeners.iter().map(|listener| {

yew/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ description = "A framework for making client-side single-page apps"
1919
travis-ci = { repository = "yewstack/yew" }
2020

2121
[dependencies]
22+
ahash = { version = "0.3", optional = true }
2223
anyhow = "1"
2324
anymap = "0.12"
2425
bincode = { version = "~1.2.1", optional = true }
@@ -127,6 +128,7 @@ bincode = "~1.2.1"
127128

128129
[features]
129130
default = ["services", "agent", "web_sys"]
131+
fast_hasher = ["ahash"]
130132
std_web = ["stdweb", "yew-macro/std_web"]
131133
web_sys = ["console_error_panic_hook", "futures", "gloo", "js-sys", "web-sys", "wasm-bindgen", "wasm-bindgen-futures"]
132134
doc_test = []

yew/src/agent/worker/public.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use cfg_match::cfg_match;
77
use slab::Slab;
88
use std::any::TypeId;
99
use std::cell::RefCell;
10-
use std::collections::{hash_map, HashMap, HashSet};
1110
use std::fmt;
1211
use std::marker::PhantomData;
1312
use std::rc::Rc;
@@ -21,6 +20,15 @@ cfg_if! {
2120
use web_sys::{Worker};
2221
}
2322
}
23+
cfg_if! {
24+
if #[cfg(feature = "fast_hasher")] {
25+
type HashMap<K, V> = ahash::AHashMap<K, V, ahash::RandomState>;
26+
type HashSet<K> = ahash::AHashSet<K, ahash::RandomState>;
27+
use std::collections::hash_map;
28+
} else {
29+
use std::collections::{hash_map, HashMap, HashSet};
30+
}
31+
}
2432

2533
thread_local! {
2634
static REMOTE_AGENTS_POOL: RefCell<AnyMap> = RefCell::new(AnyMap::new());

yew/src/html/scope.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use super::{Callback, Component, NodeRef, Renderable};
22
use crate::scheduler::{scheduler, ComponentRunnableType, Runnable, Shared};
3-
use crate::virtual_dom::{VDiff, VNode};
3+
use crate::virtual_dom::{VDiff, VDiffNodePosition, VNode};
44
use cfg_if::cfg_if;
55
use std::any::{Any, TypeId};
66
use std::cell::{Ref, RefCell};
@@ -317,16 +317,20 @@ where
317317
state.render_status = state.render_status.map(|_| DIRTY);
318318
let mut root = state.component.render();
319319
let last_root = state.last_root.take();
320-
if let Some(node) =
321-
root.apply(&state.scope.clone().into(), &state.element, None, last_root)
322-
{
323-
state.node_ref.set(Some(node));
324-
} else if let VNode::VComp(child) = &root {
320+
let new_node = root.apply(
321+
&state.scope.clone().into(),
322+
&state.element,
323+
VDiffNodePosition::FirstChild,
324+
last_root,
325+
);
326+
if let VNode::VComp(child) = &root {
325327
// If the root VNode is a VComp, we won't have access to the rendered DOM node
326328
// because components render asynchronously. In order to bubble up the DOM node
327329
// from the VComp, we need to link the currently rendering component with its
328330
// root child component.
329331
state.node_ref.link(child.node_ref.clone());
332+
} else if let Some(node) = new_node {
333+
state.node_ref.set(Some(node));
330334
}
331335
state.last_root = Some(root);
332336
};

yew/src/services/fetch/std_web.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ use super::Referrer;
44
use crate::callback::Callback;
55
use crate::format::{Binary, Format, Text};
66
use crate::services::Task;
7+
use cfg_if::cfg_if;
78
use serde::Serialize;
8-
use std::collections::HashMap;
99
use std::fmt;
1010
use stdweb::serde::Serde;
1111
use stdweb::unstable::{TryFrom, TryInto};
@@ -15,6 +15,13 @@ use stdweb::{JsSerialize, Value};
1515
#[allow(unused_imports)]
1616
use stdweb::{_js_impl, js};
1717
use thiserror::Error;
18+
cfg_if! {
19+
if #[cfg(feature = "fast_hasher")] {
20+
type HashMap<K, V> = ahash::AHashMap<K, V, ahash::RandomState>;
21+
} else {
22+
use std::collections::HashMap;
23+
}
24+
}
1825

1926
#[doc(no_inline)]
2027
pub use http::{HeaderMap, Method, Request, Response, StatusCode, Uri};

yew/src/virtual_dom/key.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//! This module contains the implementation yew's virtual nodes' keys.
2+
3+
use std::rc::Rc;
4+
5+
/// Represents the (optional) key of Yew's virtual nodes.
6+
///
7+
/// Keys are cheap to clone.
8+
// TODO: Explain when keys are useful and add an example.
9+
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
10+
pub struct Key {
11+
key: Rc<String>,
12+
}
13+
14+
impl core::fmt::Display for Key {
15+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
16+
write!(f, "{}", &self.key)
17+
}
18+
}
19+
20+
impl core::fmt::Debug for Key {
21+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
22+
write!(f, "{}", &self.key)
23+
}
24+
}
25+
26+
impl<K: Into<Rc<String>>> From<K> for Key {
27+
fn from(key: K) -> Self {
28+
Key { key: key.into() }
29+
}
30+
}

yew/src/virtual_dom/mod.rs

Lines changed: 87 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! This module contains the implementation of reactive virtual dom concept.
22
3+
#[doc(hidden)]
4+
pub mod key;
35
#[doc(hidden)]
46
pub mod vcomp;
57
#[doc(hidden)]
@@ -14,19 +16,27 @@ pub mod vtext;
1416
use crate::html::AnyScope;
1517
use cfg_if::cfg_if;
1618
use indexmap::set::IndexSet;
17-
use std::collections::HashMap;
1819
use std::fmt;
1920
use std::rc::Rc;
2021
cfg_if! {
2122
if #[cfg(feature = "std_web")] {
2223
use crate::html::EventListener;
23-
use stdweb::web::{Element, Node};
24+
use stdweb::web::{Element, INode, Node};
2425
} else if #[cfg(feature = "web_sys")] {
2526
use gloo::events::EventListener;
2627
use web_sys::{Element, Node};
2728
}
2829
}
30+
cfg_if! {
31+
if #[cfg(feature = "fast_hasher")] {
32+
type HashMap<K, V> = ahash::AHashMap<K, V, ahash::RandomState>;
33+
} else {
34+
use std::collections::HashMap;
35+
}
36+
}
2937

38+
#[doc(inline)]
39+
pub use self::key::Key;
3040
#[doc(inline)]
3141
pub use self::vcomp::{VChild, VComp};
3242
#[doc(inline)]
@@ -185,53 +195,106 @@ enum Reform {
185195

186196
/// Create a new reference (js Node).
187197
///
188-
/// The optional `Node` is used to insert the
189-
/// new node in the correct slot of the parent.
198+
/// The optional `Node` is used to insert the new node in the correct slot
199+
/// of the parent.
190200
///
191-
/// If it does not exist, a `previous_sibling` must be
192-
/// specified (see `VDiff::apply()`).
193-
Before(Option<Node>),
201+
/// If it does not exist, the `node_position` will be used (see
202+
/// `VDiff::apply()`).
203+
Replace(VDiffNodePosition),
194204
}
195205

196-
// TODO(#938): What about to implement `VDiff` for `Element`?
197-
// In makes possible to include ANY element into the tree.
206+
#[derive(Debug)]
207+
pub(crate) enum VDiffNodePosition {
208+
FirstChild,
209+
Before(Node),
210+
After(Node),
211+
LastChild,
212+
}
213+
214+
// TODO(#938): What about implementing `VDiff` for `Element`?
215+
// It would make it possible to include ANY element into the tree.
198216
// `Ace` editor embedding for example?
199217

200218
/// This trait provides features to update a tree by calculating a difference against another tree.
201219
pub(crate) trait VDiff {
202-
/// Remove itself from parent and return the next sibling.
203-
fn detach(&mut self, parent: &Element) -> Option<Node>;
220+
/// Remove self from parent and return the next sibling.
221+
fn detach(&mut self, parent: &Element) -> VDiffNodePosition;
204222

205223
/// Scoped diff apply to other tree.
206224
///
207-
/// Virtual rendering for the node. It uses parent node and existing children (virtual and DOM)
208-
/// to check the difference and apply patches to the actual DOM representation.
225+
/// Virtual rendering for the node. It uses parent node and existing
226+
/// children (virtual and DOM) to check the difference and apply patches to
227+
/// the actual DOM representation.
228+
///
229+
/// TODO: Explain that vnodes must be at their position before apply is called.
209230
///
210231
/// Parameters:
232+
/// - `parent_scope`: the parent `Scope` used for passing messages to the
233+
/// parent `Component`.
211234
/// - `parent`: the parent node in the DOM.
212-
/// - `previous_sibling`: the "previous node" in a list of nodes, used to efficiently
235+
/// - `node_position`: the position relative to `parent` used to efficiently
213236
/// find where to put the node.
214-
/// - `ancestor`: the node that this node will be replacing in the DOM.
215-
/// This method will _always_ remove the `ancestor` from the `parent`.
216-
/// - `parent_scope`: the parent `Scope` used for passing messages to the parent `Component`.
237+
/// - `ancestor`: the node that this node will be replacing in the DOM. This
238+
/// method will _always_ remove the `ancestor` from the `parent`.
239+
///
240+
/// Returns the newly inserted element, if there is one (empty VList don't have one).
217241
///
218242
/// ### Internal Behavior Notice:
219243
///
220-
/// Note that these modify the DOM by modifying the reference that _already_ exists
221-
/// on the `ancestor`. If `self.reference` exists (which it _shouldn't_) this method
222-
/// will panic.
244+
/// Note that these modify the DOM by modifying the reference that _already_
245+
/// exists on the `ancestor`. If `self.reference` exists (which it
246+
/// _shouldn't_) this method will panic.
223247
///
224-
/// The exception to this is obviously `VRef` which simply uses the inner `Node` directly
225-
/// (always removes the `Node` that exists).
248+
/// The exception to this is obviously `VRef` which simply uses the inner
249+
/// `Node` directly (always removes the `Node` that exists).
226250
fn apply(
227251
&mut self,
228-
scope: &AnyScope,
252+
parent_scope: &AnyScope,
229253
parent: &Element,
230-
previous_sibling: Option<&Node>,
254+
node_position: VDiffNodePosition, // TODO: merge `node_position` and `ancestor`
231255
ancestor: Option<VNode>,
232256
) -> Option<Node>;
233257
}
234258

259+
#[cfg(feature = "web_sys")]
260+
fn insert_node(node: &Node, parent: &Element, node_position: &VDiffNodePosition) -> Node {
261+
match node_position {
262+
VDiffNodePosition::FirstChild => parent
263+
.insert_before(&node, parent.first_child().as_ref())
264+
.expect("failed to insert tag before next sibling"),
265+
VDiffNodePosition::Before(next_sibling) => parent
266+
.insert_before(&node, Some(next_sibling))
267+
.expect("failed to insert tag before next sibling"),
268+
VDiffNodePosition::After(previous_sibling) => parent
269+
.insert_before(&node, previous_sibling.next_sibling().as_ref())
270+
.expect("failed to insert tag before next sibling"),
271+
VDiffNodePosition::LastChild => parent.append_child(node).expect("failed to append tag"),
272+
}
273+
}
274+
275+
#[cfg(feature = "std_web")]
276+
fn insert_node(node: &impl INode, parent: &impl INode, node_position: &VDiffNodePosition) -> Node {
277+
fn insert_before(node: &impl INode, parent: &impl INode, reference: Option<&Node>) {
278+
if let Some(reference) = reference {
279+
parent
280+
.insert_before(node, reference)
281+
.expect("failed to insert tag before next sibling");
282+
} else {
283+
parent.append_child(node);
284+
}
285+
}
286+
287+
match node_position {
288+
VDiffNodePosition::FirstChild => insert_before(node, parent, parent.first_child().as_ref()),
289+
VDiffNodePosition::Before(next_sibling) => insert_before(node, parent, Some(next_sibling)),
290+
VDiffNodePosition::After(previous_sibling) => {
291+
insert_before(node, parent, previous_sibling.next_sibling().as_ref())
292+
}
293+
VDiffNodePosition::LastChild => parent.append_child(node),
294+
}
295+
node.as_node().clone()
296+
}
297+
235298
/// Transform properties to the expected type.
236299
pub trait Transformer<FROM, TO> {
237300
/// Transforms one type to another.

0 commit comments

Comments
 (0)