Switch to a pool of dynamic values for hot reloading (#2705)
* create the dynamic value pool * assign ids to dynamic formatted segments * separate the rendering and literal pools * rsx output compiles again * more examples compiling with new rsx expansion * update template body explanation * all workspace examples compile * fix formatted segments in keys * start hot reload diffing * fix component literal hot reloading * start integrate new hot reloading with the CLI * simple hot reloads working * Fix hot reloading blocks with components * implement hot reloading for if chains * Fix hot reloading after a template requires a full rebuild * Fix hot reloading any attribute values * remove unsafe from hot reload utils * Fix hot reloading empty rsx * add more hot reloading tests * reorganize hot reload module * fix web hydration * fix empty rsx nodes in autoformatting * fix tests * remove path sorting logic from core * make template names more consistent in debug mode * fix quote_as_hot_reload_literal for explicitly typed literals * fix can_be_shorthand for string literals * fix formatted single dynamic expression * Fix usize component properties and playwright tests * remove default implementation for TemplateBody * add a bunch more comments for diffing, scoring and why this scoring system is optimal
This commit is contained in:
1 parent
f3ca1484a1
commit
34bdcd15cf
40 files changed
+2288
-1897
No files matched your search
@@ -467,7 +467,7 @@ impl VNode {
|
||||
dom: &VirtualDom,
|
||||
to: &mut impl WriteMutations,
|
||||
) -> usize {
|
||||
let template = self.template.get();
|
||||
let template = self.template;
|
||||
|
||||
let mount = dom.mounts.get(self.mount.get().0).unwrap();
|
||||
|
||||
|
||||
@@ -103,11 +103,11 @@ impl VirtualDom {
|
||||
to: &mut impl WriteMutations,
|
||||
mut template: Template,
|
||||
) {
|
||||
if self.templates.contains_key(template.name) {
|
||||
if self.templates.contains(&template.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
_ = self.templates.insert(template.name, template);
|
||||
_ = self.templates.insert(template.name);
|
||||
|
||||
// If it's all dynamic nodes, then we don't need to register it
|
||||
if !template.is_completely_dynamic() {
|
||||
@@ -124,7 +124,7 @@ impl VirtualDom {
|
||||
/// - for appending children we can use AppendChildren
|
||||
#[allow(dead_code)]
|
||||
fn is_dyn_node_only_child(node: &VNode, idx: usize) -> bool {
|
||||
let template = node.template.get();
|
||||
let template = node.template;
|
||||
let path = template.node_paths[idx];
|
||||
|
||||
// use a loop to index every static node's children until the path has run out
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::innerlude::MountId;
|
||||
use crate::{Attribute, AttributeValue, DynamicNode::*, Template};
|
||||
use crate::{Attribute, AttributeValue, DynamicNode::*};
|
||||
use crate::{VNode, VirtualDom, WriteMutations};
|
||||
use core::iter::Peekable;
|
||||
|
||||
@@ -21,21 +21,6 @@ impl VNode {
|
||||
// The node we are diffing from should always be mounted
|
||||
debug_assert!(dom.mounts.get(self.mount.get().0).is_some() || to.is_none());
|
||||
|
||||
// If hot reloading is enabled, we need to make sure we're using the latest template
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let name = new.template.get().name;
|
||||
if let Some(template) = dom.templates.get(name).cloned() {
|
||||
new.template.set(template);
|
||||
if template != self.template.get() {
|
||||
let mount_id = self.mount.get();
|
||||
let parent = dom.mounts[mount_id.0].parent;
|
||||
self.replace(std::slice::from_ref(new), parent, dom, to);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the templates are different by name, we need to replace the entire template
|
||||
if self.templates_are_different(new) {
|
||||
return self.light_diff_templates(new, dom, to);
|
||||
@@ -120,7 +105,7 @@ impl VNode {
|
||||
&self,
|
||||
root_idx: usize,
|
||||
) -> Option<(usize, &DynamicNode)> {
|
||||
self.template.get().roots[root_idx]
|
||||
self.template.roots[root_idx]
|
||||
.dynamic_id()
|
||||
.map(|id| (id, &self.dynamic_nodes[id]))
|
||||
}
|
||||
@@ -148,7 +133,7 @@ impl VNode {
|
||||
|
||||
pub(crate) fn find_last_element(&self, dom: &VirtualDom) -> ElementId {
|
||||
let mount = &dom.mounts[self.mount.get().0];
|
||||
let last_root_index = self.template.get().roots.len() - 1;
|
||||
let last_root_index = self.template.roots.len() - 1;
|
||||
match self.get_dynamic_root_node_and_id(last_root_index) {
|
||||
// This node is static, just get the root id
|
||||
None | Some((_, Placeholder(_) | Text(_))) => mount.root_ids[last_root_index],
|
||||
@@ -272,7 +257,7 @@ impl VNode {
|
||||
destroy_component_state: bool,
|
||||
replace_with: Option<usize>,
|
||||
) {
|
||||
let roots = self.template.get().roots;
|
||||
let roots = self.template.roots;
|
||||
for (idx, node) in roots.iter().enumerate() {
|
||||
let last_node = idx == roots.len() - 1;
|
||||
if let Some(id) = node.dynamic_id() {
|
||||
@@ -305,7 +290,7 @@ impl VNode {
|
||||
dom: &mut VirtualDom,
|
||||
destroy_component_state: bool,
|
||||
) {
|
||||
let template = self.template.get();
|
||||
let template = self.template;
|
||||
for (idx, dyn_node) in self.dynamic_nodes.iter().enumerate() {
|
||||
let path_len = template.node_paths.get(idx).map(|path| path.len());
|
||||
// Roots are cleaned up automatically above and nodes with a empty path are placeholders
|
||||
@@ -361,14 +346,14 @@ impl VNode {
|
||||
}
|
||||
|
||||
fn templates_are_different(&self, other: &VNode) -> bool {
|
||||
let self_node_name = self.template.get().id();
|
||||
let other_node_name = other.template.get().id();
|
||||
let self_node_name = self.template.id();
|
||||
let other_node_name = other.template.id();
|
||||
self_node_name != other_node_name
|
||||
}
|
||||
|
||||
pub(super) fn reclaim_attributes(&self, mount: MountId, dom: &mut VirtualDom) {
|
||||
let mut next_id = None;
|
||||
for (idx, path) in self.template.get().attr_paths.iter().enumerate() {
|
||||
for (idx, path) in self.template.attr_paths.iter().enumerate() {
|
||||
// We clean up the roots in the next step, so don't worry about them here
|
||||
if path.len() <= 1 {
|
||||
continue;
|
||||
@@ -399,7 +384,7 @@ impl VNode {
|
||||
let mut old_attributes_iter = old_attrs.iter().peekable();
|
||||
let mut new_attributes_iter = new_attrs.iter().peekable();
|
||||
let attribute_id = dom.mounts[mount_id.0].mounted_attributes[idx];
|
||||
let path = self.template.get().attr_paths[idx];
|
||||
let path = self.template.attr_paths[idx];
|
||||
|
||||
loop {
|
||||
match (old_attributes_iter.peek(), new_attributes_iter.peek()) {
|
||||
@@ -566,21 +551,6 @@ impl VNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the most up to date template for this rsx block
|
||||
#[allow(unused)]
|
||||
pub(crate) fn template(&self, dom: &VirtualDom) -> Template {
|
||||
// check for a overridden template
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let template = self.template.get();
|
||||
if let Some(new_template) = dom.templates.get(template.name) {
|
||||
self.template.set(*new_template);
|
||||
}
|
||||
};
|
||||
|
||||
self.template.get()
|
||||
}
|
||||
|
||||
/// Create this rsx block. This will create scopes from components that this rsx block contains, but it will not write anything to the DOM.
|
||||
pub(crate) fn create(
|
||||
&self,
|
||||
@@ -589,7 +559,7 @@ impl VNode {
|
||||
mut to: Option<&mut impl WriteMutations>,
|
||||
) -> usize {
|
||||
// Get the most up to date template
|
||||
let template = self.template(dom);
|
||||
let template = self.template;
|
||||
|
||||
// Initialize the mount information for this vnode if it isn't already mounted
|
||||
if !self.mount.get().mounted() {
|
||||
@@ -616,13 +586,9 @@ impl VNode {
|
||||
}
|
||||
|
||||
// Walk the roots, creating nodes and assigning IDs
|
||||
// nodes in an iterator of (dynamic_node_index, path)
|
||||
|
||||
let nodes_sorted = template.breadth_first_node_paths();
|
||||
let attrs_sorted = template.breadth_first_attribute_paths();
|
||||
|
||||
let mut nodes = nodes_sorted.peekable();
|
||||
let mut attrs = attrs_sorted.peekable();
|
||||
// nodes in an iterator of (dynamic_node_index, path) and attrs in an iterator of (attr_index, path)
|
||||
let mut nodes = template.node_paths.iter().copied().enumerate().peekable();
|
||||
let mut attrs = template.attr_paths.iter().copied().enumerate().peekable();
|
||||
|
||||
// Get the mounted id of this block
|
||||
// At this point, we should have already mounted the block
|
||||
@@ -690,7 +656,7 @@ impl VNode {
|
||||
fn reference_to_dynamic_node(&self, mount: MountId, dynamic_node_id: usize) -> ElementRef {
|
||||
ElementRef {
|
||||
path: ElementPath {
|
||||
path: self.template.get().node_paths[dynamic_node_id],
|
||||
path: self.template.node_paths[dynamic_node_id],
|
||||
},
|
||||
mount,
|
||||
}
|
||||
@@ -834,7 +800,7 @@ impl VNode {
|
||||
let this_id = dom.next_element();
|
||||
dom.mounts[mount.0].root_ids[root_idx] = this_id;
|
||||
|
||||
to.load_template(self.template.get().name, root_idx, this_id);
|
||||
to.load_template(self.template.name, root_idx, this_id);
|
||||
|
||||
this_id
|
||||
}
|
||||
@@ -874,7 +840,7 @@ impl VNode {
|
||||
dom: &mut VirtualDom,
|
||||
) -> (ElementId, &'static [u8]) {
|
||||
// Add the mutation to the list
|
||||
let path = self.template.get().node_paths[idx];
|
||||
let path = self.template.node_paths[idx];
|
||||
|
||||
// Allocate a dynamic element reference for this text node
|
||||
let new_id = mount.mount_node(idx, dom);
|
||||
@@ -940,8 +906,8 @@ fn matching_components<'a>(
|
||||
left: &'a VNode,
|
||||
right: &'a VNode,
|
||||
) -> Option<Vec<(&'a VComponent, &'a VComponent)>> {
|
||||
let left_node = left.template.get();
|
||||
let right_node = right.template.get();
|
||||
let left_node = left.template;
|
||||
let right_node = right.template;
|
||||
if left_node.roots.len() != right_node.roots.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
#[doc(hidden)]
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
use crate::nodes::deserialize_string_leaky;
|
||||
use crate::{
|
||||
Attribute, AttributeValue, DynamicNode, Template, TemplateAttribute, TemplateNode, VNode, VText,
|
||||
};
|
||||
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct HotreloadedLiteral {
|
||||
pub name: String,
|
||||
pub value: HotReloadLiteral,
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum HotReloadLiteral {
|
||||
Fmted(FmtedSegments),
|
||||
@@ -18,12 +29,53 @@ pub enum HotReloadLiteral {
|
||||
Bool(bool),
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
impl HotReloadLiteral {
|
||||
pub fn as_fmted(&self) -> Option<&FmtedSegments> {
|
||||
match self {
|
||||
Self::Fmted(segments) => Some(segments),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_float(&self) -> Option<f64> {
|
||||
match self {
|
||||
Self::Float(f) => Some(*f),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_int(&self) -> Option<i64> {
|
||||
match self {
|
||||
Self::Int(i) => Some(*i),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_bool(&self) -> Option<bool> {
|
||||
match self {
|
||||
Self::Bool(b) => Some(*b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for HotReloadLiteral {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
match self {
|
||||
Self::Fmted(segments) => segments.hash(state),
|
||||
Self::Float(f) => f.to_bits().hash(state),
|
||||
Self::Int(i) => i.hash(state),
|
||||
Self::Bool(b) => b.hash(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
|
||||
pub struct FmtedSegments {
|
||||
pub segments: Vec<FmtSegment>,
|
||||
pub(crate) segments: Vec<FmtSegment>,
|
||||
}
|
||||
|
||||
impl FmtedSegments {
|
||||
@@ -32,33 +84,23 @@ impl FmtedSegments {
|
||||
}
|
||||
|
||||
/// Render the formatted string by stitching together the segments
|
||||
pub fn render_with(&self, dynamic_nodes: Vec<String>) -> String {
|
||||
pub(crate) fn render_with(&self, dynamic_text: &[String]) -> String {
|
||||
let mut out = String::new();
|
||||
|
||||
for segment in &self.segments {
|
||||
match segment {
|
||||
FmtSegment::Literal { value } => out.push_str(value),
|
||||
FmtSegment::Dynamic { id } => out.push_str(&dynamic_nodes[*id]),
|
||||
FmtSegment::Dynamic { id } => out.push_str(&dynamic_text[*id]),
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// Update the segments with new segments
|
||||
///
|
||||
/// this will change how we render the formatted string
|
||||
pub fn update_segments(&mut self, new_segments: Vec<FmtSegment>) {
|
||||
self.segments = new_segments;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
use crate::nodes::deserialize_string_leaky;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
|
||||
pub enum FmtSegment {
|
||||
Literal {
|
||||
#[cfg_attr(
|
||||
@@ -71,3 +113,359 @@ pub enum FmtSegment {
|
||||
id: usize,
|
||||
},
|
||||
}
|
||||
|
||||
// let __pool = DynamicValuePool::new(
|
||||
// vec![...],
|
||||
// vec![...],
|
||||
// vec![...],
|
||||
// );
|
||||
// VNode::new(
|
||||
// None,
|
||||
// Template {
|
||||
// name: "...",
|
||||
// roots: &[...],
|
||||
// node_paths: &[..],
|
||||
// attr_paths: &[...],
|
||||
// },
|
||||
// Box::new([...]),
|
||||
// Box::new([...]),
|
||||
// )
|
||||
|
||||
// Open questions:
|
||||
// - How do we handle type coercion for different sized component property integers?
|
||||
// - Should non-string hot literals go through the centralized pool?
|
||||
// - Should formatted strings be a runtime concept?
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct DynamicLiteralPool {
|
||||
dynamic_text: Box<[String]>,
|
||||
}
|
||||
|
||||
impl DynamicLiteralPool {
|
||||
pub fn new(dynamic_text: Vec<String>) -> Self {
|
||||
Self {
|
||||
dynamic_text: dynamic_text.into_boxed_slice(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_component_property<'a, T>(
|
||||
&self,
|
||||
id: usize,
|
||||
hot_reload: &'a HotReloadedTemplate,
|
||||
f: impl FnOnce(&'a HotReloadLiteral) -> Option<T>,
|
||||
) -> Option<T> {
|
||||
f(hot_reload.component_values.get(id)?)
|
||||
}
|
||||
|
||||
/// Get a component property of a specific type at the component property index
|
||||
pub fn component_property<T: 'static>(
|
||||
&mut self,
|
||||
id: usize,
|
||||
hot_reload: &HotReloadedTemplate,
|
||||
// We pass in the original value for better type inference
|
||||
// For example, if the original literal is `0i128`, we know the output must be the type `i128`
|
||||
_coherse_type: T,
|
||||
) -> T {
|
||||
fn assert_type<T: 'static, T2: 'static>(t: T) -> T2 {
|
||||
*(Box::new(t) as Box<dyn Any>).downcast::<T2>().unwrap()
|
||||
}
|
||||
let grab_float = || {
|
||||
self.get_component_property(id, hot_reload, HotReloadLiteral::as_float).expect("Expected a float component property. This is probably caused by a bug in dioxus hot reloading. Please report this issue.")
|
||||
};
|
||||
let grab_int = || {
|
||||
self.get_component_property(id, hot_reload, HotReloadLiteral::as_int).expect("Expected an int component property. This is probably caused by a bug in dioxus hot reloading. Please report this issue.")
|
||||
};
|
||||
let grab_bool = || {
|
||||
self.get_component_property(id, hot_reload, HotReloadLiteral::as_bool).expect("Expected a bool component property. This is probably caused by a bug in dioxus hot reloading. Please report this issue.")
|
||||
};
|
||||
let grab_fmted = || {
|
||||
self.get_component_property(id, hot_reload, |fmted| HotReloadLiteral::as_fmted(fmted).map(|segments| self.render_formatted(segments))).expect("Expected a string component property. This is probably caused by a bug in dioxus hot reloading. Please report this issue.")
|
||||
};
|
||||
match TypeId::of::<T>() {
|
||||
// Any string types that accept a literal
|
||||
_ if TypeId::of::<String>() == TypeId::of::<T>() => assert_type(grab_fmted()),
|
||||
_ if TypeId::of::<&'static str>() == TypeId::of::<T>() => {
|
||||
assert_type(Box::leak(grab_fmted().into_boxed_str()) as &'static str)
|
||||
}
|
||||
// Any integer types that accept a literal
|
||||
_ if TypeId::of::<i128>() == TypeId::of::<T>() => assert_type(grab_int() as i128),
|
||||
_ if TypeId::of::<i64>() == TypeId::of::<T>() => assert_type(grab_int()),
|
||||
_ if TypeId::of::<i32>() == TypeId::of::<T>() => assert_type(grab_int() as i32),
|
||||
_ if TypeId::of::<i16>() == TypeId::of::<T>() => assert_type(grab_int() as i16),
|
||||
_ if TypeId::of::<i8>() == TypeId::of::<T>() => assert_type(grab_int() as i8),
|
||||
_ if TypeId::of::<isize>() == TypeId::of::<T>() => assert_type(grab_int() as isize),
|
||||
_ if TypeId::of::<u128>() == TypeId::of::<T>() => assert_type(grab_int() as u128),
|
||||
_ if TypeId::of::<u64>() == TypeId::of::<T>() => assert_type(grab_int() as u64),
|
||||
_ if TypeId::of::<u32>() == TypeId::of::<T>() => assert_type(grab_int() as u32),
|
||||
_ if TypeId::of::<u16>() == TypeId::of::<T>() => assert_type(grab_int() as u16),
|
||||
_ if TypeId::of::<u8>() == TypeId::of::<T>() => assert_type(grab_int() as u8),
|
||||
_ if TypeId::of::<usize>() == TypeId::of::<T>() => assert_type(grab_int() as usize),
|
||||
// Any float types that accept a literal
|
||||
_ if TypeId::of::<f64>() == TypeId::of::<T>() => assert_type(grab_float()),
|
||||
_ if TypeId::of::<f32>() == TypeId::of::<T>() => assert_type(grab_float() as f32),
|
||||
// Any bool types that accept a literal
|
||||
_ if TypeId::of::<bool>() == TypeId::of::<T>() => assert_type(grab_bool()),
|
||||
_ => panic!("Unsupported component property type"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_formatted(&self, segments: &FmtedSegments) -> String {
|
||||
segments.render_with(&self.dynamic_text)
|
||||
}
|
||||
}
|
||||
#[doc(hidden)]
|
||||
pub struct DynamicValuePool {
|
||||
dynamic_attributes: Box<[Box<[Attribute]>]>,
|
||||
dynamic_nodes: Box<[DynamicNode]>,
|
||||
literal_pool: DynamicLiteralPool,
|
||||
}
|
||||
|
||||
impl DynamicValuePool {
|
||||
pub fn new(
|
||||
dynamic_nodes: Vec<DynamicNode>,
|
||||
dynamic_attributes: Vec<Box<[Attribute]>>,
|
||||
literal_pool: DynamicLiteralPool,
|
||||
) -> Self {
|
||||
Self {
|
||||
dynamic_attributes: dynamic_attributes.into_boxed_slice(),
|
||||
dynamic_nodes: dynamic_nodes.into_boxed_slice(),
|
||||
literal_pool,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_with(&mut self, hot_reload: &HotReloadedTemplate) -> VNode {
|
||||
// Get the node_paths from a depth first traversal of the template
|
||||
let node_paths = hot_reload.node_paths();
|
||||
let attr_paths = hot_reload.attr_paths();
|
||||
|
||||
let template = Template {
|
||||
name: hot_reload.name,
|
||||
roots: hot_reload.roots,
|
||||
node_paths,
|
||||
attr_paths,
|
||||
};
|
||||
let key = hot_reload
|
||||
.key
|
||||
.as_ref()
|
||||
.map(|key| self.literal_pool.render_formatted(key));
|
||||
let dynamic_nodes = hot_reload
|
||||
.dynamic_nodes
|
||||
.iter()
|
||||
.map(|node| self.render_dynamic_node(node))
|
||||
.collect();
|
||||
let dynamic_attrs = hot_reload
|
||||
.dynamic_attributes
|
||||
.iter()
|
||||
.map(|attr| self.render_attribute(attr))
|
||||
.collect();
|
||||
|
||||
VNode::new(key, template, dynamic_nodes, dynamic_attrs)
|
||||
}
|
||||
|
||||
fn render_dynamic_node(&mut self, node: &HotReloadDynamicNode) -> DynamicNode {
|
||||
match node {
|
||||
// If the node is dynamic, take it from the pool and return it
|
||||
HotReloadDynamicNode::Dynamic(id) => self.dynamic_nodes[*id].clone(),
|
||||
// Otherwise, format the text node and return it
|
||||
HotReloadDynamicNode::Formatted(segments) => DynamicNode::Text(VText {
|
||||
value: self.literal_pool.render_formatted(segments),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_attribute(&mut self, attr: &HotReloadDynamicAttribute) -> Box<[Attribute]> {
|
||||
match attr {
|
||||
HotReloadDynamicAttribute::Dynamic(id) => self.dynamic_attributes[*id].clone(),
|
||||
HotReloadDynamicAttribute::Named(NamedAttribute {
|
||||
name,
|
||||
namespace,
|
||||
value,
|
||||
}) => Box::new([Attribute {
|
||||
name,
|
||||
namespace: *namespace,
|
||||
value: match value {
|
||||
HotReloadAttributeValue::Literal(HotReloadLiteral::Fmted(segments)) => {
|
||||
AttributeValue::Text(self.literal_pool.render_formatted(segments))
|
||||
}
|
||||
HotReloadAttributeValue::Literal(HotReloadLiteral::Float(f)) => {
|
||||
AttributeValue::Float(*f)
|
||||
}
|
||||
HotReloadAttributeValue::Literal(HotReloadLiteral::Int(i)) => {
|
||||
AttributeValue::Int(*i)
|
||||
}
|
||||
HotReloadAttributeValue::Literal(HotReloadLiteral::Bool(b)) => {
|
||||
AttributeValue::Bool(*b)
|
||||
}
|
||||
HotReloadAttributeValue::Dynamic(id) => {
|
||||
self.dynamic_attributes[*id][0].value.clone()
|
||||
}
|
||||
},
|
||||
volatile: false,
|
||||
}]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
|
||||
pub struct HotReloadTemplateWithLocation {
|
||||
pub location: String,
|
||||
pub template: HotReloadedTemplate,
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct HotReloadedTemplate {
|
||||
pub name: &'static str,
|
||||
pub key: Option<FmtedSegments>,
|
||||
pub dynamic_nodes: Vec<HotReloadDynamicNode>,
|
||||
pub dynamic_attributes: Vec<HotReloadDynamicAttribute>,
|
||||
pub component_values: Vec<HotReloadLiteral>,
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
serde(deserialize_with = "crate::nodes::deserialize_leaky")
|
||||
)]
|
||||
pub roots: &'static [TemplateNode],
|
||||
}
|
||||
|
||||
impl HotReloadedTemplate {
|
||||
pub fn new(
|
||||
name: &'static str,
|
||||
key: Option<FmtedSegments>,
|
||||
dynamic_nodes: Vec<HotReloadDynamicNode>,
|
||||
dynamic_attributes: Vec<HotReloadDynamicAttribute>,
|
||||
component_values: Vec<HotReloadLiteral>,
|
||||
roots: &'static [TemplateNode],
|
||||
) -> Self {
|
||||
Self {
|
||||
name,
|
||||
key,
|
||||
dynamic_nodes,
|
||||
dynamic_attributes,
|
||||
component_values,
|
||||
roots,
|
||||
}
|
||||
}
|
||||
|
||||
fn node_paths(&self) -> &'static [&'static [u8]] {
|
||||
fn add_node_paths(
|
||||
roots: &[TemplateNode],
|
||||
node_paths: &mut Vec<&'static [u8]>,
|
||||
current_path: Vec<u8>,
|
||||
) {
|
||||
for (idx, node) in roots.iter().enumerate() {
|
||||
let mut path = current_path.clone();
|
||||
path.push(idx as u8);
|
||||
match node {
|
||||
TemplateNode::Element { children, .. } => {
|
||||
add_node_paths(children, node_paths, path);
|
||||
}
|
||||
TemplateNode::Text { .. } => {}
|
||||
TemplateNode::Dynamic { id } => {
|
||||
debug_assert_eq!(node_paths.len(), *id);
|
||||
node_paths.push(Box::leak(path.into_boxed_slice()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut node_paths = Vec::new();
|
||||
add_node_paths(self.roots, &mut node_paths, Vec::new());
|
||||
let leaked: &'static [&'static [u8]] = Box::leak(node_paths.into_boxed_slice());
|
||||
leaked
|
||||
}
|
||||
|
||||
fn attr_paths(&self) -> &'static [&'static [u8]] {
|
||||
fn add_attr_paths(
|
||||
roots: &[TemplateNode],
|
||||
attr_paths: &mut Vec<&'static [u8]>,
|
||||
current_path: Vec<u8>,
|
||||
) {
|
||||
for (idx, node) in roots.iter().enumerate() {
|
||||
let mut path = current_path.clone();
|
||||
path.push(idx as u8);
|
||||
if let TemplateNode::Element {
|
||||
children, attrs, ..
|
||||
} = node
|
||||
{
|
||||
for attr in *attrs {
|
||||
if let TemplateAttribute::Dynamic { id } = attr {
|
||||
debug_assert_eq!(attr_paths.len(), *id);
|
||||
attr_paths.push(Box::leak(path.clone().into_boxed_slice()));
|
||||
}
|
||||
}
|
||||
add_attr_paths(children, attr_paths, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut attr_paths = Vec::new();
|
||||
add_attr_paths(self.roots, &mut attr_paths, Vec::new());
|
||||
let leaked: &'static [&'static [u8]] = Box::leak(attr_paths.into_boxed_slice());
|
||||
leaked
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, PartialEq, Clone, Hash)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
|
||||
pub enum HotReloadDynamicNode {
|
||||
Dynamic(usize),
|
||||
Formatted(FmtedSegments),
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, PartialEq, Clone, Hash)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
|
||||
pub enum HotReloadDynamicAttribute {
|
||||
Dynamic(usize),
|
||||
Named(NamedAttribute),
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, PartialEq, Clone, Hash)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct NamedAttribute {
|
||||
/// The name of this attribute.
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
serde(deserialize_with = "crate::nodes::deserialize_string_leaky")
|
||||
)]
|
||||
name: &'static str,
|
||||
/// The namespace of this attribute. Does not exist in the HTML spec
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
serde(deserialize_with = "crate::nodes::deserialize_option_leaky")
|
||||
)]
|
||||
namespace: Option<&'static str>,
|
||||
|
||||
value: HotReloadAttributeValue,
|
||||
}
|
||||
|
||||
impl NamedAttribute {
|
||||
pub fn new(
|
||||
name: &'static str,
|
||||
namespace: Option<&'static str>,
|
||||
value: HotReloadAttributeValue,
|
||||
) -> Self {
|
||||
Self {
|
||||
name,
|
||||
namespace,
|
||||
value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, PartialEq, Clone, Hash)]
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
|
||||
pub enum HotReloadAttributeValue {
|
||||
Literal(HotReloadLiteral),
|
||||
Dynamic(usize),
|
||||
}
|
||||
@@ -34,8 +34,11 @@ mod hotreload_utils;
|
||||
pub mod internal {
|
||||
pub use crate::properties::verify_component_called_as_component;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use crate::hotreload_utils::{
|
||||
FmtSegment, FmtedSegments, HotReloadLiteral, HotreloadedLiteral,
|
||||
DynamicLiteralPool, DynamicValuePool, FmtSegment, FmtedSegments, HotReloadAttributeValue,
|
||||
HotReloadDynamicAttribute, HotReloadDynamicNode, HotReloadLiteral,
|
||||
HotReloadTemplateWithLocation, HotReloadedTemplate, HotreloadedLiteral, NamedAttribute,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+22
-109
@@ -6,7 +6,6 @@ use crate::{
|
||||
properties::ComponentFunction,
|
||||
};
|
||||
use crate::{Properties, ScopeId, VirtualDom};
|
||||
use core::panic;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::rc::Rc;
|
||||
use std::vec;
|
||||
@@ -122,7 +121,7 @@ pub struct VNodeInner {
|
||||
pub key: Option<String>,
|
||||
|
||||
/// The static nodes and static descriptor of the template
|
||||
pub template: Cell<Template>,
|
||||
pub template: Template,
|
||||
|
||||
/// The dynamic nodes in the template
|
||||
pub dynamic_nodes: Box<[DynamicNode]>,
|
||||
@@ -252,12 +251,12 @@ impl VNode {
|
||||
key: None,
|
||||
dynamic_nodes: Box::new([DynamicNode::Placeholder(Default::default())]),
|
||||
dynamic_attrs: Box::new([]),
|
||||
template: Cell::new(Template {
|
||||
template: Template {
|
||||
name: "packages/core/nodes.rs:198:0:0",
|
||||
roots: &[TemplateNode::Dynamic { id: 0 }],
|
||||
node_paths: &[&[0]],
|
||||
attr_paths: &[],
|
||||
}),
|
||||
},
|
||||
})
|
||||
})
|
||||
.clone()
|
||||
@@ -278,7 +277,7 @@ impl VNode {
|
||||
Self {
|
||||
vnode: Rc::new(VNodeInner {
|
||||
key,
|
||||
template: Cell::new(template),
|
||||
template,
|
||||
dynamic_nodes,
|
||||
dynamic_attrs,
|
||||
}),
|
||||
@@ -290,7 +289,7 @@ impl VNode {
|
||||
///
|
||||
/// Returns [`None`] if the root is actually a static node (Element/Text)
|
||||
pub fn dynamic_root(&self, idx: usize) -> Option<&DynamicNode> {
|
||||
self.template.get().roots[idx]
|
||||
self.template.roots[idx]
|
||||
.dynamic_id()
|
||||
.map(|id| &self.dynamic_nodes[id])
|
||||
}
|
||||
@@ -411,7 +410,7 @@ where
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
fn deserialize_leaky<'a, 'de, T, D>(deserializer: D) -> Result<&'a [T], D::Error>
|
||||
pub(crate) fn deserialize_leaky<'a, 'de, T, D>(deserializer: D) -> Result<&'a [T], D::Error>
|
||||
where
|
||||
T: serde::Deserialize<'de>,
|
||||
D: serde::Deserializer<'de>,
|
||||
@@ -423,7 +422,9 @@ where
|
||||
}
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
fn deserialize_option_leaky<'a, 'de, D>(deserializer: D) -> Result<Option<&'static str>, D::Error>
|
||||
pub(crate) fn deserialize_option_leaky<'a, 'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<&'static str>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
@@ -448,36 +449,6 @@ impl Template {
|
||||
let ptr: *const str = self.name;
|
||||
ptr as *const () as usize
|
||||
}
|
||||
|
||||
/// Iterate over the attribute paths in order along with the original indexes for each path
|
||||
pub(crate) fn breadth_first_attribute_paths(
|
||||
&self,
|
||||
) -> impl Iterator<Item = (usize, &'static [u8])> {
|
||||
// In release mode, hot reloading is disabled and everything is in breadth first order already
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
self.attr_paths.iter().copied().enumerate()
|
||||
}
|
||||
// If we are in debug mode, hot reloading may have messed up the order of the paths. We need to sort them
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
sort_bfo(self.attr_paths).into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over the node paths in order along with the original indexes for each path
|
||||
pub(crate) fn breadth_first_node_paths(&self) -> impl Iterator<Item = (usize, &'static [u8])> {
|
||||
// In release mode, hot reloading is disabled and everything is in breadth first order already
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
self.node_paths.iter().copied().enumerate()
|
||||
}
|
||||
// If we are in debug mode, hot reloading may have messed up the order of the paths. We need to sort them
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
sort_bfo(self.node_paths).into_iter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A statically known node in a layout.
|
||||
@@ -523,7 +494,10 @@ pub enum TemplateNode {
|
||||
/// This template node is just a piece of static text
|
||||
Text {
|
||||
/// The actual text
|
||||
#[serde(deserialize_with = "deserialize_string_leaky")]
|
||||
#[cfg_attr(
|
||||
feature = "serialize",
|
||||
serde(deserialize_with = "deserialize_string_leaky")
|
||||
)]
|
||||
text: &'static str,
|
||||
},
|
||||
|
||||
@@ -548,7 +522,7 @@ impl TemplateNode {
|
||||
/// A node created at runtime
|
||||
///
|
||||
/// This node's index in the DynamicNode list on VNode should match its respective `Dynamic` index
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DynamicNode {
|
||||
/// A component node
|
||||
///
|
||||
@@ -692,8 +666,10 @@ pub struct VText {
|
||||
|
||||
impl VText {
|
||||
/// Create a new VText
|
||||
pub fn new(value: String) -> Self {
|
||||
Self { value }
|
||||
pub fn new(value: impl ToString) -> Self {
|
||||
Self {
|
||||
value: value.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -783,6 +759,7 @@ impl Attribute {
|
||||
///
|
||||
/// These are built-in to be faster during the diffing process. To use a custom value, use the [`AttributeValue::Any`]
|
||||
/// variant.
|
||||
#[derive(Clone)]
|
||||
pub enum AttributeValue {
|
||||
/// Text attribute
|
||||
Text(String),
|
||||
@@ -800,7 +777,7 @@ pub enum AttributeValue {
|
||||
Listener(ListenerCb),
|
||||
|
||||
/// An arbitrary value that implements PartialEq and is static
|
||||
Any(Box<dyn AnyValue>),
|
||||
Any(Rc<dyn AnyValue>),
|
||||
|
||||
/// A "none" value, resulting in the removal of an attribute from the dom
|
||||
None,
|
||||
@@ -824,7 +801,7 @@ impl AttributeValue {
|
||||
|
||||
/// Create a new [`AttributeValue`] with a value that implements [`AnyValue`]
|
||||
pub fn any_value<T: AnyValue>(value: T) -> AttributeValue {
|
||||
AttributeValue::Any(Box::new(value))
|
||||
AttributeValue::Any(Rc::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -859,19 +836,6 @@ impl PartialEq for AttributeValue {
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for AttributeValue {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::Text(arg0) => Self::Text(arg0.clone()),
|
||||
Self::Float(arg0) => Self::Float(*arg0),
|
||||
Self::Int(arg0) => Self::Int(*arg0),
|
||||
Self::Bool(arg0) => Self::Bool(*arg0),
|
||||
Self::Listener(_) | Self::Any(_) => panic!("Cannot clone listener or any value"),
|
||||
Self::None => Self::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait AnyValue: 'static {
|
||||
fn any_cmp(&self, other: &dyn AnyValue) -> bool;
|
||||
@@ -1117,7 +1081,7 @@ impl IntoAttributeValue for Arguments<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttributeValue for Box<dyn AnyValue> {
|
||||
impl IntoAttributeValue for Rc<dyn AnyValue> {
|
||||
fn into_value(self) -> AttributeValue {
|
||||
AttributeValue::Any(self)
|
||||
}
|
||||
@@ -1143,54 +1107,3 @@ pub trait HasAttributes {
|
||||
volatile: bool,
|
||||
) -> Self;
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn sort_bfo(paths: &[&'static [u8]]) -> Vec<(usize, &'static [u8])> {
|
||||
let mut with_indices = paths.iter().copied().enumerate().collect::<Vec<_>>();
|
||||
with_indices.sort_by(|(_, a), (_, b)| {
|
||||
let mut a = a.iter();
|
||||
let mut b = b.iter();
|
||||
loop {
|
||||
match (a.next(), b.next()) {
|
||||
(Some(a), Some(b)) => {
|
||||
if a != b {
|
||||
return a.cmp(b);
|
||||
}
|
||||
}
|
||||
// The shorter path goes first
|
||||
(None, Some(_)) => return std::cmp::Ordering::Less,
|
||||
(Some(_), None) => return std::cmp::Ordering::Greater,
|
||||
(None, None) => return std::cmp::Ordering::Equal,
|
||||
}
|
||||
}
|
||||
});
|
||||
with_indices
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(debug_assertions)]
|
||||
fn sorting() {
|
||||
let r: [(usize, &[u8]); 5] = [
|
||||
(0, &[0, 1]),
|
||||
(1, &[0, 2]),
|
||||
(2, &[1, 0]),
|
||||
(3, &[1, 0, 1]),
|
||||
(4, &[1, 2]),
|
||||
];
|
||||
assert_eq!(
|
||||
sort_bfo(&[&[0, 1,], &[0, 2,], &[1, 0,], &[1, 0, 1,], &[1, 2,],]),
|
||||
r
|
||||
);
|
||||
let r: [(usize, &[u8]); 6] = [
|
||||
(0, &[0]),
|
||||
(1, &[0, 1]),
|
||||
(2, &[0, 1, 2]),
|
||||
(3, &[1]),
|
||||
(4, &[1, 2]),
|
||||
(5, &[2]),
|
||||
];
|
||||
assert_eq!(
|
||||
sort_bfo(&[&[0], &[0, 1], &[0, 1, 2], &[1], &[1, 2], &[2],]),
|
||||
r
|
||||
);
|
||||
}
|
||||
@@ -18,7 +18,7 @@ use crate::{
|
||||
};
|
||||
use crate::{Task, VComponent};
|
||||
use futures_util::StreamExt;
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustc_hash::FxHashSet;
|
||||
use slab::Slab;
|
||||
use std::collections::BTreeSet;
|
||||
use std::{any::Any, rc::Rc};
|
||||
@@ -208,8 +208,8 @@ pub struct VirtualDom {
|
||||
|
||||
pub(crate) dirty_scopes: BTreeSet<ScopeOrder>,
|
||||
|
||||
// A map of overridden templates?
|
||||
pub(crate) templates: FxHashMap<TemplateId, Template>,
|
||||
// A map of templates we have sent to the renderer
|
||||
pub(crate) templates: FxHashSet<TemplateId>,
|
||||
|
||||
// Templates changes that are queued for the next render
|
||||
pub(crate) queued_templates: Vec<Template>,
|
||||
@@ -572,60 +572,6 @@ impl VirtualDom {
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace a template at runtime. This will re-render all components that use this template.
|
||||
/// This is the primitive that enables hot-reloading.
|
||||
///
|
||||
/// The caller must ensure that the template references the same dynamic attributes and nodes as the original template.
|
||||
///
|
||||
/// This will only replace the parent template, not any nested templates.
|
||||
#[instrument(skip(self), level = "trace", name = "VirtualDom::replace_template")]
|
||||
pub fn replace_template(&mut self, template: Template) {
|
||||
// we only replace templates if hot reloading is enabled
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
// Save the template ID
|
||||
self.templates.insert(template.name, template);
|
||||
|
||||
// Only queue the template to be written if its not completely dynamic
|
||||
if !template.is_completely_dynamic() {
|
||||
self.queued_templates.push(template);
|
||||
}
|
||||
|
||||
// iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
|
||||
let mut dirty = Vec::new();
|
||||
for (id, scope) in self.scopes.iter() {
|
||||
// Recurse into the dynamic nodes of the existing mounted node to see if the template is alive in the tree
|
||||
fn check_node_for_templates(node: &crate::VNode, template: Template) -> bool {
|
||||
if node.template.get().name == template.name {
|
||||
return true;
|
||||
}
|
||||
|
||||
for dynamic in node.dynamic_nodes.iter() {
|
||||
if let crate::DynamicNode::Fragment(nodes) = dynamic {
|
||||
for node in nodes {
|
||||
if check_node_for_templates(node, template) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
if let Some(sync) = scope.try_root_node() {
|
||||
if check_node_for_templates(sync, template) {
|
||||
dirty.push(ScopeId(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for dirty in dirty {
|
||||
self.mark_dirty(dirty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuild the virtualdom without handling any of the mutations
|
||||
///
|
||||
/// This is useful for testing purposes and in cases where you render the output of the virtualdom without
|
||||
@@ -885,11 +831,11 @@ impl VirtualDom {
|
||||
return;
|
||||
};
|
||||
let el_ref = &mount.node;
|
||||
let node_template = el_ref.template.get();
|
||||
let node_template = el_ref.template;
|
||||
let target_path = path.path;
|
||||
|
||||
// Accumulate listeners into the listener list bottom to top
|
||||
for (idx, this_path) in node_template.breadth_first_attribute_paths() {
|
||||
for (idx, this_path) in node_template.attr_paths.iter().enumerate() {
|
||||
let attrs = &*el_ref.dynamic_attrs[idx];
|
||||
|
||||
for attr in attrs.iter() {
|
||||
@@ -943,10 +889,10 @@ impl VirtualDom {
|
||||
return;
|
||||
};
|
||||
let el_ref = &mount.node;
|
||||
let node_template = el_ref.template.get();
|
||||
let node_template = el_ref.template;
|
||||
let target_path = node.path;
|
||||
|
||||
for (idx, this_path) in node_template.breadth_first_attribute_paths() {
|
||||
for (idx, this_path) in node_template.attr_paths.iter().enumerate() {
|
||||
let attrs = &*el_ref.dynamic_attrs[idx];
|
||||
|
||||
for attr in attrs.iter() {
|
||||
|
||||
Reference in new issue
Block a user