allow many attributes to be attached to one element

This commit is contained in:
Evan Almloff 2023-09-20 16:02:04 -05:00
parent dc446b5e5b
commit fc8c25280a
12 changed files with 289 additions and 96 deletions

View file

@ -1,5 +1,7 @@
use crate::any_props::AnyProps;
use crate::innerlude::{BorrowedAttributeValue, VComponent, VPlaceholder, VText};
use crate::innerlude::{
AttributeType, BorrowedAttributeValue, MountedAttribute, VComponent, VPlaceholder, VText,
};
use crate::mutations::Mutation;
use crate::mutations::Mutation::*;
use crate::nodes::VNode;
@ -283,7 +285,7 @@ impl<'b> VirtualDom {
let id = self.assign_static_node_as_dynamic(path, root, node, attr_id);
loop {
self.write_attribute(&node.dynamic_attrs[attr_id], id);
self.write_attribute_type(&node.dynamic_attrs[attr_id], id);
// Only push the dynamic attributes forward if they match the current path (same element)
match attrs.next_if(|(_, p)| *p == path) {
@ -294,10 +296,20 @@ impl<'b> VirtualDom {
}
}
fn write_attribute(&mut self, attribute: &'b crate::Attribute<'b>, id: ElementId) {
fn write_attribute_type(&mut self, attribute: &'b MountedAttribute<'b>, id: ElementId) {
// Make sure we set the attribute's associated id
attribute.mounted_element.set(id);
match &attribute.ty {
AttributeType::Single(attribute) => self.write_attribute(attribute, id),
AttributeType::Many(attribute) => {
for attribute in *attribute {
self.write_attribute(attribute, id);
}
}
}
}
pub(crate) fn write_attribute(&mut self, attribute: &'b crate::Attribute<'b>, id: ElementId) {
// Safety: we promise not to re-alias this text later on after committing it to the mutation
let unbounded_name: &str = unsafe { std::mem::transmute(attribute.name) };

View file

@ -1,7 +1,9 @@
use crate::{
any_props::AnyProps,
arena::ElementId,
innerlude::{BorrowedAttributeValue, DirtyScope, VComponent, VPlaceholder, VText},
innerlude::{
AttributeType, BorrowedAttributeValue, DirtyScope, VComponent, VPlaceholder, VText,
},
mutations::Mutation,
nodes::RenderReturn,
nodes::{DynamicNode, VNode},
@ -103,16 +105,51 @@ impl<'b> VirtualDom {
.zip(right_template.dynamic_attrs.iter())
.for_each(|(left_attr, right_attr)| {
// Move over the ID from the old to the new
right_attr
.mounted_element
.set(left_attr.mounted_element.get());
let mounted_id = left_attr.mounted_element.get();
right_attr.mounted_element.set(mounted_id);
// We want to make sure anything that gets pulled is valid
self.update_template(left_attr.mounted_element.get(), right_template);
// If the attributes are different (or volatile), we need to update them
if left_attr.value != right_attr.value || left_attr.volatile {
self.update_attribute(right_attr, left_attr);
match (&left_attr.ty, &right_attr.ty) {
(AttributeType::Single(left), AttributeType::Single(right)) => {
self.diff_attribute(left, right, mounted_id)
}
(AttributeType::Many(left), AttributeType::Many(right)) => {
let mut left_iter = left.iter().peekable();
let mut right_iter = right.iter().peekable();
loop {
match (left_iter.peek(), right_iter.peek()) {
(Some(left), Some(right)) => {
// check which name is greater
match left.name.cmp(right.name) {
std::cmp::Ordering::Less => self.remove_attribute(
left.name,
left.namespace,
mounted_id,
),
std::cmp::Ordering::Greater => {
self.write_attribute(right, mounted_id)
}
std::cmp::Ordering::Equal => {
self.diff_attribute(left, right, mounted_id)
}
}
}
(Some(_), None) => {
let left = left_iter.next().unwrap();
self.remove_attribute(left.name, left.namespace, mounted_id)
}
(None, Some(_)) => {
let right = right_iter.next().unwrap();
self.write_attribute(right, mounted_id)
}
(None, None) => break,
}
}
}
_ => unreachable!("The macro should never generate this case"),
}
});
@ -138,6 +175,18 @@ impl<'b> VirtualDom {
}
}
fn diff_attribute(
&mut self,
left_attr: &'b Attribute<'b>,
right_attr: &'b Attribute<'b>,
id: ElementId,
) {
// If the attributes are different (or volatile), we need to update them
if left_attr.value != right_attr.value || left_attr.volatile {
self.update_attribute(right_attr, left_attr, id);
}
}
fn diff_dynamic_node(
&mut self,
left_node: &'b DynamicNode<'b>,
@ -155,12 +204,29 @@ impl<'b> VirtualDom {
};
}
fn update_attribute(&mut self, right_attr: &'b Attribute<'b>, left_attr: &'b Attribute) {
fn remove_attribute(&mut self, name: &'b str, ns: Option<&'static str>, id: ElementId) {
let name = unsafe { std::mem::transmute(name) };
let value: BorrowedAttributeValue<'b> = BorrowedAttributeValue::None;
let value = unsafe { std::mem::transmute(value) };
self.mutations.push(Mutation::SetAttribute {
id,
ns,
name,
value,
});
}
fn update_attribute(
&mut self,
right_attr: &'b Attribute<'b>,
left_attr: &'b Attribute<'b>,
id: ElementId,
) {
let name = unsafe { std::mem::transmute(left_attr.name) };
let value: BorrowedAttributeValue<'b> = (&right_attr.value).into();
let value = unsafe { std::mem::transmute(value) };
self.mutations.push(Mutation::SetAttribute {
id: left_attr.mounted_element.get(),
id,
ns: right_attr.namespace,
name,
value,

View file

@ -73,11 +73,11 @@ pub(crate) mod innerlude {
}
pub use crate::innerlude::{
fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue,
CapturedError, Component, DynamicNode, Element, ElementId, Event, Fragment, HasAttributesBox,
IntoDynNode, LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope, ScopeId,
ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode,
VPlaceholder, VText, VirtualDom,
fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeType, AttributeValue,
BorrowedAttributeValue, CapturedError, Component, DynamicNode, Element, ElementId, Event,
Fragment, HasAttributesBox, IntoDynNode, LazyNodes, MountedAttribute, Mutation, Mutations,
Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, TaskId, Template,
TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, VirtualDom,
};
/// The purpose of this module is to alleviate imports of many common types
@ -88,9 +88,9 @@ pub mod prelude {
consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context,
provide_context, provide_context_to_scope, provide_root_context, push_future,
remove_future, schedule_update_any, spawn, spawn_forever, suspend, throw, AnyValue,
Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, LazyNodes,
Properties, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute,
TemplateNode, Throw, VNode, VirtualDom,
AttributeType, Component, Element, Event, EventHandler, Fragment, IntoAttributeValue,
LazyNodes, MountedAttribute, Properties, Scope, ScopeId, ScopeState, Scoped, TaskId,
Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
};
}

View file

@ -60,7 +60,7 @@ pub struct VNode<'a> {
pub dynamic_nodes: &'a [DynamicNode<'a>],
/// The dynamic parts of the template
pub dynamic_attrs: &'a [Attribute<'a>],
pub dynamic_attrs: &'a [MountedAttribute<'a>],
}
impl<'a> VNode<'a> {
@ -414,6 +414,44 @@ pub enum TemplateAttribute<'a> {
},
}
#[derive(Debug)]
pub struct MountedAttribute<'a> {
pub(crate) ty: AttributeType<'a>,
/// The element in the DOM that this attribute belongs to
pub(crate) mounted_element: Cell<ElementId>,
}
impl<'a> From<Attribute<'a>> for MountedAttribute<'a> {
fn from(attr: Attribute<'a>) -> Self {
Self {
ty: AttributeType::Single(attr),
mounted_element: Default::default(),
}
}
}
impl<'a> From<&'a [Attribute<'a>]> for MountedAttribute<'a> {
fn from(attr: &'a [Attribute<'a>]) -> Self {
Self {
ty: AttributeType::Many(attr),
mounted_element: Default::default(),
}
}
}
impl<'a> MountedAttribute<'a> {
/// Get the type of this attribute
pub fn attribute_type(&self) -> &AttributeType<'a> {
&self.ty
}
/// Get the element that this attribute is mounted to
pub fn mounted_element(&self) -> ElementId {
self.mounted_element.get()
}
}
/// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"`
#[derive(Debug)]
pub struct Attribute<'a> {
@ -430,9 +468,6 @@ pub struct Attribute<'a> {
/// An indication of we should always try and set the attribute. Used in controlled components to ensure changes are propagated
pub volatile: bool,
/// The element in the DOM that this attribute belongs to
pub(crate) mounted_element: Cell<ElementId>,
}
impl<'a> Attribute<'a> {
@ -448,13 +483,38 @@ impl<'a> Attribute<'a> {
value,
namespace,
volatile,
mounted_element: Cell::new(ElementId::default()),
}
}
}
#[derive(Debug)]
pub enum AttributeType<'a> {
Single(Attribute<'a>),
/// Many different attributes sorted by name
Many(&'a [Attribute<'a>]),
}
impl<'a> AttributeType<'a> {
/// Call the given function on each attribute
pub fn for_each<'b, F>(&'b self, mut f: F)
where
F: FnMut(&'b Attribute<'a>),
{
match self {
Self::Single(attr) => f(attr),
Self::Many(attrs) => attrs.iter().for_each(f),
}
}
/// Get the element that this attribute is mounted to
pub fn mounted_element(&self) -> ElementId {
self.mounted_element.get()
/// Try to call the given function on each attribute
pub fn try_for_each<'b, F, E>(&'b self, mut f: F) -> Result<(), E>
where
F: FnMut(&'b Attribute<'a>) -> Result<(), E>,
{
match self {
Self::Single(attr) => f(attr),
Self::Many(attrs) => attrs.iter().try_for_each(f),
}
}
}

View file

@ -8,7 +8,8 @@ use crate::{
nodes::{IntoAttributeValue, IntoDynNode, RenderReturn},
runtime::Runtime,
scope_context::ScopeContext,
AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId,
AnyValue, Attribute, AttributeType, AttributeValue, Element, Event, MountedAttribute,
Properties, TaskId,
};
use bumpalo::{boxed::Box as BumpBox, Bump};
use std::{
@ -335,14 +336,10 @@ impl<'src> ScopeState {
let mut listeners = self.attributes_to_drop.borrow_mut();
for attr in element.dynamic_attrs {
match attr.value {
AttributeValue::Any(_) | AttributeValue::Listener(_) => {
let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
listeners.push(unbounded);
}
_ => (),
}
attr.ty.for_each(|attr| {
let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
listeners.push(unbounded);
})
}
let mut props = self.borrowed_props.borrow_mut();
@ -393,13 +390,15 @@ impl<'src> ScopeState {
value: impl IntoAttributeValue<'src>,
namespace: Option<&'static str>,
volatile: bool,
) -> Attribute<'src> {
Attribute {
name,
namespace,
volatile,
) -> MountedAttribute<'src> {
MountedAttribute {
ty: AttributeType::Single(Attribute {
name,
namespace,
volatile,
value: value.into_value(self.bump()),
}),
mounted_element: Default::default(),
value: value.into_value(self.bump()),
}
}

View file

@ -5,13 +5,13 @@
use crate::{
any_props::VProps,
arena::{ElementId, ElementRef},
innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
innerlude::{AttributeType, DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
mutations::Mutation,
nodes::RenderReturn,
nodes::{Template, TemplateId},
runtime::{Runtime, RuntimeGuard},
scopes::{ScopeId, ScopeState},
AttributeValue, Element, Event, Scope,
Attribute, AttributeValue, Element, Event, Scope,
};
use futures_util::{pin_mut, StreamExt};
use rustc_hash::{FxHashMap, FxHashSet};
@ -371,12 +371,30 @@ impl VirtualDom {
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
let this_path = node_template.attr_paths[idx];
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
if attr.name.trim_start_matches("on") == name
&& target_path.is_decendant(&this_path)
{
listeners.push(&attr.value);
fn add_listener<'a>(
attribute: &'a Attribute<'a>,
event_name: &str,
listeners: &mut Vec<&'a AttributeValue<'a>>,
) {
if attribute.name.trim_start_matches("on") == event_name {
listeners.push(&attribute.value);
}
listeners.push(&attribute.value);
}
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
if target_path.is_decendant(&this_path) {
match &attr.ty {
AttributeType::Single(attribute) => {
add_listener(attribute, name, &mut listeners);
}
AttributeType::Many(attributes) => {
for attribute in *attributes {
add_listener(attribute, name, &mut listeners);
}
}
}
// Break if this is the exact target element.
// This means we won't call two listeners with the same name on the same element. This should be
// documented, or be rejected from the rsx! macro outright
@ -422,20 +440,57 @@ impl VirtualDom {
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
let this_path = node_template.attr_paths[idx];
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
// Only call the listener if this is the exact target element.
if attr.name.trim_start_matches("on") == name && target_path == this_path {
if let AttributeValue::Listener(listener) = &attr.value {
let origin = el_ref.scope;
self.runtime.scope_stack.borrow_mut().push(origin);
self.runtime.rendering.set(false);
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
cb(uievent.clone());
}
self.runtime.scope_stack.borrow_mut().pop();
self.runtime.rendering.set(true);
fn call_listener(
attribute: &Attribute,
event_name: &str,
uievent: &Event<dyn Any>,
runtime: &Runtime,
origin: ScopeId,
) -> bool {
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
// Only call the listener if this is the exact target element.
if attribute.name.trim_start_matches("on") == event_name {
if let AttributeValue::Listener(listener) = &attribute.value {
runtime.scope_stack.borrow_mut().push(origin);
runtime.rendering.set(false);
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
cb(uievent.clone());
}
runtime.scope_stack.borrow_mut().pop();
runtime.rendering.set(true);
break;
return true;
}
}
false
}
if target_path == this_path {
match &attr.ty {
AttributeType::Single(attribute) => {
if call_listener(
attribute,
name,
&uievent,
&self.runtime,
el_ref.scope,
) {
return;
}
}
AttributeType::Many(attributes) => {
for attribute in *attributes {
if call_listener(
attribute,
name,
&uievent,
&self.runtime,
el_ref.scope,
) {
return;
}
}
}
}
}
}

View file

@ -1,7 +1,7 @@
#![cfg(not(miri))]
use dioxus::prelude::Props;
use dioxus_core::*;
use dioxus_core::{MountedAttribute, *};
use std::{cell::Cell, collections::HashSet};
fn random_ns() -> Option<&'static str> {
@ -205,7 +205,7 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode {
}
}
fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
fn create_random_dynamic_attr(cx: &ScopeState) -> MountedAttribute {
let value = match rand::random::<u8>() % 7 {
0 => AttributeValue::Text(Box::leak(
format!("{}", rand::random::<usize>()).into_boxed_str(),
@ -217,7 +217,7 @@ fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
5 => AttributeValue::None,
6 => {
let value = cx.listener(|e: Event<String>| println!("{:?}", e));
return Attribute::new("ondata", value, None, false);
return Attribute::new("ondata", value, None, false).into();
}
_ => unreachable!(),
};
@ -227,6 +227,7 @@ fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
random_ns(),
rand::random(),
)
.into()
}
static mut TEMPLATE_COUNT: usize = 0;

View file

@ -164,7 +164,7 @@ fn props_spread() {
static TEMPLATE: ::dioxus::core::Template = ::dioxus::core::Template { name: concat!(file!(), ":", line!(), ":", column!(), ":", "" ), roots: &[::dioxus::core::TemplateNode::Element { tag: dioxus_elements::audio::TAG_NAME, namespace: dioxus_elements::audio::NAME_SPACE, attrs: &[::dioxus::core::TemplateAttribute::Dynamic { id: 0usize }], children: &[] }], node_paths: &[], attr_paths: &[&[0u8]] };
let mut attrs = vec![__cx.attr(dioxus_elements::audio::muted.0, muted, dioxus_elements::audio::muted.1, dioxus_elements::audio::muted.2)];
for attr in attributes {
attrs.push(__cx.attr(attr.name, attr.value.into_value(__cx.bump()), attr.namespace, attr.volatile));
attrs.push(attr);
}
::dioxus::core::VNode {
parent: None,

View file

@ -1 +1 @@
fn main() {}
fn main() {}

View file

@ -9,7 +9,7 @@ macro_rules! impl_event {
$(
$( #[$attr] )*
#[inline]
pub fn $name<'a, E: crate::EventReturn<T>, T>(_cx: &'a ::dioxus_core::ScopeState, mut _f: impl FnMut(::dioxus_core::Event<$data>) -> E + 'a) -> ::dioxus_core::Attribute<'a> {
pub fn $name<'a, E: crate::EventReturn<T>, T>(_cx: &'a ::dioxus_core::ScopeState, mut _f: impl FnMut(::dioxus_core::Event<$data>) -> E + 'a) -> ::dioxus_core::MountedAttribute<'a> {
::dioxus_core::Attribute::new(
stringify!($name),
_cx.listener(move |e: ::dioxus_core::Event<$data>| {
@ -17,7 +17,7 @@ macro_rules! impl_event {
}),
None,
false,
)
).into()
}
)*
};

View file

@ -167,7 +167,7 @@ impl Parse for Element {
attributes,
children,
brace,
extra_attributes
extra_attributes,
})
}
}
@ -397,11 +397,3 @@ impl ToTokens for ElementAttrNamed {
tokens.append_all(attribute);
}
}
// ::dioxus::core::Attribute {
// name: stringify!(#name),
// namespace: None,
// volatile: false,
// mounted_node: Default::default(),
// value: ::dioxus::core::AttributeValue::Text(#value),
// }

View file

@ -79,23 +79,31 @@ impl Renderer {
match segment {
Segment::Attr(idx) => {
let attr = &template.dynamic_attrs[*idx];
if attr.name == "dangerous_inner_html" {
inner_html = Some(attr);
} else if attr.namespace == Some("style") {
accumulated_dynamic_styles.push(attr);
} else {
match attr.value {
AttributeValue::Text(value) => {
write!(buf, " {}=\"{}\"", attr.name, value)?
}
AttributeValue::Bool(value) => write!(buf, " {}={}", attr.name, value)?,
AttributeValue::Int(value) => write!(buf, " {}={}", attr.name, value)?,
AttributeValue::Float(value) => {
write!(buf, " {}={}", attr.name, value)?
}
_ => {}
};
}
attr.attribute_type().try_for_each(|attr| {
if attr.name == "dangerous_inner_html" {
inner_html = Some(attr);
} else if attr.namespace == Some("style") {
accumulated_dynamic_styles.push(attr);
} else {
match attr.value {
AttributeValue::Text(value) => {
write!(buf, " {}=\"{}\"", attr.name, value)?
}
AttributeValue::Bool(value) => {
write!(buf, " {}={}", attr.name, value)?
}
AttributeValue::Int(value) => {
write!(buf, " {}={}", attr.name, value)?
}
AttributeValue::Float(value) => {
write!(buf, " {}={}", attr.name, value)?
}
_ => {}
};
}
Ok(())
})?;
}
Segment::Node(idx) => match &template.dynamic_nodes[*idx] {
DynamicNode::Component(node) => {