Justin Starry f61667be97
Remove generics from virtual dom (#783)
* Fix tests

* Remove generics from virtual dom

* Prep for degenerify

* Fix examples

* Remove props cloning

* Fix tests
2019-12-08 10:47:51 -08:00

359 lines
11 KiB
Rust

#![recursion_limit = "512"]
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, InputData, KeyPressEvent, ShouldRender};
const KEY: &'static str = "yew.todomvc.self";
pub struct Model {
link: ComponentLink<Self>,
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 Model {
type Message = Msg;
type Properties = ();
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
let storage = StorageService::new(Area::Local);
let entries = {
if let Json(Ok(restored_model)) = storage.restore(KEY) {
restored_model
} else {
Vec::new()
}
};
let state = State {
entries,
filter: Filter::All,
value: "".into(),
edit_value: "".into(),
};
Model {
link,
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
}
fn view(&self) -> Html {
html! {
<div class="todomvc-wrapper">
<section class="todoapp">
<header class="header">
<h1>{ "todos" }</h1>
{ self.view_input() }
</header>
<section class="main">
<input
type="checkbox"
class="toggle-all"
checked=self.state.is_all_completed()
onclick=self.link.callback(|_| Msg::ToggleAll) />
<ul class="todo-list">
{ for self.state.entries.iter().filter(|e| self.state.filter.fit(e)).enumerate().map(|e| self.view_entry(e)) }
</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=self.link.callback(|_| 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 Model {
fn view_filter(&self, filter: Filter) -> Html {
let flt = filter.clone();
html! {
<li>
<a class=if self.state.filter == flt { "selected" } else { "not-selected" }
href=&flt
onclick=self.link.callback(move |_| Msg::SetFilter(flt.clone()))>
{ filter }
</a>
</li>
}
}
fn view_input(&self) -> Html {
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=self.link.callback(|e: InputData| Msg::Update(e.value))
onkeypress=self.link.callback(|e: KeyPressEvent| {
if e.key() == "Enter" { Msg::Add } else { Msg::Nope }
}) />
/* Or multiline:
<ul>
<li></li>
</ul>
*/
}
}
fn view_entry(&self, (idx, entry): (usize, &Entry)) -> Html {
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
type="checkbox"
class="toggle"
checked=entry.completed
onclick=self.link.callback(move |_| Msg::Toggle(idx)) />
<label ondoubleclick=self.link.callback(move |_| Msg::ToggleEdit(idx))>{ &entry.description }</label>
<button class="destroy" onclick=self.link.callback(move |_| Msg::Remove(idx)) />
</div>
{ self.view_entry_edit_input((idx, &entry)) }
</li>
}
}
fn view_entry_edit_input(&self, (idx, entry): (usize, &Entry)) -> Html {
if entry.editing {
html! {
<input class="edit"
type="text"
value=&entry.description
oninput=self.link.callback(|e: InputData| Msg::UpdateEdit(e.value))
onblur=self.link.callback(move |_| Msg::Edit(idx))
onkeypress=self.link.callback(move |e: KeyPressEvent| {
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);
}
}