use dioxus::prelude::*; use dioxus_native_core::{custom_element::CustomElement, prelude::*}; use dioxus_native_core_macro::partial_derive_state; use shipyard::Component; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Component)] pub struct ColorState { color: usize, } #[partial_derive_state] impl State for ColorState { type ParentDependencies = (Self,); type ChildDependencies = (); type NodeDependencies = (); // The color state should not be effected by the shadow dom const TRAVERSE_SHADOW_DOM: bool = false; const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new() .with_attrs(AttributeMaskBuilder::Some(&["color"])) .with_element(); fn update<'a>( &mut self, view: NodeView, _: ::ElementBorrowed<'a>, parent: Option<::ElementBorrowed<'a>>, _: Vec<::ElementBorrowed<'a>>, _: &SendAnyMap, ) -> bool { if let Some(size) = view .attributes() .into_iter() .flatten() .find(|attr| attr.attribute.name == "color") { self.color = size .value .as_float() .or_else(|| size.value.as_int().map(|i| i as f64)) .or_else(|| size.value.as_text().and_then(|i| i.parse().ok())) .unwrap_or(0.0) as usize; } else if let Some((parent,)) = parent { *self = *parent; } true } fn create<'a>( node_view: NodeView<()>, node: ::ElementBorrowed<'a>, parent: Option<::ElementBorrowed<'a>>, children: Vec<::ElementBorrowed<'a>>, context: &SendAnyMap, ) -> Self { let mut myself = Self::default(); myself.update(node_view, node, parent, children, context); myself } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Component)] pub struct LayoutState { size: usize, } #[partial_derive_state] impl State for LayoutState { type ParentDependencies = (Self,); type ChildDependencies = (); type NodeDependencies = (); // The layout state should be effected by the shadow dom const TRAVERSE_SHADOW_DOM: bool = true; const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new() .with_attrs(AttributeMaskBuilder::Some(&["size"])) .with_element(); fn update<'a>( &mut self, view: NodeView, _: ::ElementBorrowed<'a>, parent: Option<::ElementBorrowed<'a>>, _: Vec<::ElementBorrowed<'a>>, _: &SendAnyMap, ) -> bool { if let Some(size) = view .attributes() .into_iter() .flatten() .find(|attr| attr.attribute.name == "size") { self.size = size .value .as_float() .or_else(|| size.value.as_int().map(|i| i as f64)) .or_else(|| size.value.as_text().and_then(|i| i.parse().ok())) .unwrap_or(0.0) as usize; } else if let Some((parent,)) = parent { if parent.size > 0 { self.size = parent.size - 1; } } true } fn create<'a>( node_view: NodeView<()>, node: ::ElementBorrowed<'a>, parent: Option<::ElementBorrowed<'a>>, children: Vec<::ElementBorrowed<'a>>, context: &SendAnyMap, ) -> Self { let mut myself = Self::default(); myself.update(node_view, node, parent, children, context); myself } } mod dioxus_elements { macro_rules! builder_constructors { ( $( $(#[$attr:meta])* $name:ident { $( $(#[$attr_method:meta])* $fil:ident: $vil:ident, )* }; )* ) => { $( #[allow(non_camel_case_types)] $(#[$attr])* pub struct $name; #[allow(non_upper_case_globals, unused)] impl $name { pub const TAG_NAME: &'static str = stringify!($name); pub const NAME_SPACE: Option<&'static str> = None; $( pub const $fil: (&'static str, Option<&'static str>, bool) = (stringify!($fil), None, false); )* } impl GlobalAttributes for $name {} )* } } pub trait GlobalAttributes {} pub trait SvgAttributes {} builder_constructors! { customelementslot { size: attr, color: attr, }; customelementnoslot { size: attr, color: attr, }; testing132 { color: attr, }; } } struct CustomElementWithSlot { root: NodeId, slot: NodeId, } impl CustomElement for CustomElementWithSlot { const NAME: &'static str = "customelementslot"; fn create(mut node: NodeMut<()>) -> Self { let dom = node.real_dom_mut(); let child = dom.create_node(ElementNode { tag: "div".into(), namespace: None, attributes: Default::default(), listeners: Default::default(), }); let slot_id = child.id(); let mut root = dom.create_node(ElementNode { tag: "div".into(), namespace: None, attributes: Default::default(), listeners: Default::default(), }); root.add_child(slot_id); Self { root: root.id(), slot: slot_id, } } fn slot(&self) -> Option { Some(self.slot) } fn roots(&self) -> Vec { vec![self.root] } fn attributes_changed( &mut self, node: NodeMut<()>, attributes: &dioxus_native_core::node_ref::AttributeMask, ) { println!("attributes_changed"); println!("{:?}", attributes); println!("{:?}: {:#?}", node.id(), &*node.node_type()); } } struct CustomElementWithNoSlot { root: NodeId, } impl CustomElement for CustomElementWithNoSlot { const NAME: &'static str = "customelementnoslot"; fn create(mut node: NodeMut<()>) -> Self { let dom = node.real_dom_mut(); let root = dom.create_node(ElementNode { tag: "div".into(), namespace: None, attributes: Default::default(), listeners: Default::default(), }); Self { root: root.id() } } fn roots(&self) -> Vec { vec![self.root] } fn attributes_changed( &mut self, node: NodeMut<()>, attributes: &dioxus_native_core::node_ref::AttributeMask, ) { println!("attributes_changed"); println!("{:?}", attributes); println!("{:?}: {:#?}", node.id(), &*node.node_type()); } } #[test] fn custom_elements_work() { fn app(cx: Scope) -> Element { let count = use_state(cx, || 0); use_future!(cx, |count| async move { count.with_mut(|count| *count += 1); }); cx.render(rsx! { customelementslot { size: "{count}", color: "1", customelementslot { testing132 {} } } }) } let rt = tokio::runtime::Builder::new_current_thread() .enable_time() .build() .unwrap(); rt.block_on(async { let mut rdom = RealDom::new([LayoutState::to_type_erased(), ColorState::to_type_erased()]); rdom.register_custom_element::(); let mut dioxus_state = DioxusState::create(&mut rdom); let mut dom = VirtualDom::new(app); let mutations = dom.rebuild(); dioxus_state.apply_mutations(&mut rdom, mutations); let ctx = SendAnyMap::new(); rdom.update_state(ctx); for i in 0..10usize { dom.wait_for_work().await; let mutations = dom.render_immediate(); dioxus_state.apply_mutations(&mut rdom, mutations); let ctx = SendAnyMap::new(); rdom.update_state(ctx); // render... rdom.traverse_depth_first_advanced(true, |node| { let node_type = &*node.node_type(); let height = node.height() as usize; let indent = " ".repeat(height); let color = *node.get::().unwrap(); let size = *node.get::().unwrap(); let id = node.id(); println!("{indent}{id:?} {color:?} {size:?} {node_type:?}"); match node_type { NodeType::Element(el) => { match el.tag.as_str() { // the color should bubble up from customelementslot "testing132" | "customelementslot" => { assert_eq!(color.color, 1); } // the color of the light dom should not effect the color of the shadow dom, so the color of divs in the shadow dom should be 0 "div" => { assert_eq!(color.color, 0); } _ => {} } if el.tag != "Root" { assert_eq!(size.size, (i + 2).saturating_sub(height)); } } _ => {} } }); } }); } #[test] #[should_panic] fn slotless_custom_element_cant_have_children() { fn app(cx: Scope) -> Element { cx.render(rsx! { customelementnoslot { testing132 {} } }) } let rt = tokio::runtime::Builder::new_current_thread() .enable_time() .build() .unwrap(); rt.block_on(async { let mut rdom = RealDom::new([LayoutState::to_type_erased(), ColorState::to_type_erased()]); rdom.register_custom_element::(); let mut dioxus_state = DioxusState::create(&mut rdom); let mut dom = VirtualDom::new(app); let mutations = dom.rebuild(); dioxus_state.apply_mutations(&mut rdom, mutations); let ctx = SendAnyMap::new(); rdom.update_state(ctx); }); } #[test] fn slotless_custom_element() { fn app(cx: Scope) -> Element { cx.render(rsx! { customelementnoslot { } }) } let rt = tokio::runtime::Builder::new_current_thread() .enable_time() .build() .unwrap(); rt.block_on(async { let mut rdom = RealDom::new([LayoutState::to_type_erased(), ColorState::to_type_erased()]); rdom.register_custom_element::(); let mut dioxus_state = DioxusState::create(&mut rdom); let mut dom = VirtualDom::new(app); let mutations = dom.rebuild(); dioxus_state.apply_mutations(&mut rdom, mutations); let ctx = SendAnyMap::new(); rdom.update_state(ctx); }); }