#[macro_use] extern crate stdweb; #[macro_use] extern crate dominator; #[macro_use] extern crate futures_signals; #[macro_use] extern crate serde_derive; extern crate serde; extern crate serde_json; use std::rc::Rc; use std::cell::Cell; // TODO replace most of these with dominator use stdweb::web::{window, document}; use stdweb::web::event::{InputEvent, ClickEvent, HashChangeEvent, KeyDownEvent, ChangeEvent, DoubleClickEvent, BlurEvent}; use stdweb::web::html_element::InputElement; use stdweb::web::HtmlElement; use stdweb::unstable::TryInto; use stdweb::traits::*; use futures_signals::signal::{SignalExt, Mutable}; use futures_signals::signal_vec::{SignalVecExt, MutableVec}; use dominator::{Dom, text, text_signal}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Filter { Active, Completed, All, } impl Default for Filter { #[inline] fn default() -> Self { Filter::All } } #[derive(Serialize, Deserialize)] struct Todo { id: u32, title: Mutable, completed: Mutable, #[serde(skip)] editing: Mutable>, } #[derive(Serialize, Deserialize)] struct State { todo_id: Cell, #[serde(skip)] new_todo_title: Mutable, todo_list: MutableVec>, #[serde(skip)] filter: Mutable, } impl State { fn new() -> Self { State { todo_id: Cell::new(0), new_todo_title: Mutable::new("".to_owned()), todo_list: MutableVec::new(), filter: Mutable::new(Filter::All), } } fn remove_todo(&self, todo: &Todo) { // TODO make this more efficient ? self.todo_list.retain(|x| x.id != todo.id); } fn update_filter(&self) { let hash = document().location().unwrap().hash().unwrap(); self.filter.set_neq(match hash.as_str() { "#/active" => Filter::Active, "#/completed" => Filter::Completed, _ => Filter::All, }); } fn deserialize() -> Self { window().local_storage().get("todos-rust-dominator").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(); window().local_storage().insert("todos-rust-dominator", state_json.as_str()).unwrap(); } } // TODO make this more efficient #[inline] fn trim(input: &str) -> Option { let trimmed = input.trim(); if trimmed.is_empty() { None } else { Some(trimmed.to_owned()) } } #[inline] fn get_value(event: &InputEvent) -> String { let target: InputElement = event.target().unwrap().try_into().unwrap(); target.raw_value() } #[inline] fn get_checked(event: &ChangeEvent) -> bool { js!( return @{&event.target()}.checked; ).try_into().unwrap() } #[inline] fn simple(kind: &str, children: &mut [Dom]) -> Dom { html!(kind, { .children(children) }) } #[inline] fn link(href: &str, t: &str) -> Dom { html!("a", { .attribute("href", href) .children(&mut [ text(t), ]) }) } fn filter_button(state: Rc, kind: Filter) -> Dom { html!("a", { .class_signal("selected", state.filter.signal() .map(clone!(kind => move |filter| filter == kind))) .attribute("href", match kind { Filter::Active => "#/active", Filter::Completed => "#/completed", Filter::All => "#/", }) .children(&mut [ text(match kind { Filter::Active => "Active", Filter::Completed => "Completed", Filter::All => "All", }) ]) }) } fn main() { let state = Rc::new(State::deserialize()); state.update_filter(); window().add_event_listener(clone!(state => move |_: HashChangeEvent| { state.update_filter(); })); let body = dominator::body(); dominator::append_dom(&body, html!("section", { .class("todoapp") .children(&mut [ html!("header", { .class("header") .children(&mut [ simple("h1", &mut [ text("todos"), ]), html!("input", { .focused(true) .class("new-todo") .attribute("placeholder", "What needs to be done?") .property_signal("value", state.new_todo_title.signal_cloned()) .event(clone!(state => move |event: InputEvent| { state.new_todo_title.set_neq(get_value(&event)); })) .event(clone!(state => move |event: KeyDownEvent| { if event.key() == "Enter" { event.prevent_default(); let trimmed = trim(&state.new_todo_title.lock_ref()); if let Some(title) = trimmed { state.new_todo_title.set_neq("".to_owned()); let id = state.todo_id.get(); state.todo_id.set(id + 1); state.todo_list.push_cloned(Rc::new(Todo { id: id, title: Mutable::new(title), completed: Mutable::new(false), editing: Mutable::new(None), })); state.serialize(); } } })) }), ]) }), html!("section", { .class("main") // Hide if it doesn't have any todos. .property_signal("hidden", state.todo_list.signal_vec_cloned() .len() .map(|len| len == 0)) .children(&mut [ html!("input", { .class("toggle-all") .attribute("id", "toggle-all") .attribute("type", "checkbox") .property_signal("checked", state.todo_list.signal_vec_cloned() .map_signal(|todo| todo.completed.signal()) .filter(|completed| !completed) .len() .map(|len| len != 0)) .event(clone!(state => move |event: ChangeEvent| { let checked = !get_checked(&event); { let todo_list = state.todo_list.lock_slice(); for todo in todo_list.iter() { todo.completed.set_neq(checked); } } state.serialize(); })) }), html!("label", { .attribute("for", "toggle-all") .children(&mut [ text("Mark all as complete"), ]) }), html!("ul", { .class("todo-list") .children_signal_vec(state.todo_list.signal_vec_cloned() .map(clone!(state => move |todo| { html!("li", { .class_signal("editing", todo.editing.signal_cloned() .map(|x| x.is_some())) .class_signal("completed", todo.completed.signal()) .property_signal("hidden", map_ref!( let filter = state.filter.signal(), let completed = todo.completed.signal() => match *filter { Filter::Active => !completed, Filter::Completed => *completed, Filter::All => true, } ) .dedupe_map(|show| !*show)) .children(&mut [ html!("div", { .class("view") .children(&mut [ html!("input", { .attribute("type", "checkbox") .class("toggle") .property_signal("checked", todo.completed.signal()) .event(clone!(state, todo => move |event: ChangeEvent| { todo.completed.set_neq(get_checked(&event)); state.serialize(); })) }), html!("label", { .event(clone!(todo => move |_: DoubleClickEvent| { todo.editing.set_neq(Some(todo.title.get_cloned())); })) .children(&mut [ text_signal(todo.title.signal_cloned()), ]) }), html!("button", { .class("destroy") .event(clone!(state, todo => move |_: ClickEvent| { state.remove_todo(&todo); state.serialize(); })) }), ]) }), html!("input", { .class("edit") .property_signal("value", todo.editing.signal_cloned() .map(|x| x.unwrap_or_else(|| "".to_owned()))) .property_signal("hidden", todo.editing.signal_cloned() .map(|x| x.is_none())) // TODO dedupe this somehow ? .focused_signal(todo.editing.signal_cloned() .map(|x| x.is_some())) .event(clone!(todo => move |event: KeyDownEvent| { let key = event.key(); if key == "Enter" { let element: HtmlElement = event.target().unwrap().try_into().unwrap(); element.blur(); } else if key == "Escape" { todo.editing.set_neq(None); } })) .event(clone!(todo => move |event: InputEvent| { todo.editing.set_neq(Some(get_value(&event))); })) // TODO global_event ? .event(clone!(state, todo => move |_: BlurEvent| { if let Some(title) = todo.editing.replace(None) { if let Some(title) = trim(&title) { todo.title.set_neq(title); } else { state.remove_todo(&todo); } state.serialize(); } })) }), ]) }) }))) }), ]) }), html!("footer", { .class("footer") // Hide if it doesn't have any todos. .property_signal("hidden", state.todo_list.signal_vec_cloned() .len() .map(|len| len == 0)) .children(&mut [ html!("span", { .class("todo-count") .children_signal_vec(state.todo_list.signal_vec_cloned() .map_signal(|todo| todo.completed.signal()) .filter(|completed| !completed) .len() // TODO make this more efficient .map(|len| { vec![ simple("strong", &mut [ text(&len.to_string()) ]), text(if len == 1 { " item left" } else { " items left" }), ] }) .to_signal_vec()) }), html!("ul", { .class("filters") .children(&mut [ simple("li", &mut [ filter_button(state.clone(), Filter::All), ]), simple("li", &mut [ filter_button(state.clone(), Filter::Active), ]), simple("li", &mut [ filter_button(state.clone(), Filter::Completed), ]), ]) }), html!("button", { .class("clear-completed") // Hide if it doesn't have any completed items. .property_signal("hidden", state.todo_list.signal_vec_cloned() .map_signal(|todo| todo.completed.signal()) .filter(|completed| *completed) .len() .map(|len| len == 0)) .event(clone!(state => move |_: ClickEvent| { state.todo_list.retain(|todo| todo.completed.get() == false); state.serialize(); })) .children(&mut [ text("Clear completed"), ]) }), ]) }), ]) }), ); dominator::append_dom(&body, html!("footer", { .class("info") .children(&mut [ simple("p", &mut [ text("Double-click to edit a todo"), ]), simple("p", &mut [ text("Created by "), link("https://github.com/Pauan", "Pauan"), ]), simple("p", &mut [ text("Part of "), link("http://todomvc.com", "TodoMVC"), ]), ]) }), ); }