diff --git a/examples/todomvc/src/lib.rs b/examples/todomvc/src/lib.rs index 7f9716f92..4d3a9ef30 100644 --- a/examples/todomvc/src/lib.rs +++ b/examples/todomvc/src/lib.rs @@ -1,7 +1,4 @@ -use leptos::{ - web_sys::HtmlInputElement, - *, -}; +use leptos::{web_sys::HtmlInputElement, *}; use storage::TodoSerialized; use uuid::Uuid; @@ -14,111 +11,112 @@ const STORAGE_KEY: &str = "todos-leptos"; // Basic operations to manipulate the todo list: nothing really interesting here impl Todos { - pub fn new(cx: Scope) -> Self { - let starting_todos = if let Ok(Some(storage)) = window().local_storage() { - storage - .get_item(STORAGE_KEY) - .ok() - .flatten() - .and_then(|value| { - serde_json::from_str::>(&value).ok() - }) - .map(|values| { - values - .into_iter() - .map(|stored| stored.into_todo(cx)) - .collect() - }) - .unwrap_or_default() - } else { - Vec::new() - }; - Self(starting_todos) - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - pub fn add(&mut self, todo: Todo) { - self.0.push(todo); - } - - pub fn remove(&mut self, id: Uuid) { - self.0.retain(|todo| todo.id != id); - } - - pub fn remaining(&self) -> usize { - // `todo.completed` is a signal, so we call .get() to access its value - self.0.iter().filter(|todo| !todo.completed.get()).count() - } - - pub fn completed(&self) -> usize { - // `todo.completed` is a signal, so we call .get() to access its value - self.0.iter().filter(|todo| todo.completed.get()).count() - } - - pub fn toggle_all(&self) { - // if all are complete, mark them all active - if self.remaining() == 0 { - for todo in &self.0 { - todo.completed.update(|completed| { - if *completed { - *completed = false - } - }); - } + pub fn new(cx: Scope) -> Self { + let starting_todos = if let Ok(Some(storage)) = window().local_storage() + { + storage + .get_item(STORAGE_KEY) + .ok() + .flatten() + .and_then(|value| { + serde_json::from_str::>(&value).ok() + }) + .map(|values| { + values + .into_iter() + .map(|stored| stored.into_todo(cx)) + .collect() + }) + .unwrap_or_default() + } else { + Vec::new() + }; + Self(starting_todos) } - // otherwise, mark them all complete - else { - for todo in &self.0 { - todo.completed.set(true); - } - } - } - fn clear_completed(&mut self) { - self.0.retain(|todo| !todo.completed.get()); - } + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn add(&mut self, todo: Todo) { + self.0.push(todo); + } + + pub fn remove(&mut self, id: Uuid) { + self.0.retain(|todo| todo.id != id); + } + + pub fn remaining(&self) -> usize { + // `todo.completed` is a signal, so we call .get() to access its value + self.0.iter().filter(|todo| !todo.completed.get()).count() + } + + pub fn completed(&self) -> usize { + // `todo.completed` is a signal, so we call .get() to access its value + self.0.iter().filter(|todo| todo.completed.get()).count() + } + + pub fn toggle_all(&self) { + // if all are complete, mark them all active + if self.remaining() == 0 { + for todo in &self.0 { + todo.completed.update(|completed| { + if *completed { + *completed = false + } + }); + } + } + // otherwise, mark them all complete + else { + for todo in &self.0 { + todo.completed.set(true); + } + } + } + + fn clear_completed(&mut self) { + self.0.retain(|todo| !todo.completed.get()); + } } #[derive(Debug, PartialEq, Eq, Clone)] pub struct Todo { - pub id: Uuid, - pub title: RwSignal, - pub completed: RwSignal, + pub id: Uuid, + pub title: RwSignal, + pub completed: RwSignal, } impl Todo { - pub fn new(cx: Scope, id: Uuid, title: String) -> Self { - Self::new_with_completed(cx, id, title, false) - } - - pub fn new_with_completed( - cx: Scope, - id: Uuid, - title: String, - completed: bool, - ) -> Self { - // RwSignal combines the getter and setter in one struct, rather than separating - // the getter from the setter. This makes it more convenient in some cases, such - // as when we're putting the signals into a struct and passing it around. There's - // no real difference: you could use `create_signal` here, or use `create_rw_signal` - // everywhere. - let title = create_rw_signal(cx, title); - let completed = create_rw_signal(cx, completed); - Self { - id, - title, - completed, + pub fn new(cx: Scope, id: Uuid, title: String) -> Self { + Self::new_with_completed(cx, id, title, false) } - } - pub fn toggle(&self) { - // A signal's `update()` function gives you a mutable reference to the current value - // You can use that to modify the value in place, which will notify any subscribers. - self.completed.update(|completed| *completed = !*completed); - } + pub fn new_with_completed( + cx: Scope, + id: Uuid, + title: String, + completed: bool, + ) -> Self { + // RwSignal combines the getter and setter in one struct, rather than separating + // the getter from the setter. This makes it more convenient in some cases, such + // as when we're putting the signals into a struct and passing it around. There's + // no real difference: you could use `create_signal` here, or use `create_rw_signal` + // everywhere. + let title = create_rw_signal(cx, title); + let completed = create_rw_signal(cx, completed); + Self { + id, + title, + completed, + } + } + + pub fn toggle(&self) { + // A signal's `update()` function gives you a mutable reference to the current value + // You can use that to modify the value in place, which will notify any subscribers. + self.completed.update(|completed| *completed = !*completed); + } } const ESCAPE_KEY: u32 = 27; @@ -126,231 +124,232 @@ const ENTER_KEY: u32 = 13; #[component] pub fn TodoMVC(cx: Scope) -> impl IntoView { - // The `todos` are a signal, since we need to reactively update the list - let (todos, set_todos) = create_signal(cx, Todos::new(cx)); + // The `todos` are a signal, since we need to reactively update the list + let (todos, set_todos) = create_signal(cx, Todos::new(cx)); - // We provide a context that each component can use to update the list - // Here, I'm just passing the `WriteSignal`; a doesn't need to read the whole list - // (and shouldn't try to, as that would cause each individual to re-render when - // a new todo is added! This kind of hygiene is why `create_signal` defaults to read-write - // segregation.) - provide_context(cx, set_todos); + // We provide a context that each component can use to update the list + // Here, I'm just passing the `WriteSignal`; a doesn't need to read the whole list + // (and shouldn't try to, as that would cause each individual to re-render when + // a new todo is added! This kind of hygiene is why `create_signal` defaults to read-write + // segregation.) + provide_context(cx, set_todos); - // Handle the three filter modes: All, Active, and Completed - let (mode, set_mode) = create_signal(cx, Mode::All); - window_event_listener("hashchange", move |_| { - let new_mode = location_hash().map(|hash| route(&hash)).unwrap_or_default(); - set_mode(new_mode); - }); + // Handle the three filter modes: All, Active, and Completed + let (mode, set_mode) = create_signal(cx, Mode::All); + window_event_listener("hashchange", move |_| { + let new_mode = + location_hash().map(|hash| route(&hash)).unwrap_or_default(); + set_mode(new_mode); + }); - // Callback to add a todo on pressing the `Enter` key, if the field isn't empty - let add_todo = move |ev: web_sys::KeyboardEvent| { - let target = event_target::(&ev); - ev.stop_propagation(); - let key_code = ev.key_code(); - if key_code == ENTER_KEY { - let title = event_target_value(&ev); - let title = title.trim(); - if !title.is_empty() { - let new = Todo::new(cx, Uuid::new_v4(), title.to_string()); - set_todos.update(|t| t.add(new)); - target.set_value(""); - } + // Callback to add a todo on pressing the `Enter` key, if the field isn't empty + let add_todo = move |ev: web_sys::KeyboardEvent| { + let target = event_target::(&ev); + ev.stop_propagation(); + let key_code = ev.key_code(); + if key_code == ENTER_KEY { + let title = event_target_value(&ev); + let title = title.trim(); + if !title.is_empty() { + let new = Todo::new(cx, Uuid::new_v4(), title.to_string()); + set_todos.update(|t| t.add(new)); + target.set_value(""); + } + } + }; + + // A derived signal that filters the list of the todos depending on the filter mode + // This doesn't need to be a `Memo`, because we're only reading it in one place + let filtered_todos = move || { + todos.with(|todos| match mode.get() { + Mode::All => todos.0.to_vec(), + Mode::Active => todos + .0 + .iter() + .filter(|todo| !todo.completed.get()) + .cloned() + .collect(), + Mode::Completed => todos + .0 + .iter() + .filter(|todo| todo.completed.get()) + .cloned() + .collect(), + }) + }; + + // Serialization + // + // the effect reads the `todos` signal, and each `Todo`'s title and completed + // status, so it will automatically re-run on any change to the list of tasks + // + // this is the main point of `create_effect`: to synchronize reactive state + // with something outside the reactive system (like localStorage) + create_effect(cx, move |_| { + if let Ok(Some(storage)) = window().local_storage() { + let objs = todos + .get() + .0 + .iter() + .map(TodoSerialized::from) + .collect::>(); + let json = + serde_json::to_string(&objs).expect("couldn't serialize Todos"); + if storage.set_item(STORAGE_KEY, &json).is_err() { + log::error!("error while trying to set item in localStorage"); + } + } + }); + + view! { cx, +
+
+
+

"todos"

+ +
+
+ 0)} + on:input=move |_| todos.with(|t| t.toggle_all()) + /> + +
    + } + /> +
+
+
+ + {move || todos.with(|t| t.remaining().to_string())} + {move || if todos.with(|t| t.remaining()) == 1 { + " item" + } else { + " items" + }} + " left" + + + +
+
+ +
} - }; - - // A derived signal that filters the list of the todos depending on the filter mode - // This doesn't need to be a `Memo`, because we're only reading it in one place - let filtered_todos = move || { - todos.with(|todos| match mode.get() { - Mode::All => todos.0.to_vec(), - Mode::Active => todos - .0 - .iter() - .filter(|todo| !todo.completed.get()) - .cloned() - .collect(), - Mode::Completed => todos - .0 - .iter() - .filter(|todo| todo.completed.get()) - .cloned() - .collect(), - }) - }; - - // Serialization - // - // the effect reads the `todos` signal, and each `Todo`'s title and completed - // status, so it will automatically re-run on any change to the list of tasks - // - // this is the main point of `create_effect`: to synchronize reactive state - // with something outside the reactive system (like localStorage) - create_effect(cx, move |_| { - if let Ok(Some(storage)) = window().local_storage() { - let objs = todos - .get() - .0 - .iter() - .map(TodoSerialized::from) - .collect::>(); - let json = - serde_json::to_string(&objs).expect("couldn't serialize Todos"); - if storage.set_item(STORAGE_KEY, &json).is_err() { - log::error!("error while trying to set item in localStorage"); - } - } - }); - - view! { cx, -
-
-
-

"todos"

- -
-
- 0)} - on:input=move |_| todos.with(|t| t.toggle_all()) - /> - -
    - } - /> -
-
-
- - {move || todos.with(|t| t.remaining().to_string())} - {move || if todos.with(|t| t.remaining()) == 1 { - " item" - } else { - " items" - }} - " left" - - - -
-
- -
- } } #[component] pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView { - let (editing, set_editing) = create_signal(cx, false); - let set_todos = use_context::>(cx).unwrap(); + let (editing, set_editing) = create_signal(cx, false); + let set_todos = use_context::>(cx).unwrap(); - // this will be filled by _ref=input below - let todo_input = NodeRef::::new(cx); + // this will be filled by _ref=input below + let todo_input = NodeRef::::new(cx); - let save = move |value: &str| { - let value = value.trim(); - if value.is_empty() { - set_todos.update(|t| t.remove(todo.id)); - } else { - todo.title.set(value.to_string()); + let save = move |value: &str| { + let value = value.trim(); + if value.is_empty() { + set_todos.update(|t| t.remove(todo.id)); + } else { + todo.title.set(value.to_string()); + } + set_editing(false); + }; + + view! { cx, +
  • +
    + + +
    + {move || editing().then(|| view! { cx, + + }) + } +
  • } - set_editing(false); - }; - - view! { cx, -
  • -
    - - -
    - {move || editing().then(|| view! { cx, - - }) - } -
  • - } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Mode { - Active, - Completed, - All, + Active, + Completed, + All, } impl Default for Mode { - fn default() -> Self { - Mode::All - } + fn default() -> Self { + Mode::All + } } pub fn route(hash: &str) -> Mode { - match hash { - "/active" => Mode::Active, - "/completed" => Mode::Completed, - _ => Mode::All, - } + match hash { + "/active" => Mode::Active, + "/completed" => Mode::Completed, + _ => Mode::All, + } }