Skip to content

Commit

Permalink
Add TodoMVC example
Browse files Browse the repository at this point in the history
  • Loading branch information
jstarry committed Aug 1, 2019
1 parent 26ef495 commit 7979be0
Show file tree
Hide file tree
Showing 9 changed files with 4,517 additions and 10 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -3,4 +3,6 @@
Cargo.lock
bin/
pkg/
dist/
wasm-pack.log
node_modules
11 changes: 8 additions & 3 deletions Cargo.toml
Expand Up @@ -11,15 +11,20 @@ crate-type = ["cdylib", "rlib"]
default = ["console_error_panic_hook"]

[dependencies]
wasm-bindgen = "0.2"
web_logger = { git = "https://github.com/DenisKolodin/web_logger" }
log = "0.4"
strum = "0.13"
strum_macros = "0.13"
serde = "1"
serde_derive = "1"
wasm-bindgen = "=0.2.42"
web_logger = "0.2"
yew = "0.7"

# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.1", optional = true }
console_error_panic_hook = { version = "0.1.6", optional = true }

# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
# compared to the default allocator's ~10K. It is slower than the default
Expand Down
3 changes: 3 additions & 0 deletions bootstrap.js
@@ -0,0 +1,3 @@
import("./pkg").then(module => {
module.run_app();
});
16 changes: 16 additions & 0 deletions package.json
@@ -0,0 +1,16 @@
{
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack --mode development",
"build": "webpack --mode production",
"start:dev": "webpack-dev-server --mode development"
},
"devDependencies": {
"@wasm-tool/wasm-pack-plugin": "^1.0.0",
"copy-webpack-plugin": "^5.0.4",
"webpack": "^4.29.3",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.7.2"
}
}
347 changes: 347 additions & 0 deletions src/app.rs
@@ -0,0 +1,347 @@
use log::*;
use serde_derive::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use strum_macros::{EnumIter, ToString};
use yew::events::IKeyboardEvent;
use yew::format::Json;
use yew::services::storage::{Area, StorageService};
use yew::{html, Component, ComponentLink, Href, Html, Renderable, ShouldRender};

const KEY: &'static str = "yew.todomvc.self";

pub struct App {
storage: StorageService,
state: State,
}

#[derive(Serialize, Deserialize)]
pub struct State {
entries: Vec<Entry>,
filter: Filter,
value: String,
edit_value: String,
}

#[derive(Serialize, Deserialize)]
struct Entry {
description: String,
completed: bool,
editing: bool,
}

pub enum Msg {
Add,
Edit(usize),
Update(String),
UpdateEdit(String),
Remove(usize),
SetFilter(Filter),
ToggleAll,
ToggleEdit(usize),
Toggle(usize),
ClearCompleted,
Nope,
}

impl Component for App {
type Message = Msg;
type Properties = ();

fn create(_: Self::Properties, _: ComponentLink<Self>) -> Self {
let storage = StorageService::new(Area::Local);
let entries = {
if let Json(Ok(restored_entries)) = storage.restore(KEY) {
restored_entries
} else {
Vec::new()
}
};
let state = State {
entries,
filter: Filter::All,
value: "".into(),
edit_value: "".into(),
};
App { storage, state }
}

fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::Add => {
let entry = Entry {
description: self.state.value.clone(),
completed: false,
editing: false,
};
self.state.entries.push(entry);
self.state.value = "".to_string();
}
Msg::Edit(idx) => {
let edit_value = self.state.edit_value.clone();
self.state.complete_edit(idx, edit_value);
self.state.edit_value = "".to_string();
}
Msg::Update(val) => {
println!("Input: {}", val);
self.state.value = val;
}
Msg::UpdateEdit(val) => {
println!("Input: {}", val);
self.state.edit_value = val;
}
Msg::Remove(idx) => {
self.state.remove(idx);
}
Msg::SetFilter(filter) => {
self.state.filter = filter;
}
Msg::ToggleEdit(idx) => {
self.state.edit_value = self.state.entries[idx].description.clone();
self.state.toggle_edit(idx);
}
Msg::ToggleAll => {
let status = !self.state.is_all_completed();
self.state.toggle_all(status);
}
Msg::Toggle(idx) => {
self.state.toggle(idx);
}
Msg::ClearCompleted => {
self.state.clear_completed();
}
Msg::Nope => {}
}
self.storage.store(KEY, Json(&self.state.entries));
true
}
}

impl Renderable<App> for App {
fn view(&self) -> Html<Self> {
info!("rendered!");
html! {
<div class="todomvc-wrapper">
<section class="todoapp">
<header class="header">
<h1>{ "todos" }</h1>
{ self.view_input() }
</header>
<section class="main">
<input class="toggle-all" type="checkbox" checked=self.state.is_all_completed() onclick=|_| Msg::ToggleAll />
<ul class="todo-list">
{ for self.state.entries.iter().filter(|e| self.state.filter.fit(e)).enumerate().map(view_entry) }
</ul>
</section>
<footer class="footer">
<span class="todo-count">
<strong>{ self.state.total() }</strong>
{ " item(s) left" }
</span>
<ul class="filters">
{ for Filter::iter().map(|flt| self.view_filter(flt)) }
</ul>
<button class="clear-completed" onclick=|_| Msg::ClearCompleted>
{ format!("Clear completed ({})", self.state.total_completed()) }
</button>
</footer>
</section>
<footer class="info">
<p>{ "Double-click to edit a todo" }</p>
<p>{ "Written by " }<a href="https://github.com/DenisKolodin/" target="_blank">{ "Denis Kolodin" }</a></p>
<p>{ "Part of " }<a href="http://todomvc.com/" target="_blank">{ "TodoMVC" }</a></p>
</footer>
</div>
}
}
}

impl App {
fn view_filter(&self, filter: Filter) -> Html<App> {
let flt = filter.clone();
html! {
<li>
<a class=if self.state.filter == flt { "selected" } else { "not-selected" }
href=&flt
onclick=|_| Msg::SetFilter(flt.clone())>
{ filter }
</a>
</li>
}
}

fn view_input(&self) -> Html<App> {
html! {
// You can use standard Rust comments. One line:
// <li></li>
<input class="new-todo"
placeholder="What needs to be done?"
value=&self.state.value
oninput=|e| Msg::Update(e.value)
onkeypress=|e| {
if e.key() == "Enter" { Msg::Add } else { Msg::Nope }
} />
/* Or multiline:
<ul>
<li></li>
</ul>
*/
}
}
}

fn view_entry((idx, entry): (usize, &Entry)) -> Html<App> {
let mut class = "todo".to_string();
if entry.editing {
class.push_str(" editing");
}
if entry.completed {
class.push_str(" completed");
}
html! {
<li class=class>
<div class="view">
<input class="toggle" type="checkbox" checked=entry.completed onclick=|_| Msg::Toggle(idx) />
<label ondoubleclick=|_| Msg::ToggleEdit(idx)>{ &entry.description }</label>
<button class="destroy" onclick=|_| Msg::Remove(idx) />
</div>
{ view_entry_edit_input((idx, &entry)) }
</li>
}
}

fn view_entry_edit_input((idx, entry): (usize, &Entry)) -> Html<App> {
if entry.editing == true {
html! {
<input class="edit"
type="text"
value=&entry.description
oninput=|e| Msg::UpdateEdit(e.value)
onblur=|_| Msg::Edit(idx)
onkeypress=|e| {
if e.key() == "Enter" { Msg::Edit(idx) } else { Msg::Nope }
} />
}
} else {
html! { <input type="hidden" /> }
}
}

#[derive(EnumIter, ToString, Clone, PartialEq, Serialize, Deserialize)]
pub enum Filter {
All,
Active,
Completed,
}

impl<'a> Into<Href> for &'a Filter {
fn into(self) -> Href {
match *self {
Filter::All => "#/".into(),
Filter::Active => "#/active".into(),
Filter::Completed => "#/completed".into(),
}
}
}

impl Filter {
fn fit(&self, entry: &Entry) -> bool {
match *self {
Filter::All => true,
Filter::Active => !entry.completed,
Filter::Completed => entry.completed,
}
}
}

impl State {
fn total(&self) -> usize {
self.entries.len()
}

fn total_completed(&self) -> usize {
self.entries
.iter()
.filter(|e| Filter::Completed.fit(e))
.count()
}

fn is_all_completed(&self) -> bool {
let mut filtered_iter = self
.entries
.iter()
.filter(|e| self.filter.fit(e))
.peekable();

if filtered_iter.peek().is_none() {
return false;
}

filtered_iter.all(|e| e.completed)
}

fn toggle_all(&mut self, value: bool) {
for entry in self.entries.iter_mut() {
if self.filter.fit(entry) {
entry.completed = value;
}
}
}

fn clear_completed(&mut self) {
let entries = self
.entries
.drain(..)
.filter(|e| Filter::Active.fit(e))
.collect();
self.entries = entries;
}

fn toggle(&mut self, idx: usize) {
let filter = self.filter.clone();
let mut entries = self
.entries
.iter_mut()
.filter(|e| filter.fit(e))
.collect::<Vec<_>>();
let entry = entries.get_mut(idx).unwrap();
entry.completed = !entry.completed;
}

fn toggle_edit(&mut self, idx: usize) {
let filter = self.filter.clone();
let mut entries = self
.entries
.iter_mut()
.filter(|e| filter.fit(e))
.collect::<Vec<_>>();
let entry = entries.get_mut(idx).unwrap();
entry.editing = !entry.editing;
}

fn complete_edit(&mut self, idx: usize, val: String) {
let filter = self.filter.clone();
let mut entries = self
.entries
.iter_mut()
.filter(|e| filter.fit(e))
.collect::<Vec<_>>();
let entry = entries.get_mut(idx).unwrap();
entry.description = val;
entry.editing = !entry.editing;
}

fn remove(&mut self, idx: usize) {
let idx = {
let filter = self.filter.clone();
let entries = self
.entries
.iter()
.enumerate()
.filter(|&(_, e)| filter.fit(e))
.collect::<Vec<_>>();
let &(idx, _) = entries.get(idx).unwrap();
idx
};
self.entries.remove(idx);
}
}

0 comments on commit 7979be0

Please sign in to comment.