Document Fragments (#89)

* Remove SignalVec

* Move TemplateResult into sub-module

* wrap comments at 100

* Make TemplateResult able to hold a fragment

* Iter and IntoIter for TemplateResult

* Update flow.rs

* Update render_* functions

* Update Render trait

* Make Render accept slice

* Update template! macro

* Fix template!

* Allow multiple children at template! root

* Add some integration tests

* Add some more integration tests

* Add more docs
This commit is contained in:
Luke Chu 2021-04-06 16:23:57 -07:00 committed by GitHub
parent e1f2709eee
commit 564449454a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 614 additions and 580 deletions

View File

@ -38,3 +38,18 @@ template! {
} }
} }
``` ```
Templates can also be fragments.
```rust
template! {
p { "First child" }
p { "Second child" }
}
```
Or be empty.
```rust
template! {}
```

View File

@ -31,7 +31,11 @@ impl ToTokens for Component {
args, args,
} = self; } = 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); tokens.extend(quoted);
} }

View File

@ -100,7 +100,9 @@ impl ToTokens for Element {
for child in &children.body { for child in &children.body {
let quoted = match child { let quoted = match child {
HtmlTree::Component(component) => quote_spanned! { component.span()=> 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()=> HtmlTree::Element(element) => quote_spanned! { element.span()=>
::maple_core::generic_node::GenericNode::append_child(&element, &#element); ::maple_core::generic_node::GenericNode::append_child(&element, &#element);

View File

@ -65,11 +65,62 @@ impl Parse for HtmlTree {
impl ToTokens for HtmlTree { impl ToTokens for HtmlTree {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
match self { let quoted = match self {
Self::Component(component) => component.to_tokens(tokens), Self::Component(component) => quote! {
Self::Element(element) => element.to_tokens(tokens), #component
Self::Text(text) => text.to_tokens(tokens), },
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<HtmlTree>,
}
impl Parse for HtmlRoot {
fn parse(input: ParseStream) -> Result<Self> {
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 /// TODO: write some more docs
#[proc_macro] #[proc_macro]
pub fn template(input: TokenStream) -> TokenStream { pub fn template(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as HtmlTree); let input = parse_macro_input!(input as HtmlRoot);
let quoted = quote! { TokenStream::from(input.to_token_stream())
::maple_core::TemplateResult::new(#input)
};
TokenStream::from(quoted)
} }

View File

@ -1,4 +1,4 @@
error: unexpected token error: expected a valid HTML node
--> $DIR/element-fail.rs:4:45 --> $DIR/element-fail.rs:4:45
| |
4 | let _: TemplateResult<G> = template! { p.my-class#id }; 4 | let _: TemplateResult<G> = template! { p.my-class#id };

View File

@ -0,0 +1,16 @@
use maple_core::prelude::*;
fn compile_pass<G: GenericNode>() {
let _: TemplateResult<G> = template! { "Raw text nodes!" };
let _: TemplateResult<G> = template! {
p { "First" }
p { "Second" }
"Third"
};
// let spliced = 123;
// let _: TemplateResult<G> = template! { (spliced) };
}
fn main() {}

View File

@ -26,7 +26,8 @@ where
} }
/// Keyed iteration. Use this instead of directly rendering an array of [`TemplateResult`]s. /// 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`]. /// For non keyed iteration, see [`Indexed`].
/// ///
@ -66,7 +67,8 @@ where
type TemplateValue<T, G> = (Owner, T, TemplateResult<G>, usize /* index */); type TemplateValue<T, G> = (Owner, T, TemplateResult<G>, 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<RefCell<HashMap<Key, TemplateValue<T, G>>>> = Default::default(); let templates: Rc<RefCell<HashMap<Key, TemplateValue<T, G>>>> = Default::default();
let fragment = G::fragment(); let fragment = G::fragment();
@ -84,7 +86,9 @@ where
if iterable.get().is_empty() { if iterable.get().is_empty() {
for (_, (owner, _value, template, _i)) in templates.borrow_mut().drain() { for (_, (owner, _value, template, _i)) in templates.borrow_mut().drain() {
drop(owner); // destroy owner drop(owner); // destroy owner
template.node.remove_self(); for node in &template {
node.remove_self()
}
} }
return; return;
} }
@ -101,9 +105,9 @@ where
.map(|x| (x.0.clone(), (x.1 .2.clone(), x.1 .3))) .map(|x| (x.0.clone(), (x.1 .2.clone(), x.1 .3)))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for node in &excess_nodes { for template in &excess_nodes {
let removed_index = node.1 .1; let removed_index = template.1 .1;
templates.remove(&node.0); templates.remove(&template.0);
// Offset indexes of other templates by 1. // Offset indexes of other templates by 1.
for (_, _, _, i) in templates.values_mut() { for (_, _, _, i) in templates.values_mut() {
@ -113,8 +117,10 @@ where
} }
} }
for node in excess_nodes { for template in excess_nodes {
node.1 .0.node.remove_self(); for node in template.1 .0 {
node.remove_self();
}
} }
} }
@ -158,16 +164,19 @@ where
if let Some(next_item) = iterable.get().get(i + 1) { if let Some(next_item) = iterable.get().get(i + 1) {
let templates = templates.borrow(); let templates = templates.borrow();
if let Some(next_node) = templates.get(&key_fn(next_item)) { if let Some(next_template) = templates.get(&key_fn(next_item)) {
next_node for node in &new_template.unwrap() {
.2 next_template.2.first_node().insert_sibling_before(node);
.node }
.insert_sibling_before(&new_template.unwrap().node);
} else { } else {
marker.insert_sibling_before(&new_template.unwrap().node); for node in &new_template.unwrap() {
marker.insert_sibling_before(node);
}
} }
} else { } 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 { } else if match previous_value {
Some(prev) => prev.index, Some(prev) => prev.index,
@ -177,14 +186,21 @@ where
// Location changed, move from old location to new location // Location changed, move from old location to new location
// Node was moved in the DOM. Move node to new index. // 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 templates = templates.borrow();
let next_node = templates.get(&key_fn(next_item)).unwrap(); let template = &templates.get(&key).unwrap().2;
next_node.2.node.insert_sibling_before(&node); // Move to before next node
} else { if let Some(next_item) = iterable.get().get(i + 1) {
marker.insert_sibling_before(&node); // Move to end. 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; templates.borrow_mut().get_mut(&key).unwrap().3 = i;
@ -206,19 +222,25 @@ where
let mut new_template = None; let mut new_template = None;
let owner = create_root(|| new_template = Some(template(item.clone()))); 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(), templates.get_mut(&key).unwrap(),
(owner, item.clone(), new_template.clone().unwrap(), i), (owner, item.clone(), new_template.clone().unwrap(), i),
); );
let parent = old_node.node.parent_node().unwrap(); let parent = old_template.first_node().parent_node().unwrap();
parent.replace_child(&new_template.unwrap().node, &old_node.node);
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`]. /// Props for [`Indexed`].
@ -230,8 +252,9 @@ where
pub template: F, pub template: F,
} }
/// Non keyed iteration (or keyed by index). Use this instead of directly rendering an array of [`TemplateResult`]s. /// Non keyed iteration (or keyed by index). Use this instead of directly rendering an array of
/// Using this will minimize re-renders instead of re-rendering every single node on every state change. /// [`TemplateResult`]s. Using this will minimize re-renders instead of re-rendering every single
/// node on every state change.
/// ///
/// For keyed iteration, see [`Keyed`]. /// For keyed iteration, see [`Keyed`].
/// ///
@ -275,7 +298,9 @@ where
if props.iterable.get().is_empty() { if props.iterable.get().is_empty() {
for (owner, template) in templates.borrow_mut().drain(..) { for (owner, template) in templates.borrow_mut().drain(..) {
drop(owner); // destroy owner drop(owner); // destroy owner
template.node.remove_self(); for node in template {
node.remove_self();
}
} }
return; return;
} }
@ -299,13 +324,18 @@ where
let owner = create_root(|| new_template = Some((props.template)(item.clone()))); let owner = create_root(|| new_template = Some((props.template)(item.clone())));
if templates.borrow().get(i).is_some() { if templates.borrow().get(i).is_some() {
let old_node = mem::replace( let old_template = mem::replace(
&mut templates.borrow_mut()[i], &mut templates.borrow_mut()[i],
(owner, new_template.as_ref().unwrap().clone()), (owner, new_template.as_ref().unwrap().clone()),
); );
let parent = old_node.1.node.parent_node().unwrap(); let parent = old_template.1.first_node().parent_node().unwrap();
parent.replace_child(&new_template.unwrap().node, &old_node.1.node); 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 { } else {
debug_assert!(templates.borrow().len() == i, "pushing new value scenario"); debug_assert!(templates.borrow().len() == i, "pushing new value scenario");
@ -313,7 +343,9 @@ where
.borrow_mut() .borrow_mut()
.push((owner, new_template.as_ref().unwrap().clone())); .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 mut templates = templates.borrow_mut();
let excess_nodes = templates.drain(props.iterable.get().len()..); let excess_nodes = templates.drain(props.iterable.get().len()..);
for node in excess_nodes { for template in excess_nodes {
node.1.node.remove_self(); for node in &template.1 {
node.remove_self();
}
} }
} }
@ -331,5 +365,5 @@ where
} }
}); });
TemplateResult::new(fragment) TemplateResult::new_node(fragment)
} }

View File

@ -22,18 +22,21 @@ pub type EventListener = dyn Fn(Event);
/// Abstraction over a rendering backend. /// Abstraction over a rendering backend.
/// ///
/// You would probably use this trait as a trait bound when you want to accept any rendering backend. /// You would probably use this trait as a trait bound when you want to accept any rendering
/// For example, components are often generic over [`GenericNode`] to be able to render to different backends. /// 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 /// Note that components are **NOT** represented by [`GenericNode`]. Instead, components are
/// that they are simply functions that generate [`GenericNode`]s inside a new reactive context. This means that there /// _disappearing_, meaning that they are simply functions that generate [`GenericNode`]s inside a
/// is no overhead whatsoever when using components. /// new reactive context. This means that there is no overhead whatsoever when using components.
/// ///
/// Maple ships with 2 rendering backends out of the box: /// Maple ships with 2 rendering backends out of the box:
/// * [`DomNode`] - Rendering in the browser (to real DOM nodes). /// * [`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 { pub trait GenericNode: fmt::Debug + Clone + PartialEq + Eq + 'static {
/// Create a new element node. /// Create a new element node.
fn element(tag: &str) -> Self; fn element(tag: &str) -> Self;
@ -41,12 +44,14 @@ pub trait GenericNode: fmt::Debug + Clone + PartialEq + Eq + 'static {
/// Create a new text node. /// Create a new text node.
fn text_node(text: &str) -> Self; 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; fn fragment() -> Self;
/// Create a marker (dummy) node. For [`DomNode`], this is implemented by creating an empty comment node. /// Create a marker (dummy) node. For [`DomNode`], this is implemented by creating an empty
/// This is used, for example, in [`Keyed`] and [`Indexed`] for scenarios where you want to push a new item to the /// comment node. This is used, for example, in [`Keyed`] and [`Indexed`] for scenarios
/// end of the list. If the list is empty, a dummy node is needed to store the position of the component. /// 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; fn marker() -> Self;
/// Sets an attribute on a node. /// 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. /// Appends a child to the node's children.
fn append_child(&self, child: &Self); 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 /// Insert a new child node to this node's children. If `reference_node` is `Some`, the child
/// before the reference node. Else if `None`, the child will be inserted at the end. /// 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>); fn insert_child_before(&self, new_node: &Self, reference_node: Option<&Self>);
/// Remove a child node from this node's children. /// 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`. /// Add a [`EventListener`] to the event `name`.
fn event(&self, name: &str, handler: Box<EventListener>); fn event(&self, name: &str, handler: Box<EventListener>);
/// 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); 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<dyn Fn() -> Box<dyn Render<Self>>>) { fn append_render(&self, child: Box<dyn Fn() -> Box<dyn Render<Self>>>) {
let parent = self.clone(); 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 node = RefCell::new(child().render());
let effect = cloned!((node) => move || { let effect = cloned!((node) => move || {
@ -100,6 +108,8 @@ pub trait GenericNode: fmt::Debug + Clone + PartialEq + Eq + 'static {
(Rc::new(effect), node) (Rc::new(effect), node)
})); }));
parent.append_child(&node.borrow()); for node in nodes.borrow().iter() {
parent.append_child(node);
}
} }
} }

View File

@ -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). //! This is the API docs for maple. If you are looking for the usage docs, checkout the [README](https://github.com/lukechu10/maple).
//! //!
//! ## Features //! ## Features
//! - `dom` (_default_) - Enables rendering templates to DOM nodes. Only useful on `wasm32-unknown-unknown` target. //! - `dom` (_default_) - Enables rendering templates to DOM nodes. Only useful on
//! - `ssr` - Enables rendering templates to static strings (useful for Server Side Rendering / Pre-rendering). //! `wasm32-unknown-unknown` target.
//! - `serde` - Enables serializing and deserializing `Signal`s and other wrapper types using `serde`. //! - `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)] #![allow(non_snake_case)]
#![warn(clippy::clone_on_ref_ptr)] #![warn(clippy::clone_on_ref_ptr)]
@ -15,9 +18,7 @@
#![deny(clippy::trait_duplication_in_bounds)] #![deny(clippy::trait_duplication_in_bounds)]
#![deny(clippy::type_repetition_in_bounds)] #![deny(clippy::type_repetition_in_bounds)]
use generic_node::GenericNode;
pub use maple_core_macro::template; pub use maple_core_macro::template;
use prelude::SignalVec;
pub mod easing; pub mod easing;
pub mod flow; pub mod flow;
@ -26,47 +27,17 @@ pub mod macros;
pub mod noderef; pub mod noderef;
pub mod reactive; pub mod reactive;
pub mod render; pub mod render;
pub mod template_result;
pub mod utils; pub mod utils;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TemplateResult<G: GenericNode> {
node: G,
}
impl<G: GenericNode> TemplateResult<G> {
/// 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<T: GenericNode> {
templates: reactive::SignalVec<TemplateResult<T>>,
}
impl<T: GenericNode> From<SignalVec<TemplateResult<T>>> for TemplateList<T> {
fn from(templates: SignalVec<TemplateResult<T>>) -> Self {
Self { templates }
}
}
/// Render a [`TemplateResult`] into the DOM. /// Render a [`TemplateResult`] into the DOM.
/// Alias for [`render_to`] with `parent` being the `<body>` tag. /// Alias for [`render_to`] with `parent` being the `<body>` tag.
/// ///
/// _This API requires the following crate features to be activated: `dom`_ /// _This API requires the following crate features to be activated: `dom`_
#[cfg(feature = "dom")] #[cfg(feature = "dom")]
pub fn render(template_result: impl FnOnce() -> TemplateResult<generic_node::DomNode>) { pub fn render(
template_result: impl FnOnce() -> template_result::TemplateResult<generic_node::DomNode>,
) {
let window = web_sys::window().unwrap(); let window = web_sys::window().unwrap();
let document = window.document().unwrap(); let document = window.document().unwrap();
@ -79,13 +50,13 @@ pub fn render(template_result: impl FnOnce() -> TemplateResult<generic_node::Dom
/// _This API requires the following crate features to be activated: `dom`_ /// _This API requires the following crate features to be activated: `dom`_
#[cfg(feature = "dom")] #[cfg(feature = "dom")]
pub fn render_to( pub fn render_to(
template_result: impl FnOnce() -> TemplateResult<generic_node::DomNode>, template_result: impl FnOnce() -> template_result::TemplateResult<generic_node::DomNode>,
parent: &web_sys::Node, parent: &web_sys::Node,
) { ) {
let owner = reactive::create_root(|| { let owner = reactive::create_root(|| {
parent for node in template_result() {
.append_child(&template_result().node.inner_element()) parent.append_child(&node.inner_element()).unwrap();
.unwrap(); }
}); });
thread_local! { thread_local! {
@ -95,18 +66,22 @@ pub fn render_to(
GLOBAL_OWNERS.with(|global_owners| global_owners.borrow_mut().push(owner)); 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`_ /// _This API requires the following crate features to be activated: `ssr`_
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
pub fn render_to_string( pub fn render_to_string(
template_result: impl FnOnce() -> TemplateResult<generic_node::SsrNode>, template_result: impl FnOnce() -> template_result::TemplateResult<generic_node::SsrNode>,
) -> String { ) -> String {
let mut ret = None; let mut ret = String::new();
let _owner = let _owner = reactive::create_root(|| {
reactive::create_root(|| ret = Some(format!("{}", template_result().inner_element()))); for node in template_result() {
ret.push_str(&format!("{}", node));
}
});
ret.unwrap() ret
} }
/// The maple prelude. /// The maple prelude.
@ -123,12 +98,12 @@ pub mod prelude {
pub use crate::noderef::NodeRef; pub use crate::noderef::NodeRef;
pub use crate::reactive::{ pub use crate::reactive::{
create_effect, create_effect_initial, create_memo, create_root, create_selector, 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; pub use crate::render::Render;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
pub use crate::render_to_string; pub use crate::render_to_string;
pub use crate::template_result::TemplateResult;
#[cfg(feature = "dom")] #[cfg(feature = "dom")]
pub use crate::{render, render_to}; pub use crate::{render, render_to};
pub use crate::{TemplateList, TemplateResult};
} }

View File

@ -1,16 +1,15 @@
//! Reactive primitives. //! Reactive primitives.
mod effect; mod effect;
mod signal;
mod signal_vec;
mod motion; mod motion;
mod signal;
pub use effect::*; pub use effect::*;
pub use signal::*;
pub use signal_vec::*;
pub use motion::*; 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 /// # Example
/// ``` /// ```

View File

@ -169,7 +169,8 @@ pub fn create_effect_initial<R: 'static>(
CONTEXTS.with(|contexts| { CONTEXTS.with(|contexts| {
let initial_context_size = contexts.borrow().len(); 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(); let running = running.upgrade().unwrap();
// Recreate effect dependencies each time effect is called. // 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". /// 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
/// That is why the output of the function must implement [`PartialEq`]. /// the same. That is why the output of the function must implement [`PartialEq`].
/// ///
/// To specify a custom comparison function, use [`create_selector_with`]. /// To specify a custom comparison function, use [`create_selector_with`].
pub fn create_selector<F, Out>(derived: F) -> StateHandle<Out> pub fn create_selector<F, Out>(derived: F) -> StateHandle<Out>
@ -335,7 +336,8 @@ where
} }
/// Creates a memoized value from some signals. Also know as "derived stores". /// 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 /// It takes a comparison function to compare the old and new value, which returns `true` if they
/// are the same and `false` otherwise. /// are the same and `false` otherwise.
@ -655,7 +657,8 @@ mod tests {
assert_eq!(*double.get(), 2); assert_eq!(*double.get(), 2);
state.set(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] #[test]

View File

@ -94,7 +94,8 @@ impl<T: Lerp + Clone + 'static> Tweened<T> {
/// If the value is being interpolated already due to a previous call to `set()`, the previous /// If the value is being interpolated already due to a previous call to `set()`, the previous
/// task will be canceled. /// 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) { pub fn set(&self, new_value: T) {
let start = self.signal().get_untracked().as_ref().clone(); let start = self.signal().get_untracked().as_ref().clone();
let easing_fn = Rc::clone(&self.0.borrow().easing_fn); let easing_fn = Rc::clone(&self.0.borrow().easing_fn);

View File

@ -158,8 +158,9 @@ impl<T: 'static> Signal<T> {
} }
/// Calls all the subscribers without modifying the state. /// 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. /// This can be useful when using patterns such as inner mutability where the state updated will
/// In the general case, however, it is preferable to use [`Signal::set`] instead. /// not be automatically triggered. In the general case, however, it is preferable to use
/// [`Signal::set`] instead.
pub fn trigger_subscribers(&self) { pub fn trigger_subscribers(&self) {
// Clone subscribers to prevent modifying list when calling callbacks. // Clone subscribers to prevent modifying list when calling callbacks.
let subscribers = self.handle.0.borrow().subscribers.clone(); let subscribers = self.handle.0.borrow().subscribers.clone();
@ -241,7 +242,8 @@ impl<T> SignalInner<T> {
self.subscribers.insert(handler); 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) { fn unsubscribe(&mut self, handler: &Callback) {
self.subscribers.remove(handler); self.subscribers.remove(handler);
} }

View File

@ -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<Vec>`](Signal) because it allows fine grained
/// reactivity within the `Vec`.
pub struct SignalVec<T: 'static> {
signal: Signal<RefCell<Vec<T>>>,
/// A list of past changes that is accessed by subscribers.
/// Cleared when all subscribers are called.
changes: Rc<RefCell<Vec<VecDiff<T>>>>,
}
impl<T: 'static> SignalVec<T> {
/// 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<T>) -> 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<RefCell<Vec<VecDiff<T>>>> {
&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<RefCell<Vec<T>>> {
&self.signal
}
pub fn replace(&self, values: Vec<T>) {
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<T>) {
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<U: Clone>(&self, f: impl Fn(&T) -> U + 'static) -> SignalVec<U> {
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<G: GenericNode> SignalVec<TemplateResult<G>> {
/// Create a [`TemplateList`] from the `SignalVec`.
pub fn template_list(&self) -> TemplateList<G> {
TemplateList::from(self.clone())
}
}
impl<T: 'static + Clone> SignalVec<T> {
/// 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<T> {
self.signal.get().borrow().clone()
}
}
impl<T: 'static> Default for SignalVec<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: 'static> Clone for SignalVec<T> {
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<T> {
Replace { values: Vec<T> },
Insert { index: usize, value: T },
Update { index: usize, value: T },
Remove { index: usize },
Swap { index1: usize, index2: usize },
Push { value: T },
Pop,
Clear,
}
impl<T> VecDiff<T> {
pub fn apply_to_vec(self, v: &mut Vec<T>) {
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::<i32>::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]
);
}
}

View File

@ -1,135 +1,53 @@
//! Trait for describing how something should be rendered into DOM nodes. //! Trait for describing how something should be rendered into DOM nodes.
use std::fmt; use std::fmt;
use std::rc::Rc;
use crate::generic_node::GenericNode; use crate::generic_node::GenericNode;
use crate::reactive::VecDiff; use crate::template_result::TemplateResult;
use crate::{TemplateList, TemplateResult};
/// Trait for describing how something should be rendered into DOM nodes. /// Trait for describing how something should be rendered into DOM nodes.
pub trait Render<G: GenericNode> { pub trait Render<G: GenericNode> {
/// Called during the initial render when creating the DOM nodes. Should return a [`GenericNode`]. /// Called during the initial render when creating the DOM nodes. Should return a
fn render(&self) -> G; /// `Vec` of [`GenericNode`]s.
fn render(&self) -> Vec<G>;
/// Called when the node should be updated with new state. /// 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. /// The default implementation of this will replace the child node completely with the result of
/// Another implementation might be better suited to some specific types. /// calling `render` again. Another implementation might be better suited to some specific
/// For example, text nodes can simply replace the inner text instead of recreating a new node. /// 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. /// Returns the new node. If the node is reused instead of replaced, the returned node is simply
fn update_node(&self, parent: &G, node: &G) -> G { /// the node passed in.
let new_node = self.render(); fn update_node<'a>(&self, parent: &G, node: &'a [G]) -> Vec<G> {
parent.replace_child(&new_node, &node); let new_nodes = self.render();
new_node
for new_node in &new_nodes {
parent.replace_child(new_node, node.first().unwrap());
}
new_nodes
} }
} }
impl<T: fmt::Display + ?Sized, G: GenericNode> Render<G> for T { impl<T: fmt::Display + ?Sized, G: GenericNode> Render<G> for T {
fn render(&self) -> G { fn render(&self) -> Vec<G> {
G::text_node(&format!("{}", self)) vec![G::text_node(&format!("{}", self))]
} }
fn update_node(&self, _parent: &G, node: &G) -> G { fn update_node<'a>(&self, _parent: &G, node: &'a [G]) -> Vec<G> {
// replace `textContent` of `node` instead of recreating // replace `textContent` of `node` instead of recreating
node.update_inner_text(&format!("{}", self)); node.first()
.unwrap()
.update_inner_text(&format!("{}", self));
node.clone() node.to_vec()
}
}
impl<G: GenericNode> Render<G> for TemplateList<G> {
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
}
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());
}
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.clone()
} }
} }
impl<G: GenericNode> Render<G> for TemplateResult<G> { impl<G: GenericNode> Render<G> for TemplateResult<G> {
fn render(&self) -> G { fn render(&self) -> Vec<G> {
self.node.clone() self.into_iter().cloned().collect()
} }
} }

View File

@ -0,0 +1,115 @@
use crate::generic_node::GenericNode;
/// Internal type for [`TemplateResult`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TemplateResultInner<G: GenericNode> {
Node(G),
Fragment(Vec<G>),
}
/// Result of the [`template`] macro. Should not be constructed manually.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TemplateResult<G: GenericNode> {
inner: TemplateResultInner<G>,
}
impl<G: GenericNode> TemplateResult<G> {
/// 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<G>) -> 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<G> {
match &self.inner {
TemplateResultInner::Node(node) => Iter::Node(Some(node).into_iter()),
TemplateResultInner::Fragment(fragment) => Iter::Fragment(fragment.iter()),
}
}
}
impl<G: GenericNode> IntoIterator for TemplateResult<G> {
type Item = G;
type IntoIter = std::vec::IntoIter<G>;
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<Self::Item> {
match self {
Iter::Node(node) => node.next(),
Iter::Fragment(fragment) => fragment.next(),
}
}
}
impl<'a, G: GenericNode> IntoIterator for &'a TemplateResult<G> {
type Item = &'a G;
type IntoIter = Iter<'a, G>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}

View File

@ -16,7 +16,7 @@ fn append() {
} }
}); });
render_to(|| node, &test_div()); render_to(|| node, &test_container());
let p = document().query_selector("ul").unwrap().unwrap(); 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(); let p = document().query_selector("ul").unwrap().unwrap();
assert_eq!(p.text_content().unwrap(), "123"); 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(); let p = document().query_selector("ul").unwrap().unwrap();
assert_eq!(p.text_content().unwrap(), "123"); 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(); let p = document().query_selector("ul").unwrap().unwrap();
assert_eq!(p.text_content().unwrap(), "123"); 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(); let p = document().query_selector("ul").unwrap().unwrap();
assert_eq!(p.text_content().unwrap(), "123"); 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(); let p = document().query_selector("ul").unwrap().unwrap();
assert_eq!(p.text_content().unwrap(), "123"); assert_eq!(p.text_content().unwrap(), "123");
@ -183,3 +183,90 @@ fn nested_reactivity() {
}); });
assert_eq!(p.text_content().unwrap(), "4235"); 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(),
"\
<span>The value is: </span><strong>1</strong>\
<span>The value is: </span><strong>2</strong>\
<!---->"
);
count.set({
let mut tmp = (*count.get()).clone();
tmp.push(3);
tmp
});
assert_eq!(
p.inner_html(),
"\
<span>The value is: </span><strong>1</strong>\
<span>The value is: </span><strong>2</strong>\
<span>The value is: </span><strong>3</strong>\
<!---->"
);
count.set(count.get()[1..].into());
assert_eq!(
p.inner_html(),
"\
<span>The value is: </span><strong>2</strong>\
<span>The value is: </span><strong>3</strong>\
<!---->"
);
}
#[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");
}

View File

@ -16,21 +16,24 @@ fn document() -> Document {
} }
/// Returns a [`Node`] referencing the test container with the contents cleared. /// Returns a [`Node`] referencing the test container with the contents cleared.
fn test_div() -> Node { fn test_container() -> Node {
if document() if document()
.query_selector("div#test-container") .query_selector("test-container#test-container")
.unwrap() .unwrap()
.is_none() .is_none()
{ {
document() document()
.body() .body()
.unwrap() .unwrap()
.insert_adjacent_html("beforeend", r#"<div id="test-container"></div>"#) .insert_adjacent_html(
"beforeend",
r#"<test-container id="test-container"></test-container>"#,
)
.unwrap(); .unwrap();
} }
let container = document() let container = document()
.query_selector("div#test-container") .query_selector("test-container#test-container")
.unwrap() .unwrap()
.unwrap(); .unwrap();
@ -39,13 +42,29 @@ fn test_div() -> Node {
container.into() 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] #[wasm_bindgen_test]
fn hello_world() { fn hello_world() {
let node = template! { let node = template! {
p { "Hello World!" } p { "Hello World!" }
}; };
render_to(|| node, &test_div()); render_to(|| node, &test_container());
assert_eq!( assert_eq!(
&document() &document()
@ -65,7 +84,7 @@ fn hello_world_noderef() {
p(ref=p_ref) { "Hello World!" } p(ref=p_ref) { "Hello World!" }
}; };
render_to(|| node, &test_div()); render_to(|| node, &test_container());
assert_eq!( assert_eq!(
&p_ref &p_ref
@ -83,7 +102,7 @@ fn interpolation() {
p { (text) } p { (text) }
}; };
render_to(|| node, &test_div()); render_to(|| node, &test_container());
assert_eq!( assert_eq!(
document() document()
@ -104,7 +123,7 @@ fn reactive_text() {
p { (count.get()) } p { (count.get()) }
}); });
render_to(|| node, &test_div()); render_to(|| node, &test_container());
let p = document().query_selector("p").unwrap().unwrap(); let p = document().query_selector("p").unwrap().unwrap();
@ -122,7 +141,7 @@ fn reactive_attribute() {
span(attribute=count.get()) span(attribute=count.get())
}); });
render_to(|| node, &test_div()); render_to(|| node, &test_container());
let span = document().query_selector("span").unwrap().unwrap(); 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(); let input_ref = document().query_selector("input").unwrap().unwrap();
@ -151,3 +170,21 @@ fn noderefs() {
noderef.get::<DomNode>().unchecked_into() noderef.get::<DomNode>().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");
}

View File

@ -15,7 +15,7 @@ fn append() {
} }
}); });
render_to(|| node, &test_div()); render_to(|| node, &test_container());
let p = document().query_selector("ul").unwrap().unwrap(); 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(); let p = document().query_selector("ul").unwrap().unwrap();
assert_eq!(p.text_content().unwrap(), "123"); 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(); let p = document().query_selector("ul").unwrap().unwrap();
assert_eq!(p.text_content().unwrap(), "123"); 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(); let p = document().query_selector("ul").unwrap().unwrap();
assert_eq!(p.text_content().unwrap(), "123"); 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(); let p = document().query_selector("ul").unwrap().unwrap();
assert_eq!(p.text_content().unwrap(), "123"); 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(); let p = document().query_selector("ul").unwrap().unwrap();
assert_eq!(p.text_content().unwrap(), "123"); assert_eq!(p.text_content().unwrap(), "123");
@ -177,3 +177,88 @@ fn nested_reactivity() {
}); });
assert_eq!(p.text_content().unwrap(), "4235"); 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(),
"\
<span>The value is: </span><strong>1</strong>\
<span>The value is: </span><strong>2</strong>\
<!---->"
);
count.set({
let mut tmp = (*count.get()).clone();
tmp.push(3);
tmp
});
assert_eq!(
p.inner_html(),
"\
<span>The value is: </span><strong>1</strong>\
<span>The value is: </span><strong>2</strong>\
<span>The value is: </span><strong>3</strong>\
<!---->"
);
count.set(count.get()[1..].into());
assert_eq!(
p.inner_html(),
"\
<span>The value is: </span><strong>2</strong>\
<span>The value is: </span><strong>3</strong>\
<!---->"
);
}
#[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");
}

2
rustfmt.toml Normal file
View File

@ -0,0 +1,2 @@
comment_width = 100
wrap_comments = true