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:
parent
e1f2709eee
commit
564449454a
|
@ -38,3 +38,18 @@ template! {
|
|||
}
|
||||
}
|
||||
```
|
||||
|
||||
Templates can also be fragments.
|
||||
|
||||
```rust
|
||||
template! {
|
||||
p { "First child" }
|
||||
p { "Second child" }
|
||||
}
|
||||
```
|
||||
|
||||
Or be empty.
|
||||
|
||||
```rust
|
||||
template! {}
|
||||
```
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<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
|
||||
#[proc_macro]
|
||||
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! {
|
||||
::maple_core::TemplateResult::new(#input)
|
||||
};
|
||||
|
||||
TokenStream::from(quoted)
|
||||
TokenStream::from(input.to_token_stream())
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
error: unexpected token
|
||||
error: expected a valid HTML node
|
||||
--> $DIR/element-fail.rs:4:45
|
||||
|
|
||||
4 | let _: TemplateResult<G> = template! { p.my-class#id };
|
||||
|
|
|
@ -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() {}
|
|
@ -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<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 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::<Vec<_>>();
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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<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);
|
||||
|
||||
/// 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>>>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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.
|
||||
/// Alias for [`render_to`] with `parent` being the `<body>` tag.
|
||||
///
|
||||
/// _This API requires the following crate features to be activated: `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 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`_
|
||||
#[cfg(feature = "dom")]
|
||||
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,
|
||||
) {
|
||||
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<generic_node::SsrNode>,
|
||||
template_result: impl FnOnce() -> template_result::TemplateResult<generic_node::SsrNode>,
|
||||
) -> 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};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
/// ```
|
||||
|
|
|
@ -169,7 +169,8 @@ pub fn create_effect_initial<R: 'static>(
|
|||
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<F, Out>(derived: F) -> StateHandle<Out>
|
||||
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
/// 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);
|
||||
|
|
|
@ -158,8 +158,9 @@ impl<T: 'static> Signal<T> {
|
|||
}
|
||||
|
||||
/// 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<T> SignalInner<T> {
|
|||
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);
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<G: GenericNode> {
|
||||
/// 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<G>;
|
||||
|
||||
/// 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
|
||||
/// 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<G> {
|
||||
let new_nodes = self.render();
|
||||
|
||||
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 {
|
||||
fn render(&self) -> G {
|
||||
G::text_node(&format!("{}", self))
|
||||
fn render(&self) -> Vec<G> {
|
||||
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
|
||||
|
||||
node.update_inner_text(&format!("{}", self));
|
||||
node.first()
|
||||
.unwrap()
|
||||
.update_inner_text(&format!("{}", self));
|
||||
|
||||
node.clone()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
node.to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
impl<G: GenericNode> Render<G> for TemplateResult<G> {
|
||||
fn render(&self) -> G {
|
||||
self.node.clone()
|
||||
fn render(&self) -> Vec<G> {
|
||||
self.into_iter().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
"\
|
||||
<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");
|
||||
}
|
||||
|
|
|
@ -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#"<div id="test-container"></div>"#)
|
||||
.insert_adjacent_html(
|
||||
"beforeend",
|
||||
r#"<test-container id="test-container"></test-container>"#,
|
||||
)
|
||||
.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::<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");
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
"\
|
||||
<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");
|
||||
}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
comment_width = 100
|
||||
wrap_comments = true
|
Loading…
Reference in New Issue