Skip to content

Commit 05728e1

Browse files
authored
Add Function Components Example (yewstack#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
1 parent 35e1ba6 commit 05728e1

File tree

14 files changed

+597
-0
lines changed

14 files changed

+597
-0
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ members = [
1515
"examples/crm",
1616
"examples/dyn_create_destroy_apps",
1717
"examples/file_upload",
18+
"examples/function_todomvc",
1819
"examples/futures",
1920
"examples/game_of_life",
2021
"examples/inner_html",

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ As an example, check out the TodoMVC example here: <https://examples.yew.rs/todo
3333
| [crm](crm) | Shallow customer relationship management tool |
3434
| [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 |
3535
| [file_upload](file_upload) | Uses the `gloo::file` to read the content of user uploaded files |
36+
| [function_todomvc](function_todomvc) | Implementation of [TodoMVC](http://todomvc.com/) using function components and hooks. |
3637
| [futures](futures) | Demonstrates how you can use futures and async code with Yew. Features a Markdown renderer. |
3738
| [game_of_life](game_of_life) | Implementation of [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) |
3839
| [inner_html](inner_html) | Embeds an external document as raw HTML by manually managing the element |
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "function_todomvc"
3+
version = "0.1.0"
4+
authors = ["Drew Hutton <drew.hutton@pm.me>"]
5+
edition = "2018"
6+
license = "MIT OR Apache-2.0"
7+
8+
[dependencies]
9+
serde = { version = "1.0", features = ["derive"] }
10+
strum = "0.21"
11+
strum_macros = "0.21"
12+
gloo = "0.3"
13+
yew = { path = "../../packages/yew" }
14+
15+
[dependencies.web-sys]
16+
version = "0.3"
17+
features = [
18+
"HtmlInputElement",
19+
]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# TodoMVC Example
2+
3+
[![Demo](https://img.shields.io/website?label=demo&url=https%3A%2F%2Fexamples.yew.rs%2Ffunction_todomvc)](https://examples.yew.rs/function_todomvc)
4+
5+
This is an implementation of [TodoMVC](http://todomvc.com/) for Yew using function components and hooks.
6+
7+
## Concepts
8+
9+
- Uses [`function_components`](https://yew.rs/next/concepts/function-components)
10+
- Uses [`gloo_storage`](https://gloo-rs.web.app/docs/storage) to persist the state
11+
12+
## Improvements
13+
14+
- Use `yew-router` for the hash based routing
15+
- Clean up the code
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<title>Yew • Function TodoMVC</title>
7+
<link
8+
rel="stylesheet"
9+
href="https://cdn.jsdelivr.net/npm/todomvc-common@1.0.5/base.css"
10+
/>
11+
<link
12+
rel="stylesheet"
13+
href="https://cdn.jsdelivr.net/npm/todomvc-app-css@2.3.0/index.css"
14+
/>
15+
</head>
16+
<body></body>
17+
</html>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
pub mod entry;
2+
pub mod filter;
3+
pub mod header_input;
4+
pub mod info_footer;
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
use crate::hooks::use_bool_toggle::use_bool_toggle;
2+
use crate::state::Entry as Item;
3+
use web_sys::{HtmlInputElement, MouseEvent};
4+
use yew::events::{Event, FocusEvent, KeyboardEvent};
5+
use yew::{function_component, html, Callback, Classes, Properties, TargetCast};
6+
7+
#[derive(PartialEq, Properties, Clone)]
8+
pub struct EntryProps {
9+
pub entry: Item,
10+
pub ontoggle: Callback<usize>,
11+
pub onremove: Callback<usize>,
12+
pub onedit: Callback<(usize, String)>,
13+
}
14+
15+
#[function_component(Entry)]
16+
pub fn entry(props: &EntryProps) -> Html {
17+
let id = props.entry.id;
18+
let mut class = Classes::from("todo");
19+
20+
// We use the `use_bool_toggle` hook and set the default value to `false`
21+
// as the default we are not editing the the entry. When we want to edit the
22+
// entry we can call the toggle method on the `UseBoolToggleHandle`
23+
// which will trigger a re-render with the toggle value being `true` for that
24+
// render and after that render the value of toggle will be flipped back to
25+
// its default (`false`).
26+
// We are relying on the behavior of `onblur` and `onkeypress` to cause
27+
// another render so that this component will render again with the
28+
// default value of toggle.
29+
let edit_toggle = use_bool_toggle(false);
30+
let is_editing = *edit_toggle;
31+
32+
if is_editing {
33+
class.push("editing");
34+
}
35+
36+
if props.entry.completed {
37+
class.push("completed");
38+
}
39+
40+
html! {
41+
<li {class}>
42+
<div class="view">
43+
<input
44+
type="checkbox"
45+
class="toggle"
46+
checked={props.entry.completed}
47+
onclick={props.ontoggle.reform(move |_| id)}
48+
/>
49+
<label ondblclick={Callback::once(move |_| {
50+
edit_toggle.toggle();
51+
})}>
52+
{ &props.entry.description }
53+
</label>
54+
<button class="destroy" onclick={props.onremove.reform(move |_| id)} />
55+
</div>
56+
<EntryEdit entry={props.entry.clone()} onedit={props.onedit.clone()} editing={is_editing} />
57+
</li>
58+
}
59+
}
60+
61+
#[derive(PartialEq, Properties, Clone)]
62+
pub struct EntryEditProps {
63+
pub entry: Item,
64+
pub onedit: Callback<(usize, String)>,
65+
pub editing: bool,
66+
}
67+
68+
#[function_component(EntryEdit)]
69+
pub fn entry_edit(props: &EntryEditProps) -> Html {
70+
let id = props.entry.id;
71+
72+
let target_input_value = |e: &Event| {
73+
let input: HtmlInputElement = e.target_unchecked_into();
74+
input.value()
75+
};
76+
77+
let onblur = {
78+
let edit = props.onedit.clone();
79+
80+
move |e: FocusEvent| {
81+
let value = target_input_value(&e);
82+
edit.emit((id, value))
83+
}
84+
};
85+
86+
let onkeypress = {
87+
let edit = props.onedit.clone();
88+
89+
move |e: KeyboardEvent| {
90+
if e.key() == "Enter" {
91+
let value = target_input_value(&e);
92+
edit.emit((id, value))
93+
}
94+
}
95+
};
96+
97+
let onmouseover = |e: MouseEvent| {
98+
e.target_unchecked_into::<HtmlInputElement>()
99+
.focus()
100+
.unwrap_or_default();
101+
};
102+
103+
if props.editing {
104+
html! {
105+
<input
106+
class="edit"
107+
type="text"
108+
value={props.entry.description.clone()}
109+
{onmouseover}
110+
{onblur}
111+
{onkeypress}
112+
/>
113+
}
114+
} else {
115+
html! { <input type="hidden" /> }
116+
}
117+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use crate::state::Filter as FilterEnum;
2+
use yew::{function_component, html, Callback, Properties};
3+
4+
#[derive(PartialEq, Properties)]
5+
pub struct FilterProps {
6+
pub filter: FilterEnum,
7+
pub selected: bool,
8+
pub onset_filter: Callback<FilterEnum>,
9+
}
10+
11+
#[function_component(Filter)]
12+
pub fn filter(props: &FilterProps) -> Html {
13+
let filter = props.filter;
14+
15+
let cls = if props.selected {
16+
"selected"
17+
} else {
18+
"not-selected"
19+
};
20+
21+
html! {
22+
<li>
23+
<a class={cls}
24+
href={props.filter.as_href()}
25+
onclick={props.onset_filter.reform(move |_| filter)}
26+
>
27+
{ props.filter }
28+
</a>
29+
</li>
30+
}
31+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use web_sys::HtmlInputElement;
2+
use yew::events::KeyboardEvent;
3+
use yew::{function_component, html, Callback, Properties, TargetCast};
4+
5+
#[derive(PartialEq, Properties, Clone)]
6+
pub struct HeaderInputProps {
7+
pub onadd: Callback<String>,
8+
}
9+
10+
#[function_component(HeaderInput)]
11+
pub fn header_input(props: &HeaderInputProps) -> Html {
12+
let onkeypress = {
13+
let onadd = props.onadd.clone();
14+
15+
move |e: KeyboardEvent| {
16+
if e.key() == "Enter" {
17+
let input: HtmlInputElement = e.target_unchecked_into();
18+
let value = input.value();
19+
20+
input.set_value("");
21+
onadd.emit(value);
22+
}
23+
}
24+
};
25+
26+
html! {
27+
<input
28+
class="new-todo"
29+
placeholder="What needs to be done?"
30+
{onkeypress}
31+
/>
32+
}
33+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use yew::{function_component, html};
2+
3+
#[function_component(InfoFooter)]
4+
pub fn info_footer() -> Html {
5+
html! {
6+
<footer class="info">
7+
<p>{ "Double-click to edit a todo" }</p>
8+
<p>{ "Written by " }<a href="https://github.com/Yoroshikun/" target="_blank">{ "Drew Hutton <Yoroshi>" }</a></p>
9+
<p>{ "Part of " }<a href="http://todomvc.com/" target="_blank">{ "TodoMVC" }</a></p>
10+
</footer>
11+
}
12+
}

0 commit comments

Comments
 (0)