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

467 lines
17 KiB
Rust
Raw Normal View History

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]
extern crate futures_signals;
#[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;
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
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
#[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
impl Default for Filter {
#[inline]
fn default() -> Self {
Filter::All
}
}
#[derive(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
#[derive(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>>,
#[serde(skip)]
2018-03-08 17:08:30 -05:00
filter: Mutable<Filter>,
}
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(),
filter: Mutable::new(Filter::All),
}
}
2018-03-08 18:18:28 -05:00
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(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();
}
}
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-03-08 17:08:30 -05:00
if trimmed.is_empty() {
None
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, {
children(children);
})
}
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", {
attribute("href", href);
children(&mut [
text(t),
]);
})
}
2018-03-08 17:08:30 -05:00
fn filter_button(state: Rc<State>, kind: Filter) -> Dom {
html!("a", {
class_signal("selected", state.filter.signal()
.map(clone!(kind => move |filter| filter == kind)));
2018-03-08 18:18:28 -05:00
2018-03-08 17:08:30 -05:00
attribute("href", match kind {
Filter::Active => "#/active",
Filter::Completed => "#/completed",
Filter::All => "#/",
});
2018-03-08 18:18:28 -05:00
2018-03-08 17:08:30 -05:00
children(&mut [
text(match kind {
Filter::Active => "Active",
Filter::Completed => "Completed",
Filter::All => "All",
2018-02-23 21:50:03 -05: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() {
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-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-18 14:49:40 -04:00
let body = dominator::body();
2018-03-08 17:08:30 -05:00
dominator::append_dom(&body,
html!("section", {
class("todoapp");
2018-02-26 03:01:04 -05:00
children(&mut [
2018-03-08 17:08:30 -05:00
html!("header", {
class("header");
2018-03-08 17:08:30 -05:00
children(&mut [
simple("h1", &mut [
text("todos"),
]),
html!("input", {
focused(true);
class("new-todo");
2018-03-08 17:08:30 -05:00
attribute("placeholder", "What needs to be done?");
property_signal("value", state.new_todo_title.signal_cloned());
2018-03-08 17:08:30 -05:00
event(clone!(state => move |event: InputEvent| {
state.new_todo_title.set(get_value(&event));
}));
event(clone!(state => move |event: KeyDownEvent| {
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 {
state.new_todo_title.set("".to_owned());
2018-03-08 17:08:30 -05:00
let id = state.todo_id.get();
state.todo_id.set(id + 1);
state.todo_list.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
}));
}),
]);
}),
html!("section", {
class("main");
2018-03-08 17:08:30 -05:00
// Hide if it doesn't have any todos.
property_signal("hidden", 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 [
html!("input", {
class("toggle-all");
2018-03-08 17:08:30 -05:00
attribute("id", "toggle-all");
attribute("type", "checkbox");
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: ChangeEvent| {
let checked = !get_checked(&event);
state.todo_list.with_slice(|todo_list| {
for todo in todo_list.iter() {
todo.completed.set(checked);
}
});
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");
children(&mut [
text("Mark all as complete"),
]);
}),
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
property_signal("hidden",
map_ref!(
2018-03-08 18:18:28 -05:00
let filter = state.filter.signal(),
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,
}
)
2018-06-12 03:06:51 -04:00
.dedupe_map(|show| !*show));
2018-03-08 18:18:28 -05:00
children(&mut [
html!("div", {
class("view");
2018-03-08 18:18:28 -05:00
children(&mut [
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: ChangeEvent| {
todo.completed.set(get_checked(&event));
state.serialize();
}));
}),
html!("label", {
event(clone!(todo => move |_: DoubleClickEvent| {
2018-03-17 09:38:01 -04:00
todo.editing.set(Some(todo.title.get_cloned()));
2018-03-08 18:18:28 -05:00
}));
children(&mut [
text_signal(todo.title.signal_cloned()),
2018-03-08 18:18:28 -05:00
]);
}),
html!("button", {
class("destroy");
2018-03-08 18:18:28 -05:00
event(clone!(state, todo => move |_: ClickEvent| {
state.remove_todo(&todo);
state.serialize();
}));
}),
]);
}),
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
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 ?
focused_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
event(clone!(todo => move |event: KeyDownEvent| {
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" {
todo.editing.set(None);
2018-03-08 17:08:30 -05:00
}
2018-03-08 18:18:28 -05:00
}));
2018-03-08 18:18:28 -05:00
event(clone!(todo => move |event: InputEvent| {
todo.editing.set(Some(get_value(&event)));
}));
2018-03-17 09:38:01 -04:00
// TODO global_event ?
2018-03-08 18:18:28 -05:00
event(clone!(state, todo => move |_: BlurEvent| {
if let Some(title) = todo.editing.replace(None) {
2018-03-17 09:38:01 -04:00
if let Some(title) = trim(&title) {
2018-03-08 18:18:28 -05:00
todo.title.set(title);
} else {
state.remove_todo(&todo);
}
state.serialize();
}
}));
}),
]);
})
})));
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.
property_signal("hidden", 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
2018-02-26 03:01:04 -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![
simple("strong", &mut [
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");
2018-03-08 17:08:30 -05:00
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");
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.
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()
.map(|len| len == 0));
2018-02-21 01:37:09 -05:00
2018-03-08 17:08:30 -05:00
event(clone!(state => move |_: ClickEvent| {
state.todo_list.retain(|todo| todo.completed.get() == false);
state.serialize();
2018-03-08 17:08:30 -05:00
}));
children(&mut [
text("Clear completed"),
]);
}),
]);
2018-02-25 06:58:20 -05:00
}),
2018-02-26 03:01:04 -05:00
]);
2018-03-08 17:08:30 -05:00
}),
);
dominator::append_dom(&body,
html!("footer", {
class("info");
2018-03-08 17:08:30 -05:00
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"),
]),
]);
}),
2018-02-21 01:37:09 -05:00
);
}