Feat: wire up a very basic dom updater

This commit is contained in:
Jonathan Kelley committed 2021-02-14 23:39:46 -05:00
1 parent 4dfdf91236
commit c4e8d8bb31
18 files changed
+1967 -1105

No files matched your search

+1
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+6 -2
View File
@@ -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::*;
}
+3 -3
View File
@@ -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 }
}
+2 -2
View File
@@ -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> {
+79
View File
@@ -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>>,
}
+6 -4
View File
@@ -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
View File
@@ -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() {}