diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index e1c445262..475000016 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -39,19 +39,6 @@ pub fn rsx(s: TokenStream) -> TokenStream { } } -/// A version of the rsx! macro that does not use templates. Used for testing diffing -#[proc_macro] -pub fn rsx_without_templates(s: TokenStream) -> TokenStream { - match syn::parse::(s) { - Err(err) => err.to_compile_error().into(), - Ok(body) => { - let mut tokens = proc_macro2::TokenStream::new(); - body.to_tokens_without_template(&mut tokens); - tokens.into() - } - } -} - /// The render! macro makes it easy for developers to write jsx-style markup in their components. /// /// The render macro automatically renders rsx - making it unhygenic. @@ -64,18 +51,10 @@ pub fn rsx_without_templates(s: TokenStream) -> TokenStream { pub fn render(s: TokenStream) -> TokenStream { match syn::parse::(s) { Err(err) => err.to_compile_error().into(), - Ok(body) => { - let mut inner = proc_macro2::TokenStream::new(); - body.to_tokens_without_lazynodes(&mut inner); - quote::quote! { - { - let __cx = NodeFactory::new(&cx.scope); - Some(#inner) - } - } + Ok(mut body) => { + body.inline_cx = true; + body.into_token_stream().into() } - .into_token_stream() - .into(), } } diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 37a089f37..6fece74c8 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -3,7 +3,7 @@ use crate::mutations::Mutation::*; use crate::nodes::VNode; use crate::nodes::{DynamicNode, DynamicNodeKind, TemplateNode}; use crate::virtualdom::VirtualDom; -use crate::TemplateAttribute; +use crate::{AttributeValue, TemplateAttribute}; impl VirtualDom { /// Create this template and write its mutations @@ -52,25 +52,41 @@ impl VirtualDom { TemplateNode::DynamicText { .. } => 1, }; + let mut cur_route = None; + // we're on top of a node that has a dynamic attribute for a descendant // Set that attribute now before the stack gets in a weird state - while let Some(loc) = dynamic_attrs.next_if(|a| a.path[0] == root_idx as u8) { + while let Some(attr) = dynamic_attrs.next_if(|a| a.path[0] == root_idx as u8) { + if cur_route.is_none() { + cur_route = Some((self.next_element(template), &attr.path[1..])); + } + // Attach all the elementIDs to the nodes with dynamic content - let id = self.next_element(template); + let (id, path) = cur_route.unwrap(); - mutations.push(AssignId { - path: &loc.path[1..], - id, - }); + mutations.push(AssignId { path, id }); + attr.mounted_element.set(id); - loc.mounted_element.set(id); + match attr.value { + AttributeValue::Text(value) => { + mutations.push(SetAttribute { + name: attr.name, + value, + id, + }); + } - for attr in loc.attrs.iter() { - mutations.push(SetAttribute { - name: attr.name, - value: attr.value, - id, - }); + AttributeValue::Listener(_) => { + // + } + + AttributeValue::Float(_) => todo!(), + AttributeValue::Int(_) => todo!(), + AttributeValue::Bool(_) => todo!(), + AttributeValue::Any(_) => todo!(), + + // Optional attributes + AttributeValue::None => todo!(), } } @@ -94,6 +110,7 @@ impl VirtualDom { node: &'a TemplateNode<'static>, ) { match *node { + // Todo: create the children's template TemplateNode::Dynamic(_) => mutations.push(CreatePlaceholder), TemplateNode::Text(value) => mutations.push(CreateText { value }), TemplateNode::DynamicText { .. } => mutations.push(CreateText { diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 3b7ebf4a1..d4fc0f8b6 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -1,6 +1,6 @@ use std::{any::Any, cell::Cell}; -use crate::{arena::ElementId, nodes::Listener, scopes::ScopeId, virtualdom::VirtualDom}; +use crate::{arena::ElementId, scopes::ScopeId, virtualdom::VirtualDom, Attribute, AttributeValue}; /// User Events are events that are shuttled from the renderer into the [`VirtualDom`] through the scheduler channel. /// @@ -106,12 +106,12 @@ impl VirtualDom { let location = unsafe { &mut *path.template } .dynamic_attrs - .iter_mut() + .iter() .position(|attr| attr.mounted_element.get() == event.element)?; let mut index = Some((path.template, location)); - let mut listeners = Vec::<&mut Listener>::new(); + let mut listeners = Vec::<&Attribute>::new(); while let Some((raw_parent, dyn_index)) = index { let parent = unsafe { &mut *raw_parent }; @@ -120,17 +120,18 @@ impl VirtualDom { listeners.extend( parent .dynamic_attrs - .iter_mut() + .iter() .filter(|attr| is_path_ascendant(attr.path, path)) - .map(|f| f.listeners.iter_mut().filter(|f| f.name == event.name)) - .flatten(), + .filter(|attr| attr.name == event.name), ); index = parent.parent; } for listener in listeners { - (listener.callback)(&event.event); + if let AttributeValue::Listener(listener) = &listener.value { + (listener.borrow_mut())(&event.event) + } } Some(()) diff --git a/packages/core/src/factory.rs b/packages/core/src/factory.rs index 09d316029..c41bb98cb 100644 --- a/packages/core/src/factory.rs +++ b/packages/core/src/factory.rs @@ -1,9 +1,12 @@ use std::{cell::Cell, fmt::Arguments}; +use bumpalo::Bump; + use crate::{ + any_props::{AnyProps, VComponentProps}, arena::ElementId, innerlude::{DynamicNode, DynamicNodeKind}, - LazyNodes, ScopeState, VNode, + Attribute, AttributeValue, Element, LazyNodes, Properties, Scope, ScopeState, VNode, }; impl ScopeState { @@ -50,6 +53,56 @@ impl ScopeState { }, } } + + /// Create a new [`Attribute`] + pub fn attr<'a>( + &'a self, + name: &'static str, + val: impl IntoAttributeValue<'a>, + namespace: Option<&'static str>, + is_volatile: bool, + ) -> Attribute<'a> { + Attribute { + name, + namespace, + mounted_element: Cell::new(ElementId(0)), + path: &[0], + value: val.into_value(self.bump()), + } + } + + /// Create a new [`VNode::Component`] + pub fn component<'a, P>( + &'a self, + component: fn(Scope<'a, P>) -> Element, + props: P, + fn_name: &'static str, + ) -> DynamicNode<'a> + where + P: Properties + 'a, + { + let props = self.bump().alloc(props); + let detached = unsafe { std::mem::transmute(component) }; + let vcomp = VComponentProps::new(detached, P::memoize, props); + let as_dyn = self.bump().alloc(vcomp) as &mut dyn AnyProps; + let detached_dyn: *mut dyn AnyProps = unsafe { std::mem::transmute(as_dyn) }; + + // todo: clean up borrowed props + // if !P::IS_STATIC { + // let vcomp = &*vcomp; + // let vcomp = unsafe { std::mem::transmute(vcomp) }; + // self.scope.items.borrow_mut().borrowed_props.push(vcomp); + // } + + DynamicNode { + path: &[], + kind: DynamicNodeKind::Component { + name: fn_name, + can_memoize: P::IS_STATIC, + props: detached_dyn, + }, + } + } } pub trait IntoVnode<'a, A = ()> { @@ -61,3 +114,56 @@ impl<'a, 'b> IntoVnode<'a> for LazyNodes<'a, 'b> { self.call(cx) } } + +impl<'a, 'b> IntoVnode<'a> for VNode<'a> { + fn into_dynamic_node(self, cx: &'a ScopeState) -> VNode<'a> { + self + } +} +impl<'a, 'b> IntoVnode<'a> for &'a VNode<'a> { + fn into_dynamic_node(self, cx: &'a ScopeState) -> VNode<'a> { + VNode { + node_id: self.node_id.clone(), + parent: self.parent, + template: self.template, + root_ids: self.root_ids, + dynamic_nodes: self.dynamic_nodes, + dynamic_attrs: self.dynamic_attrs, + } + } +} + +/// A value that can be converted into an attribute value +pub trait IntoAttributeValue<'a> { + /// Convert into an attribute value + fn into_value(self, bump: &'a Bump) -> AttributeValue<'a>; +} + +impl<'a> IntoAttributeValue<'a> for &'a str { + fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { + AttributeValue::Text(self) + } +} +impl<'a> IntoAttributeValue<'a> for f32 { + fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { + AttributeValue::Float(self) + } +} +impl<'a> IntoAttributeValue<'a> for i32 { + fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { + AttributeValue::Int(self) + } +} +impl<'a> IntoAttributeValue<'a> for bool { + fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { + AttributeValue::Bool(self) + } +} +impl<'a> IntoAttributeValue<'a> for Arguments<'_> { + fn into_value(self, bump: &'a Bump) -> AttributeValue<'a> { + use bumpalo::core_alloc::fmt::Write; + let mut str_buf = bumpalo::collections::String::new_in(bump); + str_buf.write_fmt(self).unwrap(); + AttributeValue::Text(str_buf.into_bump_str()) + } +} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index c5f77e03d..fd0de5a12 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -12,6 +12,7 @@ mod garbage; mod lazynodes; mod mutations; mod nodes; +mod properties; mod scope_arena; mod scopes; mod virtualdom; @@ -20,10 +21,10 @@ pub(crate) mod innerlude { pub use crate::element::Element; pub use crate::events::*; pub use crate::future_container::*; + pub use crate::lazynodes::*; pub use crate::mutations::*; pub use crate::nodes::*; - // pub use crate::properties::*; - pub use crate::lazynodes::*; + pub use crate::properties::*; pub use crate::scopes::*; pub use crate::virtualdom::*; @@ -66,6 +67,7 @@ pub(crate) mod innerlude { } pub use crate::innerlude::{ + fc_to_builder, // AnyAttributeValue, AnyEvent, Attribute, AttributeValue, Component, Element, ElementId, Attribute, AttributeValue, @@ -74,8 +76,8 @@ pub use crate::innerlude::{ Element, EventPriority, LazyNodes, - Listener, NodeFactory, + Properties, Scope, ScopeId, ScopeState, @@ -96,9 +98,9 @@ pub use crate::innerlude::{ /// This includes types like [`Scope`], [`Element`], and [`Component`]. pub mod prelude { pub use crate::innerlude::{ - Attribute, DynamicNode, DynamicNodeKind, Element, EventPriority, LazyNodes, Listener, - NodeFactory, Scope, ScopeId, ScopeState, TaskId, Template, TemplateAttribute, TemplateNode, - UiEvent, VNode, VirtualDom, + fc_to_builder, Attribute, DynamicNode, DynamicNodeKind, Element, EventPriority, LazyNodes, + NodeFactory, Properties, Scope, ScopeId, ScopeState, TaskId, Template, TemplateAttribute, + TemplateNode, UiEvent, VNode, VirtualDom, }; } diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index e6da66e18..17c9e9395 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -7,7 +7,7 @@ pub struct Renderer<'a> { #[derive(Debug)] pub enum Mutation<'a> { SetAttribute { - name: &'static str, + name: &'a str, value: &'a str, id: ElementId, }, diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 106c01761..c5c2305f9 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,5 +1,9 @@ use crate::{any_props::AnyProps, arena::ElementId}; -use std::{any::Any, cell::Cell, hash::Hasher}; +use std::{ + any::Any, + cell::{Cell, RefCell}, + hash::Hasher, +}; pub type TemplateId = &'static str; @@ -8,17 +12,16 @@ pub struct VNode<'a> { // The ID assigned for the root of this template pub node_id: Cell, - // When rendered, this template will be linked to its parent + // When rendered, this template will be linked to its parent manually pub parent: Option<(*mut VNode<'static>, usize)>, pub template: Template<'static>, pub root_ids: &'a [Cell], - /// All the dynamic nodes for a template - pub dynamic_nodes: &'a mut [DynamicNode<'a>], + pub dynamic_nodes: &'a [DynamicNode<'a>], - pub dynamic_attrs: &'a mut [AttributeLocation<'a>], + pub dynamic_attrs: &'a [Attribute<'a>], } #[derive(Debug, Clone, Copy)] @@ -75,6 +78,7 @@ pub enum DynamicNodeKind<'a> { // IE in caps or with underscores Component { name: &'static str, + can_memoize: bool, props: *mut dyn AnyProps, }, @@ -104,18 +108,12 @@ pub enum TemplateAttribute<'a> { }, } -pub struct AttributeLocation<'a> { - pub mounted_element: Cell, - pub attrs: &'a mut [Attribute<'a>], - pub listeners: &'a mut [Listener<'a>], - pub path: &'static [u8], -} - -#[derive(Debug)] pub struct Attribute<'a> { - pub name: &'static str, - pub value: &'a str, + pub name: &'a str, + pub value: AttributeValue<'a>, pub namespace: Option<&'static str>, + pub mounted_element: Cell, + pub path: &'static [u8], } pub enum AttributeValue<'a> { @@ -123,7 +121,15 @@ pub enum AttributeValue<'a> { Float(f32), Int(i32), Bool(bool), + Listener(RefCell<&'a mut dyn FnMut(&dyn Any)>), Any(&'a dyn AnyValue), + None, +} + +impl<'a> AttributeValue<'a> { + fn is_listener(&self) -> bool { + matches!(self, AttributeValue::Listener(_)) + } } pub trait AnyValue { @@ -142,11 +148,6 @@ where } } -pub struct Listener<'a> { - pub name: &'static str, - pub callback: &'a mut dyn FnMut(&dyn Any), -} - #[test] fn what_are_the_sizes() { dbg!(std::mem::size_of::()); diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs new file mode 100644 index 000000000..569648a6a --- /dev/null +++ b/packages/core/src/properties.rs @@ -0,0 +1,170 @@ +use crate::innerlude::*; + +pub struct FragmentProps<'a>(Element<'a>); +pub struct FragmentBuilder<'a, const BUILT: bool>(Element<'a>); +impl<'a> FragmentBuilder<'a, false> { + pub fn children(self, children: Element<'a>) -> FragmentBuilder<'a, true> { + FragmentBuilder(children) + } +} +impl<'a, const A: bool> FragmentBuilder<'a, A> { + pub fn build(self) -> FragmentProps<'a> { + FragmentProps(self.0) + } +} + +/// Access the children elements passed into the component +/// +/// This enables patterns where a component is passed children from its parent. +/// +/// ## Details +/// +/// Unlike React, Dioxus allows *only* lists of children to be passed from parent to child - not arbitrary functions +/// or classes. If you want to generate nodes instead of accepting them as a list, consider declaring a closure +/// on the props that takes Context. +/// +/// If a parent passes children into a component, the child will always re-render when the parent re-renders. In other +/// words, a component cannot be automatically memoized if it borrows nodes from its parent, even if the component's +/// props are valid for the static lifetime. +/// +/// ## Example +/// +/// ```rust, ignore +/// fn App(cx: Scope) -> Element { +/// cx.render(rsx!{ +/// CustomCard { +/// h1 {} +/// p {} +/// } +/// }) +/// } +/// +/// #[derive(PartialEq, Props)] +/// struct CardProps { +/// children: Element +/// } +/// +/// fn CustomCard(cx: Scope) -> Element { +/// cx.render(rsx!{ +/// div { +/// h1 {"Title card"} +/// {cx.props.children} +/// } +/// }) +/// } +/// ``` +impl<'a> Properties for FragmentProps<'a> { + type Builder = FragmentBuilder<'a, false>; + const IS_STATIC: bool = false; + fn builder() -> Self::Builder { + FragmentBuilder(None) + } + unsafe fn memoize(&self, _other: &Self) -> bool { + false + } +} + +// /// Create inline fragments using Component syntax. +// /// +// /// ## Details +// /// +// /// Fragments capture a series of children without rendering extra nodes. +// /// +// /// Creating fragments explicitly with the Fragment component is particularly useful when rendering lists or tables and +// /// a key is needed to identify each item. +// /// +// /// ## Example +// /// +// /// ```rust, ignore +// /// rsx!{ +// /// Fragment { key: "abc" } +// /// } +// /// ``` +// /// +// /// ## Usage +// /// +// /// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase. +// /// Try to avoid highly nested fragments if you can. Unlike React, there is no protection against infinitely nested fragments. +// /// +// /// This function defines a dedicated `Fragment` component that can be used to create inline fragments in the RSX macro. +// /// +// /// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it. +// #[allow(non_upper_case_globals, non_snake_case)] +// pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element { +// let i = cx.props.0.as_ref().map(|f| f.decouple()); +// cx.render(LazyNodes::new(|f| f.fragment_from_iter(i))) +// } + +/// Every "Props" used for a component must implement the `Properties` trait. This trait gives some hints to Dioxus +/// on how to memoize the props and some additional optimizations that can be made. We strongly encourage using the +/// derive macro to implement the `Properties` trait automatically as guarantee that your memoization strategy is safe. +/// +/// If your props are 'static, then Dioxus will require that they also be PartialEq for the derived memoize strategy. However, +/// if your props borrow data, then the memoization strategy will simply default to "false" and the PartialEq will be ignored. +/// This tends to be useful when props borrow something that simply cannot be compared (IE a reference to a closure); +/// +/// By default, the memoization strategy is very conservative, but can be tuned to be more aggressive manually. However, +/// this is only safe if the props are 'static - otherwise you might borrow references after-free. +/// +/// We strongly suggest that any changes to memoization be done at the "PartialEq" level for 'static props. Additionally, +/// we advise the use of smart pointers in cases where memoization is important. +/// +/// ## Example +/// +/// For props that are 'static: +/// ```rust, ignore +/// #[derive(Props, PartialEq)] +/// struct MyProps { +/// data: String +/// } +/// ``` +/// +/// For props that borrow: +/// +/// ```rust, ignore +/// #[derive(Props)] +/// struct MyProps<'a >{ +/// data: &'a str +/// } +/// ``` +pub trait Properties: Sized { + /// The type of the builder for this component. + /// Used to create "in-progress" versions of the props. + type Builder; + + /// An indication if these props are can be memoized automatically. + const IS_STATIC: bool; + + /// Create a builder for this component. + fn builder() -> Self::Builder; + + /// Memoization can only happen if the props are valid for the 'static lifetime + /// + /// # Safety + /// The user must know if their props are static, but if they make a mistake, UB happens + /// Therefore it's unsafe to memoize. + unsafe fn memoize(&self, other: &Self) -> bool; +} + +impl Properties for () { + type Builder = EmptyBuilder; + const IS_STATIC: bool = true; + fn builder() -> Self::Builder { + EmptyBuilder {} + } + unsafe fn memoize(&self, _other: &Self) -> bool { + true + } +} +// We allow components to use the () generic parameter if they have no props. This impl enables the "build" method +// that the macros use to anonymously complete prop construction. +pub struct EmptyBuilder; +impl EmptyBuilder { + pub fn build(self) {} +} + +/// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern +/// to initialize a component's props. +pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element) -> T::Builder { + T::builder() +} diff --git a/packages/dioxus/tests/rsx_syntax.rs b/packages/dioxus/tests/rsx_syntax.rs index 321d2aacf..dd5d81069 100644 --- a/packages/dioxus/tests/rsx_syntax.rs +++ b/packages/dioxus/tests/rsx_syntax.rs @@ -16,10 +16,7 @@ fn basic_syntax_is_a_template(cx: Scope) -> Element { p { "you're great!" } div { background_color: "red", h1 { "var" } - div { - b { "asd" } - "not great" - } + div { b { "asd" } "not great" } } p { "you're great!" } } @@ -30,16 +27,17 @@ fn basic_syntax_is_a_template(cx: Scope) -> Element { fn basic_template(cx: Scope) -> Element { cx.render(rsx! { div { - (0..2).map(|i| rsx! { - div { "asd" } - }), - (0..2).map(|i| rsx! { - div { "asd" } - }) + (0..2).map(|i| rsx! { div { "asd {i}" } }) + basic_child { } } }) } +/// A beautiful component +fn basic_child(cx: Scope) -> Element { + todo!() +} + #[test] fn basic_prints() { let mut dom = VirtualDom::new(basic_template); diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 1646c7053..51decfa91 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -38,7 +38,7 @@ pub mod on { factory: NodeFactory<'a>, mut callback: impl FnMut($wrapper) + 'a, // mut callback: impl FnMut(UiEvent<$data>) + 'a, - ) -> Listener<'a> + ) -> Attribute<'a> { // let bump = &factory.bump(); diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs index 141ec6ab0..ef7b0983b 100644 --- a/packages/rsx/src/component.rs +++ b/packages/rsx/src/component.rs @@ -117,8 +117,6 @@ impl ToTokens for Component { let name = &self.name; let prop_gen_args = &self.prop_gen_args; - let mut has_key = None; - let builder = match &self.manual_props { Some(manual_props) => { let mut toks = quote! { @@ -126,7 +124,7 @@ impl ToTokens for Component { }; for field in &self.fields { if field.name == "key" { - has_key = Some(field); + // skip keys } else { let name = &field.name; let val = &field.content; @@ -149,18 +147,22 @@ impl ToTokens for Component { }; for field in &self.fields { match field.name.to_string().as_str() { - "key" => { - // - has_key = Some(field); - } + "key" => {} _ => toks.append_all(quote! {#field}), } } if !self.children.is_empty() { - let childs = &self.children; + let renderer = TemplateRenderer { + roots: &self.children, + }; + toks.append_all(quote! { - .children(__cx.create_children([ #( #childs ),* ])) + .children( + Some({ + #renderer + }) + ) }); } @@ -171,25 +173,12 @@ impl ToTokens for Component { } }; - let key_token = match has_key { - Some(field) => { - let inners = &field.content; - if let ContentField::Formatted(ifmt) = inners { - quote! { Some(#ifmt) } - } else { - unreachable!() - } - } - None => quote! { None }, - }; - let fn_name = self.name.segments.last().unwrap().ident.to_string(); tokens.append_all(quote! { __cx.component( #name, #builder, - #key_token, #fn_name ) }) diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index cda8e363b..99c7f34b2 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -70,147 +70,14 @@ impl Parse for CallBody { /// Serialize the same way, regardless of flavor impl ToTokens for CallBody { fn to_tokens(&self, out_tokens: &mut TokenStream2) { - // As we print out the dynamic nodes, we want to keep track of them in a linear fashion - // We'll use the size of the vecs to determine the index of the dynamic node in the final - struct DynamicContext<'a> { - dynamic_nodes: Vec<&'a BodyNode>, - dynamic_attributes: HashMap, AttrLocation<'a>>, - current_path: Vec, - } - - #[derive(Default)] - struct AttrLocation<'a> { - attrs: Vec<&'a ElementAttrNamed>, - listeners: Vec<&'a ElementAttrNamed>, - } - - let mut context = DynamicContext { - dynamic_nodes: vec![], - dynamic_attributes: HashMap::new(), - current_path: vec![], - }; - - fn render_static_node<'a>(root: &'a BodyNode, cx: &mut DynamicContext<'a>) -> TokenStream2 { - match root { - BodyNode::Element(el) => { - let el_name = &el.name; - - let children = el.children.iter().enumerate().map(|(idx, root)| { - cx.current_path.push(idx as u8); - let out = render_static_node(root, cx); - cx.current_path.pop(); - out - }); - - let children = quote! { #(#children),* }; - - let attrs = el.attributes.iter().filter_map(|attr| { - // - match &attr.attr { - ElementAttr::AttrText { name, value } if value.is_static() => { - let value = value.source.as_ref().unwrap(); - Some(quote! { - ::dioxus::core::TemplateAttribute::Static { - name: dioxus_elements::#el_name::#name.0, - namespace: dioxus_elements::#el_name::#name.1, - volatile: dioxus_elements::#el_name::#name.2, - value: #value, - } - }) - } - - ElementAttr::CustomAttrText { name, value } if value.is_static() => { - let value = value.source.as_ref().unwrap(); - Some(quote! { - ::dioxus::core::TemplateAttribute::Static { - name: dioxus_elements::#el_name::#name.0, - namespace: dioxus_elements::#el_name::#name.1, - volatile: dioxus_elements::#el_name::#name.2, - value: #value, - } - }) - } - - ElementAttr::AttrExpression { .. } - | ElementAttr::AttrText { .. } - | ElementAttr::CustomAttrText { .. } - | ElementAttr::CustomAttrExpression { .. } => { - // todo!(); - let ct = cx.dynamic_attributes.len(); - // cx.dynamic_attributes.push(attr); - // quote! {} - // None - Some(quote! { ::dioxus::core::TemplateAttribute::Dynamic { - name: "asd", - index: #ct - } }) - } - - ElementAttr::EventTokens { .. } => { - // todo!(); - // let ct = cx.dynamic_listeners.len(); - // cx.dynamic_listeners.push(attr); - // quote! {} - None - } - } - }); - - quote! { - ::dioxus::core::TemplateNode::Element { - tag: dioxus_elements::#el_name::TAG_NAME, - namespace: dioxus_elements::#el_name::NAME_SPACE, - attrs: &[ #(#attrs),* ], - children: &[ #children ], - } - } - } - - BodyNode::Text(text) if text.is_static() => { - let text = text.source.as_ref().unwrap(); - quote! { ::dioxus::core::TemplateNode::Text(#text) } - } - - BodyNode::RawExpr(_) | BodyNode::Component(_) | BodyNode::Text(_) => { - let ct = cx.dynamic_nodes.len(); - cx.dynamic_nodes.push(root); - quote! { ::dioxus::core::TemplateNode::Dynamic(#ct) } - } - } - } - - let root_printer = self.roots.iter().enumerate().map(|(idx, root)| { - context.current_path.push(idx as u8); - let out = render_static_node(root, &mut context); - context.current_path.pop(); - out - }); - - // Render and release the mutable borrow on context - let roots = quote! { #( #root_printer ),* }; - let node_printer = &context.dynamic_nodes; - - let body = quote! { - static TEMPLATE: ::dioxus::core::Template = ::dioxus::core::Template { - id: ::dioxus::core::get_line_num!(), - roots: &[ #roots ] - }; - ::dioxus::core::VNode { - node_id: Default::default(), - parent: None, - template: TEMPLATE, - root_ids: __cx.bump().alloc([]), - dynamic_nodes: __cx.bump().alloc([ #( #node_printer ),* ]), - dynamic_attrs: __cx.bump().alloc([]), - } - }; + let body = TemplateRenderer { roots: &self.roots }; if self.inline_cx { out_tokens.append_all(quote! { - { + Some({ let __cx = cx; #body - } + }) }) } else { out_tokens.append_all(quote! { @@ -222,42 +89,147 @@ impl ToTokens for CallBody { } } -impl CallBody { - pub fn to_tokens_without_template(&self, out_tokens: &mut TokenStream2) { +pub struct TemplateRenderer<'a> { + pub roots: &'a [BodyNode], +} - // let children = &self.roots; - // let inner = if children.len() == 1 { - // let inner = &self.roots[0]; - // quote! { #inner } - // } else { - // quote! { __cx.fragment_root([ #(#children),* ]) } - // }; +impl<'a> ToTokens for TemplateRenderer<'a> { + fn to_tokens(&self, out_tokens: &mut TokenStream2) { + let mut context = DynamicContext { + dynamic_nodes: vec![], + dynamic_attributes: vec![], + current_path: vec![], + }; - // out_tokens.append_all(quote! { - // LazyNodes::new(move |__cx: NodeFactory| -> VNode { - // use dioxus_elements::{GlobalAttributes, SvgAttributes}; - // #inner - // }) - // }) - } + let root_printer = self.roots.iter().enumerate().map(|(idx, root)| { + context.current_path.push(idx as u8); + let out = context.render_static_node(root); + context.current_path.pop(); + out + }); + + // Render and release the mutable borrow on context + let roots = quote! { #( #root_printer ),* }; + let node_printer = &context.dynamic_nodes; + let dyn_attr_printer = &context.dynamic_attributes; - pub fn to_tokens_without_lazynodes(&self, out_tokens: &mut TokenStream2) { out_tokens.append_all(quote! { static TEMPLATE: ::dioxus::core::Template = ::dioxus::core::Template { id: ::dioxus::core::get_line_num!(), - roots: &[] + roots: &[ #roots ] }; + ::dioxus::core::VNode { + node_id: Default::default(), + parent: None, + template: TEMPLATE, + root_ids: __cx.bump().alloc([]), + dynamic_nodes: __cx.bump().alloc([ #( #node_printer ),* ]), + dynamic_attrs: __cx.bump().alloc([ #( #dyn_attr_printer ),* ]), + } + }); + } +} +// As we print out the dynamic nodes, we want to keep track of them in a linear fashion +// We'll use the size of the vecs to determine the index of the dynamic node in the final +pub struct DynamicContext<'a> { + dynamic_nodes: Vec<&'a BodyNode>, + dynamic_attributes: Vec<&'a ElementAttrNamed>, + current_path: Vec, +} - LazyNodes::new( move | __cx: ::dioxus::core::NodeFactory| -> ::dioxus::core::VNode { - ::dioxus::core::VNode { - node_id: Default::default(), - parent: None, - template: &TEMPLATE, - root_ids: __cx.bump().alloc([]), - dynamic_nodes: __cx.bump().alloc([]), - dynamic_attrs: __cx.bump().alloc([]), +impl<'a> DynamicContext<'a> { + fn render_static_node(&mut self, root: &'a BodyNode) -> TokenStream2 { + match root { + BodyNode::Element(el) => { + let el_name = &el.name; + + // dynamic attributes + // [0] + // [0, 1] + // [0, 1] + // [0, 1] + // [0, 1, 2] + // [0, 2] + // [0, 2, 1] + + let static_attrs = el.attributes.iter().filter_map(|attr| match &attr.attr { + ElementAttr::AttrText { name, value } if value.is_static() => { + let value = value.source.as_ref().unwrap(); + Some(quote! { + ::dioxus::core::TemplateAttribute::Static { + name: dioxus_elements::#el_name::#name.0, + namespace: dioxus_elements::#el_name::#name.1, + volatile: dioxus_elements::#el_name::#name.2, + value: #value, + } + }) + } + + ElementAttr::CustomAttrText { name, value } if value.is_static() => { + let value = value.source.as_ref().unwrap(); + Some(quote! { + ::dioxus::core::TemplateAttribute::Static { + name: dioxus_elements::#el_name::#name.0, + namespace: dioxus_elements::#el_name::#name.1, + volatile: dioxus_elements::#el_name::#name.2, + value: #value, + } + }) + } + + ElementAttr::AttrExpression { .. } + | ElementAttr::AttrText { .. } + | ElementAttr::CustomAttrText { .. } + | ElementAttr::CustomAttrExpression { .. } => { + let ct = self.dynamic_attributes.len(); + self.dynamic_attributes.push(attr); + Some(quote! { ::dioxus::core::TemplateAttribute::Dynamic { + name: "asd", + index: #ct + } }) + } + + ElementAttr::EventTokens { .. } => { + let ct = self.dynamic_attributes.len(); + self.dynamic_attributes.push(attr); + Some(quote! { ::dioxus::core::TemplateAttribute::Dynamic { + name: "asd", + index: #ct + } }) + } + }); + + let attrs = quote! { #(#static_attrs),*}; + + let children = el.children.iter().enumerate().map(|(idx, root)| { + self.current_path.push(idx as u8); + let out = self.render_static_node(root); + self.current_path.pop(); + out + }); + + let children = quote! { #(#children),* }; + + quote! { + ::dioxus::core::TemplateNode::Element { + tag: dioxus_elements::#el_name::TAG_NAME, + namespace: dioxus_elements::#el_name::NAME_SPACE, + attrs: &[ #attrs ], + children: &[ #children ], + } } - }) - }) + } + + BodyNode::Text(text) if text.is_static() => { + let text = text.source.as_ref().unwrap(); + quote! { ::dioxus::core::TemplateNode::Text(#text) } + } + + BodyNode::RawExpr(_) | BodyNode::Text(_) | BodyNode::Component(_) => { + let ct = self.dynamic_nodes.len(); + self.dynamic_nodes.push(root); + quote! { ::dioxus::core::TemplateNode::Dynamic(#ct) } + } + } } } diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index 406a24ddc..4013c9cba 100644 --- a/packages/rsx/src/node.rs +++ b/packages/rsx/src/node.rs @@ -51,17 +51,18 @@ impl Parse for BodyNode { // - one ident // - followed by `{` // - 1st char is lowercase + // - no underscores (reserved for components) // // example: // div {} if let Some(ident) = path.get_ident() { + let el_name = ident.to_string(); + + let first_char = el_name.chars().next().unwrap(); + if body_stream.peek(token::Brace) - && ident - .to_string() - .chars() - .next() - .unwrap() - .is_ascii_lowercase() + && first_char.is_ascii_lowercase() + && !el_name.contains('_') { return Ok(BodyNode::Element(stream.parse::()?)); } diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index bdb4f6767..6407d4de8 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -1,7 +1,7 @@ #![doc = include_str!("../README.md")] use std::fmt::{Display, Formatter, Write}; -mod template; +pub mod template; use dioxus_core::exports::bumpalo; diff --git a/packages/ssr/src/template.rs b/packages/ssr/src/template.rs index 3e229298c..363d77f56 100644 --- a/packages/ssr/src/template.rs +++ b/packages/ssr/src/template.rs @@ -1,6 +1,6 @@ -use dioxus_core::prelude::*; +use dioxus_core::{prelude::*, AttributeValue}; use std::cell::RefCell; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::fmt::Write; use std::rc::Rc; @@ -22,7 +22,7 @@ struct StringChain { #[derive(Debug, Clone)] enum Segment { Attr(usize), - Dyn(usize), + Node(usize), PreRendered(String), } @@ -89,8 +89,9 @@ impl StringCache { cur_path.pop(); } TemplateNode::Text(text) => write!(chain, "{}", text)?, - TemplateNode::Dynamic(idx) => chain.segments.push(Segment::Dyn(*idx)), - TemplateNode::DynamicText(idx) => chain.segments.push(Segment::Dyn(*idx)), + TemplateNode::Dynamic(idx) | TemplateNode::DynamicText(idx) => { + chain.segments.push(Segment::Node(*idx)) + } } Ok(()) @@ -98,7 +99,7 @@ impl StringCache { } impl SsrRender { - fn render_vdom(&mut self, dom: &VirtualDom) -> String { + pub fn render_vdom(&mut self, dom: &VirtualDom) -> String { let scope = dom.base_scope(); let root = scope.root_node(); @@ -119,14 +120,17 @@ impl SsrRender { for segment in entry.segments.iter() { match segment { Segment::Attr(idx) => { - todo!("properly implement attrs in the macro"); - // let loc = &template.dynamic_attrs[*idx]; - // for attr in loc.attrs.iter() { - // write!(buf, " {}=\"{}\"", attr.name, attr.value)?; - // } + let attr = &template.dynamic_attrs[*idx]; + match attr.value { + AttributeValue::Text(value) => write!(buf, " {}=\"{}\"", attr.name, value)?, + _ => {} + }; } - Segment::Dyn(idx) => match &template.dynamic_nodes[*idx].kind { - DynamicNodeKind::Text { value, .. } => write!(buf, "{}", value)?, + Segment::Node(idx) => match &template.dynamic_nodes[*idx].kind { + DynamicNodeKind::Text { value, .. } => { + // todo: escape the text + write!(buf, "{}", value)? + } DynamicNodeKind::Fragment { children } => { for child in *children { self.render_template(buf, child)?; @@ -152,20 +156,21 @@ fn to_string_works() { fn app(cx: Scope) -> Element { let dynamic = 123; + let dyn2 = ""; // todo: escape this - cx.render(rsx! { - div { class: "asdasdasd", class: "asdasdasd", + render! { + div { class: "asdasdasd", class: "asdasdasd", id: "id-{dynamic}", "Hello world 1 -->" "{dynamic}" "<-- Hello world 2" div { "nest 1" } div {} div { "nest 2" } - (0..5).map(|i| rsx! { - div { "finalize {i}" } - }) + "{dyn2}" + + (0..5).map(|i| rsx! { div { "finalize {i}" } }) } - }) + } } let mut dom = VirtualDom::new(app); @@ -173,10 +178,52 @@ fn to_string_works() { dom.rebuild(&mut mutations); let cache = StringCache::from_template(&dom.base_scope().root_node()).unwrap(); - dbg!(cache.segments); let mut renderer = SsrRender::default(); - dbg!(renderer.render_vdom(&dom)); } + +#[test] +fn children_processes_properly() { + use dioxus::prelude::*; + + fn app(cx: Scope) -> Element { + render! { + div { + "yo!" + "yo!" + "yo!" + "yo!" + "yo!" + Child {} + Child {} + Child {} + Child {} + ChildWithChildren { + "hii" + } + (0..10).map(|f| rsx! { + "div {f}" + }) + } + } + } + + /// Yo its the child component! + fn Child(cx: Scope) -> Element { + render! ( div { "child" } ) + } + + #[inline_props] + fn ChildWithChildren<'a>(cx: Scope<'a>, children: Element<'a>) -> Element { + render! { + div { + "div" + children + } + } + } + + let mut dom = VirtualDom::new(app); +} diff --git a/packages/ssr/tests/caches.rs b/packages/ssr/tests.old/caches.rs similarity index 100% rename from packages/ssr/tests/caches.rs rename to packages/ssr/tests.old/caches.rs diff --git a/packages/ssr/tests/renders.rs b/packages/ssr/tests.old/renders.rs similarity index 100% rename from packages/ssr/tests/renders.rs rename to packages/ssr/tests.old/renders.rs