From 314ecf9eeca4151ad6413642e7accc8a7de603c6 Mon Sep 17 00:00:00 2001 From: Pauan Date: Fri, 28 May 2021 15:10:10 +0200 Subject: [PATCH] Modernizing todomvc example --- examples/todomvc/Cargo.toml | 19 ++-- examples/todomvc/package.json | 9 +- examples/todomvc/rollup.config.js | 4 +- examples/todomvc/src/app.rs | 153 ++++++++++++++++++++---------- examples/todomvc/src/lib.rs | 1 - examples/todomvc/src/routing.rs | 34 ------- examples/todomvc/src/todo.rs | 72 +++++++------- examples/todomvc/src/util.rs | 5 +- 8 files changed, 162 insertions(+), 135 deletions(-) delete mode 100644 examples/todomvc/src/routing.rs diff --git a/examples/todomvc/Cargo.toml b/examples/todomvc/Cargo.toml index d3992c7..95c3ce9 100644 --- a/examples/todomvc/Cargo.toml +++ b/examples/todomvc/Cargo.toml @@ -14,21 +14,24 @@ lto = true [lib] crate-type = ["cdylib"] +[workspace] + [dependencies] -console_error_panic_hook = "0.1.5" -dominator = "0.5.0" -futures-signals = "0.3.0" -wasm-bindgen = "0.2.48" -serde_json = "1.0.10" -serde_derive = "1.0.27" +console_error_panic_hook = "0.1.6" +dominator = "0.5.18" +wasm-bindgen = "0.2.74" +futures-signals = "0.3.20" +serde_json = "1.0.64" +serde_derive = "1.0.126" [dependencies.serde] -version = "1.0.27" +version = "1.0.126" features = ["rc"] [dependencies.web-sys] -version = "0.3.22" +version = "0.3.51" features = [ + "HtmlInputElement", "Storage", "Url", ] diff --git a/examples/todomvc/package.json b/examples/todomvc/package.json index 41cd1da..e08d8c7 100644 --- a/examples/todomvc/package.json +++ b/examples/todomvc/package.json @@ -8,10 +8,11 @@ "start": "rimraf dist/js && rollup --config --watch" }, "devDependencies": { - "@wasm-tool/rollup-plugin-rust": "^1.0.0", + "@wasm-tool/rollup-plugin-rust": "^1.0.6", "rimraf": "^3.0.2", - "rollup": "^1.31.0", - "rollup-plugin-livereload": "^1.2.0", - "rollup-plugin-serve": "^1.0.1" + "rollup": "^2.50.2", + "rollup-plugin-livereload": "^2.0.0", + "rollup-plugin-serve": "^1.1.0", + "rollup-plugin-terser": "^7.0.2" } } diff --git a/examples/todomvc/rollup.config.js b/examples/todomvc/rollup.config.js index 97166a3..bdc5904 100644 --- a/examples/todomvc/rollup.config.js +++ b/examples/todomvc/rollup.config.js @@ -1,6 +1,7 @@ import rust from "@wasm-tool/rollup-plugin-rust"; import serve from "rollup-plugin-serve"; import livereload from "rollup-plugin-livereload"; +import { terser } from "rollup-plugin-terser"; const is_watch = !!process.env.ROLLUP_WATCH; @@ -16,7 +17,6 @@ export default { plugins: [ rust({ serverPath: "js/", - debug: false, }), is_watch && serve({ @@ -25,5 +25,7 @@ export default { }), is_watch && livereload("dist"), + + !is_watch && terser(), ], }; diff --git a/examples/todomvc/src/app.rs b/examples/todomvc/src/app.rs index 2600d93..17adece 100644 --- a/examples/todomvc/src/app.rs +++ b/examples/todomvc/src/app.rs @@ -1,17 +1,51 @@ -use std::rc::Rc; +use std::sync::Arc; use std::cell::Cell; - use wasm_bindgen::prelude::*; +use web_sys::{Url, HtmlInputElement}; use serde_derive::{Serialize, Deserialize}; use futures_signals::signal::{Signal, SignalExt, Mutable}; use futures_signals::signal_vec::{SignalVec, SignalVecExt, MutableVec}; -use dominator::{Dom, text_signal, html, clone, events, link}; +use dominator::{Dom, text_signal, html, clone, events, link, with_node, routing}; use crate::todo::Todo; -use crate::routing::Route; use crate::util::{trim, local_storage}; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Route { + Active, + Completed, + All, +} + +impl Route { + // This could use more advanced URL parsing, but it isn't needed + pub fn from_url(url: &str) -> Self { + let url = Url::new(&url).unwrap_throw(); + match url.hash().as_str() { + "#/active" => Route::Active, + "#/completed" => Route::Completed, + _ => Route::All, + } + } + + pub fn to_url(&self) -> &'static str { + match self { + Route::Active => "#/active", + Route::Completed => "#/completed", + Route::All => "#/", + } + } +} + +impl Default for Route { + fn default() -> Self { + // Create the Route based on the current URL + Self::from_url(&routing::url().lock_ref()) + } +} + + #[derive(Debug, Serialize, Deserialize)] pub struct App { todo_id: Cell, @@ -19,19 +53,23 @@ pub struct App { #[serde(skip)] new_todo_title: Mutable, - todo_list: MutableVec>, + todo_list: MutableVec>, + + #[serde(skip)] + route: Mutable, } impl App { - fn new() -> Rc { - Rc::new(App { + fn new() -> Arc { + Arc::new(App { todo_id: Cell::new(0), new_todo_title: Mutable::new("".to_owned()), todo_list: MutableVec::new(), + route: Mutable::new(Route::default()), }) } - pub fn deserialize() -> Rc { + pub fn deserialize() -> Arc { local_storage() .get_item("todos-rust-dominator") .unwrap_throw() @@ -49,23 +87,27 @@ impl App { .unwrap_throw(); } + pub fn route(&self) -> impl Signal { + self.route.signal() + } + fn create_new_todo(&self) { - let trimmed = trim(&self.new_todo_title.lock_ref()); + let mut title = self.new_todo_title.lock_mut(); // Only create a new Todo if the text box is not empty - if let Some(title) = trimmed { - self.new_todo_title.set_neq("".to_owned()); - + if let Some(trimmed) = trim(&title) { let id = self.todo_id.get(); self.todo_id.set(id + 1); - self.todo_list.lock_mut().push_cloned(Todo::new(id, title)); + + self.todo_list.lock_mut().push_cloned(Todo::new(id, trimmed.to_string())); + + *title = "".to_string(); self.serialize(); } } pub fn remove_todo(&self, todo: &Todo) { - // TODO make this more efficient ? self.todo_list.lock_mut().retain(|x| **x != *todo); } @@ -98,7 +140,14 @@ impl App { .len() } - fn render_header(app: Rc) -> Dom { + fn has_todos(&self) -> impl Signal { + self.todo_list.signal_vec_cloned() + .len() + .map(|len| len > 0) + .dedupe() + } + + fn render_header(app: Arc) -> Dom { html!("header", { .class("header") .children(&mut [ @@ -106,20 +155,22 @@ impl App { .text("todos") }), - html!("input", { + html!("input" => HtmlInputElement, { .focused(true) .class("new-todo") - .attribute("placeholder", "What needs to be done?") - .property_signal("value", app.new_todo_title.signal_cloned()) + .attr("placeholder", "What needs to be done?") + .prop_signal("value", app.new_todo_title.signal_cloned()) - .event(clone!(app => move |event: events::Input| { - app.new_todo_title.set_neq(event.value().unwrap_throw()); - })) + .with_node!(element => { + .event(clone!(app => move |_: events::Input| { + app.new_todo_title.set_neq(element.value()); + })) + }) .event_preventable(clone!(app => move |event: events::KeyDown| { if event.key() == "Enter" { event.prevent_default(); - Self::create_new_todo(&app); + app.create_new_todo(); } })) }), @@ -127,30 +178,28 @@ impl App { }) } - fn render_main(app: Rc) -> Dom { + fn render_main(app: Arc) -> Dom { html!("section", { .class("main") - // Hide if it doesn't have any todos. - .visible_signal(app.todo_list.signal_vec_cloned() - .len() - .map(|len| len > 0)) + .visible_signal(app.has_todos()) .children(&mut [ - html!("input", { + html!("input" => HtmlInputElement, { .class("toggle-all") - .attribute("id", "toggle-all") - .attribute("type", "checkbox") - .property_signal("checked", app.not_completed_len().map(|len| len == 0)) + .attr("id", "toggle-all") + .attr("type", "checkbox") + .prop_signal("checked", app.not_completed_len().map(|len| len == 0).dedupe()) - .event(clone!(app => move |event: events::Change| { - let checked = event.checked().unwrap_throw(); - app.set_all_todos_completed(checked); - })) + .with_node!(element => { + .event(clone!(app => move |_: events::Change| { + app.set_all_todos_completed(element.checked()); + })) + }) }), html!("label", { - .attribute("for", "toggle-all") + .attr("for", "toggle-all") .text("Mark all as complete") }), @@ -163,25 +212,22 @@ impl App { }) } - fn render_button(text: &str, route: Route) -> Dom { + fn render_button(app: &App, text: &str, route: Route) -> Dom { html!("li", { .children(&mut [ - link!(route.url(), { + link!(route.to_url(), { .text(text) - .class_signal("selected", Route::signal().map(move |x| x == route)) + .class_signal("selected", app.route().map(move |x| x == route)) }) ]) }) } - fn render_footer(app: Rc) -> Dom { + fn render_footer(app: Arc) -> Dom { html!("footer", { .class("footer") - // Hide if it doesn't have any todos. - .visible_signal(app.todo_list.signal_vec_cloned() - .len() - .map(|len| len > 0)) + .visible_signal(app.has_todos()) .children(&mut [ html!("span", { @@ -205,9 +251,9 @@ impl App { html!("ul", { .class("filters") .children(&mut [ - Self::render_button("All", Route::All), - Self::render_button("Active", Route::Active), - Self::render_button("Completed", Route::Completed), + Self::render_button(&app, "All", Route::All), + Self::render_button(&app, "Active", Route::Active), + Self::render_button(&app, "Completed", Route::Completed), ]) }), @@ -215,7 +261,7 @@ impl App { .class("clear-completed") // Show if there is at least one completed item. - .visible_signal(app.completed_len().map(|len| len > 0)) + .visible_signal(app.completed_len().map(|len| len > 0).dedupe()) .event(clone!(app => move |_: events::Click| { app.remove_all_completed_todos(); @@ -228,9 +274,18 @@ impl App { }) } - pub fn render(app: Rc) -> Dom { + pub fn render(app: Arc) -> Dom { html!("section", { .class("todoapp") + + // Update the Route when the URL changes + .future(routing::url() + .signal_ref(|url| Route::from_url(url)) + .for_each(clone!(app => move |route| { + app.route.set_neq(route); + async {} + }))) + .children(&mut [ Self::render_header(app.clone()), Self::render_main(app.clone()), diff --git a/examples/todomvc/src/lib.rs b/examples/todomvc/src/lib.rs index a7ddb4a..13f4468 100644 --- a/examples/todomvc/src/lib.rs +++ b/examples/todomvc/src/lib.rs @@ -2,7 +2,6 @@ use wasm_bindgen::prelude::*; use crate::app::App; mod util; -mod routing; mod todo; mod app; diff --git a/examples/todomvc/src/routing.rs b/examples/todomvc/src/routing.rs deleted file mode 100644 index ca5514a..0000000 --- a/examples/todomvc/src/routing.rs +++ /dev/null @@ -1,34 +0,0 @@ -use wasm_bindgen::prelude::*; -use web_sys::Url; -use futures_signals::signal::{Signal, SignalExt}; -use dominator::routing; - - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Route { - Active, - Completed, - All, -} - -impl Route { - pub fn signal() -> impl Signal { - routing::url() - .signal_ref(|url| Url::new(&url).unwrap_throw()) - .map(|url| { - match url.hash().as_str() { - "#/active" => Route::Active, - "#/completed" => Route::Completed, - _ => Route::All, - } - }) - } - - pub fn url(&self) -> &'static str { - match self { - Route::Active => "#/active", - Route::Completed => "#/completed", - Route::All => "#/", - } - } -} diff --git a/examples/todomvc/src/todo.rs b/examples/todomvc/src/todo.rs index 8182a01..2ef193c 100644 --- a/examples/todomvc/src/todo.rs +++ b/examples/todomvc/src/todo.rs @@ -1,14 +1,13 @@ -use std::rc::Rc; - +use std::sync::Arc; use wasm_bindgen::prelude::*; use serde_derive::{Serialize, Deserialize}; use futures_signals::map_ref; -use futures_signals::signal::{SignalExt, Mutable}; +use futures_signals::signal::{Signal, SignalExt, Mutable}; use dominator::{Dom, html, clone, events, with_node}; +use web_sys::HtmlInputElement; use crate::util::trim; -use crate::app::App; -use crate::routing::Route; +use crate::app::{App, Route}; #[derive(Debug, Serialize, Deserialize)] @@ -22,8 +21,8 @@ pub struct Todo { } impl Todo { - pub fn new(id: u32, title: String) -> Rc { - Rc::new(Self { + pub fn new(id: u32, title: String) -> Arc { + Arc::new(Self { id: id, title: Mutable::new(title), completed: Mutable::new(false), @@ -41,6 +40,22 @@ impl Todo { app.serialize(); } + fn is_visible(&self, app: &App) -> impl Signal { + (map_ref! { + let route = app.route(), + let completed = self.completed.signal() => + match *route { + Route::Active => !completed, + Route::Completed => *completed, + Route::All => true, + } + }).dedupe() + } + + fn is_editing(&self) -> impl Signal { + self.editing.signal_ref(|x| x.is_some()).dedupe() + } + fn cancel_editing(&self) { self.editing.set_neq(None); } @@ -48,7 +63,7 @@ impl Todo { fn done_editing(&self, app: &App) { if let Some(title) = self.editing.replace(None) { if let Some(title) = trim(&title) { - self.title.set_neq(title); + self.title.set_neq(title.to_string()); } else { app.remove_todo(&self); @@ -58,35 +73,27 @@ impl Todo { } } - pub fn render(todo: Rc, app: Rc) -> Dom { + pub fn render(todo: Arc, app: Arc) -> Dom { html!("li", { - .class_signal("editing", todo.editing.signal_cloned().map(|x| x.is_some())) + .class_signal("editing", todo.is_editing()) .class_signal("completed", todo.completed.signal()) - .visible_signal(map_ref!( - let route = Route::signal(), - let completed = todo.completed.signal() => - match *route { - Route::Active => !completed, - Route::Completed => *completed, - Route::All => true, - } - ) - .dedupe()) + .visible_signal(todo.is_visible(&app)) .children(&mut [ html!("div", { .class("view") .children(&mut [ - html!("input", { - .attribute("type", "checkbox") + html!("input" => HtmlInputElement, { .class("toggle") + .attr("type", "checkbox") + .prop_signal("checked", todo.completed.signal()) - .property_signal("checked", todo.completed.signal()) - - .event(clone!(todo, app => move |event: events::Change| { - todo.set_completed(&app, event.checked().unwrap_throw()); - })) + .with_node!(element => { + .event(clone!(todo, app => move |_: events::Change| { + todo.set_completed(&app, element.checked()); + })) + }) }), html!("label", { @@ -109,15 +116,11 @@ impl Todo { html!("input", { .class("edit") - .property_signal("value", todo.editing.signal_cloned() + .prop_signal("value", todo.editing.signal_cloned() .map(|x| x.unwrap_or_else(|| "".to_owned()))) - .visible_signal(todo.editing.signal_cloned() - .map(|x| x.is_some())) - - // TODO dedupe this somehow ? - .focused_signal(todo.editing.signal_cloned() - .map(|x| x.is_some())) + .visible_signal(todo.is_editing()) + .focused_signal(todo.is_editing()) .with_node!(element => { .event(clone!(todo => move |event: events::KeyDown| { @@ -137,7 +140,6 @@ impl Todo { todo.editing.set_neq(Some(event.value().unwrap_throw())); })) - // TODO global_event ? .event(clone!(todo, app => move |_: events::Blur| { todo.done_editing(&app); })) diff --git a/examples/todomvc/src/util.rs b/examples/todomvc/src/util.rs index 7f4ad13..1b2bd27 100644 --- a/examples/todomvc/src/util.rs +++ b/examples/todomvc/src/util.rs @@ -6,15 +6,14 @@ pub fn local_storage() -> Storage { window().unwrap_throw().local_storage().unwrap_throw().unwrap_throw() } -// TODO make this more efficient #[inline] -pub fn trim(input: &str) -> Option { +pub fn trim(input: &str) -> Option<&str> { let trimmed = input.trim(); if trimmed.is_empty() { None } else { - Some(trimmed.to_owned()) + Some(trimmed) } }