Reorganize benchmarks and add more SSR benchmarks

This commit is contained in:
Greg Johnston
2022-10-22 21:40:41 -04:00
parent b5e3fcae0a
commit 3382891790
8 changed files with 1827 additions and 321 deletions

View File

@@ -3,8 +3,26 @@ name = "benchmarks"
version = "0.1.0"
edition = "2021"
[dev-dependencies]
[dependencies]
leptos = { path = "../leptos", default-features = false, features = ["ssr"] }
sycamore = { version = "0.8", features = ["ssr"] }
yew = { git = "https://github.com/yewstack/yew", features = ["ssr", "tokio"] }
tokio-test = "0.4"
yew = { git = "https://github.com/yewstack/yew", features = ["ssr"] }
tokio-test = "0.4"
miniserde = "0.1"
gloo = "0.8"
uuid = { version = "1", features = ["serde", "v4", "wasm-bindgen"] }
wasm-bindgen = "0.2"
log = "0.4"
strum = "0.24"
strum_macros = "0.24"
serde = { version = "1", features = ["derive", "rc"]}
serde_json = "1"
[dependencies.web-sys]
version = "0.3"
features = [
"Window",
"Document",
"HtmlElement",
"HtmlInputElement"
]

View File

@@ -1,322 +1,7 @@
#![feature(test)]
extern crate test;
/*
mod reactive {
use test::Bencher;
use std::{cell::Cell, rc::Rc};
#[bench]
fn leptos_create_1000_signals(b: &mut Bencher) {
use leptos::{create_isomorphic_effect, create_memo, create_scope, create_signal};
b.iter(|| {
create_scope(|cx| {
let acc = Rc::new(Cell::new(0));
let sigs = (0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
let memo = create_memo(cx, move |_| reads.iter().map(|r| r.get()).sum::<i32>());
assert_eq!(memo(), 499500);
})
.dispose()
});
}
#[bench]
fn leptos_create_and_update_1000_signals(b: &mut Bencher) {
use leptos::{create_isomorphic_effect, create_memo, create_scope, create_signal};
b.iter(|| {
create_scope(|cx| {
let acc = Rc::new(Cell::new(0));
let sigs = (0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
let memo = create_memo(cx, move |_| reads.iter().map(|r| r.get()).sum::<i32>());
assert_eq!(memo(), 499500);
create_isomorphic_effect(cx, {
let acc = Rc::clone(&acc);
move |_| {
acc.set(memo());
}
});
assert_eq!(acc.get(), 499500);
writes[1].update(|n| *n += 1);
writes[10].update(|n| *n += 1);
writes[100].update(|n| *n += 1);
assert_eq!(acc.get(), 499503);
assert_eq!(memo(), 499503);
})
.dispose()
});
}
#[bench]
fn leptos_create_and_dispose_1000_scopes(b: &mut Bencher) {
use leptos::{create_isomorphic_effect, create_scope, create_signal};
b.iter(|| {
let acc = Rc::new(Cell::new(0));
let disposers = (0..1000)
.map(|_| {
create_scope({
let acc = Rc::clone(&acc);
move |cx| {
let (r, w) = create_signal(cx, 0);
create_isomorphic_effect(cx, {
move |_| {
acc.set(r());
}
});
w.update(|n| *n += 1);
}
})
})
.collect::<Vec<_>>();
for disposer in disposers {
disposer.dispose();
}
});
}
#[bench]
fn sycamore_create_1000_signals(b: &mut Bencher) {
use sycamore::reactive::{create_effect, create_memo, create_scope, create_signal};
b.iter(|| {
let d = create_scope(|cx| {
let acc = Rc::new(Cell::new(0));
let sigs = Rc::new((0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>());
let memo = create_memo(cx, {
let sigs = Rc::clone(&sigs);
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
});
assert_eq!(*memo.get(), 499500);
});
unsafe { d.dispose() };
});
}
#[bench]
fn sycamore_create_and_update_1000_signals(b: &mut Bencher) {
use sycamore::reactive::{create_effect, create_memo, create_scope, create_signal};
b.iter(|| {
let d = create_scope(|cx| {
let acc = Rc::new(Cell::new(0));
let sigs = Rc::new((0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>());
let memo = create_memo(cx, {
let sigs = Rc::clone(&sigs);
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
});
assert_eq!(*memo.get(), 499500);
create_effect(cx, {
let acc = Rc::clone(&acc);
move || {
acc.set(*memo.get());
}
});
assert_eq!(acc.get(), 499500);
sigs[1].set(*sigs[1].get() + 1);
sigs[10].set(*sigs[10].get() + 1);
sigs[100].set(*sigs[100].get() + 1);
assert_eq!(acc.get(), 499503);
assert_eq!(*memo.get(), 499503);
});
unsafe { d.dispose() };
});
}
#[bench]
fn sycamore_create_and_dispose_1000_scopes(b: &mut Bencher) {
use sycamore::reactive::{create_effect, create_scope, create_signal};
b.iter(|| {
let acc = Rc::new(Cell::new(0));
let disposers = (0..1000)
.map(|_| {
create_scope({
let acc = Rc::clone(&acc);
move |cx| {
let s = create_signal(cx, 0);
create_effect(cx, {
move || {
acc.set(*s.get());
}
});
s.set(*s.get() + 1);
}
})
})
.collect::<Vec<_>>();
for disposer in disposers {
unsafe {
disposer.dispose();
}
}
});
}
} */
mod ssr {
use test::Bencher;
#[bench]
fn leptos_ssr_bench(b: &mut Bencher) {
use leptos::*;
b.iter(|| {
_ = create_scope(|cx| {
#[component]
fn Counter(cx: Scope, initial: i32) -> Element {
let (value, set_value) = create_signal(cx, initial);
view! {
cx,
<div>
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
<span>"Value: " {move || value().to_string()} "!"</span>
<button on:click=move |_| set_value.update(|value| *value += 1)>"+1"</button>
</div>
}
}
let rendered = view! {
cx,
<main>
<h1>"Welcome to our benchmark page."</h1>
<p>"Here's some introductory text."</p>
<Counter initial=1/>
<Counter initial=2/>
<Counter initial=3/>
</main>
};
assert_eq!(
rendered,
"<main data-hk=\"0-0\"><h1>Welcome to our benchmark page.</h1><p>Here's some introductory text.</p><!--#--><div data-hk=\"0-2-0\"><button>-1</button><span>Value: <!--#-->1<!--/-->!</span><button>+1</button></div><!--/--><!--#--><div data-hk=\"0-3-0\"><button>-1</button><span>Value: <!--#-->2<!--/-->!</span><button>+1</button></div><!--/--><!--#--><div data-hk=\"0-4-0\"><button>-1</button><span>Value: <!--#-->3<!--/-->!</span><button>+1</button></div><!--/--></main>"
);
});
});
}
#[bench]
fn sycamore_ssr_bench(b: &mut Bencher) {
use sycamore::*;
use sycamore::prelude::*;
b.iter(|| {
_ = create_scope(|cx| {
#[derive(Prop)]
struct CounterProps {
initial: i32
}
#[component]
fn Counter<G: Html>(cx: Scope, props: CounterProps) -> View<G> {
let value = create_signal(cx, props.initial);
view! {
cx,
div {
button(on:click=|_| value.set(*value.get() - 1)) {
"-1"
}
span {
"Value: "
(value.get().to_string())
"!"
}
button(on:click=|_| value.set(*value.get() + 1)) {
"+1"
}
}
}
}
let rendered = render_to_string(|cx| view! {
cx,
main {
h1 {
"Welcome to our benchmark page."
}
p {
"Here's some introductory text."
}
Counter(initial = 1)
Counter(initial = 2)
Counter(initial = 3)
}
});
assert_eq!(
rendered,
"<main data-hk=\"0.0\"><h1 data-hk=\"0.1\">Welcome to our benchmark page.</h1><p data-hk=\"0.2\">Here's some introductory text.</p><!--#--><div data-hk=\"1.0\"><button data-hk=\"1.1\">-1</button><span data-hk=\"1.2\">Value: <!--#-->1<!--/-->!</span><button data-hk=\"1.3\">+1</button></div><!--/--><!----><!--#--><div data-hk=\"2.0\"><button data-hk=\"2.1\">-1</button><span data-hk=\"2.2\">Value: <!--#-->2<!--/-->!</span><button data-hk=\"2.3\">+1</button></div><!--/--><!----><!--#--><div data-hk=\"3.0\"><button data-hk=\"3.1\">-1</button><span data-hk=\"3.2\">Value: <!--#-->3<!--/-->!</span><button data-hk=\"3.3\">+1</button></div><!--/--></main>"
);
});
});
}
#[bench]
fn yew_ssr_bench(b: &mut Bencher) {
use yew::prelude::*;
use yew::ServerRenderer;
b.iter(|| {
#[derive(Properties, PartialEq, Eq, Debug)]
struct CounterProps {
initial: i32
}
#[function_component(Counter)]
fn counter(props: &CounterProps) -> Html {
let state = use_state(|| props.initial);
let incr_counter = {
let state = state.clone();
Callback::from(move |_| state.set(&*state + 1))
};
let decr_counter = {
let state = state.clone();
Callback::from(move |_| state.set(&*state - 1))
};
html! {
<div>
<h1>{"Welcome to our benchmark page."}</h1>
<p>{"Here's some introductory text."}</p>
<button onclick={decr_counter}> {"-1"} </button>
<p> {"Value: "} {*state} {"!"} </p>
<button onclick={incr_counter}> {"+1"} </button>
</div>
}
}
#[function_component]
fn App() -> Html {
html! {
<main>
<Counter initial=1/>
<Counter initial=2/>
<Counter initial=3/>
</main>
}
}
tokio_test::block_on(async {
let renderer = ServerRenderer::<App>::new();
let rendered = renderer.render().await;
assert_eq!(
rendered,
"<!--<[]>--><main><!--<[]>--><div><h1>Welcome to our benchmark page.</h1><p>Here's some introductory text.</p><button>-1</button><p>Value: 1!</p><button>+1</button></div><!--</[]>--><!--<[]>--><div><h1>Welcome to our benchmark page.</h1><p>Here's some introductory text.</p><button>-1</button><p>Value: 2!</p><button>+1</button></div><!--</[]>--><!--<[]>--><div><h1>Welcome to our benchmark page.</h1><p>Here's some introductory text.</p><button>-1</button><p>Value: 3!</p><button>+1</button></div><!--</[]>--></main><!--</[]>-->"
);
});
});
}
}
mod reactive;
mod ssr;
mod todomvc;

159
benchmarks/src/reactive.rs Normal file
View File

@@ -0,0 +1,159 @@
use test::Bencher;
use std::{cell::Cell, rc::Rc};
#[bench]
fn leptos_create_1000_signals(b: &mut Bencher) {
use leptos::{create_isomorphic_effect, create_memo, create_scope, create_signal};
b.iter(|| {
create_scope(|cx| {
let acc = Rc::new(Cell::new(0));
let sigs = (0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
let memo = create_memo(cx, move |_| reads.iter().map(|r| r.get()).sum::<i32>());
assert_eq!(memo(), 499500);
})
.dispose()
});
}
#[bench]
fn leptos_create_and_update_1000_signals(b: &mut Bencher) {
use leptos::{create_isomorphic_effect, create_memo, create_scope, create_signal};
b.iter(|| {
create_scope(|cx| {
let acc = Rc::new(Cell::new(0));
let sigs = (0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
let memo = create_memo(cx, move |_| reads.iter().map(|r| r.get()).sum::<i32>());
assert_eq!(memo(), 499500);
create_isomorphic_effect(cx, {
let acc = Rc::clone(&acc);
move |_| {
acc.set(memo());
}
});
assert_eq!(acc.get(), 499500);
writes[1].update(|n| *n += 1);
writes[10].update(|n| *n += 1);
writes[100].update(|n| *n += 1);
assert_eq!(acc.get(), 499503);
assert_eq!(memo(), 499503);
})
.dispose()
});
}
#[bench]
fn leptos_create_and_dispose_1000_scopes(b: &mut Bencher) {
use leptos::{create_isomorphic_effect, create_scope, create_signal};
b.iter(|| {
let acc = Rc::new(Cell::new(0));
let disposers = (0..1000)
.map(|_| {
create_scope({
let acc = Rc::clone(&acc);
move |cx| {
let (r, w) = create_signal(cx, 0);
create_isomorphic_effect(cx, {
move |_| {
acc.set(r());
}
});
w.update(|n| *n += 1);
}
})
})
.collect::<Vec<_>>();
for disposer in disposers {
disposer.dispose();
}
});
}
#[bench]
fn sycamore_create_1000_signals(b: &mut Bencher) {
use sycamore::reactive::{create_effect, create_memo, create_scope, create_signal};
b.iter(|| {
let d = create_scope(|cx| {
let acc = Rc::new(Cell::new(0));
let sigs = Rc::new((0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>());
let memo = create_memo(cx, {
let sigs = Rc::clone(&sigs);
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
});
assert_eq!(*memo.get(), 499500);
});
unsafe { d.dispose() };
});
}
#[bench]
fn sycamore_create_and_update_1000_signals(b: &mut Bencher) {
use sycamore::reactive::{create_effect, create_memo, create_scope, create_signal};
b.iter(|| {
let d = create_scope(|cx| {
let acc = Rc::new(Cell::new(0));
let sigs = Rc::new((0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>());
let memo = create_memo(cx, {
let sigs = Rc::clone(&sigs);
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
});
assert_eq!(*memo.get(), 499500);
create_effect(cx, {
let acc = Rc::clone(&acc);
move || {
acc.set(*memo.get());
}
});
assert_eq!(acc.get(), 499500);
sigs[1].set(*sigs[1].get() + 1);
sigs[10].set(*sigs[10].get() + 1);
sigs[100].set(*sigs[100].get() + 1);
assert_eq!(acc.get(), 499503);
assert_eq!(*memo.get(), 499503);
});
unsafe { d.dispose() };
});
}
#[bench]
fn sycamore_create_and_dispose_1000_scopes(b: &mut Bencher) {
use sycamore::reactive::{create_effect, create_scope, create_signal};
b.iter(|| {
let acc = Rc::new(Cell::new(0));
let disposers = (0..1000)
.map(|_| {
create_scope({
let acc = Rc::clone(&acc);
move |cx| {
let s = create_signal(cx, 0);
create_effect(cx, {
move || {
acc.set(*s.get());
}
});
s.set(*s.get() + 1);
}
})
})
.collect::<Vec<_>>();
for disposer in disposers {
unsafe {
disposer.dispose();
}
}
});
}

154
benchmarks/src/ssr.rs Normal file
View File

@@ -0,0 +1,154 @@
use test::Bencher;
#[bench]
fn leptos_ssr_bench(b: &mut Bencher) {
use leptos::*;
b.iter(|| {
_ = create_scope(|cx| {
#[component]
fn Counter(cx: Scope, initial: i32) -> Element {
let (value, set_value) = create_signal(cx, initial);
view! {
cx,
<div>
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
<span>"Value: " {move || value().to_string()} "!"</span>
<button on:click=move |_| set_value.update(|value| *value += 1)>"+1"</button>
</div>
}
}
let rendered = view! {
cx,
<main>
<h1>"Welcome to our benchmark page."</h1>
<p>"Here's some introductory text."</p>
<Counter initial=1/>
<Counter initial=2/>
<Counter initial=3/>
</main>
};
assert_eq!(
rendered,
"<main data-hk=\"0-0\"><h1>Welcome to our benchmark page.</h1><p>Here's some introductory text.</p><!--#--><div data-hk=\"0-2-0\"><button>-1</button><span>Value: <!--#-->1<!--/-->!</span><button>+1</button></div><!--/--><!--#--><div data-hk=\"0-3-0\"><button>-1</button><span>Value: <!--#-->2<!--/-->!</span><button>+1</button></div><!--/--><!--#--><div data-hk=\"0-4-0\"><button>-1</button><span>Value: <!--#-->3<!--/-->!</span><button>+1</button></div><!--/--></main>"
);
});
});
}
#[bench]
fn sycamore_ssr_bench(b: &mut Bencher) {
use sycamore::*;
use sycamore::prelude::*;
b.iter(|| {
_ = create_scope(|cx| {
#[derive(Prop)]
struct CounterProps {
initial: i32
}
#[component]
fn Counter<G: Html>(cx: Scope, props: CounterProps) -> View<G> {
let value = create_signal(cx, props.initial);
view! {
cx,
div {
button(on:click=|_| value.set(*value.get() - 1)) {
"-1"
}
span {
"Value: "
(value.get().to_string())
"!"
}
button(on:click=|_| value.set(*value.get() + 1)) {
"+1"
}
}
}
}
let rendered = render_to_string(|cx| view! {
cx,
main {
h1 {
"Welcome to our benchmark page."
}
p {
"Here's some introductory text."
}
Counter(initial = 1)
Counter(initial = 2)
Counter(initial = 3)
}
});
assert_eq!(
rendered,
"<main data-hk=\"0.0\"><h1 data-hk=\"0.1\">Welcome to our benchmark page.</h1><p data-hk=\"0.2\">Here's some introductory text.</p><!--#--><div data-hk=\"1.0\"><button data-hk=\"1.1\">-1</button><span data-hk=\"1.2\">Value: <!--#-->1<!--/-->!</span><button data-hk=\"1.3\">+1</button></div><!--/--><!----><!--#--><div data-hk=\"2.0\"><button data-hk=\"2.1\">-1</button><span data-hk=\"2.2\">Value: <!--#-->2<!--/-->!</span><button data-hk=\"2.3\">+1</button></div><!--/--><!----><!--#--><div data-hk=\"3.0\"><button data-hk=\"3.1\">-1</button><span data-hk=\"3.2\">Value: <!--#-->3<!--/-->!</span><button data-hk=\"3.3\">+1</button></div><!--/--></main>"
);
});
});
}
#[bench]
fn yew_ssr_bench(b: &mut Bencher) {
use yew::prelude::*;
use yew::ServerRenderer;
b.iter(|| {
#[derive(Properties, PartialEq, Eq, Debug)]
struct CounterProps {
initial: i32
}
#[function_component(Counter)]
fn counter(props: &CounterProps) -> Html {
let state = use_state(|| props.initial);
let incr_counter = {
let state = state.clone();
Callback::from(move |_| state.set(&*state + 1))
};
let decr_counter = {
let state = state.clone();
Callback::from(move |_| state.set(&*state - 1))
};
html! {
<div>
<h1>{"Welcome to our benchmark page."}</h1>
<p>{"Here's some introductory text."}</p>
<button onclick={decr_counter}> {"-1"} </button>
<p> {"Value: "} {*state} {"!"} </p>
<button onclick={incr_counter}> {"+1"} </button>
</div>
}
}
#[function_component]
fn App() -> Html {
html! {
<main>
<Counter initial=1/>
<Counter initial=2/>
<Counter initial=3/>
</main>
}
}
tokio_test::block_on(async {
let renderer = ServerRenderer::<App>::new();
let rendered = renderer.render().await;
assert_eq!(
rendered,
"<!--<[]>--><main><!--<[]>--><div><h1>Welcome to our benchmark page.</h1><p>Here's some introductory text.</p><button>-1</button><p>Value: 1!</p><button>+1</button></div><!--</[]>--><!--<[]>--><div><h1>Welcome to our benchmark page.</h1><p>Here's some introductory text.</p><button>-1</button><p>Value: 2!</p><button>+1</button></div><!--</[]>--><!--<[]>--><div><h1>Welcome to our benchmark page.</h1><p>Here's some introductory text.</p><button>-1</button><p>Value: 3!</p><button>+1</button></div><!--</[]>--></main><!--</[]>-->"
);
});
});
}

View File

@@ -0,0 +1,324 @@
use leptos::*;
use miniserde::*;
use web_sys::HtmlInputElement;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Todos(pub Vec<Todo>);
const STORAGE_KEY: &str = "todos-leptos";
impl Todos {
pub fn new(cx: Scope) -> Self {
Self(vec![])
}
pub fn new_with_1000(cx: Scope) -> Self {
let todos = (0..1000)
.map(|id| Todo::new(cx, id, format!("Todo #{id}")))
.collect();
Self(todos)
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn add(&mut self, todo: Todo) {
self.0.push(todo);
}
pub fn remove(&mut self, id: usize) {
self.0.retain(|todo| todo.id != id);
}
pub fn remaining(&self) -> usize {
self.0.iter().filter(|todo| !(todo.completed)()).count()
}
pub fn completed(&self) -> usize {
self.0.iter().filter(|todo| (todo.completed)()).count()
}
pub fn toggle_all(&self) {
// if all are complete, mark them all active instead
if self.remaining() == 0 {
for todo in &self.0 {
if todo.completed.get() {
(todo.set_completed)(false);
}
}
}
// otherwise, mark them all complete
else {
for todo in &self.0 {
(todo.set_completed)(true);
}
}
}
fn clear_completed(&mut self) {
self.0.retain(|todo| !todo.completed.get());
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Todo {
pub id: usize,
pub title: ReadSignal<String>,
pub set_title: WriteSignal<String>,
pub completed: ReadSignal<bool>,
pub set_completed: WriteSignal<bool>,
}
impl Todo {
pub fn new(cx: Scope, id: usize, title: String) -> Self {
Self::new_with_completed(cx, id, title, false)
}
pub fn new_with_completed(cx: Scope, id: usize, title: String, completed: bool) -> Self {
let (title, set_title) = create_signal(cx, title);
let (completed, set_completed) = create_signal(cx, completed);
Self {
id,
title,
set_title,
completed,
set_completed,
}
}
pub fn toggle(&self) {
self.set_completed
.update(|completed| *completed = !*completed);
}
}
const ESCAPE_KEY: u32 = 27;
const ENTER_KEY: u32 = 13;
#[component]
pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
let mut next_id = todos
.0
.iter()
.map(|todo| todo.id)
.max()
.map(|last| last + 1)
.unwrap_or(0);
let (todos, set_todos) = create_signal(cx, todos);
provide_context(cx, set_todos);
let (mode, set_mode) = create_signal(cx, Mode::All);
window_event_listener("hashchange", move |_| {
let new_mode = location_hash().map(|hash| route(&hash)).unwrap_or_default();
set_mode(new_mode);
});
let add_todo = move |ev: web_sys::Event| {
let target = event_target::<HtmlInputElement>(&ev);
ev.stop_propagation();
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
if key_code == ENTER_KEY {
let title = event_target_value(&ev);
let title = title.trim();
if !title.is_empty() {
let new = Todo::new(cx, next_id, title.to_string());
set_todos.update(|t| t.add(new));
next_id += 1;
target.set_value("");
}
}
};
let filtered_todos = create_memo::<Vec<Todo>>(cx, move |_| {
todos.with(|todos| match mode.get() {
Mode::All => todos.0.to_vec(),
Mode::Active => todos
.0
.iter()
.filter(|todo| !todo.completed.get())
.cloned()
.collect(),
Mode::Completed => todos
.0
.iter()
.filter(|todo| todo.completed.get())
.cloned()
.collect(),
})
});
// effect to serialize to JSON
// this does reactive reads, so it will automatically serialize on any relevant change
create_effect(cx, move |_| {
if let Ok(Some(storage)) = window().local_storage() {
let objs = todos
.get()
.0
.iter()
.map(TodoSerialized::from)
.collect::<Vec<_>>();
let json = json::to_string(&objs);
if storage.set_item(STORAGE_KEY, &json).is_err() {
log::error!("error while trying to set item in localStorage");
}
}
});
view! { cx,
<main>
<section class="todoapp">
<header class="header">
<h1>"todos"</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus on:keydown=add_todo />
</header>
<section class="main" class:hidden={move || todos.with(|t| t.is_empty())}>
<input id="toggle-all" class="toggle-all" type="checkbox"
prop:checked={move || todos.with(|t| t.remaining() > 0)}
on:input=move |_| set_todos.update(|t| t.toggle_all())
/>
<label for="toggle-all">"Mark all as complete"</label>
<ul class="todo-list">
<For each=filtered_todos key=|todo| todo.id>
{move |cx, todo: &Todo| view! { cx, <Todo todo=todo.clone() /> }}
</For>
</ul>
</section>
<footer class="footer" class:hidden={move || todos.with(|t| t.is_empty())}>
<span class="todo-count">
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
{move || if todos.with(|t| t.remaining()) == 1 {
" item"
} else {
" items"
}}
" left"
</span>
<ul class="filters">
<li><a href="#/" class="selected" class:selected={move || mode() == Mode::All}>"All"</a></li>
<li><a href="#/active" class:selected={move || mode() == Mode::Active}>"Active"</a></li>
<li><a href="#/completed" class:selected={move || mode() == Mode::Completed}>"Completed"</a></li>
</ul>
<button
class="clear-completed hidden"
class:hidden={move || todos.with(|t| t.completed() == 0)}
on:click=move |_| set_todos.update(|t| t.clear_completed())
>
"Clear completed"
</button>
</footer>
</section>
<footer class="info">
<p>"Double-click to edit a todo"</p>
<p>"Created by "<a href="http://todomvc.com">"Greg Johnston"</a></p>
<p>"Part of "<a href="http://todomvc.com">"TodoMVC"</a></p>
</footer>
</main>
}
}
#[component]
pub fn Todo(cx: Scope, todo: Todo) -> Element {
let (editing, set_editing) = create_signal(cx, false);
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
let input: Element;
let save = move |value: &str| {
let value = value.trim();
if value.is_empty() {
set_todos.update(|t| t.remove(todo.id));
} else {
(todo.set_title)(value.to_string());
}
set_editing(false);
};
let tpl = view! { cx,
<li
class="todo"
class:editing={editing}
class:completed={move || (todo.completed)()}
_ref=input
>
<div class="view">
<input
class="toggle"
type="checkbox"
prop:checked={move || (todo.completed)()}
on:input={move |ev| {
let checked = event_target_checked(&ev);
(todo.set_completed)(checked);
}}
/>
<label on:dblclick=move |_| set_editing(true)>
{move || todo.title.get()}
</label>
<button class="destroy" on:click=move |_| set_todos.update(|t| t.remove(todo.id))/>
</div>
{move || editing().then(|| view! { cx,
<input
class="edit"
class:hidden={move || !(editing)()}
prop:value={move || todo.title.get()}
on:focusout=move |ev| save(&event_target_value(&ev))
on:keyup={move |ev| {
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
if key_code == ENTER_KEY {
save(&event_target_value(&ev));
} else if key_code == ESCAPE_KEY {
set_editing(false);
}
}}
/>
})
}
</li>
};
tpl
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Active,
Completed,
All,
}
impl Default for Mode {
fn default() -> Self {
Mode::All
}
}
pub fn route(hash: &str) -> Mode {
match hash {
"/active" => Mode::Active,
"/completed" => Mode::Completed,
_ => Mode::All,
}
}
#[derive(Serialize, Deserialize)]
pub struct TodoSerialized {
pub id: usize,
pub title: String,
pub completed: bool,
}
impl TodoSerialized {
pub fn into_todo(self, cx: Scope) -> Todo {
Todo::new_with_completed(cx, self.id, self.title, self.completed)
}
}
impl From<&Todo> for TodoSerialized {
fn from(todo: &Todo) -> Self {
Self {
id: todo.id,
title: todo.title.get(),
completed: (todo.completed)(),
}
}
}

View File

@@ -0,0 +1,109 @@
use test::Bencher;
mod leptos;
mod sycamore;
mod yew;
#[bench]
fn leptos_todomvc_ssr(b: &mut Bencher) {
use self::leptos::*;
use ::leptos::*;
b.iter(|| {
_ = create_scope(|cx| {
let rendered = view! {
cx,
<TodoMVC todos=Todos::new(cx)/>
};
assert!(rendered.len() > 1);
});
});
}
#[bench]
fn sycamore_todomvc_ssr(b: &mut Bencher) {
use self::sycamore::*;
use ::sycamore::prelude::*;
use ::sycamore::*;
b.iter(|| {
_ = create_scope(|cx| {
let rendered = render_to_string(|cx| {
view! {
cx,
App()
}
});
assert!(rendered.len() > 1);
});
});
}
#[bench]
fn yew_todomvc_ssr(b: &mut Bencher) {
use self::yew::*;
use ::yew::prelude::*;
use ::yew::ServerRenderer;
b.iter(|| {
tokio_test::block_on(async {
let renderer = ServerRenderer::<App>::new();
let rendered = renderer.render().await;
assert!(rendered.len() > 1);
});
});
}
#[bench]
fn leptos_todomvc_ssr_with_1000(b: &mut Bencher) {
use self::leptos::*;
use ::leptos::*;
b.iter(|| {
_ = create_scope(|cx| {
let rendered = view! {
cx,
<TodoMVC todos=Todos::new_with_1000(cx)/>
};
assert!(rendered.len() > 1);
});
});
}
#[bench]
fn sycamore_todomvc_ssr_with_1000(b: &mut Bencher) {
use self::sycamore::*;
use ::sycamore::prelude::*;
use ::sycamore::*;
b.iter(|| {
_ = create_scope(|cx| {
let rendered = render_to_string(|cx| {
view! {
cx,
AppWith1000()
}
});
assert!(rendered.len() > 1);
});
});
}
#[bench]
fn yew_todomvc_ssr_with_1000(b: &mut Bencher) {
use self::yew::*;
use ::yew::prelude::*;
use ::yew::ServerRenderer;
b.iter(|| {
tokio_test::block_on(async {
let renderer = ServerRenderer::<AppWith1000>::new();
let rendered = renderer.render().await;
assert!(rendered.len() > 1);
});
});
}

View File

@@ -0,0 +1,438 @@
use serde::{Deserialize, Serialize};
use sycamore::prelude::*;
use uuid::Uuid;
use wasm_bindgen::JsCast;
use web_sys::{Event, HtmlInputElement, KeyboardEvent};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
pub struct Todo {
title: String,
completed: bool,
id: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Filter {
All,
Active,
Completed,
}
impl Default for Filter {
fn default() -> Self {
Self::All
}
}
impl Filter {
fn url(self) -> &'static str {
match self {
Filter::All => "#",
Filter::Active => "#/active",
Filter::Completed => "#/completed",
}
}
fn get_filter_from_hash() -> Self {
let hash = web_sys::window().unwrap().location().hash().unwrap();
match hash.as_str() {
"#/active" => Filter::Active,
"#/completed" => Filter::Completed,
_ => Filter::All,
}
}
}
#[derive(Debug, Default, Clone)]
pub struct AppState {
pub todos: RcSignal<Vec<RcSignal<Todo>>>,
pub filter: RcSignal<Filter>,
}
impl AppState {
fn add_todo(&self, title: String, id: usize) {
self.todos.modify().push(create_rc_signal(Todo {
title,
completed: false,
id,
}))
}
fn remove_todo(&self, id: usize) {
self.todos.modify().retain(|todo| todo.get().id != id);
}
fn todos_left(&self) -> usize {
self.todos.get().iter().fold(
0,
|acc, todo| if todo.get().completed { acc } else { acc + 1 },
)
}
fn toggle_complete_all(&self) {
if self.todos_left() == 0 {
// make all todos active
for todo in self.todos.get().iter() {
if todo.get().completed {
todo.set(Todo {
completed: false,
..todo.get().as_ref().clone()
})
}
}
} else {
// make all todos completed
for todo in self.todos.get().iter() {
if !todo.get().completed {
todo.set(Todo {
completed: true,
..todo.get().as_ref().clone()
})
}
}
}
}
fn clear_completed(&self) {
self.todos.modify().retain(|todo| !todo.get().completed);
}
}
const KEY: &str = "todos-sycamore";
#[component]
pub fn App<G: Html>(cx: Scope) -> View<G> {
// Initialize application state
let todos = create_rc_signal(Vec::new());
let app_state = AppState {
todos,
filter: create_rc_signal(Filter::All),
};
provide_context(cx, app_state);
view! { cx,
div(class="todomvc-wrapper") {
section(class="todoapp") {
Header {}
List {}
Footer {}
}
Copyright {}
}
}
}
#[component]
pub fn AppWith1000<G: Html>(cx: Scope) -> View<G> {
// Initialize application state
let todos = (0..1000)
.map(|id| {
create_rc_signal(Todo {
title: format!("Todo #{id}"),
completed: false,
id,
})
})
.collect();
let todos = create_rc_signal(todos);
let app_state = AppState {
todos,
filter: create_rc_signal(Filter::All),
};
provide_context(cx, app_state);
view! { cx,
div(class="todomvc-wrapper") {
section(class="todoapp") {
Header {}
List {}
Footer {}
}
Copyright {}
}
}
}
#[component]
pub fn Copyright<G: Html>(cx: Scope) -> View<G> {
view! { cx,
footer(class="info") {
p { "Double click to edit a todo" }
p {
"Created by "
a(href="https://github.com/lukechu10", target="_blank") { "lukechu10" }
}
p {
"Part of "
a(href="http://todomvc.com") { "TodoMVC" }
}
}
}
}
#[component]
pub fn Header<G: Html>(cx: Scope) -> View<G> {
let app_state = use_context::<AppState>(cx);
let value = create_signal(cx, String::new());
let input_ref = create_node_ref(cx);
let handle_submit = |event: Event| {
let event: KeyboardEvent = event.unchecked_into();
if event.key() == "Enter" {
let mut task = value.get().as_ref().clone();
task = task.trim().to_string();
if !task.is_empty() {
app_state.add_todo(task, 0);
value.set("".to_string());
input_ref
.get::<DomNode>()
.unchecked_into::<HtmlInputElement>()
.set_value("");
}
}
};
view! { cx,
header(class="header") {
h1 { "todos" }
input(ref=input_ref,
class="new-todo",
placeholder="What needs to be done?",
bind:value=value,
on:keyup=handle_submit,
)
}
}
}
#[component(inline_props)]
pub fn Item<G: Html>(cx: Scope, todo: RcSignal<Todo>) -> View<G> {
let app_state = use_context::<AppState>(cx);
// Make `todo` live as long as the scope.
let todo = create_ref(cx, todo);
let title = || todo.get().title.clone();
let completed = create_selector(cx, || todo.get().completed);
let id = todo.get().id;
let editing = create_signal(cx, false);
let input_ref = create_node_ref(cx);
let value = create_signal(cx, "".to_string());
let handle_input = |event: Event| {
let target: HtmlInputElement = event.target().unwrap().unchecked_into();
value.set(target.value());
};
let toggle_completed = |_| {
todo.set(Todo {
completed: !todo.get().completed,
..todo.get().as_ref().clone()
});
};
let handle_dblclick = move |_| {
editing.set(true);
input_ref
.get::<DomNode>()
.unchecked_into::<HtmlInputElement>()
.focus()
.unwrap();
value.set(title());
};
let handle_blur = move || {
editing.set(false);
let mut value = value.get().as_ref().clone();
value = value.trim().to_string();
if value.is_empty() {
app_state.remove_todo(id);
} else {
todo.set(Todo {
title: value,
..todo.get().as_ref().clone()
})
}
};
let handle_submit = move |event: Event| {
let event: KeyboardEvent = event.unchecked_into();
match event.key().as_str() {
"Enter" => handle_blur(),
"Escape" => {
input_ref
.get::<DomNode>()
.unchecked_into::<HtmlInputElement>()
.set_value(&title());
editing.set(false);
}
_ => {}
}
};
let handle_destroy = move |_| {
app_state.remove_todo(id);
};
// We need a separate signal for checked because clicking the checkbox will detach the binding
// between the attribute and the view.
let checked = create_signal(cx, false);
create_effect(cx, || {
// Calling checked.set will also update the `checked` property on the input element.
checked.set(*completed.get())
});
let class = || {
format!(
"{} {}",
if *completed.get() { "completed" } else { "" },
if *editing.get() { "editing" } else { "" }
)
};
view! { cx,
li(class=class()) {
div(class="view") {
input(
class="toggle",
type="checkbox",
on:input=toggle_completed,
bind:checked=checked
)
label(on:dblclick=handle_dblclick) {
(title())
}
button(class="destroy", on:click=handle_destroy)
}
(if *editing.get() {
view! { cx,
input(ref=input_ref,
class="edit",
prop:value=&todo.get().title,
on:blur=move |_| handle_blur(),
on:keyup=handle_submit,
on:input=handle_input,
)
}
} else {
View::empty()
})
}
}
}
#[component]
pub fn List<G: Html>(cx: Scope) -> View<G> {
let app_state = use_context::<AppState>(cx);
let todos_left = create_selector(cx, || app_state.todos_left());
let filtered_todos = create_memo(cx, || {
app_state
.todos
.get()
.iter()
.filter(|todo| match *app_state.filter.get() {
Filter::All => true,
Filter::Active => !todo.get().completed,
Filter::Completed => todo.get().completed,
})
.cloned()
.collect::<Vec<_>>()
});
// We need a separate signal for checked because clicking the checkbox will detach the binding
// between the attribute and the view.
let checked = create_signal(cx, false);
create_effect(cx, || {
// Calling checked.set will also update the `checked` property on the input element.
checked.set(*todos_left.get() == 0)
});
view! { cx,
section(class="main") {
input(
id="toggle-all",
class="toggle-all",
type="checkbox",
readonly=true,
bind:checked=checked,
on:input=|_| app_state.toggle_complete_all()
)
label(for="toggle-all")
ul(class="todo-list") {
Keyed(
iterable=filtered_todos,
view=|cx, todo| view! { cx,
Item(todo=todo)
},
key=|todo| todo.get().id,
)
}
}
}
}
#[component(inline_props)]
pub fn TodoFilter<G: Html>(cx: Scope, filter: Filter) -> View<G> {
let app_state = use_context::<AppState>(cx);
let selected = move || filter == *app_state.filter.get();
let set_filter = |filter| app_state.filter.set(filter);
view! { cx,
li {
a(
class=if selected() { "selected" } else { "" },
href=filter.url(),
on:click=move |_| set_filter(filter),
) {
(format!("{filter:?}"))
}
}
}
}
#[component]
pub fn Footer<G: Html>(cx: Scope) -> View<G> {
let app_state = use_context::<AppState>(cx);
let items_text = || match app_state.todos_left() {
1 => "item",
_ => "items",
};
let has_completed_todos =
create_selector(cx, || app_state.todos_left() < app_state.todos.get().len());
let handle_clear_completed = |_| app_state.clear_completed();
view! { cx,
footer(class="footer") {
span(class="todo-count") {
strong { (app_state.todos_left()) }
span { " " (items_text()) " left" }
}
ul(class="filters") {
TodoFilter(filter=Filter::All)
TodoFilter(filter=Filter::Active)
TodoFilter(filter=Filter::Completed)
}
(if *has_completed_todos.get() {
view! { cx,
button(class="clear-completed", on:click=handle_clear_completed) {
"Clear completed"
}
}
} else {
view! { cx, }
})
}
}
}

View File

@@ -0,0 +1,619 @@
use gloo::storage::{LocalStorage, Storage};
use strum::IntoEnumIterator;
use web_sys::HtmlInputElement as InputElement;
use yew::events::{FocusEvent, KeyboardEvent};
use yew::html::Scope;
use yew::{classes, html, Classes, Component, Context, Html, NodeRef, TargetCast};
const KEY: &str = "yew.todomvc.self";
pub enum Msg {
Add(String),
Edit((usize, String)),
Remove(usize),
SetFilter(Filter),
ToggleAll,
ToggleEdit(usize),
Toggle(usize),
ClearCompleted,
Focus,
}
pub struct App {
state: State,
focus_ref: NodeRef,
}
impl Component for App {
type Message = Msg;
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
let entries = vec![]; //LocalStorage::get(KEY).unwrap_or_else(|_| Vec::new());
let state = State {
entries,
filter: Filter::All,
edit_value: "".into(),
};
let focus_ref = NodeRef::default();
Self { state, focus_ref }
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::Add(description) => {
if !description.is_empty() {
let entry = Entry {
description: description.trim().to_string(),
completed: false,
editing: false,
};
self.state.entries.push(entry);
}
}
Msg::Edit((idx, edit_value)) => {
self.state.complete_edit(idx, edit_value.trim().to_string());
self.state.edit_value = "".to_string();
}
Msg::Remove(idx) => {
self.state.remove(idx);
}
Msg::SetFilter(filter) => {
self.state.filter = filter;
}
Msg::ToggleEdit(idx) => {
let entry = self
.state
.entries
.iter()
.filter(|e| self.state.filter.fits(e))
.nth(idx)
.unwrap();
self.state.edit_value = entry.description.clone();
self.state.clear_all_edit();
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::Focus => {
if let Some(input) = self.focus_ref.cast::<InputElement>() {
input.focus().unwrap();
}
}
}
LocalStorage::set(KEY, &self.state.entries).expect("failed to set");
true
}
fn view(&self, ctx: &Context<Self>) -> Html {
let hidden_class = if self.state.entries.is_empty() {
"hidden"
} else {
""
};
html! {
<div class="todomvc-wrapper">
<section class="todoapp">
<header class="header">
<h1>{ "todos" }</h1>
{ self.view_input(ctx.link()) }
</header>
<section class={classes!("main", hidden_class)}>
<input
type="checkbox"
class="toggle-all"
id="toggle-all"
checked={self.state.is_all_completed()}
onclick={ctx.link().callback(|_| Msg::ToggleAll)}
/>
<label for="toggle-all" />
<ul class="todo-list">
{ for self.state.entries.iter().filter(|e| self.state.filter.fits(e)).enumerate().map(|e| self.view_entry(e, ctx.link())) }
</ul>
</section>
<footer class={classes!("footer", hidden_class)}>
<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, ctx.link())) }
</ul>
<button class="clear-completed" onclick={ctx.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 App {
fn view_filter(&self, filter: Filter, link: &Scope<Self>) -> Html {
let cls = if self.state.filter == filter {
"selected"
} else {
"not-selected"
};
html! {
<li>
<a class={cls}
href={filter.as_href()}
onclick={link.callback(move |_| Msg::SetFilter(filter))}
>
{ filter }
</a>
</li>
}
}
fn view_input(&self, link: &Scope<Self>) -> Html {
let onkeypress = link.batch_callback(|e: KeyboardEvent| {
if e.key() == "Enter" {
let input: InputElement = e.target_unchecked_into();
let value = input.value();
input.set_value("");
Some(Msg::Add(value))
} else {
None
}
});
html! {
// You can use standard Rust comments. One line:
// <li></li>
<input
class="new-todo"
placeholder="What needs to be done?"
{onkeypress}
/>
/* Or multiline:
<ul>
<li></li>
</ul>
*/
}
}
fn view_entry(&self, (idx, entry): (usize, &Entry), link: &Scope<Self>) -> Html {
let mut class = Classes::from("todo");
if entry.editing {
class.push(" editing");
}
if entry.completed {
class.push(" completed");
}
html! {
<li {class}>
<div class="view">
<input
type="checkbox"
class="toggle"
checked={entry.completed}
onclick={link.callback(move |_| Msg::Toggle(idx))}
/>
<label ondblclick={link.callback(move |_| Msg::ToggleEdit(idx))}>{ &entry.description }</label>
<button class="destroy" onclick={link.callback(move |_| Msg::Remove(idx))} />
</div>
{ self.view_entry_edit_input((idx, entry), link) }
</li>
}
}
fn view_entry_edit_input(&self, (idx, entry): (usize, &Entry), link: &Scope<Self>) -> Html {
let edit = move |input: InputElement| {
let value = input.value();
input.set_value("");
Msg::Edit((idx, value))
};
let onblur = link.callback(move |e: FocusEvent| edit(e.target_unchecked_into()));
let onkeypress = link.batch_callback(move |e: KeyboardEvent| {
(e.key() == "Enter").then(|| edit(e.target_unchecked_into()))
});
if entry.editing {
html! {
<input
class="edit"
type="text"
ref={self.focus_ref.clone()}
value={self.state.edit_value.clone()}
onmouseover={link.callback(|_| Msg::Focus)}
{onblur}
{onkeypress}
/>
}
} else {
html! { <input type="hidden" /> }
}
}
}
pub struct AppWith1000 {
state: State,
focus_ref: NodeRef,
}
impl Component for AppWith1000 {
type Message = Msg;
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
let entries = (0..1000)
.map(|id| Entry {
description: format!("Todo #{id}"),
completed: false,
editing: false,
})
.collect();
let state = State {
entries,
filter: Filter::All,
edit_value: "".into(),
};
let focus_ref = NodeRef::default();
Self { state, focus_ref }
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::Add(description) => {
if !description.is_empty() {
let entry = Entry {
description: description.trim().to_string(),
completed: false,
editing: false,
};
self.state.entries.push(entry);
}
}
Msg::Edit((idx, edit_value)) => {
self.state.complete_edit(idx, edit_value.trim().to_string());
self.state.edit_value = "".to_string();
}
Msg::Remove(idx) => {
self.state.remove(idx);
}
Msg::SetFilter(filter) => {
self.state.filter = filter;
}
Msg::ToggleEdit(idx) => {
let entry = self
.state
.entries
.iter()
.filter(|e| self.state.filter.fits(e))
.nth(idx)
.unwrap();
self.state.edit_value = entry.description.clone();
self.state.clear_all_edit();
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::Focus => {
if let Some(input) = self.focus_ref.cast::<InputElement>() {
input.focus().unwrap();
}
}
}
LocalStorage::set(KEY, &self.state.entries).expect("failed to set");
true
}
fn view(&self, ctx: &Context<Self>) -> Html {
let hidden_class = if self.state.entries.is_empty() {
"hidden"
} else {
""
};
html! {
<div class="todomvc-wrapper">
<section class="todoapp">
<header class="header">
<h1>{ "todos" }</h1>
{ self.view_input(ctx.link()) }
</header>
<section class={classes!("main", hidden_class)}>
<input
type="checkbox"
class="toggle-all"
id="toggle-all"
checked={self.state.is_all_completed()}
onclick={ctx.link().callback(|_| Msg::ToggleAll)}
/>
<label for="toggle-all" />
<ul class="todo-list">
{ for self.state.entries.iter().filter(|e| self.state.filter.fits(e)).enumerate().map(|e| self.view_entry(e, ctx.link())) }
</ul>
</section>
<footer class={classes!("footer", hidden_class)}>
<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, ctx.link())) }
</ul>
<button class="clear-completed" onclick={ctx.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 AppWith1000 {
fn view_filter(&self, filter: Filter, link: &Scope<Self>) -> Html {
let cls = if self.state.filter == filter {
"selected"
} else {
"not-selected"
};
html! {
<li>
<a class={cls}
href={filter.as_href()}
onclick={link.callback(move |_| Msg::SetFilter(filter))}
>
{ filter }
</a>
</li>
}
}
fn view_input(&self, link: &Scope<Self>) -> Html {
let onkeypress = link.batch_callback(|e: KeyboardEvent| {
if e.key() == "Enter" {
let input: InputElement = e.target_unchecked_into();
let value = input.value();
input.set_value("");
Some(Msg::Add(value))
} else {
None
}
});
html! {
// You can use standard Rust comments. One line:
// <li></li>
<input
class="new-todo"
placeholder="What needs to be done?"
{onkeypress}
/>
/* Or multiline:
<ul>
<li></li>
</ul>
*/
}
}
fn view_entry(&self, (idx, entry): (usize, &Entry), link: &Scope<Self>) -> Html {
let mut class = Classes::from("todo");
if entry.editing {
class.push(" editing");
}
if entry.completed {
class.push(" completed");
}
html! {
<li {class}>
<div class="view">
<input
type="checkbox"
class="toggle"
checked={entry.completed}
onclick={link.callback(move |_| Msg::Toggle(idx))}
/>
<label ondblclick={link.callback(move |_| Msg::ToggleEdit(idx))}>{ &entry.description }</label>
<button class="destroy" onclick={link.callback(move |_| Msg::Remove(idx))} />
</div>
{ self.view_entry_edit_input((idx, entry), link) }
</li>
}
}
fn view_entry_edit_input(&self, (idx, entry): (usize, &Entry), link: &Scope<Self>) -> Html {
let edit = move |input: InputElement| {
let value = input.value();
input.set_value("");
Msg::Edit((idx, value))
};
let onblur = link.callback(move |e: FocusEvent| edit(e.target_unchecked_into()));
let onkeypress = link.batch_callback(move |e: KeyboardEvent| {
(e.key() == "Enter").then(|| edit(e.target_unchecked_into()))
});
if entry.editing {
html! {
<input
class="edit"
type="text"
ref={self.focus_ref.clone()}
value={self.state.edit_value.clone()}
onmouseover={link.callback(|_| Msg::Focus)}
{onblur}
{onkeypress}
/>
}
} else {
html! { <input type="hidden" /> }
}
}
}
use serde::{Deserialize, Serialize};
use strum_macros::{Display, EnumIter};
#[derive(Debug, Serialize, Deserialize)]
pub struct State {
pub entries: Vec<Entry>,
pub filter: Filter,
pub edit_value: String,
}
impl State {
pub fn total(&self) -> usize {
self.entries.len()
}
pub fn total_completed(&self) -> usize {
self.entries
.iter()
.filter(|e| Filter::Completed.fits(e))
.count()
}
pub fn is_all_completed(&self) -> bool {
let mut filtered_iter = self
.entries
.iter()
.filter(|e| self.filter.fits(e))
.peekable();
if filtered_iter.peek().is_none() {
return false;
}
filtered_iter.all(|e| e.completed)
}
pub fn clear_completed(&mut self) {
let entries = self
.entries
.drain(..)
.filter(|e| Filter::Active.fits(e))
.collect();
self.entries = entries;
}
pub fn toggle(&mut self, idx: usize) {
let filter = self.filter;
let entry = self
.entries
.iter_mut()
.filter(|e| filter.fits(e))
.nth(idx)
.unwrap();
entry.completed = !entry.completed;
}
pub fn toggle_all(&mut self, value: bool) {
for entry in &mut self.entries {
if self.filter.fits(entry) {
entry.completed = value;
}
}
}
pub fn toggle_edit(&mut self, idx: usize) {
let filter = self.filter;
let entry = self
.entries
.iter_mut()
.filter(|e| filter.fits(e))
.nth(idx)
.unwrap();
entry.editing = !entry.editing;
}
pub fn clear_all_edit(&mut self) {
for entry in &mut self.entries {
entry.editing = false;
}
}
pub fn complete_edit(&mut self, idx: usize, val: String) {
if val.is_empty() {
self.remove(idx);
} else {
let filter = self.filter;
let entry = self
.entries
.iter_mut()
.filter(|e| filter.fits(e))
.nth(idx)
.unwrap();
entry.description = val;
entry.editing = !entry.editing;
}
}
pub fn remove(&mut self, idx: usize) {
let idx = {
let entries = self
.entries
.iter()
.enumerate()
.filter(|&(_, e)| self.filter.fits(e))
.collect::<Vec<_>>();
let &(idx, _) = entries.get(idx).unwrap();
idx
};
self.entries.remove(idx);
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Entry {
pub description: String,
pub completed: bool,
pub editing: bool,
}
#[derive(Clone, Copy, Debug, EnumIter, Display, PartialEq, Serialize, Deserialize, Eq)]
pub enum Filter {
All,
Active,
Completed,
}
impl Filter {
pub fn fits(&self, entry: &Entry) -> bool {
match *self {
Filter::All => true,
Filter::Active => !entry.completed,
Filter::Completed => entry.completed,
}
}
pub fn as_href(&self) -> &'static str {
match self {
Filter::All => "#/",
Filter::Active => "#/active",
Filter::Completed => "#/completed",
}
}
}