mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 09:54:41 -05:00
Reorganize benchmarks and add more SSR benchmarks
This commit is contained in:
@@ -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"
|
||||
]
|
||||
@@ -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
159
benchmarks/src/reactive.rs
Normal 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
154
benchmarks/src/ssr.rs
Normal 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><!--</[]>-->"
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
324
benchmarks/src/todomvc/leptos.rs
Normal file
324
benchmarks/src/todomvc/leptos.rs
Normal 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)(),
|
||||
}
|
||||
}
|
||||
}
|
||||
109
benchmarks/src/todomvc/mod.rs
Normal file
109
benchmarks/src/todomvc/mod.rs
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
438
benchmarks/src/todomvc/sycamore.rs
Normal file
438
benchmarks/src/todomvc/sycamore.rs
Normal 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, }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
619
benchmarks/src/todomvc/yew.rs
Normal file
619
benchmarks/src/todomvc/yew.rs
Normal 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",
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user