diff --git a/Cargo.toml b/Cargo.toml index cac380bc..4585c348 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,6 @@ with-node-macro = ["sauron-macro"] html-parser = ["sauron-html-parser"] use-skipdiff = ["sauron-core/use-skipdiff"] - [dev-dependencies] console_error_panic_hook = "0.1.7" console_log = "1.0" @@ -62,8 +61,13 @@ features = [ "InputEvent", "console", "Performance", + "Element", + "Window", ] +[dev-dependencies.criterion] +version = "0.5.1" +default-features = false [workspace] members = [ @@ -77,18 +81,6 @@ exclude = [ "examples/progressive-rendering", ] - - -[patch.crates-io] -#mt-dom = { git = "https://github.com/ivanceras/mt-dom.git", branch = "master" } -#mt-dom = { path = "../mt-dom" } -#jss = { git = "https://github.com/ivanceras/jss.git", branch = "master" } -#jss = { path = "../jss" } - -[dev-dependencies.criterion] -version = "0.5.1" -default-features = false - [package.metadata.docs.rs] all-features = true default-target = "wasm32-unknown-unknown" diff --git a/README.md b/README.md index 461cdac1..29186018 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ Sauron is inspired by elm-lang and is following The Elm Architecture. - html syntax for writing views - elegant macro to write styles - batteries included +- standalone DOM/VirtualDOM patcher + (see [`examples/patch_dom_node`](https://github.com/ivanceras/sauron/tree/master/examples/patch_dom_node)) ### Devoid of unnecessary framework complexities - **no** framework specific cli needed diff --git a/crates/core/src/dom.rs b/crates/core/src/dom.rs index 882a420c..2201a67e 100644 --- a/crates/core/src/dom.rs +++ b/crates/core/src/dom.rs @@ -12,11 +12,32 @@ mod effects; use cfg_if::cfg_if; cfg_if! {if #[cfg(feature = "with-dom")] { + mod application; + mod dom_node; + mod dom_patch; + mod dom_attr; + mod http; + mod program; + mod raf; + mod ric; + mod window; + mod document; + mod time; + mod timeout; + + pub mod events; + pub mod dispatch; + pub mod util; + pub use application::{Application, Measurements, SkipDiff, skip_if, skip_diff, SkipPath}; pub use component::{stateful_component, StatefulComponent, StatefulModel, StatelessModel}; pub use component::component; - pub use dom_patch::{DomPatch, PatchVariant}; + pub use dispatch::Dispatch; + pub use document::Document; + pub use dom_patch::{DomPatch, PatchVariant, apply_dom_patches, convert_patches}; pub use dom_attr::{DomAttr, DomAttrValue, GroupedDomAttrValues}; + pub use dom_node::DomNode; + pub use dom_node::create_dom_node; pub use http::Http; pub use program::{MountAction, MountTarget, Program, MountProcedure}; pub use util::{ @@ -26,29 +47,10 @@ cfg_if! {if #[cfg(feature = "with-dom")] { pub use raf::{request_animation_frame, AnimationFrameHandle}; pub use ric::{request_idle_callback, IdleCallbackHandle, IdleDeadline}; pub use timeout::{delay, request_timeout_callback, TimeoutCallbackHandle}; - pub use dispatch::Dispatch; - use crate::dom::events::MountEvent; pub use window::Window; - pub use dom_node::DomNode; - pub use document::Document; pub use time::Time; - mod application; - pub mod dispatch; - mod dom_node; - mod dom_patch; - mod dom_attr; - pub mod events; - mod http; - mod program; - pub mod util; - mod raf; - mod ric; - mod window; - mod document; - mod time; - mod timeout; - + use crate::dom::events::MountEvent; /// Map the Event to DomEvent, which are browser events #[derive(Debug, Clone)] diff --git a/crates/core/src/dom/dom_node.rs b/crates/core/src/dom/dom_node.rs index e7e464a3..777bacdc 100644 --- a/crates/core/src/dom/dom_node.rs +++ b/crates/core/src/dom/dom_node.rs @@ -1,26 +1,23 @@ -use crate::dom::component::StatelessModel; -use crate::dom::DomAttr; -use crate::dom::GroupedDomAttrValues; -use crate::dom::StatefulComponent; -use crate::dom::StatefulModel; -use crate::html::lookup; -use crate::vdom::TreePath; -use crate::{ - dom::document, - dom::events::MountEvent, - dom::{Application, Program}, - vdom, - vdom::{Attribute, Leaf}, +use std::{ + borrow::Cow, + cell::{Ref, RefCell}, + fmt, + rc::Rc, }; + use indexmap::IndexMap; -use std::borrow::Cow; -use std::cell::Ref; -use std::cell::RefCell; -use std::fmt; -use std::rc::Rc; use wasm_bindgen::{closure::Closure, JsCast, JsValue}; use web_sys::{self, Node}; +use crate::{ + dom::{ + component::StatelessModel, document, dom_patch, events::MountEvent, Application, DomAttr, + GroupedDomAttrValues, Program, StatefulComponent, StatefulModel, + }, + html::lookup, + vdom::{self, Attribute, Leaf, TreePath}, +}; + pub(crate) type EventClosure = Closure; pub type NamedEventClosures = IndexMap<&'static str, EventClosure>; @@ -33,6 +30,7 @@ pub type NamedEventClosures = IndexMap<&'static str, EventClosure>; pub struct DomNode { pub(crate) inner: DomInner, } + #[derive(Clone)] pub enum DomInner { /// a reference to an element node @@ -648,151 +646,133 @@ where { /// Create a dom node pub fn create_dom_node(&self, node: &vdom::Node) -> DomNode { - match node { - vdom::Node::Element(elm) => self.create_element_node(elm), - vdom::Node::Leaf(leaf) => self.create_leaf_node(leaf), - } + let ev_callback = self.create_ev_callback(); + create_dom_node(node, ev_callback) } - fn create_element_node(&self, elm: &vdom::Element) -> DomNode { - let document = document(); - let element = if let Some(namespace) = elm.namespace() { - document - .create_element_ns(Some(intern(namespace)), intern(elm.tag())) - .expect("Unable to create element") - } else { - document - .create_element(intern(elm.tag())) - .expect("create element") + pub(crate) fn create_ev_callback(&self) -> impl Fn(APP::MSG) + Clone { + let program = self.downgrade(); + let ev_callback = move |msg| { + let mut program = program.upgrade().expect("must upgrade"); + program.dispatch(msg); }; - // TODO: dispatch the mount event recursively after the dom node is mounted into - // the root node - let attrs = Attribute::merge_attributes_of_same_name(elm.attributes().iter()); - - let dom_node = DomNode { - inner: DomInner::Element { - element, - listeners: Rc::new(RefCell::new(None)), - children: Rc::new(RefCell::new(vec![])), - has_mount_callback: elm.has_mount_callback(), - }, - }; - let dom_attrs = attrs.iter().map(|a| self.convert_attr(a)); - dom_node.set_dom_attrs(dom_attrs).expect("set dom attrs"); - let children: Vec = elm - .children() - .iter() - .map(|child| self.create_dom_node(child)) - .collect(); - dom_node.append_children(children); - dom_node - } - - fn create_leaf_node(&self, leaf: &vdom::Leaf) -> DomNode { - match leaf { - Leaf::Text(txt) => DomNode { - inner: DomInner::Text(document().create_text_node(txt)), - }, - Leaf::Symbol(symbol) => DomNode { - inner: DomInner::Symbol(symbol.clone()), - }, - Leaf::Comment(comment) => DomNode { - inner: DomInner::Comment(document().create_comment(comment)), - }, - Leaf::Fragment(nodes) => self.create_fragment_node(nodes), - // NodeList that goes here is only possible when it is the root_node, - // since node_list as children will be unrolled into as child_elements of the parent - // We need to wrap this node_list into doc_fragment since root_node is only 1 element - Leaf::NodeList(nodes) => self.create_fragment_node(nodes), - Leaf::StatefulComponent(comp) => { - //TODO: also put the children and attributes here - DomNode { - inner: DomInner::StatefulComponent { - comp: Rc::clone(&comp.comp), - dom_node: Rc::new(self.create_stateful_component(comp)), - }, - } - } - Leaf::StatelessComponent(comp) => { - self.create_stateless_component(comp) - } - Leaf::TemplatedView(view) => { - unreachable!("template view should not be created: {:#?}", view) - } - Leaf::DocType(_) => unreachable!("doc type is never converted"), - } + ev_callback } +} - fn create_fragment_node<'a>( - &self, - nodes: impl IntoIterator>, - ) -> DomNode { - let fragment = document().create_document_fragment(); - let dom_node = DomNode { - inner: DomInner::Fragment { - fragment, - children: Rc::new(RefCell::new(vec![])), - }, - }; - let children = nodes - .into_iter() - .map(|node| self.create_dom_node(node)) - .collect(); - dom_node.append_children(children); - dom_node +/// Create a dom node +pub fn create_dom_node(node: &vdom::Node, ev_callback: F) -> DomNode +where + Msg: 'static, + F: Fn(Msg) + 'static + Clone, +{ + match node { + vdom::Node::Element(elm) => create_element_node(elm, ev_callback), + vdom::Node::Leaf(leaf) => create_leaf_node(leaf, ev_callback), } } -/// 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. -impl Program +fn create_element_node(elm: &vdom::Element, ev_callback: F) -> DomNode where - APP: Application, + Msg: 'static, + F: Fn(Msg) + 'static + Clone, { - /// TODO: register the template if not yet - /// pass a program to leaf component and mount itself and its view to the program - /// There are 2 types of children components of Stateful Component - /// - Internal children - /// - External children - /// Internal children is managed by the Stateful Component - /// while external children are managed by the top level program. - /// The external children can be diffed, and send the patches to the StatefulComponent - /// - The TreePath of the children starts at the external children location - /// The attributes affects the Stateful component state. - /// The attributes can be diff and send the patches to the StatefulComponent - /// - Changes to the attributes will call on attribute_changed of the StatefulComponent - fn create_stateful_component(&self, comp: &StatefulModel) -> DomNode { - let comp_node = self.create_dom_node(&crate::html::div( - [crate::html::attributes::class("component")] - .into_iter() - .chain(comp.attrs.clone()), - [], - )); + let document = document(); + let element = if let Some(namespace) = elm.namespace() { + document + .create_element_ns(Some(intern(namespace)), intern(elm.tag())) + .expect("Unable to create element") + } else { + document + .create_element(intern(elm.tag())) + .expect("create element") + }; + // TODO: dispatch the mount event recursively after the dom node is mounted into + // the root node + let attrs = Attribute::merge_attributes_of_same_name(elm.attributes().iter()); + + let dom_node = DomNode { + inner: DomInner::Element { + element, + listeners: Rc::new(RefCell::new(None)), + children: Rc::new(RefCell::new(vec![])), + has_mount_callback: elm.has_mount_callback(), + }, + }; + let dom_attrs = attrs + .iter() + .map(|a| dom_patch::convert_attr(a, ev_callback.clone())); + dom_node.set_dom_attrs(dom_attrs).expect("set dom attrs"); + let children: Vec = elm + .children() + .iter() + .map(|child| create_dom_node(child, ev_callback.clone())) + .collect(); + dom_node.append_children(children); + dom_node +} - let dom_attrs: Vec = comp.attrs.iter().map(|a| self.convert_attr(a)).collect(); - for dom_attr in dom_attrs.into_iter() { - log::info!("calling attribute changed.."); - comp.comp.borrow_mut().attribute_changed(dom_attr); +fn create_leaf_node(leaf: &vdom::Leaf, ev_callback: F) -> DomNode +where + Msg: 'static, + F: Fn(Msg) + 'static + Clone, +{ + match leaf { + Leaf::Text(txt) => DomNode { + inner: DomInner::Text(document().create_text_node(txt)), + }, + Leaf::Symbol(symbol) => DomNode { + inner: DomInner::Symbol(symbol.clone()), + }, + Leaf::Comment(comment) => DomNode { + inner: DomInner::Comment(document().create_comment(comment)), + }, + Leaf::Fragment(nodes) => create_fragment_node(nodes, ev_callback), + + // NodeList that goes here is only possible when it is the root_node, + // since node_list as children will be unrolled into as child_elements of the parent + // We need to wrap this node_list into doc_fragment since root_node is only 1 element + Leaf::NodeList(nodes) => create_fragment_node(nodes, ev_callback), + + Leaf::StatefulComponent(comp) => { + //TODO: also put the children and attributes here + DomNode { + inner: DomInner::StatefulComponent { + comp: Rc::clone(&comp.comp), + dom_node: Rc::new(create_stateful_component(comp, ev_callback)), + }, + } } + Leaf::StatelessComponent(comp) => create_stateless_component(comp, ev_callback), - // the component children is manually appended to the StatefulComponent - // here to allow the conversion of dom nodes with its event - // listener and removing the generics msg - let created_children = comp - .children - .iter() - .map(|child| self.create_dom_node(child)) - .collect(); - comp.comp.borrow_mut().append_children(created_children); - comp_node + Leaf::TemplatedView(view) => { + unreachable!("template view should not be created: {:#?}", view) + } + Leaf::DocType(_) => unreachable!("doc type is never converted"), } +} - #[allow(unused)] - pub(crate) fn create_stateless_component(&self, comp: &StatelessModel) -> DomNode { - let comp_view = &comp.view; - let real_comp_view = comp_view.unwrap_template_ref(); - self.create_dom_node(real_comp_view) - } +fn create_fragment_node<'a, Msg, F>( + nodes: impl IntoIterator>, + ev_callback: F, +) -> DomNode +where + Msg: 'static, + F: Fn(Msg) + 'static + Clone, +{ + let fragment = document().create_document_fragment(); + let dom_node = DomNode { + inner: DomInner::Fragment { + fragment, + children: Rc::new(RefCell::new(vec![])), + }, + }; + let children = nodes + .into_iter() + .map(|node| create_dom_node(node, ev_callback.clone())) + .collect(); + dom_node.append_children(children); + dom_node } /// render the underlying real dom node into string @@ -847,3 +827,67 @@ pub fn render_real_dom(node: &web_sys::Node, buffer: &mut dyn fmt::Write) -> fmt _ => todo!("for other else"), } } + +#[allow(unused)] +pub(crate) fn create_stateless_component( + comp: &StatelessModel, + ev_callback: F, +) -> DomNode +where + Msg: 'static, + F: Fn(Msg) + 'static + Clone, +{ + let comp_view = &comp.view; + let real_comp_view = comp_view.unwrap_template_ref(); + create_dom_node(real_comp_view, ev_callback) +} + +/// TODO: register the template if not yet +/// pass a program to leaf component and mount itself and its view to the program +/// There are 2 types of children components of Stateful Component +/// - Internal children +/// - External children +/// Internal children is managed by the Stateful Component +/// while external children are managed by the top level program. +/// The external children can be diffed, and send the patches to the StatefulComponent +/// - The TreePath of the children starts at the external children location +/// The attributes affects the Stateful component state. +/// The attributes can be diff and send the patches to the StatefulComponent +/// - Changes to the attributes will call on attribute_changed of the StatefulComponent +fn create_stateful_component(comp: &StatefulModel, ev_callback: F) -> DomNode +where + Msg: 'static, + F: Fn(Msg) + 'static + Clone, +{ + let comp_node = create_dom_node( + &crate::html::div( + [crate::html::attributes::class("component")] + .into_iter() + .chain(comp.attrs.clone()), + [], + ), + ev_callback.clone(), + ); + + let dom_attrs: Vec = comp + .attrs + .iter() + .map(|a| dom_patch::convert_attr(a, ev_callback.clone())) + .collect(); + + for dom_attr in dom_attrs.into_iter() { + log::info!("calling attribute changed.."); + comp.comp.borrow_mut().attribute_changed(dom_attr); + } + + // the component children is manually appended to the StatefulComponent + // here to allow the conversion of dom nodes with its event + // listener and removing the generics msg + let created_children = comp + .children + .iter() + .map(|child| create_dom_node(child, ev_callback.clone())) + .collect(); + comp.comp.borrow_mut().append_children(created_children); + comp_node +} diff --git a/crates/core/src/dom/dom_patch.rs b/crates/core/src/dom/dom_patch.rs index 94f05032..61b1cd30 100644 --- a/crates/core/src/dom/dom_patch.rs +++ b/crates/core/src/dom/dom_patch.rs @@ -1,18 +1,19 @@ -use crate::dom; -use crate::dom::dom_node; -use crate::dom::dom_node::DomInner; -use crate::dom::DomAttr; -use crate::dom::DomAttrValue; -use crate::dom::DomNode; -use crate::dom::{Application, Program}; -use crate::vdom::ComponentEventCallback; -use crate::vdom::EventCallback; -use crate::vdom::TreePath; -use crate::vdom::{Attribute, AttributeValue, Patch, PatchType}; +use std::{cell::RefCell, rc::Rc}; + use indexmap::IndexMap; use wasm_bindgen::closure::Closure; use wasm_bindgen::JsValue; +use crate::{ + dom::{ + self, dom_node, dom_node::DomInner, Application, DomAttr, DomAttrValue, DomNode, Program, + }, + vdom::{ + Attribute, AttributeValue, ComponentEventCallback, EventCallback, Patch, PatchType, + TreePath, + }, +}; + /// a Patch where the virtual nodes are all created in the document. /// This is necessary since the created Node doesn't contain references /// as opposed to Patch which contains reference to the vdom, which makes it hard @@ -146,97 +147,15 @@ impl Program where APP: Application + 'static, { - pub(crate) fn convert_attr(&self, attr: &Attribute) -> DomAttr { - DomAttr { - namespace: attr.namespace, - name: attr.name, - value: attr - .value - .iter() - .filter_map(|v| self.convert_attr_value(v)) - .collect(), - } - } - - fn convert_attr_value(&self, attr_value: &AttributeValue) -> Option { - match attr_value { - AttributeValue::Simple(v) => Some(DomAttrValue::Simple(v.clone())), - AttributeValue::Style(v) => Some(DomAttrValue::Style(v.clone())), - AttributeValue::EventListener(v) => { - Some(DomAttrValue::EventListener(self.convert_event_listener(v))) - } - AttributeValue::ComponentEventListener(v) => Some(DomAttrValue::EventListener( - self.convert_component_event_listener(v), - )), - AttributeValue::Empty => None, - } - } - - fn convert_event_listener( - &self, - event_listener: &EventCallback, - ) -> Closure { - let program = self.downgrade(); - let event_listener = event_listener.clone(); - let closure: Closure = - Closure::new(move |event: web_sys::Event| { - let msg = event_listener.emit(dom::Event::from(event)); - let mut program = program.upgrade().expect("must upgrade"); - program.dispatch(msg); - }); - closure - } - - fn convert_component_event_listener( - &self, - component_callback: &ComponentEventCallback, - ) -> Closure { - let component_callback = component_callback.clone(); - let closure: Closure = - Closure::new(move |event: web_sys::Event| { - component_callback.emit(dom::Event::from(event)); - }); - closure - } /// get the real DOM target node and make a DomPatch object for each of the Patch pub(crate) fn convert_patches( &self, target_node: &DomNode, patches: &[Patch], ) -> Result, JsValue> { - let nodes_to_find: Vec<(&TreePath, Option<&&'static str>)> = patches - .iter() - .map(|patch| (patch.path(), patch.tag())) - .chain( - patches - .iter() - .flat_map(|patch| patch.node_paths()) - .map(|path| (path, None)), - ) - .collect(); - - let nodes_lookup = target_node.find_all_nodes(&nodes_to_find); - - let dom_patches:Vec = patches.iter().map(|patch|{ - let patch_path = patch.path(); - let patch_tag = patch.tag(); - if let Some((target_node, target_parent)) = nodes_lookup.get(patch_path) { - let target_tag = target_node.tag(); - if let (Some(patch_tag), Some(target_tag)) = (patch_tag, target_tag) { - if **patch_tag != target_tag{ - panic!( - "expecting a tag: {patch_tag:?}, but found: {target_tag:?}" - ); - } - } - self.convert_patch(&nodes_lookup, target_node, target_parent, patch) - } else { - unreachable!("Getting here means we didn't find the element of next node that we are supposed to patch, patch_path: {:?}, with tag: {:?}", patch_path, patch_tag); - } - }).collect(); - - Ok(dom_patches) + convert_patches(target_node, patches, self.create_ev_callback()) } + /// convert a virtual DOM Patch into a created DOM node Patch pub fn convert_patch( &self, @@ -245,244 +164,382 @@ where target_parent: &DomNode, patch: &Patch, ) -> DomPatch { - let target_element = target_element.clone(); - let target_parent = target_parent.clone(); - let Patch { - patch_path, - patch_type, - .. - } = patch; + convert_patch( + nodes_lookup, + target_element, + target_parent, + patch, + self.create_ev_callback(), + ) + } +} - let patch_path = patch_path.clone(); +/// get the real DOM target node and make a DomPatch object for each of the Patch +pub fn convert_patches( + target_node: &DomNode, + patches: &[Patch], + ev_callback: F, +) -> Result, JsValue> +where + Msg: 'static, + F: Fn(Msg) + 'static + Clone, +{ + let nodes_to_find: Vec<(&TreePath, Option<&&'static str>)> = patches + .iter() + .map(|patch| (patch.path(), patch.tag())) + .chain( + patches + .iter() + .flat_map(|patch| patch.node_paths()) + .map(|path| (path, None)), + ) + .collect(); - match patch_type { - PatchType::InsertBeforeNode { nodes } => { - let nodes = nodes - .iter() - .map(|for_insert| self.create_dom_node(for_insert)) - .collect(); - DomPatch { - patch_path, - target_element, - target_parent, - patch_variant: PatchVariant::InsertBeforeNode { nodes }, - } - } - PatchType::InsertAfterNode { nodes } => { - let nodes = nodes - .iter() - .map(|for_insert| self.create_dom_node(for_insert)) - .collect(); - DomPatch { - patch_path, - target_element, - target_parent, - patch_variant: PatchVariant::InsertAfterNode { nodes }, + let nodes_lookup = target_node.find_all_nodes(&nodes_to_find); + + let dom_patches:Vec = patches.iter().map(|patch|{ + let patch_path = patch.path(); + let patch_tag = patch.tag(); + if let Some((target_node, target_parent)) = nodes_lookup.get(patch_path) { + let target_tag = target_node.tag(); + if let (Some(patch_tag), Some(target_tag)) = (patch_tag, target_tag) { + if **patch_tag != target_tag{ + panic!( + "expecting a tag: {patch_tag:?}, but found: {target_tag:?}" + ); } } + convert_patch(&nodes_lookup, target_node, target_parent, patch, ev_callback.clone()) + } else { + unreachable!("Getting here means we didn't find the element of next node that we are supposed to patch, patch_path: {:?}, with tag: {:?}", patch_path, patch_tag); + } + }).collect(); - PatchType::AddAttributes { attrs } => { - // we merge the attributes here prior to conversion - let attrs = Attribute::merge_attributes_of_same_name(attrs.iter().copied()); - DomPatch { - patch_path, - target_element, - target_parent, - patch_variant: PatchVariant::AddAttributes { - attrs: attrs.iter().map(|a| self.convert_attr(a)).collect(), - }, - } + Ok(dom_patches) +} + +/// convert a virtual DOM Patch into a created DOM node Patch +pub fn convert_patch( + nodes_lookup: &IndexMap, + target_element: &DomNode, + target_parent: &DomNode, + patch: &Patch, + ev_callback: F, +) -> DomPatch +where + Msg: 'static, + F: Fn(Msg) + 'static + Clone, +{ + let target_element = target_element.clone(); + let target_parent = target_parent.clone(); + let Patch { + patch_path, + patch_type, + .. + } = patch; + + let patch_path = patch_path.clone(); + + match patch_type { + PatchType::InsertBeforeNode { nodes } => { + let nodes = nodes + .iter() + .map(|for_insert| dom::create_dom_node(for_insert, ev_callback.clone())) + .collect(); + DomPatch { + patch_path, + target_element, + target_parent, + patch_variant: PatchVariant::InsertBeforeNode { nodes }, } - PatchType::RemoveAttributes { attrs } => DomPatch { + } + PatchType::InsertAfterNode { nodes } => { + let nodes = nodes + .iter() + .map(|for_insert| dom::create_dom_node(for_insert, ev_callback.clone())) + .collect(); + DomPatch { patch_path, target_element, target_parent, - patch_variant: PatchVariant::RemoveAttributes { - attrs: attrs.iter().map(|a| self.convert_attr(a)).collect(), + patch_variant: PatchVariant::InsertAfterNode { nodes }, + } + } + + PatchType::AddAttributes { attrs } => { + // we merge the attributes here prior to conversion + let attrs = Attribute::merge_attributes_of_same_name(attrs.iter().copied()); + DomPatch { + patch_path, + target_element, + target_parent, + patch_variant: PatchVariant::AddAttributes { + attrs: attrs + .iter() + .map(|a| convert_attr(a, ev_callback.clone())) + .collect(), }, + } + } + PatchType::RemoveAttributes { attrs } => DomPatch { + patch_path, + target_element, + target_parent, + patch_variant: PatchVariant::RemoveAttributes { + attrs: attrs + .iter() + .map(|a| convert_attr(a, ev_callback.clone())) + .collect(), }, + }, - PatchType::ReplaceNode { replacement } => { - let replacement = replacement - .iter() - .map(|node| self.create_dom_node(node)) - .collect(); - DomPatch { - patch_path, - target_element, - target_parent, - patch_variant: PatchVariant::ReplaceNode { replacement }, - } - } - PatchType::RemoveNode => DomPatch { + PatchType::ReplaceNode { replacement } => { + let replacement = replacement + .iter() + .map(|node| dom::create_dom_node(node, ev_callback.clone())) + .collect(); + DomPatch { patch_path, target_element, target_parent, - patch_variant: PatchVariant::RemoveNode, - }, - PatchType::ClearChildren => DomPatch { + patch_variant: PatchVariant::ReplaceNode { replacement }, + } + } + PatchType::RemoveNode => DomPatch { + patch_path, + target_element, + target_parent, + patch_variant: PatchVariant::RemoveNode, + }, + PatchType::ClearChildren => DomPatch { + patch_path, + target_element, + target_parent, + patch_variant: PatchVariant::ClearChildren, + }, + PatchType::MoveBeforeNode { nodes_path } => { + let for_moving = nodes_path + .iter() + .map(|path| { + let (node, _) = nodes_lookup.get(path).expect("must have found the node"); + node.clone() + }) + .collect(); + DomPatch { patch_path, target_element, target_parent, - patch_variant: PatchVariant::ClearChildren, - }, - PatchType::MoveBeforeNode { nodes_path } => { - let for_moving = nodes_path - .iter() - .map(|path| { - let (node, _) = nodes_lookup.get(path).expect("must have found the node"); - node.clone() - }) - .collect(); - DomPatch { - patch_path, - target_element, - target_parent, - patch_variant: PatchVariant::MoveBeforeNode { for_moving }, - } + patch_variant: PatchVariant::MoveBeforeNode { for_moving }, } - PatchType::MoveAfterNode { nodes_path } => { - let for_moving = nodes_path - .iter() - .map(|path| { - let (node, _) = nodes_lookup.get(path).expect("must have found the node"); - node.clone() - }) - .collect(); - DomPatch { - patch_path, - target_element, - target_parent, - patch_variant: PatchVariant::MoveAfterNode { for_moving }, - } + } + PatchType::MoveAfterNode { nodes_path } => { + let for_moving = nodes_path + .iter() + .map(|path| { + let (node, _) = nodes_lookup.get(path).expect("must have found the node"); + node.clone() + }) + .collect(); + DomPatch { + patch_path, + target_element, + target_parent, + patch_variant: PatchVariant::MoveAfterNode { for_moving }, } - PatchType::AppendChildren { children } => { - let children = children - .iter() - .map(|for_insert| self.create_dom_node(for_insert)) - .collect(); + } + PatchType::AppendChildren { children } => { + let children = children + .iter() + .map(|for_insert| dom::create_dom_node(for_insert, ev_callback.clone())) + .collect(); - DomPatch { - patch_path, - target_element, - target_parent, - patch_variant: PatchVariant::AppendChildren { children }, - } + DomPatch { + patch_path, + target_element, + target_parent, + patch_variant: PatchVariant::AppendChildren { children }, } } } +} - /// TODO: this should not have access to root_node, so it can generically - /// apply patch to any dom node - pub(crate) fn apply_dom_patches( - &self, - dom_patches: impl IntoIterator, - ) -> Result<(), JsValue> { - for dom_patch in dom_patches { - self.apply_dom_patch(dom_patch)?; - } - Ok(()) +pub(crate) fn convert_attr(attr: &Attribute, ev_callback: F) -> DomAttr +where + Msg: 'static, + F: Fn(Msg) + 'static + Clone, +{ + DomAttr { + namespace: attr.namespace, + name: attr.name, + value: attr + .value + .iter() + .filter_map(|v| convert_attr_value(v, ev_callback.clone())) + .collect(), } +} - /// apply a dom patch to this root node, - /// return a new root_node if it would replace the original root_node - /// TODO: this should have no access to root_node, so it can be used in general sense - pub(crate) fn apply_dom_patch(&self, dom_patch: DomPatch) -> Result<(), JsValue> { - let DomPatch { - patch_path, - target_element, - target_parent, - patch_variant, - } = dom_patch; +fn convert_attr_value( + attr_value: &AttributeValue, + ev_callback: F, +) -> Option +where + Msg: 'static, + F: Fn(Msg) + 'static, +{ + match attr_value { + AttributeValue::Simple(v) => Some(DomAttrValue::Simple(v.clone())), + AttributeValue::Style(v) => Some(DomAttrValue::Style(v.clone())), + AttributeValue::EventListener(v) => Some(DomAttrValue::EventListener( + convert_event_listener(v, ev_callback), + )), + AttributeValue::ComponentEventListener(v) => Some(DomAttrValue::EventListener( + convert_component_event_listener(v), + )), + AttributeValue::Empty => None, + } +} - match patch_variant { - PatchVariant::InsertBeforeNode { nodes } => { - target_parent.insert_before(&target_element, nodes); - } +fn convert_event_listener( + event_listener: &EventCallback, + callback: F, +) -> Closure +where + Msg: 'static, + F: Fn(Msg) + 'static, +{ + let event_listener = event_listener.clone(); + let closure: Closure = Closure::new(move |event: web_sys::Event| { + let msg = event_listener.emit(dom::Event::from(event)); + callback(msg); + }); + closure +} - PatchVariant::InsertAfterNode { nodes } => { - target_parent.insert_after(&target_element, nodes); - } - PatchVariant::AppendChildren { children } => { - target_element.append_children(children); - } +/// TODO: this should not have access to root_node, so it can generically +/// apply patch to any dom node +pub fn apply_dom_patches( + root_node: Rc>>, + mount_node: Rc>>, + dom_patches: impl IntoIterator, +) -> Result<(), JsValue> { + for dom_patch in dom_patches { + apply_dom_patch(Rc::clone(&root_node), Rc::clone(&mount_node), dom_patch)?; + } + Ok(()) +} - PatchVariant::AddAttributes { attrs } => { - target_element.set_dom_attrs(attrs).unwrap(); - } - PatchVariant::RemoveAttributes { attrs } => { - for attr in attrs.iter() { - for att_value in attr.value.iter() { - match att_value { - DomAttrValue::Simple(_) => { - target_element.remove_dom_attr(attr)?; - } - // it is an event listener - DomAttrValue::EventListener(_) => { - let DomInner::Element { listeners, .. } = &target_element.inner - else { - unreachable!("must be an element"); - }; - if let Some(listener) = listeners.borrow_mut().as_mut() { - listener.retain(|event, _| *event != attr.name) - } - } - DomAttrValue::Style(_) => { - target_element.remove_dom_attr(attr)?; +/// apply a dom patch to this root node, +/// return a new root_node if it would replace the original root_node +/// TODO: this should have no access to root_node, so it can be used in general sense +pub(crate) fn apply_dom_patch( + root_node: Rc>>, + mount_node: Rc>>, + dom_patch: DomPatch, +) -> Result<(), JsValue> { + let DomPatch { + patch_path, + target_element, + target_parent, + patch_variant, + } = dom_patch; + + match patch_variant { + PatchVariant::InsertBeforeNode { nodes } => { + target_parent.insert_before(&target_element, nodes); + } + + PatchVariant::InsertAfterNode { nodes } => { + target_parent.insert_after(&target_element, nodes); + } + PatchVariant::AppendChildren { children } => { + target_element.append_children(children); + } + + PatchVariant::AddAttributes { attrs } => { + target_element.set_dom_attrs(attrs).unwrap(); + } + PatchVariant::RemoveAttributes { attrs } => { + for attr in attrs.iter() { + for att_value in attr.value.iter() { + match att_value { + DomAttrValue::Simple(_) => { + target_element.remove_dom_attr(attr)?; + } + // it is an event listener + DomAttrValue::EventListener(_) => { + let DomInner::Element { listeners, .. } = &target_element.inner else { + unreachable!("must be an element"); + }; + if let Some(listener) = listeners.borrow_mut().as_mut() { + listener.retain(|event, _| *event != attr.name) } - DomAttrValue::Empty => (), } + DomAttrValue::Style(_) => { + target_element.remove_dom_attr(attr)?; + } + DomAttrValue::Empty => (), } } } + } - // This also removes the associated closures and event listeners to the node being replaced - // including the associated closures of the descendant of replaced node - // before it is actully replaced in the DOM - // TODO: make root node a Vec - PatchVariant::ReplaceNode { mut replacement } => { - let first_node = replacement.remove(0); + // This also removes the associated closures and event listeners to the node being replaced + // including the associated closures of the descendant of replaced node + // before it is actully replaced in the DOM + // TODO: make root node a Vec + PatchVariant::ReplaceNode { mut replacement } => { + let first_node = replacement.remove(0); - if target_element.is_fragment() { - assert!( - patch_path.is_empty(), - "this should only happen to root node" - ); - let mut mount_node = self.mount_node.borrow_mut(); + if target_element.is_fragment() { + assert!( + patch_path.is_empty(), + "this should only happen to root node" + ); + let mut mount_node = mount_node.borrow_mut(); + let mount_node = mount_node.as_mut().expect("must have a mount node"); + mount_node.append_children(vec![first_node.clone()]); + mount_node.append_children(replacement); + } else { + if patch_path.path.is_empty() { + let mut mount_node = mount_node.borrow_mut(); let mount_node = mount_node.as_mut().expect("must have a mount node"); - mount_node.append_children(vec![first_node.clone()]); - mount_node.append_children(replacement); + mount_node.replace_child(&target_element, first_node.clone()); } else { - if patch_path.path.is_empty() { - let mut mount_node = self.mount_node.borrow_mut(); - let mount_node = mount_node.as_mut().expect("must have a mount node"); - mount_node.replace_child(&target_element, first_node.clone()); - } else { - target_parent.replace_child(&target_element, first_node.clone()); - } - //insert the rest - target_parent.insert_after(&first_node, replacement); - } - if patch_path.path.is_empty() { - *self.root_node.borrow_mut() = Some(first_node); + target_parent.replace_child(&target_element, first_node.clone()); } + //insert the rest + target_parent.insert_after(&first_node, replacement); } - PatchVariant::RemoveNode => { - target_parent.remove_children(&[&target_element]); - } - PatchVariant::ClearChildren => { - target_element.clear_children(); - } - PatchVariant::MoveBeforeNode { for_moving } => { - target_parent.remove_children(&for_moving.iter().collect::>()); - target_parent.insert_before(&target_element, for_moving); + if patch_path.path.is_empty() { + *root_node.borrow_mut() = Some(first_node); } + } + PatchVariant::RemoveNode => { + target_parent.remove_children(&[&target_element]); + } + PatchVariant::ClearChildren => { + target_element.clear_children(); + } + PatchVariant::MoveBeforeNode { for_moving } => { + target_parent.remove_children(&for_moving.iter().collect::>()); + target_parent.insert_before(&target_element, for_moving); + } - PatchVariant::MoveAfterNode { for_moving } => { - target_parent.remove_children(&for_moving.iter().collect::>()); - target_parent.insert_after(&target_element, for_moving); - } + PatchVariant::MoveAfterNode { for_moving } => { + target_parent.remove_children(&for_moving.iter().collect::>()); + target_parent.insert_after(&target_element, for_moving); } - Ok(()) } + Ok(()) +} + +fn convert_component_event_listener( + component_callback: &ComponentEventCallback, +) -> Closure { + let component_callback = component_callback.clone(); + let closure: Closure = Closure::new(move |event: web_sys::Event| { + component_callback.emit(dom::Event::from(event)); + }); + closure } diff --git a/crates/core/src/dom/program.rs b/crates/core/src/dom/program.rs index 47f3bae7..d3b1758f 100644 --- a/crates/core/src/dom/program.rs +++ b/crates/core/src/dom/program.rs @@ -1,35 +1,30 @@ -use crate::dom::program::app_context::WeakContext; -#[cfg(feature = "with-raf")] -use crate::dom::request_animation_frame; -#[cfg(feature = "with-ric")] -use crate::dom::request_idle_callback; -use crate::dom::DomNode; -use crate::dom::SkipDiff; -use crate::dom::SkipPath; -use crate::dom::{document, now, IdleDeadline, Measurements}; -use crate::dom::{util::body, AnimationFrameHandle, Application, DomPatch, IdleCallbackHandle}; -use crate::html::{self, attributes::class, text}; -use crate::vdom; -use crate::vdom::diff; -use crate::vdom::diff_recursive; -use crate::vdom::Patch; -use std::collections::hash_map::DefaultHasher; -use std::collections::VecDeque; -use std::hash::{Hash, Hasher}; -use std::mem::ManuallyDrop; use std::{ any::TypeId, cell::{Ref, RefCell, RefMut}, + collections::{hash_map::DefaultHasher, VecDeque}, + hash::{Hash, Hasher}, + mem::ManuallyDrop, rc::Rc, rc::Weak, }; + use wasm_bindgen::{JsCast, JsValue}; -use web_sys; -pub(crate) use app_context::AppContext; -pub use mount_procedure::{MountAction, MountProcedure, MountTarget}; +use crate::{ + dom::{ + document, dom_patch, now, program::app_context::WeakContext, util::body, + AnimationFrameHandle, Application, DomNode, DomPatch, IdleCallbackHandle, IdleDeadline, + Measurements, SkipDiff, SkipPath, + }, + html::{self, attributes::class, text}, + vdom::{self, diff, diff_recursive, Patch}, +}; +#[cfg(feature = "with-raf")] +use crate::dom::request_animation_frame; +#[cfg(feature = "with-ric")] +use crate::dom::request_idle_callback; thread_local! { static CANCEL_CNT: RefCell = RefCell::new(0); @@ -40,7 +35,10 @@ thread_local! { } mod app_context; +use self::app_context::AppContext; + mod mount_procedure; +pub use self::mount_procedure::{MountAction, MountProcedure, MountTarget}; /// Program handle the lifecycle of the APP pub struct Program @@ -451,17 +449,24 @@ where let mut program = self.clone(); //#[cfg(feature = "with-debounce")] crate::dom::request_timeout_callback( - move||{ + move || { program.update_dom().unwrap(); - }, remaining.round() as i32).unwrap(); + }, + remaining.round() as i32, + ) + .unwrap(); log::info!("update is cancelled.."); - CANCEL_CNT.with_borrow_mut(|c|*c += 1); - return Ok(()) + CANCEL_CNT.with_borrow_mut(|c| *c += 1); + return Ok(()); } } log::info!("Doing and update..."); - UPDATE_CNT.with_borrow_mut(|c|*c += 1); - log::info!("ratio(cancelled/update): {}/{}", CANCEL_CNT.with_borrow(|c|*c), UPDATE_CNT.with_borrow(|c|*c)); + UPDATE_CNT.with_borrow_mut(|c| *c += 1); + log::info!( + "ratio(cancelled/update): {}/{}", + CANCEL_CNT.with_borrow(|c| *c), + UPDATE_CNT.with_borrow(|c| *c) + ); // a new view is created due to the app update let view = self.app_context.view(); let t2 = now(); @@ -489,7 +494,13 @@ where ) .expect("must convert patches") } else { - self.create_dom_patch(&view) + let current_vdom = self.app_context.current_vdom(); + create_dom_patch( + &self.root_node, + ¤t_vdom, + &view, + self.create_ev_callback(), + ) }; let total_patches = dom_patches.len(); @@ -519,7 +530,6 @@ where } } - // tell the app about the performance measurement and only if there was patches applied #[cfg(feature = "with-measure")] self.app_context.measurements(measurements); @@ -559,26 +569,6 @@ where ) } - fn create_dom_patch(&self, new_vdom: &vdom::Node) -> Vec { - let current_vdom = self.app_context.current_vdom(); - let patches = diff(¤t_vdom, new_vdom); - - #[cfg(all(feature = "with-debug", feature = "log-patches"))] - { - log::debug!("There are {} patches", patches.len()); - log::debug!("patches: {patches:#?}"); - } - - self.convert_patches( - self.root_node - .borrow() - .as_ref() - .expect("must have a root node"), - &patches, - ) - .expect("must convert patches") - } - #[cfg(feature = "with-raf")] fn apply_pending_patches_with_raf(&mut self) -> Result<(), JsValue> { let program = Program::downgrade(&self); @@ -597,7 +587,11 @@ where return Ok(()); } let dom_patches: Vec = self.pending_patches.borrow_mut().drain(..).collect(); - self.apply_dom_patches(dom_patches)?; + dom_patch::apply_dom_patches( + Rc::clone(&self.root_node), + Rc::clone(&self.mount_node), + dom_patches, + )?; Ok(()) } @@ -743,6 +737,44 @@ where pub fn dispatch(&mut self, msg: APP::MSG) { self.dispatch_multiple([msg]) } + + /// patch the DOM to reflect the App's view + /// + /// Note: This is in another function so as to allow tests to use this shared code + pub fn create_dom_patch(&self, new_vdom: &vdom::Node) -> Vec { + create_dom_patch( + &self.root_node, + &self.app_context.current_vdom(), + new_vdom, + self.create_ev_callback(), + ) + } +} + +fn create_dom_patch( + root_node: &Rc>>, + current_vdom: &vdom::Node, + new_vdom: &vdom::Node, + ev_callback: F, +) -> Vec +where + Msg: 'static, + F: Fn(Msg) + 'static + Clone, +{ + let patches = diff(¤t_vdom, new_vdom); + + #[cfg(all(feature = "with-debug", feature = "log-patches"))] + { + log::debug!("There are {} patches", patches.len()); + log::debug!("patches: {patches:#?}"); + } + + dom_patch::convert_patches( + root_node.borrow().as_ref().expect("must have a root node"), + &patches, + ev_callback, + ) + .expect("must convert patches") } impl Program diff --git a/crates/html-parser/src/lib.rs b/crates/html-parser/src/lib.rs index 2272928c..67b775ac 100644 --- a/crates/html-parser/src/lib.rs +++ b/crates/html-parser/src/lib.rs @@ -1,17 +1,17 @@ #![deny(warnings)] -use rphtml::config::ParseOptions; -use rphtml::parser::Doc; -use rphtml::parser::NodeType; -use rphtml::types::BoxDynError; + +use std::{fmt, io, ops::Deref}; + +use rphtml::{ + config::ParseOptions, + parser::{Doc, NodeType}, + types::BoxDynError, +}; + use sauron_core::{ html::{attributes::*, lookup, *}, - vdom::AttributeValue, - vdom::Node, - vdom::Value, + vdom::{AttributeValue, Node, Value}, }; -use std::fmt; -use std::io; -use std::ops::Deref; /// all the possible error when parsing html string #[derive(Debug, thiserror::Error)] @@ -30,7 +30,8 @@ pub enum ParseError { InvalidTag(String), } -/// parse the html string and build a node tree +/// Parse escaped html strings like `"Hello world!"` +/// into `"Hello world!"` and then into a node tree. pub fn raw_html(html: &str) -> Node { // decode html entitiesd back since it will be safely converted into text let html = html_escape::decode_html_entities(html); @@ -39,7 +40,8 @@ pub fn raw_html(html: &str) -> Node { .expect("must have a node") } -/// the document is not wrapped with html +/// Parse none-escaped html strings like `"Hello world!"` +/// into a node tree (see also [raw_html]). pub fn parse_html(html: &str) -> Result>, ParseError> { let doc = Doc::parse( html, diff --git a/crates/macro/src/lib.rs b/crates/macro/src/lib.rs index ad38263f..3722cf96 100644 --- a/crates/macro/src/lib.rs +++ b/crates/macro/src/lib.rs @@ -209,7 +209,6 @@ pub fn extract_skip_diff(input: proc_macro::TokenStream) -> proc_macro::TokenStr extract_skip_diff::to_token_stream(input).into() } - /// build a css string /// /// # Example: diff --git a/examples/patch_dom_node/Cargo.toml b/examples/patch_dom_node/Cargo.toml new file mode 100644 index 00000000..6b54f086 --- /dev/null +++ b/examples/patch_dom_node/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "patch_dom_node" +version = "0.1.0" +edition = "2021" + +[dependencies] +console_error_panic_hook = "0.1.7" +console_log = { version = "1.0.0", features = ["color"] } +log = "0.4.25" +sauron-core = { version = "0.61.9", path = "../../crates/core" } +sauron-html-parser = { version = "0.61.9", path = "../../crates/html-parser" } +web-sys = "0.3.77" diff --git a/examples/patch_dom_node/index.html b/examples/patch_dom_node/index.html new file mode 100644 index 00000000..60b6255a --- /dev/null +++ b/examples/patch_dom_node/index.html @@ -0,0 +1,9 @@ + + + + + + Sauron • Use VDOM + + + diff --git a/examples/patch_dom_node/src/main.rs b/examples/patch_dom_node/src/main.rs new file mode 100644 index 00000000..d43c3dea --- /dev/null +++ b/examples/patch_dom_node/src/main.rs @@ -0,0 +1,50 @@ +use std::{cell::RefCell, rc::Rc}; + +use sauron_core::{ + dom::{self, DomNode}, + prelude::Node, + vdom, +}; +use sauron_html_parser::parse_html; + +fn main() { + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + log::info!("Run VDOM patch example"); + + let ev_callback = |_| {}; + + let body_node = DomNode::from(web_sys::Node::from(dom::util::body())); + let mount_node = Rc::new(RefCell::new(Some(body_node))); + + let new_html = r#" +
+
+ Hello world! + If you can see this text on the webpage then the DOM patching was executed! +
+
This is a footer
+
"#; + + let old_node: Node<()> = parse_html::<()>("").unwrap().unwrap(); + let new_node: Node<()> = parse_html::<()>(new_html).unwrap().unwrap(); + + let root = dom::create_dom_node(&old_node, ev_callback); + let root_node = Rc::new(RefCell::new(Some(root))); + + let vdom_patches = vdom::diff(&old_node, &new_node); + log::info!("Created {} VDOM patch(es)", vdom_patches.len()); + log::debug!("VDOM patch(es): {vdom_patches:?}"); + + // convert vdom patch to real dom patches + let dom_patches = dom::convert_patches( + &root_node.borrow().as_ref().unwrap(), + &vdom_patches, + ev_callback, + ) + .unwrap(); + log::info!("Converted {} DOM patch(es)", dom_patches.len()); + log::debug!("DOM patch(es): {dom_patches:?}"); + + dom::apply_dom_patches(root_node, mount_node, dom_patches).unwrap(); +} diff --git a/tests/dom_vdom_standalone.rs b/tests/dom_vdom_standalone.rs new file mode 100644 index 00000000..b3131ce6 --- /dev/null +++ b/tests/dom_vdom_standalone.rs @@ -0,0 +1,67 @@ +use std::{cell::RefCell, rc::Rc}; + +use wasm_bindgen_test::*; +use web_sys::{Element, HtmlElement}; + +use sauron_core::{ + dom::{self, DomNode}, + prelude::Node, + vdom, +}; +use sauron_html_parser::{parse_html, raw_html}; + +wasm_bindgen_test_configure!(run_in_browser); + +// Verify that our DomUpdater's patch method works. +// We test a simple case here, since diff_patch.rs is responsible for testing more complex +// diffing and patching. +#[wasm_bindgen_test] +fn test_dom_vdom_standalone() { + console_log::init_with_level(log::Level::Trace).unwrap(); + console_error_panic_hook::set_once(); + + let ev_callback = |_| {}; + + let window = web_sys::window().expect("no global `window` exists"); + let document = window.document().expect("should have a document on window"); + + let div: Element = document.create_element("div").unwrap(); + div.set_attribute("id", "here").unwrap(); + document.body().unwrap().append_child(&div).unwrap(); + + let web_sys_node: web_sys::Node = web_sys::Node::from(div); + let div_node = DomNode::from(web_sys_node); + let mount_node = Rc::new(RefCell::new(Some(div_node))); + + let new_html = r#" +
boak
+ "#; + + let old_node: Node<()> = parse_html::<()>("").unwrap().unwrap(); + let new_node: Node<()> = raw_html::<()>(new_html); + let root = dom::create_dom_node(&old_node, ev_callback); + let root_node = Rc::new(RefCell::new(Some(root))); + + let vdom_patches = vdom::diff(&old_node, &new_node); + log::debug!("Created {} VDOM patch(es)", vdom_patches.len()); + log::debug!("Created {:?}", vdom_patches); + + // convert vdom patch to real dom patches + let dom_patches = dom::convert_patches( + &root_node.borrow().as_ref().unwrap(), + &vdom_patches, + ev_callback, + ) + .unwrap(); + log::debug!("Converted {} DOM patch(es)", dom_patches.len()); + log::debug!("Converted {:?}", dom_patches); + + dom::apply_dom_patches(root_node, mount_node, dom_patches).unwrap(); + + let target: Element = document.get_element_by_id("here").unwrap(); + + // Get the inner HTML from the body element + let html_content: String = target.inner_html(); + + assert_eq!("
boak
".to_string(), html_content); +}