rust-dominator/examples/todomvc/src/lib.rs

434 lines
17 KiB
Rust
Raw Normal View History

2018-02-21 01:37:09 -05:00
use std::rc::Rc;
2018-03-08 17:08:30 -05:00
use std::cell::Cell;
2018-03-18 14:49:40 -04:00
use wasm_bindgen::prelude::*;
use serde_derive::{Serialize, Deserialize};
use web_sys::{window, HtmlElement, Storage};
use futures_signals::map_ref;
2018-11-01 07:40:31 -04:00
use futures_signals::signal::{Signal, SignalExt, Mutable};
use futures_signals::signal_vec::{SignalVecExt, MutableVec};
use dominator::{Dom, text, routing, html, clone, events};
fn local_storage() -> Storage {
window().unwrap_throw().local_storage().unwrap_throw().unwrap_throw()
}
// TODO make this more efficient
#[inline]
fn trim(input: &str) -> Option<String> {
let trimmed = input.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_owned())
}
}
2018-02-21 01:37:09 -05:00
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2018-03-08 17:08:30 -05:00
enum Filter {
Active,
Completed,
All,
}
2018-02-21 01:37:09 -05:00
2018-11-01 07:40:31 -04:00
impl Filter {
fn signal() -> impl Signal<Item = Self> {
routing::url().map(|url| {
match url.hash().as_str() {
"#/active" => Filter::Active,
"#/completed" => Filter::Completed,
_ => Filter::All,
}
})
}
#[inline]
2018-11-01 07:40:31 -04:00
fn button(kind: Self) -> Dom {
let url = match kind {
Filter::Active => "#/active",
Filter::Completed => "#/completed",
Filter::All => "#/",
};
let text = match kind {
Filter::Active => "Active",
Filter::Completed => "Completed",
Filter::All => "All",
};
routing::link(url, |dom| { dom
.class_signal("selected", Self::signal()
.map(clone!(kind => move |filter| filter == kind)))
.text(text)
})
}
}
2018-11-01 07:40:31 -04:00
#[derive(Debug, Serialize, Deserialize)]
2018-03-08 17:08:30 -05:00
struct Todo {
id: u32,
title: Mutable<String>,
completed: Mutable<bool>,
#[serde(skip)]
2018-03-08 17:08:30 -05:00
editing: Mutable<Option<String>>,
}
2018-02-21 01:37:09 -05:00
2018-11-01 07:40:31 -04:00
#[derive(Debug, Serialize, Deserialize)]
2018-03-08 17:08:30 -05:00
struct State {
todo_id: Cell<u32>,
#[serde(skip)]
2018-03-08 17:08:30 -05:00
new_todo_title: Mutable<String>,
todo_list: MutableVec<Rc<Todo>>,
2018-03-08 17:08:30 -05:00
}
2018-02-21 01:37:09 -05:00
impl State {
fn new() -> Self {
State {
todo_id: Cell::new(0),
new_todo_title: Mutable::new("".to_owned()),
todo_list: MutableVec::new(),
}
}
2018-03-08 18:18:28 -05:00
fn remove_todo(&self, todo: &Todo) {
// TODO make this more efficient ?
self.todo_list.lock_mut().retain(|x| x.id != todo.id);
2018-03-08 18:18:28 -05:00
}
fn deserialize() -> Self {
local_storage()
.get_item("todos-rust-dominator")
.unwrap_throw()
2018-11-01 07:40:31 -04:00
.and_then(|state_json| {
serde_json::from_str(state_json.as_str()).ok()
})
.unwrap_or_else(State::new)
}
fn serialize(&self) {
let state_json = serde_json::to_string(self).unwrap_throw();
local_storage().set_item("todos-rust-dominator", state_json.as_str()).unwrap_throw();
2018-03-08 17:08:30 -05:00
}
}
2018-02-23 21:50:03 -05:00
2018-03-08 17:08:30 -05:00
#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
#[cfg(debug_assertions)]
console_error_panic_hook::set_once();
2018-03-08 17:08:30 -05:00
2018-02-21 01:37:09 -05:00
let state = Rc::new(State::deserialize());
2018-03-08 17:08:30 -05:00
dominator::append_dom(dominator::body(),
2018-03-08 17:08:30 -05:00
html!("section", {
.class("todoapp")
.children(&mut [
2018-03-08 17:08:30 -05:00
html!("header", {
.class("header")
.children(&mut [
html!("h1", {
.text("todos")
}),
2018-03-08 17:08:30 -05:00
html!("input", {
.focused(true)
.class("new-todo")
.attribute("placeholder", "What needs to be done?")
2018-03-08 17:08:30 -05:00
.property_signal("value", state.new_todo_title.signal_cloned())
2018-03-08 17:08:30 -05:00
.event(clone!(state => move |event: events::Input| {
state.new_todo_title.set_neq(event.value().unwrap_throw());
}))
2018-03-08 17:08:30 -05:00
.event(clone!(state => move |event: events::KeyDown| {
2018-03-08 17:08:30 -05:00
if event.key() == "Enter" {
event.prevent_default();
2018-06-12 03:06:51 -04:00
let trimmed = trim(&state.new_todo_title.lock_ref());
2018-03-17 09:38:01 -04:00
if let Some(title) = trimmed {
2018-07-14 17:36:50 -04:00
state.new_todo_title.set_neq("".to_owned());
2018-03-17 09:38:01 -04:00
2018-03-08 17:08:30 -05:00
let id = state.todo_id.get();
state.todo_id.set(id + 1);
state.todo_list.lock_mut().push_cloned(Rc::new(Todo {
2018-03-08 17:08:30 -05:00
id: id,
title: Mutable::new(title),
completed: Mutable::new(false),
editing: Mutable::new(None),
}));
state.serialize();
2018-03-08 17:08:30 -05:00
}
}
}))
2018-03-08 17:08:30 -05:00
}),
])
2018-03-08 17:08:30 -05:00
}),
html!("section", {
.class("main")
2018-03-08 17:08:30 -05:00
// Hide if it doesn't have any todos.
.visible_signal(state.todo_list.signal_vec_cloned()
2018-03-08 17:08:30 -05:00
.len()
.map(|len| len > 0))
2018-03-08 17:08:30 -05:00
.children(&mut [
2018-03-08 17:08:30 -05:00
html!("input", {
.class("toggle-all")
.attribute("id", "toggle-all")
.attribute("type", "checkbox")
2018-03-08 17:08:30 -05:00
.property_signal("checked", state.todo_list.signal_vec_cloned()
2018-03-08 17:08:30 -05:00
.map_signal(|todo| todo.completed.signal())
2018-03-12 20:09:30 -04:00
.filter(|completed| !completed)
2018-03-08 17:08:30 -05:00
.len()
.map(|len| len != 0))
2018-03-08 17:08:30 -05:00
.event(clone!(state => move |event: events::Change| {
// Toggles the boolean
let checked = !event.checked().unwrap_throw();
2018-03-08 17:08:30 -05:00
2018-06-22 14:08:04 -04:00
{
let todo_list = state.todo_list.lock_ref();
2018-06-22 14:08:04 -04:00
for todo in todo_list.iter() {
2018-07-14 17:36:50 -04:00
todo.completed.set_neq(checked);
}
2018-06-22 14:08:04 -04:00
}
state.serialize();
}))
2018-03-08 17:08:30 -05:00
}),
2018-03-08 18:18:28 -05:00
2018-03-08 17:08:30 -05:00
html!("label", {
.attribute("for", "toggle-all")
.text("Mark all as complete")
2018-03-08 17:08:30 -05:00
}),
2018-03-08 18:18:28 -05:00
2018-03-08 17:08:30 -05:00
html!("ul", {
.class("todo-list")
2018-03-08 17:08:30 -05:00
.children_signal_vec(state.todo_list.signal_vec_cloned()
2018-03-08 18:18:28 -05:00
.map(clone!(state => move |todo| {
html!("li", {
.class_signal("editing", todo.editing.signal_cloned()
.map(|x| x.is_some()))
2018-03-08 18:18:28 -05:00
.class_signal("completed", todo.completed.signal())
2018-03-08 18:18:28 -05:00
.visible_signal(map_ref!(
2018-11-01 07:40:31 -04:00
let filter = Filter::signal(),
2018-03-08 18:18:28 -05:00
let completed = todo.completed.signal() =>
match *filter {
2018-03-08 18:18:28 -05:00
Filter::Active => !completed,
Filter::Completed => *completed,
2018-03-08 18:18:28 -05:00
Filter::All => true,
}
)
.dedupe())
2018-03-08 18:18:28 -05:00
.children(&mut [
2018-03-08 18:18:28 -05:00
html!("div", {
.class("view")
.children(&mut [
2018-03-08 18:18:28 -05:00
html!("input", {
.attribute("type", "checkbox")
.class("toggle")
2018-03-08 18:18:28 -05:00
.property_signal("checked", todo.completed.signal())
2018-03-08 18:18:28 -05:00
.event(clone!(state, todo => move |event: events::Change| {
todo.completed.set_neq(event.checked().unwrap_throw());
2018-03-08 18:18:28 -05:00
state.serialize();
}))
2018-03-08 18:18:28 -05:00
}),
html!("label", {
.event(clone!(todo => move |_: events::DoubleClick| {
2018-07-14 17:36:50 -04:00
todo.editing.set_neq(Some(todo.title.get_cloned()));
}))
2018-03-08 18:18:28 -05:00
.text_signal(todo.title.signal_cloned())
2018-03-08 18:18:28 -05:00
}),
html!("button", {
.class("destroy")
.event(clone!(state, todo => move |_: events::Click| {
2018-03-08 18:18:28 -05:00
state.remove_todo(&todo);
state.serialize();
}))
2018-03-08 18:18:28 -05:00
}),
])
2018-03-08 18:18:28 -05:00
}),
2018-03-08 17:08:30 -05:00
2018-03-08 18:18:28 -05:00
html!("input", {
.class("edit")
2018-03-08 17:08:30 -05:00
.property_signal("value", todo.editing.signal_cloned()
.map(|x| x.unwrap_or_else(|| "".to_owned())))
2018-03-08 17:08:30 -05:00
.visible_signal(todo.editing.signal_cloned()
.map(|x| x.is_some()))
2018-03-08 17:08:30 -05:00
2018-03-08 18:18:28 -05:00
// TODO dedupe this somehow ?
.focused_signal(todo.editing.signal_cloned()
.map(|x| x.is_some()))
2018-03-08 17:08:30 -05:00
.event(clone!(todo => move |event: events::KeyDown| {
match event.key().as_str() {
"Enter" => {
event.dyn_target::<HtmlElement>().unwrap_throw().blur();
},
"Escape" => {
todo.editing.set_neq(None);
},
_ => {}
2018-03-08 17:08:30 -05:00
}
}))
.event(clone!(todo => move |event: events::Input| {
todo.editing.set_neq(Some(event.value().unwrap_throw()));
}))
2018-03-08 18:18:28 -05:00
2018-03-17 09:38:01 -04:00
// TODO global_event ?
.event(clone!(state, todo => move |_: events::Blur| {
2018-03-08 18:18:28 -05:00
if let Some(title) = todo.editing.replace(None) {
2018-03-17 09:38:01 -04:00
if let Some(title) = trim(&title) {
2018-07-14 17:36:50 -04:00
todo.title.set_neq(title);
2018-03-08 18:18:28 -05:00
} else {
state.remove_todo(&todo);
}
state.serialize();
}
}))
2018-03-08 18:18:28 -05:00
}),
])
2018-03-08 18:18:28 -05:00
})
})))
2018-03-08 17:08:30 -05:00
}),
])
2018-02-25 06:58:20 -05:00
}),
2018-03-08 17:08:30 -05:00
html!("footer", {
.class("footer")
2018-03-08 17:08:30 -05:00
// Hide if it doesn't have any todos.
.visible_signal(state.todo_list.signal_vec_cloned()
2018-03-08 18:18:28 -05:00
.len()
.map(|len| len > 0))
2018-03-08 17:08:30 -05:00
.children(&mut [
2018-03-08 17:08:30 -05:00
html!("span", {
.class("todo-count")
2018-03-08 18:18:28 -05:00
.children_signal_vec(state.todo_list.signal_vec_cloned()
2018-03-08 18:18:28 -05:00
.map_signal(|todo| todo.completed.signal())
2018-03-12 20:09:30 -04:00
.filter(|completed| !completed)
2018-03-08 18:18:28 -05:00
.len()
// TODO make this more efficient
.map(|len| {
vec![
html!("strong", {
.text(&len.to_string())
}),
2018-03-08 18:18:28 -05:00
text(if len == 1 {
" item left"
} else {
" items left"
}),
]
})
.to_signal_vec())
2018-03-08 17:08:30 -05:00
}),
html!("ul", {
.class("filters")
.children(&mut [
2018-11-01 07:40:31 -04:00
html!("li", {
.children(&mut [
Filter::button(Filter::All),
])
}),
html!("li", {
.children(&mut [
Filter::button(Filter::Active),
])
}),
html!("li", {
.children(&mut [
Filter::button(Filter::Completed),
])
}),
])
2018-03-08 17:08:30 -05:00
}),
html!("button", {
.class("clear-completed")
2018-02-25 06:58:20 -05:00
2018-03-08 17:08:30 -05:00
// Hide if it doesn't have any completed items.
.visible_signal(state.todo_list.signal_vec_cloned()
2018-03-08 17:08:30 -05:00
.map_signal(|todo| todo.completed.signal())
2018-03-15 06:54:18 -04:00
.filter(|completed| *completed)
2018-03-08 17:08:30 -05:00
.len()
.map(|len| len > 0))
2018-02-21 01:37:09 -05:00
.event(clone!(state => move |_: events::Click| {
state.todo_list.lock_mut().retain(|todo| todo.completed.get() == false);
state.serialize();
}))
2018-03-08 17:08:30 -05:00
.text("Clear completed")
2018-03-08 17:08:30 -05:00
}),
])
2018-02-25 06:58:20 -05:00
}),
])
2018-03-08 17:08:30 -05:00
}),
);
dominator::append_dom(dominator::body(),
2018-03-08 17:08:30 -05:00
html!("footer", {
.class("info")
.children(&mut [
html!("p", {
.text("Double-click to edit a todo")
}),
2018-11-01 07:40:31 -04:00
html!("p", {
.children(&mut [
text("Created by "),
html!("a", {
.attribute("href", "https://github.com/Pauan")
.text("Pauan")
}),
])
}),
html!("p", {
.children(&mut [
text("Part of "),
html!("a", {
.attribute("href", "http://todomvc.com")
.text("TodoMVC")
}),
])
}),
])
2018-03-08 17:08:30 -05:00
}),
2018-02-21 01:37:09 -05:00
);
Ok(())
2018-02-21 01:37:09 -05:00
}