diff --git a/docs/markdown/basics/template.md b/docs/markdown/basics/template.md index 79ebe97dd..c052f4c89 100644 --- a/docs/markdown/basics/template.md +++ b/docs/markdown/basics/template.md @@ -38,3 +38,18 @@ template! { } } ``` + +Templates can also be fragments. + +```rust +template! { + p { "First child" } + p { "Second child" } +} +``` + +Or be empty. + +```rust +template! {} +``` diff --git a/maple-core-macro/src/component.rs b/maple-core-macro/src/component.rs index 157df3c22..f460fcd96 100644 --- a/maple-core-macro/src/component.rs +++ b/maple-core-macro/src/component.rs @@ -31,7 +31,11 @@ impl ToTokens for Component { args, } = self; - let quoted = quote! { ::maple_core::reactive::untrack(|| ::maple_core::TemplateResult::inner_element(&#path(#args))) }; + let quoted = quote! { + ::maple_core::reactive::untrack(|| + #path(#args) + ) + }; tokens.extend(quoted); } diff --git a/maple-core-macro/src/element.rs b/maple-core-macro/src/element.rs index 9eec2f330..00909c9a0 100644 --- a/maple-core-macro/src/element.rs +++ b/maple-core-macro/src/element.rs @@ -100,7 +100,9 @@ impl ToTokens for Element { for child in &children.body { let quoted = match child { HtmlTree::Component(component) => quote_spanned! { component.span()=> - ::maple_core::generic_node::GenericNode::append_child(&element, &#component); + for node in &#component { + ::maple_core::generic_node::GenericNode::append_child(&element, node); + } }, HtmlTree::Element(element) => quote_spanned! { element.span()=> ::maple_core::generic_node::GenericNode::append_child(&element, &#element); diff --git a/maple-core-macro/src/lib.rs b/maple-core-macro/src/lib.rs index d5b5e4f44..a242ac841 100644 --- a/maple-core-macro/src/lib.rs +++ b/maple-core-macro/src/lib.rs @@ -65,11 +65,62 @@ impl Parse for HtmlTree { impl ToTokens for HtmlTree { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - match self { - Self::Component(component) => component.to_tokens(tokens), - Self::Element(element) => element.to_tokens(tokens), - Self::Text(text) => text.to_tokens(tokens), + let quoted = match self { + Self::Component(component) => quote! { + #component + }, + Self::Element(element) => quote! { + ::maple_core::template_result::TemplateResult::new_node(#element) + }, + Self::Text(text) => match text { + text::Text::Text(_) => quote! { + ::maple_core::template_result::TemplateResult::new_node( + ::maple_core::generic_node::GenericNode::text_node(#text), + ) + }, + text::Text::Splice(_, _) => unimplemented!("splice at top level is not supported"), + }, + }; + + tokens.extend(quoted); + } +} + +pub(crate) struct HtmlRoot { + children: Vec, +} + +impl Parse for HtmlRoot { + fn parse(input: ParseStream) -> Result { + let mut children = Vec::new(); + + while !input.is_empty() { + children.push(input.parse()?); } + + Ok(Self { children }) + } +} + +impl ToTokens for HtmlRoot { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let quoted = match self.children.as_slice() { + [] => quote! { + ::maple_core::template_result::TemplateResult::empty() + }, + [node] => node.to_token_stream(), + nodes => quote! { + ::maple_core::template_result::TemplateResult::new_fragment({ + let mut children = ::std::vec::Vec::new(); + #( for node in #nodes { + children.push(node); + } )* + children + }) + }, + }; + + tokens.extend(quoted); } } @@ -78,11 +129,7 @@ impl ToTokens for HtmlTree { /// TODO: write some more docs #[proc_macro] pub fn template(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as HtmlTree); - - let quoted = quote! { - ::maple_core::TemplateResult::new(#input) - }; + let input = parse_macro_input!(input as HtmlRoot); - TokenStream::from(quoted) + TokenStream::from(input.to_token_stream()) } diff --git a/maple-core-macro/tests/ui/element-fail.stderr b/maple-core-macro/tests/ui/element-fail.stderr index a43db5315..d74329d68 100644 --- a/maple-core-macro/tests/ui/element-fail.stderr +++ b/maple-core-macro/tests/ui/element-fail.stderr @@ -1,4 +1,4 @@ -error: unexpected token +error: expected a valid HTML node --> $DIR/element-fail.rs:4:45 | 4 | let _: TemplateResult = template! { p.my-class#id }; diff --git a/maple-core-macro/tests/ui/root-pass.rs b/maple-core-macro/tests/ui/root-pass.rs new file mode 100644 index 000000000..d138fefad --- /dev/null +++ b/maple-core-macro/tests/ui/root-pass.rs @@ -0,0 +1,16 @@ +use maple_core::prelude::*; + +fn compile_pass() { + let _: TemplateResult = template! { "Raw text nodes!" }; + + let _: TemplateResult = template! { + p { "First" } + p { "Second" } + "Third" + }; + + // let spliced = 123; + // let _: TemplateResult = template! { (spliced) }; +} + +fn main() {} diff --git a/maple-core/src/flow.rs b/maple-core/src/flow.rs index b3ce9b45e..54369a05f 100644 --- a/maple-core/src/flow.rs +++ b/maple-core/src/flow.rs @@ -26,7 +26,8 @@ where } /// Keyed iteration. Use this instead of directly rendering an array of [`TemplateResult`]s. -/// Using this will minimize re-renders instead of re-rendering every single node on every state change. +/// Using this will minimize re-renders instead of re-rendering every single node on every state +/// change. /// /// For non keyed iteration, see [`Indexed`]. /// @@ -66,7 +67,8 @@ where type TemplateValue = (Owner, T, TemplateResult, usize /* index */); - // A tuple with a value of type `T` and the `TemplateResult` produces by calling `props.template` with the first value. + // A tuple with a value of type `T` and the `TemplateResult` produces by calling + // `props.template` with the first value. let templates: Rc>>> = Default::default(); let fragment = G::fragment(); @@ -84,7 +86,9 @@ where if iterable.get().is_empty() { for (_, (owner, _value, template, _i)) in templates.borrow_mut().drain() { drop(owner); // destroy owner - template.node.remove_self(); + for node in &template { + node.remove_self() + } } return; } @@ -101,9 +105,9 @@ where .map(|x| (x.0.clone(), (x.1 .2.clone(), x.1 .3))) .collect::>(); - for node in &excess_nodes { - let removed_index = node.1 .1; - templates.remove(&node.0); + for template in &excess_nodes { + let removed_index = template.1 .1; + templates.remove(&template.0); // Offset indexes of other templates by 1. for (_, _, _, i) in templates.values_mut() { @@ -113,8 +117,10 @@ where } } - for node in excess_nodes { - node.1 .0.node.remove_self(); + for template in excess_nodes { + for node in template.1 .0 { + node.remove_self(); + } } } @@ -158,16 +164,19 @@ where if let Some(next_item) = iterable.get().get(i + 1) { let templates = templates.borrow(); - if let Some(next_node) = templates.get(&key_fn(next_item)) { - next_node - .2 - .node - .insert_sibling_before(&new_template.unwrap().node); + if let Some(next_template) = templates.get(&key_fn(next_item)) { + for node in &new_template.unwrap() { + next_template.2.first_node().insert_sibling_before(node); + } } else { - marker.insert_sibling_before(&new_template.unwrap().node); + for node in &new_template.unwrap() { + marker.insert_sibling_before(node); + } } } else { - marker.insert_sibling_before(&new_template.unwrap().node); + for node in &new_template.unwrap() { + marker.insert_sibling_before(node); + } } } else if match previous_value { Some(prev) => prev.index, @@ -177,14 +186,21 @@ where // Location changed, move from old location to new location // Node was moved in the DOM. Move node to new index. - let node = templates.borrow().get(&key).unwrap().2.node.clone(); - - if let Some(next_item) = iterable.get().get(i + 1) { + { let templates = templates.borrow(); - let next_node = templates.get(&key_fn(next_item)).unwrap(); - next_node.2.node.insert_sibling_before(&node); // Move to before next node - } else { - marker.insert_sibling_before(&node); // Move to end. + let template = &templates.get(&key).unwrap().2; + + if let Some(next_item) = iterable.get().get(i + 1) { + let next_node = templates.get(&key_fn(next_item)).unwrap(); + for node in template { + // Move to before next node. + next_node.2.first_node().insert_sibling_before(node); + } + } else { + for node in template { + marker.insert_sibling_before(node); // Move to end. + } + } } templates.borrow_mut().get_mut(&key).unwrap().3 = i; @@ -206,19 +222,25 @@ where let mut new_template = None; let owner = create_root(|| new_template = Some(template(item.clone()))); - let (_, _, old_node, _) = mem::replace( + let (_, _, old_template, _) = mem::replace( templates.get_mut(&key).unwrap(), (owner, item.clone(), new_template.clone().unwrap(), i), ); - let parent = old_node.node.parent_node().unwrap(); - parent.replace_child(&new_template.unwrap().node, &old_node.node); + let parent = old_template.first_node().parent_node().unwrap(); + + for new_node in &new_template.unwrap() { + parent.insert_child_before(new_node, Some(old_template.first_node())); + } + for old_node in &old_template { + parent.remove_child(old_node); + } } } } }); - TemplateResult::new(fragment) + TemplateResult::new_node(fragment) } /// Props for [`Indexed`]. @@ -230,8 +252,9 @@ where pub template: F, } -/// Non keyed iteration (or keyed by index). Use this instead of directly rendering an array of [`TemplateResult`]s. -/// Using this will minimize re-renders instead of re-rendering every single node on every state change. +/// Non keyed iteration (or keyed by index). Use this instead of directly rendering an array of +/// [`TemplateResult`]s. Using this will minimize re-renders instead of re-rendering every single +/// node on every state change. /// /// For keyed iteration, see [`Keyed`]. /// @@ -275,7 +298,9 @@ where if props.iterable.get().is_empty() { for (owner, template) in templates.borrow_mut().drain(..) { drop(owner); // destroy owner - template.node.remove_self(); + for node in template { + node.remove_self(); + } } return; } @@ -299,13 +324,18 @@ where let owner = create_root(|| new_template = Some((props.template)(item.clone()))); if templates.borrow().get(i).is_some() { - let old_node = mem::replace( + let old_template = mem::replace( &mut templates.borrow_mut()[i], (owner, new_template.as_ref().unwrap().clone()), ); - let parent = old_node.1.node.parent_node().unwrap(); - parent.replace_child(&new_template.unwrap().node, &old_node.1.node); + let parent = old_template.1.first_node().parent_node().unwrap(); + for node in &new_template.unwrap() { + parent.insert_child_before(node, Some(old_template.1.first_node())); + } + for old_node in &old_template.1 { + parent.remove_child(old_node); + } } else { debug_assert!(templates.borrow().len() == i, "pushing new value scenario"); @@ -313,7 +343,9 @@ where .borrow_mut() .push((owner, new_template.as_ref().unwrap().clone())); - marker.insert_sibling_before(&new_template.unwrap().node); + for node in &new_template.unwrap() { + marker.insert_sibling_before(node); + } } } } @@ -322,8 +354,10 @@ where let mut templates = templates.borrow_mut(); let excess_nodes = templates.drain(props.iterable.get().len()..); - for node in excess_nodes { - node.1.node.remove_self(); + for template in excess_nodes { + for node in &template.1 { + node.remove_self(); + } } } @@ -331,5 +365,5 @@ where } }); - TemplateResult::new(fragment) + TemplateResult::new_node(fragment) } diff --git a/maple-core/src/generic_node.rs b/maple-core/src/generic_node.rs index fcaa34d02..77b6a881e 100644 --- a/maple-core/src/generic_node.rs +++ b/maple-core/src/generic_node.rs @@ -22,18 +22,21 @@ pub type EventListener = dyn Fn(Event); /// Abstraction over a rendering backend. /// -/// You would probably use this trait as a trait bound when you want to accept any rendering backend. -/// For example, components are often generic over [`GenericNode`] to be able to render to different backends. +/// You would probably use this trait as a trait bound when you want to accept any rendering +/// backend. For example, components are often generic over [`GenericNode`] to be able to render to +/// different backends. /// -/// Note that components are **NOT** represented by [`GenericNode`]. Instead, components are _disappearing_, meaning -/// that they are simply functions that generate [`GenericNode`]s inside a new reactive context. This means that there -/// is no overhead whatsoever when using components. +/// Note that components are **NOT** represented by [`GenericNode`]. Instead, components are +/// _disappearing_, meaning that they are simply functions that generate [`GenericNode`]s inside a +/// new reactive context. This means that there is no overhead whatsoever when using components. /// /// Maple ships with 2 rendering backends out of the box: /// * [`DomNode`] - Rendering in the browser (to real DOM nodes). -/// * [`SsrNode`] - Render to a static string (often on the server side for Server Side Rendering, aka. SSR). +/// * [`SsrNode`] - Render to a static string (often on the server side for Server Side Rendering, +/// aka. SSR). /// -/// To implement your own rendering backend, you will need to create a new struct which implements [`GenericNode`]. +/// To implement your own rendering backend, you will need to create a new struct which implements +/// [`GenericNode`]. pub trait GenericNode: fmt::Debug + Clone + PartialEq + Eq + 'static { /// Create a new element node. fn element(tag: &str) -> Self; @@ -41,12 +44,14 @@ pub trait GenericNode: fmt::Debug + Clone + PartialEq + Eq + 'static { /// Create a new text node. fn text_node(text: &str) -> Self; - /// Create a new fragment (list of nodes). A fragment is not necessarily wrapped around by an element. + /// Create a new fragment (list of nodes). A fragment is not necessarily wrapped around by an + /// element. fn fragment() -> Self; - /// Create a marker (dummy) node. For [`DomNode`], this is implemented by creating an empty comment node. - /// This is used, for example, in [`Keyed`] and [`Indexed`] for scenarios where you want to push a new item to the - /// end of the list. If the list is empty, a dummy node is needed to store the position of the component. + /// Create a marker (dummy) node. For [`DomNode`], this is implemented by creating an empty + /// comment node. This is used, for example, in [`Keyed`] and [`Indexed`] for scenarios + /// where you want to push a new item to the end of the list. If the list is empty, a dummy + /// node is needed to store the position of the component. fn marker() -> Self; /// Sets an attribute on a node. @@ -55,8 +60,9 @@ pub trait GenericNode: fmt::Debug + Clone + PartialEq + Eq + 'static { /// Appends a child to the node's children. fn append_child(&self, child: &Self); - /// Insert a new child node to this node's children. If `reference_node` is `Some`, the child will be inserted - /// before the reference node. Else if `None`, the child will be inserted at the end. + /// Insert a new child node to this node's children. If `reference_node` is `Some`, the child + /// will be inserted before the reference node. Else if `None`, the child will be inserted + /// at the end. fn insert_child_before(&self, new_node: &Self, reference_node: Option<&Self>); /// Remove a child node from this node's children. @@ -82,14 +88,16 @@ pub trait GenericNode: fmt::Debug + Clone + PartialEq + Eq + 'static { /// Add a [`EventListener`] to the event `name`. fn event(&self, name: &str, handler: Box); - /// Update inner text of the node. If the node has elements, all the elements are replaced with a new text node. + /// Update inner text of the node. If the node has elements, all the elements are replaced with + /// a new text node. fn update_inner_text(&self, text: &str); - /// Append an item that implements [`Render`] and automatically updates the DOM inside an effect. + /// Append an item that implements [`Render`] and automatically updates the DOM inside an + /// effect. fn append_render(&self, child: Box Box>>) { let parent = self.clone(); - let node = create_effect_initial(cloned!((parent) => move || { + let nodes = create_effect_initial(cloned!((parent) => move || { let node = RefCell::new(child().render()); let effect = cloned!((node) => move || { @@ -100,6 +108,8 @@ pub trait GenericNode: fmt::Debug + Clone + PartialEq + Eq + 'static { (Rc::new(effect), node) })); - parent.append_child(&node.borrow()); + for node in nodes.borrow().iter() { + parent.append_child(node); + } } } diff --git a/maple-core/src/lib.rs b/maple-core/src/lib.rs index 9deaa0502..7d2f86d9f 100644 --- a/maple-core/src/lib.rs +++ b/maple-core/src/lib.rs @@ -5,9 +5,12 @@ //! This is the API docs for maple. If you are looking for the usage docs, checkout the [README](https://github.com/lukechu10/maple). //! //! ## Features -//! - `dom` (_default_) - Enables rendering templates to DOM nodes. Only useful on `wasm32-unknown-unknown` target. -//! - `ssr` - Enables rendering templates to static strings (useful for Server Side Rendering / Pre-rendering). -//! - `serde` - Enables serializing and deserializing `Signal`s and other wrapper types using `serde`. +//! - `dom` (_default_) - Enables rendering templates to DOM nodes. Only useful on +//! `wasm32-unknown-unknown` target. +//! - `ssr` - Enables rendering templates to static strings (useful for Server Side Rendering / +//! Pre-rendering). +//! - `serde` - Enables serializing and deserializing `Signal`s and other wrapper types using +//! `serde`. #![allow(non_snake_case)] #![warn(clippy::clone_on_ref_ptr)] @@ -15,9 +18,7 @@ #![deny(clippy::trait_duplication_in_bounds)] #![deny(clippy::type_repetition_in_bounds)] -use generic_node::GenericNode; pub use maple_core_macro::template; -use prelude::SignalVec; pub mod easing; pub mod flow; @@ -26,47 +27,17 @@ pub mod macros; pub mod noderef; pub mod reactive; pub mod render; +pub mod template_result; pub mod utils; -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TemplateResult { - node: G, -} - -impl TemplateResult { - /// Create a new [`TemplateResult`] from a [`GenericNode`]. - pub fn new(node: G) -> Self { - Self { node } - } - - /// Create a new [`TemplateResult`] with a blank comment node - pub fn empty() -> Self { - Self::new(G::marker()) - } - - pub fn inner_element(&self) -> G { - self.node.clone() - } -} - -/// A [`SignalVec`](reactive::SignalVec) of [`TemplateResult`]s. -#[derive(Clone)] -pub struct TemplateList { - templates: reactive::SignalVec>, -} - -impl From>> for TemplateList { - fn from(templates: SignalVec>) -> Self { - Self { templates } - } -} - /// Render a [`TemplateResult`] into the DOM. /// Alias for [`render_to`] with `parent` being the `` tag. /// /// _This API requires the following crate features to be activated: `dom`_ #[cfg(feature = "dom")] -pub fn render(template_result: impl FnOnce() -> TemplateResult) { +pub fn render( + template_result: impl FnOnce() -> template_result::TemplateResult, +) { let window = web_sys::window().unwrap(); let document = window.document().unwrap(); @@ -79,13 +50,13 @@ pub fn render(template_result: impl FnOnce() -> TemplateResult TemplateResult, + template_result: impl FnOnce() -> template_result::TemplateResult, parent: &web_sys::Node, ) { let owner = reactive::create_root(|| { - parent - .append_child(&template_result().node.inner_element()) - .unwrap(); + for node in template_result() { + parent.append_child(&node.inner_element()).unwrap(); + } }); thread_local! { @@ -95,18 +66,22 @@ pub fn render_to( GLOBAL_OWNERS.with(|global_owners| global_owners.borrow_mut().push(owner)); } -/// Render a [`TemplateResult`] into a static [`String`]. Useful for rendering to a string on the server side. +/// Render a [`TemplateResult`] into a static [`String`]. Useful for rendering to a string on the +/// server side. /// /// _This API requires the following crate features to be activated: `ssr`_ #[cfg(feature = "ssr")] pub fn render_to_string( - template_result: impl FnOnce() -> TemplateResult, + template_result: impl FnOnce() -> template_result::TemplateResult, ) -> String { - let mut ret = None; - let _owner = - reactive::create_root(|| ret = Some(format!("{}", template_result().inner_element()))); + let mut ret = String::new(); + let _owner = reactive::create_root(|| { + for node in template_result() { + ret.push_str(&format!("{}", node)); + } + }); - ret.unwrap() + ret } /// The maple prelude. @@ -123,12 +98,12 @@ pub mod prelude { pub use crate::noderef::NodeRef; pub use crate::reactive::{ create_effect, create_effect_initial, create_memo, create_root, create_selector, - create_selector_with, on_cleanup, untrack, Signal, SignalVec, StateHandle, + create_selector_with, on_cleanup, untrack, Signal, StateHandle, }; pub use crate::render::Render; #[cfg(feature = "ssr")] pub use crate::render_to_string; + pub use crate::template_result::TemplateResult; #[cfg(feature = "dom")] pub use crate::{render, render_to}; - pub use crate::{TemplateList, TemplateResult}; } diff --git a/maple-core/src/reactive.rs b/maple-core/src/reactive.rs index 50a932876..098e7e061 100644 --- a/maple-core/src/reactive.rs +++ b/maple-core/src/reactive.rs @@ -1,16 +1,15 @@ //! Reactive primitives. mod effect; -mod signal; -mod signal_vec; mod motion; +mod signal; pub use effect::*; -pub use signal::*; -pub use signal_vec::*; pub use motion::*; +pub use signal::*; -/// Creates a new reactive root. Generally, you won't need this method as it is called automatically in [`render`](crate::render()). +/// Creates a new reactive root. Generally, you won't need this method as it is called automatically +/// in [`render`](crate::render()). /// /// # Example /// ``` diff --git a/maple-core/src/reactive/effect.rs b/maple-core/src/reactive/effect.rs index 6f4d9d8c4..2727f9ba3 100644 --- a/maple-core/src/reactive/effect.rs +++ b/maple-core/src/reactive/effect.rs @@ -169,7 +169,8 @@ pub fn create_effect_initial( CONTEXTS.with(|contexts| { let initial_context_size = contexts.borrow().len(); - // Upgrade running now to make sure running is valid for the whole duration of the effect. + // Upgrade running now to make sure running is valid for the whole duration of + // the effect. let running = running.upgrade().unwrap(); // Recreate effect dependencies each time effect is called. @@ -322,8 +323,8 @@ where } /// Creates a memoized value from some signals. Also know as "derived stores". -/// Unlike [`create_memo`], this function will not notify dependents of a change if the output is the same. -/// That is why the output of the function must implement [`PartialEq`]. +/// Unlike [`create_memo`], this function will not notify dependents of a change if the output is +/// the same. That is why the output of the function must implement [`PartialEq`]. /// /// To specify a custom comparison function, use [`create_selector_with`]. pub fn create_selector(derived: F) -> StateHandle @@ -335,7 +336,8 @@ where } /// Creates a memoized value from some signals. Also know as "derived stores". -/// Unlike [`create_memo`], this function will not notify dependents of a change if the output is the same. +/// Unlike [`create_memo`], this function will not notify dependents of a change if the output is +/// the same. /// /// It takes a comparison function to compare the old and new value, which returns `true` if they /// are the same and `false` otherwise. @@ -655,7 +657,8 @@ mod tests { assert_eq!(*double.get(), 2); state.set(2); - assert_eq!(*double.get(), 2); // double value should still be true because state.get() was inside untracked + assert_eq!(*double.get(), 2); // double value should still be true because state.get() was + // inside untracked } #[test] diff --git a/maple-core/src/reactive/motion.rs b/maple-core/src/reactive/motion.rs index b4072669d..a305a4819 100644 --- a/maple-core/src/reactive/motion.rs +++ b/maple-core/src/reactive/motion.rs @@ -94,7 +94,8 @@ impl Tweened { /// If the value is being interpolated already due to a previous call to `set()`, the previous /// task will be canceled. /// - /// To immediately set the value without interpolating the value, use `signal().set(...)` instead. + /// To immediately set the value without interpolating the value, use `signal().set(...)` + /// instead. pub fn set(&self, new_value: T) { let start = self.signal().get_untracked().as_ref().clone(); let easing_fn = Rc::clone(&self.0.borrow().easing_fn); diff --git a/maple-core/src/reactive/signal.rs b/maple-core/src/reactive/signal.rs index 66b71f89b..b92a708da 100644 --- a/maple-core/src/reactive/signal.rs +++ b/maple-core/src/reactive/signal.rs @@ -158,8 +158,9 @@ impl Signal { } /// Calls all the subscribers without modifying the state. - /// This can be useful when using patterns such as inner mutability where the state updated will not be automatically triggered. - /// In the general case, however, it is preferable to use [`Signal::set`] instead. + /// This can be useful when using patterns such as inner mutability where the state updated will + /// not be automatically triggered. In the general case, however, it is preferable to use + /// [`Signal::set`] instead. pub fn trigger_subscribers(&self) { // Clone subscribers to prevent modifying list when calling callbacks. let subscribers = self.handle.0.borrow().subscribers.clone(); @@ -241,7 +242,8 @@ impl SignalInner { self.subscribers.insert(handler); } - /// Removes a handler from the subscriber list. If the handler is not a subscriber, does nothing. + /// Removes a handler from the subscriber list. If the handler is not a subscriber, does + /// nothing. fn unsubscribe(&mut self, handler: &Callback) { self.subscribers.remove(handler); } diff --git a/maple-core/src/reactive/signal_vec.rs b/maple-core/src/reactive/signal_vec.rs deleted file mode 100644 index 65bbc164d..000000000 --- a/maple-core/src/reactive/signal_vec.rs +++ /dev/null @@ -1,318 +0,0 @@ -use std::cell::RefCell; -use std::rc::Rc; - -use crate::{TemplateList, TemplateResult}; - -use super::*; -use crate::generic_node::GenericNode; - -/// A reactive [`Vec`]. -/// This is more effective than using a [`Signal`](Signal) because it allows fine grained -/// reactivity within the `Vec`. -pub struct SignalVec { - signal: Signal>>, - /// A list of past changes that is accessed by subscribers. - /// Cleared when all subscribers are called. - changes: Rc>>>, -} - -impl SignalVec { - /// Create a new empty `SignalVec`. - pub fn new() -> Self { - Self { - signal: Signal::new(RefCell::new(Vec::new())), - changes: Rc::new(RefCell::new(Vec::new())), - } - } - - /// Create a new `SignalVec` with existing values from a [`Vec`]. - pub fn with_values(values: Vec) -> Self { - Self { - signal: Signal::new(RefCell::new(values)), - changes: Rc::new(RefCell::new(Vec::new())), - } - } - - /// Get the current pending changes that will be applied to the `SignalVec`. - pub fn changes(&self) -> &Rc>>> { - &self.changes - } - - /// Returns the inner backing [`Signal`] used to store the data. This method should used with - /// care as unintentionally modifying the [`Vec`] will not trigger any updates and cause - /// potential future problems. - pub fn inner_signal(&self) -> &Signal>> { - &self.signal - } - - pub fn replace(&self, values: Vec) { - self.add_change(VecDiff::Replace { values }); - - self.trigger_and_apply_changes(); - } - - pub fn insert(&self, index: usize, value: T) { - self.add_change(VecDiff::Insert { index, value }); - - self.trigger_and_apply_changes(); - } - - pub fn update(&self, index: usize, value: T) { - self.add_change(VecDiff::Update { index, value }) - } - - pub fn remove(&self, index: usize) { - self.add_change(VecDiff::Remove { index }); - - self.trigger_and_apply_changes(); - } - - pub fn swap(&self, index1: usize, index2: usize) { - self.add_change(VecDiff::Swap { index1, index2 }); - - self.trigger_and_apply_changes(); - } - - pub fn push(&self, value: T) { - self.add_change(VecDiff::Push { value }); - - self.trigger_and_apply_changes(); - } - - pub fn pop(&self) { - self.add_change(VecDiff::Pop); - - self.trigger_and_apply_changes(); - } - - pub fn clear(&self) { - self.add_change(VecDiff::Clear); - - self.trigger_and_apply_changes(); - } - - fn add_change(&self, change: VecDiff) { - self.changes.borrow_mut().push(change); - } - - fn trigger_and_apply_changes(&self) { - self.signal.trigger_subscribers(); - - for change in self.changes.take() { - change.apply_to_vec(&mut self.signal.get().borrow_mut()); - } - } - - /// Creates a derived `SignalVec`. - /// - /// # Example - /// ``` - /// use maple_core::prelude::*; - /// - /// let my_vec = SignalVec::with_values(vec![1, 2, 3]); - /// let squared = my_vec.map(|x| *x * *x); - /// - /// assert_eq!(*squared.inner_signal().get().borrow(), vec![1, 4, 9]); - /// - /// my_vec.push(4); - /// assert_eq!(*squared.inner_signal().get().borrow(), vec![1, 4, 9, 16]); - /// - /// my_vec.swap(0, 1); - /// assert_eq!(*squared.inner_signal().get().borrow(), vec![4, 1, 9, 16]); - /// ``` - pub fn map(&self, f: impl Fn(&T) -> U + 'static) -> SignalVec { - let signal = self.inner_signal().clone(); - let changes = Rc::clone(&self.changes()); - let f = Rc::new(f); - - create_effect_initial(move || { - let derived = SignalVec::with_values( - signal.get().borrow().iter().map(|value| f(value)).collect(), - ); - - let effect = { - let derived = derived.clone(); - let signal = signal.clone(); - move || { - signal.get(); // subscribe to signal - for change in changes.borrow().iter() { - match change { - VecDiff::Replace { values } => { - derived.replace(values.iter().map(|value| f(value)).collect()) - } - VecDiff::Insert { index, value } => derived.insert(*index, f(value)), - VecDiff::Update { index, value } => derived.update(*index, f(value)), - VecDiff::Remove { index } => derived.remove(*index), - VecDiff::Swap { index1, index2 } => derived.swap(*index1, *index2), - VecDiff::Push { value } => derived.push(f(value)), - VecDiff::Pop => derived.pop(), - VecDiff::Clear => derived.clear(), - } - } - } - }; - - (Rc::new(effect), derived) - }) - } -} - -impl SignalVec> { - /// Create a [`TemplateList`] from the `SignalVec`. - pub fn template_list(&self) -> TemplateList { - TemplateList::from(self.clone()) - } -} - -impl SignalVec { - /// Create a [`Vec`] from a [`SignalVec`]. The returned [`Vec`] is cloned from the data which - /// requires `T` to be `Clone`. - /// - /// # Example - /// ``` - /// use maple_core::prelude::*; - /// - /// let signal = SignalVec::with_values(vec![1, 2, 3]); - /// assert_eq!(signal.to_vec(), vec![1, 2, 3]); - /// ``` - pub fn to_vec(&self) -> Vec { - self.signal.get().borrow().clone() - } -} - -impl Default for SignalVec { - fn default() -> Self { - Self::new() - } -} - -impl Clone for SignalVec { - fn clone(&self) -> Self { - Self { - signal: self.signal.clone(), - changes: Rc::clone(&self.changes), - } - } -} - -/// An enum describing the changes applied on a [`SignalVec`]. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum VecDiff { - Replace { values: Vec }, - Insert { index: usize, value: T }, - Update { index: usize, value: T }, - Remove { index: usize }, - Swap { index1: usize, index2: usize }, - Push { value: T }, - Pop, - Clear, -} - -impl VecDiff { - pub fn apply_to_vec(self, v: &mut Vec) { - match self { - VecDiff::Replace { values } => *v = values, - VecDiff::Insert { index, value } => v.insert(index, value), - VecDiff::Update { index, value } => v[index] = value, - VecDiff::Remove { index } => { - v.remove(index); - } - VecDiff::Swap { index1, index2 } => v.swap(index1, index2), - VecDiff::Push { value } => v.push(value), - VecDiff::Pop => { - v.pop(); - } - VecDiff::Clear => v.clear(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn signal_vec() { - let my_vec = SignalVec::new(); - assert_eq!(*my_vec.inner_signal().get().borrow(), Vec::::new()); - - my_vec.push(3); - assert_eq!(*my_vec.inner_signal().get().borrow(), vec![3]); - - my_vec.push(4); - assert_eq!(*my_vec.inner_signal().get().borrow(), vec![3, 4]); - - my_vec.pop(); - assert_eq!(*my_vec.inner_signal().get().borrow(), vec![3]); - } - - #[test] - fn map() { - let my_vec = SignalVec::with_values(vec![1, 2, 3]); - let squared = my_vec.map(|x| *x * *x); - - assert_eq!(*squared.inner_signal().get().borrow(), vec![1, 4, 9]); - - my_vec.push(4); - assert_eq!(*squared.inner_signal().get().borrow(), vec![1, 4, 9, 16]); - - my_vec.pop(); - assert_eq!(*squared.inner_signal().get().borrow(), vec![1, 4, 9]); - } - - #[test] - fn map_chain() { - let my_vec = SignalVec::with_values(vec![1, 2, 3]); - let squared = my_vec.map(|x| *x * 2); - let quadrupled = squared.map(|x| *x * 2); - - assert_eq!(*quadrupled.inner_signal().get().borrow(), vec![4, 8, 12]); - - my_vec.push(4); - assert_eq!( - *quadrupled.inner_signal().get().borrow(), - vec![4, 8, 12, 16] - ); - - my_vec.pop(); - assert_eq!(*quadrupled.inner_signal().get().borrow(), vec![4, 8, 12]); - } - - #[test] - fn map_chain_temporary() { - let my_vec = SignalVec::with_values(vec![1, 2, 3]); - let quadrupled = my_vec.map(|x| *x * 2).map(|x| *x * 2); - - assert_eq!(*quadrupled.inner_signal().get().borrow(), vec![4, 8, 12]); - - my_vec.push(4); - assert_eq!( - *quadrupled.inner_signal().get().borrow(), - vec![4, 8, 12, 16] - ); - - my_vec.pop(); - assert_eq!(*quadrupled.inner_signal().get().borrow(), vec![4, 8, 12]); - } - - #[test] - fn map_inner_scope() { - let my_vec = SignalVec::with_values(vec![1, 2, 3]); - let quadrupled; - - let doubled = my_vec.map(|x| *x * 2); - assert_eq!(*doubled.inner_signal().get().borrow(), vec![2, 4, 6]); - - quadrupled = doubled.map(|x| *x * 2); - assert_eq!(*quadrupled.inner_signal().get().borrow(), vec![4, 8, 12]); - - drop(doubled); - assert_eq!(*quadrupled.inner_signal().get().borrow(), vec![4, 8, 12]); - - my_vec.push(4); - assert_eq!( - *quadrupled.inner_signal().get().borrow(), - vec![4, 8, 12, 16] - ); - } -} diff --git a/maple-core/src/render.rs b/maple-core/src/render.rs index 1ec92e5af..3d1f5b881 100644 --- a/maple-core/src/render.rs +++ b/maple-core/src/render.rs @@ -1,135 +1,53 @@ //! Trait for describing how something should be rendered into DOM nodes. use std::fmt; -use std::rc::Rc; use crate::generic_node::GenericNode; -use crate::reactive::VecDiff; -use crate::{TemplateList, TemplateResult}; +use crate::template_result::TemplateResult; /// Trait for describing how something should be rendered into DOM nodes. pub trait Render { - /// Called during the initial render when creating the DOM nodes. Should return a [`GenericNode`]. - fn render(&self) -> G; + /// Called during the initial render when creating the DOM nodes. Should return a + /// `Vec` of [`GenericNode`]s. + fn render(&self) -> Vec; /// Called when the node should be updated with new state. - /// The default implementation of this will replace the child node completely with the result of calling `render` again. - /// Another implementation might be better suited to some specific types. - /// For example, text nodes can simply replace the inner text instead of recreating a new node. + /// The default implementation of this will replace the child node completely with the result of + /// calling `render` again. Another implementation might be better suited to some specific + /// types. For example, text nodes can simply replace the inner text instead of recreating a + /// new node. /// - /// Returns the new node. If the node is reused instead of replaced, the returned node is simply the node passed in. - fn update_node(&self, parent: &G, node: &G) -> G { - let new_node = self.render(); - parent.replace_child(&new_node, &node); - new_node - } -} - -impl Render for T { - fn render(&self) -> G { - G::text_node(&format!("{}", self)) - } - - fn update_node(&self, _parent: &G, node: &G) -> G { - // replace `textContent` of `node` instead of recreating + /// Returns the new node. If the node is reused instead of replaced, the returned node is simply + /// the node passed in. + fn update_node<'a>(&self, parent: &G, node: &'a [G]) -> Vec { + let new_nodes = self.render(); - node.update_inner_text(&format!("{}", self)); + for new_node in &new_nodes { + parent.replace_child(new_node, node.first().unwrap()); + } - node.clone() + new_nodes } } -impl Render for TemplateList { - fn render(&self) -> G { - let fragment = G::fragment(); - - for item in self - .templates - .inner_signal() - .get() - .borrow() - .clone() - .into_iter() - { - fragment.append_render(Box::new(move || { - let item = item.clone(); - Box::new(item) - })); - } - - fragment +impl Render for T { + fn render(&self) -> Vec { + vec![G::text_node(&format!("{}", self))] } - fn update_node(&self, parent: &G, node: &G) -> G { - let templates = self.templates.inner_signal().get(); // subscribe to templates - let changes = Rc::clone(&self.templates.changes()); - - for change in changes.borrow().iter() { - match change { - VecDiff::Replace { values } => { - let first = templates.borrow().first().map(|x| x.node.clone()); - - for value in values { - parent.insert_child_before(&value.node, first.as_ref()); - } + fn update_node<'a>(&self, _parent: &G, node: &'a [G]) -> Vec { + // replace `textContent` of `node` instead of recreating - for template in templates.borrow().iter() { - parent.remove_child(&template.node); - } - } - VecDiff::Insert { index, value } => { - parent.insert_child_before( - &value.node, - templates - .borrow() - .get(*index) - .map(|template| template.node.next_sibling()) - .flatten() - .as_ref(), - ); - } - VecDiff::Update { index, value } => { - parent.replace_child(&templates.borrow()[*index].node, &value.node); - } - VecDiff::Remove { index } => { - parent.remove_child(&templates.borrow()[*index].node); - } - VecDiff::Swap { index1, index2 } => { - let child1 = &templates.borrow()[*index1].node; - let child2 = &templates.borrow()[*index2].node; - parent.replace_child(child1, child2); - parent.replace_child(child2, child1); - } - VecDiff::Push { value } => { - parent.insert_child_before( - &value.node, - templates - .borrow() - .last() - .map(|last| last.node.next_sibling()) - .flatten() - .as_ref(), - ); - } - VecDiff::Pop => { - if let Some(last) = templates.borrow().last() { - parent.remove_child(&last.node); - } - } - VecDiff::Clear => { - for template in templates.borrow().iter() { - parent.remove_child(&template.node); - } - } - } - } + node.first() + .unwrap() + .update_inner_text(&format!("{}", self)); - node.clone() + node.to_vec() } } impl Render for TemplateResult { - fn render(&self) -> G { - self.node.clone() + fn render(&self) -> Vec { + self.into_iter().cloned().collect() } } diff --git a/maple-core/src/template_result.rs b/maple-core/src/template_result.rs new file mode 100644 index 000000000..611db872e --- /dev/null +++ b/maple-core/src/template_result.rs @@ -0,0 +1,115 @@ +use crate::generic_node::GenericNode; + +/// Internal type for [`TemplateResult`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TemplateResultInner { + Node(G), + Fragment(Vec), +} + +/// Result of the [`template`] macro. Should not be constructed manually. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TemplateResult { + inner: TemplateResultInner, +} + +impl TemplateResult { + /// Create a new [`TemplateResult`] from a [`GenericNode`]. + pub fn new_node(node: G) -> Self { + Self { + inner: TemplateResultInner::Node(node), + } + } + + /// Create a new [`TemplateResult`] from a `Vec` of [`GenericNode`]s. + pub fn new_fragment(fragment: Vec) -> Self { + debug_assert!( + !fragment.is_empty(), + "fragment must have at least 1 child node, use empty() instead" + ); + + Self { + inner: TemplateResultInner::Fragment(fragment), + } + } + + /// Create a new [`TemplateResult`] with a blank comment node + pub fn empty() -> Self { + Self::new_node(G::marker()) + } + + /// Gets the first node in the [`TemplateResult`]. + /// + /// # Panics + /// + /// Panics if the fragment has no child nodes. + pub fn first_node(&self) -> &G { + match &self.inner { + TemplateResultInner::Node(node) => node, + TemplateResultInner::Fragment(fragment) => { + fragment.first().expect("fragment has no child nodes") + } + } + } + + /// Gets the last node in the [`TemplateResult`]. + /// + /// # Panics + /// + /// Panics if the fragment has no child nodes. + pub fn last_node(&self) -> &G { + match &self.inner { + TemplateResultInner::Node(node) => node, + TemplateResultInner::Fragment(fragment) => { + fragment.last().expect("fragment has no child nodes") + } + } + } + + pub fn iter(&self) -> Iter { + match &self.inner { + TemplateResultInner::Node(node) => Iter::Node(Some(node).into_iter()), + TemplateResultInner::Fragment(fragment) => Iter::Fragment(fragment.iter()), + } + } +} + +impl IntoIterator for TemplateResult { + type Item = G; + + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + match self.inner { + TemplateResultInner::Node(node) => vec![node].into_iter(), + TemplateResultInner::Fragment(fragment) => fragment.into_iter(), + } + } +} + +/// An iterator over references of the nodes in [`TemplateResult`]. Created using [`TemplateResult::iter`]. +pub enum Iter<'a, G: GenericNode> { + Node(std::option::IntoIter<&'a G>), + Fragment(std::slice::Iter<'a, G>), +} + +impl<'a, G: GenericNode> Iterator for Iter<'a, G> { + type Item = &'a G; + + fn next(&mut self) -> Option { + match self { + Iter::Node(node) => node.next(), + Iter::Fragment(fragment) => fragment.next(), + } + } +} + +impl<'a, G: GenericNode> IntoIterator for &'a TemplateResult { + type Item = &'a G; + + type IntoIter = Iter<'a, G>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} diff --git a/maple-core/tests/integration/keyed.rs b/maple-core/tests/integration/keyed.rs index d8329ce7e..ca34d530d 100644 --- a/maple-core/tests/integration/keyed.rs +++ b/maple-core/tests/integration/keyed.rs @@ -16,7 +16,7 @@ fn append() { } }); - render_to(|| node, &test_div()); + render_to(|| node, &test_container()); let p = document().query_selector("ul").unwrap().unwrap(); @@ -49,7 +49,7 @@ fn swap_rows() { } }); - render_to(|| node, &test_div()); + render_to(|| node, &test_container()); let p = document().query_selector("ul").unwrap().unwrap(); assert_eq!(p.text_content().unwrap(), "123"); @@ -85,7 +85,7 @@ fn delete_row() { } }); - render_to(|| node, &test_div()); + render_to(|| node, &test_container()); let p = document().query_selector("ul").unwrap().unwrap(); assert_eq!(p.text_content().unwrap(), "123"); @@ -114,7 +114,7 @@ fn clear() { } }); - render_to(|| node, &test_div()); + render_to(|| node, &test_container()); let p = document().query_selector("ul").unwrap().unwrap(); assert_eq!(p.text_content().unwrap(), "123"); @@ -139,7 +139,7 @@ fn insert_front() { } }); - render_to(|| node, &test_div()); + render_to(|| node, &test_container()); let p = document().query_selector("ul").unwrap().unwrap(); assert_eq!(p.text_content().unwrap(), "123"); @@ -168,7 +168,7 @@ fn nested_reactivity() { } }); - render_to(|| node, &test_div()); + render_to(|| node, &test_container()); let p = document().query_selector("ul").unwrap().unwrap(); assert_eq!(p.text_content().unwrap(), "123"); @@ -183,3 +183,90 @@ fn nested_reactivity() { }); assert_eq!(p.text_content().unwrap(), "4235"); } + +#[wasm_bindgen_test] +fn fragment_template() { + let count = Signal::new(vec![1, 2]); + + let node = cloned!((count) => template! { + div { + Keyed(KeyedProps { + iterable: count.handle(), + template: |item| template! { + span { "The value is: " } + strong { (item) } + }, + key: |item| *item, + }) + } + }); + + render_to(|| node, &test_container()); + + let p = document().query_selector("div").unwrap().unwrap(); + + assert_eq!( + p.inner_html(), + "\ +The value is: 1\ +The value is: 2\ +" + ); + + count.set({ + let mut tmp = (*count.get()).clone(); + tmp.push(3); + tmp + }); + assert_eq!( + p.inner_html(), + "\ +The value is: 1\ +The value is: 2\ +The value is: 3\ +" + ); + + count.set(count.get()[1..].into()); + assert_eq!( + p.inner_html(), + "\ +The value is: 2\ +The value is: 3\ +" + ); +} + +#[wasm_bindgen_test] +fn template_top_level() { + let count = Signal::new(vec![1, 2]); + + let node = cloned!((count) => template! { + Keyed(KeyedProps { + iterable: count.handle(), + template: |item| template! { + li { (item) } + }, + key: |item| *item, + }) + }); + + render_to(|| node, &test_container()); + + let p = document() + .query_selector("#test-container") + .unwrap() + .unwrap(); + + assert_eq!(p.text_content().unwrap(), "12"); + + count.set({ + let mut tmp = (*count.get()).clone(); + tmp.push(3); + tmp + }); + assert_eq!(p.text_content().unwrap(), "123"); + + count.set(count.get()[1..].into()); + assert_eq!(p.text_content().unwrap(), "23"); +} diff --git a/maple-core/tests/integration/main.rs b/maple-core/tests/integration/main.rs index 9c8112282..d21e41bbb 100644 --- a/maple-core/tests/integration/main.rs +++ b/maple-core/tests/integration/main.rs @@ -16,21 +16,24 @@ fn document() -> Document { } /// Returns a [`Node`] referencing the test container with the contents cleared. -fn test_div() -> Node { +fn test_container() -> Node { if document() - .query_selector("div#test-container") + .query_selector("test-container#test-container") .unwrap() .is_none() { document() .body() .unwrap() - .insert_adjacent_html("beforeend", r#"
"#) + .insert_adjacent_html( + "beforeend", + r#""#, + ) .unwrap(); } let container = document() - .query_selector("div#test-container") + .query_selector("test-container#test-container") .unwrap() .unwrap(); @@ -39,13 +42,29 @@ fn test_div() -> Node { container.into() } +#[wasm_bindgen_test] +fn empty_template() { + let node = template! {}; + + render_to(|| node, &test_container()); + + assert_eq!( + document() + .query_selector("#test-container") + .unwrap() + .unwrap() + .inner_html(), + "" + ); +} + #[wasm_bindgen_test] fn hello_world() { let node = template! { p { "Hello World!" } }; - render_to(|| node, &test_div()); + render_to(|| node, &test_container()); assert_eq!( &document() @@ -65,7 +84,7 @@ fn hello_world_noderef() { p(ref=p_ref) { "Hello World!" } }; - render_to(|| node, &test_div()); + render_to(|| node, &test_container()); assert_eq!( &p_ref @@ -83,7 +102,7 @@ fn interpolation() { p { (text) } }; - render_to(|| node, &test_div()); + render_to(|| node, &test_container()); assert_eq!( document() @@ -104,7 +123,7 @@ fn reactive_text() { p { (count.get()) } }); - render_to(|| node, &test_div()); + render_to(|| node, &test_container()); let p = document().query_selector("p").unwrap().unwrap(); @@ -122,7 +141,7 @@ fn reactive_attribute() { span(attribute=count.get()) }); - render_to(|| node, &test_div()); + render_to(|| node, &test_container()); let span = document().query_selector("span").unwrap().unwrap(); @@ -142,7 +161,7 @@ fn noderefs() { } }; - render_to(|| node, &test_div()); + render_to(|| node, &test_container()); let input_ref = document().query_selector("input").unwrap().unwrap(); @@ -151,3 +170,21 @@ fn noderefs() { noderef.get::().unchecked_into() ); } + +#[wasm_bindgen_test] +fn fragments() { + let node = template! { + p { "1" } + p { "2" } + p { "3" } + }; + + render_to(|| node, &test_container()); + + let test_container = document() + .query_selector("#test-container") + .unwrap() + .unwrap(); + + assert_eq!(test_container.text_content().unwrap(), "123"); +} diff --git a/maple-core/tests/integration/non_keyed.rs b/maple-core/tests/integration/non_keyed.rs index d96481bdc..c1d309e33 100644 --- a/maple-core/tests/integration/non_keyed.rs +++ b/maple-core/tests/integration/non_keyed.rs @@ -15,7 +15,7 @@ fn append() { } }); - render_to(|| node, &test_div()); + render_to(|| node, &test_container()); let p = document().query_selector("ul").unwrap().unwrap(); @@ -47,7 +47,7 @@ fn swap_rows() { } }); - render_to(|| node, &test_div()); + render_to(|| node, &test_container()); let p = document().query_selector("ul").unwrap().unwrap(); assert_eq!(p.text_content().unwrap(), "123"); @@ -82,7 +82,7 @@ fn delete_row() { } }); - render_to(|| node, &test_div()); + render_to(|| node, &test_container()); let p = document().query_selector("ul").unwrap().unwrap(); assert_eq!(p.text_content().unwrap(), "123"); @@ -110,7 +110,7 @@ fn clear() { } }); - render_to(|| node, &test_div()); + render_to(|| node, &test_container()); let p = document().query_selector("ul").unwrap().unwrap(); assert_eq!(p.text_content().unwrap(), "123"); @@ -134,7 +134,7 @@ fn insert_front() { } }); - render_to(|| node, &test_div()); + render_to(|| node, &test_container()); let p = document().query_selector("ul").unwrap().unwrap(); assert_eq!(p.text_content().unwrap(), "123"); @@ -162,7 +162,7 @@ fn nested_reactivity() { } }); - render_to(|| node, &test_div()); + render_to(|| node, &test_container()); let p = document().query_selector("ul").unwrap().unwrap(); assert_eq!(p.text_content().unwrap(), "123"); @@ -177,3 +177,88 @@ fn nested_reactivity() { }); assert_eq!(p.text_content().unwrap(), "4235"); } + +#[wasm_bindgen_test] +fn fragment_template() { + let count = Signal::new(vec![1, 2]); + + let node = cloned!((count) => template! { + div { + Indexed(IndexedProps { + iterable: count.handle(), + template: |item| template! { + span { "The value is: " } + strong { (item) } + }, + }) + } + }); + + render_to(|| node, &test_container()); + + let p = document().query_selector("div").unwrap().unwrap(); + + assert_eq!( + p.inner_html(), + "\ +The value is: 1\ +The value is: 2\ +" + ); + + count.set({ + let mut tmp = (*count.get()).clone(); + tmp.push(3); + tmp + }); + assert_eq!( + p.inner_html(), + "\ +The value is: 1\ +The value is: 2\ +The value is: 3\ +" + ); + + count.set(count.get()[1..].into()); + assert_eq!( + p.inner_html(), + "\ +The value is: 2\ +The value is: 3\ +" + ); +} + +#[wasm_bindgen_test] +fn template_top_level() { + let count = Signal::new(vec![1, 2]); + + let node = cloned!((count) => template! { + Indexed(IndexedProps { + iterable: count.handle(), + template: |item| template! { + li { (item) } + }, + }) + }); + + render_to(|| node, &test_container()); + + let p = document() + .query_selector("#test-container") + .unwrap() + .unwrap(); + + assert_eq!(p.text_content().unwrap(), "12"); + + count.set({ + let mut tmp = (*count.get()).clone(); + tmp.push(3); + tmp + }); + assert_eq!(p.text_content().unwrap(), "123"); + + count.set(count.get()[1..].into()); + assert_eq!(p.text_content().unwrap(), "23"); +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 000000000..31c3d14f1 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +comment_width = 100 +wrap_comments = true