Compare commits

..

1 Commits
1754 ... 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
129 changed files with 1275 additions and 4742 deletions

View File

@@ -34,7 +34,6 @@ jobs:
examples
!examples/cargo-make
!examples/gtk
!examples/hackernews_js_fetch
!examples/Makefile.toml
!examples/*.md
json: true

View File

@@ -26,22 +26,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.5.0"
version = "0.5.0-rc2"
[workspace.dependencies]
leptos = { path = "./leptos", version = "0.5.0" }
leptos_dom = { path = "./leptos_dom", version = "0.5.0" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.0" }
leptos_macro = { path = "./leptos_macro", version = "0.5.0" }
leptos_reactive = { path = "./leptos_reactive", version = "0.5.0" }
leptos_server = { path = "./leptos_server", version = "0.5.0" }
server_fn = { path = "./server_fn", version = "0.5.0" }
server_fn_macro = { path = "./server_fn_macro", version = "0.5.0" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.0" }
leptos_config = { path = "./leptos_config", version = "0.5.0" }
leptos_router = { path = "./router", version = "0.5.0" }
leptos_meta = { path = "./meta", version = "0.5.0" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.0" }
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

@@ -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

@@ -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 upcoming `0.5.0` release, which you can install as version `0.5.0`. 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,7 +23,7 @@ cargo init leptos-tutorial
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
```bash
cargo add leptos@0.5.0 --features=csr,nightly
cargo add leptos@0.5.0-rc2 --features=csr,nightly
```
> **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.
@@ -31,7 +31,7 @@ cargo add leptos@0.5.0 --features=csr,nightly
Or you can leave off `nightly` if you're using stable Rust
```bash
cargo add leptos@0.5.0 --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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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();
@@ -1185,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");
@@ -1244,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
@@ -1271,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()
@@ -1303,10 +1258,8 @@ where
if path.is_empty() {
RouteListing::new(
"/".to_string(),
listing.path(),
listing.mode(),
listing.methods(),
listing.static_mode(),
)
} else {
listing
@@ -1314,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
@@ -1370,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>
@@ -1610,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 => {
@@ -1693,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

@@ -35,7 +35,9 @@ use leptos_reactive::{create_memo, signal_prelude::*};
)]
#[component]
pub fn Show<F, W, IV>(
/// The children will be shown whenever the condition in the `when` closure returns `true`.
/// 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,

View File

@@ -2,7 +2,7 @@ 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},
@@ -39,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 {
@@ -72,7 +72,7 @@ pub fn Transition<F, E>(
/// 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>,
@@ -125,7 +125,7 @@ where
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());
@@ -161,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

@@ -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

@@ -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

@@ -212,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)>>>);
}

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"

View File

@@ -10,10 +10,9 @@ use quote::{format_ident, quote_spanned, ToTokens, TokenStreamExt};
use syn::{
parse::Parse, parse_quote, spanned::Spanned,
AngleBracketedGenericArguments, Attribute, FnArg, GenericArgument, Item,
ItemFn, LitStr, Meta, Pat, PatIdent, Path, PathArguments, ReturnType,
Signature, Stmt, Type, TypePath, Visibility,
ItemFn, LitStr, Meta, Pat, PatIdent, Path, PathArguments, ReturnType, Stmt,
Type, TypePath, Visibility,
};
pub struct Model {
is_transparent: bool,
is_island: bool,
@@ -176,7 +175,7 @@ impl ToTokens for Model {
let prop_names = prop_names(props);
let builder_name_doc = LitStr::new(
&format!(" Props for the [`{name}`] component."),
&format!("Props for the [`{name}`] component."),
name.span(),
);
@@ -373,7 +372,9 @@ impl ToTokens for Model {
#component
};
let binding = if *is_island && cfg!(feature = "hydrate") {
let binding = if *is_island
&& cfg!(any(feature = "csr", feature = "hydrate"))
{
let island_props = if is_island_with_children
|| is_island_with_other_props
{
@@ -419,7 +420,6 @@ impl ToTokens for Model {
::leptos::leptos_dom::html::custom(
::leptos::leptos_dom::html::Custom::new("leptos-children"),
)
.prop("$$owner", ::leptos::Owner::current().map(|n| n.as_ffi()))
.into_view()
})])))
}
@@ -455,12 +455,9 @@ impl ToTokens for Model {
::leptos::leptos_dom::HydrationCtx::continue_from(key);
}
#deserialize_island_props
_ = ::leptos::run_as_child(move || {
::leptos::SharedContext::register_island(&el);
::leptos::leptos_dom::mount_to_with_stop_hydrating(el, false, move || {
#name(#island_props)
})
});
::leptos::leptos_dom::mount_to_with_stop_hydrating(el, false, move || {
#name(#island_props)
})
}
}
} else {
@@ -477,7 +474,6 @@ impl ToTokens for Model {
#[doc = #builder_name_doc]
#[doc = ""]
#docs
#[doc = ""]
#component_fn_prop_docs
#[derive(::leptos::typed_builder_macro::TypedBuilder #props_derive_serialize)]
//#[builder(doc)]
@@ -508,7 +504,6 @@ impl ToTokens for Model {
#into_view
#docs
#[doc = ""]
#component_fn_prop_docs
#[allow(non_snake_case, clippy::too_many_arguments)]
#tracing_instrument_attr
@@ -541,67 +536,6 @@ impl Model {
}
}
/// A model that is more lenient in case of a syntax error in the function body,
/// but does not actually implement the behavior of the real model. This is
/// used to improve IDEs and rust-analyzer's auto-completion behavior in case
/// of a syntax error.
pub struct DummyModel {
attrs: Vec<Attribute>,
vis: Visibility,
sig: Signature,
body: TokenStream,
}
impl Parse for DummyModel {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let attrs = input.call(Attribute::parse_outer)?;
let vis: Visibility = input.parse()?;
let sig: Signature = input.parse()?;
// The body is left untouched, so it will not cause an error
// even if the syntax is invalid.
let body: TokenStream = input.parse()?;
Ok(Self {
attrs,
vis,
sig,
body,
})
}
}
impl ToTokens for DummyModel {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Self {
attrs,
vis,
sig,
body,
} = self;
// Strip attributes like documentation comments and #[prop]
// from the signature, so as to not confuse the user with incorrect
// error messages.
let sig = {
let mut sig = sig.clone();
sig.inputs.iter_mut().for_each(|arg| {
if let FnArg::Typed(ty) = arg {
ty.attrs.clear();
}
});
sig
};
let output = quote! {
#(#attrs)*
#vis #sig #body
};
tokens.append_all(output)
}
}
struct Prop {
docs: Docs,
prop_opts: PropOpt,
@@ -676,8 +610,7 @@ impl Docs {
// Seperated out of chain to allow rustfmt to work
let map = |(doc, span): (String, Span)| {
doc.split('\n')
.map(str::trim_end)
doc.lines()
.flat_map(|doc| {
let trimmed_doc = doc.trim_start();
let leading_ws = &doc[..doc.len() - trimmed_doc.len()];
@@ -997,7 +930,7 @@ fn generate_component_fn_prop_docs(props: &[Prop]) -> TokenStream {
let required_prop_docs = if !required_prop_docs.is_empty() {
quote! {
#[doc = " # Required Props"]
#[doc = "# Required Props"]
#required_prop_docs
}
} else {
@@ -1006,7 +939,7 @@ fn generate_component_fn_prop_docs(props: &[Prop]) -> TokenStream {
let optional_prop_docs = if !optional_prop_docs.is_empty() {
quote! {
#[doc = " # Optional Props"]
#[doc = "# Optional Props"]
#optional_prop_docs
}
} else {
@@ -1108,10 +1041,10 @@ fn prop_to_doc(
PropDocStyle::List => {
let arg_ty_doc = LitStr::new(
&if !prop_opts.into {
format!(" - **{}**: [`{pretty_ty}`]", quote!(#name))
format!("- **{}**: [`{pretty_ty}`]", quote!(#name))
} else {
format!(
" - **{}**: [`impl Into<{pretty_ty}>`]({pretty_ty})",
"- **{}**: [`impl Into<{pretty_ty}>`]({pretty_ty})",
quote!(#name),
)
},

View File

@@ -597,21 +597,10 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
false
};
let parse_result = syn::parse::<component::Model>(s.clone());
if let Ok(model) = parse_result {
model
.is_transparent(is_transparent)
.into_token_stream()
.into()
} else {
// When the input syntax is invalid, e.g. while typing, we let
// the dummy model output tokens similar to the input, which improves
// IDEs and rust-analyzer's auto-complete capabilities.
parse_macro_input!(s as component::DummyModel)
.into_token_stream()
.into()
}
parse_macro_input!(s as component::Model)
.is_transparent(is_transparent)
.into_token_stream()
.into()
}
/// Defines a component as an interactive island when you are using the

View File

@@ -11,12 +11,11 @@ pub fn server_impl(
args: proc_macro::TokenStream,
s: TokenStream,
) -> TokenStream {
let function: syn::ItemFn = match syn::parse(s.clone()) {
Ok(f) => f,
// Returning the original input stream in the case of a parsing
// error helps IDEs and rust-analyzer with auto-completion.
Err(_) => return s,
};
let function: syn::ItemFn =
match syn::parse(s).map_err(|e| e.to_compile_error()) {
Ok(f) => f,
Err(e) => return e.into(),
};
let ItemFn {
attrs,
vis,

View File

@@ -127,12 +127,6 @@ pub(crate) fn node_to_tokens(
Node::Block(node) => Some(quote! { #node }),
Node::RawText(r) => {
let text = r.to_string_best();
if text == "cx," {
proc_macro_error::abort!(
r.span(),
"`cx,` is not used with the `view!` macro in 0.5."
)
}
let text = syn::LitStr::new(&text, r.span());
Some(quote! { #text })
}

View File

@@ -30,7 +30,7 @@ pub(crate) fn component_to_tokens(
let props = attrs
.clone()
.filter(|attr| {
!attr.key.to_string().starts_with("let:")
!attr.key.to_string().starts_with("bind:")
&& !attr.key.to_string().starts_with("clone:")
&& !attr.key.to_string().starts_with("on:")
&& !attr.key.to_string().starts_with("attr:")
@@ -55,7 +55,7 @@ pub(crate) fn component_to_tokens(
.filter_map(|attr| {
attr.key
.to_string()
.strip_prefix("let:")
.strip_prefix("bind:")
.map(|ident| format_ident!("{ident}", span = attr.key.span()))
})
.collect::<Vec<_>>();

View File

@@ -48,7 +48,7 @@ pub(crate) fn slot_to_tokens(
let props = attrs
.clone()
.filter(|attr| {
!attr.key.to_string().starts_with("let:")
!attr.key.to_string().starts_with("bind:")
&& !attr.key.to_string().starts_with("clone:")
})
.map(|attr| {
@@ -71,7 +71,7 @@ pub(crate) fn slot_to_tokens(
.filter_map(|attr| {
attr.key
.to_string()
.strip_prefix("let:")
.strip_prefix("bind:")
.map(|ident| format_ident!("{ident}", span = attr.key.span()))
})
.collect::<Vec<_>>();

View File

@@ -28,7 +28,7 @@ serde-wasm-bindgen = "0.5"
serde_json = "1"
base64 = "0.21"
thiserror = "1"
tokio = { version = "1", features = ["rt"], optional = true, default-features = false }
tokio = { version = "1", features = ["rt"], optional = true }
tracing = "0.1"
wasm-bindgen = { version = "0.2", optional = true }
wasm-bindgen-futures = { version = "0.4", optional = true }
@@ -42,7 +42,6 @@ web-sys = { version = "0.3", optional = true, features = [
cfg-if = "1"
indexmap = "2"
self_cell = "1.0.0"
pin-project = "1"
[dev-dependencies]
criterion = { version = "0.5.1", features = ["html_reports"] }
@@ -53,9 +52,6 @@ log = "0.4"
tokio-test = "0.4"
leptos = { path = "../leptos" }
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen-futures = { version = "0.4" }
[features]
default = []
csr = [

View File

@@ -1,316 +0,0 @@
//! Callbacks define a standard way to store functions and closures. They are useful
//! for component properties, because they can be used to define optional callback functions,
//! which generic props dont support.
//!
//! # Usage
//! Callbacks can be created manually from any function or closure, but the easiest way
//! to create them is to use `#[prop(into)]]` when defining a component.
//! ```
//! # use leptos::*;
//! #[component]
//! fn MyComponent(
//! #[prop(into)] render_number: Callback<i32, String>,
//! ) -> impl IntoView {
//! view! {
//! <div>
//! {render_number.call(1)}
//! // callbacks can be called multiple times
//! {render_number.call(42)}
//! </div>
//! }
//! }
//! // you can pass a closure directly as `render_number`
//! fn test() -> impl IntoView {
//! view! {
//! <MyComponent render_number=|x: i32| x.to_string()/>
//! }
//! }
//! ```
//!
//! *Notes*:
//! - The `render_number` prop can receive any type that implements `Fn(i32) -> String`.
//! - Callbacks are most useful when you want optional generic props.
//! - All callbacks implement the [`Callable`] trait, and can be invoked with `my_callback.call(input)`. On nightly, you can even do `my_callback(input)`
//! - The callback types implement [`Copy`], so they can easily be moved into and out of other closures, just like signals.
//!
//! # Types
//! This modules implements 2 callback types:
//! - [`Callback`]
//! - [`SyncCallback`]
//!
//! Use `SyncCallback` when you want the function to be `Sync` and `Send`.
use crate::{store_value, StoredValue};
use std::{fmt, sync::Arc};
/// A wrapper trait for calling callbacks.
pub trait Callable<In: 'static, Out: 'static = ()> {
/// calls the callback with the specified argument.
fn call(&self, input: In) -> Out;
}
/// Callbacks define a standard way to store functions and closures.
///
/// # Example
/// ```
/// # use leptos::*;
/// # use leptos::{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()/>
/// }
/// }
/// ```
pub struct Callback<In: 'static, Out: 'static = ()>(
StoredValue<Box<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)
}
}
impl<In, Out> Callback<In, Out> {
/// Creates a new callback from the given function.
pub fn new<F>(f: F) -> Callback<In, Out>
where
F: Fn(In) -> Out + 'static,
{
Self(store_value(Box::new(f)))
}
}
impl<In: 'static, Out: 'static> Callable<In, Out> for Callback<In, Out> {
fn call(&self, input: In) -> Out {
self.0.with_value(|f| f(input))
}
}
#[cfg(not(feature = "nightly"))]
impl<F, In, T, Out> From<F> for Callback<In, Out>
where
F: Fn(In) -> T + 'static,
T: Into<Out> + 'static,
{
fn from(f: F) -> Callback<In, Out> {
Callback::new(move |x| f(x).into())
}
}
#[cfg(feature = "nightly")]
auto trait NotRawCallback {}
#[cfg(feature = "nightly")]
impl<A, B> !NotRawCallback for Callback<A, B> {}
#[cfg(feature = "nightly")]
impl<F, In, T, Out> From<F> for Callback<In, Out>
where
F: Fn(In) -> T + NotRawCallback + 'static,
T: Into<Out> + 'static,
{
fn from(f: F) -> Callback<In, Out> {
Callback::new(move |x| f(x).into())
}
}
#[cfg(feature = "nightly")]
impl<In, Out> FnOnce<(In,)> for Callback<In, Out> {
type Output = Out;
extern "rust-call" fn call_once(self, args: (In,)) -> Self::Output {
Callable::call(&self, args.0)
}
}
#[cfg(feature = "nightly")]
impl<In, Out> FnMut<(In,)> for Callback<In, Out> {
extern "rust-call" fn call_mut(&mut self, args: (In,)) -> Self::Output {
Callable::call(&*self, args.0)
}
}
#[cfg(feature = "nightly")]
impl<In, Out> Fn<(In,)> for Callback<In, Out> {
extern "rust-call" fn call(&self, args: (In,)) -> Self::Output {
Callable::call(self, args.0)
}
}
/// A callback type that is `Send` and `Sync` if its input type is `Send` and `Sync`.
/// Otherwise, you can use exactly the way you use [`Callback`].
pub struct SyncCallback<In: 'static, Out: 'static = ()>(
StoredValue<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> Callable<In, Out> for SyncCallback<In, Out> {
fn call(&self, input: In) -> Out {
self.0.with_value(|f| f(input))
}
}
impl<In, Out> Clone for SyncCallback<In, Out> {
fn clone(&self) -> Self {
Self(self.0)
}
}
impl<In: 'static, Out: 'static> SyncCallback<In, Out> {
/// Creates a new callback from the given function.
pub fn new<F>(fun: F) -> Self
where
F: Fn(In) -> Out + 'static,
{
Self(store_value(Arc::new(fun)))
}
}
#[cfg(not(feature = "nightly"))]
impl<F, In, T, Out> From<F> for SyncCallback<In, Out>
where
F: Fn(In) -> T + 'static,
T: Into<Out> + 'static,
{
fn from(f: F) -> SyncCallback<In, Out> {
SyncCallback::new(move |x| f(x).into())
}
}
#[cfg(feature = "nightly")]
auto trait NotRawSyncCallback {}
#[cfg(feature = "nightly")]
impl<A, B> !NotRawSyncCallback for SyncCallback<A, B> {}
#[cfg(feature = "nightly")]
impl<F, In, T, Out> From<F> for SyncCallback<In, Out>
where
F: Fn(In) -> T + NotRawSyncCallback + 'static,
T: Into<Out> + 'static,
{
fn from(f: F) -> SyncCallback<In, Out> {
SyncCallback::new(move |x| f(x).into())
}
}
#[cfg(feature = "nightly")]
impl<In, Out> FnOnce<(In,)> for SyncCallback<In, Out> {
type Output = Out;
extern "rust-call" fn call_once(self, args: (In,)) -> Self::Output {
Callable::call(&self, args.0)
}
}
#[cfg(feature = "nightly")]
impl<In, Out> FnMut<(In,)> for SyncCallback<In, Out> {
extern "rust-call" fn call_mut(&mut self, args: (In,)) -> Self::Output {
Callable::call(&*self, args.0)
}
}
#[cfg(feature = "nightly")]
impl<In, Out> Fn<(In,)> for SyncCallback<In, Out> {
extern "rust-call" fn call(&self, args: (In,)) -> Self::Output {
Callable::call(self, args.0)
}
}
#[cfg(test)]
mod tests {
use crate::{
callback::{Callback, SyncCallback},
create_runtime,
};
struct NoClone {}
#[test]
fn clone_callback() {
let rt = create_runtime();
let callback = Callback::new(move |_no_clone: NoClone| NoClone {});
let _cloned = callback.clone();
rt.dispose();
}
#[test]
fn clone_sync_callback() {
let rt = create_runtime();
let callback = SyncCallback::new(move |_no_clone: NoClone| NoClone {});
let _cloned = callback.clone();
rt.dispose();
}
#[test]
fn callback_from() {
let rt = create_runtime();
let _callback: Callback<(), String> = (|()| "test").into();
rt.dispose();
}
#[test]
fn callback_from_html() {
let rt = create_runtime();
use leptos::{
html::{AnyElement, HtmlElement},
*,
};
let _callback: Callback<String, HtmlElement<AnyElement>> =
(|x: String| {
view! {
<h1>{x}</h1>
}
})
.into();
rt.dispose();
}
#[test]
fn sync_callback_from() {
let rt = create_runtime();
let _callback: SyncCallback<(), String> = (|()| "test").into();
rt.dispose();
}
#[test]
fn sync_callback_from_html() {
use leptos::{
html::{AnyElement, HtmlElement},
*,
};
let rt = create_runtime();
let _callback: SyncCallback<String, HtmlElement<AnyElement>> =
(|x: String| {
view! {
<h1>{x}</h1>
}
})
.into();
rt.dispose();
}
}

View File

@@ -1,5 +1,3 @@
#[cfg(all(feature = "hydrate", feature = "experimental-islands"))]
use crate::Owner;
use crate::{
runtime::PinnedFuture, suspense::StreamChunk, with_runtime, ResourceId,
SuspenseContext,
@@ -22,8 +20,6 @@ pub struct SharedContext {
pub pending_fragments: HashMap<String, FragmentData>,
#[cfg(feature = "experimental-islands")]
pub no_hydrate: bool,
#[cfg(all(feature = "hydrate", feature = "experimental-islands"))]
pub islands: HashMap<Owner, web_sys::HtmlElement>,
}
impl SharedContext {
@@ -169,22 +165,6 @@ impl SharedContext {
})
.unwrap_or_default()
}
/// Registers the given element as an island with the current reactive owner.
#[cfg(all(feature = "hydrate", feature = "experimental-islands"))]
#[cfg_attr(
any(debug_assertions, features = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn register_island(el: &web_sys::HtmlElement) {
if let Some(owner) = Owner::current() {
let el = el.clone();
_ = with_runtime(|runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
shared_context.islands.insert(owner, el);
});
}
}
}
/// Represents its pending `<Suspense/>` fragment.
@@ -247,11 +227,6 @@ impl Default for SharedContext {
pending_fragments: Default::default(),
#[cfg(feature = "experimental-islands")]
no_hydrate: true,
#[cfg(all(
feature = "hydrate",
feature = "experimental-islands"
))]
islands: Default::default(),
}
}
#[cfg(not(all(feature = "hydrate", target_arch = "wasm32")))]
@@ -264,11 +239,6 @@ impl Default for SharedContext {
pending_fragments: Default::default(),
#[cfg(feature = "experimental-islands")]
no_hydrate: true,
#[cfg(all(
feature = "hydrate",
feature = "experimental-islands"
))]
islands: Default::default(),
}
}
}

View File

@@ -3,8 +3,6 @@
#![cfg_attr(feature = "nightly", feature(fn_traits))]
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
#![cfg_attr(feature = "nightly", feature(type_name_of_val))]
#![cfg_attr(feature = "nightly", feature(auto_traits))]
#![cfg_attr(feature = "nightly", feature(negative_impls))]
//! The reactive system for the [Leptos](https://docs.rs/leptos/latest/leptos/) Web framework.
//!
@@ -80,7 +78,6 @@ extern crate tracing;
#[macro_use]
mod signal;
mod callback;
mod context;
#[macro_use]
mod diagnostics;
@@ -106,7 +103,6 @@ pub mod suspense;
mod trigger;
mod watch;
pub use callback::*;
pub use context::*;
pub use diagnostics::SpecialNonReactiveZone;
pub use effect::*;
@@ -118,11 +114,8 @@ pub use resource::*;
use runtime::*;
pub use runtime::{
as_child_of_current_owner, batch, create_runtime, current_runtime,
on_cleanup, run_as_child, set_current_runtime,
spawn_local_with_current_owner, spawn_local_with_owner,
try_spawn_local_with_current_owner, try_spawn_local_with_owner,
try_with_owner, untrack, untrack_with_diagnostics, with_current_owner,
with_owner, Owner, RuntimeId, ScopedFuture,
on_cleanup, set_current_runtime, untrack, untrack_with_diagnostics,
with_current_owner, with_owner, Owner, RuntimeId,
};
pub use selector::*;
pub use serialization::*;

View File

@@ -32,7 +32,7 @@ pub enum Oco<'a, T: ?Sized + ToOwned + 'a> {
Owned(<T as ToOwned>::Owned),
}
impl<'a, T: ?Sized + ToOwned> Oco<'a, T> {
impl<T: ?Sized + ToOwned> Oco<'_, T> {
/// Converts the value into an owned value.
pub fn into_owned(self) -> <T as ToOwned>::Owned {
match self {
@@ -211,16 +211,15 @@ where
}
}
impl<'a, T> Clone for Oco<'a, T>
where
T: ?Sized + ToOwned + 'a,
for<'b> Rc<T>: From<&'b T>,
{
// ------------------------------------------------------------------------------------------------------
// Cloning (has to be implemented manually because of the `Rc<T>: From<&<T as ToOwned>::Owned>` bound)
// ------------------------------------------------------------------------------------------------------
impl Clone for Oco<'_, str> {
/// Returns a new [`Oco`] with the same value as this one.
/// If the value is [`Oco::Owned`], this will convert it into
/// [`Oco::Counted`], so that the next clone will be O(1).
/// # Examples
/// [`String`] :
/// ```
/// # use leptos_reactive::oco::Oco;
/// let oco = Oco::<str>::Owned("Hello".to_string());
@@ -228,48 +227,106 @@ where
/// assert_eq!(oco, oco2);
/// assert!(oco2.is_counted());
/// ```
/// [`Vec`] :
fn clone(&self) -> Self {
match self {
Oco::Borrowed(v) => Oco::Borrowed(v),
Oco::Counted(v) => Oco::Counted(v.clone()),
Oco::Owned(v) => Oco::Counted(Rc::<str>::from(v.as_str())),
}
}
}
impl Clone for Oco<'_, CStr> {
/// Returns a new [`Oco`] with the same value as this one.
/// If the value is [`Oco::Owned`], this will convert it into
/// [`Oco::Counted`], so that the next clone will be O(1).
/// # Examples
/// ```
/// # use leptos_reactive::oco::Oco;
/// let oco = Oco::<[u8]>::Owned(b"Hello".to_vec());
/// use std::ffi::CStr;
///
/// let oco = Oco::<CStr>::Owned(
/// CStr::from_bytes_with_nul(b"Hello\0").unwrap().to_owned(),
/// );
/// let oco2 = oco.clone();
/// assert_eq!(oco, oco2);
/// assert!(oco2.is_counted());
/// ```
fn clone(&self) -> Self {
match self {
Self::Borrowed(v) => Self::Borrowed(v),
Self::Counted(v) => Self::Counted(Rc::clone(v)),
Self::Owned(v) => Self::Counted(Rc::from(v.borrow())),
Oco::Borrowed(v) => Oco::Borrowed(v),
Oco::Counted(v) => Oco::Counted(v.clone()),
Oco::Owned(v) => Oco::Counted(Rc::<CStr>::from(v.as_c_str())),
}
}
}
impl<'a, T> Oco<'a, T>
where
T: ?Sized + ToOwned + 'a,
for<'b> Rc<T>: From<&'b T>,
{
/// Clones the value with inplace conversion into [`Oco::Counted`] if it
/// was previously [`Oco::Owned`].
impl Clone for Oco<'_, OsStr> {
/// Returns a new [`Oco`] with the same value as this one.
/// If the value is [`Oco::Owned`], this will convert it into
/// [`Oco::Counted`], so that the next clone will be O(1).
/// # Examples
/// ```
/// # use leptos_reactive::oco::Oco;
/// let mut oco1 = Oco::<str>::Owned("Hello".to_string());
/// let oco2 = oco1.clone_inplace();
/// assert_eq!(oco1, oco2);
/// assert!(oco1.is_counted());
/// use std::ffi::OsStr;
///
/// let oco = Oco::<OsStr>::Owned(OsStr::new("Hello").to_owned());
/// let oco2 = oco.clone();
/// assert_eq!(oco, oco2);
/// assert!(oco2.is_counted());
/// ```
pub fn clone_inplace(&mut self) -> Self {
match &*self {
Self::Borrowed(v) => Self::Borrowed(v),
Self::Counted(v) => Self::Counted(Rc::clone(v)),
Self::Owned(v) => {
let rc = Rc::from(v.borrow());
*self = Self::Counted(rc.clone());
Self::Counted(rc)
}
fn clone(&self) -> Self {
match self {
Oco::Borrowed(v) => Oco::Borrowed(v),
Oco::Counted(v) => Oco::Counted(v.clone()),
Oco::Owned(v) => Oco::Counted(Rc::<OsStr>::from(v.as_os_str())),
}
}
}
impl Clone for Oco<'_, Path> {
/// Returns a new [`Oco`] with the same value as this one.
/// If the value is [`Oco::Owned`], this will convert it into
/// [`Oco::Counted`], so that the next clone will be O(1).
/// # Examples
/// ```
/// # use leptos_reactive::oco::Oco;
/// use std::path::Path;
///
/// let oco = Oco::<Path>::Owned(Path::new("Hello").to_owned());
/// let oco2 = oco.clone();
/// assert_eq!(oco, oco2);
/// assert!(oco2.is_counted());
/// ```
fn clone(&self) -> Self {
match self {
Oco::Borrowed(v) => Oco::Borrowed(v),
Oco::Counted(v) => Oco::Counted(v.clone()),
Oco::Owned(v) => Oco::Counted(Rc::<Path>::from(v.as_path())),
}
}
}
impl<T: Clone> Clone for Oco<'_, [T]>
where
[T]: ToOwned<Owned = Vec<T>>,
{
/// Returns a new [`Oco`] with the same value as this one.
/// If the value is [`Oco::Owned`], this will convert it into
/// [`Oco::Counted`], so that the next clone will be O(1).
/// # Examples
/// ```
/// # use leptos_reactive::oco::Oco;
/// let oco = Oco::<[i32]>::Owned(vec![1, 2, 3]);
/// let oco2 = oco.clone();
/// assert_eq!(oco, oco2);
/// assert!(oco2.is_counted());
/// ```
fn clone(&self) -> Self {
match self {
Oco::Borrowed(v) => Oco::Borrowed(v),
Oco::Counted(v) => Oco::Counted(v.clone()),
Oco::Owned(v) => Oco::Counted(Rc::<[T]>::from(v.as_slice())),
}
}
}
@@ -632,46 +689,23 @@ mod tests {
}
#[test]
fn cloned_owned_string_should_make_counted_str() {
fn cloned_owned_string_should_become_counted_str() {
let s: Oco<str> = Oco::Owned(String::from("hello"));
assert!(s.clone().is_counted());
}
#[test]
fn cloned_borrowed_str_should_make_borrowed_str() {
fn cloned_borrowed_str_should_remain_borrowed_str() {
let s: Oco<str> = Oco::Borrowed("hello");
assert!(s.clone().is_borrowed());
}
#[test]
fn cloned_counted_str_should_make_counted_str() {
fn cloned_counted_str_should_remain_counted_str() {
let s: Oco<str> = Oco::Counted(Rc::from("hello"));
assert!(s.clone().is_counted());
}
#[test]
fn cloned_inplace_owned_string_should_make_counted_str_and_become_counted()
{
let mut s: Oco<str> = Oco::Owned(String::from("hello"));
assert!(s.clone_inplace().is_counted());
assert!(s.is_counted());
}
#[test]
fn cloned_inplace_borrowed_str_should_make_borrowed_str_and_remain_borrowed(
) {
let mut s: Oco<str> = Oco::Borrowed("hello");
assert!(s.clone_inplace().is_borrowed());
assert!(s.is_borrowed());
}
#[test]
fn cloned_inplace_counted_str_should_make_counted_str_and_remain_counted() {
let mut s: Oco<str> = Oco::Counted(Rc::from("hello"));
assert!(s.clone_inplace().is_counted());
assert!(s.is_counted());
}
#[test]
fn serialization_works() {
let s = serde_json::to_string(&Oco::Borrowed("foo"))

View File

@@ -3,8 +3,8 @@ use crate::SharedContext;
#[cfg(debug_assertions)]
use crate::SpecialNonReactiveZone;
use crate::{
create_isomorphic_effect, create_memo, create_render_effect, create_signal,
queue_microtask, runtime::with_runtime, serialization::Serializable,
create_isomorphic_effect, create_memo, create_signal, queue_microtask,
runtime::with_runtime, serialization::Serializable,
signal_prelude::format_signal_warning, spawn::spawn_local, use_context,
GlobalSuspenseContext, Memo, ReadSignal, ScopeProperty, Signal,
SignalDispose, SignalGet, SignalGetUntracked, SignalSet, SignalUpdate,
@@ -143,11 +143,6 @@ where
///
/// **Note**: This is not “blocking” in the sense that it blocks the current thread. Rather,
/// it is blocking in the sense that it blocks the server from sending a response.
///
/// When used with the leptos_router and `SsrMode::PartiallyBlocked`, a
/// blocking resource will ensure `<Suspense/>` blocks depending on the resource
/// are fully rendered on the server side, without requiring JavaScript or
/// WebAssembly on the client.
#[cfg_attr(
any(debug_assertions, feature="ssr"),
instrument(
@@ -248,9 +243,7 @@ where
/// value of the `source` changes, a new [`Future`] will be created and run.
///
/// Unlike [`create_resource()`], this [`Future`] is always run on the local system
/// and therefore its result type does not need to be [`Serializable`].
///
/// Local resources do not load on the server, only in the clients browser.
/// and therefore it's result type does not need to be [`Serializable`].
///
/// ```
/// # use leptos_reactive::*;
@@ -305,8 +298,6 @@ where
/// Unlike [`create_resource_with_initial_value()`], this [`Future`] will always run
/// on the local system and therefore its output type does not need to be
/// [`Serializable`].
///
/// Local resources do not load on the server, only in the clients browser.
#[cfg_attr(
any(debug_assertions, feature="ssr"),
instrument(
@@ -362,10 +353,10 @@ where
})
.expect("tried to create a Resource in a runtime that has been disposed.");
// This is a local resource, so we're always going to handle it on the
// client
create_render_effect({
create_isomorphic_effect({
let r = Rc::clone(&r);
// This is a local resource, so we're always going to handle it on the
// client
move |_| r.load(false)
});

View File

@@ -13,7 +13,6 @@ use cfg_if::cfg_if;
use core::hash::BuildHasherDefault;
use futures::stream::FuturesUnordered;
use indexmap::IndexSet;
use pin_project::pin_project;
use rustc_hash::{FxHashMap, FxHasher};
use slotmap::{SecondaryMap, SlotMap, SparseSecondaryMap};
use std::{
@@ -24,9 +23,7 @@ use std::{
marker::PhantomData,
pin::Pin,
rc::Rc,
task::Poll,
};
use thiserror::Error;
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
@@ -98,29 +95,15 @@ pub struct Owner(pub(crate) NodeId);
impl Owner {
/// Returns the current reactive owner.
///
/// ## Panics
/// Panics if there is no current reactive runtime.
pub fn current() -> Option<Owner> {
with_runtime(|runtime| runtime.owner.get())
.ok()
.flatten()
.map(Owner)
}
/// Returns a unique handle for this owner for FFI purposes.
pub fn as_ffi(&self) -> u64 {
use slotmap::Key;
self.0.data().as_ffi()
}
/// Parses a unique handler back into an owner.
///
/// Iff `value` is value received from `k.as_ffi()`, returns a key equal to `k`.
/// Otherwise the behavior is safe but unspecified.
pub fn from_ffi(ffi: u64) -> Self {
use slotmap::KeyData;
Self(NodeId::from(KeyData::from_ffi(ffi)))
}
}
// This core Runtime impl block handles all the work of marking and updating
@@ -535,96 +518,11 @@ impl Runtime {
});
match local_value {
Some(val) => Some(val),
None => {
#[cfg(all(
feature = "hydrate",
feature = "experimental-islands"
))]
{
self.get_island_context(
self.shared_context
.borrow()
.islands
.get(&Owner(node))
.cloned(),
node,
ty,
)
}
#[cfg(not(all(
feature = "hydrate",
feature = "experimental-islands"
)))]
{
self.node_owners
.borrow()
.get(node)
.and_then(|parent| self.get_context(*parent, ty))
}
}
}
}
#[cfg(all(feature = "hydrate", feature = "experimental-islands"))]
pub(crate) fn get_island_context<T: Clone + 'static>(
&self,
el: Option<web_sys::HtmlElement>,
node: NodeId,
ty: TypeId,
) -> Option<T> {
let contexts = self.contexts.borrow();
let context = contexts.get(node);
let local_value = context.and_then(|context| {
context
.get(&ty)
.and_then(|val| val.downcast_ref::<T>())
.cloned()
});
match (el, local_value) {
(_, Some(val)) => Some(val),
// if we're already in the island's scope, island-hop
(Some(el), None) => {
use js_sys::Reflect;
use wasm_bindgen::{intern, JsCast, JsValue};
let parent_el = el
.parent_element()
.expect("to have parent")
.unchecked_ref::<web_sys::HtmlElement>()
.closest("leptos-children")
.expect("to find island")
//.flatten()
.and_then(|el| el.dyn_into::<web_sys::HtmlElement>().ok());
match parent_el
.clone()
.and_then(|el| {
Reflect::get(&el, &JsValue::from_str(intern("$$owner")))
.ok()
})
.and_then(|value| u64::try_from(value).ok())
.map(Owner::from_ffi)
{
Some(owner) => {
self.get_island_context(parent_el, owner.0, ty)
}
None => None,
}
}
// otherwise, check for a parent scope
(None, None) => {
self.node_owners.borrow().get(node).and_then(|parent| {
self.get_island_context(
self.shared_context
.borrow()
.islands
.get(&Owner(*parent))
.cloned(),
*parent,
ty,
)
})
}
None => self
.node_owners
.borrow()
.get(node)
.and_then(|parent| self.get_context(*parent, ty)),
}
}
@@ -684,9 +582,7 @@ impl Debug for Runtime {
instrument(level = "trace", skip_all,)
)]
#[inline(always)] // it monomorphizes anyway
pub(crate) fn with_runtime<T>(
f: impl FnOnce(&Runtime) -> T,
) -> Result<T, ReactiveSystemError> {
pub(crate) fn with_runtime<T>(f: impl FnOnce(&Runtime) -> T) -> Result<T, ()> {
// in the browser, everything should exist under one runtime
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
@@ -694,9 +590,8 @@ pub(crate) fn with_runtime<T>(
} else {
RUNTIMES.with(|runtimes| {
let runtimes = runtimes.borrow();
let rt = Runtime::current();
match runtimes.get(rt) {
None => Err(ReactiveSystemError::RuntimeDisposed(rt)),
match runtimes.get(Runtime::current()) {
None => Err(()),
Some(runtime) => Ok(f(runtime))
}
})
@@ -820,84 +715,25 @@ where
///
/// ## Panics
/// Panics if there is no current reactive runtime.
pub fn with_owner<T>(owner: Owner, f: impl FnOnce() -> T) -> T {
try_with_owner(owner, f).unwrap()
}
#[derive(Error, Debug)]
pub enum ReactiveSystemError {
#[error("Runtime {0:?} has been disposed.")]
RuntimeDisposed(RuntimeId),
#[error("Owner {0:?} has been disposed.")]
OwnerDisposed(Owner),
#[error("Error borrowing runtime.nodes {0:?}")]
Borrow(std::cell::BorrowError),
}
/// Runs the given code with the given reactive owner.
pub fn try_with_owner<T>(
owner: Owner,
f: impl FnOnce() -> T,
) -> Result<T, ReactiveSystemError> {
pub fn with_owner<T>(owner: Owner, f: impl FnOnce() -> T + 'static) -> T
where
T: 'static,
{
with_runtime(|runtime| {
let scope_exists = {
let nodes = runtime
.nodes
.try_borrow()
.map_err(ReactiveSystemError::Borrow)?;
nodes.contains_key(owner.0)
};
if scope_exists {
let prev_observer = runtime.observer.take();
let prev_owner = runtime.owner.take();
runtime.owner.set(Some(owner.0));
runtime.observer.set(Some(owner.0));
let v = f();
runtime.observer.set(prev_observer);
runtime.owner.set(prev_owner);
Ok(v)
} else {
Err(ReactiveSystemError::OwnerDisposed(owner))
}
})?
}
/// Runs the given function as a child of the current Owner, once.
pub fn run_as_child<T>(f: impl FnOnce() -> T + 'static) -> T {
let owner = with_runtime(|runtime| runtime.owner.get())
.expect("runtime should be alive when created");
let (value, disposer) = with_runtime(|runtime| {
let prev_observer = runtime.observer.take();
let prev_owner = runtime.owner.take();
runtime.owner.set(owner);
runtime.observer.set(owner);
let id = runtime.nodes.borrow_mut().insert(ReactiveNode {
value: None,
state: ReactiveNodeState::Clean,
node_type: ReactiveNodeType::Trigger,
});
runtime.push_scope_property(ScopeProperty::Trigger(id));
let disposer = Disposer(id);
runtime.owner.set(Some(id));
runtime.observer.set(Some(id));
runtime.owner.set(Some(owner.0));
runtime.observer.set(Some(owner.0));
let v = f();
runtime.observer.set(prev_observer);
runtime.owner.set(prev_owner);
(v, disposer)
v
})
.expect("runtime should be alive when run");
on_cleanup(move || drop(disposer));
value
.expect("runtime should be alive when with_owner runs")
}
impl RuntimeId {
@@ -1497,153 +1333,3 @@ pub fn untrack<T>(f: impl FnOnce() -> T) -> T {
pub fn untrack_with_diagnostics<T>(f: impl FnOnce() -> T) -> T {
Runtime::current().untrack(f, true)
}
/// Allows running a future that has access to a given scope.
#[pin_project]
pub struct ScopedFuture<Fut: Future> {
owner: Owner,
#[pin]
future: Fut,
}
/// Errors that can occur when trying to spawn a [`ScopedFuture`].
#[derive(Error, Debug, Clone)]
pub enum ScopedFutureError {
#[error(
"Tried to spawn a scoped Future without a current reactive Owner."
)]
NoCurrentOwner,
}
impl<Fut: Future + 'static> Future for ScopedFuture<Fut> {
type Output = Option<Fut::Output>;
fn poll(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Self::Output> {
let this = self.project();
if let Ok(poll) = try_with_owner(*this.owner, || this.future.poll(cx)) {
match poll {
Poll::Ready(res) => Poll::Ready(Some(res)),
Poll::Pending => Poll::Pending,
}
} else {
Poll::Ready(None)
}
}
}
impl<Fut: Future> ScopedFuture<Fut> {
/// Creates a new future that will have access to the `[Owner]`'s
/// scope context.
pub fn new(owner: Owner, fut: Fut) -> Self {
Self { owner, future: fut }
}
/// Runs the future in the current [`Owner`]'s scope context.
#[track_caller]
pub fn new_current(fut: Fut) -> Result<Self, ScopedFutureError> {
Owner::current()
.map(|owner| Self { owner, future: fut })
.ok_or(ScopedFutureError::NoCurrentOwner)
}
}
/// Runs a future that has access to the provided [`Owner`]'s
/// scope context.
#[track_caller]
pub fn spawn_local_with_owner(
owner: Owner,
fut: impl Future<Output = ()> + 'static,
) {
let scoped_future = ScopedFuture::new(owner, fut);
#[cfg(debug_assertions)]
let loc = std::panic::Location::caller();
crate::spawn_local(async move {
if scoped_future.await.is_none() {
crate::macros::debug_warn!(
"`spawn_local_with_owner` called at {loc} returned `None`, \
i.e., its Owner was disposed before the `Future` resolved."
);
}
});
}
/// Runs a future that has access to the provided [`Owner`]'s
/// scope context.
///
/// # Panics
/// Panics if there is no [`Owner`] context available.
#[track_caller]
pub fn spawn_local_with_current_owner(
fut: impl Future<Output = ()> + 'static,
) -> Result<(), ScopedFutureError> {
let scoped_future = ScopedFuture::new_current(fut)?;
#[cfg(debug_assertions)]
let loc = std::panic::Location::caller();
crate::spawn_local(async move {
if scoped_future.await.is_none() {
crate::macros::debug_warn!(
"`spawn_local_with_owner` called at {loc} returned `None`, \
i.e., its Owner was disposed before the `Future` resolved."
);
}
});
Ok(())
}
/// Runs a future that has access to the provided [`Owner`]'s
/// scope context.
///
/// Since futures run in the background, it is possible that
/// the scope has been cleaned up since the future started running.
/// If this happens, the future will not be completed.
///
/// The `on_cancelled` callback can be used to notify you that the
/// future was cancelled.
pub fn try_spawn_local_with_owner(
owner: Owner,
fut: impl Future<Output = ()> + 'static,
on_cancelled: impl FnOnce() + 'static,
) {
let scoped_future = ScopedFuture::new(owner, fut);
crate::spawn_local(async move {
if scoped_future.await.is_none() {
on_cancelled();
}
});
}
/// Runs a future that has access to the provided [`Owner`]'s
/// scope context.
///
/// Since futures run in the background, it is possible that
/// the scope has been cleaned up since the future started running.
/// If this happens, the future will not be completed.
///
/// The `on_cancelled` callback can be used to notify you that the
/// future was cancelled.
///
/// # Panics
/// Panics if there is no [`Owner`] context available.
#[track_caller]
pub fn try_spawn_local_with_current_owner(
fut: impl Future<Output = ()> + 'static,
on_cancelled: impl FnOnce() + 'static,
) -> Result<(), ScopedFutureError> {
let scoped_future = ScopedFuture::new_current(fut)?;
crate::spawn_local(async move {
if scoped_future.await.is_none() {
on_cancelled();
}
});
Ok(())
}

View File

@@ -428,6 +428,25 @@ impl<T> From<Memo<T>> for Signal<T> {
}
}
impl<T: Clone> From<T> for Signal<T> {
#[track_caller]
fn from(value: T) -> Self {
Self {
inner: SignalTypes::DerivedSignal(store_value(Box::new(
move || value.clone(),
))),
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: std::panic::Location::caller(),
}
}
}
impl From<&str> for Signal<String> {
fn from(value: &str) -> Self {
Self::from(value.to_string())
}
}
enum SignalTypes<T>
where
T: 'static,
@@ -835,36 +854,6 @@ impl From<&str> for MaybeSignal<String> {
}
}
#[cfg(feature = "nightly")]
mod from_fn_for_signals {
use super::{MaybeSignal, Memo, ReadSignal, RwSignal, Signal};
auto trait NotSignalMarker {}
impl<T> !NotSignalMarker for Signal<T> {}
impl<T> !NotSignalMarker for ReadSignal<T> {}
impl<T> !NotSignalMarker for Memo<T> {}
impl<T> !NotSignalMarker for RwSignal<T> {}
impl<T> !NotSignalMarker for MaybeSignal<T> {}
impl<F, T> From<F> for Signal<T>
where
F: Fn() -> T + NotSignalMarker + 'static,
{
fn from(value: F) -> Self {
Signal::derive(value)
}
}
}
#[cfg(not(feature = "nightly"))]
impl<F, T> From<F> for Signal<T>
where
F: Fn() -> T + 'static,
{
fn from(value: F) -> Self {
Signal::derive(value)
}
}
/// A wrapping type for an optional component prop, which can either be a signal or a
/// non-reactive value, and which may or may not have a value. In other words, this is
/// an `Option<MaybeSignal<Option<T>>>` that automatically flattens its getters.

View File

@@ -75,7 +75,7 @@ where
F: Future<Output = ()> + 'static,
{
cfg_if! {
if #[cfg(target_arch = "wasm32")] {
if #[cfg(all(target_arch = "wasm32", any(feature = "csr", feature = "hydrate")))] {
wasm_bindgen_futures::spawn_local(fut)
}
else if #[cfg(any(test, doctest))] {

View File

@@ -118,29 +118,33 @@ impl SuspenseContext {
let setter = self.set_pending_resources;
let serializable_resources = self.pending_serializable_resources;
let has_local_only = self.has_local_only;
setter.update(|n| *n += 1);
if serializable {
serializable_resources.update(|n| *n += 1);
has_local_only.set_value(false);
}
queue_microtask(move || {
setter.update(|n| *n += 1);
if serializable {
serializable_resources.update(|n| *n += 1);
has_local_only.set_value(false);
}
});
}
/// Notifies the suspense context that a resource has resolved.
pub fn decrement(&self, serializable: bool) {
let setter = self.set_pending_resources;
let serializable_resources = self.pending_serializable_resources;
setter.update(|n| {
if *n > 0 {
*n -= 1
}
});
if serializable {
serializable_resources.update(|n| {
queue_microtask(move || {
setter.update(|n| {
if *n > 0 {
*n -= 1;
*n -= 1
}
});
}
if serializable {
serializable_resources.update(|n| {
if *n > 0 {
*n -= 1;
}
});
}
});
}
/// Resets the counter of pending resources.

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.5.0"
version = "0.5.0-rc2"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

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