mirror of https://github.com/yewstack/yew
Add Function Components Example (#2088)
* feat(examples): add function components todo example * chore(examples): apply feedback for function components example * chore(examples): apply more feedback for function components example * feat(examples): implement custom hook for edit boolean toggle * chore(examples): prep for merge, add more documentation * chore(examples): add more descriptive comment to function component hook, fix cargo.toml
This commit is contained in:
parent
35e1ba60aa
commit
05728e1001
|
@ -15,6 +15,7 @@ members = [
|
||||||
"examples/crm",
|
"examples/crm",
|
||||||
"examples/dyn_create_destroy_apps",
|
"examples/dyn_create_destroy_apps",
|
||||||
"examples/file_upload",
|
"examples/file_upload",
|
||||||
|
"examples/function_todomvc",
|
||||||
"examples/futures",
|
"examples/futures",
|
||||||
"examples/game_of_life",
|
"examples/game_of_life",
|
||||||
"examples/inner_html",
|
"examples/inner_html",
|
||||||
|
|
|
@ -33,6 +33,7 @@ As an example, check out the TodoMVC example here: <https://examples.yew.rs/todo
|
||||||
| [crm](crm) | Shallow customer relationship management tool |
|
| [crm](crm) | Shallow customer relationship management tool |
|
||||||
| [dyn_create_destroy_apps](dyn_create_destroy_apps) | Uses the function `start_app_in_element` and the `AppHandle` struct to dynamically create and delete Yew apps |
|
| [dyn_create_destroy_apps](dyn_create_destroy_apps) | Uses the function `start_app_in_element` and the `AppHandle` struct to dynamically create and delete Yew apps |
|
||||||
| [file_upload](file_upload) | Uses the `gloo::file` to read the content of user uploaded files |
|
| [file_upload](file_upload) | Uses the `gloo::file` to read the content of user uploaded files |
|
||||||
|
| [function_todomvc](function_todomvc) | Implementation of [TodoMVC](http://todomvc.com/) using function components and hooks. |
|
||||||
| [futures](futures) | Demonstrates how you can use futures and async code with Yew. Features a Markdown renderer. |
|
| [futures](futures) | Demonstrates how you can use futures and async code with Yew. Features a Markdown renderer. |
|
||||||
| [game_of_life](game_of_life) | Implementation of [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) |
|
| [game_of_life](game_of_life) | Implementation of [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) |
|
||||||
| [inner_html](inner_html) | Embeds an external document as raw HTML by manually managing the element |
|
| [inner_html](inner_html) | Embeds an external document as raw HTML by manually managing the element |
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "function_todomvc"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Drew Hutton <drew.hutton@pm.me>"]
|
||||||
|
edition = "2018"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
strum = "0.21"
|
||||||
|
strum_macros = "0.21"
|
||||||
|
gloo = "0.3"
|
||||||
|
yew = { path = "../../packages/yew" }
|
||||||
|
|
||||||
|
[dependencies.web-sys]
|
||||||
|
version = "0.3"
|
||||||
|
features = [
|
||||||
|
"HtmlInputElement",
|
||||||
|
]
|
|
@ -0,0 +1,15 @@
|
||||||
|
# TodoMVC Example
|
||||||
|
|
||||||
|
[](https://examples.yew.rs/function_todomvc)
|
||||||
|
|
||||||
|
This is an implementation of [TodoMVC](http://todomvc.com/) for Yew using function components and hooks.
|
||||||
|
|
||||||
|
## Concepts
|
||||||
|
|
||||||
|
- Uses [`function_components`](https://yew.rs/next/concepts/function-components)
|
||||||
|
- Uses [`gloo_storage`](https://gloo-rs.web.app/docs/storage) to persist the state
|
||||||
|
|
||||||
|
## Improvements
|
||||||
|
|
||||||
|
- Use `yew-router` for the hash based routing
|
||||||
|
- Clean up the code
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Yew • Function TodoMVC</title>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/todomvc-common@1.0.5/base.css"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/todomvc-app-css@2.3.0/index.css"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod entry;
|
||||||
|
pub mod filter;
|
||||||
|
pub mod header_input;
|
||||||
|
pub mod info_footer;
|
|
@ -0,0 +1,117 @@
|
||||||
|
use crate::hooks::use_bool_toggle::use_bool_toggle;
|
||||||
|
use crate::state::Entry as Item;
|
||||||
|
use web_sys::{HtmlInputElement, MouseEvent};
|
||||||
|
use yew::events::{Event, FocusEvent, KeyboardEvent};
|
||||||
|
use yew::{function_component, html, Callback, Classes, Properties, TargetCast};
|
||||||
|
|
||||||
|
#[derive(PartialEq, Properties, Clone)]
|
||||||
|
pub struct EntryProps {
|
||||||
|
pub entry: Item,
|
||||||
|
pub ontoggle: Callback<usize>,
|
||||||
|
pub onremove: Callback<usize>,
|
||||||
|
pub onedit: Callback<(usize, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(Entry)]
|
||||||
|
pub fn entry(props: &EntryProps) -> Html {
|
||||||
|
let id = props.entry.id;
|
||||||
|
let mut class = Classes::from("todo");
|
||||||
|
|
||||||
|
// We use the `use_bool_toggle` hook and set the default value to `false`
|
||||||
|
// as the default we are not editing the the entry. When we want to edit the
|
||||||
|
// entry we can call the toggle method on the `UseBoolToggleHandle`
|
||||||
|
// which will trigger a re-render with the toggle value being `true` for that
|
||||||
|
// render and after that render the value of toggle will be flipped back to
|
||||||
|
// its default (`false`).
|
||||||
|
// We are relying on the behavior of `onblur` and `onkeypress` to cause
|
||||||
|
// another render so that this component will render again with the
|
||||||
|
// default value of toggle.
|
||||||
|
let edit_toggle = use_bool_toggle(false);
|
||||||
|
let is_editing = *edit_toggle;
|
||||||
|
|
||||||
|
if is_editing {
|
||||||
|
class.push("editing");
|
||||||
|
}
|
||||||
|
|
||||||
|
if props.entry.completed {
|
||||||
|
class.push("completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<li {class}>
|
||||||
|
<div class="view">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle"
|
||||||
|
checked={props.entry.completed}
|
||||||
|
onclick={props.ontoggle.reform(move |_| id)}
|
||||||
|
/>
|
||||||
|
<label ondblclick={Callback::once(move |_| {
|
||||||
|
edit_toggle.toggle();
|
||||||
|
})}>
|
||||||
|
{ &props.entry.description }
|
||||||
|
</label>
|
||||||
|
<button class="destroy" onclick={props.onremove.reform(move |_| id)} />
|
||||||
|
</div>
|
||||||
|
<EntryEdit entry={props.entry.clone()} onedit={props.onedit.clone()} editing={is_editing} />
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Properties, Clone)]
|
||||||
|
pub struct EntryEditProps {
|
||||||
|
pub entry: Item,
|
||||||
|
pub onedit: Callback<(usize, String)>,
|
||||||
|
pub editing: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(EntryEdit)]
|
||||||
|
pub fn entry_edit(props: &EntryEditProps) -> Html {
|
||||||
|
let id = props.entry.id;
|
||||||
|
|
||||||
|
let target_input_value = |e: &Event| {
|
||||||
|
let input: HtmlInputElement = e.target_unchecked_into();
|
||||||
|
input.value()
|
||||||
|
};
|
||||||
|
|
||||||
|
let onblur = {
|
||||||
|
let edit = props.onedit.clone();
|
||||||
|
|
||||||
|
move |e: FocusEvent| {
|
||||||
|
let value = target_input_value(&e);
|
||||||
|
edit.emit((id, value))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let onkeypress = {
|
||||||
|
let edit = props.onedit.clone();
|
||||||
|
|
||||||
|
move |e: KeyboardEvent| {
|
||||||
|
if e.key() == "Enter" {
|
||||||
|
let value = target_input_value(&e);
|
||||||
|
edit.emit((id, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let onmouseover = |e: MouseEvent| {
|
||||||
|
e.target_unchecked_into::<HtmlInputElement>()
|
||||||
|
.focus()
|
||||||
|
.unwrap_or_default();
|
||||||
|
};
|
||||||
|
|
||||||
|
if props.editing {
|
||||||
|
html! {
|
||||||
|
<input
|
||||||
|
class="edit"
|
||||||
|
type="text"
|
||||||
|
value={props.entry.description.clone()}
|
||||||
|
{onmouseover}
|
||||||
|
{onblur}
|
||||||
|
{onkeypress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! { <input type="hidden" /> }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
use crate::state::Filter as FilterEnum;
|
||||||
|
use yew::{function_component, html, Callback, Properties};
|
||||||
|
|
||||||
|
#[derive(PartialEq, Properties)]
|
||||||
|
pub struct FilterProps {
|
||||||
|
pub filter: FilterEnum,
|
||||||
|
pub selected: bool,
|
||||||
|
pub onset_filter: Callback<FilterEnum>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(Filter)]
|
||||||
|
pub fn filter(props: &FilterProps) -> Html {
|
||||||
|
let filter = props.filter;
|
||||||
|
|
||||||
|
let cls = if props.selected {
|
||||||
|
"selected"
|
||||||
|
} else {
|
||||||
|
"not-selected"
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<li>
|
||||||
|
<a class={cls}
|
||||||
|
href={props.filter.as_href()}
|
||||||
|
onclick={props.onset_filter.reform(move |_| filter)}
|
||||||
|
>
|
||||||
|
{ props.filter }
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
use web_sys::HtmlInputElement;
|
||||||
|
use yew::events::KeyboardEvent;
|
||||||
|
use yew::{function_component, html, Callback, Properties, TargetCast};
|
||||||
|
|
||||||
|
#[derive(PartialEq, Properties, Clone)]
|
||||||
|
pub struct HeaderInputProps {
|
||||||
|
pub onadd: Callback<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(HeaderInput)]
|
||||||
|
pub fn header_input(props: &HeaderInputProps) -> Html {
|
||||||
|
let onkeypress = {
|
||||||
|
let onadd = props.onadd.clone();
|
||||||
|
|
||||||
|
move |e: KeyboardEvent| {
|
||||||
|
if e.key() == "Enter" {
|
||||||
|
let input: HtmlInputElement = e.target_unchecked_into();
|
||||||
|
let value = input.value();
|
||||||
|
|
||||||
|
input.set_value("");
|
||||||
|
onadd.emit(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<input
|
||||||
|
class="new-todo"
|
||||||
|
placeholder="What needs to be done?"
|
||||||
|
{onkeypress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
use yew::{function_component, html};
|
||||||
|
|
||||||
|
#[function_component(InfoFooter)]
|
||||||
|
pub fn info_footer() -> Html {
|
||||||
|
html! {
|
||||||
|
<footer class="info">
|
||||||
|
<p>{ "Double-click to edit a todo" }</p>
|
||||||
|
<p>{ "Written by " }<a href="https://github.com/Yoroshikun/" target="_blank">{ "Drew Hutton <Yoroshi>" }</a></p>
|
||||||
|
<p>{ "Part of " }<a href="http://todomvc.com/" target="_blank">{ "TodoMVC" }</a></p>
|
||||||
|
</footer>
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod use_bool_toggle;
|
|
@ -0,0 +1,70 @@
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use yew::functional::use_hook;
|
||||||
|
|
||||||
|
pub struct UseBoolToggleHandle {
|
||||||
|
value: bool,
|
||||||
|
toggle: Rc<dyn Fn()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UseBoolToggleHandle {
|
||||||
|
pub fn toggle(self) {
|
||||||
|
(self.toggle)()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for UseBoolToggleHandle {
|
||||||
|
type Target = bool;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This hook can be used to cause a re-render with the non-default value, which is
|
||||||
|
/// then reset to the default value after that render.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `default` - The default value.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use crate::hooks::use_bool_toggle::use_bool_toggle;
|
||||||
|
/// ...
|
||||||
|
/// let value = use_bool_toggle(false);
|
||||||
|
/// ...
|
||||||
|
/// <button onclick={Callback::once(move |_| {
|
||||||
|
/// value.toggle();
|
||||||
|
/// // This will toggle the value to true.
|
||||||
|
/// // Then render.
|
||||||
|
/// // Post render it will toggle back to false skipping the render.
|
||||||
|
/// })}>
|
||||||
|
/// ...
|
||||||
|
/// ```
|
||||||
|
pub fn use_bool_toggle(default: bool) -> UseBoolToggleHandle {
|
||||||
|
use_hook(
|
||||||
|
|| default,
|
||||||
|
move |hook, updater| {
|
||||||
|
updater.post_render(move |state: &mut bool| {
|
||||||
|
if *state != default {
|
||||||
|
*state = default;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
});
|
||||||
|
|
||||||
|
let toggle = Rc::new(move || {
|
||||||
|
updater.callback(move |st: &mut bool| {
|
||||||
|
*st = !*st;
|
||||||
|
true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
UseBoolToggleHandle {
|
||||||
|
value: *hook,
|
||||||
|
toggle,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|_| {},
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,236 @@
|
||||||
|
use gloo::storage::{LocalStorage, Storage};
|
||||||
|
use state::{Entry, Filter, State};
|
||||||
|
use std::rc::Rc;
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
use yew::{classes, function_component, html, use_effect_with_deps, use_reducer, Callback};
|
||||||
|
|
||||||
|
mod components;
|
||||||
|
mod hooks;
|
||||||
|
mod state;
|
||||||
|
|
||||||
|
use components::{
|
||||||
|
entry::Entry as EntryItem, filter::Filter as FilterItem, header_input::HeaderInput,
|
||||||
|
info_footer::InfoFooter,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum Action {
|
||||||
|
Add(String),
|
||||||
|
Edit((usize, String)),
|
||||||
|
Remove(usize),
|
||||||
|
SetFilter(Filter),
|
||||||
|
ToggleAll,
|
||||||
|
Toggle(usize),
|
||||||
|
ClearCompleted,
|
||||||
|
}
|
||||||
|
|
||||||
|
const KEY: &str = "yew.functiontodomvc.self";
|
||||||
|
|
||||||
|
#[function_component(App)]
|
||||||
|
fn app() -> Html {
|
||||||
|
let state = use_reducer(
|
||||||
|
|prev: Rc<State>, action: Action| match action {
|
||||||
|
Action::Add(description) => {
|
||||||
|
let mut entries = prev.entries.clone();
|
||||||
|
entries.push(Entry {
|
||||||
|
id: entries.last().map(|entry| entry.id + 1).unwrap_or(1),
|
||||||
|
description,
|
||||||
|
completed: false,
|
||||||
|
});
|
||||||
|
State {
|
||||||
|
entries,
|
||||||
|
filter: prev.filter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::Remove(id) => {
|
||||||
|
let mut entries = prev.entries.clone();
|
||||||
|
entries.retain(|entry| entry.id != id);
|
||||||
|
State {
|
||||||
|
entries,
|
||||||
|
filter: prev.filter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::Toggle(id) => {
|
||||||
|
let mut entries = prev.entries.clone();
|
||||||
|
let entry = entries.iter_mut().find(|entry| entry.id == id);
|
||||||
|
if let Some(entry) = entry {
|
||||||
|
entry.completed = !entry.completed;
|
||||||
|
}
|
||||||
|
State {
|
||||||
|
entries,
|
||||||
|
filter: prev.filter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::Edit((id, description)) => {
|
||||||
|
let mut entries = prev.entries.clone();
|
||||||
|
|
||||||
|
if description.is_empty() {
|
||||||
|
entries.retain(|entry| entry.id != id)
|
||||||
|
}
|
||||||
|
|
||||||
|
let entry = entries.iter_mut().find(|entry| entry.id == id);
|
||||||
|
if let Some(entry) = entry {
|
||||||
|
entry.description = description;
|
||||||
|
}
|
||||||
|
State {
|
||||||
|
entries,
|
||||||
|
filter: prev.filter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::ToggleAll => {
|
||||||
|
let mut entries = prev.entries.clone();
|
||||||
|
for entry in &mut entries {
|
||||||
|
if prev.filter.fits(entry) {
|
||||||
|
entry.completed = !entry.completed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
State {
|
||||||
|
entries,
|
||||||
|
filter: prev.filter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::ClearCompleted => {
|
||||||
|
let mut entries = prev.entries.clone();
|
||||||
|
entries.retain(|e| Filter::Active.fits(e));
|
||||||
|
State {
|
||||||
|
entries,
|
||||||
|
filter: prev.filter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::SetFilter(filter) => State {
|
||||||
|
filter,
|
||||||
|
entries: prev.entries.clone(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Initial state
|
||||||
|
State {
|
||||||
|
entries: LocalStorage::get(KEY).unwrap_or_else(|_| vec![]),
|
||||||
|
filter: Filter::All, // TODO: get from uri
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Effect
|
||||||
|
use_effect_with_deps(
|
||||||
|
move |state| {
|
||||||
|
LocalStorage::set(KEY, &state.clone().entries).expect("failed to set");
|
||||||
|
|| ()
|
||||||
|
},
|
||||||
|
state.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
let onremove = {
|
||||||
|
let state = state.clone();
|
||||||
|
Callback::from(move |id: usize| state.dispatch(Action::Remove(id)))
|
||||||
|
};
|
||||||
|
|
||||||
|
let ontoggle = {
|
||||||
|
let state = state.clone();
|
||||||
|
Callback::from(move |id: usize| state.dispatch(Action::Toggle(id)))
|
||||||
|
};
|
||||||
|
|
||||||
|
let ontoggle_all = {
|
||||||
|
let state = state.clone();
|
||||||
|
Callback::from(move |_| state.dispatch(Action::ToggleAll))
|
||||||
|
};
|
||||||
|
|
||||||
|
let onclear_completed = {
|
||||||
|
let state = state.clone();
|
||||||
|
Callback::from(move |_| state.dispatch(Action::ClearCompleted))
|
||||||
|
};
|
||||||
|
|
||||||
|
let onedit = {
|
||||||
|
let state = state.clone();
|
||||||
|
Callback::from(move |(id, value): (usize, String)| {
|
||||||
|
state.dispatch(Action::Edit((id, value)));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let onadd = {
|
||||||
|
let state = state.clone();
|
||||||
|
Callback::from(move |value: String| {
|
||||||
|
state.dispatch(Action::Add(value));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let onset_filter = {
|
||||||
|
let state = state.clone();
|
||||||
|
Callback::from(move |filter: Filter| {
|
||||||
|
state.dispatch(Action::SetFilter(filter));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
let completed = state
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| Filter::Completed.fits(entry))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
let is_all_completed = state
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.all(|e| state.filter.fits(e) & e.completed);
|
||||||
|
|
||||||
|
let total = state.entries.len();
|
||||||
|
|
||||||
|
let hidden_class = if state.entries.is_empty() {
|
||||||
|
"hidden"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="todomvc-wrapper">
|
||||||
|
<section class="todoapp">
|
||||||
|
<header class="header">
|
||||||
|
<h1>{ "todos" }</h1>
|
||||||
|
<HeaderInput {onadd} />
|
||||||
|
</header>
|
||||||
|
<section class={classes!("main", hidden_class)}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle-all"
|
||||||
|
id="toggle-all"
|
||||||
|
checked={is_all_completed}
|
||||||
|
onclick={ontoggle_all}
|
||||||
|
/>
|
||||||
|
<label for="toggle-all" />
|
||||||
|
<ul class="todo-list">
|
||||||
|
{ for state.entries.iter().filter(|e| state.filter.fits(e)).cloned().map(|entry|
|
||||||
|
html! {
|
||||||
|
<EntryItem {entry}
|
||||||
|
ontoggle={ontoggle.clone()}
|
||||||
|
onremove={onremove.clone()}
|
||||||
|
onedit={onedit.clone()}
|
||||||
|
/>
|
||||||
|
}) }
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<footer class={classes!("footer", hidden_class)}>
|
||||||
|
<span class="todo-count">
|
||||||
|
<strong>{ total }</strong>
|
||||||
|
{ " item(s) left" }
|
||||||
|
</span>
|
||||||
|
<ul class="filters">
|
||||||
|
{ for Filter::iter().map(|filter| {
|
||||||
|
html! {
|
||||||
|
<FilterItem {filter}
|
||||||
|
selected={state.filter == filter}
|
||||||
|
onset_filter={onset_filter.clone()}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}) }
|
||||||
|
</ul>
|
||||||
|
<button class="clear-completed" onclick={onclear_completed}>
|
||||||
|
{ format!("Clear completed ({})", completed) }
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
|
<InfoFooter />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
yew::start_app::<App>();
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use strum_macros::{EnumIter, ToString};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct State {
|
||||||
|
pub entries: Vec<Entry>,
|
||||||
|
pub filter: Filter,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct Entry {
|
||||||
|
pub id: usize,
|
||||||
|
pub description: String,
|
||||||
|
pub completed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, EnumIter, ToString, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum Filter {
|
||||||
|
All,
|
||||||
|
Active,
|
||||||
|
Completed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filter {
|
||||||
|
pub fn fits(&self, entry: &Entry) -> bool {
|
||||||
|
match *self {
|
||||||
|
Filter::All => true,
|
||||||
|
Filter::Active => !entry.completed,
|
||||||
|
Filter::Completed => entry.completed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_href(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Filter::All => "#/",
|
||||||
|
Filter::Active => "#/active",
|
||||||
|
Filter::Completed => "#/completed",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue