Skip to content

Commit

Permalink
Add Function Components Example (#2088)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
yoroshikun authored Oct 16, 2021
1 parent 35e1ba6 commit 05728e1
Show file tree
Hide file tree
Showing 14 changed files with 597 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ members = [
"examples/crm",
"examples/dyn_create_destroy_apps",
"examples/file_upload",
"examples/function_todomvc",
"examples/futures",
"examples/game_of_life",
"examples/inner_html",
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| [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 |
| [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. |
| [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 |
Expand Down
19 changes: 19 additions & 0 deletions examples/function_todomvc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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",
]
15 changes: 15 additions & 0 deletions examples/function_todomvc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# TodoMVC Example

[![Demo](https://img.shields.io/website?label=demo&url=https%3A%2F%2Fexamples.yew.rs%2Ffunction_todomvc)](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
17 changes: 17 additions & 0 deletions examples/function_todomvc/index.html
Original file line number Diff line number Diff line change
@@ -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>
4 changes: 4 additions & 0 deletions examples/function_todomvc/src/components.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod entry;
pub mod filter;
pub mod header_input;
pub mod info_footer;
117 changes: 117 additions & 0 deletions examples/function_todomvc/src/components/entry.rs
Original file line number Diff line number Diff line change
@@ -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" /> }
}
}
31 changes: 31 additions & 0 deletions examples/function_todomvc/src/components/filter.rs
Original file line number Diff line number Diff line change
@@ -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>
}
}
33 changes: 33 additions & 0 deletions examples/function_todomvc/src/components/header_input.rs
Original file line number Diff line number Diff line change
@@ -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}
/>
}
}
12 changes: 12 additions & 0 deletions examples/function_todomvc/src/components/info_footer.rs
Original file line number Diff line number Diff line change
@@ -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>
}
}
1 change: 1 addition & 0 deletions examples/function_todomvc/src/hooks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod use_bool_toggle;
70 changes: 70 additions & 0 deletions examples/function_todomvc/src/hooks/use_bool_toggle.rs
Original file line number Diff line number Diff line change
@@ -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,
}
},
|_| {},
)
}
Loading

0 comments on commit 05728e1

Please sign in to comment.