From 3a2b3926abe61033942886db04120a5b336948a8 Mon Sep 17 00:00:00 2001 From: Ryan Haywood Date: Fri, 5 Nov 2021 22:38:18 -0400 Subject: [PATCH] Update TodoMVC example to use Context API Does what it says on the tin. Closes https://github.com/sycamore-rs/sycamore/issues/279 --- examples/todomvc/src/filter.rs | 25 +++++++++++++ examples/todomvc/src/footer.rs | 46 +++++++++-------------- examples/todomvc/src/header.rs | 8 +++- examples/todomvc/src/item.rs | 14 +++---- examples/todomvc/src/list.rs | 13 +++++-- examples/todomvc/src/main.rs | 67 +++++++++++++++++++--------------- 6 files changed, 102 insertions(+), 71 deletions(-) create mode 100644 examples/todomvc/src/filter.rs diff --git a/examples/todomvc/src/filter.rs b/examples/todomvc/src/filter.rs new file mode 100644 index 000000000..0cd102657 --- /dev/null +++ b/examples/todomvc/src/filter.rs @@ -0,0 +1,25 @@ +use sycamore::context::use_context; +use sycamore::prelude::*; + +use crate::{AppState, Filter}; + +#[component(TodoFilter)] +pub fn todo_filter(filter: Filter) -> Template { + let app_state = use_context::(); + let selected = cloned!((app_state) => move || filter == *app_state.filter.get()); + let set_filter = cloned!((app_state) => move |filter| { + app_state.filter.set(filter) + }); + + template! { + li { + a( + class=if selected() { "selected" } else { "" }, + href=filter.url(), + on:click=move |_| set_filter(filter), + ) { + (format!("{:?}", filter)) + } + } + } +} diff --git a/examples/todomvc/src/footer.rs b/examples/todomvc/src/footer.rs index 73d99d468..b5343ee53 100644 --- a/examples/todomvc/src/footer.rs +++ b/examples/todomvc/src/footer.rs @@ -1,9 +1,16 @@ +use sycamore::context::use_context; use sycamore::prelude::*; -use crate::{AppState, Filter}; +use crate::{ + AppState, + Filter, + filter::TodoFilter, +}; #[component(Footer)] -pub fn footer(app_state: AppState) -> Template { +pub fn footer() -> Template { + let app_state = use_context::(); + let items_text = cloned!((app_state) => move || { match app_state.todos_left() { 1 => "item", @@ -15,8 +22,9 @@ pub fn footer(app_state: AppState) -> Template { app_state.todos_left() < app_state.todos.get().len() })); - let app_state2 = app_state.clone(); - let app_state3 = app_state.clone(); + let handle_clear_completed = cloned!((app_state) => move |_| { + app_state.clear_completed() + }); template! { footer(class="footer") { @@ -25,35 +33,17 @@ pub fn footer(app_state: AppState) -> Template { span { " " (items_text()) " left" } } ul(class="filters") { - Indexed(IndexedProps { - iterable: Signal::new(vec![Filter::All, Filter::Active, Filter::Completed]).handle(), - template: cloned!((app_state2) => move |filter| { - let selected = cloned!((app_state2) => move || filter == *app_state2.filter.get()); - let set_filter = cloned!((app_state2) => move |filter| { - app_state2.filter.set(filter) - }); - - template! { - li { - a( - class=if selected() { "selected" } else { "" }, - href=filter.url(), - on:click=move |_| set_filter(filter), - ) { - (format!("{:?}", filter)) - } - } - } - }) - }) + TodoFilter(Filter::All) + TodoFilter(Filter::Active) + TodoFilter(Filter::Completed) } (if *has_completed_todos.get() { - template! { - button(class="clear-completed", on:click=cloned!((app_state3) => move |_| app_state3.clear_completed())) { + cloned!((handle_clear_completed) => template! { + button(class="clear-completed", on:click=handle_clear_completed) { "Clear completed" } - } + }) } else { Template::empty() }) diff --git a/examples/todomvc/src/header.rs b/examples/todomvc/src/header.rs index 892108a96..72d8de658 100644 --- a/examples/todomvc/src/header.rs +++ b/examples/todomvc/src/header.rs @@ -1,11 +1,15 @@ -use sycamore::prelude::*; +use sycamore::{ + context::use_context, + prelude::*, +}; use wasm_bindgen::JsCast; use web_sys::{Event, KeyboardEvent}; use crate::AppState; #[component(Header)] -pub fn header(app_state: AppState) -> Template { +pub fn header() -> Template { + let app_state = use_context::(); let value = Signal::new(String::new()); let handle_submit = cloned!((app_state, value) => move |event: Event| { diff --git a/examples/todomvc/src/item.rs b/examples/todomvc/src/item.rs index 61156cc20..9f9805022 100644 --- a/examples/todomvc/src/item.rs +++ b/examples/todomvc/src/item.rs @@ -1,17 +1,15 @@ -use sycamore::prelude::*; +use sycamore::{ + prelude::*, + context::use_context, +}; use wasm_bindgen::JsCast; use web_sys::{Event, HtmlInputElement, KeyboardEvent}; use crate::{AppState, Todo}; -pub struct ItemProps { - pub todo: Signal, - pub app_state: AppState, -} - #[component(Item)] -pub fn item(props: ItemProps) -> Template { - let ItemProps { todo, app_state } = props; +pub fn item(todo: Signal) -> Template { + let app_state = use_context::(); let title = cloned!((todo) => move || todo.get().title.clone()); let completed = create_selector(cloned!((todo) => move || todo.get().completed)); diff --git a/examples/todomvc/src/list.rs b/examples/todomvc/src/list.rs index a28b07130..c42f9a791 100644 --- a/examples/todomvc/src/list.rs +++ b/examples/todomvc/src/list.rs @@ -1,9 +1,15 @@ use sycamore::prelude::*; +use sycamore::context::use_context; -use crate::{AppState, Filter}; +use crate::{ + AppState, + Filter, + item::Item, +}; #[component(List)] -pub fn list(app_state: AppState) -> Template { +pub fn list() -> Template { + let app_state = use_context::(); let todos_left = create_selector(cloned!((app_state) => move || { app_state.todos_left() })); @@ -40,7 +46,7 @@ pub fn list(app_state: AppState) -> Template { Keyed(KeyedProps { iterable: filtered_todos, template: move |todo| template! { - crate::item::Item(crate::item::ItemProps { todo, app_state: app_state.clone() }) + Item(todo) }, key: |todo| todo.get().id, }) @@ -48,3 +54,4 @@ pub fn list(app_state: AppState) -> Template { } } } + diff --git a/examples/todomvc/src/main.rs b/examples/todomvc/src/main.rs index 8da495507..e13726f78 100644 --- a/examples/todomvc/src/main.rs +++ b/examples/todomvc/src/main.rs @@ -1,10 +1,13 @@ mod copyright; +mod filter; mod footer; mod header; mod item; mod list; +use log::Level::Debug; use serde::{Deserialize, Serialize}; +use sycamore::context::{ContextProvider, ContextProviderProps}; use sycamore::prelude::*; use uuid::Uuid; @@ -128,10 +131,27 @@ impl Filter { } } -const KEY: &str = "todos-sycamore"; - #[component(App)] fn app() -> Template { + template! { + div(class="todomvc-wrapper") { + section(class="todoapp") { + header::Header() + list::List() + footer::Footer() + } + copyright::Copyright() + } + } +} + +const KEY: &str = "todos-sycamore"; + +fn main() { + console_error_panic_hook::set_once(); + console_log::init_with_level(Debug).unwrap(); + + // Initialize application state let local_storage = web_sys::window() .unwrap() .local_storage() @@ -149,40 +169,27 @@ fn app() -> Template { filter: Signal::new(Filter::get_filter_from_hash()), }; + // Set up an effect that runs a function anytime app_state.todos changes create_effect(cloned!((local_storage, app_state) => move || { for todo in app_state.todos.get().iter() { todo.get(); // subscribe to changes in all todos } - local_storage.set_item(KEY, &serde_json::to_string(app_state.todos.get().as_ref()).unwrap()).unwrap(); })); - let todos_is_empty = - create_selector(cloned!((app_state) => move || app_state.todos.get().len() == 0)); - - template! { - div(class="todomvc-wrapper") { - section(class="todoapp") { - header::Header(app_state.clone()) - - (if !*todos_is_empty.get() { - template! { - list::List(app_state.clone()) - footer::Footer(app_state.clone()) - } - } else { - Template::empty() - }) - } - - copyright::Copyright() + /* + The application's root component. We use a provider to 'provide' access + to our app_state via the `use_context` API, which can be used from any + level in the view tree. + */ + sycamore::render(|| { + template! { + ContextProvider(ContextProviderProps { + value: app_state, + children: || template! { + App() + } + }) } - } -} - -fn main() { - console_error_panic_hook::set_once(); - console_log::init_with_level(log::Level::Debug).unwrap(); - - sycamore::render(|| template! { App() }); + }); }