Feat: wire up a very basic dom updater
This commit is contained in:
1 parent
4dfdf91236
commit
c4e8d8bb31
18 files changed
+1967
-1105
No files matched your search
Vendored
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"rust-analyzer.inlayHints.enable": false
|
||||
"rust-analyzer.inlayHints.enable": true
|
||||
}
|
||||
Vendored
+2
@@ -23,3 +23,5 @@ deref
|
||||
derefs
|
||||
Derefing
|
||||
rei
|
||||
RefMut
|
||||
diffed
|
||||
@@ -0,0 +1,3 @@
|
||||
# Dioxus Architecture
|
||||
|
||||
:)
|
||||
File renamed without changes.
Whitespace-only changes.
@@ -244,16 +244,41 @@ A few notes:
|
||||
|
||||
## Concurrency
|
||||
|
||||
I don't even know yet
|
||||
|
||||
For Dioxus, concurrency is built directly into the VirtualDOM lifecycle and event system. Suspended components prove "no changes" while diffing, and will cause a lifecycle update later. This is considered a "trigger" and will cause targeted diffs and re-renders. Renderers will need to await the Dioxus suspense queue if they want to process these updates. This will typically involve joining the suspense queue and event listeners together like:
|
||||
```rust
|
||||
// wait for an even either from the suspense queue or our own custom listener system
|
||||
let (left, right) = join!(vdom.suspense_queue, self.custom_event_listener);
|
||||
```
|
||||
LiveView is built on this model, and updates from the WebSocket connection to the host server are treated as external updates. This means any renderer can feed targeted EditLists (the underlying message of this event) directly into the VirtualDOM.
|
||||
|
||||
## Execution Model
|
||||
<!-- todo -->
|
||||
|
||||
## Diffing
|
||||
|
||||
Diffing is an interesting story. Since we don't re-render the entire DOM, we need a way to patch up the DOM without visiting every component. To get this working, we need to think in cycles, queues, and stacks. Most of the original logic is pulled from Dodrio as Dioxus and Dodrio share much of the same DNA.
|
||||
|
||||
When an event is triggered, we find the callback that installed the listener and run it. We then record all components affected by the running of the "subscription" primitive. In practice, many hooks will initiate a subscription, so it is likely that many components throughout the entire tree will need to be re-rendered. For each component, we attach its index and the type of update it needs.
|
||||
|
||||
In practice, removals trump prop updates which trump subscription updates. Therefore, we only process updates where props are directly changed first, as this will likely flow into child components.
|
||||
|
||||
Roughly, the flow looks like:
|
||||
- Process the initiating event
|
||||
- Mark components affected by the subscription API (the only way of causing forward updates)
|
||||
- Descend from the root into children, ignoring those not affected by the subscription API. (walking the tree until we hit the first affected component, or choosing the highest component)
|
||||
- Run this component and then immediately diff its output, marking any children that also need to be updated and putting them into the immediate queue
|
||||
- Mark this component as already-ran and remove it from the need_to_diff list, instead moving it into the "already diffed list"
|
||||
- Run the marked children until the immediate queue is empty
|
||||
|
||||
```rust
|
||||
struct DiffMachine {
|
||||
immediate_queue: Vec<Index>,
|
||||
diffed: HashSet<Index>,
|
||||
need_to_diff: HashSet<Index>
|
||||
marked_for_removal: Vec<Index>
|
||||
}
|
||||
```
|
||||
|
||||
On the actual diffing level, we're using the diffing algorithm pulled from Dodrio, but plan to move to a dedicated crate that implements Meyers/Patience for us. During the diffing phase, we track our current position using a "Traversal" which implements the "MoveTo". When "MoveTo" is combined with "Edit", it is possible for renderers to fully interpret a series of Moves and Edits together to update their internal node structures for rendering.
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::dom_updater::ActiveClosures;
|
||||
use crate::patch::Patch;
|
||||
|
||||
use std::cmp::min;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::dom_updater::ActiveClosures;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::{Element, Node, Text};
|
||||
|
||||
@@ -30,13 +30,12 @@ pub use apply_patches::patch;
|
||||
/// .─. .─.
|
||||
/// ( 1 ) ( 4 )
|
||||
/// `┬' `─'
|
||||
/// ┌────┴───┐ │
|
||||
/// │ │ ├─────┬─────┐
|
||||
/// ▼ ▼ │ │ │
|
||||
/// .─. .─. ▼ ▼ ▼
|
||||
/// ( 2 ) ( 3 ) .─. .─. .─.
|
||||
/// `─' `─' ( 5 ) ( 6 ) ( 7 )
|
||||
/// `─' `─' `─'
|
||||
/// ┌────┴───┐ ├─────┬─────┐
|
||||
/// │ │ │ │ │
|
||||
/// ▼ ▼ ▼ ▼ ▼
|
||||
/// .─. .─. .─. .─. .─.
|
||||
/// ( 2 ) ( 3 ) ( 5 ) ( 6 ) ( 7 )
|
||||
/// `─' `─' `─' `─' `─'
|
||||
/// ```
|
||||
///
|
||||
/// The patching process is tested in a real browser in crates/virtual-dom-rs/tests/diff_patch.rs
|
||||
|
||||
@@ -31,3 +31,4 @@ id-arena = "2.2.1"
|
||||
thiserror = "1.0.23"
|
||||
fxhash = "0.2.1"
|
||||
longest-increasing-subsequence = "0.1.0"
|
||||
# hashbrown = { version = "0.9.1", features = ["bumpalo"] }
|
||||
+283
-1076
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -65,11 +65,13 @@
|
||||
//! - dioxus-liveview (SSR + StringRenderer)
|
||||
//!
|
||||
|
||||
pub mod changelist; // An "edit phase" described by transitions and edit operations
|
||||
// pub mod changelist; // An "edit phase" described by transitions and edit operations
|
||||
pub mod component; // Logic for extending FC
|
||||
pub mod context; // Logic for providing hook + context functionality to user components
|
||||
pub mod debug_renderer; // Test harness for validating that lifecycles and diffs work appropriately
|
||||
pub mod diff; // The diffing algorithm that builds the ChangeList
|
||||
pub mod diff;
|
||||
pub mod patch; // The diffing algorithm that builds the ChangeList
|
||||
// pub mod dodriodiff; // The diffing algorithm that builds the ChangeList
|
||||
pub mod error; // Error type we expose to the renderers
|
||||
pub mod events; // Manages the synthetic event API
|
||||
pub mod hooks; // Built-in hooks
|
||||
@@ -137,5 +139,7 @@ pub mod prelude {
|
||||
pub use dioxus_core_macro::fc;
|
||||
pub use dioxus_html_2::html;
|
||||
|
||||
pub use crate::diff::DiffMachine;
|
||||
|
||||
pub use crate::hooks::*;
|
||||
}
|
||||
@@ -366,7 +366,7 @@ where
|
||||
/// let my_div = div(&b).attr("id", "my-div").finish();
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn attr(mut self, name: &'a str, value: &'a str) -> Self {
|
||||
pub fn attr(mut self, name: &'static str, value: &'a str) -> Self {
|
||||
self.attributes.push(Attribute { name, value });
|
||||
self
|
||||
}
|
||||
@@ -398,7 +398,7 @@ where
|
||||
/// .bool_attr("hidden", Math::random() >= 0.5)
|
||||
/// .finish();
|
||||
/// ```
|
||||
pub fn bool_attr(mut self, name: &'a str, should_add: bool) -> Self {
|
||||
pub fn bool_attr(mut self, name: &'static str, should_add: bool) -> Self {
|
||||
if should_add {
|
||||
self.attributes.push(Attribute { name, value: "" });
|
||||
}
|
||||
@@ -1049,7 +1049,7 @@ pub fn text<'a>(contents: &'a str) -> VNode<'a> {
|
||||
///
|
||||
/// let my_id_attr = attr("id", "my-id");
|
||||
/// ```
|
||||
pub fn attr<'a>(name: &'a str, value: &'a str) -> Attribute<'a> {
|
||||
pub fn attr<'a>(name: &'static str, value: &'a str) -> Attribute<'a> {
|
||||
Attribute { name, value }
|
||||
}
|
||||
|
||||
|
||||
@@ -143,8 +143,8 @@ mod velement {
|
||||
/// `href="https://example.com"`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Attribute<'a> {
|
||||
pub(crate) name: &'a str,
|
||||
pub(crate) value: &'a str,
|
||||
pub name: &'static str,
|
||||
pub value: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Attribute<'a> {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
use fxhash::FxHashMap;
|
||||
|
||||
use crate::innerlude::{VNode, VText};
|
||||
|
||||
/// A Patch encodes an operation that modifies a real DOM element.
|
||||
///
|
||||
/// To update the real DOM that a user sees you'll want to first diff your
|
||||
/// old virtual dom and new virtual dom.
|
||||
///
|
||||
/// This diff operation will generate `Vec<Patch>` with zero or more patches that, when
|
||||
/// applied to your real DOM, will make your real DOM look like your new virtual dom.
|
||||
///
|
||||
/// Each Patch has a u32 node index that helps us identify the real DOM node that it applies to.
|
||||
///
|
||||
/// Our old virtual dom's nodes are indexed depth first, as shown in this illustration
|
||||
/// (0 being the root node, 1 being it's first child, 2 being it's first child's first child).
|
||||
///
|
||||
/// ```text
|
||||
/// .─.
|
||||
/// ( 0 )
|
||||
/// `┬'
|
||||
/// ┌────┴──────┐
|
||||
/// │ │
|
||||
/// ▼ ▼
|
||||
/// .─. .─.
|
||||
/// ( 1 ) ( 4 )
|
||||
/// `┬' `─'
|
||||
/// ┌────┴───┐ ├─────┬─────┐
|
||||
/// │ │ │ │ │
|
||||
/// ▼ ▼ ▼ ▼ ▼
|
||||
/// .─. .─. .─. .─. .─.
|
||||
/// ( 2 ) ( 3 ) ( 5 ) ( 6 ) ( 7 )
|
||||
/// `─' `─' `─' `─' `─'
|
||||
/// ```
|
||||
///
|
||||
/// The patching process is tested in a real browser in crates/virtual-dom-rs/tests/diff_patch.rs
|
||||
|
||||
pub enum Patch<'a> {
|
||||
/// Append a vector of child nodes to a parent node id.
|
||||
AppendChildren(NodeIdx, Vec<&'a VNode<'a>>),
|
||||
|
||||
/// For a `node_i32`, remove all children besides the first `len`
|
||||
TruncateChildren(NodeIdx, usize),
|
||||
|
||||
/// Replace a node with another node. This typically happens when a node's tag changes.
|
||||
/// ex: <div> becomes <span>
|
||||
Replace(NodeIdx, &'a VNode<'a>),
|
||||
|
||||
/// Add attributes that the new node has that the old node does not
|
||||
AddAttributes(NodeIdx, FxHashMap<&'a str, &'a str>),
|
||||
|
||||
/// Remove attributes that the old node had that the new node doesn't
|
||||
RemoveAttributes(NodeIdx, Vec<&'a str>),
|
||||
|
||||
/// Change the text of a Text node.
|
||||
ChangeText(NodeIdx, &'a VText<'a>),
|
||||
}
|
||||
|
||||
type NodeIdx = usize;
|
||||
|
||||
impl<'a> Patch<'a> {
|
||||
/// Every Patch is meant to be applied to a specific node within the DOM. Get the
|
||||
/// index of the DOM node that this patch should apply to. DOM nodes are indexed
|
||||
/// depth first with the root node in the tree having index 0.
|
||||
pub fn node_idx(&self) -> usize {
|
||||
match self {
|
||||
Patch::AppendChildren(node_idx, _) => *node_idx,
|
||||
Patch::TruncateChildren(node_idx, _) => *node_idx,
|
||||
Patch::Replace(node_idx, _) => *node_idx,
|
||||
Patch::AddAttributes(node_idx, _) => *node_idx,
|
||||
Patch::RemoveAttributes(node_idx, _) => *node_idx,
|
||||
Patch::ChangeText(node_idx, _) => *node_idx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PatchList<'a> {
|
||||
patches: Vec<Patch<'a>>,
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{changelist::EditList, nodes::VNode};
|
||||
// use crate::{changelist::EditList, nodes::VNode};
|
||||
use crate::nodes::VNode;
|
||||
use crate::{events::EventTrigger, innerlude::*};
|
||||
use any::Any;
|
||||
use bumpalo::Bump;
|
||||
@@ -121,7 +122,8 @@ impl VirtualDom {
|
||||
///
|
||||
///
|
||||
/// ```
|
||||
pub async fn progress_with_event(&mut self, evt: EventTrigger) -> Result<EditList<'_>> {
|
||||
pub async fn progress_with_event(&mut self, evt: EventTrigger) -> Result<()> {
|
||||
// pub async fn progress_with_event(&mut self, evt: EventTrigger) -> Result<EditList<'_>> {
|
||||
let EventTrigger {
|
||||
component_id,
|
||||
listener_id,
|
||||
@@ -156,8 +158,8 @@ impl VirtualDom {
|
||||
process_event(&mut self.components, event)?;
|
||||
}
|
||||
|
||||
todo!()
|
||||
// Ok(())
|
||||
// todo!()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn progress_completely(&mut self) -> Result<()> {
|
||||
|
||||
+423
-6
@@ -17,9 +17,11 @@
|
||||
//!
|
||||
//! The `WebsysRenderer` is particularly useful when needing to cache a Virtual DOM in between requests
|
||||
|
||||
use dioxus::{patch::Patch, prelude::VText};
|
||||
pub use dioxus_core as dioxus;
|
||||
use dioxus_core::{
|
||||
events::EventTrigger,
|
||||
prelude::{Properties, VNode, VirtualDom, FC},
|
||||
prelude::{bumpalo::Bump, html, DiffMachine, Properties, VNode, VirtualDom, FC},
|
||||
};
|
||||
use futures::{channel::mpsc, future, SinkExt, StreamExt};
|
||||
use mpsc::UnboundedSender;
|
||||
@@ -92,17 +94,432 @@ impl WebsysRenderer {
|
||||
|
||||
match internal_dom.progress_with_event(event).await {
|
||||
Err(_) => {}
|
||||
Ok(_) => render_diffs(),
|
||||
Ok(_) => {} // Ok(_) => render_diffs(),
|
||||
}
|
||||
// waiting for next event to arrive from the external triggers
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn simple_render(tree: impl for<'a> Fn(&'a Bump) -> VNode<'a>) {
|
||||
let bump = Bump::new();
|
||||
|
||||
let old = html! { <div> </div> }(&bump);
|
||||
let new = tree(&bump);
|
||||
|
||||
let mut machine = DiffMachine::new();
|
||||
|
||||
let patches = machine.diff(&old, &new);
|
||||
}
|
||||
}
|
||||
|
||||
/// For any listeners in the tree, attach the sender closure.
|
||||
/// When a event is triggered, we convert it into the synthetic event type and dump it back in the Virtual Dom's queu
|
||||
fn attach_listeners(sender: &UnboundedSender<EventTrigger>, dom: &VirtualDom) {}
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::{cmp::min, rc::Rc};
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::{Element, Node, Text};
|
||||
|
||||
fn render_diffs() {}
|
||||
/// Apply all of the patches to our old root node in order to create the new root node
|
||||
/// that we desire.
|
||||
/// This is usually used after diffing two virtual nodes.
|
||||
pub fn patch<N: Into<Node>>(root_node: N, patches: &Vec<Patch>) -> Result<(), JsValue> {
|
||||
// pub fn patch<N: Into<Node>>(root_node: N, patches: &Vec<Patch>) -> Result<ActiveClosures, JsValue> {
|
||||
let root_node: Node = root_node.into();
|
||||
|
||||
let mut cur_node_idx = 0;
|
||||
|
||||
let mut nodes_to_find = HashSet::new();
|
||||
|
||||
for patch in patches {
|
||||
nodes_to_find.insert(patch.node_idx());
|
||||
}
|
||||
|
||||
let mut element_nodes_to_patch = HashMap::new();
|
||||
let mut text_nodes_to_patch = HashMap::new();
|
||||
|
||||
// Closures that were added to the DOM during this patch operation.
|
||||
// let mut active_closures = HashMap::new();
|
||||
|
||||
find_nodes(
|
||||
root_node,
|
||||
&mut cur_node_idx,
|
||||
&mut nodes_to_find,
|
||||
&mut element_nodes_to_patch,
|
||||
&mut text_nodes_to_patch,
|
||||
);
|
||||
|
||||
for patch in patches {
|
||||
let patch_node_idx = patch.node_idx();
|
||||
|
||||
if let Some(element) = element_nodes_to_patch.get(&patch_node_idx) {
|
||||
let new_closures = apply_element_patch(&element, &patch)?;
|
||||
// active_closures.extend(new_closures);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(text_node) = text_nodes_to_patch.get(&patch_node_idx) {
|
||||
apply_text_patch(&text_node, &patch)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
unreachable!("Getting here means we didn't find the element or next node that we were supposed to patch.")
|
||||
}
|
||||
|
||||
// Ok(active_closures)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find_nodes(
|
||||
root_node: Node,
|
||||
cur_node_idx: &mut usize,
|
||||
nodes_to_find: &mut HashSet<usize>,
|
||||
element_nodes_to_patch: &mut HashMap<usize, Element>,
|
||||
text_nodes_to_patch: &mut HashMap<usize, Text>,
|
||||
) {
|
||||
if nodes_to_find.len() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// We use child_nodes() instead of children() because children() ignores text nodes
|
||||
let children = root_node.child_nodes();
|
||||
let child_node_count = children.length();
|
||||
|
||||
// If the root node matches, mark it for patching
|
||||
if nodes_to_find.get(&cur_node_idx).is_some() {
|
||||
match root_node.node_type() {
|
||||
Node::ELEMENT_NODE => {
|
||||
element_nodes_to_patch.insert(*cur_node_idx, root_node.unchecked_into());
|
||||
}
|
||||
Node::TEXT_NODE => {
|
||||
text_nodes_to_patch.insert(*cur_node_idx, root_node.unchecked_into());
|
||||
}
|
||||
other => unimplemented!("Unsupported root node type: {}", other),
|
||||
}
|
||||
nodes_to_find.remove(&cur_node_idx);
|
||||
}
|
||||
|
||||
*cur_node_idx += 1;
|
||||
|
||||
for i in 0..child_node_count {
|
||||
let node = children.item(i).unwrap();
|
||||
|
||||
match node.node_type() {
|
||||
Node::ELEMENT_NODE => {
|
||||
find_nodes(
|
||||
node,
|
||||
cur_node_idx,
|
||||
nodes_to_find,
|
||||
element_nodes_to_patch,
|
||||
text_nodes_to_patch,
|
||||
);
|
||||
}
|
||||
Node::TEXT_NODE => {
|
||||
if nodes_to_find.get(&cur_node_idx).is_some() {
|
||||
text_nodes_to_patch.insert(*cur_node_idx, node.unchecked_into());
|
||||
}
|
||||
|
||||
*cur_node_idx += 1;
|
||||
}
|
||||
Node::COMMENT_NODE => {
|
||||
// At this time we do not support user entered comment nodes, so if we see a comment
|
||||
// then it was a delimiter created by virtual-dom-rs in order to ensure that two
|
||||
// neighboring text nodes did not get merged into one by the browser. So we skip
|
||||
// over this virtual-dom-rs generated comment node.
|
||||
}
|
||||
_other => {
|
||||
// Ignoring unsupported child node type
|
||||
// TODO: What do we do with this situation? Log a warning?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pub type ActiveClosures = HashMap<u32, Vec<DynClosure>>;
|
||||
|
||||
// fn apply_element_patch(node: &Element, patch: &Patch) -> Result<ActiveClosures, JsValue> {
|
||||
fn apply_element_patch(node: &Element, patch: &Patch) -> Result<(), JsValue> {
|
||||
// let active_closures = HashMap::new();
|
||||
|
||||
match patch {
|
||||
Patch::AddAttributes(_node_idx, attributes) => {
|
||||
for (attrib_name, attrib_val) in attributes.iter() {
|
||||
node.set_attribute(attrib_name, attrib_val)?;
|
||||
}
|
||||
|
||||
// Ok(active_closures)
|
||||
Ok(())
|
||||
}
|
||||
Patch::RemoveAttributes(_node_idx, attributes) => {
|
||||
for attrib_name in attributes.iter() {
|
||||
node.remove_attribute(attrib_name)?;
|
||||
}
|
||||
|
||||
// Ok(active_closures)
|
||||
Ok(())
|
||||
}
|
||||
Patch::Replace(_node_idx, new_node) => {
|
||||
let created_node = create_dom_node(&new_node);
|
||||
|
||||
node.replace_with_with_node_1(&created_node.node)?;
|
||||
|
||||
Ok(())
|
||||
// Ok(created_node.closures)
|
||||
}
|
||||
Patch::TruncateChildren(_node_idx, num_children_remaining) => {
|
||||
let children = node.child_nodes();
|
||||
let mut child_count = children.length();
|
||||
|
||||
// We skip over any separators that we placed between two text nodes
|
||||
// -> `<!--ptns-->`
|
||||
// and trim all children that come after our new desired `num_children_remaining`
|
||||
let mut non_separator_children_found = 0;
|
||||
|
||||
for index in 0 as u32..child_count {
|
||||
let child = children
|
||||
.get(min(index, child_count - 1))
|
||||
.expect("Potential child to truncate");
|
||||
|
||||
// If this is a comment node then we know that it is a `<!--ptns-->`
|
||||
// text node separator that was created in virtual_node/mod.rs.
|
||||
if child.node_type() == Node::COMMENT_NODE {
|
||||
continue;
|
||||
}
|
||||
|
||||
non_separator_children_found += 1;
|
||||
|
||||
if non_separator_children_found <= *num_children_remaining as u32 {
|
||||
continue;
|
||||
}
|
||||
|
||||
node.remove_child(&child).expect("Truncated children");
|
||||
child_count -= 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
// Ok(active_closures)
|
||||
}
|
||||
Patch::AppendChildren(_node_idx, new_nodes) => {
|
||||
let parent = &node;
|
||||
|
||||
let mut active_closures = HashMap::new();
|
||||
|
||||
for new_node in new_nodes {
|
||||
let created_node = create_dom_node(&new_node);
|
||||
// let created_node = new_node.create_dom_node();
|
||||
|
||||
parent.append_child(&created_node.node)?;
|
||||
|
||||
active_closures.extend(created_node.closures);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
// Ok(active_closures)
|
||||
}
|
||||
Patch::ChangeText(_node_idx, _new_node) => {
|
||||
unreachable!("Elements should not receive ChangeText patches.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_text_patch(node: &Text, patch: &Patch) -> Result<(), JsValue> {
|
||||
match patch {
|
||||
Patch::ChangeText(_node_idx, new_node) => {
|
||||
node.set_node_value(Some(&new_node.text));
|
||||
}
|
||||
Patch::Replace(_node_idx, new_node) => {
|
||||
node.replace_with_with_node_1(&create_dom_node(&new_node).node)?;
|
||||
// node.replace_with_with_node_1(&new_node.create_dom_node().node)?;
|
||||
}
|
||||
other => unreachable!(
|
||||
"Text nodes should only receive ChangeText or Replace patches, not ",
|
||||
// other,
|
||||
// "Text nodes should only receive ChangeText or Replace patches, not {:?}.",
|
||||
// other,
|
||||
),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A node along with all of the closures that were created for that
|
||||
/// node's events and all of it's child node's events.
|
||||
pub struct CreatedNode<T> {
|
||||
/// A `Node` or `Element` that was created from a `VirtualNode`
|
||||
pub node: T,
|
||||
/// A map of a node's unique identifier along with all of the Closures for that node.
|
||||
///
|
||||
/// The DomUpdater uses this to look up nodes and see if they're still in the page. If not
|
||||
/// the reference that we maintain to their closure will be dropped, thus freeing the Closure's
|
||||
/// memory.
|
||||
pub closures: HashMap<u32, Vec<DynClosure>>,
|
||||
}
|
||||
|
||||
/// Box<dyn AsRef<JsValue>>> is our js_sys::Closure. Stored this way to allow us to store
|
||||
/// any Closure regardless of the arguments.
|
||||
pub type DynClosure = Rc<dyn AsRef<JsValue>>;
|
||||
|
||||
impl<T> CreatedNode<T> {
|
||||
pub fn without_closures<N: Into<T>>(node: N) -> Self {
|
||||
CreatedNode {
|
||||
node: node.into(),
|
||||
closures: HashMap::with_capacity(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::Deref for CreatedNode<T> {
|
||||
type Target = T;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.node
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CreatedNode<Element>> for CreatedNode<Node> {
|
||||
fn from(other: CreatedNode<Element>) -> CreatedNode<Node> {
|
||||
CreatedNode {
|
||||
node: other.node.into(),
|
||||
closures: other.closures,
|
||||
}
|
||||
}
|
||||
}
|
||||
fn create_dom_node(node: &VNode<'_>) -> CreatedNode<Node> {
|
||||
match node {
|
||||
VNode::Text(text_node) => CreatedNode::without_closures(create_text_node(text_node)),
|
||||
VNode::Element(element_node) => create_element_node(element_node).into(),
|
||||
// VNode::Element(element_node) => element_node.create_element_node().into(),
|
||||
VNode::Suspended => todo!(" not iimplemented yet"),
|
||||
VNode::Component(_) => todo!(" not iimplemented yet"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a DOM element by recursively creating DOM nodes for this element and it's
|
||||
/// children, it's children's children, etc.
|
||||
pub fn create_element_node(node: &dioxus_core::nodes::VElement) -> CreatedNode<Element> {
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
|
||||
// TODO: enable svg again
|
||||
// let element = if html_validation::is_svg_namespace(&node.tag_name) {
|
||||
// document
|
||||
// .create_element_ns(Some("http://www.w3.org/2000/svg"), &node.tag_name)
|
||||
// .unwrap()
|
||||
// } else {
|
||||
let element = document.create_element(&node.tag_name).unwrap();
|
||||
// };
|
||||
|
||||
let mut closures = HashMap::new();
|
||||
|
||||
node.attributes
|
||||
.iter()
|
||||
.map(|f| (f.name, f.value))
|
||||
.for_each(|(name, value)| {
|
||||
if name == "unsafe_inner_html" {
|
||||
element.set_inner_html(value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
element
|
||||
.set_attribute(name, value)
|
||||
.expect("Set element attribute in create element");
|
||||
});
|
||||
|
||||
// if node.events.0.len() > 0 {
|
||||
// let unique_id = create_unique_identifier();
|
||||
|
||||
// element
|
||||
// .set_attribute("data-vdom-id".into(), &unique_id.to_string())
|
||||
// .expect("Could not set attribute on element");
|
||||
|
||||
// closures.insert(unique_id, vec![]);
|
||||
|
||||
// node.events.0.iter().for_each(|(onevent, callback)| {
|
||||
// // onclick -> click
|
||||
// let event = &onevent[2..];
|
||||
|
||||
// let current_elem: &EventTarget = element.dyn_ref().unwrap();
|
||||
|
||||
// current_elem
|
||||
// .add_event_listener_with_callback(event, callback.as_ref().as_ref().unchecked_ref())
|
||||
// .unwrap();
|
||||
|
||||
// closures
|
||||
// .get_mut(&unique_id)
|
||||
// .unwrap()
|
||||
// .push(Rc::clone(callback));
|
||||
// });
|
||||
// }
|
||||
|
||||
let mut previous_node_was_text = false;
|
||||
|
||||
node.children.iter().for_each(|child| {
|
||||
match child {
|
||||
VNode::Text(text_node) => {
|
||||
let current_node = element.as_ref() as &web_sys::Node;
|
||||
|
||||
// We ensure that the text siblings are patched by preventing the browser from merging
|
||||
// neighboring text nodes. Originally inspired by some of React's work from 2016.
|
||||
// -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes
|
||||
// -> https://github.com/facebook/react/pull/5753
|
||||
//
|
||||
// `ptns` = Percy text node separator
|
||||
if previous_node_was_text {
|
||||
let separator = document.create_comment("ptns");
|
||||
current_node
|
||||
.append_child(separator.as_ref() as &web_sys::Node)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
current_node
|
||||
.append_child(&create_text_node(&text_node))
|
||||
.unwrap();
|
||||
|
||||
previous_node_was_text = true;
|
||||
}
|
||||
VNode::Element(element_node) => {
|
||||
previous_node_was_text = false;
|
||||
|
||||
let child = create_element_node(element_node);
|
||||
// let child = element_node.create_element_node();
|
||||
let child_elem: Element = child.node;
|
||||
|
||||
closures.extend(child.closures);
|
||||
|
||||
element.append_child(&child_elem).unwrap();
|
||||
}
|
||||
VNode::Suspended => {
|
||||
todo!("Not yet supported")
|
||||
}
|
||||
VNode::Component(_) => {
|
||||
todo!("Not yet supported")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: connect on mount to the event system somehow
|
||||
// if let Some(on_create_elem) = node.events.0.get("on_create_elem") {
|
||||
// let on_create_elem: &js_sys::Function = on_create_elem.as_ref().as_ref().unchecked_ref();
|
||||
// on_create_elem
|
||||
// .call1(&wasm_bindgen::JsValue::NULL, &element)
|
||||
// .unwrap();
|
||||
// }
|
||||
|
||||
CreatedNode {
|
||||
node: element,
|
||||
closures,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a `Text` element from a `VirtualNode`, typically right before adding it
|
||||
/// into the DOM.
|
||||
pub fn create_text_node(node: &VText) -> Text {
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
document.create_text_node(&node.text)
|
||||
}
|
||||
|
||||
// /// For any listeners in the tree, attach the sender closure.
|
||||
// /// When a event is triggered, we convert it into the synthetic event type and dump it back in the Virtual Dom's queu
|
||||
// fn attach_listeners(sender: &UnboundedSender<EventTrigger>, dom: &VirtualDom) {}
|
||||
|
||||
// fn render_diffs() {}
|
||||
Reference in new issue
Block a user