feat: a new vnode type for anchors

This commit is contained in:
Jonathan Kelley 2021-07-29 18:04:09 -04:00
parent 63a85e4865
commit d618092e9d
15 changed files with 715 additions and 441 deletions

View file

@ -41,6 +41,7 @@ slab = "0.4.3"
[dev-dependencies]
anyhow = "1.0.42"
dioxus-html = { path = "../html" }

View file

@ -2,18 +2,18 @@
This is the core crate for the Dioxus Virtual DOM. This README will focus on the technical design and layout of this Virtual DOM implementation. If you want to read more about using Dioxus, then check out the Dioxus crate, documentation, and website.
We reserve the "dioxus" name and aggregate all the various renderers under it. If you want just a single dioxus renderer, then chose from "dioxus-web", "dioxus-desktop", etc.
To build new apps with Dioxus or to extend the ecosystem with new hooks or components, use the `Dioxus` crate with the appropriate feature flags.
## Internals
Dioxus-core builds off the many frameworks that came before it. Notably, Dioxus borrows these concepts:
- React: hooks, concurrency, suspense
- Dodrio: bump allocation, double buffering, and source code for NodeBuilder
- Dodrio: bump allocation, double buffering, and some diffing architecture
- Percy: html! macro architecture, platform-agnostic edits
- Yew: passion and inspiration ❤️
- InfernoJS: approach to fragments and node diffing
- InfernoJS: approach to keyed diffing
- Preact: approach for normalization and ref
- Yew: passion and inspiration ❤️
Dioxus-core leverages some really cool techniques and hits a very high level of parity with mature frameworks. Some unique features include:

View file

@ -5,7 +5,9 @@ use std::{cell::UnsafeCell, rc::Rc};
use crate::heuristics::*;
use crate::innerlude::*;
use futures_util::stream::FuturesUnordered;
use fxhash::{FxHashMap, FxHashSet};
use slab::Slab;
use smallvec::SmallVec;
// slotmap::new_key_type! {
// // A dedicated key type for the all the scopes
@ -47,7 +49,7 @@ pub struct SharedResources {
/// We use a SlotSet to keep track of the keys that are currently being used.
/// However, we don't store any specific data since the "mirror"
pub raw_elements: Shared<Slab<()>>,
pub raw_elements: Rc<RefCell<Slab<()>>>,
pub task_setter: Rc<dyn Fn(ScopeId)>,
}
@ -133,7 +135,10 @@ impl SharedResources {
}
/// return the id, freeing the space of the original node
pub fn collect_garbage(&self, id: ElementId) {}
pub fn collect_garbage(&self, id: ElementId) {
let mut r: RefMut<Slab<()>> = self.raw_elements.borrow_mut();
r.remove(id.0);
}
pub fn borrow_queue(&self) -> RefMut<Vec<HeightMarker>> {
self.event_queue.borrow_mut()

View file

@ -138,8 +138,8 @@ impl<'src, P> Context<'src, P> {
self,
lazy_nodes: LazyNodes<'src, F>,
) -> DomTree<'src> {
let scope_ref = self.scope;
Some(lazy_nodes.into_vnode(NodeFactory { scope: scope_ref }))
let bump = &self.scope.frames.wip_frame().bump;
Some(lazy_nodes.into_vnode(NodeFactory { bump }))
}
/// `submit_task` will submit the future to be polled.
@ -178,7 +178,7 @@ impl<'src, P> Context<'src, P> {
///
pub fn consume_shared_state<T: 'static>(self) -> Option<Rc<T>> {
let mut scope = Some(self.scope);
let mut par = None;
let mut parent = None;
let ty = TypeId::of::<T>();
while let Some(inner) = scope {
@ -202,7 +202,7 @@ impl<'src, P> Context<'src, P> {
.clone()
.downcast::<T>()
.expect("Should not fail, already validated the type from the hashmap");
par = Some(rc);
parent = Some(rc);
break;
} else {
match inner.parent_idx {
@ -213,7 +213,7 @@ impl<'src, P> Context<'src, P> {
}
}
}
par
parent
}
/// Store a value between renders

File diff suppressed because it is too large Load diff

View file

@ -64,3 +64,24 @@ pub enum DomEdit<'bump> {
name: &'static str,
},
}
impl DomEdit<'_> {
pub fn is(&self, id: &'static str) -> bool {
match self {
DomEdit::PushRoot { .. } => id == "PushRoot",
DomEdit::PopRoot => id == "PopRoot",
DomEdit::AppendChildren { .. } => id == "AppendChildren",
DomEdit::ReplaceWith { .. } => id == "ReplaceWith",
DomEdit::Remove => id == "Remove",
DomEdit::RemoveAllChildren => id == "RemoveAllChildren",
DomEdit::CreateTextNode { .. } => id == "CreateTextNode",
DomEdit::CreateElement { .. } => id == "CreateElement",
DomEdit::CreateElementNs { .. } => id == "CreateElementNs",
DomEdit::CreatePlaceholder { .. } => id == "CreatePlaceholder",
DomEdit::NewEventListener { .. } => id == "NewEventListener",
DomEdit::RemoveEventListener { .. } => id == "RemoveEventListener",
DomEdit::SetText { .. } => id == "SetText",
DomEdit::SetAttribute { .. } => id == "SetAttribute",
DomEdit::RemoveAttribute { .. } => id == "RemoveAttribute",
}
}
}

View file

@ -9,7 +9,10 @@ use std::{
rc::Rc,
};
use crate::innerlude::{ElementId, HeightMarker, ScopeId};
use crate::{
innerlude::{ElementId, HeightMarker, ScopeId},
VNode,
};
#[derive(Debug)]
pub struct EventTrigger {
@ -46,15 +49,16 @@ impl EventTrigger {
/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes.
#[derive(Debug)]
pub enum EventPriority {
/// "Immediate" work will interrupt whatever work is currently being done and force its way through. This type of work
/// is typically reserved for small changes to single elements.
/// Garbage collection is a type of work than can be scheduled around other work, but must be completed in a specific
/// order. The GC must be run for a component before any other future work for that component is run. Otherwise,
/// we will leak slots in our slab.
///
/// The primary user of the "Immediate" priority is the `Signal` API which performs surgical mutations to the DOM.
Immediate,
/// Garbage collection mixes with the safety aspects of the virtualdom so it's very important to get it done before
/// other work.
GarbageCollection,
/// "High Priority" work will not interrupt other high priority work, but will interrupt long medium and low priority work.
///
///
/// This is typically reserved for things like user interaction.
High,
@ -106,6 +110,8 @@ pub enum VirtualEvent {
MouseEvent(on::MouseEvent),
PointerEvent(on::PointerEvent),
GarbageCollection,
// image event has conflicting method types
// ImageEvent(event_data::ImageEvent),
SetStateEvent {
@ -198,7 +204,6 @@ pub mod on {
Listener {
event: shortname,
mounted_node: Cell::new(None),
scope: c.scope.our_arena_idx,
callback: RefCell::new(Some(callback)),
}
}

View file

@ -190,11 +190,10 @@ where
None => {
//
Some(VNode {
dom_id: empty_cell(),
key: None,
kind: VNodeKind::Suspended {
kind: VNodeKind::Suspended(VSuspended {
node: domnode.clone(),
},
}),
})
}
}
@ -250,7 +249,8 @@ impl<'src> SuspendedContext<'src> {
lazy_nodes: LazyNodes<'src, F>,
) -> DomTree<'src> {
let scope_ref = self.inner.scope;
let bump = &self.inner.scope.frames.wip_frame().bump;
Some(lazy_nodes.into_vnode(NodeFactory { scope: scope_ref }))
Some(lazy_nodes.into_vnode(NodeFactory { bump }))
}
}

View file

@ -1,4 +1,5 @@
#![allow(non_snake_case)]
#![doc = include_str!("../README.md")]
//! Dioxus Core
//! ----------
//!

View file

@ -7,7 +7,7 @@ use crate::{
events::VirtualEvent,
innerlude::{empty_cell, Context, DomTree, ElementId, Properties, Scope, ScopeId, FC},
};
use bumpalo::boxed::Box as BumpBox;
use bumpalo::{boxed::Box as BumpBox, Bump};
use std::{
cell::{Cell, RefCell},
fmt::{Arguments, Debug, Formatter},
@ -20,16 +20,24 @@ pub struct VNode<'src> {
pub kind: VNodeKind<'src>,
pub(crate) key: Option<&'src str>,
/// ElementId supports NonZero32 and Cell is zero cost, so the size of this field is unaffected
pub(crate) dom_id: Cell<Option<ElementId>>,
}
impl VNode<'_> {
pub fn key(&self) -> Option<&str> {
impl<'src> VNode<'src> {
pub fn key(&self) -> Option<&'src str> {
self.key
}
pub fn element_id(&self) -> Option<ElementId> {
self.dom_id.get()
pub fn direct_id(&self) -> ElementId {
self.try_direct_id().unwrap()
}
pub fn try_direct_id(&self) -> Option<ElementId> {
match &self.kind {
VNodeKind::Text(el) => el.dom_id.get(),
VNodeKind::Element(el) => el.dom_id.get(),
VNodeKind::Anchor(el) => el.dom_id.get(),
VNodeKind::Fragment(_) => None,
VNodeKind::Component(_) => None,
VNodeKind::Suspended(_) => None,
}
}
}
@ -47,11 +55,18 @@ pub enum VNodeKind<'src> {
Component(&'src VComponent<'src>),
Suspended { node: Rc<Cell<Option<ElementId>>> },
Suspended(VSuspended),
Anchor(VAnchor),
}
pub struct VAnchor {
pub dom_id: Cell<Option<ElementId>>,
}
pub struct VText<'src> {
pub text: &'src str,
pub dom_id: Cell<Option<ElementId>>,
pub is_static: bool,
}
@ -77,6 +92,7 @@ pub struct VElement<'a> {
// tag is always static
pub tag_name: &'static str,
pub namespace: Option<&'static str>,
pub dom_id: Cell<Option<ElementId>>,
pub static_listeners: bool,
pub listeners: &'a [Listener<'a>],
@ -110,8 +126,6 @@ pub struct Listener<'bump> {
/// The type of event to listen for.
pub(crate) event: &'static str,
pub scope: ScopeId,
pub mounted_node: Cell<Option<ElementId>>,
pub(crate) callback: RefCell<Option<BumpBox<'bump, dyn FnMut(VirtualEvent) + 'bump>>>,
@ -150,27 +164,42 @@ pub struct VComponent<'src> {
pub(crate) user_fc: *const (),
}
pub struct VSuspended {
pub node: Rc<Cell<Option<ElementId>>>,
}
/// This struct provides an ergonomic API to quickly build VNodes.
///
/// NodeFactory is used to build VNodes in the component's memory space.
/// This struct adds metadata to the final VNode about listeners, attributes, and children
#[derive(Copy, Clone)]
pub struct NodeFactory<'a> {
pub scope: &'a Scope,
pub(crate) bump: &'a Bump,
}
impl<'a> NodeFactory<'a> {
pub fn new(bump: &'a Bump) -> NodeFactory<'a> {
NodeFactory { bump }
}
#[inline]
pub fn bump(&self) -> &'a bumpalo::Bump {
&self.scope.frames.wip_frame().bump
&self.bump
}
pub fn render_directly<F>(&self, lazy_nodes: LazyNodes<'a, F>) -> DomTree<'a>
where
F: FnOnce(NodeFactory<'a>) -> VNode<'a>,
{
Some(lazy_nodes.into_vnode(NodeFactory { bump: self.bump }))
}
pub fn unstable_place_holder() -> VNode<'static> {
VNode {
dom_id: empty_cell(),
key: None,
kind: VNodeKind::Text(VText {
text: "",
dom_id: empty_cell(),
is_static: true,
}),
}
@ -179,9 +208,9 @@ impl<'a> NodeFactory<'a> {
/// Used in a place or two to make it easier to build vnodes from dummy text
pub fn static_text(&self, text: &'static str) -> VNode<'a> {
VNode {
dom_id: empty_cell(),
key: None,
kind: VNodeKind::Text(VText {
dom_id: empty_cell(),
text,
is_static: true,
}),
@ -208,9 +237,12 @@ impl<'a> NodeFactory<'a> {
pub fn text(&self, args: Arguments) -> VNode<'a> {
let (text, is_static) = self.raw_text(args);
VNode {
dom_id: empty_cell(),
key: None,
kind: VNodeKind::Text(VText { text, is_static }),
kind: VNodeKind::Text(VText {
text,
is_static,
dom_id: empty_cell(),
}),
}
}
@ -260,19 +292,7 @@ impl<'a> NodeFactory<'a> {
let children: &'a V = self.bump().alloc(children);
let children = children.as_ref();
// We take the references directly from the bump arena
//
// TODO: this code shouldn't necessarily be here of all places
// It would make more sense to do this in diffing
let mut queue = self.scope.listeners.borrow_mut();
for listener in listeners.iter() {
let long_listener: &'a Listener<'static> = unsafe { std::mem::transmute(listener) };
queue.push(long_listener as *const _)
}
VNode {
dom_id: empty_cell(),
key,
kind: VNodeKind::Element(self.bump().alloc(VElement {
tag_name: tag,
@ -280,6 +300,7 @@ impl<'a> NodeFactory<'a> {
listeners,
attributes,
children,
dom_id: empty_cell(),
// todo: wire up more constization
static_listeners: false,
@ -291,11 +312,10 @@ impl<'a> NodeFactory<'a> {
pub fn suspended() -> VNode<'static> {
VNode {
dom_id: empty_cell(),
key: None,
kind: VNodeKind::Suspended {
kind: VNodeKind::Suspended(VSuspended {
node: Rc::new(empty_cell()),
},
}),
}
}
@ -390,7 +410,6 @@ impl<'a> NodeFactory<'a> {
VNode {
key,
dom_id: empty_cell(),
kind: VNodeKind::Component(self.bump().alloc_with(|| VComponent {
user_fc,
comparator,
@ -430,7 +449,6 @@ impl<'a> NodeFactory<'a> {
let children = node_iter.into_vnode_list(self);
VNode {
dom_id: empty_cell(),
key: None,
kind: VNodeKind::Fragment(VFragment {
children,
@ -491,6 +509,15 @@ where
}
}
if nodes.len() == 0 {
nodes.push(VNode {
kind: VNodeKind::Anchor(VAnchor {
dom_id: empty_cell(),
}),
key: None,
});
}
nodes.into_bump_slice()
}
}
@ -625,8 +652,10 @@ impl Debug for NodeFactory<'_> {
impl Debug for VNode<'_> {
fn fmt(&self, s: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
match &self.kind {
VNodeKind::Element(el) => write!(s, "element, {}", el.tag_name),
VNodeKind::Text(t) => write!(s, "text, {}", t.text),
VNodeKind::Element(el) => write!(s, "VElement {{ name: {} }}", el.tag_name),
VNodeKind::Text(t) => write!(s, "VText {{ text: {} }}", t.text),
VNodeKind::Anchor(a) => write!(s, "VAnchor"),
VNodeKind::Fragment(_) => write!(s, "fragment"),
VNodeKind::Suspended { .. } => write!(s, "suspended"),
VNodeKind::Component(_) => write!(s, "component"),

View file

@ -1,5 +1,6 @@
use crate::innerlude::*;
use bumpalo::boxed::Box as BumpBox;
use fxhash::FxHashSet;
use std::{
any::{Any, TypeId},
borrow::BorrowMut,
@ -24,13 +25,15 @@ pub struct Scope {
pub(crate) parent_idx: Option<ScopeId>,
pub(crate) our_arena_idx: ScopeId,
pub(crate) height: u32,
pub(crate) descendents: RefCell<HashSet<ScopeId>>,
pub(crate) descendents: RefCell<FxHashSet<ScopeId>>,
// Nodes
// an internal, highly efficient storage of vnodes
// lots of safety condsiderations
pub(crate) frames: ActiveFrame,
pub(crate) caller: Rc<WrappedCaller>,
pub(crate) child_nodes: ScopeChildren<'static>,
pub(crate) pending_garbage: RefCell<Vec<*const VNode<'static>>>,
// Listeners
pub(crate) listeners: RefCell<Vec<*const Listener<'static>>>,
@ -67,11 +70,18 @@ impl Scope {
height: u32,
child_nodes: ScopeChildren,
// child_nodes: &'creator_node [VNode<'creator_node>],
vdom: SharedResources,
) -> Self {
let child_nodes = unsafe { child_nodes.extend_lifetime() };
// insert ourself as a descendent of the parent
// when the parent is removed, this map will be traversed, and we will also be cleaned up.
if let Some(parent) = &parent {
let parent = unsafe { vdom.get_scope(*parent) }.unwrap();
parent.descendents.borrow_mut().insert(arena_idx);
}
Self {
child_nodes,
caller,
@ -80,10 +90,12 @@ impl Scope {
height,
vdom,
frames: ActiveFrame::new(),
hooks: Default::default(),
shared_contexts: Default::default(),
listeners: Default::default(),
descendents: Default::default(),
pending_garbage: Default::default(),
}
}
@ -102,6 +114,9 @@ impl Scope {
// Cycle to the next frame and then reset it
// This breaks any latent references, invalidating every pointer referencing into it.
// Remove all the outdated listeners
if !self.pending_garbage.borrow().is_empty() {
panic!("cannot run scope while garbage is pending! Please clean up your mess first");
}
log::debug!("reset okay");
@ -189,11 +204,11 @@ impl Scope {
if let Some(raw_listener) = raw_listener {
let listener = unsafe { &**raw_listener };
log::info!(
"calling listener {:?}, {:?}",
listener.event,
listener.scope
);
// log::info!(
// "calling listener {:?}, {:?}",
// listener.event,
// // listener.scope
// );
let mut cb = listener.callback.borrow_mut();
if let Some(cb) = cb.as_mut() {
(cb)(event);
@ -209,10 +224,20 @@ impl Scope {
self.frames.fin_head()
}
// #[inline]
pub fn child_nodes<'a>(&'a self) -> ScopeChildren {
// self.child_nodes
unsafe { self.child_nodes.unextend_lfetime() }
// unsafe { std::mem::transmute(self.child_nodes) }
}
pub fn consume_garbage(&self) -> Vec<&VNode> {
let mut garbage = self.pending_garbage.borrow_mut();
garbage
.drain(..)
.map(|node| {
// safety: scopes cannot cycle without their garbage being collected. these nodes are safe
let node: &VNode<'static> = unsafe { &*node };
let node: &VNode = unsafe { std::mem::transmute(node) };
node
})
.collect::<Vec<_>>()
}
}

View file

@ -5,7 +5,7 @@ use crate::innerlude::*;
// create a cell with a "none" value
#[inline]
pub fn empty_cell() -> Cell<Option<ElementId>> {
Cell::new(None as Option<ElementId>)
Cell::new(None)
}
/// A helper type that lets scopes be ordered by their height

View file

@ -25,7 +25,7 @@ use crate::{arena::SharedResources, innerlude::*};
use std::any::Any;
use std::any::TypeId;
use std::cell::RefCell;
use std::cell::{Ref, RefCell, RefMut};
use std::pin::Pin;
/// An integrated virtual node system that progresses events and diffs UI trees.
@ -193,8 +193,8 @@ impl VirtualDom {
// We run the component. If it succeeds, then we can diff it and add the changes to the dom.
if cur_component.run_scope().is_ok() {
let meta = diff_machine.create(cur_component.frames.fin_head());
diff_machine.append_children(meta.added_to_stack);
let meta = diff_machine.create_vnode(cur_component.frames.fin_head());
diff_machine.edit_append_children(meta.added_to_stack);
} else {
// todo: should this be a hard error?
log::warn!(
@ -269,6 +269,47 @@ impl VirtualDom {
let mut diff_machine = DiffMachine::new(edits, realdom, trigger.originator, &self.shared);
match &trigger.event {
// When a scope gets destroyed during a diff, it gets its own garbage collection event
// However, an old scope might be attached
VirtualEvent::GarbageCollection => {
let scope = diff_machine.get_scope_mut(&trigger.originator).unwrap();
let mut garbage_list = scope.consume_garbage();
while let Some(node) = garbage_list.pop() {
match &node.kind {
VNodeKind::Text(_) => {
//
self.shared.collect_garbage(node.direct_id())
}
VNodeKind::Anchor(anchor) => {
//
}
VNodeKind::Element(el) => {
self.shared.collect_garbage(node.direct_id());
for child in el.children {
garbage_list.push(child);
}
}
VNodeKind::Fragment(frag) => {
for child in frag.children {
garbage_list.push(child);
}
}
VNodeKind::Component(comp) => {
// run the destructors
todo!();
}
VNodeKind::Suspended(node) => {
// make sure the task goes away
todo!();
}
}
}
}
// Nothing yet
VirtualEvent::AsyncEvent { .. } => {}
@ -295,13 +336,13 @@ impl VirtualDom {
// push the old node's root onto the stack
let real_id = domnode.get().ok_or(Error::NotMounted)?;
diff_machine.push_root(real_id);
diff_machine.edit_push_root(real_id);
// push these new nodes onto the diff machines stack
let meta = diff_machine.create(&*nodes);
let meta = diff_machine.create_vnode(&*nodes);
// replace the placeholder with the new nodes we just pushed on the stack
diff_machine.replace_with(meta.added_to_stack);
diff_machine.edit_replace_with(meta.added_to_stack);
}
}
}
@ -330,7 +371,7 @@ impl VirtualDom {
// Make sure this isn't a node we've already seen, we don't want to double-render anything
// If we double-renderer something, this would cause memory safety issues
if diff_machine.seen_nodes.contains(&update.idx) {
if diff_machine.seen_scopes.contains(&update.idx) {
log::debug!("Skipping update for: {:#?}", update);
continue;
}
@ -342,7 +383,7 @@ impl VirtualDom {
.expect("Failed to find scope or borrow would be aliasing");
// Now, all the "seen nodes" are nodes that got notified by running this listener
diff_machine.seen_nodes.insert(update.idx.clone());
diff_machine.seen_scopes.insert(update.idx.clone());
if cur_component.run_scope().is_ok() {
let (old, new) = (

View file

@ -1,17 +1,111 @@
use dioxus::prelude::*;
use bumpalo::Bump;
use anyhow::{Context, Result};
use dioxus::{
arena::SharedResources,
diff::{CreateMeta, DiffMachine},
prelude::*,
util::DebugDom,
DomEdit,
};
use dioxus_core as dioxus;
use dioxus_html as dioxus_elements;
struct TestDom {
bump: Bump,
resources: SharedResources,
}
impl TestDom {
fn new() -> TestDom {
let bump = Bump::new();
let resources = SharedResources::new();
TestDom { bump, resources }
}
fn new_factory<'a>(&'a self) -> NodeFactory<'a> {
NodeFactory::new(&self.bump)
}
fn render<'a, F>(&'a self, lazy_nodes: LazyNodes<'a, F>) -> VNode<'a>
where
F: FnOnce(NodeFactory<'a>) -> VNode<'a>,
{
use dioxus_core::nodes::{IntoVNode, IntoVNodeList};
lazy_nodes.into_vnode(NodeFactory::new(&self.bump))
}
fn diff<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Vec<DomEdit<'a>> {
let mut edits = Vec::new();
let dom = DebugDom::new();
let mut machine = DiffMachine::new_headless(&mut edits, &dom, &self.resources);
machine.diff_node(old, new);
edits
}
fn create<'a, F1>(&'a self, left: LazyNodes<'a, F1>) -> (CreateMeta, Vec<DomEdit<'a>>)
where
F1: FnOnce(NodeFactory<'a>) -> VNode<'a>,
{
let old = self.bump.alloc(self.render(left));
let mut edits = Vec::new();
let dom = DebugDom::new();
let mut machine = DiffMachine::new_headless(&mut edits, &dom, &self.resources);
let meta = machine.create_vnode(old);
(meta, edits)
}
fn lazy_diff<'a, F1, F2>(
&'a self,
left: LazyNodes<'a, F1>,
right: LazyNodes<'a, F2>,
) -> Vec<DomEdit<'a>>
where
F1: FnOnce(NodeFactory<'a>) -> VNode<'a>,
F2: FnOnce(NodeFactory<'a>) -> VNode<'a>,
{
let old = self.bump.alloc(self.render(left));
let new = self.bump.alloc(self.render(right));
let mut edits = Vec::new();
let dom = DebugDom::new();
let mut machine = DiffMachine::new_headless(&mut edits, &dom, &self.resources);
machine.create_vnode(old);
edits.clear();
let mut machine = DiffMachine::new_headless(&mut edits, &dom, &self.resources);
machine.diff_node(old, new);
edits
}
}
#[test]
fn diffing_works() {}
#[test]
fn html_and_rsx_generate_the_same_output() {
let old = rsx! {
div { "Hello world!" }
};
let dom = TestDom::new();
// let new = html! {
// <div>"Hello world!"</div>
// };
let edits = dom.lazy_diff(
rsx! ( div { "Hello world" } ),
rsx! ( div { "Goodbye world" } ),
);
dbg!(edits);
}
#[test]
fn fragments_create_properly() {
let dom = TestDom::new();
let (meta, edits) = dom.create(rsx! {
div { "Hello a" }
div { "Hello b" }
div { "Hello c" }
});
assert!(&edits[0].is("CreateElement"));
assert!(&edits[3].is("CreateElement"));
assert!(&edits[6].is("CreateElement"));
assert_eq!(meta.added_to_stack, 3);
dbg!(edits);
}

View file

@ -120,7 +120,7 @@ impl<'a> TextRenderer<'a> {
//
// when the page is loaded, the `querySelectorAll` will be used to collect all the nodes, and then add
// them interpreter's stack
match (self.cfg.pre_render, node.element_id()) {
match (self.cfg.pre_render, node.dom_id()) {
(true, Some(id)) => {
write!(f, " dio_el=\"{}\"", id)?;
//