Skip to content

Commit

Permalink
Lists in template! (#45)
Browse files Browse the repository at this point in the history
* wip

* Render::update_node
  • Loading branch information
lukechu10 authored Mar 14, 2021
1 parent 2711ccd commit ec53b84
Show file tree
Hide file tree
Showing 12 changed files with 213 additions and 29 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ members = [
"examples/components",
"examples/counter",
"examples/hello",
"examples/todomvc",
"docs",
]
1 change: 1 addition & 0 deletions examples/components/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
authors = ["Luke Chu <[email protected]>"]
edition = "2018"
name = "components"
publish = false
version = "0.1.0"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand Down
1 change: 1 addition & 0 deletions examples/counter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
authors = ["Luke Chu <[email protected]>"]
edition = "2018"
name = "counter"
publish = false
version = "0.1.0"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand Down
1 change: 1 addition & 0 deletions examples/hello/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
authors = ["Luke Chu <[email protected]>"]
edition = "2018"
name = "hello"
publish = false
version = "0.1.0"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand Down
19 changes: 19 additions & 0 deletions examples/todomvc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
authors = ["Luke Chu <[email protected]>"]
edition = "2018"
name = "todomvc"
publish = false
version = "0.1.0"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
console_error_panic_hook = "0.1.6"
console_log = "0.2.0"
log = "0.4.14"
maple-core = {path = "../../maple-core"}
wasm-bindgen = "0.2.71"

[dependencies.web-sys]
features = ["HtmlInputElement", "InputEvent"]
version = "0.3"
15 changes: 15 additions & 0 deletions examples/todomvc/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Todos</title>

<style>
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
</style>
</head>
<body></body>
</html>
52 changes: 52 additions & 0 deletions examples/todomvc/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#![allow(non_snake_case)]

use maple_core::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{Event, HtmlInputElement};

fn TodoItem(item: String) -> TemplateResult {
template! {
li { (item.clone()) }
}
}

fn App() -> TemplateResult {
let todos: Signal<Vec<String>> = Signal::new(Vec::new());

let value = Signal::new(String::new());

let handle_input = cloned!((value) => move |event: Event| {
let target: HtmlInputElement = event.target().unwrap().dyn_into().unwrap();
value.set(target.value());
});

let handle_click = cloned!((todos) => move |_| {
let mut tmp = todos.get().as_ref().clone();
tmp.push(value.get().as_ref().clone());

todos.set(tmp);
});

template! {
main {
h1 {
"todos"
}

input(placeholder="What needs to be done?", on:input=handle_input)
button(on:click=handle_click) { "Add todo" }

ul {
h1 { "Test" }
(todos.get().iter().map(|todo| template! { TodoItem(todo.clone()) }).collect::<TemplateList>())
}
}
}
}

fn main() {
console_error_panic_hook::set_once();
console_log::init_with_level(log::Level::Debug).unwrap();

render(|| template! { App() });
}
6 changes: 3 additions & 3 deletions maple-core-macro/src/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,13 @@ impl ToTokens for Element {
if let Some(children) = children {
for child in &children.body {
let quoted = match child {
HtmlTree::Component(component) => quote! {
HtmlTree::Component(component) => quote_spanned! { component.span()=>
::maple_core::internal::append(&element, &#component);
},
HtmlTree::Element(element) => quote! {
HtmlTree::Element(element) => quote_spanned! { element.span()=>
::maple_core::internal::append(&element, &#element);
},
HtmlTree::Text(text) => quote! {
HtmlTree::Text(text) => quote_spanned! { text.span()=>
::maple_core::internal::append_render(&element, ::std::boxed::Box::new(move || {
::std::boxed::Box::new(#text)
}));
Expand Down
18 changes: 9 additions & 9 deletions maple-core/src/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use web_sys::{DocumentFragment, Element, Event, Node};

use crate::prelude::*;

/// Create a new [`HtmlElement`] with the specified tag.
/// Create a new [`Element`] with the specified tag.
pub fn element(tag: &str) -> Element {
web_sys::window()
.unwrap()
Expand All @@ -29,7 +29,7 @@ pub fn fragment() -> DocumentFragment {
.create_document_fragment()
}

/// Create a new [`Text`] with the specified content.
/// Create a new [`Node`] with the specified text content.
pub fn text(value: impl Fn() -> String + 'static) -> Node {
let text_node = web_sys::window()
.unwrap()
Expand Down Expand Up @@ -80,20 +80,20 @@ pub fn append(element: &impl AsRef<Node>, child: &impl AsRef<Node>) {
}

/// Appends a [`dyn Render`](Render) to the `parent` node.
/// Node is created inside an effect.
/// Node is created inside an effect with [`Render::update_node`].
pub fn append_render(parent: &impl AsRef<Node>, child: Box<dyn Fn() -> Box<dyn Render>>) {
let node = create_effect_initial(move || {
let parent = parent.as_ref().clone();

let node = create_effect_initial(cloned!((parent) => move || {
let node = RefCell::new(child().render());

let effect = cloned!((node) => move || {
let new_node = child().render();
node.borrow().parent_element().unwrap().replace_child(&new_node, &node.borrow()).unwrap();

let new_node = child().update_node(&parent, &node.borrow());
*node.borrow_mut() = new_node;
});

(Rc::new(effect), node)
});
}));

parent.as_ref().append_child(&node.borrow()).unwrap();
parent.append_child(&node.borrow()).unwrap();
}
42 changes: 28 additions & 14 deletions maple-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub mod render;
use web_sys::Node;

use std::cell::RefCell;
use std::iter::FromIterator;
use std::rc::Rc;

/// The result of the `template!` macro. Should not be used directly.
Expand All @@ -25,6 +26,30 @@ pub struct TemplateResult {
node: Node,
}

impl TemplateResult {
/// Create a new `TemplateResult` from a [`Node`].
pub fn new(node: Node) -> Self {
Self { node }
}

pub fn inner_element(&self) -> Node {
self.node.clone()
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TemplateList {
templates: Vec<TemplateResult>,
}

impl FromIterator<TemplateResult> for TemplateList {
fn from_iter<T: IntoIterator<Item = TemplateResult>>(iter: T) -> Self {
Self {
templates: FromIterator::from_iter(iter),
}
}
}

/// Render a [`TemplateResult`] into the DOM.
pub fn render(template_result: impl FnOnce() -> TemplateResult + 'static) {
let window = web_sys::window().unwrap();
Expand All @@ -45,26 +70,15 @@ pub fn render(template_result: impl FnOnce() -> TemplateResult + 'static) {
GLOBAL_OWNERS.with(|global_owners| global_owners.borrow_mut().push(owner));
}

impl TemplateResult {
/// Create a new `TemplateResult` from an [`HtmlElement`].
pub fn new(node: Node) -> Self {
Self { node }
}

pub fn inner_element(&self) -> Node {
self.node.clone()
}
}

/// The maple prelude.
pub mod prelude {
pub use crate::cloned;
pub use crate::reactive::{
create_effect, create_effect_initial, create_memo, create_selector, create_selector_with,
Signal, StateHandle,
create_effect, create_effect_initial, create_memo, create_root, create_selector,
create_selector_with, Signal, StateHandle,
};
pub use crate::render::Render;
pub use crate::{render, TemplateResult};
pub use crate::{render, TemplateList, TemplateResult};

pub use maple_core_macro::template;
}
26 changes: 25 additions & 1 deletion maple-core/src/reactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,31 @@ use std::rc::Rc;
pub use effect::*;
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
/// ```
/// use maple_core::prelude::*;
///
/// let trigger = Signal::new(());
/// let counter = Signal::new(0);
///
/// let owner = create_root(cloned!((trigger, counter) => move || {
/// create_effect(move || {
/// trigger.get(); // subscribe to trigger
/// counter.set(*counter.get_untracked() + 1);
/// });
/// }));
///
/// assert_eq!(*counter.get(), 1);
///
/// trigger.set(());
/// assert_eq!(*counter.get(), 2);
///
/// drop(owner);
/// trigger.set(());
/// assert_eq!(*counter.get(), 2); // should not be updated because owner was dropped
/// ```
#[must_use = "create_root returns the owner of the effects created inside this scope"]
pub fn create_root(callback: impl FnOnce()) -> Rc<RefCell<Owner>> {
OWNER.with(|owner| {
Expand Down
60 changes: 58 additions & 2 deletions maple-core/src/render.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
use std::fmt;

use web_sys::Node;
use wasm_bindgen::JsCast;
use web_sys::{Element, Node, Text};

use crate::TemplateResult;
use crate::{internal::*, TemplateList};

/// Trait for describing how something should be rendered into DOM nodes.
pub trait Render {
/// Called during the initial render when creating the DOM nodes. Should return a [`Node`].
fn render(&self) -> Node;

/// 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.
///
/// 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: &Node, node: &Node) -> Node {
let new_node = self.render();
parent.replace_child(&new_node, &node).unwrap();
new_node
}
}

impl<T: fmt::Display> Render for T {
impl<T: fmt::Display + ?Sized> Render for T {
fn render(&self) -> Node {
web_sys::window()
.unwrap()
Expand All @@ -17,6 +33,46 @@ impl<T: fmt::Display> Render for T {
.create_text_node(&format!("{}", self))
.into()
}

fn update_node(&self, _parent: &Node, node: &Node) -> Node {
// replace `textContent` of `node` instead of recreating
node.clone()
.dyn_into::<Text>()
.unwrap()
.set_text_content(Some(&format!("{}", self)));

node.clone()
}
}

impl Render for TemplateList {
fn render(&self) -> Node {
let fragment = fragment();

for item in self.templates.clone().into_iter() {
append_render(
&fragment,
Box::new(move || {
let item = item.clone();
Box::new(item)
}),
);
}

fragment.into()
}

fn update_node(&self, parent: &Node, node: &Node) -> Node {
while let Some(child) = parent.last_child() {
child.dyn_into::<Element>().unwrap().remove();
}

for item in self.templates.clone().into_iter() {
parent.append_child(&item.render()).unwrap();
}

node.clone()
}
}

impl Render for TemplateResult {
Expand Down

0 comments on commit ec53b84

Please sign in to comment.