mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
* Fix tests * Remove generics from virtual dom * Prep for degenerify * Fix examples * Remove props cloning * Fix tests
359 lines
11 KiB
Rust
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);
|
|
}
|
|
}
|