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,
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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.
|
/// 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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
/// ```
|
/// ```
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
//! 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
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");
|
||||||
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
comment_width = 100
|
||||||
|
wrap_comments = true
|
Loading…
Reference in New Issue