@@ -0,0 +1,450 @@
1+//! Type definitions and `dodrio::Render` implementation for a collection of
4+use crate::controller::Controller;
5+use crate::todo::{Todo, TodoActions};
6+use crate::visibility::Visibility;
7+use crate::{keys, utils};
9+ bumpalo::{self, Bump},
10+ on, Attribute, Node, Render, RootRender, VdomWeak,
12+use serde::{Deserialize, Serialize};
13+use std::marker::PhantomData;
15+use wasm_bindgen::prelude::*;
16+use wasm_bindgen::JsCast;
18+/// A collection of todos.
19+#[derive(Default, Serialize, Deserialize)]
20+#[serde(rename = "todos-dodrio", bound = "")]
21+pub struct Todos<C = Controller> {
28+ visibility: Visibility,
31+ _controller: PhantomData<C>,
34+/// Actions for `Todos` that can be triggered by UI interactions.
35+pub trait TodosActions: TodoActions {
36+ /// Toggle the completion state of all todo items.
37+ fn toggle_all(root: &mut dyn RootRender, vdom: VdomWeak);
39+ /// Update the draft todo item's text.
40+ fn update_draft(root: &mut dyn RootRender, vdom: VdomWeak, draft: String);
42+ /// Finish the current draft todo item and add it to the collection of
44+ fn finish_draft(root: &mut dyn RootRender, vdom: VdomWeak);
46+ /// Change the todo item visibility filtering to the given `Visibility`.
47+ fn change_visibility(root: &mut dyn RootRender, vdom: VdomWeak, vis: Visibility);
49+ /// Delete all completed todo items.
50+ fn delete_completed(root: &mut dyn RootRender, vdom: VdomWeak);
54+ /// Construct a new todos set.
56+ /// If an existing set is available in local storage, then us that,
57+ /// otherwise create a new set.
62+ Self::from_local_storage().unwrap_or_default()
65+ /// Deserialize a set of todos from local storage.
66+ pub fn from_local_storage() -> Option<Self> {
67+ utils::local_storage()
68+ .get("todomvc-dodrio")
71+ .and_then(|json| serde_json::from_str(&json).ok())
74+ /// Serialize this set of todos to local storage.
75+ pub fn save_to_local_storage(&self) {
76+ let serialized = serde_json::to_string(self).unwrap_throw();
77+ utils::local_storage()
78+ .set("todomvc-dodrio", &serialized)
82+ /// Add a new todo item to this collection.
83+ pub fn add_todo(&mut self, todo: Todo<C>) {
84+ self.todos.push(todo);
87+ /// Delete the todo with the given id.
88+ pub fn delete_todo(&mut self, id: usize) {
89+ self.todos.remove(id);
93+ /// Delete all completed todo items.
94+ pub fn delete_completed(&mut self) {
95+ self.todos.retain(|t| !t.is_complete());
99+ // Fix all todo identifiers so that they match their index once again.
100+ fn fix_ids(&mut self) {
101+ for (id, todo) in self.todos.iter_mut().enumerate() {
106+ /// Get a shared slice of the underlying set of todo items.
107+ pub fn todos(&self) -> &[Todo<C>] {
111+ /// Get an exclusive slice of the underlying set of todo items.
112+ pub fn todos_mut(&mut self) -> &mut [Todo<C>] {
116+ /// Set the draft todo item text.
117+ pub fn set_draft<S: Into<String>>(&mut self, draft: S) {
118+ self.draft = draft.into();
121+ /// Take the current draft text and replace it with an empty string.
122+ pub fn take_draft(&mut self) -> String {
123+ mem::replace(&mut self.draft, String::new())
126+ /// Get the current visibility for these todos.
127+ pub fn visibility(&self) -> Visibility {
131+ /// Set the visibility for these todoS.
132+ pub fn set_visibility(&mut self, vis: Visibility) {
133+ self.visibility = vis;
137+/// Rendering helpers.
138+impl<C: TodosActions> Todos<C> {
139+ fn header<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
152+ Node::element(bump, "h1", [], [], [Node::text("todos")]),
157+ on(bump, "input", |root, vdom, event| {
161+ .unchecked_into::<web_sys::HtmlInputElement>();
162+ C::update_draft(root, vdom, input.value());
164+ on(bump, "keydown", |root, vdom, event| {
165+ let event = event.unchecked_into::<web_sys::KeyboardEvent>();
166+ if event.key_code() == keys::ENTER {
167+ C::finish_draft(root, vdom);
177+ name: "placeholder",
178+ value: "What needs to be done?",
195+ fn todos_list<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
199+ use dodrio::bumpalo::collections::Vec;
201+ let mut todos = Vec::new_in(bump);
205+ .filter(|t| match self.visibility {
206+ Visibility::All => true,
207+ Visibility::Active => !t.is_complete(),
208+ Visibility::Completed => t.is_complete(),
210+ .map(|t| t.render(bump)),
212+ let todos = todos.into_bump_slice();
214+ let mut input_attrs = bumpalo::vec![
218+ value: "toggle-all",
222+ value: "toggle-all",
233+ if self.todos.iter().all(|t| t.is_complete()) {
234+ input_attrs.push(Attribute {
239+ let input_attrs = input_attrs.into_bump_slice();
252+ value: if self.todos.is_empty() {
263+ [on(bump, "click", |root, vdom, _event| {
264+ C::toggle_all(root, vdom);
275+ value: "toggle-all",
277+ [Node::text("Mark all as complete")],
293+ fn footer<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
297+ let completed_count = self.todos.iter().filter(|t| t.is_complete()).count();
298+ let incomplete_count = self.todos.len() - completed_count;
299+ let items_left = if incomplete_count == 1 {
304+ let incomplete_count = bumpalo::format!(in bump, "{}", incomplete_count);
306+ let attrs = if self.todos.is_empty() {
318+ &bump.alloc([Attribute {
324+ let clear_completed_text = bumpalo::format!(
326+ "Clear completed ({})",
327+ self.todos.iter().filter(|t| t.is_complete()).count()
342+ value: "todo-count",
350+ [Node::text(incomplete_count.into_bump_str())],
352+ Node::text(items_left),
364+ self.visibility_swap(bump, "#/", Visibility::All),
365+ self.visibility_swap(bump, "#/active", Visibility::Active),
366+ self.visibility_swap(bump, "#/completed", Visibility::Completed),
372+ [on(bump, "click", |root, vdom, _event| {
373+ C::delete_completed(root, vdom);
376+ let mut attrs = bumpalo::vec![
380+ value: "clear-completed",
383+ if self.todos.iter().all(|t| !t.is_complete()) {
384+ attrs.push(Attribute {
391+ [Node::text(clear_completed_text.into_bump_str())],
397+ fn visibility_swap<'a, 'bump>(
401+ target_vis: Visibility,
409+ [on(bump, "click", move |root, vdom, _event| {
410+ C::change_visibility(root, vdom, target_vis);
424+ value: if self.visibility == target_vis {
431+ [Node::text(target_vis.label())],
437+impl<C: TodosActions> Render for Todos<C> {
438+ fn render<'a, 'bump>(&'a self, bump: &'bump Bump) -> Node<'bump>
447+ [self.header(bump), self.todos_list(bump), self.footer(bump)],