2018-02-21 01:37:09 -05:00
|
|
|
#[macro_use]
|
|
|
|
extern crate stdweb;
|
|
|
|
#[macro_use]
|
|
|
|
extern crate dominator;
|
2018-02-25 06:58:20 -05:00
|
|
|
#[macro_use]
|
2018-03-18 12:11:43 -04:00
|
|
|
extern crate futures_signals;
|
2018-03-08 18:04:20 -05:00
|
|
|
#[macro_use]
|
|
|
|
extern crate serde_derive;
|
|
|
|
extern crate serde;
|
|
|
|
extern crate serde_json;
|
2018-02-25 06:58:20 -05:00
|
|
|
|
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
|
|
|
|
|
|
|
// TODO replace most of these with dominator
|
|
|
|
use stdweb::web::{window, document};
|
2018-03-08 17:08:30 -05:00
|
|
|
use stdweb::web::event::{InputEvent, ClickEvent, HashChangeEvent, KeyDownEvent, ChangeEvent, DoubleClickEvent, BlurEvent};
|
|
|
|
use stdweb::web::html_element::InputElement;
|
2018-03-29 21:48:20 -04:00
|
|
|
use stdweb::web::HtmlElement;
|
2018-03-08 17:08:30 -05:00
|
|
|
use stdweb::unstable::TryInto;
|
|
|
|
use stdweb::traits::*;
|
2018-02-21 01:37:09 -05:00
|
|
|
|
2018-04-25 19:01:52 -04:00
|
|
|
use futures_signals::signal::{SignalExt, Mutable};
|
|
|
|
use futures_signals::signal_vec::{SignalVecExt, MutableVec};
|
|
|
|
use dominator::{Dom, text, text_signal};
|
2018-02-21 01:37:09 -05:00
|
|
|
|
|
|
|
|
2018-04-25 19:01:52 -04: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-03-08 18:04:20 -05:00
|
|
|
impl Default for Filter {
|
|
|
|
#[inline]
|
|
|
|
fn default() -> Self {
|
|
|
|
Filter::All
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-03-29 21:48:20 -04:00
|
|
|
#[derive(Serialize, Deserialize)]
|
2018-03-08 17:08:30 -05:00
|
|
|
struct Todo {
|
|
|
|
id: u32,
|
|
|
|
title: Mutable<String>,
|
|
|
|
completed: Mutable<bool>,
|
2018-03-08 18:04:20 -05:00
|
|
|
|
|
|
|
#[serde(skip)]
|
2018-03-08 17:08:30 -05:00
|
|
|
editing: Mutable<Option<String>>,
|
|
|
|
}
|
2018-02-21 01:37:09 -05:00
|
|
|
|
2018-03-08 18:04:20 -05:00
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize)]
|
2018-03-08 17:08:30 -05:00
|
|
|
struct State {
|
|
|
|
todo_id: Cell<u32>,
|
2018-03-08 18:04:20 -05:00
|
|
|
|
|
|
|
#[serde(skip)]
|
2018-03-08 17:08:30 -05:00
|
|
|
new_todo_title: Mutable<String>,
|
2018-03-08 18:04:20 -05:00
|
|
|
|
2018-03-29 21:48:20 -04:00
|
|
|
todo_list: MutableVec<Rc<Todo>>,
|
2018-03-08 18:04:20 -05:00
|
|
|
|
|
|
|
#[serde(skip)]
|
2018-03-08 17:08:30 -05:00
|
|
|
filter: Mutable<Filter>,
|
|
|
|
}
|
2018-02-21 01:37:09 -05:00
|
|
|
|
2018-03-08 18:04:20 -05:00
|
|
|
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),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-08 18:18:28 -05:00
|
|
|
fn remove_todo(&self, todo: &Todo) {
|
|
|
|
// TODO make this more efficient ?
|
2018-07-22 16:01:48 -04:00
|
|
|
self.todo_list.lock_mut().retain(|x| x.id != todo.id);
|
2018-03-08 18:18:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
fn update_filter(&self) {
|
|
|
|
let hash = document().location().unwrap().hash().unwrap();
|
|
|
|
|
2018-07-14 17:36:50 -04:00
|
|
|
self.filter.set_neq(match hash.as_str() {
|
2018-03-08 18:18:28 -05:00
|
|
|
"#/active" => Filter::Active,
|
|
|
|
"#/completed" => Filter::Completed,
|
|
|
|
_ => Filter::All,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-03-08 18:04:20 -05:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-21 03:08:36 -05:00
|
|
|
|
2018-03-08 17:08:30 -05:00
|
|
|
// TODO make this more efficient
|
|
|
|
#[inline]
|
2018-03-17 09:38:01 -04:00
|
|
|
fn trim(input: &str) -> Option<String> {
|
2018-03-08 17:08:30 -05:00
|
|
|
let trimmed = input.trim();
|
2018-02-21 03:08:36 -05:00
|
|
|
|
2018-03-08 17:08:30 -05:00
|
|
|
if trimmed.is_empty() {
|
|
|
|
None
|
2018-02-21 03:08:36 -05:00
|
|
|
|
2018-03-08 17:08:30 -05:00
|
|
|
} else {
|
|
|
|
Some(trimmed.to_owned())
|
|
|
|
}
|
|
|
|
}
|
2018-02-23 21:50:03 -05:00
|
|
|
|
2018-03-08 17:08:30 -05:00
|
|
|
#[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, {
|
2018-07-14 19:26:52 -04:00
|
|
|
.children(children)
|
2018-03-08 17:08:30 -05:00
|
|
|
})
|
|
|
|
}
|
2018-02-23 21:50:03 -05:00
|
|
|
|
2018-03-08 17:08:30 -05:00
|
|
|
#[inline]
|
|
|
|
fn link(href: &str, t: &str) -> Dom {
|
|
|
|
html!("a", {
|
2018-07-14 19:26:52 -04:00
|
|
|
.attribute("href", href)
|
|
|
|
.children(&mut [
|
2018-03-08 17:08:30 -05:00
|
|
|
text(t),
|
2018-07-14 19:26:52 -04:00
|
|
|
])
|
2018-03-08 17:08:30 -05:00
|
|
|
})
|
|
|
|
}
|
2018-03-01 08:39:58 -05:00
|
|
|
|
2018-03-08 17:08:30 -05:00
|
|
|
fn filter_button(state: Rc<State>, kind: Filter) -> Dom {
|
|
|
|
html!("a", {
|
2018-07-14 19:26:52 -04:00
|
|
|
.class_signal("selected", state.filter.signal()
|
|
|
|
.map(clone!(kind => move |filter| filter == kind)))
|
2018-03-08 18:18:28 -05:00
|
|
|
|
2018-07-14 19:26:52 -04:00
|
|
|
.attribute("href", match kind {
|
2018-03-08 17:08:30 -05:00
|
|
|
Filter::Active => "#/active",
|
|
|
|
Filter::Completed => "#/completed",
|
|
|
|
Filter::All => "#/",
|
2018-07-14 19:26:52 -04:00
|
|
|
})
|
2018-03-08 18:18:28 -05:00
|
|
|
|
2018-07-14 19:26:52 -04:00
|
|
|
.children(&mut [
|
2018-03-08 17:08:30 -05:00
|
|
|
text(match kind {
|
|
|
|
Filter::Active => "Active",
|
|
|
|
Filter::Completed => "Completed",
|
|
|
|
Filter::All => "All",
|
2018-02-23 21:50:03 -05:00
|
|
|
})
|
2018-07-14 19:26:52 -04:00
|
|
|
])
|
2018-03-08 17:08:30 -05:00
|
|
|
})
|
|
|
|
}
|
2018-02-23 21:50:03 -05:00
|
|
|
|
2018-02-21 01:37:09 -05:00
|
|
|
|
2018-03-08 17:08:30 -05:00
|
|
|
fn main() {
|
2018-03-08 18:04:20 -05:00
|
|
|
let state = Rc::new(State::deserialize());
|
2018-03-08 17:08:30 -05:00
|
|
|
|
2018-03-08 18:18:28 -05:00
|
|
|
state.update_filter();
|
2018-03-01 08:39:58 -05:00
|
|
|
|
2018-03-08 17:08:30 -05:00
|
|
|
window().add_event_listener(clone!(state => move |_: HashChangeEvent| {
|
2018-03-08 18:18:28 -05:00
|
|
|
state.update_filter();
|
2018-03-08 17:08:30 -05:00
|
|
|
}));
|
2018-03-01 08:39:58 -05:00
|
|
|
|
|
|
|
|
2018-03-18 14:49:40 -04:00
|
|
|
let body = dominator::body();
|
2018-03-01 08:39:58 -05:00
|
|
|
|
2018-03-08 17:08:30 -05:00
|
|
|
dominator::append_dom(&body,
|
|
|
|
html!("section", {
|
2018-07-14 19:26:52 -04:00
|
|
|
.class("todoapp")
|
|
|
|
.children(&mut [
|
2018-03-08 17:08:30 -05:00
|
|
|
html!("header", {
|
2018-07-14 19:26:52 -04:00
|
|
|
.class("header")
|
|
|
|
.children(&mut [
|
2018-03-08 17:08:30 -05:00
|
|
|
simple("h1", &mut [
|
|
|
|
text("todos"),
|
|
|
|
]),
|
|
|
|
html!("input", {
|
2018-07-14 19:26:52 -04:00
|
|
|
.focused(true)
|
|
|
|
.class("new-todo")
|
|
|
|
.attribute("placeholder", "What needs to be done?")
|
2018-03-08 17:08:30 -05:00
|
|
|
|
2018-07-14 19:26:52 -04:00
|
|
|
.property_signal("value", state.new_todo_title.signal_cloned())
|
2018-03-08 17:08:30 -05:00
|
|
|
|
2018-07-14 19:26:52 -04:00
|
|
|
.event(clone!(state => move |event: InputEvent| {
|
2018-07-14 17:36:50 -04:00
|
|
|
state.new_todo_title.set_neq(get_value(&event));
|
2018-07-14 19:26:52 -04:00
|
|
|
}))
|
2018-03-08 17:08:30 -05:00
|
|
|
|
2018-07-14 19:26:52 -04:00
|
|
|
.event(clone!(state => move |event: KeyDownEvent| {
|
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);
|
|
|
|
|
2018-07-22 16:01:48 -04:00
|
|
|
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),
|
2018-03-29 21:48:20 -04:00
|
|
|
}));
|
2018-03-08 18:04:20 -05:00
|
|
|
|
|
|
|
state.serialize();
|
2018-03-08 17:08:30 -05:00
|
|
|
}
|
2018-03-01 08:39:58 -05:00
|
|
|
}
|
2018-07-14 19:26:52 -04:00
|
|
|
}))
|
2018-03-08 17:08:30 -05:00
|
|
|
}),
|
2018-07-14 19:26:52 -04:00
|
|
|
])
|
2018-03-08 17:08:30 -05:00
|
|
|
}),
|
|
|
|
|
|
|
|
html!("section", {
|
2018-07-14 19:26:52 -04:00
|
|
|
.class("main")
|
2018-03-08 17:08:30 -05:00
|
|
|
|
|
|
|
// Hide if it doesn't have any todos.
|
2018-07-14 19:26:52 -04:00
|
|
|
.property_signal("hidden", state.todo_list.signal_vec_cloned()
|
2018-03-08 17:08:30 -05:00
|
|
|
.len()
|
2018-07-14 19:26:52 -04:00
|
|
|
.map(|len| len == 0))
|
2018-03-08 17:08:30 -05:00
|
|
|
|
2018-07-14 19:26:52 -04:00
|
|
|
.children(&mut [
|
2018-03-08 17:08:30 -05:00
|
|
|
html!("input", {
|
2018-07-14 19:26:52 -04:00
|
|
|
.class("toggle-all")
|
|
|
|
.attribute("id", "toggle-all")
|
|
|
|
.attribute("type", "checkbox")
|
2018-03-08 17:08:30 -05:00
|
|
|
|
2018-07-14 19:26:52 -04: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()
|
2018-07-14 19:26:52 -04:00
|
|
|
.map(|len| len != 0))
|
2018-03-08 17:08:30 -05:00
|
|
|
|
2018-07-14 19:26:52 -04:00
|
|
|
.event(clone!(state => move |event: ChangeEvent| {
|
2018-03-08 17:08:30 -05:00
|
|
|
let checked = !get_checked(&event);
|
|
|
|
|
2018-06-22 14:08:04 -04:00
|
|
|
{
|
2018-07-22 16:01:48 -04:00
|
|
|
let todo_list = state.todo_list.lock_ref();
|
2018-06-22 14:08:04 -04:00
|
|
|
|
2018-04-25 19:01:52 -04:00
|
|
|
for todo in todo_list.iter() {
|
2018-07-14 17:36:50 -04:00
|
|
|
todo.completed.set_neq(checked);
|
2018-04-25 19:01:52 -04:00
|
|
|
}
|
2018-06-22 14:08:04 -04:00
|
|
|
}
|
2018-03-08 18:04:20 -05:00
|
|
|
|
|
|
|
state.serialize();
|
2018-07-14 19:26:52 -04:00
|
|
|
}))
|
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", {
|
2018-07-14 19:26:52 -04:00
|
|
|
.attribute("for", "toggle-all")
|
|
|
|
.children(&mut [
|
2018-03-08 17:08:30 -05:00
|
|
|
text("Mark all as complete"),
|
2018-07-14 19:26:52 -04:00
|
|
|
])
|
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", {
|
2018-07-14 19:26:52 -04:00
|
|
|
.class("todo-list")
|
2018-03-08 17:08:30 -05:00
|
|
|
|
2018-07-14 19:26:52 -04: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", {
|
2018-07-14 19:26:52 -04:00
|
|
|
.class_signal("editing", todo.editing.signal_cloned()
|
|
|
|
.map(|x| x.is_some()))
|
2018-03-08 18:18:28 -05:00
|
|
|
|
2018-07-14 19:26:52 -04:00
|
|
|
.class_signal("completed", todo.completed.signal())
|
2018-03-08 18:18:28 -05:00
|
|
|
|
2018-07-14 19:26:52 -04:00
|
|
|
.property_signal("hidden",
|
2018-03-29 21:48:20 -04:00
|
|
|
map_ref!(
|
2018-03-08 18:18:28 -05:00
|
|
|
let filter = state.filter.signal(),
|
|
|
|
let completed = todo.completed.signal() =>
|
2018-03-29 21:48:20 -04:00
|
|
|
match *filter {
|
2018-03-08 18:18:28 -05:00
|
|
|
Filter::Active => !completed,
|
2018-03-29 21:48:20 -04:00
|
|
|
Filter::Completed => *completed,
|
2018-03-08 18:18:28 -05:00
|
|
|
Filter::All => true,
|
|
|
|
}
|
|
|
|
)
|
2018-07-14 19:26:52 -04:00
|
|
|
.dedupe_map(|show| !*show))
|
2018-03-08 18:18:28 -05:00
|
|
|
|
2018-07-14 19:26:52 -04:00
|
|
|
.children(&mut [
|
2018-03-08 18:18:28 -05:00
|
|
|
html!("div", {
|
2018-07-14 19:26:52 -04:00
|
|
|
.class("view")
|
|
|
|
.children(&mut [
|
2018-03-08 18:18:28 -05:00
|
|
|
html!("input", {
|
2018-07-14 19:26:52 -04:00
|
|
|
.attribute("type", "checkbox")
|
|
|
|
.class("toggle")
|
2018-03-08 18:18:28 -05:00
|
|
|
|
2018-07-14 19:26:52 -04:00
|
|
|
.property_signal("checked", todo.completed.signal())
|
2018-03-08 18:18:28 -05:00
|
|
|
|
2018-07-14 19:26:52 -04:00
|
|
|
.event(clone!(state, todo => move |event: ChangeEvent| {
|
2018-07-14 17:36:50 -04:00
|
|
|
todo.completed.set_neq(get_checked(&event));
|
2018-03-08 18:18:28 -05:00
|
|
|
state.serialize();
|
2018-07-14 19:26:52 -04:00
|
|
|
}))
|
2018-03-08 18:18:28 -05:00
|
|
|
}),
|
|
|
|
|
|
|
|
html!("label", {
|
2018-07-14 19:26:52 -04:00
|
|
|
.event(clone!(todo => move |_: DoubleClickEvent| {
|
2018-07-14 17:36:50 -04:00
|
|
|
todo.editing.set_neq(Some(todo.title.get_cloned()));
|
2018-07-14 19:26:52 -04:00
|
|
|
}))
|
2018-03-08 18:18:28 -05:00
|
|
|
|
2018-07-14 19:26:52 -04:00
|
|
|
.children(&mut [
|
2018-04-25 19:01:52 -04:00
|
|
|
text_signal(todo.title.signal_cloned()),
|
2018-07-14 19:26:52 -04:00
|
|
|
])
|
2018-03-08 18:18:28 -05:00
|
|
|
}),
|
|
|
|
|
|
|
|
html!("button", {
|
2018-07-14 19:26:52 -04:00
|
|
|
.class("destroy")
|
|
|
|
.event(clone!(state, todo => move |_: ClickEvent| {
|
2018-03-08 18:18:28 -05:00
|
|
|
state.remove_todo(&todo);
|
|
|
|
state.serialize();
|
2018-07-14 19:26:52 -04:00
|
|
|
}))
|
2018-03-08 18:18:28 -05:00
|
|
|
}),
|
2018-07-14 19:26:52 -04: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", {
|
2018-07-14 19:26:52 -04:00
|
|
|
.class("edit")
|
2018-03-08 17:08:30 -05:00
|
|
|
|
2018-07-14 19:26:52 -04:00
|
|
|
.property_signal("value", todo.editing.signal_cloned()
|
|
|
|
.map(|x| x.unwrap_or_else(|| "".to_owned())))
|
2018-03-08 17:08:30 -05:00
|
|
|
|
2018-07-14 19:26:52 -04:00
|
|
|
.property_signal("hidden", todo.editing.signal_cloned()
|
|
|
|
.map(|x| x.is_none()))
|
2018-03-08 17:08:30 -05:00
|
|
|
|
2018-03-08 18:18:28 -05:00
|
|
|
// TODO dedupe this somehow ?
|
2018-07-14 19:26:52 -04:00
|
|
|
.focused_signal(todo.editing.signal_cloned()
|
|
|
|
.map(|x| x.is_some()))
|
2018-03-08 17:08:30 -05:00
|
|
|
|
2018-07-14 19:26:52 -04:00
|
|
|
.event(clone!(todo => move |event: KeyDownEvent| {
|
2018-03-08 18:18:28 -05:00
|
|
|
let key = event.key();
|
2018-03-08 17:08:30 -05:00
|
|
|
|
2018-03-08 18:18:28 -05:00
|
|
|
if key == "Enter" {
|
|
|
|
let element: HtmlElement = event.target().unwrap().try_into().unwrap();
|
|
|
|
element.blur();
|
2018-03-08 17:08:30 -05:00
|
|
|
|
2018-03-08 18:18:28 -05:00
|
|
|
} else if key == "Escape" {
|
2018-07-14 17:36:50 -04:00
|
|
|
todo.editing.set_neq(None);
|
2018-03-08 17:08:30 -05:00
|
|
|
}
|
2018-07-14 19:26:52 -04:00
|
|
|
}))
|
2018-03-08 18:04:20 -05:00
|
|
|
|
2018-07-14 19:26:52 -04:00
|
|
|
.event(clone!(todo => move |event: InputEvent| {
|
2018-07-14 17:36:50 -04:00
|
|
|
todo.editing.set_neq(Some(get_value(&event)));
|
2018-07-14 19:26:52 -04:00
|
|
|
}))
|
2018-03-08 18:18:28 -05:00
|
|
|
|
2018-03-17 09:38:01 -04:00
|
|
|
// TODO global_event ?
|
2018-07-14 19:26:52 -04:00
|
|
|
.event(clone!(state, todo => move |_: BlurEvent| {
|
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-07-14 19:26:52 -04:00
|
|
|
}))
|
2018-03-08 18:18:28 -05:00
|
|
|
}),
|
2018-07-14 19:26:52 -04:00
|
|
|
])
|
2018-03-08 18:18:28 -05:00
|
|
|
})
|
2018-07-14 19:26:52 -04:00
|
|
|
})))
|
2018-03-08 17:08:30 -05:00
|
|
|
}),
|
2018-07-14 19:26:52 -04:00
|
|
|
])
|
2018-02-25 06:58:20 -05:00
|
|
|
}),
|
|
|
|
|
2018-03-08 17:08:30 -05:00
|
|
|
html!("footer", {
|
2018-07-14 19:26:52 -04:00
|
|
|
.class("footer")
|
2018-03-08 17:08:30 -05:00
|
|
|
|
|
|
|
// Hide if it doesn't have any todos.
|
2018-07-14 19:26:52 -04:00
|
|
|
.property_signal("hidden", state.todo_list.signal_vec_cloned()
|
2018-03-08 18:18:28 -05:00
|
|
|
.len()
|
2018-07-14 19:26:52 -04:00
|
|
|
.map(|len| len == 0))
|
2018-03-08 17:08:30 -05:00
|
|
|
|
2018-07-14 19:26:52 -04:00
|
|
|
.children(&mut [
|
2018-03-08 17:08:30 -05:00
|
|
|
html!("span", {
|
2018-07-14 19:26:52 -04:00
|
|
|
.class("todo-count")
|
2018-03-08 18:18:28 -05:00
|
|
|
|
2018-07-14 19:26:52 -04: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![
|
|
|
|
simple("strong", &mut [
|
2018-04-25 19:01:52 -04:00
|
|
|
text(&len.to_string())
|
2018-03-08 18:18:28 -05:00
|
|
|
]),
|
|
|
|
text(if len == 1 {
|
|
|
|
" item left"
|
|
|
|
} else {
|
|
|
|
" items left"
|
|
|
|
}),
|
|
|
|
]
|
|
|
|
})
|
2018-07-14 19:26:52 -04:00
|
|
|
.to_signal_vec())
|
2018-03-08 17:08:30 -05:00
|
|
|
}),
|
|
|
|
html!("ul", {
|
2018-07-14 19:26:52 -04:00
|
|
|
.class("filters")
|
|
|
|
.children(&mut [
|
2018-03-08 17:08:30 -05:00
|
|
|
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),
|
|
|
|
]),
|
2018-07-14 19:26:52 -04:00
|
|
|
])
|
2018-03-08 17:08:30 -05:00
|
|
|
}),
|
|
|
|
html!("button", {
|
2018-07-14 19:26:52 -04:00
|
|
|
.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.
|
2018-07-14 19:26:52 -04:00
|
|
|
.property_signal("hidden", 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()
|
2018-07-14 19:26:52 -04:00
|
|
|
.map(|len| len == 0))
|
2018-02-21 01:37:09 -05:00
|
|
|
|
2018-07-14 19:26:52 -04:00
|
|
|
.event(clone!(state => move |_: ClickEvent| {
|
2018-07-22 16:01:48 -04:00
|
|
|
state.todo_list.lock_mut().retain(|todo| todo.completed.get() == false);
|
2018-03-08 18:04:20 -05:00
|
|
|
state.serialize();
|
2018-07-14 19:26:52 -04:00
|
|
|
}))
|
2018-03-08 17:08:30 -05:00
|
|
|
|
2018-07-14 19:26:52 -04:00
|
|
|
.children(&mut [
|
2018-03-08 17:08:30 -05:00
|
|
|
text("Clear completed"),
|
2018-07-14 19:26:52 -04:00
|
|
|
])
|
2018-03-08 17:08:30 -05:00
|
|
|
}),
|
2018-07-14 19:26:52 -04:00
|
|
|
])
|
2018-02-25 06:58:20 -05:00
|
|
|
}),
|
2018-07-14 19:26:52 -04:00
|
|
|
])
|
2018-03-08 17:08:30 -05:00
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
dominator::append_dom(&body,
|
|
|
|
html!("footer", {
|
2018-07-14 19:26:52 -04:00
|
|
|
.class("info")
|
|
|
|
.children(&mut [
|
2018-03-08 17:08:30 -05:00
|
|
|
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"),
|
|
|
|
]),
|
2018-07-14 19:26:52 -04:00
|
|
|
])
|
2018-03-08 17:08:30 -05:00
|
|
|
}),
|
2018-02-21 01:37:09 -05:00
|
|
|
);
|
|
|
|
}
|