Compare commits

..

1 Commits
v0.5.1 ... 1751

Author SHA1 Message Date
Greg Johnston
c65da282c8 fix: Resource::with() (pt. 3!) — closes #1751 without breaking #1742 or #1711 2023-09-18 20:16:51 -04:00
151 changed files with 1409 additions and 5524 deletions

View File

@@ -20,7 +20,7 @@ jobs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
fetch-depth: 0
@@ -34,7 +34,6 @@ jobs:
examples
!examples/cargo-make
!examples/gtk
!examples/hackernews_js_fetch
!examples/Makefile.toml
!examples/*.md
json: true

View File

@@ -15,7 +15,7 @@ jobs:
example_changed: ${{ steps.set-example-changed.outputs.example_changed }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
fetch-depth: 0

View File

@@ -15,7 +15,7 @@ jobs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Install JQ Tool
uses: mbround18/install-jq@v1
@@ -23,12 +23,12 @@ jobs:
- name: Set Matrix
id: set-matrix
run: |
examples=$(ls examples |
awk '{print "examples/" $0}' |
grep -v .md |
grep -v examples/Makefile.toml |
grep -v examples/cargo-make |
grep -v examples/gtk |
examples=$(ls examples |
awk '{print "examples/" $0}' |
grep -v .md |
grep -v examples/Makefile.toml |
grep -v examples/cargo-make |
grep -v examples/gtk |
jq -R -s -c 'split("\n")[:-1]')
echo "Example Directories: $examples"
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"

View File

@@ -15,7 +15,7 @@ jobs:
leptos_changed: ${{ steps.set-source-changed.outputs.leptos_changed }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Get source files that changed
id: changed-source

View File

@@ -12,7 +12,7 @@ jobs:
contents: write # To push a branch
pull-requests: write # To create a PR from that branch
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install mdbook

View File

@@ -26,22 +26,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.5.1"
version = "0.5.0-rc2"
[workspace.dependencies]
leptos = { path = "./leptos", version = "0.5.1" }
leptos_dom = { path = "./leptos_dom", version = "0.5.1" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.1" }
leptos_macro = { path = "./leptos_macro", version = "0.5.1" }
leptos_reactive = { path = "./leptos_reactive", version = "0.5.1" }
leptos_server = { path = "./leptos_server", version = "0.5.1" }
server_fn = { path = "./server_fn", version = "0.5.1" }
server_fn_macro = { path = "./server_fn_macro", version = "0.5.1" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.1" }
leptos_config = { path = "./leptos_config", version = "0.5.1" }
leptos_router = { path = "./router", version = "0.5.1" }
leptos_meta = { path = "./meta", version = "0.5.1" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.1" }
leptos = { path = "./leptos", version = "0.5.0-rc2" }
leptos_dom = { path = "./leptos_dom", version = "0.5.0-rc2" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.0-rc2" }
leptos_macro = { path = "./leptos_macro", version = "0.5.0-rc2" }
leptos_reactive = { path = "./leptos_reactive", version = "0.5.0-rc2" }
leptos_server = { path = "./leptos_server", version = "0.5.0-rc2" }
server_fn = { path = "./server_fn", version = "0.5.0-rc2" }
server_fn_macro = { path = "./server_fn_macro", version = "0.5.0-rc2" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.0-rc2" }
leptos_config = { path = "./leptos_config", version = "0.5.0-rc2" }
leptos_router = { path = "./router", version = "0.5.0-rc2" }
leptos_meta = { path = "./meta", version = "0.5.0-rc2" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.0-rc2" }
[profile.release]
codegen-units = 1

View File

@@ -48,7 +48,7 @@ pub fn main() {
### Important Note
This example, and the entire `main` branch, now reflect the upcoming `0.5.1` release. You can use `0.5.1` with the `0.5.1-beta` release on crates.io or by a git dependency on the `main` branch of this repo. [Click here for the 0.4.9 `README`](https://crates.io/crates/leptos).
This example, and the entire `main` branch, now reflect the upcoming `0.5.0` release. You can use `0.5.0` with the `0.5.0-beta` release on crates.io or by a git dependency on the `main` branch of this repo. [Click here for the 0.4.9 `README`](https://crates.io/crates/leptos).
## About the Framework

View File

@@ -4,14 +4,12 @@ version = "0.1.0"
edition = "2021"
[dependencies]
l0410 = { package = "leptos", version = "0.4.10", features = [
"nightly",
l021 = { package = "leptos", version = "0.2.1" }
leptos = { path = "../leptos", features = [
"ssr",
"nightly",
"experimental-islands",
] }
leptos = { path = "../leptos", features = ["ssr", "nightly"] }
leptos_reactive = { path = "../leptos_reactive", features = ["ssr", "nightly"] }
tachydom = { git = "https://github.com/gbj/tachys", features = ["nightly"] }
tachy_maccy = { git = "https://github.com/gbj/tachys", features = ["nightly"] }
sycamore = { version = "0.8", features = ["ssr"] }
yew = { version = "0.20", features = ["ssr"] }
tokio-test = "0.4"
@@ -26,6 +24,7 @@ strum_macros = "0.24"
serde = { version = "1", features = ["derive", "rc"] }
serde_json = "1"
tera = "1"
reactive-signals = "0.1.0-alpha.4"
[dependencies.web-sys]
version = "0.3"

View File

@@ -2,6 +2,6 @@
extern crate test;
mod reactive;
mod ssr;
//åmod reactive;
//mod ssr;
mod todomvc;

View File

@@ -7,16 +7,19 @@ fn leptos_deep_creation(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
let signal = create_rw_signal(0);
let mut memos = Vec::<Memo<usize>>::new();
for _ in 0..1000usize {
let prev = memos.last().copied();
if let Some(prev) = prev {
memos.push(create_memo(move |_| prev.get() + 1));
} else {
memos.push(create_memo(move |_| signal.get() + 1));
create_scope(runtime, || {
let signal = create_rw_signal(0);
let mut memos = Vec::<Memo<usize>>::new();
for _ in 0..1000usize {
let prev = memos.last().copied();
if let Some(prev) = prev {
memos.push(create_memo(move |_| prev.get() + 1));
} else {
memos.push(create_memo(move |_| signal.get() + 1));
}
}
}
})
.dispose()
});
runtime.dispose();
@@ -28,17 +31,20 @@ fn leptos_deep_update(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
let signal = create_rw_signal(0);
let mut memos = Vec::<Memo<usize>>::new();
for _ in 0..1000usize {
if let Some(prev) = memos.last().copied() {
memos.push(create_memo(move |_| prev.get() + 1));
} else {
memos.push(create_memo(move |_| signal.get() + 1));
create_scope(runtime, || {
let signal = create_rw_signal(0);
let mut memos = Vec::<Memo<usize>>::new();
for _ in 0..1000usize {
if let Some(prev) = memos.last().copied() {
memos.push(create_memo(move |_| prev.get() + 1));
} else {
memos.push(create_memo(move |_| signal.get() + 1));
}
}
}
signal.set(1);
assert_eq!(memos[999].get(), 1001);
signal.set(1);
assert_eq!(memos[999].get(), 1001);
})
.dispose()
});
runtime.dispose();
@@ -50,12 +56,16 @@ fn leptos_narrowing_down(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
let sigs = (0..1000).map(|n| create_signal(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(move |_| reads.iter().map(|r| r.get()).sum::<i32>());
assert_eq!(memo(), 499500);
create_scope(runtime, || {
let sigs = (0..1000).map(|n| create_signal(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(move |_| {
reads.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo(), 499500);
})
.dispose()
});
runtime.dispose();
@@ -67,13 +77,16 @@ fn leptos_fanning_out(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
let sig = create_rw_signal(0);
let memos = (0..1000)
.map(|_| create_memo(move |_| sig.get()))
.collect::<Vec<_>>();
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
sig.set(1);
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
create_scope(runtime, || {
let sig = create_rw_signal(0);
let memos = (0..1000)
.map(|_| create_memo(move |_| sig.get()))
.collect::<Vec<_>>();
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
sig.set(1);
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
})
.dispose()
});
runtime.dispose();
@@ -85,35 +98,144 @@ fn leptos_narrowing_update(b: &mut Bencher) {
let runtime = create_runtime();
b.iter(|| {
let acc = Rc::new(Cell::new(0));
let sigs = (0..1000).map(|n| create_signal(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(move |_| reads.iter().map(|r| r.get()).sum::<i32>());
assert_eq!(memo(), 499500);
create_isomorphic_effect({
let acc = Rc::clone(&acc);
move |_| {
acc.set(memo());
}
});
assert_eq!(acc.get(), 499500);
create_scope(runtime, || {
let acc = Rc::new(Cell::new(0));
let sigs = (0..1000).map(|n| create_signal(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(move |_| {
reads.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo(), 499500);
create_isomorphic_effect({
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);
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);
assert_eq!(acc.get(), 499503);
assert_eq!(memo(), 499503);
})
.dispose()
});
runtime.dispose();
}
#[bench]
fn l0410_deep_creation(b: &mut Bencher) {
use l0410::*;
fn leptos_scope_creation_and_disposal(b: &mut Bencher) {
use leptos::*;
let runtime = create_runtime();
b.iter(|| {
let acc = Rc::new(Cell::new(0));
let disposers = (0..1000)
.map(|_| {
create_scope(runtime, {
let acc = Rc::clone(&acc);
move || {
let (r, w) = create_signal(0);
create_isomorphic_effect({
move |_| {
acc.set(r());
}
});
w.update(|n| *n += 1);
}
})
})
.collect::<Vec<_>>();
for disposer in disposers {
disposer.dispose();
}
});
runtime.dispose();
}
#[bench]
fn rs_deep_update(b: &mut Bencher) {
use reactive_signals::{
runtimes::ClientRuntime, signal, types::Func, Scope, Signal,
};
let sc = ClientRuntime::new_root_scope();
b.iter(|| {
let signal = signal!(sc, 0);
let mut memos = Vec::<Signal<Func<i32>, ClientRuntime>>::new();
for i in 0..1000usize {
let prev = memos.get(i.saturating_sub(1)).copied();
if let Some(prev) = prev {
memos.push(signal!(sc, move || prev.get() + 1))
} else {
memos.push(signal!(sc, move || signal.get() + 1))
}
}
signal.set(1);
assert_eq!(memos[999].get(), 1001);
});
}
#[bench]
fn rs_fanning_out(b: &mut Bencher) {
use reactive_signals::{
runtimes::ClientRuntime, signal, types::Func, Scope, Signal,
};
let cx = ClientRuntime::new_root_scope();
b.iter(|| {
let sig = signal!(cx, 0);
let memos = (0..1000)
.map(|_| signal!(cx, move || sig.get()))
.collect::<Vec<_>>();
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
sig.set(1);
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
});
}
#[bench]
fn rs_narrowing_update(b: &mut Bencher) {
use reactive_signals::{
runtimes::ClientRuntime, signal, types::Func, Scope, Signal,
};
let cx = ClientRuntime::new_root_scope();
b.iter(|| {
let acc = Rc::new(Cell::new(0));
let sigs = (0..1000).map(|n| signal!(cx, n)).collect::<Vec<_>>();
let memo = signal!(cx, {
let sigs = sigs.clone();
move || sigs.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo.get(), 499500);
signal!(cx, {
let acc = Rc::clone(&acc);
move || {
acc.set(memo.get());
}
});
assert_eq!(acc.get(), 499500);
sigs[1].update(|n| *n += 1);
sigs[10].update(|n| *n += 1);
sigs[100].update(|n| *n += 1);
assert_eq!(acc.get(), 499503);
assert_eq!(memo.get(), 499503);
});
}
#[bench]
fn l021_deep_creation(b: &mut Bencher) {
use l021::*;
let runtime = create_runtime();
b.iter(|| {
@@ -135,8 +257,8 @@ fn l0410_deep_creation(b: &mut Bencher) {
}
#[bench]
fn l0410_deep_update(b: &mut Bencher) {
use l0410::*;
fn l021_deep_update(b: &mut Bencher) {
use l021::*;
let runtime = create_runtime();
b.iter(|| {
@@ -160,8 +282,8 @@ fn l0410_deep_update(b: &mut Bencher) {
}
#[bench]
fn l0410_narrowing_down(b: &mut Bencher) {
use l0410::*;
fn l021_narrowing_down(b: &mut Bencher) {
use l021::*;
let runtime = create_runtime();
b.iter(|| {
@@ -183,8 +305,8 @@ fn l0410_narrowing_down(b: &mut Bencher) {
}
#[bench]
fn l0410_fanning_out(b: &mut Bencher) {
use l0410::*;
fn l021_fanning_out(b: &mut Bencher) {
use leptos::*;
let runtime = create_runtime();
b.iter(|| {
@@ -203,8 +325,8 @@ fn l0410_fanning_out(b: &mut Bencher) {
runtime.dispose();
}
#[bench]
fn l0410_narrowing_update(b: &mut Bencher) {
use l0410::*;
fn l021_narrowing_update(b: &mut Bencher) {
use l021::*;
let runtime = create_runtime();
b.iter(|| {
@@ -217,11 +339,11 @@ fn l0410_narrowing_update(b: &mut Bencher) {
let memo = create_memo(cx, move |_| {
reads.iter().map(|r| r.get()).sum::<i32>()
});
assert_eq!(memo.get(), 499500);
assert_eq!(memo(), 499500);
create_isomorphic_effect(cx, {
let acc = Rc::clone(&acc);
move |_| {
acc.set(memo.get());
acc.set(memo());
}
});
assert_eq!(acc.get(), 499500);
@@ -231,7 +353,7 @@ fn l0410_narrowing_update(b: &mut Bencher) {
writes[100].update(|n| *n += 1);
assert_eq!(acc.get(), 499503);
assert_eq!(memo.get(), 499503);
assert_eq!(memo(), 499503);
})
.dispose()
});
@@ -240,8 +362,8 @@ fn l0410_narrowing_update(b: &mut Bencher) {
}
#[bench]
fn l0410_scope_creation_and_disposal(b: &mut Bencher) {
use l0410::*;
fn l021_scope_creation_and_disposal(b: &mut Bencher) {
use l021::*;
let runtime = create_runtime();
b.iter(|| {
@@ -254,7 +376,7 @@ fn l0410_scope_creation_and_disposal(b: &mut Bencher) {
let (r, w) = create_signal(cx, 0);
create_isomorphic_effect(cx, {
move |_| {
acc.set(r.get());
acc.set(r());
}
});
w.update(|n| *n += 1);

View File

@@ -2,14 +2,15 @@ use test::Bencher;
#[bench]
fn leptos_ssr_bench(b: &mut Bencher) {
use leptos::*;
let r = create_runtime();
b.iter(|| {
leptos::leptos_dom::HydrationCtx::reset_id();
use leptos::*;
leptos_dom::HydrationCtx::reset_id();
_ = create_scope(create_runtime(), |cx| {
#[component]
fn Counter(initial: i32) -> impl IntoView {
let (value, set_value) = create_signal(initial);
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>
@@ -19,6 +20,7 @@ fn leptos_ssr_bench(b: &mut Bencher) {
}
let rendered = view! {
cx,
<main>
<h1>"Welcome to our benchmark page."</h1>
<p>"Here's some introductory text."</p>
@@ -26,51 +28,14 @@ fn leptos_ssr_bench(b: &mut Bencher) {
<Counter initial=2/>
<Counter initial=3/>
</main>
}.into_view().render_to_string();
}.into_view(cx).render_to_string(cx);
assert_eq!(
rendered,
"<main data-hk=\"0-0-1\"><h1 data-hk=\"0-0-2\">Welcome to our benchmark page.</h1><p data-hk=\"0-0-3\">Here&#x27;s some introductory text.</p><div data-hk=\"0-0-5\"><button data-hk=\"0-0-6\">-1</button><span data-hk=\"0-0-7\">Value: <!>1<!--hk=0-0-8-->!</span><button data-hk=\"0-0-9\">+1</button></div><!--hk=0-0-4--><div data-hk=\"0-0-11\"><button data-hk=\"0-0-12\">-1</button><span data-hk=\"0-0-13\">Value: <!>2<!--hk=0-0-14-->!</span><button data-hk=\"0-0-15\">+1</button></div><!--hk=0-0-10--><div data-hk=\"0-0-17\"><button data-hk=\"0-0-18\">-1</button><span data-hk=\"0-0-19\">Value: <!>3<!--hk=0-0-20-->!</span><button data-hk=\"0-0-21\">+1</button></div><!--hk=0-0-16--></main>"
"<main id=\"_0-1\"><h1 id=\"_0-2\">Welcome to our benchmark page.</h1><p id=\"_0-3\">Here&#x27;s some introductory text.</p><div id=\"_0-3-1\"><button id=\"_0-3-2\">-1</button><span id=\"_0-3-3\">Value: <!>1<!--hk=_0-3-4-->!</span><button id=\"_0-3-5\">+1</button></div><!--hk=_0-3-0--><div id=\"_0-3-5-1\"><button id=\"_0-3-5-2\">-1</button><span id=\"_0-3-5-3\">Value: <!>2<!--hk=_0-3-5-4-->!</span><button id=\"_0-3-5-5\">+1</button></div><!--hk=_0-3-5-0--><div id=\"_0-3-5-5-1\"><button id=\"_0-3-5-5-2\">-1</button><span id=\"_0-3-5-5-3\">Value: <!>3<!--hk=_0-3-5-5-4-->!</span><button id=\"_0-3-5-5-5\">+1</button></div><!--hk=_0-3-5-5-0--></main>"
);
});
});
r.dispose();
}
#[bench]
fn tachys_ssr_bench(b: &mut Bencher) {
use leptos::{create_runtime, create_signal, SignalGet, SignalUpdate};
use tachy_maccy::view;
use tachydom::view::Render;
let rt = create_runtime();
b.iter(|| {
fn counter(initial: i32) -> impl Render {
let (value, set_value) = create_signal(initial);
view! {
<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 mut buf = String::with_capacity(1024);
let rendered = view! {
<main>
<h1>"Welcome to our benchmark page."</h1>
<p>"Here's some introductory text."</p>
{counter(1)}
{counter(2)}
{counter(3)}
</main>
};
rendered.to_html(&mut buf, &Default::default());
assert_eq!(
buf,
"<main><h1>Welcome to our benchmark page.</h1><p>Here's some introductory text.</p><div><button>-1</button><span>Value: <!>1<!>!</span><button>+1</button></div><div><button>-1</button><span>Value: <!>2<!>!</span><button>+1</button></div><div><button>-1</button><span>Value: <!>3<!>!</span><button>+1</button></div></main>"
);
});
rt.dispose();
}
#[bench]

View File

@@ -192,7 +192,7 @@ pub fn TodoMVC(todos: Todos) -> impl IntoView {
<For
each=filtered_todos
key=|todo| todo.id
children=move |todo: Todo| {
view=move |todo: Todo| {
view! { <Todo todo=todo.clone()/> }
}
/>

View File

@@ -2,7 +2,6 @@ use test::Bencher;
mod leptos;
mod sycamore;
mod tachys;
mod tera;
mod yew;
@@ -18,32 +17,13 @@ fn leptos_todomvc_ssr(b: &mut Bencher) {
});
assert!(html.len() > 1);
});
runtime.dispose();
}
#[bench]
fn tachys_todomvc_ssr(b: &mut Bencher) {
use ::leptos::*;
let runtime = create_runtime();
b.iter(|| {
use crate::todomvc::tachys::*;
use tachydom::view::Render;
let mut buf = String::new();
let rendered = TodoMVC(Todos::new());
rendered.to_html(&mut buf, &Default::default());
assert_eq!(
buf,
"<main><section class=\"todoapp\"><header class=\"header\"><h1>todos</h1><input placeholder=\"What needs to be done?\" autofocus=\"\" class=\"new-todo\"></header><section class=\"main hidden\"><input id=\"toggle-all\" type=\"checkbox\" class=\"toggle-all\"><label for=\"toggle-all\">Mark all as complete</label><ul class=\"todo-list\"></ul></section><footer class=\"footer hidden\"><span class=\"todo-count\"><strong>0</strong><!> items<!> left</span><ul class=\"filters\"><li><a href=\"#/\" class=\"selected selected\">All</a></li><li><a href=\"#/active\" class=\"\">Active</a></li><li><a href=\"#/completed\" class=\"\">Completed</a></li></ul><button class=\"clear-completed hidden hidden\">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>"
);
});
runtime.dispose();
}
#[bench]
fn sycamore_todomvc_ssr(b: &mut Bencher) {
use self::sycamore::*;
use ::sycamore::{prelude::*, *};
use ::sycamore::prelude::*;
use ::sycamore::*;
b.iter(|| {
_ = create_scope(|cx| {
@@ -62,7 +42,8 @@ fn sycamore_todomvc_ssr(b: &mut Bencher) {
#[bench]
fn yew_todomvc_ssr(b: &mut Bencher) {
use self::yew::*;
use ::yew::{prelude::*, ServerRenderer};
use ::yew::prelude::*;
use ::yew::ServerRenderer;
b.iter(|| {
tokio_test::block_on(async {
@@ -79,35 +60,21 @@ fn leptos_todomvc_ssr_with_1000(b: &mut Bencher) {
use self::leptos::*;
use ::leptos::*;
let html = ::leptos::ssr::render_to_string(|| {
let html = ::leptos::ssr::render_to_string(|cx| {
view! {
<TodoMVC todos=Todos::new_with_1000()/>
cx,
<TodoMVC todos=Todos::new_with_1000(cx)/>
}
});
assert!(html.len() > 1);
});
}
#[bench]
fn tachys_todomvc_ssr_with_1000(b: &mut Bencher) {
use ::leptos::*;
let runtime = create_runtime();
b.iter(|| {
use crate::todomvc::tachys::*;
use tachydom::view::Render;
let mut buf = String::new();
let rendered = TodoMVC(Todos::new_with_1000());
rendered.to_html(&mut buf, &Default::default());
assert!(buf.len() > 20_000)
});
runtime.dispose();
}
#[bench]
fn sycamore_todomvc_ssr_with_1000(b: &mut Bencher) {
use self::sycamore::*;
use ::sycamore::{prelude::*, *};
use ::sycamore::prelude::*;
use ::sycamore::*;
b.iter(|| {
_ = create_scope(|cx| {
@@ -126,7 +93,8 @@ fn sycamore_todomvc_ssr_with_1000(b: &mut Bencher) {
#[bench]
fn yew_todomvc_ssr_with_1000(b: &mut Bencher) {
use self::yew::*;
use ::yew::{prelude::*, ServerRenderer};
use ::yew::prelude::*;
use ::yew::ServerRenderer;
b.iter(|| {
tokio_test::block_on(async {
@@ -135,19 +103,4 @@ fn yew_todomvc_ssr_with_1000(b: &mut Bencher) {
assert!(rendered.len() > 1);
});
});
}
#[bench]
fn tera_todomvc_ssr(b: &mut Bencher) {
use ::leptos::*;
let runtime = create_runtime();
b.iter(|| {
use crate::todomvc::leptos::*;
let html = ::leptos::ssr::render_to_string(|| {
view! { <TodoMVC todos=Todos::new()/> }
});
assert!(html.len() > 1);
});
runtime.dispose();
}
}

View File

@@ -1,324 +0,0 @@
pub use leptos_reactive::*;
use miniserde::*;
use tachy_maccy::view;
use tachydom::view::Render;
use wasm_bindgen::JsCast;
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() -> Self {
Self(vec![])
}
pub fn new_with_1000() -> Self {
let todos = (0..1000)
.map(|id| Todo::new(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(id: usize, title: String) -> Self {
Self::new_with_completed(id, title, false)
}
pub fn new_with_completed(
id: usize,
title: String,
completed: bool,
) -> Self {
let (title, set_title) = create_signal(title);
let (completed, set_completed) = create_signal(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;
pub fn TodoMVC(todos: Todos) -> impl Render {
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(todos);
provide_context(set_todos);
let (mode, set_mode) = create_signal(Mode::All);
let add_todo = move |ev: web_sys::KeyboardEvent| {
todo!()
/* 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(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>>(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(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! {
<main>
<section class="todoapp">
<header class="header">
<h1>"todos"</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autofocus=""
/>
</header>
<section class="main" class:hidden=move || todos.with(|t| t.is_empty())>
<input
id="toggle-all"
class="toggle-all"
r#type="checkbox"
//prop:checked=move || todos.with(|t| t.remaining() > 0)
on:input=move |_| set_todos.update(|t| t.toggle_all())
/>
<label r#for="toggle-all">"Mark all as complete"</label>
<ul class="todo-list">
{filtered_todos.get().into_iter().map(Todo).collect::<Vec<_>>()}
</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>
}
}
pub fn Todo(todo: Todo) -> impl Render {
let (editing, set_editing) = create_signal(false);
let set_todos = use_context::<WriteSignal<Todos>>().unwrap();
//let input = NodeRef::new();
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);
};
view! {
<li class="todo" class:editing=editing class:completed=move || (todo.completed)()>
/* <div class="view">
<input class="toggle" r#type="checkbox"/>
<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))
></button>
</div>
{move || {
editing()
.then(|| {
view! {
<input
class="edit"
class:hidden=move || !(editing)()
/>
}
})
}} */
</li>
}
}
#[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) -> Todo {
Todo::new_with_completed(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

@@ -87,7 +87,7 @@ static TEMPLATE: &str = r#"<main>
</main>"#;
#[bench]
fn tera_todomvc_ssr(b: &mut Bencher) {
fn tera_todomvc(b: &mut Bencher) {
use serde::{Deserialize, Serialize};
use tera::*;
@@ -127,7 +127,7 @@ fn tera_todomvc_ssr(b: &mut Bencher) {
}
#[bench]
fn tera_todomvc_ssr_1000(b: &mut Bencher) {
fn tera_todomvc_1000(b: &mut Bencher) {
use serde::{Deserialize, Serialize};
use tera::*;
@@ -174,4 +174,4 @@ fn tera_todomvc_ssr_1000(b: &mut Bencher) {
let _ = TERA.render("template.html", &ctx).unwrap();
});
}
}

View File

@@ -1,10 +1,2 @@
[output.html]
additional-css = ["./mdbook-admonish.css"]
[output.html.playground]
runnable = false
[preprocessor]
[preprocessor.admonish]
command = "mdbook-admonish"
assets_version = "2.0.2" # do not edit: managed by `mdbook-admonish install`

View File

@@ -1,353 +0,0 @@
@charset "UTF-8";
:root {
--md-admonition-icon--note:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75M3 17.25V21h3.75L17.81 9.93l-3.75-3.75L3 17.25z'/></svg>");
--md-admonition-icon--abstract:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17 9H7V7h10m0 6H7v-2h10m-3 6H7v-2h7M12 3a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z'/></svg>");
--md-admonition-icon--info:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2z'/></svg>");
--md-admonition-icon--tip:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17.66 11.2c-.23-.3-.51-.56-.77-.82-.67-.6-1.43-1.03-2.07-1.66C13.33 7.26 13 4.85 13.95 3c-.95.23-1.78.75-2.49 1.32-2.59 2.08-3.61 5.75-2.39 8.9.04.1.08.2.08.33 0 .22-.15.42-.35.5-.23.1-.47.04-.66-.12a.58.58 0 0 1-.14-.17c-1.13-1.43-1.31-3.48-.55-5.12C5.78 10 4.87 12.3 5 14.47c.06.5.12 1 .29 1.5.14.6.41 1.2.71 1.73 1.08 1.73 2.95 2.97 4.96 3.22 2.14.27 4.43-.12 6.07-1.6 1.83-1.66 2.47-4.32 1.53-6.6l-.13-.26c-.21-.46-.77-1.26-.77-1.26m-3.16 6.3c-.28.24-.74.5-1.1.6-1.12.4-2.24-.16-2.9-.82 1.19-.28 1.9-1.16 2.11-2.05.17-.8-.15-1.46-.28-2.23-.12-.74-.1-1.37.17-2.06.19.38.39.76.63 1.06.77 1 1.98 1.44 2.24 2.8.04.14.06.28.06.43.03.82-.33 1.72-.93 2.27z'/></svg>");
--md-admonition-icon--success:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m9 20.42-6.21-6.21 2.83-2.83L9 14.77l9.88-9.89 2.83 2.83L9 20.42z'/></svg>");
--md-admonition-icon--question:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m15.07 11.25-.9.92C13.45 12.89 13 13.5 13 15h-2v-.5c0-1.11.45-2.11 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41a2 2 0 0 0-2-2 2 2 0 0 0-2 2H8a4 4 0 0 1 4-4 4 4 0 0 1 4 4 3.2 3.2 0 0 1-.93 2.25M13 19h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10c0-5.53-4.5-10-10-10z'/></svg>");
--md-admonition-icon--warning:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 14h-2V9h2m0 9h-2v-2h2M1 21h22L12 2 1 21z'/></svg>");
--md-admonition-icon--failure:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20 6.91 17.09 4 12 9.09 6.91 4 4 6.91 9.09 12 4 17.09 6.91 20 12 14.91 17.09 20 20 17.09 14.91 12 20 6.91z'/></svg>");
--md-admonition-icon--danger:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M11 15H6l7-14v8h5l-7 14v-8z'/></svg>");
--md-admonition-icon--bug:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 12h-4v-2h4m0 6h-4v-2h4m6-6h-2.81a5.985 5.985 0 0 0-1.82-1.96L17 4.41 15.59 3l-2.17 2.17a6.002 6.002 0 0 0-2.83 0L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8z'/></svg>");
--md-admonition-icon--example:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M7 13v-2h14v2H7m0 6v-2h14v2H7M7 7V5h14v2H7M3 8V5H2V4h2v4H3m-1 9v-1h3v4H2v-1h2v-.5H3v-1h1V17H2m2.25-7a.75.75 0 0 1 .75.75c0 .2-.08.39-.21.52L3.12 13H5v1H2v-.92L4 11H2v-1h2.25z'/></svg>");
--md-admonition-icon--quote:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 17h3l2-4V7h-6v6h3M6 17h3l2-4V7H5v6h3l-2 4z'/></svg>");
--md-details-icon:
url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M8.59 16.58 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42Z'/></svg>");
}
:is(.admonition) {
display: flow-root;
margin: 1.5625em 0;
padding: 0 1.2rem;
color: var(--fg);
page-break-inside: avoid;
background-color: var(--bg);
border: 0 solid black;
border-inline-start-width: 0.4rem;
border-radius: 0.2rem;
box-shadow: 0 0.2rem 1rem rgba(0, 0, 0, 0.05), 0 0 0.1rem rgba(0, 0, 0, 0.1);
}
@media print {
:is(.admonition) {
box-shadow: none;
}
}
:is(.admonition) > * {
box-sizing: border-box;
}
:is(.admonition) :is(.admonition) {
margin-top: 1em;
margin-bottom: 1em;
}
:is(.admonition) > .tabbed-set:only-child {
margin-top: 0;
}
html :is(.admonition) > :last-child {
margin-bottom: 1.2rem;
}
a.admonition-anchor-link {
display: none;
position: absolute;
left: -1.2rem;
padding-right: 1rem;
}
a.admonition-anchor-link:link, a.admonition-anchor-link:visited {
color: var(--fg);
}
a.admonition-anchor-link:link:hover, a.admonition-anchor-link:visited:hover {
text-decoration: none;
}
a.admonition-anchor-link::before {
content: "§";
}
:is(.admonition-title, summary.admonition-title) {
position: relative;
min-height: 4rem;
margin-block: 0;
margin-inline: -1.6rem -1.2rem;
padding-block: 0.8rem;
padding-inline: 4.4rem 1.2rem;
font-weight: 700;
background-color: rgba(68, 138, 255, 0.1);
display: flex;
}
:is(.admonition-title, summary.admonition-title) p {
margin: 0;
}
html :is(.admonition-title, summary.admonition-title):last-child {
margin-bottom: 0;
}
:is(.admonition-title, summary.admonition-title)::before {
position: absolute;
top: 0.625em;
inset-inline-start: 1.6rem;
width: 2rem;
height: 2rem;
background-color: #448aff;
mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"></svg>');
-webkit-mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"></svg>');
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-size: contain;
content: "";
}
:is(.admonition-title, summary.admonition-title):hover a.admonition-anchor-link {
display: initial;
}
details.admonition > summary.admonition-title::after {
position: absolute;
top: 0.625em;
inset-inline-end: 1.6rem;
height: 2rem;
width: 2rem;
background-color: currentcolor;
mask-image: var(--md-details-icon);
-webkit-mask-image: var(--md-details-icon);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-size: contain;
content: "";
transform: rotate(0deg);
transition: transform 0.25s;
}
details[open].admonition > summary.admonition-title::after {
transform: rotate(90deg);
}
:is(.admonition):is(.note) {
border-color: #448aff;
}
:is(.note) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(68, 138, 255, 0.1);
}
:is(.note) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #448aff;
mask-image: var(--md-admonition-icon--note);
-webkit-mask-image: var(--md-admonition-icon--note);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.abstract, .summary, .tldr) {
border-color: #00b0ff;
}
:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(0, 176, 255, 0.1);
}
:is(.abstract, .summary, .tldr) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #00b0ff;
mask-image: var(--md-admonition-icon--abstract);
-webkit-mask-image: var(--md-admonition-icon--abstract);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.info, .todo) {
border-color: #00b8d4;
}
:is(.info, .todo) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(0, 184, 212, 0.1);
}
:is(.info, .todo) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #00b8d4;
mask-image: var(--md-admonition-icon--info);
-webkit-mask-image: var(--md-admonition-icon--info);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.tip, .hint, .important) {
border-color: #00bfa5;
}
:is(.tip, .hint, .important) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(0, 191, 165, 0.1);
}
:is(.tip, .hint, .important) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #00bfa5;
mask-image: var(--md-admonition-icon--tip);
-webkit-mask-image: var(--md-admonition-icon--tip);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.success, .check, .done) {
border-color: #00c853;
}
:is(.success, .check, .done) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(0, 200, 83, 0.1);
}
:is(.success, .check, .done) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #00c853;
mask-image: var(--md-admonition-icon--success);
-webkit-mask-image: var(--md-admonition-icon--success);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.question, .help, .faq) {
border-color: #64dd17;
}
:is(.question, .help, .faq) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(100, 221, 23, 0.1);
}
:is(.question, .help, .faq) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #64dd17;
mask-image: var(--md-admonition-icon--question);
-webkit-mask-image: var(--md-admonition-icon--question);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.warning, .caution, .attention) {
border-color: #ff9100;
}
:is(.warning, .caution, .attention) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(255, 145, 0, 0.1);
}
:is(.warning, .caution, .attention) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #ff9100;
mask-image: var(--md-admonition-icon--warning);
-webkit-mask-image: var(--md-admonition-icon--warning);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.failure, .fail, .missing) {
border-color: #ff5252;
}
:is(.failure, .fail, .missing) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(255, 82, 82, 0.1);
}
:is(.failure, .fail, .missing) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #ff5252;
mask-image: var(--md-admonition-icon--failure);
-webkit-mask-image: var(--md-admonition-icon--failure);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.danger, .error) {
border-color: #ff1744;
}
:is(.danger, .error) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(255, 23, 68, 0.1);
}
:is(.danger, .error) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #ff1744;
mask-image: var(--md-admonition-icon--danger);
-webkit-mask-image: var(--md-admonition-icon--danger);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.bug) {
border-color: #f50057;
}
:is(.bug) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(245, 0, 87, 0.1);
}
:is(.bug) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #f50057;
mask-image: var(--md-admonition-icon--bug);
-webkit-mask-image: var(--md-admonition-icon--bug);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.example) {
border-color: #7c4dff;
}
:is(.example) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(124, 77, 255, 0.1);
}
:is(.example) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #7c4dff;
mask-image: var(--md-admonition-icon--example);
-webkit-mask-image: var(--md-admonition-icon--example);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
:is(.admonition):is(.quote, .cite) {
border-color: #9e9e9e;
}
:is(.quote, .cite) > :is(.admonition-title, summary.admonition-title) {
background-color: rgba(158, 158, 158, 0.1);
}
:is(.quote, .cite) > :is(.admonition-title, summary.admonition-title)::before {
background-color: #9e9e9e;
mask-image: var(--md-admonition-icon--quote);
-webkit-mask-image: var(--md-admonition-icon--quote);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
}
.navy :is(.admonition) {
background-color: var(--sidebar-bg);
}
.ayu :is(.admonition), .coal :is(.admonition) {
background-color: var(--theme-hover);
}
.rust :is(.admonition) {
background-color: var(--sidebar-bg);
color: var(--sidebar-fg);
}
.rust .admonition-anchor-link:link, .rust .admonition-anchor-link:visited {
color: var(--sidebar-fg);
}

View File

@@ -17,6 +17,6 @@ understand Leptos.
You can find more detailed docs for each part of the API at [Docs.rs](https://docs.rs/leptos/latest/leptos/).
**Important Note**: This current version of the book reflects the `0.5.1` release. The CodeSandbox versions of the examples still reflect `0.4` and earlier APIs and are in the process of being updated.
**Important Note**: This current version of the book reflects the upcoming `0.5.0` release, which you can install as version `0.5.0-rc2`. The CodeSandbox versions of the examples still reflect `0.4` and earlier APIs and are in the process of being updated.
> The source code for the book is available [here](https://github.com/leptos-rs/leptos/tree/main/docs/book). PRs for typos or clarification are always welcome.

View File

@@ -23,15 +23,15 @@ cargo init leptos-tutorial
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
```bash
cargo add leptos --features=csr,nightly
cargo add leptos@0.5.0-rc2 --features=csr,nightly
```
> **Note**: This version of the book reflects the Leptos 0.5 release. The CodeSandbox examples have not yet been updated from 0.4 and earlier versions.
> **Note**: This version of the book reflects the upcoming Leptos 0.5.0 release. The CodeSandbox examples have not yet been updated from 0.4 and earlier versions.
Or you can leave off `nightly` if you're using stable Rust
```bash
cargo add leptos --features=csr
cargo add leptos@0.5.0-rc2 --features=csr
```
> Using `nightly` Rust, and the `nightly` feature in Leptos enables the function-call syntax for signal getters and setters that is used in most of this book.

View File

@@ -47,14 +47,3 @@ Helix, in `.helix/languages.toml`:
name = "rust"
config = { procMacro = {ignored = {leptos_macro = ["component"]}}}
```
```admonish info
The Jetbrains `intellij-rust` plugin (RustRover as well) currently does not support dynamic config for macro exclusion.
However, the project currently maintains a hardcoded list of excluded macros.
As soon as [this open PR](https://github.com/intellij-rust/intellij-rust/pull/10873) is merged, the `component` and
`server` macro will be excluded automatically without additional configuration needed.
Update (2023/10/02):
The `intellij-rust` plugin got deprecated in favor of RustRover at the same time the PR was opened, but an official
support request was made to integrate the contents of this PR.
```

View File

@@ -89,7 +89,7 @@ view! {
// `future` provides the `Future` to be resolved
future=|| fetch_monkeys(3)
// the data is bound to whatever variable name you provide
let:data
bind:data
>
// you receive the data by reference and can use it in your view here
<p>{*data} " little monkeys, jumping on the bed."</p>

View File

@@ -50,7 +50,7 @@ use stylers::style;
#[component]
pub fn App() -> impl IntoView {
let styler_class = style! { "App",
##two{
#two{
color: blue;
}
div.one{

View File

@@ -108,8 +108,8 @@ let memoized_double_count = create_memo(move |_| count() * 2);
```
> For guidance on whether to use a derived signal or a memo, see the docs for [`create_memo`](https://docs.rs/leptos/latest/leptos/fn.create_memo.html)
**2) C is a function of A and some other thing B.** Create signals for A and B and a derived signal or memo for C.
>
> **2) C is a function of A and some other thing B.** Create signals for A and B and a derived signal or memo for C.
```rust
let (first_name, set_first_name) = create_signal("Bridget".to_string());

View File

@@ -143,8 +143,8 @@ pub fn ContactList() -> impl IntoView {
// the contact list
<For each=contacts
key=|contact| contact.id
children=|contact| todo!()
/>
view=|contact| todo!()
>
// the nested child, if any
// dont forget this!
<Outlet/>

View File

@@ -8,12 +8,12 @@ If youve ever listened to streaming music or watched a video online, Im su
Let me say a little more about what I mean.
Leptos supports all the major ways of rendering HTML that includes asynchronous data:
Leptos supports all four different modes of rendering HTML that includes asynchronous data:
1. [Synchronous Rendering](#synchronous-rendering)
1. [Async Rendering](#async-rendering)
1. [In-Order streaming](#in-order-streaming)
1. [Out-of-Order Streaming](#out-of-order-streaming) (and a partially-blocked variant)
1. [Out-of-Order Streaming](#out-of-order-streaming)
## Synchronous Rendering
@@ -67,7 +67,7 @@ If youre using server-side rendering, the synchronous mode is almost never wh
- Able to show the fallback loading state and dynamically replace it, instead of showing blank sections for un-loaded data.
- _Cons_: Requires JavaScript to be enabled for suspended fragments to appear in correct order. (This small chunk of JS streamed down in a `<script>` tag alongside the `<template>` tag that contains the rendered `<Suspense/>` fragment, so it does not need to load any additional JS files.)
5. **Partially-blocked streaming**: “Partially-blocked” streaming is useful when you have multiple separate `<Suspense/>` components on the page. It is triggered by setting `ssr=SsrMode::PartiallyBlocked` on a route, and depending on blocking resources within the view. If one of the `<Suspense/>` components reads from one or more “blocking resources” (see below), the fallback will not be sent; rather, the server will wait until that `<Suspense/>` has resolved and then replace the fallback with the resolved fragment on the server, which means that it is included in the initial HTML response and appears even if JavaScript is disabled or not supported. Other `<Suspense/>` stream in out of order, similar to the `SsrMode::OutOfOrder` default.
5. **Partially-blocked streaming**: “Partially-blocked” streaming is useful when you have multiple separate `<Suspense/>` components on the page. If one of them reads from one or more “blocking resources” (see below), the fallback will not be sent; rather, the server will wait until that `<Suspense/>` has resolved and then replace the fallback with the resolved fragment on the server, which means that it is included in the initial HTML response and appears even if JavaScript is disabled or not supported. Other `<Suspense/>` stream in out of order as usual.
This is useful when you have multiple `<Suspense/>` on the page, and one is more important than the other: think of a blog post and comments, or product information and reviews. It is _not_ useful if theres only one `<Suspense/>`, or if every `<Suspense/>` reads from blocking resources. In those cases it is a slower form of `async` rendering.
@@ -134,23 +134,4 @@ pub fn BlogPost() -> impl IntoView {
}
```
The first `<Suspense/>`, with the body of the blog post, will block my HTML stream, because it reads from a blocking resource. Meta tags and other head elements awaiting the blocking resource will be rendered before the stream is sent.
Combined with the following route definition, which uses `SsrMode::PartiallyBlocked`, the blocking resource will be fully rendered on the server side, making it accessible to users who disable WebAssembly or JavaScript.
```rust
<Routes>
// Well load the home page with out-of-order streaming and <Suspense/>
<Route path="" view=HomePage/>
// We'll load the posts with async rendering, so they can set
// the title and metadata *after* loading the data
<Route
path="/post/:id"
view=Post
ssr=SsrMode::PartiallyBlocked
/>
</Routes>
```
The second `<Suspense/>`, with the comments, will not block the stream. Blocking resources gave me exactly the power and granularity I needed to optimize my page for SEO and user experience.
The first `<Suspense/>`, with the body of the blog post, will block my HTML stream, because it reads from a blocking resource. The second `<Suspense/>`, with the comments, will not block the stream. Blocking resources gave me exactly the power and granularity I needed to optimize my page for SEO and user experience.

View File

@@ -229,9 +229,9 @@ fn DynamicList(
// can only grow, because moving items around inside the list
// means their indices will change and they will all rerender
key=|counter| counter.0
// `children` receives each item from your `each` iterator
// the view function receives each item from your `each` iterator
// and returns a view
children=move |(id, (count, set_count))| {
view=move |(id, (count, set_count))| {
view! {
<li>
<button

View File

@@ -65,7 +65,7 @@ pub fn Counters() -> impl IntoView {
<For
each=counters
key=|counter| counter.0
children=move |(id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
view=move |(id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
view! {
<Counter id value set_value/>
}

View File

@@ -67,7 +67,7 @@ pub fn Counters() -> impl IntoView {
<For
each={move || counters.get()}
key={|counter| counter.0}
children=move |(id, (value, set_value))| {
view=move |(id, (value, set_value))| {
view! {
<Counter id value set_value/>
}

View File

@@ -45,7 +45,7 @@ pub fn ErrorTemplate(
// a unique key for each item as a reference
key=|(index, _)| *index
// renders each item to a view
children=move |error| {
view=move |error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! {

View File

@@ -28,7 +28,7 @@ async fn custom_handler(
move || {
provide_context(id.clone());
},
App,
|| view! { <App/> },
);
handler(req).await.into_response()
}
@@ -48,13 +48,13 @@ async fn main() {
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(App);
let routes = generate_route_list(|| view! { <App/> }).await;
// build our application with a route
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.route("/special/:id", get(custom_handler))
.leptos_routes(&leptos_options, routes, App)
.leptos_routes(&leptos_options, routes, || view! { <App/> })
.fallback(file_and_error_handler)
.with_state(leptos_options);

View File

@@ -5,4 +5,4 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
gtk = { version = "0.5.0", package = "gtk4" }
gtk = { version = "0.5.0", package = "gtk4" }

View File

@@ -16,7 +16,10 @@ actix-web = { version = "4", optional = true, features = ["macros"] }
console_log = "1"
console_error_panic_hook = "0.1"
cfg-if = "1"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos = { path = "../../leptos", features = [
"nightly",
"experimental-islands",
] }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_router = { path = "../../router", features = ["nightly"] }

View File

@@ -41,11 +41,13 @@ pub fn Stories() -> impl IntoView {
};
view! {
<div class="news-view">
<div class="news-list-nav">
<span>
{move || if page() > 1 {
view! {
<a class="page-link"
href=move || format!("/{}?page={}", story_type(), page() - 1)
attr:aria_label="Previous Page"
@@ -55,6 +57,7 @@ pub fn Stories() -> impl IntoView {
}.into_any()
} else {
view! {
<span class="page-link disabled" aria-hidden="true">
"< prev"
</span>
@@ -76,22 +79,24 @@ pub fn Stories() -> impl IntoView {
<main class="news-list">
<div>
<Transition
fallback=move || view! { <p>"Loading..."</p> }
set_pending
fallback=move || view! { <p>"Loading..."</p> }
set_pending=set_pending.into()
>
{move || match stories.get() {
None => None,
Some(None) => Some(view! { <p>"Error loading stories."</p> }.into_any()),
Some(None) => Some(view! { <p>"Error loading stories."</p> }.into_any()),
Some(Some(stories)) => {
Some(view! {
<ul>
<For
each=move || stories.clone()
key=|story| story.id
let:story
>
<Story story/>
</For>
view=move |story: api::Story| {
view! {
<Story story/>
}
}
/>
</ul>
}.into_any())
}
@@ -120,7 +125,7 @@ fn Story(story: api::Story) -> impl IntoView {
}.into_view()
} else {
let title = story.title.clone();
view! { <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }.into_view()
view! { <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }.into_view()
}}
</span>
<br />
@@ -129,7 +134,7 @@ fn Story(story: api::Story) -> impl IntoView {
view! {
<span>
{"by "}
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
{format!(" {} | ", story.time_ago)}
<A href=format!("/stories/{}", story.id)>
{if story.comments_count.unwrap_or_default() > 0 {
@@ -142,7 +147,7 @@ fn Story(story: api::Story) -> impl IntoView {
}.into_view()
} else {
let title = story.title.clone();
view! { <A href=format!("/item/{}", story.id)>{title.clone()}</A> }.into_view()
view! { <A href=format!("/item/{}", story.id)>{title.clone()}</A> }.into_view()
}}
</span>
{(story.story_type != "link").then(|| view! {

View File

@@ -57,10 +57,8 @@ pub fn Story() -> impl IntoView {
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
let:comment
>
<Comment comment />
</For>
view=move |comment| view! { <Comment comment /> }
/>
</ul>
</div>
</div>
@@ -102,10 +100,8 @@ pub fn Comment(comment: api::Comment) -> impl IntoView {
<For
each=move || comments.clone()
key=|comment| comment.id
let:comment
>
<Comment comment />
</For>
view=move |comment: api::Comment| view! { <Comment comment /> }
/>
</ul>
}
})}

View File

@@ -15,7 +15,7 @@ pub fn error_template(errors: Option<RwSignal<Errors>>) -> View {
// a unique key for each item as a reference
key=|(key, _)| key.clone()
// renders each item to a view
children=move | (_, error)| {
view= move | (_, error)| {
let error_string = error.to_string();
view! {

View File

@@ -18,7 +18,7 @@ if #[cfg(feature = "ssr")] {
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(App);
let routes = generate_route_list(|| view! { <App/> }).await;
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");

View File

@@ -79,7 +79,7 @@ pub fn Stories() -> impl IntoView {
<div>
<Transition
fallback=move || view! { <p>"Loading..."</p> }
set_pending
set_pending=set_pending.into()
>
{move || match stories.get() {
None => None,
@@ -90,10 +90,12 @@ pub fn Stories() -> impl IntoView {
<For
each=move || stories.clone()
key=|story| story.id
let:story
>
<Story story/>
</For>
view=move | story: api::Story| {
view! {
<Story story/>
}
}
/>
</ul>
}.into_any())
}

View File

@@ -57,10 +57,8 @@ pub fn Story() -> impl IntoView {
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
let:comment
>
<Comment comment />
</For>
view=move | comment| view! { <Comment comment /> }
/>
</ul>
</div>
</div>
@@ -102,10 +100,8 @@ pub fn Comment(comment: api::Comment) -> impl IntoView {
<For
each=move || comments.clone()
key=|comment| comment.id
let:comment
>
<Comment comment />
</For>
view=move | comment: api::Comment| view! { <Comment comment /> }
/>
</ul>
}
})}

View File

@@ -1,5 +1,5 @@
[package]
name = "hackernews_islands"
name = "hackernews"
version = "0.1.0"
edition = "2021"

View File

@@ -15,7 +15,7 @@ pub fn error_template(errors: Option<RwSignal<Errors>>) -> View {
// a unique key for each item as a reference
key=|(key, _)| key.clone()
// renders each item to a view
children= move | (_, error)| {
view= move | (_, error)| {
let error_string = error.to_string();
view! {

View File

@@ -1,7 +1,7 @@
#[cfg(feature = "ssr")]
mod ssr_imports {
pub use axum::{routing::get, Router};
pub use hackernews_islands::fallback::file_and_error_handler;
pub use hackernews::fallback::file_and_error_handler;
pub use leptos::*;
pub use leptos_axum::{generate_route_list, LeptosRoutes};
pub use tower_http::{compression::CompressionLayer, services::ServeFile};
@@ -10,18 +10,18 @@ mod ssr_imports {
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use hackernews_islands::*;
use hackernews::*;
use ssr_imports::*;
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(App);
let routes = generate_route_list(|| view! { <App/> }).await;
// build our application with a route
let app = Router::new()
.route("/favicon.ico", get(file_and_error_handler))
.leptos_routes(&leptos_options, routes, App)
.leptos_routes(&leptos_options, routes, || view! { <App/> })
.fallback(file_and_error_handler)
.with_state(leptos_options);
@@ -37,7 +37,7 @@ async fn main() {
// client-only stuff for Trunk
#[cfg(not(feature = "ssr"))]
pub fn main() {
use hackernews_islands::*;
use hackernews::*;
use leptos::*;
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();

View File

@@ -91,17 +91,19 @@ pub fn Stories() -> impl IntoView {
<div>
<Transition
fallback=|| ()
set_pending
set_pending=set_pending.into()
>
{move || stories.get().map(|story| story.map(|stories| view! {
<ul>
<For
each=move || stories.clone()
key=|story| story.id
let:story
>
<Story story/>
</For>
view=move |story: api::Story| {
view! {
<Story story/>
}
}
/>
</ul>
}))}
</Transition>

View File

@@ -1,2 +0,0 @@
[build]
target = "wasm32-unknown-unknown"

View File

@@ -1,98 +0,0 @@
[package]
name = "hackernews-js-fetch"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[profile.release]
codegen-units = 1
lto = true
[dependencies]
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
leptos = { path = "../../leptos", features = ["nightly"] }
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_router = { path = "../../router", features = ["nightly"] }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
tracing = "0.1"
gloo-net = { version = "0.4.0", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6", default-features = false, optional = true }
tower = { version = "0.4.13", optional = true }
http = { version = "0.2.8", optional = true }
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal", "Request", "Response"] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = { version = "0.4.37", features = ["futures-core-03-stream"], optional = true }
axum-js-fetch = { version = "0.2.1", optional = true }
lazy_static = "1.4.0"
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:tower",
"dep:http",
"dep:axum",
"dep:wasm-bindgen-futures",
"dep:axum-js-fetch",
"leptos/ssr",
"leptos_axum/wasm",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "http", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "hackernews_axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Greg Johnston
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1 +0,0 @@
extend = [{ path = "../cargo-make/main.toml" }]

View File

@@ -1,14 +0,0 @@
# Leptos Hacker News Example with Axum
This example uses the basic Hacker News example as its basis, but shows how to run the server side as WASM running in a JS environment. In this example, Deno is used as the runtime.
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle. Make sure you have trunk installed with `cargo install trunk`.
## Server Side Rendering with Deno
To run the Deno version, run
```bash
deno task build
deno task start
```

View File

@@ -1,8 +0,0 @@
{
"tasks": {
"build:server": "wasm-pack build --release --target web --out-name server --features ssr --no-default-features",
"build:client": "wasm-pack build --release --target web --out-name client --features hydrate --no-default-features",
"build": "deno task build:server & deno task build:client",
"start": "deno run --allow-read --allow-net run.ts"
}
}

View File

@@ -1,58 +0,0 @@
{
"version": "2",
"remote": {
"https://deno.land/std@0.177.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462",
"https://deno.land/std@0.177.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3",
"https://deno.land/std@0.177.0/async/abortable.ts": "73acfb3ed7261ce0d930dbe89e43db8d34e017b063cf0eaa7d215477bf53442e",
"https://deno.land/std@0.177.0/async/deadline.ts": "c5facb0b404eede83e38bd2717ea8ab34faa2ffb20ef87fd261fcba32ba307aa",
"https://deno.land/std@0.177.0/async/debounce.ts": "adab11d04ca38d699444ac8a9d9856b4155e8dda2afd07ce78276c01ea5a4332",
"https://deno.land/std@0.177.0/async/deferred.ts": "42790112f36a75a57db4a96d33974a936deb7b04d25c6084a9fa8a49f135def8",
"https://deno.land/std@0.177.0/async/delay.ts": "73aa04cec034c84fc748c7be49bb15cac3dd43a57174bfdb7a4aec22c248f0dd",
"https://deno.land/std@0.177.0/async/mod.ts": "f04344fa21738e5ad6bea37a6bfffd57c617c2d372bb9f9dcfd118a1b622e576",
"https://deno.land/std@0.177.0/async/mux_async_iterator.ts": "70c7f2ee4e9466161350473ad61cac0b9f115cff4c552eaa7ef9d50c4cbb4cc9",
"https://deno.land/std@0.177.0/async/pool.ts": "fd082bd4aaf26445909889435a5c74334c017847842ec035739b4ae637ae8260",
"https://deno.land/std@0.177.0/async/retry.ts": "5efa3ba450ac0c07a40a82e2df296287b5013755d232049efd7ea2244f15b20f",
"https://deno.land/std@0.177.0/async/tee.ts": "47e42d35f622650b02234d43803d0383a89eb4387e1b83b5a40106d18ae36757",
"https://deno.land/std@0.177.0/collections/_utils.ts": "5114abc026ddef71207a79609b984614e66a63a4bda17d819d56b0e72c51527e",
"https://deno.land/std@0.177.0/collections/deep_merge.ts": "5a8ed29030f4471a5272785c57c3455fa79697b9a8f306013a8feae12bafc99a",
"https://deno.land/std@0.177.0/crypto/_fnv/fnv32.ts": "e4649dfdefc5c987ed53c3c25db62db771a06d9d1b9c36d2b5cf0853b8e82153",
"https://deno.land/std@0.177.0/crypto/_fnv/fnv64.ts": "bfa0e4702061fdb490a14e6bf5f9168a22fb022b307c5723499469bfefca555e",
"https://deno.land/std@0.177.0/crypto/_fnv/index.ts": "169c213eb75de2d6738c1ed66a8e5782bd222b70b187cc4e7fb7b73edfcf0927",
"https://deno.land/std@0.177.0/crypto/_fnv/util.ts": "accba12bfd80a352e32a872f87df2a195e75561f1b1304a4cb4f5a4648d288f9",
"https://deno.land/std@0.177.0/crypto/_util.ts": "0522d1466e3c92df84cea94da85dbb7bd93e629dacb2aa5b39cab432ab7cb3d6",
"https://deno.land/std@0.177.0/crypto/_wasm/lib/deno_std_wasm_crypto.generated.mjs": "5dedb7f9aa05f0e18ed017691c58df5f4686e4cbbd70368c6f896e5cca03f2b4",
"https://deno.land/std@0.177.0/crypto/_wasm/mod.ts": "e2df88236fc061eac7a89e8cb0b97843f5280b08b2a990e473b7397a3e566003",
"https://deno.land/std@0.177.0/crypto/crypto.ts": "d5ce53784ab7b1348095389426a7ea98536223fb143812ecb50724a0aa1ec657",
"https://deno.land/std@0.177.0/crypto/keystack.ts": "877ab0f19eb7d37ad6495190d3c3e39f58e9c52e0b6a966f82fd6df67ca55f90",
"https://deno.land/std@0.177.0/crypto/mod.ts": "885738e710868202d7328305b0c0c134e36a2d9c98ceab9513ea2442863c00eb",
"https://deno.land/std@0.177.0/crypto/timing_safe_equal.ts": "8d69ab611c67fe51b6127d97fcfb4d8e7d0e1b6b4f3e0cc4ab86744c3691f965",
"https://deno.land/std@0.177.0/crypto/to_hash_string.ts": "fe4e95239d7afb617f469bc2f76ff20f888ddb8d1385e0d92276f6e4d5a809d1",
"https://deno.land/std@0.177.0/encoding/base64.ts": "7de04c2f8aeeb41453b09b186480be90f2ff357613b988e99fabb91d2eeceba1",
"https://deno.land/std@0.177.0/encoding/base64url.ts": "3f1178f6446834457b16bfde8b559c1cd3481727fe384d3385e4a9995dc2d851",
"https://deno.land/std@0.177.0/encoding/hex.ts": "50f8c95b52eae24395d3dfcb5ec1ced37c5fe7610ef6fffdcc8b0fdc38e3b32f",
"https://deno.land/std@0.177.0/flags/mod.ts": "d1cdefa18472ef69858a17df5cf7c98445ed27ac10e1460183081303b0ebc270",
"https://deno.land/std@0.177.0/fmt/colors.ts": "938c5d44d889fb82eff6c358bea8baa7e85950a16c9f6dae3ec3a7a729164471",
"https://deno.land/std@0.177.0/http/file_server.ts": "86d624c0c908a4a377090668ee872cf5c064245da71b3e8f8f7df888cac869d5",
"https://deno.land/std@0.177.0/http/http_status.ts": "8a7bcfe3ac025199ad804075385e57f63d055b2aed539d943ccc277616d6f932",
"https://deno.land/std@0.177.0/http/server.ts": "cbb17b594651215ba95c01a395700684e569c165a567e4e04bba327f41197433",
"https://deno.land/std@0.177.0/http/util.ts": "36c0b60c031f9e2ba024353ed11693f76c714551f9e766b36cdaacda54f25a21",
"https://deno.land/std@0.177.0/media_types/_db.ts": "7606d83e31f23ce1a7968cbaee852810c2cf477903a095696cdc62eaab7ce570",
"https://deno.land/std@0.177.0/media_types/_util.ts": "916efbd30b6148a716f110e67a4db29d6949bf4048997b754415dd7e42c52378",
"https://deno.land/std@0.177.0/media_types/content_type.ts": "c682589a0aeb016bfed355cc1ed6fbb3ead2ea48fc0000ac5de6a5730613ad1c",
"https://deno.land/std@0.177.0/media_types/format_media_type.ts": "1e35e16562e5c417401ffc388a9f8f421f97f0ee06259cbe990c51bae4e6c7a8",
"https://deno.land/std@0.177.0/media_types/get_charset.ts": "8be15a1fd31a545736b91ace56d0e4c66ea0d7b3fdc5c90760e8202e7b4b1fad",
"https://deno.land/std@0.177.0/media_types/parse_media_type.ts": "bed260d868ea271445ae41d748e7afed9b5a7f407d2777ead08cecf73e9278de",
"https://deno.land/std@0.177.0/media_types/type_by_extension.ts": "6076a7fc63181d70f92ec582fdea2c927eb2cfc7f9c9bee9d6add2aca86f2355",
"https://deno.land/std@0.177.0/media_types/vendor/mime-db.v1.52.0.ts": "6925bbcae81ca37241e3f55908d0505724358cda3384eaea707773b2c7e99586",
"https://deno.land/std@0.177.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0",
"https://deno.land/std@0.177.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b",
"https://deno.land/std@0.177.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0",
"https://deno.land/std@0.177.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000",
"https://deno.land/std@0.177.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1",
"https://deno.land/std@0.177.0/path/mod.ts": "4b83694ac500d7d31b0cdafc927080a53dc0c3027eb2895790fb155082b0d232",
"https://deno.land/std@0.177.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d",
"https://deno.land/std@0.177.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1",
"https://deno.land/std@0.177.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba",
"https://deno.land/std@0.177.0/version.ts": "259c8866ec257c3511b437baa95205a86761abaef852a9b2199072accb2ef046"
}
}

View File

@@ -1,8 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="css" href="/style.css"/>
</head>
<body></body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,326 +0,0 @@
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
font-size: 15px;
background-color: #f2f3f5;
margin: 0;
padding-top: 55px;
color: #34495e;
overflow-y: scroll
}
a {
color: #34495e;
text-decoration: none
}
.header {
background-color: #335d92;
position: fixed;
z-index: 999;
height: 55px;
top: 0;
left: 0;
right: 0
}
.header .inner {
max-width: 800px;
box-sizing: border-box;
margin: 0 auto;
padding: 15px 5px
}
.header a {
color: rgba(255, 255, 255, .8);
line-height: 24px;
transition: color .15s ease;
display: inline-block;
vertical-align: middle;
font-weight: 300;
letter-spacing: .075em;
margin-right: 1.8em
}
.header a:hover {
color: #fff
}
.header a.active {
color: #fff;
font-weight: 400
}
.header a:nth-child(6) {
margin-right: 0
}
.header .github {
color: #fff;
font-size: .9em;
margin: 0;
float: right
}
.logo {
width: 24px;
margin-right: 10px;
display: inline-block;
vertical-align: middle
}
.view {
max-width: 800px;
margin: 0 auto;
position: relative
}
.fade-enter-active,
.fade-exit-active {
transition: all .2s ease
}
.fade-enter,
.fade-exit-active {
opacity: 0
}
@media (max-width:860px) {
.header .inner {
padding: 15px 30px
}
}
@media (max-width:600px) {
.header .inner {
padding: 15px
}
.header a {
margin-right: 1em
}
.header .github {
display: none
}
}
.news-view {
padding-top: 45px
}
.news-list,
.news-list-nav {
background-color: #fff;
border-radius: 2px
}
.news-list-nav {
padding: 15px 30px;
position: fixed;
text-align: center;
top: 55px;
left: 0;
right: 0;
z-index: 998;
box-shadow: 0 1px 2px rgba(0, 0, 0, .1)
}
.news-list-nav .page-link {
margin: 0 1em
}
.news-list-nav .disabled {
color: #aaa
}
.news-list {
position: absolute;
margin: 30px 0;
width: 100%;
transition: all .5s cubic-bezier(.55, 0, .1, 1)
}
.news-list ul {
list-style-type: none;
padding: 0;
margin: 0
}
@media (max-width:600px) {
.news-list {
margin: 10px 0
}
}
.news-item {
background-color: #fff;
padding: 20px 30px 20px 80px;
border-bottom: 1px solid #eee;
position: relative;
line-height: 20px
}
.news-item .score {
color: #335d92;
font-size: 1.1em;
font-weight: 700;
position: absolute;
top: 50%;
left: 0;
width: 80px;
text-align: center;
margin-top: -10px
}
.news-item .host,
.news-item .meta {
font-size: .85em;
color: #626262
}
.news-item .host a,
.news-item .meta a {
color: #626262;
text-decoration: underline
}
.news-item .host a:hover,
.news-item .meta a:hover {
color: #335d92
}
.item-view-header {
background-color: #fff;
padding: 1.8em 2em 1em;
box-shadow: 0 1px 2px rgba(0, 0, 0, .1)
}
.item-view-header h1 {
display: inline;
font-size: 1.5em;
margin: 0;
margin-right: .5em
}
.item-view-header .host,
.item-view-header .meta,
.item-view-header .meta a {
color: #626262
}
.item-view-header .meta a {
text-decoration: underline
}
.item-view-comments {
background-color: #fff;
margin-top: 10px;
padding: 0 2em .5em
}
.item-view-comments-header {
margin: 0;
font-size: 1.1em;
padding: 1em 0;
position: relative
}
.item-view-comments-header .spinner {
display: inline-block;
margin: -15px 0
}
.comment-children {
list-style-type: none;
padding: 0;
margin: 0
}
@media (max-width:600px) {
.item-view-header h1 {
font-size: 1.25em
}
}
.comment-children .comment-children {
margin-left: 1.5em
}
.comment {
border-top: 1px solid #eee;
position: relative
}
.comment .by,
.comment .text,
.comment .toggle {
font-size: .9em;
margin: 1em 0
}
.comment .by {
color: #626262
}
.comment .by a {
color: #626262;
text-decoration: underline
}
.comment .text {
overflow-wrap: break-word
}
.comment .text a:hover {
color: #335d92
}
.comment .text pre {
white-space: pre-wrap
}
.comment .toggle {
background-color: #fffbf2;
padding: .3em .5em;
border-radius: 4px
}
.comment .toggle a {
color: #626262;
cursor: pointer
}
.comment .toggle.open {
padding: 0;
background-color: transparent;
margin-bottom: -.5em
}
.user-view {
background-color: #fff;
box-sizing: border-box;
padding: 2em 3em
}
.user-view h1 {
margin: 0;
font-size: 1.5em
}
.user-view .meta {
list-style-type: none;
padding: 0
}
.user-view .label {
display: inline-block;
min-width: 4em
}
.user-view .about {
margin: 1em 0
}
.user-view .links a {
text-decoration: underline
}

View File

@@ -1,13 +0,0 @@
import init, { Handler } from "./pkg/server.js";
import { serveDir } from "https://deno.land/std/http/file_server.ts";
await init();
const handler = await Handler.new();
Deno.serve((req) => {
const u = new URL(req.url);
if (u.pathname.startsWith("/pkg") || u.pathname.startsWith("/public")) {
return serveDir(req);
}
return handler.serve(req);
});

View File

@@ -1,91 +0,0 @@
use leptos::Serializable;
use serde::{Deserialize, Serialize};
pub fn story(path: &str) -> String {
format!("https://node-hnapi.herokuapp.com/{path}")
}
pub fn user(path: &str) -> String {
format!("https://hacker-news.firebaseio.com/v0/user/{path}.json")
}
#[cfg(not(feature = "ssr"))]
pub async fn fetch_api<T>(path: &str) -> Option<T>
where
T: Serializable,
{
let abort_controller = web_sys::AbortController::new().ok();
let abort_signal = abort_controller.as_ref().map(|a| a.signal());
// abort in-flight requests if the Scope is disposed
// i.e., if we've navigated away from this page
leptos::on_cleanup(move || {
if let Some(abort_controller) = abort_controller {
abort_controller.abort()
}
});
let json = gloo_net::http::Request::get(path)
.abort_signal(abort_signal.as_ref())
.send()
.await
.map_err(|e| log::error!("{e}"))
.ok()?
.text()
.await
.ok()?;
T::de(&json).ok()
}
#[cfg(feature = "ssr")]
pub async fn fetch_api<T>(path: &str) -> Option<T>
where
T: Serializable,
{
let json = reqwest::get(path)
.await
.map_err(|e| log::error!("{e}"))
.ok()?
.text()
.await
.ok()?;
T::de(&json).map_err(|e| log::error!("{e}")).ok()
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct Story {
pub id: usize,
pub title: String,
pub points: Option<i32>,
pub user: Option<String>,
pub time: usize,
pub time_ago: String,
#[serde(alias = "type")]
pub story_type: String,
pub url: String,
#[serde(default)]
pub domain: String,
#[serde(default)]
pub comments: Option<Vec<Comment>>,
pub comments_count: Option<usize>,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct Comment {
pub id: usize,
pub level: usize,
pub user: Option<String>,
pub time: usize,
pub time_ago: String,
pub content: Option<String>,
pub comments: Vec<Comment>,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct User {
pub created: usize,
pub id: String,
pub karma: i32,
pub about: Option<String>,
}

View File

@@ -1,28 +0,0 @@
use leptos::{view, Errors, For, IntoView, RwSignal, View};
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them
pub fn error_template(errors: Option<RwSignal<Errors>>) -> View {
let Some(errors) = errors else {
panic!("No Errors found and we expected errors!");
};
view! {
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each=errors
// a unique key for each item as a reference
key=|(key, _)| key.clone()
// renders each item to a view
children= move |(_, error)| {
let error_string = error.to_string();
view! {
<p>"Error: " {error_string}</p>
}
}
/>
}
.into_view()
}

View File

@@ -1,39 +0,0 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{Body, BoxBody},
extract::State,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
//use tower::ServiceExt;
use leptos::{LeptosOptions};
use crate::error_template::error_template;
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(options.to_owned(), || error_template(None));
handler(req).await.into_response()
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
_ = req;
_ = root;
todo!()
}
}
}

View File

@@ -1,92 +0,0 @@
use cfg_if::cfg_if;
use leptos::{component, view, IntoView};
use leptos_meta::*;
use leptos_router::*;
use log::{info, Level};
mod api;
pub mod error_template;
pub mod fallback;
mod routes;
use routes::{counters::*, nav::*, stories::*, story::*, users::*};
#[component]
pub fn App() -> impl IntoView {
provide_meta_context();
view! {
<>
<Link rel="shortcut icon" type_="image/ico" href="/public/favicon.ico"/>
<Stylesheet id="leptos" href="/public/style.css"/>
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
<Router>
<Nav />
<main>
<Routes>
<Route path="users/:id" view=User/>
<Route path="stories/:id" view=Story/>
<Route path=":stories?" view=Stories/>
<Route path="counter" view=Counter/>
</Routes>
</main>
</Router>
</>
}
}
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(move || {
view! { <App/> }
});
}
} else if #[cfg(feature = "ssr")] {
use axum::{
Router,
routing::post
};
use leptos_axum::{generate_route_list, LeptosRoutes};
use wasm_bindgen::prelude::*;
use leptos::*;
#[wasm_bindgen]
pub struct Handler(axum_js_fetch::App);
#[wasm_bindgen]
impl Handler {
pub async fn new() -> Self {
console_log::init_with_level(Level::Debug);
console_error_panic_hook::set_once();
let leptos_options = LeptosOptions::builder().output_name("client").site_pkg_dir("pkg").build();
let routes = generate_route_list(App);
ClearServerCount::register_explicit().unwrap();
AdjustServerCount::register_explicit().unwrap();
GetServerCount::register_explicit().unwrap();
// build our application with a route
let app: axum::Router<(), axum::body::Body> = Router::new()
.leptos_routes(&leptos_options, routes, || view! { <App/> } )
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.with_state(leptos_options);
info!("creating handler instance");
Self(axum_js_fetch::App::new(app))
}
pub async fn serve(&self, req: web_sys::Request) -> web_sys::Response {
self.0.serve(req).await
}
}
}
}

View File

@@ -1,5 +0,0 @@
pub mod counters;
pub mod nav;
pub mod stories;
pub mod story;
pub mod users;

View File

@@ -1,79 +0,0 @@
/// This file is mostly copied from the counters isomorphic example
/// just to demonstrate server actions in wasm
use leptos::*;
use std::sync::atomic::{AtomicI32, Ordering};
static COUNT: AtomicI32 = AtomicI32::new(0);
// "/api" is an optional prefix that allows you to locate server functions wherever you'd like on the server
#[server(GetServerCount, "/api")]
pub async fn get_server_count() -> Result<i32, ServerFnError> {
Ok(COUNT.load(Ordering::Relaxed))
}
#[server(AdjustServerCount, "/api")]
pub async fn adjust_server_count(
delta: i32,
msg: String,
) -> Result<i32, ServerFnError> {
let new = COUNT.load(Ordering::Relaxed) + delta;
COUNT.store(new, Ordering::Relaxed);
println!("message = {:?}", msg);
Ok(new)
}
#[server(ClearServerCount, "/api")]
pub async fn clear_server_count() -> Result<i32, ServerFnError> {
COUNT.store(0, Ordering::Relaxed);
Ok(0)
}
// This is an example of "single-user" server functions
// The counter value is loaded from the server, and re-fetches whenever
// it's invalidated by one of the user's own actions
// This is the typical pattern for a CRUD app
#[component]
pub fn Counter() -> impl IntoView {
let dec = create_action(|_| adjust_server_count(-1, "decing".into()));
let inc = create_action(|_| adjust_server_count(1, "incing".into()));
let clear = create_action(|_| clear_server_count());
let counter = create_resource(
move || {
(
dec.version().get(),
inc.version().get(),
clear.version().get(),
)
},
|_| get_server_count(),
);
let value =
move || counter.get().map(|count| count.unwrap_or(0)).unwrap_or(0);
let error_msg = move || {
counter.get().and_then(|res| match res {
Ok(_) => None,
Err(e) => Some(e),
})
};
view! {
<div>
<h2>"Simple Counter"</h2>
<p>
"This counter sets the value on the server and automatically reloads the new value."
</p>
<div>
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
<button on:click=move |_| dec.dispatch(())>"-1"</button>
<span>"Value: " {value}</span>
<button on:click=move |_| inc.dispatch(())>"+1"</button>
</div>
{move || {
error_msg()
.map(|msg| {
view! { <p>"Error: " {msg.to_string()}</p> }
})
}}
</div>
}
}

View File

@@ -1,30 +0,0 @@
use leptos::{component, view, IntoView};
use leptos_router::*;
#[component]
pub fn Nav() -> impl IntoView {
view! {
<header class="header">
<nav class="inner">
<A href="/">
<strong>"HN"</strong>
</A>
<A href="/new">
<strong>"New"</strong>
</A>
<A href="/show">
<strong>"Show"</strong>
</A>
<A href="/ask">
<strong>"Ask"</strong>
</A>
<A href="/job">
<strong>"Jobs"</strong>
</A>
<a class="github" href="http://github.com/leptos-rs/leptos" target="_blank" rel="noreferrer">
"Built with Leptos"
</a>
</nav>
</header>
}
}

View File

@@ -1,161 +0,0 @@
use crate::api;
use leptos::*;
use leptos_router::*;
fn category(from: &str) -> &'static str {
match from {
"new" => "newest",
"show" => "show",
"ask" => "ask",
"job" => "jobs",
_ => "news",
}
}
#[component]
pub fn Stories() -> impl IntoView {
let query = use_query_map();
let params = use_params_map();
let page = move || {
query
.with(|q| q.get("page").and_then(|page| page.parse::<usize>().ok()))
.unwrap_or(1)
};
let story_type = move || {
params
.with(|p| p.get("stories").cloned())
.unwrap_or_else(|| "top".to_string())
};
let stories = create_resource(
move || (page(), story_type()),
move |(page, story_type)| async move {
let path = format!("{}?page={}", category(&story_type), page);
api::fetch_api::<Vec<api::Story>>(&api::story(&path)).await
},
);
let (pending, set_pending) = create_signal(false);
let hide_more_link = move || {
pending()
|| stories.get().unwrap_or(None).unwrap_or_default().len() < 28
};
view! {
<div class="news-view">
<div class="news-list-nav">
<span>
{move || if page() > 1 {
view! {
<a class="page-link"
href=move || format!("/{}?page={}", story_type(), page() - 1)
attr:aria_label="Previous Page"
>
"< prev"
</a>
}.into_any()
} else {
view! {
<span class="page-link disabled" aria-hidden="true">
"< prev"
</span>
}.into_any()
}}
</span>
<span>"page " {page}</span>
<Transition
fallback=move || view! { <p>"Loading..."</p> }
>
<span class="page-link"
class:disabled=move || hide_more_link()
aria-hidden=move || hide_more_link()
>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
>
"more >"
</a>
</span>
</Transition>
</div>
<main class="news-list">
<div>
<Transition
fallback=move || view! { <p>"Loading..."</p> }
set_pending
>
{move || match stories.get() {
None => None,
Some(None) => Some(view! { <p>"Error loading stories."</p> }.into_any()),
Some(Some(stories)) => {
Some(view! {
<ul>
<For
each=move || stories.clone()
key=|story| story.id
let:story
>
<Story story/>
</For>
</ul>
}.into_any())
}
}}
</Transition>
</div>
</main>
</div>
}
}
#[component]
fn Story(story: api::Story) -> impl IntoView {
view! {
<li class="news-item">
<span class="score">{story.points}</span>
<span class="title">
{if !story.url.starts_with("item?id=") {
view! {
<span>
<a href=story.url target="_blank" rel="noreferrer">
{story.title.clone()}
</a>
<span class="host">"("{story.domain}")"</span>
</span>
}.into_view()
} else {
let title = story.title.clone();
view! { <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }.into_view()
}}
</span>
<br />
<span class="meta">
{if story.story_type != "job" {
view! {
<span>
{"by "}
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
{format!(" {} | ", story.time_ago)}
<A href=format!("/stories/{}", story.id)>
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"discuss".into()
}}
</A>
</span>
}.into_view()
} else {
let title = story.title.clone();
view! { <A href=format!("/item/{}", story.id)>{title.clone()}</A> }.into_view()
}}
</span>
{(story.story_type != "link").then(|| view! {
" "
<span class="label">{story.story_type}</span>
})}
</li>
}
}

View File

@@ -1,128 +0,0 @@
use crate::api;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = create_resource(
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move {
if id.is_empty() {
None
} else {
api::fetch_api::<api::Story>(&api::story(&format!("item/{id}")))
.await
}
},
);
let meta_description = move || {
story
.get()
.and_then(|story| story.map(|story| story.title))
.unwrap_or_else(|| "Loading story...".to_string())
};
view! {
<>
<Meta name="description" content=meta_description/>
<Suspense fallback=|| view! { "Loading..." }>
{move || story.get().map(|story| match story {
None => view! { <div class="item-view">"Error loading this story."</div> },
Some(story) => view! {
<div class="item-view">
<div class="item-view-header">
<a href=story.url target="_blank">
<h1>{story.title}</h1>
</a>
<span class="host">
"("{story.domain}")"
</span>
{story.user.map(|user| view! { <p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>})}
</div>
<div class="item-view-comments">
<p class="item-view-comments-header">
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"No comments yet.".into()
}}
</p>
<ul class="comment-children">
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
let:comment
>
<Comment comment/>
</For>
</ul>
</div>
</div>
}})
}
</Suspense>
</>
}
}
#[component]
pub fn Comment(comment: api::Comment) -> impl IntoView {
let (open, set_open) = create_signal(true);
view! {
<li class="comment">
<div class="by">
<A href=format!("/users/{}", comment.user.clone().unwrap_or_default())>{comment.user.clone()}</A>
{format!(" {}", comment.time_ago)}
</div>
<div class="text" inner_html=comment.content></div>
{(!comment.comments.is_empty()).then(|| {
view! {
<div>
<div class="toggle" class:open=open>
<a on:click=move |_| set_open.update(|n| *n = !*n)>
{
let comments_len = comment.comments.len();
move || if open() {
"[-]".into()
} else {
format!("[+] {}{} collapsed", comments_len, pluralize(comments_len))
}
}
</a>
</div>
{move || open().then({
let comments = comment.comments.clone();
move || view! {
<ul class="comment-children">
<For
each=move || comments.clone()
key=|comment| comment.id
let:comment
>
<Comment comment />
</For>
</ul>
}
})}
</div>
}
})}
</li>
}
}
fn pluralize(n: usize) -> &'static str {
if n == 1 {
" reply"
} else {
" replies"
}
}

View File

@@ -1,46 +0,0 @@
use crate::api::{self, User};
use leptos::*;
use leptos_router::*;
#[component]
pub fn User() -> impl IntoView {
let params = use_params_map();
let user = create_resource(
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move {
if id.is_empty() {
None
} else {
api::fetch_api::<User>(&api::user(&id)).await
}
},
);
view! {
<div class="user-view">
<Suspense fallback=|| view! { "Loading..." }>
{move || user.get().map(|user| match user {
None => view! { <h1>"User not found."</h1> }.into_any(),
Some(user) => view! {
<div>
<h1>"User: " {&user.id}</h1>
<ul class="meta">
<li>
<span class="label">"Created: "</span> {user.created}
</li>
<li>
<span class="label">"Karma: "</span> {user.karma}
</li>
{user.about.as_ref().map(|about| view! { <li inner_html=about class="about"></li> })}
</ul>
<p class="links">
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>
" | "
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
</p>
</div>
}.into_any()
})}
</Suspense>
</div>
}
}

View File

@@ -168,7 +168,7 @@ pub fn App() -> impl IntoView {
<For
each={data}
key={|row| row.id}
children=move |row: RowData| {
view=move |row: RowData| {
let row_id = row.id;
let (label, _) = row.label;
on_cleanup({

View File

@@ -19,7 +19,7 @@ async fn main() {
let addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
let routes = generate_route_list(|| view! { <App/> }).await;
// build our application with a route
let app = Router::new()

View File

@@ -70,7 +70,7 @@ pub fn App() -> impl IntoView {
fetch_user_info.dispatch(());
}
log::debug!("User is logged in: {}", logged_in.get_untracked());
log::debug!("User is logged in: {}", logged_in.get());
// -- effects -- //

View File

@@ -46,7 +46,7 @@ pub fn ErrorTemplate(
// a unique key for each item as a reference
key=|(index, _error)| *index
// renders each item to a view
children=move |error| {
view= move |error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! {

View File

@@ -16,7 +16,7 @@ if #[cfg(feature = "ssr")] {
use session_auth_axum::state::AppState;
use session_auth_axum::fallback::file_and_error_handler;
use leptos_axum::{generate_route_list, LeptosRoutes, handle_server_fns_with_context};
use leptos::{logging::log, provide_context, get_configuration};
use leptos::{logging::log, view, provide_context, get_configuration};
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
use axum_session::{SessionConfig, SessionLayer, SessionStore};
use axum_session_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool};
@@ -39,7 +39,7 @@ if #[cfg(feature = "ssr")] {
provide_context(auth_session.clone());
provide_context(app_state.pool.clone());
},
TodoApp
|| view! { <TodoApp/> }
);
handler(req).await.into_response()
}
@@ -80,7 +80,7 @@ if #[cfg(feature = "ssr")] {
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(TodoApp);
let routes = generate_route_list(|| view! { <TodoApp/> }).await;
let app_state = AppState{
leptos_options,

View File

@@ -10,7 +10,7 @@ async fn main() {
let addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
let routes = generate_route_list(|| view! { <App/> }).await;
// Explicit server function registration is no longer required
// on the main branch. On 0.3.0 and earlier, uncomment the lines

View File

@@ -20,7 +20,7 @@ async fn second_wait_fn(seconds: u64) -> Result<(), ServerFnError> {
#[component]
pub fn App() -> impl IntoView {
let style = r"
let style = r#"
nav {
display: flex;
width: 100%;
@@ -30,7 +30,7 @@ pub fn App() -> impl IntoView {
[aria-current] {
font-weight: bold;
}
";
"#;
view! {
<style>{style}</style>

View File

@@ -46,7 +46,7 @@ pub fn ErrorTemplate(
// a unique key for each item as a reference
key=|(index, _error)| *index
// renders each item to a view
children=move |error| {
view= move |error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! {

View File

@@ -48,7 +48,7 @@ cfg_if! {
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(TodoApp);
let routes = generate_route_list(|| view! { <TodoApp/> }).await;
// build our application with a route
let app = Router::new()

View File

@@ -46,7 +46,7 @@ pub fn ErrorTemplate(
// a unique key for each item as a reference
key=|(index, _error)| *index
// renders each item to a view
children= move |error| {
view= move |error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! {

View File

@@ -24,7 +24,7 @@ cfg_if! {
move || {
provide_context(id.clone());
},
TodoApp,
|| view! { <TodoApp/> },
);
handler(req).await
}
@@ -51,7 +51,7 @@ cfg_if! {
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
let routes = generate_route_list(TodoApp);
let routes = generate_route_list(|| view! { <TodoApp/> }).await;
// build our application with a route
let app = Router::new()
@@ -60,7 +60,7 @@ cfg_if! {
.leptos_routes(
leptos_options.clone(),
routes,
TodoApp,
|| view! { <TodoApp/> },
)
.get("/*", file_and_error_handler)
.with(State(leptos_options));

View File

@@ -240,10 +240,8 @@ pub fn TodoMVC() -> impl IntoView {
<For
each=filtered_todos
key=|todo| todo.id
let:todo
>
<Todo todo/>
</For>
view=move |todo: Todo| view! { <Todo todo /> }
/>
</ul>
</section>
<footer

View File

@@ -22,8 +22,6 @@
openssl
pkg-config
cacert
cargo-make
trunk
(rust-bin.selectLatestNightlyWith( toolchain: toolchain.default.override {
extensions= [ "rust-src" "rust-analyzer" ];
targets = [ "wasm32-unknown-unknown" ];

View File

@@ -19,7 +19,7 @@ serde_json = "1"
parking_lot = "0.12.1"
regex = "1.7.0"
tracing = "0.1.37"
tokio = { version = "1", features = ["rt", "fs"] }
tokio = { version = "1", features = ["rt"] }
[features]
nonce = ["leptos/nonce"]

View File

@@ -6,7 +6,6 @@
//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
//! directory in the Leptos repository.
use actix_http::header::{HeaderName, HeaderValue};
use actix_web::{
body::BoxBody,
dev::{ServiceFactory, ServiceRequest},
@@ -27,7 +26,7 @@ use leptos_meta::*;
use leptos_router::*;
use parking_lot::RwLock;
use regex::Regex;
use std::{fmt::Display, future::Future, pin::Pin, sync::Arc};
use std::{fmt::Display, future::Future, sync::Arc};
#[cfg(debug_assertions)]
use tracing::instrument;
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
@@ -870,24 +869,12 @@ async fn render_app_async_helper(
/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths.
pub fn generate_route_list<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl FnOnce() -> IV + 'static,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions_and_ssg(app_fn, None).0
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths.
pub fn generate_route_list_with_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
) -> (Vec<RouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions_and_ssg(app_fn, None)
generate_route_list_with_exclusions(app_fn, None)
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
@@ -895,28 +882,13 @@ where
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. Adding excluded_routes
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format
pub fn generate_route_list_with_exclusions<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
app_fn: impl FnOnce() -> IV + 'static,
excluded_routes: Option<Vec<String>>,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions_and_ssg(app_fn, excluded_routes).0
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. Adding excluded_routes
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format
pub fn generate_route_list_with_exclusions_and_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
excluded_routes: Option<Vec<String>>,
) -> (Vec<RouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
let (mut routes, static_data_map) =
leptos_router::generate_route_list_inner(app_fn);
let mut routes = leptos_router::generate_route_list_inner(app_fn);
// Actix's Router doesn't follow Leptos's
// Match `*` or `*someword` to replace with replace it with "/{tail.*}
@@ -932,54 +904,30 @@ where
if path.is_empty() {
return RouteListing::new(
"/".to_string(),
listing.path(),
listing.mode(),
listing.methods(),
listing.static_mode(),
);
}
RouteListing::new(
listing.path(),
listing.path(),
listing.mode(),
listing.methods(),
listing.static_mode(),
)
RouteListing::new(listing.path(), listing.mode(), listing.methods())
})
.map(|listing| {
let path = wildcard_re
.replace_all(listing.path(), "{tail:.*}")
.to_string();
let path = capture_re.replace_all(&path, "{$1}").to_string();
RouteListing::new(
path,
listing.path(),
listing.mode(),
listing.methods(),
listing.static_mode(),
)
RouteListing::new(path, listing.mode(), listing.methods())
})
.collect::<Vec<_>>();
(
if routes.is_empty() {
vec![RouteListing::new(
"/",
"",
Default::default(),
[Method::Get],
None,
)]
} else {
// Routes to exclude from auto generation
if let Some(excluded_routes) = excluded_routes {
routes
.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
}
routes
},
static_data_map,
)
if routes.is_empty() {
vec![RouteListing::new("/", Default::default(), [Method::Get])]
} else {
// Routes to exclude from auto generation
if let Some(excluded_routes) = excluded_routes {
routes.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
}
routes
}
}
pub enum DataResponse<T> {
@@ -987,179 +935,6 @@ pub enum DataResponse<T> {
Response(actix_web::dev::Response<BoxBody>),
}
fn handle_static_response<'a, IV>(
path: &'a str,
options: &'a LeptosOptions,
app_fn: &'a (impl Fn() -> IV + Clone + Send + 'static),
additional_context: &'a (impl Fn() + 'static + Clone + Send),
res: StaticResponse,
) -> Pin<Box<dyn Future<Output = HttpResponse<String>> + 'a>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
match res {
StaticResponse::ReturnResponse {
body,
status,
content_type,
} => {
let mut res = HttpResponse::new(match status {
StaticStatusCode::Ok => StatusCode::OK,
StaticStatusCode::NotFound => StatusCode::NOT_FOUND,
StaticStatusCode::InternalServerError => {
StatusCode::INTERNAL_SERVER_ERROR
}
});
if let Some(v) = content_type {
res.headers_mut().insert(
HeaderName::from_static("content-type"),
HeaderValue::from_static(v),
);
}
res.set_body(body)
}
StaticResponse::RenderDynamic => {
handle_static_response(
path,
options,
app_fn,
additional_context,
render_dynamic(
path,
options,
app_fn.clone(),
additional_context.clone(),
)
.await,
)
.await
}
StaticResponse::RenderNotFound => {
handle_static_response(
path,
options,
app_fn,
additional_context,
not_found_page(
tokio::fs::read_to_string(not_found_path(options))
.await,
),
)
.await
}
StaticResponse::WriteFile { body, path } => {
if let Some(path) = path.parent() {
if let Err(e) = std::fs::create_dir_all(path) {
tracing::error!(
"encountered error {} writing directories {}",
e,
path.display()
);
}
}
if let Err(e) = std::fs::write(&path, &body) {
tracing::error!(
"encountered error {} writing file {}",
e,
path.display()
);
}
handle_static_response(
path.to_str().unwrap(),
options,
app_fn,
additional_context,
StaticResponse::ReturnResponse {
body,
status: StaticStatusCode::Ok,
content_type: Some("text/html"),
},
)
.await
}
}
})
}
fn static_route<IV>(
options: LeptosOptions,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + 'static + Clone + Send,
method: Method,
mode: StaticMode,
) -> Route
where
IV: IntoView + 'static,
{
match mode {
StaticMode::Incremental => {
let handler = move |req: HttpRequest| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
handle_static_response(
req.path(),
&options,
&app_fn,
&additional_context,
incremental_static_route(
tokio::fs::read_to_string(static_file_path(
&options,
req.path(),
))
.await,
),
)
.await
}
})
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
StaticMode::Upfront => {
let handler = move |req: HttpRequest| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
handle_static_response(
req.path(),
&options,
&app_fn,
&additional_context,
upfront_static_route(
tokio::fs::read_to_string(static_file_path(
&options,
req.path(),
))
.await,
),
)
.await
}
})
};
match method {
Method::Get => web::get().to(handler),
Method::Post => web::post().to(handler),
Method::Put => web::put().to(handler),
Method::Delete => web::delete().to(handler),
Method::Patch => web::patch().to(handler),
}
}
}
}
/// This trait allows one to pass a list of routes and a render function to Actix's router, letting us avoid
/// having to use wildcards or manually define all routes in multiple places.
pub trait LeptosRoutes {
@@ -1224,19 +999,7 @@ where
let mode = listing.mode();
for method in listing.methods() {
router = if let Some(static_mode) = listing.static_mode() {
router.route(
path,
static_route(
options.clone(),
app_fn.clone(),
additional_context.clone(),
method,
static_mode,
),
)
} else {
router.route(
router = router.route(
path,
match mode {
SsrMode::OutOfOrder => {
@@ -1271,8 +1034,7 @@ where
method,
),
},
)
};
);
}
}
router

View File

@@ -8,7 +8,7 @@ repository = "https://github.com/leptos-rs/leptos"
description = "Axum integrations for the Leptos web framework."
[dependencies]
axum = { version = "0.6", default-features = false, features=["matched-path"] }
axum = { version = "0.6", features = ["macros"] }
futures = "0.3"
http = "0.2.8"
hyper = "0.14.23"
@@ -17,15 +17,12 @@ leptos_meta = { workspace = true, features = ["ssr"] }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_integration_utils = { workspace = true }
serde_json = "1"
tokio = { version = "1", default-features = false }
tokio = { version = "1", features = ["full"] }
parking_lot = "0.12.1"
tokio-util = { version = "0.7.7", features = ["rt"] }
tracing = "0.1.37"
once_cell = "1.17"
cfg-if = "1.0.0"
[features]
nonce = ["leptos/nonce"]
wasm = []
default = ["tokio/full", "axum/macros"]
experimental-islands = ["leptos_integration_utils/experimental-islands"]
experimental-islands = ["leptos_integration_utils/experimental-islands"]

View File

@@ -7,7 +7,7 @@
use axum::{
body::{Body, Bytes, Full, StreamBody},
extract::{FromRef, FromRequestParts, MatchedPath, Path, RawQuery},
extract::{FromRef, FromRequestParts, Path, RawQuery},
http::{
header::{HeaderName, HeaderValue},
HeaderMap, Request, StatusCode,
@@ -36,6 +36,7 @@ use leptos_router::*;
use once_cell::sync::OnceCell;
use parking_lot::RwLock;
use std::{io, pin::Pin, sync::Arc, thread::available_parallelism};
use tokio::task::LocalSet;
use tokio_util::task::LocalPoolHandle;
use tracing::Instrument;
/// A struct to hold the parts of the incoming Request. Since `http::Request` isn't cloneable, we're forced
@@ -158,7 +159,6 @@ pub async fn generate_request_and_parts(
/// use std::net::SocketAddr;
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[cfg(feature = "default")]
/// #[tokio::main]
/// async fn main() {
/// let addr = SocketAddr::from(([127, 0, 0, 1], 8082));
@@ -193,25 +193,6 @@ pub async fn handle_server_fns(
handle_server_fns_inner(fn_name, headers, query, || {}, req).await
}
/// Leptos pool causes wasm to panic and leptos_reactive::spawn::spawn_local causes native
/// to panic so we define a macro to conditionally compile the correct code.
macro_rules! spawn_task {
($block:expr) => {
cfg_if::cfg_if! {
if #[cfg(feature = "wasm")] {
spawn_local($block);
} else if #[cfg(feature = "default")] {
let pool_handle = get_leptos_pool();
pool_handle.spawn_pinned(move || { $block });
} else {
// either the `wasm` feature or `default` feature should be enabled
// this is here mostly to suppress unused import warnings etc.
spawn_local($block);
}
}
};
}
/// An Axum handlers to listens for a request with Leptos server function arguments in the body,
/// run the server function if found, and return the resulting [Response].
///
@@ -252,10 +233,12 @@ async fn handle_server_fns_inner(
.unwrap_or(fn_name);
let (tx, rx) = futures::channel::oneshot::channel();
spawn_task!(async move {
let res =
if let Some(server_fn) = server_fn_by_path(fn_name.as_str()) {
let pool_handle = get_leptos_pool();
pool_handle.spawn_pinned(move || {
async move {
let res = if let Some(server_fn) =
server_fn_by_path(fn_name.as_str())
{
let runtime = create_runtime();
additional_context();
@@ -362,7 +345,8 @@ async fn handle_server_fns_inner(
}
.expect("could not build Response");
_ = tx.send(res);
_ = tx.send(res);
}
});
rx.await.unwrap()
@@ -393,7 +377,6 @@ pub type PinnedHtmlStream =
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[cfg(feature = "default")]
/// #[tokio::main]
/// async fn main() {
/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
@@ -493,7 +476,6 @@ where
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[cfg(feature = "default")]
/// #[tokio::main]
/// async fn main() {
/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
@@ -644,22 +626,17 @@ where
);
move |req| {
let uri = req.uri();
// 1. Process route to match the values in routeListing
let path = req
.extensions()
.get::<MatchedPath>()
.expect("Failed to get Axum router rule")
.as_str();
let path = uri.path();
// 2. Find RouteListing in paths. This should probably be optimized, we probably don't want to
// search for this every time
let listing: &RouteListing =
paths.iter().find(|r| r.path() == path).unwrap_or_else(|| {
panic!(
"Failed to find the route {path} requested by the user. \
This suggests that the routing rules in the Router that \
call this handler needs to be edited!"
)
});
paths.iter().find(|r| r.path() == path).expect(
"Failed to find the route {path} requested by the user. This \
suggests that the routing rules in the Router that call this \
handler needs to be edited!",
);
// 3. Match listing mode against known, and choose function
match listing.mode() {
SsrMode::OutOfOrder => ooo(req),
@@ -717,10 +694,11 @@ where
let default_res_options = ResponseOptions::default();
let res_options2 = default_res_options.clone();
let res_options3 = default_res_options.clone();
let local_pool = get_leptos_pool();
let (tx, rx) = futures::channel::mpsc::channel(8);
let current_span = tracing::Span::current();
spawn_task!(async move {
local_pool.spawn_pinned(move || async move {
let app = {
// Need to get the path and query string of the Request
// For reasons that escape me, if the incoming URI protocol is https, it provides the absolute URI
@@ -778,20 +756,9 @@ async fn generate_response(
if let Some(status) = res_options.status {
*res.status_mut() = status
}
let headers = res.headers_mut();
let mut res_headers = res_options.headers.clone();
headers.extend(res_headers.drain());
res.headers_mut().extend(res_headers.drain());
if !headers.contains_key(http::header::CONTENT_TYPE) {
// Set the Content Type headers on all responses. This makes Firefox show the page source
// without complaining
headers.insert(
http::header::CONTENT_TYPE,
HeaderValue::from_str("text/html; charset=utf-8").unwrap(),
);
}
res
}
#[tracing::instrument(level = "info", fields(error), skip_all)]
@@ -892,8 +859,9 @@ where
let full_path = format!("http://leptos.dev{path}");
let (tx, rx) = futures::channel::mpsc::channel(8);
let local_pool = get_leptos_pool();
let current_span = tracing::Span::current();
spawn_task!(async move {
local_pool.spawn_pinned(|| async move {
let app = {
let full_path = full_path.clone();
let (req, req_parts) = generate_request_and_parts(req).await;
@@ -962,7 +930,6 @@ fn provide_contexts(
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[cfg(feature = "default")]
/// #[tokio::main]
/// async fn main() {
/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
@@ -1118,21 +1085,8 @@ where
if let Some(status) = res_options.status {
*res.status_mut() = status
}
let headers = res.headers_mut();
let mut res_headers = res_options.headers.clone();
headers.extend(res_headers.drain());
// This one doesn't use generate_response(), so we need to do this seperately
if !headers.contains_key(http::header::CONTENT_TYPE) {
// Set the Content Type headers on all responses. This makes Firefox show the page source
// without complaining
headers.insert(
http::header::CONTENT_TYPE,
HeaderValue::from_str("text/html; charset=utf-8")
.unwrap(),
);
}
res.headers_mut().extend(res_headers.drain());
res
}
@@ -1198,42 +1152,38 @@ where
let full_path = format!("http://leptos.dev{path}");
let (tx, rx) = futures::channel::oneshot::channel();
let local_pool = get_leptos_pool();
local_pool.spawn_pinned(move || {
async move {
let app = {
let full_path = full_path.clone();
let (req, req_parts) = generate_request_and_parts(req).await;
move || {
provide_contexts(full_path, req_parts, req.into(), default_res_options);
app_fn().into_view()
}
};
spawn_task!(async move {
let app = {
let full_path = full_path.clone();
let (req, req_parts) =
generate_request_and_parts(req).await;
move || {
provide_contexts(
full_path,
req_parts,
req.into(),
default_res_options,
);
app_fn().into_view()
}
};
let (stream, runtime) =
let (stream, runtime) =
render_to_stream_in_order_with_prefix_undisposed_with_context(
app,
|| "".into(),
add_context,
);
// Extract the value of ResponseOptions from here
let res_options = use_context::<ResponseOptions>().unwrap();
// Extract the value of ResponseOptions from here
let res_options =
use_context::<ResponseOptions>().unwrap();
let html =
build_async_response(stream, &options, runtime).await;
let html = build_async_response(stream, &options, runtime).await;
let new_res_parts = res_options.0.read().clone();
let new_res_parts = res_options.0.read().clone();
let mut writable = res_options2.0.write();
*writable = new_res_parts;
let mut writable = res_options2.0.write();
*writable = new_res_parts;
_ = tx.send(html);
_ = tx.send(html);
}
});
let html = rx.await.expect("to complete HTML rendering");
@@ -1257,26 +1207,13 @@ where
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn generate_route_list<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
pub async fn generate_route_list<IV>(
app_fn: impl FnOnce() -> IV + 'static,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions_and_ssg(app_fn, None).0
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn generate_route_list_with_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
) -> (Vec<RouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions_and_ssg(app_fn, None)
generate_route_list_with_exclusions(app_fn, None).await
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
@@ -1284,30 +1221,35 @@ where
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths. Adding excluded_routes
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Axum path format
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn generate_route_list_with_exclusions<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
pub async fn generate_route_list_with_exclusions<IV>(
app_fn: impl FnOnce() -> IV + 'static,
excluded_routes: Option<Vec<String>>,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions_and_ssg(app_fn, excluded_routes).0
}
#[derive(Default, Clone, Debug)]
pub struct Routes(pub Arc<RwLock<Vec<RouteListing>>>);
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths. Adding excluded_routes
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Axum path format
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn generate_route_list_with_exclusions_and_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
excluded_routes: Option<Vec<String>>,
) -> (Vec<RouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
let (routes, static_data_map) =
leptos_router::generate_route_list_inner(app_fn);
let routes = Routes::default();
let routes_inner = routes.clone();
let local = LocalSet::new();
// Run the local task set.
local
.run_until(async move {
tokio::task::spawn_local(async move {
let routes = leptos_router::generate_route_list_inner(app_fn);
let mut writable = routes_inner.0.write();
*writable = routes;
})
.await
.unwrap();
})
.await;
let routes = routes.0.read().to_owned();
// Axum's Router defines Root routes as "/" not ""
let mut routes = routes
.into_iter()
@@ -1316,10 +1258,8 @@ where
if path.is_empty() {
RouteListing::new(
"/".to_string(),
listing.path(),
listing.mode(),
listing.methods(),
listing.static_mode(),
)
} else {
listing
@@ -1327,25 +1267,19 @@ where
})
.collect::<Vec<_>>();
(
if routes.is_empty() {
vec![RouteListing::new(
"/",
"",
Default::default(),
[leptos_router::Method::Get],
None,
)]
} else {
// Routes to exclude from auto generation
if let Some(excluded_routes) = excluded_routes {
routes
.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
}
routes
},
static_data_map,
)
if routes.is_empty() {
vec![RouteListing::new(
"/",
Default::default(),
[leptos_router::Method::Get],
)]
} else {
// Routes to exclude from auto generation
if let Some(excluded_routes) = excluded_routes {
routes.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
}
routes
}
}
/// This trait allows one to pass a list of routes and a render function to Axum's router, letting us avoid
@@ -1383,210 +1317,6 @@ where
T: 'static;
}
#[cfg(feature = "default")]
fn handle_static_response<IV>(
path: String,
options: LeptosOptions,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + Clone + Send + 'static,
res: StaticResponse,
) -> Pin<Box<dyn Future<Output = Response<String>> + 'static>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
match res {
StaticResponse::ReturnResponse {
body,
status,
content_type,
} => {
let mut res = Response::new(body);
if let Some(v) = content_type {
res.headers_mut().insert(
HeaderName::from_static("content-type"),
HeaderValue::from_static(v),
);
}
*res.status_mut() = match status {
StaticStatusCode::Ok => StatusCode::OK,
StaticStatusCode::NotFound => StatusCode::NOT_FOUND,
StaticStatusCode::InternalServerError => {
StatusCode::INTERNAL_SERVER_ERROR
}
};
res
}
StaticResponse::RenderDynamic => {
let res = render_dynamic(
&path,
&options,
app_fn.clone(),
additional_context.clone(),
)
.await;
handle_static_response(
path,
options,
app_fn,
additional_context,
res,
)
.await
}
StaticResponse::RenderNotFound => {
let res = not_found_page(
tokio::fs::read_to_string(not_found_path(&options)).await,
);
handle_static_response(
path,
options,
app_fn,
additional_context,
res,
)
.await
}
StaticResponse::WriteFile { body, path } => {
if let Some(path) = path.parent() {
if let Err(e) = std::fs::create_dir_all(path) {
tracing::error!(
"encountered error {} writing directories {}",
e,
path.display()
);
}
}
if let Err(e) = std::fs::write(&path, &body) {
tracing::error!(
"encountered error {} writing file {}",
e,
path.display()
);
}
handle_static_response(
path.to_str().unwrap().to_string(),
options,
app_fn,
additional_context,
StaticResponse::ReturnResponse {
body,
status: StaticStatusCode::Ok,
content_type: Some("text/html"),
},
)
.await
}
}
})
}
#[cfg(feature = "default")]
fn static_route<IV, S>(
router: axum::Router<S>,
path: &str,
options: LeptosOptions,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + Clone + Send + 'static,
method: leptos_router::Method,
mode: StaticMode,
) -> axum::Router<S>
where
IV: IntoView + 'static,
S: Clone + Send + Sync + 'static,
{
match mode {
StaticMode::Incremental => {
let handler = move |req: Request<Body>| {
Box::pin({
let path = req.uri().path().to_string();
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
let (tx, rx) = futures::channel::oneshot::channel();
let local_pool = get_leptos_pool();
local_pool.spawn_pinned(move || async move {
let res = incremental_static_route(
tokio::fs::read_to_string(static_file_path(
&options, &path,
))
.await,
);
let res = handle_static_response(
path.clone(),
options,
app_fn,
additional_context,
res,
)
.await;
let _ = tx.send(res);
});
rx.await.expect("to complete HTML rendering")
}
})
};
router.route(
path,
match method {
leptos_router::Method::Get => get(handler),
leptos_router::Method::Post => post(handler),
leptos_router::Method::Put => put(handler),
leptos_router::Method::Delete => delete(handler),
leptos_router::Method::Patch => patch(handler),
},
)
}
StaticMode::Upfront => {
let handler = move |req: Request<Body>| {
Box::pin({
let path = req.uri().path().to_string();
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
let (tx, rx) = futures::channel::oneshot::channel();
let local_pool = get_leptos_pool();
local_pool.spawn_pinned(move || async move {
let res = upfront_static_route(
tokio::fs::read_to_string(static_file_path(
&options, &path,
))
.await,
);
let res = handle_static_response(
path.clone(),
options,
app_fn,
additional_context,
res,
)
.await;
let _ = tx.send(res);
});
rx.await.expect("to complete HTML rendering")
}
})
};
router.route(
path,
match method {
leptos_router::Method::Get => get(handler),
leptos_router::Method::Post => post(handler),
leptos_router::Method::Put => put(handler),
leptos_router::Method::Delete => delete(handler),
leptos_router::Method::Patch => patch(handler),
},
)
}
}
}
/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
/// to those paths to Leptos's renderer.
impl<S> LeptosRoutes<S> for axum::Router<S>
@@ -1623,29 +1353,7 @@ where
let path = listing.path();
for method in listing.methods() {
router = if let Some(static_mode) = listing.static_mode() {
#[cfg(feature = "default")]
{
static_route(
router,
path,
LeptosOptions::from_ref(options),
app_fn.clone(),
additional_context.clone(),
method,
static_mode,
)
}
#[cfg(not(feature = "default"))]
{
_ = static_mode;
panic!(
"Static site generation is not currently \
supported on WASM32 server targets."
)
}
} else {
router.route(
router = router.route(
path,
match listing.mode() {
SsrMode::OutOfOrder => {
@@ -1706,8 +1414,7 @@ where
}
}
},
)
};
);
}
}
router

View File

@@ -23,7 +23,7 @@ use leptos_meta::{generate_head_metadata_separated, MetaContext};
use leptos_router::*;
use parking_lot::RwLock;
use std::{pin::Pin, sync::Arc};
use tokio::task::spawn_blocking;
use tokio::task::{spawn_blocking, LocalSet};
use viz::{
headers::{HeaderMap, HeaderName, HeaderValue},
Body, Bytes, Error, Handler, IntoResponse, Request, RequestExt, Response,
@@ -988,51 +988,47 @@ where
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Viz's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Viz compatible paths.
pub fn generate_route_list<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
pub async fn generate_route_list<IV>(
app_fn: impl FnOnce() -> IV + 'static,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions_and_ssg(app_fn, None).0
generate_route_list_with_exclusions(app_fn, None).await
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Viz's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Viz compatible paths.
pub fn generate_route_list_with_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
) -> (Vec<RouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions_and_ssg(app_fn, None)
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Viz's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Viz compatible paths.
pub fn generate_route_list_with_exclusions<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
pub async fn generate_route_list_with_exclusions<IV>(
app_fn: impl FnOnce() -> IV + 'static,
excluded_routes: Option<Vec<String>>,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions_and_ssg(app_fn, excluded_routes).0
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Viz's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Viz compatible paths.
pub fn generate_route_list_with_exclusions_and_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
excluded_routes: Option<Vec<String>>,
) -> (Vec<RouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
let (routes, static_data_map) =
leptos_router::generate_route_list_inner(app_fn);
#[derive(Default, Clone, Debug)]
pub struct Routes(pub Arc<RwLock<Vec<RouteListing>>>);
let routes = Routes::default();
let routes_inner = routes.clone();
let local = LocalSet::new();
// Run the local task set.
local
.run_until(async move {
tokio::task::spawn_local(async move {
let routes = leptos_router::generate_route_list_inner(app_fn);
let mut writable = routes_inner.0.write();
*writable = routes;
})
.await
.unwrap();
})
.await;
let routes = routes.0.read().to_owned();
// Viz's Router defines Root routes as "/" not ""
let mut routes = routes
.into_iter()
@@ -1041,10 +1037,8 @@ where
if path.is_empty() {
RouteListing::new(
"/".to_string(),
listing.path(),
listing.mode(),
listing.methods(),
listing.static_mode(),
)
} else {
listing
@@ -1052,260 +1046,17 @@ where
})
.collect::<Vec<_>>();
(
if routes.is_empty() {
vec![RouteListing::new(
"/",
"",
Default::default(),
[leptos_router::Method::Get],
None,
)]
} else {
if let Some(excluded_routes) = excluded_routes {
routes
.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
}
routes
},
static_data_map,
)
}
fn handle_static_response<IV>(
path: String,
options: LeptosOptions,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + Clone + Send + Sync + 'static,
res: StaticResponse,
) -> Pin<Box<dyn Future<Output = Result<Response>> + 'static>>
where
IV: IntoView + 'static,
{
Box::pin(async move {
match res {
StaticResponse::ReturnResponse {
body,
status,
content_type,
} => {
let mut res = Response::html(body);
if let Some(v) = content_type {
res.headers_mut().insert(
HeaderName::from_static("content-type"),
HeaderValue::from_static(v),
);
}
*res.status_mut() = match status {
StaticStatusCode::Ok => StatusCode::OK,
StaticStatusCode::NotFound => StatusCode::NOT_FOUND,
StaticStatusCode::InternalServerError => {
StatusCode::INTERNAL_SERVER_ERROR
}
};
Ok(res)
}
StaticResponse::RenderDynamic => {
let res = render_dynamic(
&path,
&options,
app_fn.clone(),
additional_context.clone(),
)
.await;
handle_static_response(
path,
options,
app_fn,
additional_context,
res,
)
.await
}
StaticResponse::RenderNotFound => {
let res = not_found_page(
tokio::fs::read_to_string(not_found_path(&options)).await,
);
handle_static_response(
path,
options,
app_fn,
additional_context,
res,
)
.await
}
StaticResponse::WriteFile { body, path } => {
if let Some(path) = path.parent() {
if let Err(e) = std::fs::create_dir_all(path) {
tracing::error!(
"encountered error {} writing directories {}",
e,
path.display()
);
}
}
if let Err(e) = std::fs::write(&path, &body) {
tracing::error!(
"encountered error {} writing file {}",
e,
path.display()
);
}
handle_static_response(
path.to_str().unwrap().to_string(),
options,
app_fn,
additional_context,
StaticResponse::ReturnResponse {
body,
status: StaticStatusCode::Ok,
content_type: Some("text/html"),
},
)
.await
}
}
})
}
fn static_route<IV>(
router: Router,
path: &str,
options: LeptosOptions,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
additional_context: impl Fn() + Clone + Send + Sync + 'static,
method: leptos_router::Method,
mode: StaticMode,
) -> Router
where
IV: IntoView + 'static,
{
match mode {
StaticMode::Incremental => {
let handler = move |req: Request| {
Box::pin({
let path = req.path().to_string();
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
let (tx, rx) = futures::channel::oneshot::channel();
spawn_blocking(move || {
let path = path.clone();
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on({
let path = path.clone();
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context =
additional_context.clone();
async move {
tokio::task::LocalSet::new().run_until(async {
let res = incremental_static_route(
tokio::fs::read_to_string(
static_file_path(
&options,
&path,
),
)
.await,
);
let res = handle_static_response(
path.clone(),
options,
app_fn,
additional_context,
res,
)
.await;
let _ = tx.send(res);
}).await;
}
})
});
rx.await.expect("to complete HTML rendering")
}
})
};
match method {
leptos_router::Method::Get => router.get(path, handler),
leptos_router::Method::Post => router.post(path, handler),
leptos_router::Method::Put => router.put(path, handler),
leptos_router::Method::Delete => router.delete(path, handler),
leptos_router::Method::Patch => router.patch(path, handler),
}
}
StaticMode::Upfront => {
let handler = move |req: Request| {
Box::pin({
let path = req.path().to_string();
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
async move {
let (tx, rx) = futures::channel::oneshot::channel();
spawn_blocking(move || {
let path = path.clone();
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on({
let path = path.clone();
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context =
additional_context.clone();
async move {
tokio::task::LocalSet::new()
.run_until(async {
let res = upfront_static_route(
tokio::fs::read_to_string(
static_file_path(
&options, &path,
),
)
.await,
);
let res =
handle_static_response(
path.clone(),
options,
app_fn,
additional_context,
res,
)
.await;
let _ = tx.send(res);
})
.await;
}
})
});
rx.await.expect("to complete HTML rendering")
}
})
};
match method {
leptos_router::Method::Get => router.get(path, handler),
leptos_router::Method::Post => router.post(path, handler),
leptos_router::Method::Put => router.put(path, handler),
leptos_router::Method::Delete => router.delete(path, handler),
leptos_router::Method::Patch => router.patch(path, handler),
}
if routes.is_empty() {
vec![RouteListing::new(
"/",
Default::default(),
[leptos_router::Method::Get],
)]
} else {
if let Some(excluded_routes) = excluded_routes {
routes.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
}
routes
}
}
@@ -1369,117 +1120,63 @@ impl LeptosRoutes for Router {
let path = listing.path();
let mode = listing.mode();
listing.methods().fold(router, |router, method| {
if let Some(static_mode) = listing.static_mode() {
static_route(
router,
path,
listing.methods().fold(router, |router, method| match mode {
SsrMode::OutOfOrder => {
let s = render_app_to_stream_with_context(
options.clone(),
app_fn.clone(),
additional_context.clone(),
method,
static_mode,
)
} else {
match mode {
SsrMode::OutOfOrder => {
let s = render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => {
router.get(path, s)
}
leptos_router::Method::Post => {
router.post(path, s)
}
leptos_router::Method::Put => {
router.put(path, s)
}
leptos_router::Method::Delete => {
router.delete(path, s)
}
leptos_router::Method::Patch => {
router.patch(path, s)
}
}
}
SsrMode::PartiallyBlocked => {
let s =
app_fn.clone(),
);
match method {
leptos_router::Method::Get => router.get(path, s),
leptos_router::Method::Post => router.post(path, s),
leptos_router::Method::Put => router.put(path, s),
leptos_router::Method::Delete => router.delete(path, s),
leptos_router::Method::Patch => router.patch(path, s),
}
}
SsrMode::PartiallyBlocked => {
let s =
render_app_to_stream_with_context_and_replace_blocks(
options.clone(),
additional_context.clone(),
app_fn.clone(),
true,
);
match method {
leptos_router::Method::Get => {
router.get(path, s)
}
leptos_router::Method::Post => {
router.post(path, s)
}
leptos_router::Method::Put => {
router.put(path, s)
}
leptos_router::Method::Delete => {
router.delete(path, s)
}
leptos_router::Method::Patch => {
router.patch(path, s)
}
}
}
SsrMode::InOrder => {
let s = render_app_to_stream_in_order_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => {
router.get(path, s)
}
leptos_router::Method::Post => {
router.post(path, s)
}
leptos_router::Method::Put => {
router.put(path, s)
}
leptos_router::Method::Delete => {
router.delete(path, s)
}
leptos_router::Method::Patch => {
router.patch(path, s)
}
}
}
SsrMode::Async => {
let s = render_app_async_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => {
router.get(path, s)
}
leptos_router::Method::Post => {
router.post(path, s)
}
leptos_router::Method::Put => {
router.put(path, s)
}
leptos_router::Method::Delete => {
router.delete(path, s)
}
leptos_router::Method::Patch => {
router.patch(path, s)
}
}
}
match method {
leptos_router::Method::Get => router.get(path, s),
leptos_router::Method::Post => router.post(path, s),
leptos_router::Method::Put => router.put(path, s),
leptos_router::Method::Delete => router.delete(path, s),
leptos_router::Method::Patch => router.patch(path, s),
}
}
SsrMode::InOrder => {
let s = render_app_to_stream_in_order_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => router.get(path, s),
leptos_router::Method::Post => router.post(path, s),
leptos_router::Method::Put => router.put(path, s),
leptos_router::Method::Delete => router.delete(path, s),
leptos_router::Method::Patch => router.patch(path, s),
}
}
SsrMode::Async => {
let s = render_app_async_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
);
match method {
leptos_router::Method::Get => router.get(path, s),
leptos_router::Method::Post => router.post(path, s),
leptos_router::Method::Put => router.put(path, s),
leptos_router::Method::Delete => router.delete(path, s),
leptos_router::Method::Patch => router.patch(path, s),
}
}
})

View File

@@ -11,7 +11,7 @@ use leptos_reactive::{
/// [`create_resource`] that only loads once (i.e., with a source signal `|| ()`) with
/// a [`Suspense`] with no `fallback`.
///
/// Adding `let:{variable name}` to the props makes the data available in the children
/// Adding `bind:{variable name}` to the props makes the data available in the children
/// that variable name, when resolved.
/// ```
/// # use leptos_reactive::*;
@@ -27,7 +27,7 @@ use leptos_reactive::{
/// view! {
/// <Await
/// future=|| fetch_monkeys(3)
/// let:data
/// bind:data
/// >
/// <p>{*data} " little monkeys, jumping on the bed."</p>
/// </Await>
@@ -49,7 +49,7 @@ pub fn Await<T, Fut, FF, VF, V>(
///
/// ## Syntax
/// This can be passed in the `view` children of the `<Await/>` by using the
/// `let:` syntax to specify the name for the data variable.
/// `bind:` syntax to specify the name for the data variable.
///
/// ```rust
/// # use leptos::*;
@@ -61,7 +61,7 @@ pub fn Await<T, Fut, FF, VF, V>(
/// view! {
/// <Await
/// future=|| fetch_monkeys(3)
/// let:data
/// bind:data
/// >
/// <p>{*data} " little monkeys, jumping on the bed."</p>
/// </Await>

View File

@@ -28,19 +28,7 @@ use std::hash::Hash;
/// // a unique key for each item
/// key=|counter| counter.id
/// // renders each item to a view
/// children=move |counter: Counter| {
/// view! {
/// <button>"Value: " {move || counter.count.get()}</button>
/// }
/// }
/// />
/// <For
/// // a function that returns the items we're iterating over; a signal is fine
/// each=move || counters.get()
/// // a unique key for each item
/// key=|counter| counter.id
/// // renders each item to a view
/// children=move |counter: Counter| {
/// view=move |counter: Counter| {
/// view! {
/// <button>"Value: " {move || counter.count.get()}</button>
/// }
@@ -60,45 +48,8 @@ pub fn For<IF, I, T, EF, N, KF, K>(
each: IF,
/// A key function that will be applied to each item.
key: KF,
/// A function that takes the item, and returns the view that will be displayed for each item.
///
/// ## Syntax
/// This can be passed directly in the `view` children of the `<For/>` by using the
/// `let:` syntax to specify the name for the data variable passed in the argument.
///
/// ```rust
/// # use leptos::*;
/// # if false {
/// let (data, set_data) = create_signal(vec![0, 1, 2]);
/// view! {
/// <For
/// each=move || data.get()
/// key=|n| *n
/// // stores the item in each row in a variable named `data`
/// let:data
/// >
/// <p>{data}</p>
/// </For>
/// }
/// # ;
/// # }
/// ```
/// is the same as
/// ```rust
/// # use leptos::*;
/// # if false {
/// let (data, set_data) = create_signal(vec![0, 1, 2]);
/// view! {
/// <For
/// each=move || data.get()
/// key=|n| *n
/// children=|data| view! { <p>{data}</p> }
/// />
/// }
/// # ;
/// # }
/// ```
children: EF,
/// The view that will be displayed for each item.
view: EF,
) -> impl IntoView
where
IF: Fn() -> I + 'static,
@@ -109,5 +60,5 @@ where
K: Eq + Hash + 'static,
T: 'static,
{
leptos_dom::Each::new(each, key, children).into_view()
leptos_dom::Each::new(each, key, view).into_view()
}

View File

@@ -178,7 +178,7 @@ pub mod error {
}
#[cfg(all(target_arch = "wasm32", feature = "template_macro"))]
pub use leptos_macro::template;
#[cfg(not(all(target_arch = "wasm32", feature = "template_macro")))]
#[cfg(not(any(target_arch = "wasm32", feature = "template_macro")))]
pub use leptos_macro::view as template;
pub use leptos_macro::{component, island, server, slot, view, Params};
pub use leptos_reactive::*;
@@ -230,12 +230,8 @@ pub use wasm_bindgen; // used in islands
feature = "template_macro"
))]
pub use web_sys; // used in islands
mod children;
mod view_fn;
pub use children::*;
pub use view_fn::*;
extern crate self as leptos;
/// A type for taking anything that implements [`IntoAttribute`].

View File

@@ -1,4 +1,4 @@
use leptos::{component, ChildrenFn, ViewFn};
use leptos::{component, ChildrenFn};
use leptos_dom::IntoView;
use leptos_reactive::{create_memo, signal_prelude::*};
@@ -6,7 +6,10 @@ use leptos_reactive::{create_memo, signal_prelude::*};
/// and show the fallback when it is `false`, without rerendering every time
/// the condition changes.
///
/// The fallback prop is optional and defaults to rendering nothing.
/// *Note*: Because of the nature of generic arguments, its not really possible
/// to make the `fallback` optional. If you want an empty fallback state—in other
/// words, if you want to show the children if `when` is true and noting otherwise—use
/// `fallback=|_| ()` (i.e., a fallback function that returns the unit type `()`).
///
/// ```rust
/// # use leptos_reactive::*;
@@ -31,22 +34,25 @@ use leptos_reactive::{create_memo, signal_prelude::*};
tracing::instrument(level = "info", skip_all)
)]
#[component]
pub fn Show<W>(
/// The children will be shown whenever the condition in the `when` closure returns `true`.
pub fn Show<F, W, IV>(
/// The scope the component is running in
/// The components Show wraps
children: ChildrenFn,
/// A closure that returns a bool that determines whether this thing runs
when: W,
/// A closure that returns what gets rendered if the when statement is false. By default this is the empty view.
#[prop(optional, into)]
fallback: ViewFn,
/// A closure that returns what gets rendered if the when statement is false
fallback: F,
) -> impl IntoView
where
W: Fn() -> bool + 'static,
F: Fn() -> IV + 'static,
IV: IntoView,
{
let memoized_when = create_memo(move |_| when());
move || match memoized_when.get() {
true => children().into_view(),
false => fallback.run(),
false => fallback().into_view(),
}
}

View File

@@ -1,8 +1,5 @@
use leptos::ViewFn;
use leptos_dom::{DynChild, HydrationCtx, IntoView};
use leptos_macro::component;
#[cfg(feature = "hydrate")]
use leptos_reactive::SharedContext;
#[cfg(any(feature = "csr", feature = "hydrate"))]
use leptos_reactive::SignalGet;
use leptos_reactive::{
@@ -60,14 +57,15 @@ use std::rc::Rc;
tracing::instrument(level = "info", skip_all)
)]
#[component]
pub fn Suspense<V>(
/// Returns a fallback UI that will be shown while `async` [`Resource`](leptos_reactive::Resource)s are still loading. By default this is the empty view.
#[prop(optional, into)]
fallback: ViewFn,
pub fn Suspense<F, E, V>(
/// Returns a fallback UI that will be shown while `async` [`Resource`](leptos_reactive::Resource)s are still loading.
fallback: F,
/// Children will be displayed once all `async` [`Resource`](leptos_reactive::Resource)s have resolved.
children: Rc<dyn Fn() -> V>,
) -> impl IntoView
where
F: Fn() -> E + 'static,
E: IntoView,
V: IntoView + 'static,
{
let orig_children = children;
@@ -77,11 +75,8 @@ where
let owner =
Owner::current().expect("<Suspense/> created with no reactive owner");
let current_id = HydrationCtx::next_component();
// provide this SuspenseContext to any resources below it
// run in a memo so the children are children of this parent
#[cfg(not(feature = "hydrate"))]
let children = create_memo({
let orig_children = Rc::clone(&orig_children);
move |_| {
@@ -89,32 +84,17 @@ where
orig_children().into_view()
}
});
#[cfg(feature = "hydrate")]
let children = create_memo({
let orig_children = Rc::clone(&orig_children);
move |_| {
provide_context(context);
if SharedContext::fragment_has_local_resources(
&current_id.to_string(),
) {
HydrationCtx::with_hydration_off({
let orig_children = Rc::clone(&orig_children);
move || orig_children().into_view()
})
} else {
orig_children().into_view()
}
}
});
// likewise for the fallback
let fallback = create_memo({
move |_| {
provide_context(context);
fallback.run()
fallback().into_view()
}
});
let current_id = HydrationCtx::next_component();
#[cfg(any(feature = "csr", feature = "hydrate"))]
let ready = context.ready();
@@ -146,11 +126,6 @@ where
DynChild::new(move || children_rendered.clone())
.into_view()
})
} else if context.has_any_local() {
SharedContext::register_local_fragment(
current_id.to_string(),
);
fallback.get_untracked()
}
// show the fallback, but also prepare to stream HTML
else {

View File

@@ -1,9 +1,8 @@
use leptos::ViewFn;
use leptos_dom::{Fragment, HydrationCtx, IntoView, View};
use leptos_macro::component;
use leptos_reactive::{
create_isomorphic_effect, create_rw_signal, use_context, RwSignal,
SignalGet, SignalGetUntracked, SignalSet, SignalSetter, SuspenseContext,
SignalGet, SignalSet, SignalSetter, SuspenseContext,
};
use std::{
cell::{Cell, RefCell},
@@ -40,7 +39,7 @@ use std::{
/// <div>
/// <Transition
/// fallback=move || view! { <p>"Loading..."</p>}
/// set_pending
/// set_pending=set_pending.into()
/// >
/// {move || {
/// cats.read().map(|data| match data {
@@ -67,18 +66,21 @@ use std::{
tracing::instrument(level = "info", skip_all)
)]
#[component(transparent)]
pub fn Transition(
/// Will be displayed while resources are pending. By default this is the empty view.
#[prop(optional, into)]
fallback: ViewFn,
pub fn Transition<F, E>(
/// Will be displayed while resources are pending.
fallback: F,
/// A function that will be called when the component transitions into or out of
/// the `pending` state, with its argument indicating whether it is pending (`true`)
/// or not pending (`false`).
#[prop(optional, into)]
#[prop(optional)]
set_pending: Option<SignalSetter<bool>>,
/// Will be displayed once all resources have resolved.
children: Box<dyn Fn() -> Fragment>,
) -> impl IntoView {
) -> impl IntoView
where
F: Fn() -> E + 'static,
E: IntoView,
{
let prev_children = Rc::new(RefCell::new(None::<View>));
let first_run = create_rw_signal(true);
@@ -103,12 +105,12 @@ pub fn Transition(
if let Some(prev_children) = &*prev_child.borrow() {
if is_first_run || was_first_run {
fallback.run()
fallback().into_view()
} else {
prev_children.clone()
}
} else {
fallback.run()
fallback().into_view()
}
}
})
@@ -123,7 +125,7 @@ pub fn Transition(
let suspense_context = held_suspense_context.borrow().unwrap();
if cfg!(feature = "hydrate")
|| !first_run.get_untracked()
|| !first_run.get()
|| (cfg!(feature = "csr") && first_run.get())
{
*prev_children.borrow_mut() = Some(frag.clone());
@@ -159,7 +161,7 @@ fn is_first_run(
false
} else {
match (
first_run.get_untracked(),
first_run.get(),
cfg!(feature = "hydrate"),
suspense_context.has_local_only(),
) {

View File

@@ -1,30 +0,0 @@
use leptos_dom::{IntoView, View};
use std::rc::Rc;
/// New-type wrapper for the a function that returns a view with `From` and `Default` traits implemented
/// to enable optional props in for example `<Show>` and `<Suspense>`.
#[derive(Clone)]
pub struct ViewFn(Rc<dyn Fn() -> View>);
impl Default for ViewFn {
fn default() -> Self {
Self(Rc::new(|| ().into_view()))
}
}
impl<F, IV> From<F> for ViewFn
where
F: Fn() -> IV + 'static,
IV: IntoView,
{
fn from(value: F) -> Self {
Self(Rc::new(move || value().into_view()))
}
}
impl ViewFn {
/// Execute the wrapped function
pub fn run(&self) -> View {
(self.0)()
}
}

View File

@@ -9,7 +9,7 @@ description = "Configuration for the Leptos web framework."
readme = "../README.md"
[dependencies]
config = { version = "0.13.3", default-features = false, features = ["toml"] }
config = "0.13.3"
regex = "1.7.0"
serde = { version = "1.0.151", features = ["derive"] }
thiserror = "1.0.38"

View File

@@ -66,10 +66,6 @@ pub struct LeptosOptions {
#[builder(default)]
#[serde(default)]
pub reload_ws_protocol: ReloadWSProtocol,
/// The path of a custom 404 Not Found page to display when statically serving content, defaults to `site_root/404.html`
#[builder(default = default_not_found_path())]
#[serde(default = "default_not_found_path")]
pub not_found_path: String,
}
impl LeptosOptions {
@@ -107,7 +103,6 @@ impl LeptosOptions {
reload_ws_protocol: ws_from_str(
env_w_default("LEPTOS_RELOAD_WS_PROTOCOL", "ws")?.as_str(),
)?,
not_found_path: env_w_default("LEPTOS_NOT_FOUND_PATH", "/404")?,
})
}
}
@@ -131,11 +126,6 @@ fn default_site_addr() -> SocketAddr {
fn default_reload_port() -> u32 {
3001
}
fn default_not_found_path() -> String {
"/404".to_string()
}
fn env_wo_default(key: &str) -> Result<Option<String>, LeptosConfigError> {
match std::env::var(key) {
Ok(val) => Ok(Some(val)),

412
leptos_dom/src/callback.rs Normal file
View File

@@ -0,0 +1,412 @@
//! Callbacks define a standard way to store functions and closures,
//! in particular for component properties.
//!
//! # How to use them
//! You can always create a callback from a closure, but the prefered way is to use `prop(into)`
//! when you define your component:
//! ```
//! # use leptos::*;
//! # use leptos::leptos_dom::{Callback, Callable};
//! #[component]
//! fn MyComponent(
//! #[prop(into)] render_number: Callback<i32, String>,
//! ) -> impl IntoView {
//! view! {
//! <div>
//! {render_number.call(42)}
//! </div>
//! }
//! }
//! // now you can use it from a closure directly:
//! fn test() -> impl IntoView {
//! view! {
//! <MyComponent render_number = |x: i32| x.to_string()/>
//! }
//! }
//! ```
//!
//! *Notes*:
//! - in this example, you should use a generic type that implements `Fn(i32) -> String`.
//! Callbacks are more usefull when you want optional generic props.
//! - All callbacks implement the `Callable` trait. You have to write `my_callback.call(input)`
//!
//!
//! # Types
//! This modules defines:
//! - [Callback], the most basic callback type
//! - [SyncCallback] for scenarios when you need `Send` and `Sync`
//! - [HtmlCallback] for a function that returns a [HtmlElement]
//! - [ViewCallback] for a function that returns some kind of [view][IntoView]
//!
//! # Copying vs cloning
//! All callbacks type defined in this module are [Clone] but not [Copy].
//! To solve this issue, use [StoredValue]; see [StoredCallback] for more
//! ```
//! # use leptos::*;
//! # use leptos::leptos_dom::{Callback, Callable};
//! fn test() -> impl IntoView {
//! let callback: Callback<i32, String> =
//! Callback::new(|x: i32| x.to_string());
//! let stored_callback = store_value(callback);
//!
//! view! {
//! <div>
//! // `stored_callback` can be moved multiple times
//! {move || stored_callback.call(1)}
//! {move || stored_callback.call(42)}
//! </div>
//! }
//! }
//! ```
//!
//! Note that for each callback type `T`, `StoredValue<T>` implements `Call`, so you can call them
//! without even thinking about it.
use crate::{AnyElement, ElementDescriptor, HtmlElement, IntoView, View};
use leptos_reactive::StoredValue;
use std::{fmt, rc::Rc, sync::Arc};
/// A wrapper trait for calling callbacks.
pub trait Callable<In, Out = ()> {
/// calls the callback with the specified argument.
fn call(&self, input: In) -> Out;
}
/// The most basic leptos callback type.
/// For how to use callbacks, see [here][crate::callback]
///
/// # Example
/// ```
/// # use leptos::*;
/// # use leptos::leptos_dom::{Callable, Callback};
/// #[component]
/// fn MyComponent(
/// #[prop(into)] render_number: Callback<i32, String>,
/// ) -> impl IntoView {
/// view! {
/// <div>
/// {render_number.call(42)}
/// </div>
/// }
/// }
///
/// fn test() -> impl IntoView {
/// view! {
/// <MyComponent render_number=move |x: i32| x.to_string()/>
/// }
/// }
/// ```
///
/// # Cloning
/// See [StoredCallback]
pub struct Callback<In, Out = ()>(Rc<dyn Fn(In) -> Out>);
impl<In> fmt::Debug for Callback<In> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
fmt.write_str("Callback")
}
}
impl<In, Out> Clone for Callback<In, Out> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<In, Out> Callback<In, Out> {
/// creates a new callback from the function or closure
pub fn new<F>(f: F) -> Callback<In, Out>
where
F: Fn(In) -> Out + 'static,
{
Self(Rc::new(f))
}
}
impl<In, Out> Callable<In, Out> for Callback<In, Out> {
fn call(&self, input: In) -> Out {
(self.0)(input)
}
}
impl<F, In, Out> From<F> for Callback<In, Out>
where
F: Fn(In) -> Out + 'static,
{
fn from(f: F) -> Callback<In, Out> {
Callback::new(f)
}
}
/// A callback type that implements `Copy`.
/// `StoredCallback<In,Out>` is an alias for `StoredValue<Callback<In, Out>>`.
///
/// # Example
/// ```
/// # use leptos::*;
/// # use leptos::leptos_dom::{Callback, StoredCallback, Callable};
/// fn test() -> impl IntoView {
/// let callback: Callback<i32, String> =
/// Callback::new(|x: i32| x.to_string());
/// let stored_callback: StoredCallback<i32, String> =
/// store_value(callback);
/// view! {
/// <div>
/// {move || stored_callback.call(1)}
/// {move || stored_callback.call(42)}
/// </div>
/// }
/// }
/// ```
///
/// Note that in this example, you can replace `Callback` by `SyncCallback` or `ViewCallback`, and
/// it will work in the same way.
///
///
/// Note that a prop should never be a [StoredCallback]:
/// you have to call [store_value][leptos_reactive::store_value] inside your component code.
pub type StoredCallback<In, Out> = StoredValue<Callback<In, Out>>;
impl<F, In, Out> Callable<In, Out> for StoredValue<F>
where
F: Callable<In, Out>,
{
fn call(&self, input: In) -> Out {
self.with_value(|cb| cb.call(input))
}
}
/// a callback type that is `Send` and `Sync` if the input type is
pub struct SyncCallback<In, Out = ()>(Arc<dyn Fn(In) -> Out>);
impl<In> fmt::Debug for SyncCallback<In> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
fmt.write_str("SyncCallback")
}
}
impl<In, Out> Clone for SyncCallback<In, Out> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<In: 'static, Out: 'static> SyncCallback<In, Out> {
/// creates a new callback from the function or closure
pub fn new<F>(fun: F) -> Self
where
F: Fn(In) -> Out + 'static,
{
Self(Arc::new(fun))
}
}
/// A special callback type that returns any Html element.
/// You can use it exactly the same way as a classic callback.
///
/// For how to use callbacks, see [here][crate::callback]
///
/// # Example
///
/// ```
/// # use leptos::*;
/// # use leptos::leptos_dom::{Callable, HtmlCallback};
/// #[component]
/// fn MyComponent(
/// #[prop(into)] render_number: HtmlCallback<i32>,
/// ) -> impl IntoView {
/// view! {
/// <div>
/// {render_number.call(42)}
/// </div>
/// }
/// }
/// fn test() -> impl IntoView {
/// view! {
/// <MyComponent render_number=move |x: i32| view!{<span>{x}</span>}/>
/// }
/// }
/// ```
///
/// # `HtmlCallback` with empty input type.
/// Note that when `my_html_callback` is `HtmlCallback<()>`, you can use it more easily because it
/// implements [IntoView]
///
/// view!{
/// <div>
/// {render_number}
/// </div>
/// }
pub struct HtmlCallback<In = ()>(Rc<dyn Fn(In) -> HtmlElement<AnyElement>>);
impl<In> fmt::Debug for HtmlCallback<In> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
fmt.write_str("HtmlCallback")
}
}
impl<In> Clone for HtmlCallback<In> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<In> HtmlCallback<In> {
/// creates a new callback from the function or closure
pub fn new<F, H>(f: F) -> Self
where
F: Fn(In) -> HtmlElement<H> + 'static,
H: ElementDescriptor + 'static,
{
Self(Rc::new(move |x| f(x).into_any()))
}
}
impl<In> Callable<In, HtmlElement<AnyElement>> for HtmlCallback<In> {
fn call(&self, input: In) -> HtmlElement<AnyElement> {
(self.0)(input)
}
}
impl<In, F, H> From<F> for HtmlCallback<In>
where
F: Fn(In) -> HtmlElement<H> + 'static,
H: ElementDescriptor + 'static,
{
fn from(f: F) -> Self {
HtmlCallback(Rc::new(move |x| f(x).into_any()))
}
}
impl IntoView for HtmlCallback<()> {
fn into_view(self) -> View {
self.call(()).into_view()
}
}
/// A special callback type that returns any [`View`].
///
/// You can use it exactly the same way as a classic callback.
/// For how to use callbacks, see [here][crate::callback]
///
/// ```
/// # use leptos::*;
/// # use leptos::leptos_dom::{ViewCallback, Callable};
/// #[component]
/// fn MyComponent(
/// #[prop(into)] render_number: ViewCallback<i32>,
/// ) -> impl IntoView {
/// view! {
/// <div>
/// {render_number.call(42)}
/// </div>
/// }
/// }
/// fn test() -> impl IntoView {
/// view! {
/// <MyComponent render_number=move |x: i32| view!{<span>{x}</span>}/>
/// }
/// }
/// ```
///
/// # `ViewCallback` with empty input type.
/// Note that when `my_view_callback` is `ViewCallback<()>`, you can use it more easily because it
/// implements [IntoView]
///
/// view!{
/// <div>
/// {render_number}
/// </div>
/// }
pub struct ViewCallback<In>(Rc<dyn Fn(In) -> View>);
impl<In> fmt::Debug for ViewCallback<In> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
fmt.write_str("ViewCallback")
}
}
impl<In> Clone for ViewCallback<In> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<In> ViewCallback<In> {
/// creates a new callback from the function or closure
pub fn new<F, V>(f: F) -> Self
where
F: Fn(In) -> V + 'static,
V: IntoView + 'static,
{
ViewCallback(Rc::new(move |x| f(x).into_view()))
}
}
impl<In> Callable<In, View> for ViewCallback<In> {
fn call(&self, input: In) -> View {
(self.0)(input)
}
}
impl<In, F, V> From<F> for ViewCallback<In>
where
F: Fn(In) -> V + 'static,
V: IntoView + 'static,
{
fn from(f: F) -> Self {
Self::new(f)
}
}
impl IntoView for ViewCallback<()> {
fn into_view(self) -> View {
self.call(()).into_view()
}
}
#[cfg(test)]
mod tests {
use crate::{Callback, HtmlCallback, SyncCallback, ViewCallback};
struct NoClone {}
#[test]
fn clone_callback() {
let callback = Callback::new(move |_no_clone: NoClone| NoClone {});
let _cloned = callback.clone();
}
#[test]
fn clone_sync_callback() {
let callback = SyncCallback::new(move |_no_clone: NoClone| NoClone {});
let _cloned = callback.clone();
}
#[test]
fn clone_html_callback() {
#[derive(Debug)]
struct TestElem;
impl crate::ElementDescriptor for TestElem {
fn name(&self) -> leptos_reactive::Oco<'static, str> {
leptos_reactive::Oco::Borrowed("test-elem")
}
fn hydration_id(&self) -> &Option<crate::HydrationKey> {
&None
}
}
let callback = HtmlCallback::new(move |_no_clone: NoClone| {
crate::HtmlElement::new(TestElem {})
});
let _cloned = callback.clone();
}
#[test]
fn clone_view_callback() {
let callback =
ViewCallback::new(move |_no_clone: NoClone| crate::View::default());
let _cloned = callback.clone();
}
}

View File

@@ -250,19 +250,6 @@ impl HydrationCtx {
value
}
#[doc(hidden)]
#[cfg(feature = "hydrate")]
pub fn with_hydration_off<T>(f: impl FnOnce() -> T) -> T {
let prev = IS_HYDRATING.with(|is_hydrating| {
let prev = is_hydrating.get();
is_hydrating.set(false);
prev
});
let value = f();
IS_HYDRATING.with(|is_hydrating| is_hydrating.set(prev));
value
}
/// Whether the UI is currently in the process of hydrating from the server-sent HTML.
#[inline(always)]
pub fn is_hydrating() -> bool {

View File

@@ -9,6 +9,7 @@
#[cfg_attr(any(debug_assertions, feature = "ssr"), macro_use)]
pub extern crate tracing;
pub mod callback;
mod components;
mod events;
pub mod helpers;
@@ -25,6 +26,7 @@ pub mod ssr;
pub mod ssr_in_order;
pub mod svg;
mod transparent;
pub use callback::*;
use cfg_if::cfg_if;
pub use components::*;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@@ -927,7 +929,7 @@ thread_local! {
/// This is cached as a thread-local variable, so calling `window()` multiple times
/// requires only one call out to JavaScript.
pub fn window() -> web_sys::Window {
WINDOW.with(Clone::clone)
WINDOW.with(|window| window.clone())
}
/// Returns the [`Document`](https://developer.mozilla.org/en-US/docs/Web/API/Document).
@@ -935,7 +937,7 @@ pub fn window() -> web_sys::Window {
/// This is cached as a thread-local variable, so calling `document()` multiple times
/// requires only one call out to JavaScript.
pub fn document() -> web_sys::Document {
DOCUMENT.with(Clone::clone)
DOCUMENT.with(|document| document.clone())
}
/// Returns true if running on the server (SSR).

View File

@@ -203,9 +203,6 @@ pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacemen
.map(|nonce| format!(" nonce=\"{nonce}\""))
.unwrap_or_default();
let local_only = SharedContext::fragments_with_local_resources();
let local_only = serde_json::to_string(&local_only).unwrap();
let mut blocking_fragments = FuturesUnordered::new();
let fragments = FuturesUnordered::new();
@@ -215,7 +212,7 @@ pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacemen
.push(async move { (fragment_id, data.out_of_order.await) });
} else {
fragments.push(Box::pin(async move {
(fragment_id, data.out_of_order.await)
(fragment_id.clone(), data.out_of_order.await)
})
as Pin<Box<dyn Future<Output = (String, String)>>>);
}
@@ -229,8 +226,7 @@ pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacemen
let resolvers = format!(
"<script{nonce_str}>__LEPTOS_PENDING_RESOURCES = \
{pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \
Map();__LEPTOS_RESOURCE_RESOLVERS = new \
Map();__LEPTOS_LOCAL_ONLY = {local_only};</script>"
Map();__LEPTOS_RESOURCE_RESOLVERS = new Map();</script>"
);
if replace_blocks {

View File

@@ -125,9 +125,6 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
.map(|nonce| format!(" nonce=\"{nonce}\""))
.unwrap_or_default();
let local_only = SharedContext::fragments_with_local_resources();
let local_only = serde_json::to_string(&local_only).unwrap();
let stream = futures::stream::once({
let nonce_str = nonce_str.clone();
async move {
@@ -139,7 +136,6 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
__LEPTOS_PENDING_RESOURCES = {pending_resources};
__LEPTOS_RESOLVED_RESOURCES = new Map();
__LEPTOS_RESOURCE_RESOLVERS = new Map();
__LEPTOS_LOCAL_ONLY = {local_only};
</script>
"#
)

View File

@@ -6,7 +6,6 @@ use serde::{Deserialize, Serialize};
struct OldChildren(IndexMap<LNode, Vec<usize>>);
impl LNode {
#[must_use]
pub fn diff(&self, other: &LNode) -> Vec<Patch> {
let mut old_children = OldChildren::default();
self.add_old_children(vec![], &mut old_children);
@@ -197,7 +196,7 @@ impl LNode {
if replace {
match &new_value {
LAttributeValue::Boolean => {
Some((name.to_owned(), String::new()))
Some((name.to_owned(), "".to_string()))
}
LAttributeValue::Static(s) => {
Some((name.to_owned(), s.to_owned()))
@@ -214,13 +213,13 @@ impl LNode {
});
let removals = old.iter().filter_map(|(name, _)| {
if new.iter().any(|(new_name, _)| new_name == name) {
None
} else {
if !new.iter().any(|(new_name, _)| new_name == name) {
Some(Patch {
path: path.to_owned(),
action: PatchAction::RemoveAttribute(name.to_owned()),
})
} else {
None
}
});
@@ -260,6 +259,7 @@ impl LNode {
let new = new.get(a);
match (old, new) {
(None, None) => {}
(None, Some(new)) => patches.push(Patch {
path: path.to_owned(),
action: PatchAction::InsertChild {
@@ -271,10 +271,11 @@ impl LNode {
path: path.to_owned(),
action: PatchAction::RemoveChild { at: a },
}),
(Some(old), Some(new)) if old != new => {
break;
(Some(old), Some(new)) => {
if old != new {
break;
}
}
_ => {}
}
a += 1;
@@ -286,6 +287,7 @@ impl LNode {
let new = new.get(b);
match (old, new) {
(None, None) => {}
(None, Some(new)) => patches.push(Patch {
path: path.to_owned(),
action: PatchAction::InsertChildAfter {
@@ -297,16 +299,18 @@ impl LNode {
path: path.to_owned(),
action: PatchAction::RemoveChild { at: b },
}),
(Some(old), Some(new)) if old != new => {
break;
(Some(old), Some(new)) => {
if old != new {
break;
}
}
_ => {}
}
if b == 0 {
break;
} else {
b -= 1;
}
b -= 1;
}
// diffing in middle

View File

@@ -33,14 +33,10 @@ pub struct ViewMacros {
}
impl ViewMacros {
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// # Errors
///
/// Will return `Err` if the path is not UTF-8 path or the contents of the file cannot be parsed.
pub fn update_from_paths<T: AsRef<Path>>(&self, paths: &[T]) -> Result<()> {
let mut views = HashMap::new();
@@ -63,9 +59,6 @@ impl ViewMacros {
Ok(())
}
/// # Errors
///
/// Will return `Err` if the contents of the file cannot be parsed.
pub fn parse_file(path: &Utf8PathBuf) -> Result<Vec<MacroInvocation>> {
let mut file = File::open(path)?;
let mut content = String::new();
@@ -83,14 +76,11 @@ impl ViewMacros {
let rsx =
rstml::parse2(tokens.collect::<proc_macro2::TokenStream>())?;
let template = LNode::parse_view(rsx)?;
views.push(MacroInvocation { id, template });
views.push(MacroInvocation { id, template })
}
Ok(views)
}
/// # Errors
///
/// Will return `Err` if the contents of the file cannot be parsed.
pub fn patch(&self, path: &Utf8PathBuf) -> Result<Option<Patches>> {
let new_views = Self::parse_file(path)?;
let mut lock = self.views.write();
@@ -135,7 +125,7 @@ impl std::fmt::Debug for MacroInvocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MacroInvocation")
.field("id", &self.id)
.finish_non_exhaustive()
.finish()
}
}
@@ -146,7 +136,7 @@ pub struct ViewMacroVisitor<'a> {
impl<'ast> Visit<'ast> for ViewMacroVisitor<'ast> {
fn visit_macro(&mut self, node: &'ast Macro) {
let ident = node.path.get_ident().map(ToString::to_string);
let ident = node.path.get_ident().map(|n| n.to_string());
if ident == Some("view".to_string()) {
self.views.push(node);
}

View File

@@ -9,7 +9,6 @@ use serde::{Deserialize, Serialize};
// `syn` types are `!Send` so we can't store them as we might like.
// This is only used to diff view macros for hot reloading so it's very minimal
// and ignores many of the data types.
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum LNode {
Fragment(Vec<LNode>),
@@ -39,26 +38,18 @@ pub enum LAttributeValue {
}
impl LNode {
/// # Errors
///
/// Will return `Err` if parsing the view fails.
pub fn parse_view(nodes: Vec<Node>) -> Result<LNode> {
let mut out = Vec::new();
for node in nodes {
LNode::parse_node(node, &mut out)?;
}
if out.len() == 1 {
out.pop().ok_or_else(|| {
unreachable!("The last element should not be None.")
})
Ok(out.pop().unwrap())
} else {
Ok(LNode::Fragment(out))
}
}
/// # Errors
///
/// Will return `Err` if parsing the node fails.
pub fn parse_node(node: Node, views: &mut Vec<LNode>) -> Result<()> {
match node {
Node::Fragment(frag) => {
@@ -70,9 +61,9 @@ impl LNode {
views.push(LNode::Text(text.value_string()));
}
Node::Block(block) => {
views.push(LNode::DynChild(
block.into_token_stream().to_string(),
));
let code = block.into_token_stream();
let code = code.to_string();
views.push(LNode::DynChild(code));
}
Node::Element(el) => {
if is_component_node(&el) {
@@ -92,7 +83,7 @@ impl LNode {
attr.key.to_string(),
format!("{:#?}", attr.value()),
)),
NodeAttribute::Block(_) => None,
_ => None,
})
.collect(),
children,
@@ -160,9 +151,8 @@ impl LNode {
LAttributeValue::Static(value) => {
Some(format!("{name}=\"{value}\" "))
}
LAttributeValue::Dynamic | LAttributeValue::Noop => {
None
}
LAttributeValue::Dynamic => None,
LAttributeValue::Noop => None,
})
.collect::<String>();

View File

@@ -1,5 +1,6 @@
use rstml::node::{NodeElement, NodeName};
///
/// Converts `syn::Block` to simple expression
///
/// For example:
@@ -13,7 +14,6 @@ use rstml::node::{NodeElement, NodeName};
/// // variable
/// {path::x}
/// ```
#[must_use]
pub fn block_to_primitive_expression(block: &syn::Block) -> Option<&syn::Expr> {
// its empty block, or block with multi lines
if block.stmts.len() != 1 {
@@ -29,7 +29,6 @@ pub fn block_to_primitive_expression(block: &syn::Block) -> Option<&syn::Expr> {
///
/// This function doesn't convert literal wrapped inside block
/// like: `{"string"}`.
#[must_use]
pub fn value_to_string(value: &syn::Expr) -> Option<String> {
match &value {
syn::Expr::Lit(lit) => match &lit.lit {
@@ -43,10 +42,6 @@ pub fn value_to_string(value: &syn::Expr) -> Option<String> {
}
}
/// # Panics
///
/// Will panic if the last element does not exist in the path.
#[must_use]
pub fn is_component_tag_name(name: &NodeName) -> bool {
match name {
NodeName::Path(path) => {
@@ -60,11 +55,11 @@ pub fn is_component_tag_name(name: &NodeName) -> bool {
.to_string()
.starts_with(|c: char| c.is_ascii_uppercase())
}
NodeName::Block(_) | NodeName::Punctuated(_) => false,
NodeName::Block(_) => false,
NodeName::Punctuated(_) => false,
}
}
#[must_use]
pub fn is_component_node(node: &NodeElement) -> bool {
is_component_tag_name(node.name())
}

View File

@@ -12,10 +12,10 @@ readme = "../README.md"
proc-macro = true
[dependencies]
attribute-derive = { version = "0.8", features = ["syn-full"] }
attribute-derive = { version = "0.6", features = ["syn-full"] }
cfg-if = "1"
html-escape = "0.2"
itertools = "0.11"
itertools = "0.10"
prettyplease = "0.2.4"
proc-macro-error = { version = "1", default-features = false }
proc-macro2 = "1"

Some files were not shown because too many files have changed in this diff Show More