Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update TodoMVC example to use Context API #295

Merged
merged 1 commit into from
Nov 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions examples/todomvc/src/filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use sycamore::context::use_context;
use sycamore::prelude::*;

use crate::{AppState, Filter};

#[component(TodoFilter<G>)]
pub fn todo_filter(filter: Filter) -> Template<G> {
let app_state = use_context::<AppState>();
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))
}
}
}
}
46 changes: 18 additions & 28 deletions examples/todomvc/src/footer.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
use sycamore::context::use_context;
use sycamore::prelude::*;

use crate::{AppState, Filter};
use crate::{
AppState,
Filter,
filter::TodoFilter,
};

#[component(Footer<G>)]
pub fn footer(app_state: AppState) -> Template<G> {
pub fn footer() -> Template<G> {
let app_state = use_context::<AppState>();

let items_text = cloned!((app_state) => move || {
match app_state.todos_left() {
1 => "item",
Expand All @@ -15,8 +22,9 @@ pub fn footer(app_state: AppState) -> Template<G> {
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") {
Expand All @@ -25,35 +33,17 @@ pub fn footer(app_state: AppState) -> Template<G> {
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()
})
Expand Down
8 changes: 6 additions & 2 deletions examples/todomvc/src/header.rs
Original file line number Diff line number Diff line change
@@ -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<G>)]
pub fn header(app_state: AppState) -> Template<G> {
pub fn header() -> Template<G> {
let app_state = use_context::<AppState>();
let value = Signal::new(String::new());

let handle_submit = cloned!((app_state, value) => move |event: Event| {
Expand Down
14 changes: 6 additions & 8 deletions examples/todomvc/src/item.rs
Original file line number Diff line number Diff line change
@@ -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<Todo>,
pub app_state: AppState,
}

#[component(Item<G>)]
pub fn item(props: ItemProps) -> Template<G> {
let ItemProps { todo, app_state } = props;
pub fn item(todo: Signal<Todo>) -> Template<G> {
let app_state = use_context::<AppState>();

let title = cloned!((todo) => move || todo.get().title.clone());
let completed = create_selector(cloned!((todo) => move || todo.get().completed));
Expand Down
13 changes: 10 additions & 3 deletions examples/todomvc/src/list.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
use sycamore::prelude::*;
use sycamore::context::use_context;

use crate::{AppState, Filter};
use crate::{
AppState,
Filter,
item::Item,
};

#[component(List<G>)]
pub fn list(app_state: AppState) -> Template<G> {
pub fn list() -> Template<G> {
let app_state = use_context::<AppState>();
let todos_left = create_selector(cloned!((app_state) => move || {
app_state.todos_left()
}));
Expand Down Expand Up @@ -40,11 +46,12 @@ pub fn list(app_state: AppState) -> Template<G> {
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,
})
}
}
}
}

67 changes: 37 additions & 30 deletions examples/todomvc/src/main.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -128,10 +131,27 @@ impl Filter {
}
}

const KEY: &str = "todos-sycamore";

#[component(App<G>)]
fn app() -> Template<G> {
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()
Expand All @@ -149,40 +169,27 @@ fn app() -> Template<G> {
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() });
});
}