Compare commits

...

127 Commits

Author SHA1 Message Date
Greg Johnston
9e8b559e51 handle optional props correctly 2023-08-19 07:21:56 -04:00
Greg Johnston
db4ce00f98 add MaybeSignal deserialization 2023-08-19 07:21:56 -04:00
Greg Johnston
9edcc5f43b add support for island props 2023-08-19 07:21:56 -04:00
Greg Johnston
ea2cda3d17 work on serializing props 2023-08-19 07:21:56 -04:00
Greg Johnston
1d5252b97e fix iOS Safari pleeeaaassseeee stop it, Apple 2023-08-19 07:21:56 -04:00
Greg Johnston
d175249d00 fix build 2023-08-19 07:21:56 -04:00
Greg Johnston
35a9d84734 ignore rIC if not supported 2023-08-19 07:21:56 -04:00
Greg Johnston
c8a5434c9b start work on serializing props in DOM 2023-08-19 07:21:56 -04:00
Greg Johnston
ac00ea4ffe fix merge issues 2023-08-19 07:21:56 -04:00
Greg Johnston
59b708db9f support suspense with islands 2023-08-19 07:21:56 -04:00
Greg Johnston
722f6913d1 make whole JS import idle-only 2023-08-19 07:21:56 -04:00
Greg Johnston
aabcfb7ac3 switch to data-hk for all use cases 2023-08-19 07:21:56 -04:00
Greg Johnston
e691ae391f fix re-layouts on hydration (WIP) 2023-08-19 07:21:56 -04:00
Greg Johnston
7c3fd1d67d use correct name for hkc 2023-08-19 07:21:56 -04:00
Greg Johnston
88ee228389 turn fragments back off 2023-08-19 07:21:56 -04:00
Greg Johnston
02b0ac15a3 make whole hydration process idle 2023-08-19 07:21:56 -04:00
Greg Johnston
83edcfe29d restore compression 2023-08-19 07:21:56 -04:00
Greg Johnston
b288b7d838 do set fragment 2023-08-19 07:21:56 -04:00
Greg Johnston
a1a78f165a axum islands example 2023-08-19 07:21:56 -04:00
Greg Johnston
1f3453e94b rIC per island and resuming based on keys both working 2023-08-19 07:21:56 -04:00
Greg Johnston
e8b35ba284 work on resuming HK 2023-08-19 07:21:56 -04:00
Greg Johnston
02275d4fc0 requestIdleCallback per island 2023-08-19 07:21:56 -04:00
Greg Johnston
18e44be19a playing with islands hackernews 2023-08-19 07:21:56 -04:00
Greg Johnston
0bd4e5269a request idle callback before hydration 2023-08-19 07:21:56 -04:00
Greg Johnston
88405bfd10 intern id removal 2023-08-19 07:21:56 -04:00
Greg Johnston
80bc9a4e10 islands 2023-08-19 07:21:56 -04:00
Greg Johnston
b4c3068ddd got children working! 2023-08-19 07:21:56 -04:00
Greg Johnston
ac5406b8a6 fix merge issues 2023-08-19 07:21:56 -04:00
Greg Johnston
8f38a8a5f5 do the island hydration directly in JS, save a kb 2023-08-19 07:21:56 -04:00
Greg Johnston
bd7ff6a3fc support multiple islands, oops 2023-08-19 07:21:56 -04:00
Greg Johnston
c2d9696494 working super-basic islands implementation 2023-08-19 07:21:56 -04:00
Greg Johnston
35893d4eb9 release mode benchmarks 2023-08-19 07:20:10 -04:00
Greg Johnston
c3959d1de8 make hydration keys optional (to allow NoHydrate areas) 2023-08-19 07:20:10 -04:00
Greg Johnston
76c9d8233a islands 2023-08-19 07:20:10 -04:00
Greg Johnston
2988fc1c36 initial islands work 2023-08-19 07:20:09 -04:00
Greg Johnston
e7662ae940 perf: don't include unused hydration code in CSR bundles 2023-08-19 07:20:09 -04:00
Greg Johnston
674fcd2ade stash 2023-08-19 07:20:09 -04:00
Greg Johnston
5beab9f3cc fix examples 2023-08-19 07:03:54 -04:00
Greg Johnston
49be4a219d .map_ref() => .map() 2023-08-18 14:14:45 -04:00
Greg Johnston
f92aebdf01 cargo fmt example 2023-08-18 14:14:07 -04:00
Greg Johnston
6ba7a8e235 nope 2023-08-18 14:07:42 -04:00
Greg Johnston
10e3106760 removing cx/Scope from some merged docs 2023-08-18 08:41:37 -04:00
Greg Johnston
63dee1c93f closes issue #1558 2023-08-18 08:36:56 -04:00
Greg Johnston
fca4215fdb Change resource API to .get(), .with(), .map_ref(), and .and_then() 2023-08-18 08:36:56 -04:00
Greg Johnston
72cde5c355 use tuple form of .child() 2023-08-18 08:36:56 -04:00
Greg Johnston
0bf1f11638 avoid panic in use_navigate during SSR 2023-08-18 08:36:56 -04:00
Greg Johnston
2ea4531313 0.5.0-beta 2023-08-18 08:36:56 -04:00
Greg Johnston
bd2acfc530 fix example 2023-08-18 08:36:56 -04:00
Greg Johnston
42ff663622 sometimes Rust's variable scoping rules baffle me (should fix panic with overlapping BorrowMut in on_cleanup) 2023-08-18 08:36:56 -04:00
Greg Johnston
8ffd5c69ab erroneous log 2023-08-18 08:36:56 -04:00
Greg Johnston
90ce31edf8 fix unsetting of title when navigating between multiple pages 2023-08-18 08:36:56 -04:00
Greg Johnston
e7160092f6 fix Suspense-For-Suspense panic 2023-08-18 08:36:56 -04:00
Greg Johnston
c6bd7a40e7 pull in changes to view macor from main 2023-08-18 08:36:56 -04:00
Greg Johnston
d544a678b1 remove removed API 2023-08-18 08:36:56 -04:00
Greg Johnston
4b96a71da5 on_cleanup untracked (fixes #1494) 2023-08-18 08:36:56 -04:00
Greg Johnston
6ab95d8a16 remove deprecated APIs 2023-08-18 08:36:56 -04:00
Greg Johnston
db51f6d2b4 embed requestAnimationFrame into navigate() 2023-08-18 08:36:56 -04:00
Greg Johnston
964a26b5d3 BAD rebase, NO rebase. 2023-08-18 08:36:56 -04:00
Greg Johnston
1871d2bc6d impl Serialize and Deserialize for signal types 2023-08-18 08:36:56 -04:00
Greg Johnston
fc82788bcc add some useful From implementations 2023-08-18 08:36:56 -04:00
Ben Wishovich
dba5c444ae fix: render_route() path matching (#1479) 2023-08-18 08:36:56 -04:00
Greg Johnston
95f285f126 fix <For/> in SSR 2023-08-18 08:36:56 -04:00
Greg Johnston
5865ea4048 fix: restore missing fixes in <For/> (closes #1473) 2023-08-18 08:36:56 -04:00
Greg Johnston
121c312ba3 alpha 2 2023-08-18 08:36:56 -04:00
Greg Johnston
19967d1dd5 remove duplicate from merge 2023-08-18 08:36:56 -04:00
Greg Johnston
42a1395ffd clippy 2023-08-18 08:36:56 -04:00
Greg Johnston
421715f923 clean up window click listener 2023-08-18 08:36:56 -04:00
Greg Johnston
0eba690993 cancelable handle for window_event_listener 2023-08-18 08:36:56 -04:00
Greg Johnston
2a971b5fd3 remove scope 2023-08-18 08:36:56 -04:00
Greg Johnston
8701bd88bd closes #1465 2023-08-18 08:36:56 -04:00
Greg Johnston
8ccc731e91 better error messages for expect_context 2023-08-18 08:36:56 -04:00
Greg Johnston
b9bb999b6a was breaking something 2023-08-18 08:36:56 -04:00
Greg Johnston
d7080bff96 don't double-create children in nested suspense in ssr 2023-08-18 08:36:56 -04:00
Greg Johnston
52eea6222b track caller for better error messages 2023-08-18 08:36:56 -04:00
Greg Johnston
c1c469fd08 better error messages 2023-08-18 08:36:56 -04:00
Greg Johnston
42d18d4b54 clippy 2023-08-18 08:36:56 -04:00
Greg Johnston
e69d55190e fix routing progress display 2023-08-18 08:36:56 -04:00
Ben Wishovich
431f5398f9 Add render_route functions to respect SsrMode on routes when using custom handlers (#1460) 2023-08-18 08:36:56 -04:00
Greg Johnston
3223182141 add <Route data/> loaders again 2023-08-18 08:36:56 -04:00
Greg Johnston
815acee19b even even better error message 2023-08-18 08:36:56 -04:00
Greg Johnston
270622ce86 typo in doctest 2023-08-18 08:36:56 -04:00
Greg Johnston
de97e2dc12 fix ssr tests 2023-08-18 08:36:56 -04:00
Greg Johnston
3dd12bf416 fix release build SSR 2023-08-18 08:36:56 -04:00
Greg Johnston
59d3450d5b gtfo 2023-08-18 08:36:56 -04:00
Greg Johnston
02d0849a34 don't need to render EachItem markers for View::Element nodes 2023-08-18 08:36:56 -04:00
Greg Johnston
41f8d66565 restore continuing from current id 2023-08-18 08:36:56 -04:00
Greg Johnston
8da6710e44 add separate error field in hydration keys 2023-08-18 08:36:56 -04:00
IcosaHedron
38a68926ca cleanups must be called before properties are disposed (#1449) 2023-08-18 08:36:56 -04:00
Greg Johnston
895c8765ed add MaybeProp 2023-08-18 08:36:56 -04:00
Greg Johnston
ddd797f853 clean up logs 2023-08-18 08:36:56 -04:00
Greg Johnston
39dd204cda fix spawn docs examples 2023-08-18 08:36:56 -04:00
Greg Johnston
90041a9e99 fix reactive tests 2023-08-18 08:36:56 -04:00
Greg Johnston
d3a6d59970 cx in tests 2023-08-18 08:36:56 -04:00
Greg Johnston
314c803e4d v0.5.0-alpha 2023-08-18 08:36:56 -04:00
Greg Johnston
f5028e200e add some panic docs 2023-08-18 08:36:56 -04:00
Greg Johnston
fd15859ee4 clean up docs 2023-08-18 08:36:56 -04:00
Greg Johnston
d4ede63b3a clippy 2023-08-18 08:36:42 -04:00
Greg Johnston
b7c4a9d5c7 fix errorboundary 2023-08-18 08:36:42 -04:00
Greg Johnston
c780924c3d fix views in component macro docs 2023-08-18 08:36:42 -04:00
Greg Johnston
d2e8981e94 component macro 2023-08-18 08:36:42 -04:00
Greg Johnston
449a14bf16 fix issues in examples 2023-08-18 08:36:42 -04:00
Greg Johnston
5cc4c977ad tests 2023-08-18 08:36:42 -04:00
Greg Johnston
67ee6d5dbf ssr tests 2023-08-18 08:36:42 -04:00
Greg Johnston
919ab91cb0 fix 2023-08-18 08:36:42 -04:00
Greg Johnston
3f0c908479 fixing tests 2023-08-18 08:36:42 -04:00
Greg Johnston
a7cf566700 clippy 2023-08-18 08:36:42 -04:00
Greg Johnston
a30356da12 remove _ = cx; 2023-08-18 08:36:42 -04:00
Greg Johnston
c612c2c0a3 clippy 2023-08-18 08:36:42 -04:00
Greg Johnston
370d8a951a fix merge issues 2023-08-18 08:36:42 -04:00
Greg Johnston
b3d75125d4 clean up 2023-08-18 08:36:42 -04:00
Greg Johnston
b12b397f15 clean up 2023-08-18 08:36:42 -04:00
Greg Johnston
39d9b806ff remove Scope from docs 2023-08-18 08:36:42 -04:00
Greg Johnston
02c4bb31bc remove cx from docs 2023-08-18 08:36:41 -04:00
Greg Johnston
52f0ce4b48 fix Viz 2023-08-18 08:35:57 -04:00
Greg Johnston
b8d30676f8 move forbid(unsafe) to crate level 2023-08-18 08:35:57 -04:00
Greg Johnston
e7b52c0076 fix watch 2023-08-18 08:35:57 -04:00
Greg Johnston
8cc3ece052 even better error msg 2023-08-18 08:35:57 -04:00
Greg Johnston
a19e83818d better error when failing to deserialize resource JSON 2023-08-18 08:35:57 -04:00
Greg Johnston
e92029d36c fix suspense ownership chain 2023-08-18 08:35:57 -04:00
Greg Johnston
4a9d959488 unused import 2023-08-18 08:35:57 -04:00
Greg Johnston
9fd9d94783 get AnimatedOutlet working 2023-08-18 08:35:57 -04:00
Greg Johnston
241ca5e4dd restore <AnimatedRoutes/> 2023-08-18 08:35:57 -04:00
Greg Johnston
39e81136c5 fix suspense 2023-08-18 08:35:57 -04:00
Greg Johnston
0ebf63f8af cleaning up tests 2023-08-18 08:35:57 -04:00
Greg Johnston
0b532735fc default impls 2023-08-18 08:35:57 -04:00
Greg Johnston
40998f62c3 don't store runtime ID in signals 2023-08-18 08:35:57 -04:00
Greg Johnston
4df6057393 squash work on reactive ownership 2023-08-18 08:35:57 -04:00
340 changed files with 10591 additions and 7679 deletions

View File

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

View File

@@ -5,9 +5,9 @@ edition = "2021"
[dependencies]
l021 = { package = "leptos", version = "0.2.1" }
leptos = { path = "../leptos", features = ["ssr"] }
leptos = { path = "../leptos", features = ["ssr", "nightly", "islands"] }
sycamore = { version = "0.8", features = ["ssr"] }
yew = { git = "https://github.com/yewstack/yew", features = ["ssr"] }
yew = { version = "0.20", features = ["ssr"] }
tokio-test = "0.4"
miniserde = "0.1"
gloo = "0.8"

View File

@@ -9,10 +9,10 @@ This document is intended as a running list of common issues, with example code
**Issue**: Sometimes you want to update a reactive signal in a way that depends on another signal.
```rust
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, false);
let (a, set_a) = create_signal(0);
let (b, set_b) = create_signal(false);
create_effect(cx, move |_| {
create_effect(move |_| {
if a() > 5 {
set_b(true);
}
@@ -24,7 +24,7 @@ This creates an inefficient chain of updates, and can easily lead to infinite lo
**Solution**: Follow the rule, _What can be derived, should be derived._ In this case, this has the benefit of massively reducing the code size, too!
```rust
let (a, set_a) = create_signal(cx, 0);
let (a, set_a) = create_signal(0);
let b = move || a () > 5;
```
@@ -34,19 +34,19 @@ Sometimes you have nested signals: for example, hash-map that can change over ti
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let resources = create_rw_signal(cx, HashMap::new());
pub fn App() -> impl IntoView {
let resources = create_rw_signal(HashMap::new());
let update = move |id: usize| {
resources.update(|resources| {
resources
.entry(id)
.or_insert_with(|| create_rw_signal(cx, 0))
.or_insert_with(|| create_rw_signal(0))
.update(|amount| *amount += 1)
})
};
view! { cx,
view! {
<div>
<pre>{move || format!("{:#?}", resources.get().into_iter().map(|(id, resource)| (id, resource.get())).collect::<Vec<_>>())}</pre>
<button on:click=move |_| update(1)>"+"</button>
@@ -55,17 +55,17 @@ pub fn App(cx: Scope) -> impl IntoView {
}
```
Clicking the button twice will cause a panic, because of the nested signal *read*. Calling the `update` function on `resources` immediately takes out a mutable borrow on `resources`, then updates the `resource` signal—which re-runs the effect that reads from the signals, which tries to immutably access `resources` and panics. It's the nested update here which causes a problem, because the inner update triggers and effect that tries to read both signals while the outer is still updating.
Clicking the button twice will cause a panic, because of the nested signal _read_. Calling the `update` function on `resources` immediately takes out a mutable borrow on `resources`, then updates the `resource` signal—which re-runs the effect that reads from the signals, which tries to immutably access `resources` and panics. It's the nested update here which causes a problem, because the inner update triggers and effect that tries to read both signals while the outer is still updating.
You can fix this fairly easily by using the [`Scope::batch()`](https://docs.rs/leptos/latest/leptos/struct.Scope.html#method.batch) method:
You can fix this fairly easily by using the [`batch()`](https://docs.rs/leptos/latest/leptos/fn.batch.html) method:
```rust
let update = move |id: usize| {
cx.batch(move || {
batch(move || {
resources.update(|resources| {
resources
.entry(id)
.or_insert_with(|| create_rw_signal(cx, 0))
.or_insert_with(|| create_rw_signal(0))
.update(|amount| *amount += 1)
})
});
@@ -83,11 +83,11 @@ Many DOM attributes can be updated either by setting an attribute on the DOM nod
This means that in practice, attributes like `value` or `checked` on an `<input/>` element only update the _default_ value for the `<input/>`. If you want to reactively update the value, you should use `prop:value` instead to set the `value` property.
```rust
let (a, set_a) = create_signal(cx, "Starting value".to_string());
let (a, set_a) = create_signal("Starting value".to_string());
let on_input = move |ev| set_a(event_target_value(&ev));
view! {
cx,
// ❌ reactivity doesn't work as expected: typing only updates the default
// of each input, so if you start typing in the second input, it won't
// update the first one
@@ -97,11 +97,11 @@ view! {
```
```rust
let (a, set_a) = create_signal(cx, "Starting value".to_string());
let (a, set_a) = create_signal("Starting value".to_string());
let on_input = move |ev| set_a(event_target_value(&ev));
view! {
cx,
// ✅ works as intended by setting the value *property*
<input prop:value=a on:input=on_input />
<input prop:value=a on:input=on_input />

View File

@@ -27,6 +27,7 @@ cargo add leptos --features=csr,nightly
```
Or you can leave off `nightly` if you're using stable Rust
```bash
cargo add leptos --features=csr
```
@@ -64,7 +65,7 @@ And add a simple “Hello, world!” to your `main.rs`
use leptos::*;
fn main() {
mount_to_body(|cx| view! { cx, <p>"Hello, world!"</p> })
mount_to_body(|| view! { <p>"Hello, world!"</p> })
}
```

View File

@@ -29,15 +29,15 @@ all its children and descendants using `provide_context`.
```rust
#[component]
fn App(cx: Scope) -> impl IntoView {
fn App() -> impl IntoView {
// here we create a signal in the root that can be consumed
// anywhere in the app.
let (count, set_count) = create_signal(cx, 0);
let (count, set_count) = create_signal(0);
// we'll pass the setter to specific components,
// but provide the count itself to the whole app via context
provide_context(cx, count);
provide_context(count);
view! { cx,
view! {
// SetterButton is allowed to modify the count
<SetterButton set_count/>
// These consumers can only read from it
@@ -57,14 +57,14 @@ fn App(cx: Scope) -> impl IntoView {
```rust
/// A component that does some "fancy" math with the global count
#[component]
fn FancyMath(cx: Scope) -> impl IntoView {
fn FancyMath() -> impl IntoView {
// here we consume the global count signal with `use_context`
let count = use_context::<ReadSignal<u32>>(cx)
let count = use_context::<ReadSignal<u32>>()
// we know we just provided this in the parent component
.expect("there to be a `count` signal provided");
let is_even = move || count() & 1 == 0;
view! { cx,
view! {
<div class="consumer blue">
"The number "
<strong>{count}</strong>
@@ -89,17 +89,17 @@ struct GlobalState {
}
impl GlobalState {
pub fn new(cx: Scope) -> Self {
pub fn new() -> Self {
Self {
count: create_rw_signal(cx, 0),
name: create_rw_signal(cx, "Bob".to_string())
count: create_rw_signal(0),
name: create_rw_signal("Bob".to_string())
}
}
}
#[component]
fn App(cx: Scope) -> impl IntoView {
provide_context(cx, GlobalState::new(cx));
fn App() -> impl IntoView {
provide_context(GlobalState::new());
// etc.
}
@@ -117,8 +117,8 @@ struct GlobalState {
}
#[component]
fn App(cx: Scope) -> impl IntoView {
provide_context(cx, create_rw_signal(GlobalState::default()));
fn App() -> impl IntoView {
provide_context(create_rw_signal(GlobalState::default()));
// etc.
}
@@ -127,8 +127,8 @@ fn App(cx: Scope) -> impl IntoView {
But theres a problem: because our whole state is wrapped in one signal, updating the value of one field will cause reactive updates in parts of the UI that only depend on the other.
```rust
let state = expect_context::<RwSignal<GlobalState>>(cx);
view! { cx,
let state = expect_context::<RwSignal<GlobalState>>();
view! {
<button on:click=move |_| state.update(|n| *n += 1)>"+1"</button>
<p>{move || state.with(|state| state.name.clone())}</p>
}
@@ -143,12 +143,12 @@ Here, instead of reading from the state signal directly, we create “slices”
```rust
/// A component that updates the count in the global state.
#[component]
fn GlobalStateCounter(cx: Scope) -> impl IntoView {
let state = expect_context::<RwSignal<GlobalState>>(cx);
fn GlobalStateCounter() -> impl IntoView {
let state = expect_context::<RwSignal<GlobalState>>();
// `create_slice` lets us create a "lens" into the data
let (count, set_count) = create_slice(
cx,
// we take a slice *from* `state`
state,
// our getter returns a "slice" of the data
@@ -157,7 +157,7 @@ fn GlobalStateCounter(cx: Scope) -> impl IntoView {
|state, n| state.count = n,
);
view! { cx,
view! {
<div class="consumer blue">
<button
on:click=move |_| {
@@ -214,15 +214,15 @@ use leptos::*;
// components using provide_context(). Changing it will only cause rerendering
// in the specific places it is actually used, not the whole app.
#[component]
fn Option2(cx: Scope) -> impl IntoView {
fn Option2() -> impl IntoView {
// here we create a signal in the root that can be consumed
// anywhere in the app.
let (count, set_count) = create_signal(cx, 0);
let (count, set_count) = create_signal(0);
// we'll pass the setter to specific components,
// but provide the count itself to the whole app via context
provide_context(cx, count);
provide_context(count);
view! { cx,
view! {
<h1>"Option 2: Passing Signals"</h1>
// SetterButton is allowed to modify the count
<SetterButton set_count/>
@@ -237,8 +237,8 @@ fn Option2(cx: Scope) -> impl IntoView {
/// A button that increments our global counter.
#[component]
fn SetterButton(cx: Scope, set_count: WriteSignal<u32>) -> impl IntoView {
view! { cx,
fn SetterButton(set_count: WriteSignal<u32>) -> impl IntoView {
view! {
<div class="provider red">
<button on:click=move |_| set_count.update(|count| *count += 1)>
"Increment Global Count"
@@ -249,14 +249,14 @@ fn SetterButton(cx: Scope, set_count: WriteSignal<u32>) -> impl IntoView {
/// A component that does some "fancy" math with the global count
#[component]
fn FancyMath(cx: Scope) -> impl IntoView {
fn FancyMath() -> impl IntoView {
// here we consume the global count signal with `use_context`
let count = use_context::<ReadSignal<u32>>(cx)
let count = use_context::<ReadSignal<u32>>()
// we know we just provided this in the parent component
.expect("there to be a `count` signal provided");
let is_even = move || count() & 1 == 0;
view! { cx,
view! {
<div class="consumer blue">
"The number "
<strong>{count}</strong>
@@ -272,17 +272,17 @@ fn FancyMath(cx: Scope) -> impl IntoView {
/// A component that shows a list of items generated from the global count.
#[component]
fn ListItems(cx: Scope) -> impl IntoView {
fn ListItems() -> impl IntoView {
// again, consume the global count signal with `use_context`
let count = use_context::<ReadSignal<u32>>(cx).expect("there to be a `count` signal provided");
let count = use_context::<ReadSignal<u32>>().expect("there to be a `count` signal provided");
let squares = move || {
(0..count())
.map(|n| view! { cx, <li>{n}<sup>"2"</sup> " is " {n * n}</li> })
.map(|n| view! { <li>{n}<sup>"2"</sup> " is " {n * n}</li> })
.collect::<Vec<_>>()
};
view! { cx,
view! {
<div class="consumer green">
<ul>{squares}</ul>
</div>
@@ -304,13 +304,13 @@ struct GlobalState {
}
#[component]
fn Option3(cx: Scope) -> impl IntoView {
fn Option3() -> impl IntoView {
// we'll provide a single signal that holds the whole state
// each component will be responsible for creating its own "lens" into it
let state = create_rw_signal(cx, GlobalState::default());
provide_context(cx, state);
let state = create_rw_signal(GlobalState::default());
provide_context(state);
view! { cx,
view! {
<h1>"Option 3: Passing Signals"</h1>
<div class="red consumer" style="width: 100%">
<h2>"Current Global State"</h2>
@@ -329,12 +329,12 @@ fn Option3(cx: Scope) -> impl IntoView {
/// A component that updates the count in the global state.
#[component]
fn GlobalStateCounter(cx: Scope) -> impl IntoView {
let state = use_context::<RwSignal<GlobalState>>(cx).expect("state to have been provided");
fn GlobalStateCounter() -> impl IntoView {
let state = use_context::<RwSignal<GlobalState>>().expect("state to have been provided");
// `create_slice` lets us create a "lens" into the data
let (count, set_count) = create_slice(
cx,
// we take a slice *from* `state`
state,
// our getter returns a "slice" of the data
@@ -343,7 +343,7 @@ fn GlobalStateCounter(cx: Scope) -> impl IntoView {
|state, n| state.count = n,
);
view! { cx,
view! {
<div class="consumer blue">
<button
on:click=move |_| {
@@ -360,14 +360,14 @@ fn GlobalStateCounter(cx: Scope) -> impl IntoView {
/// A component that updates the count in the global state.
#[component]
fn GlobalStateInput(cx: Scope) -> impl IntoView {
let state = use_context::<RwSignal<GlobalState>>(cx).expect("state to have been provided");
fn GlobalStateInput() -> impl IntoView {
let state = use_context::<RwSignal<GlobalState>>().expect("state to have been provided");
// this slice is completely independent of the `count` slice
// that we created in the other component
// neither of them will cause the other to rerun
let (name, set_name) = create_slice(
cx,
// we take a slice *from* `state`
state,
// our getter returns a "slice" of the data
@@ -376,7 +376,7 @@ fn GlobalStateInput(cx: Scope) -> impl IntoView {
|state, n| state.name = n,
);
view! { cx,
view! {
<div class="consumer green">
<input
type="text"
@@ -395,7 +395,7 @@ fn GlobalStateInput(cx: Scope) -> impl IntoView {
// Because we defined it as `fn App`, we can now use it in a
// template as <App/>
fn main() {
leptos::mount_to_body(|cx| view! { cx, <Option2/><Option3/> })
leptos::mount_to_body(|| view! { <Option2/><Option3/> })
}
```

View File

@@ -38,13 +38,13 @@ So imagine the following code:
```rust
// A
let (name, set_name) = create_signal(cx, "Alice");
let (name, set_name) = create_signal("Alice");
// B
let name_upper = create_memo(cx, move |_| name.with(|n| n.to_uppercase()));
let name_upper = create_memo(move |_| name.with(|n| n.to_uppercase()));
// C
create_effect(cx, move |_| {
create_effect(move |_| {
log!("{}", name_upper());
});
@@ -67,21 +67,21 @@ Lets make it a little more complex.
```rust
// A
let (name, set_name) = create_signal(cx, "Alice");
let (name, set_name) = create_signal("Alice");
// B
let name_upper = create_memo(cx, move |_| name.with(|n| n.to_uppercase()));
let name_upper = create_memo(move |_| name.with(|n| n.to_uppercase()));
// C
let name_len = create_memo(cx, move |_| name.len());
let name_len = create_memo(move |_| name.len());
// D
create_effect(cx, move |_| {
create_effect(move |_| {
log!("len = {}", name_len());
});
// E
create_effect(cx, move |_| {
create_effect(move |_| {
log!("name = {}", name_upper());
});
```
@@ -131,16 +131,16 @@ One more example, of whats sometimes called **the diamond problem**.
```rust
// A
let (name, set_name) = create_signal(cx, "Alice");
let (name, set_name) = create_signal("Alice");
// B
let name_upper = create_memo(cx, move |_| name.with(|n| n.to_uppercase()));
let name_upper = create_memo(move |_| name.with(|n| n.to_uppercase()));
// C
let name_len = create_memo(cx, move |_| name.len());
let name_len = create_memo(move |_| name.len());
// D
create_effect(cx, move |_| {
create_effect(move |_| {
log!("{} is {} characters long", name_upper(), name_len());
});
```
@@ -218,7 +218,7 @@ All of this is cool, and memos are pretty great. But most actual applications ha
In cases in which the computation itself is cheaper than this reactive work, you should avoid “over-wrapping” with memos and simply use derived signals. Heres a great example in which you should never use a memo:
```rust
let (a, set_a) = create_signal(cx, 1);
let (a, set_a) = create_signal(1);
// none of these make sense as memos
let b = move || a() + 2;
let c = move || b() % 2 == 0;
@@ -234,10 +234,10 @@ Even though memoizing would technically save an extra calculation of `d` between
At the very most, you might consider memoizing the final node before running some expensive side effect:
```rust
let text = create_memo(cx, move |_| {
let text = create_memo(move |_| {
d()
});
create_effect(cx, move |_| {
create_effect(move |_| {
engrave_text_into_bar_of_gold(&text());
});
```

View File

@@ -2,7 +2,7 @@
A [Resource](https://docs.rs/leptos/latest/leptos/struct.Resource.html) is a reactive data structure that reflects the current state of an asynchronous task, allowing you to integrate asynchronous `Future`s into the synchronous reactive system. Rather than waiting for its data to load with `.await`, you transform the `Future` into a signal that returns `Some(T)` if it has resolved, and `None` if its still pending.
You do this by using the [`create_resource`](https://docs.rs/leptos/latest/leptos/fn.create_resource.html) function. This takes two arguments (other than the ubiquitous `cx`):
You do this by using the [`create_resource`](https://docs.rs/leptos/latest/leptos/fn.create_resource.html) function. This takes two arguments:
1. a source signal, which will generate a new `Future` whenever it changes
2. a fetcher function, which takes the data from that signal and returns a `Future`
@@ -11,10 +11,10 @@ Heres an example
```rust
// our source signal: some synchronous, local state
let (count, set_count) = create_signal(cx, 0);
let (count, set_count) = create_signal(0);
// our resource
let async_data = create_resource(cx,
let async_data = create_resource(
count,
// every time `count` changes, this will run
|value| async move {
@@ -27,23 +27,20 @@ let async_data = create_resource(cx,
To create a resource that simply runs once, you can pass a non-reactive, empty source signal:
```rust
let once = create_resource(cx, || (), |_| async move { load_data().await });
let once = create_resource(|| (), |_| async move { load_data().await });
```
To access the value you can use `.read(cx)` or `.with(cx, |data| /* */)`. These work just like `.get()` and `.with()` on a signal—`read` clones the value and returns it, `with` applies a closure to it—but with two differences
1. For any `Resource<_, T>`, they always return `Option<T>`, not `T`: because its always possible that your resource is still loading.
2. They take a `Scope` argument. Youll see why in the next chapter, on `<Suspense/>`.
To access the value you can use `.read()` or `.with(|data| /* */)`. These work just like `.get()` and `.with()` on a signal—`read` clones the value and returns it, `with` applies a closure to it—but for any `Resource<_, T>`, they always return `Option<T>`, not `T`: because its always possible that your resource is still loading.
So, you can show the current state of a resource in your view:
```rust
let once = create_resource(cx, || (), |_| async move { load_data().await });
view! { cx,
let once = create_resource(|| (), |_| async move { load_data().await });
view! {
<h1>"My Data"</h1>
{move || match once.read(cx) {
None => view! { cx, <p>"Loading..."</p> }.into_view(cx),
Some(data) => view! { cx, <ShowData data/> }.into_view(cx)
{move || match once.read() {
None => view! { <p>"Loading..."</p> }.into_view(),
Some(data) => view! { <ShowData data/> }.into_view()
}}
}
```
@@ -71,13 +68,13 @@ async fn load_data(value: i32) -> i32 {
}
#[component]
fn App(cx: Scope) -> impl IntoView {
fn App() -> impl IntoView {
// this count is our synchronous, local state
let (count, set_count) = create_signal(cx, 0);
let (count, set_count) = create_signal(0);
// create_resource takes two arguments after its scope
let async_data = create_resource(
cx,
// the first is the "source signal"
count,
// the second is the loader
@@ -90,14 +87,14 @@ fn App(cx: Scope) -> impl IntoView {
// you can also create resources that only load once
// just return the unit type () from the source signal
// that doesn't depend on anything: we just load it once
let stable = create_resource(cx, || (), |_| async move { load_data(1).await });
let stable = create_resource(|| (), |_| async move { load_data(1).await });
// we can access the resource values with .read()
// this will reactively return None before the Future has resolved
// and update to Some(T) when it has resolved
let async_result = move || {
async_data
.read(cx)
.read()
.map(|value| format!("Server returned {value:?}"))
// This loading state will only show before the first load
.unwrap_or_else(|| "Loading...".into())
@@ -108,7 +105,7 @@ fn App(cx: Scope) -> impl IntoView {
let loading = async_data.loading();
let is_loading = move || if loading() { "Loading..." } else { "Idle." };
view! { cx,
view! {
<button
on:click=move |_| {
set_count.update(|n| *n += 1);
@@ -117,7 +114,7 @@ fn App(cx: Scope) -> impl IntoView {
"Click me"
</button>
<p>
<code>"stable"</code>": " {move || stable.read(cx)}
<code>"stable"</code>": " {move || stable.read()}
</p>
<p>
<code>"count"</code>": " {count}
@@ -132,7 +129,7 @@ fn App(cx: Scope) -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(|| view! { <App/> })
}
```

View File

@@ -3,14 +3,14 @@
In the previous chapter, we showed how you can create a simple loading screen to show some fallback while a resource is loading.
```rust
let (count, set_count) = create_signal(cx, 0);
let a = create_resource(cx, count, |count| async move { load_a(count).await });
let (count, set_count) = create_signal(0);
let a = create_resource(count, |count| async move { load_a(count).await });
view! { cx,
view! {
<h1>"My Data"</h1>
{move || match once.read(cx) {
None => view! { cx, <p>"Loading..."</p> }.into_view(cx),
Some(data) => view! { cx, <ShowData data/> }.into_view(cx)
{move || match once.read() {
None => view! { <p>"Loading..."</p> }.into_view(),
Some(data) => view! { <ShowData data/> }.into_view()
}}
}
```
@@ -18,19 +18,19 @@ view! { cx,
But what if we have two resources, and want to wait for both of them?
```rust
let (count, set_count) = create_signal(cx, 0);
let (count2, set_count2) = create_signal(cx, 0);
let a = create_resource(cx, count, |count| async move { load_a(count).await });
let b = create_resource(cx, count2, |count| async move { load_b(count).await });
let (count, set_count) = create_signal(0);
let (count2, set_count2) = create_signal(0);
let a = create_resource(count, |count| async move { load_a(count).await });
let b = create_resource(count2, |count| async move { load_b(count).await });
view! { cx,
view! {
<h1>"My Data"</h1>
{move || match (a.read(cx), b.read(cx)) {
(Some(a), Some(b)) => view! { cx,
{move || match (a.read(), b.read()) {
(Some(a), Some(b)) => view! {
<ShowA a/>
<ShowA b/>
}.into_view(cx),
_ => view! { cx, <p>"Loading..."</p> }.into_view(cx)
}.into_view(),
_ => view! { <p>"Loading..."</p> }.into_view()
}}
}
```
@@ -40,26 +40,26 @@ Thats not _so_ bad, but its kind of annoying. What if we could invert the
The [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) component lets us do exactly that. You give it a `fallback` prop and children, one or more of which usually involves reading from a resource. Reading from a resource “under” a `<Suspense/>` (i.e., in one of its children) registers that resource with the `<Suspense/>`. If its still waiting for resources to load, it shows the `fallback`. When theyve all loaded, it shows the children.
```rust
let (count, set_count) = create_signal(cx, 0);
let (count2, set_count2) = create_signal(cx, 0);
let a = create_resource(cx, count, |count| async move { load_a(count).await });
let b = create_resource(cx, count2, |count| async move { load_b(count).await });
let (count, set_count) = create_signal(0);
let (count2, set_count2) = create_signal(0);
let a = create_resource(count, |count| async move { load_a(count).await });
let b = create_resource(count2, |count| async move { load_b(count).await });
view! { cx,
view! {
<h1>"My Data"</h1>
<Suspense
fallback=move || view! { cx, <p>"Loading..."</p> }
fallback=move || view! { <p>"Loading..."</p> }
>
<h2>"My Data"</h2>
<h3>"A"</h3>
{move || {
a.read(cx)
.map(|a| view! { cx, <ShowA a/> })
a.read()
.map(|a| view! { <ShowA a/> })
}}
<h3>"B"</h3>
{move || {
b.read(cx)
.map(|b| view! { cx, <ShowB b/> })
b.read()
.map(|b| view! { <ShowB b/> })
}}
</Suspense>
}
@@ -84,10 +84,10 @@ async fn fetch_monkeys(monkey: i32) -> i32 {
// maybe this didn't need to be async
monkey * 2
}
view! { cx,
view! {
<Await
// `future` provides the `Future` to be resolved
future=|cx| fetch_monkeys(3)
future=|| fetch_monkeys(3)
// the data is bound to whatever variable name you provide
bind:data
>
@@ -114,17 +114,17 @@ async fn important_api_call(name: String) -> String {
}
#[component]
fn App(cx: Scope) -> impl IntoView {
let (name, set_name) = create_signal(cx, "Bill".to_string());
fn App() -> impl IntoView {
let (name, set_name) = create_signal("Bill".to_string());
// this will reload every time `name` changes
let async_data = create_resource(
cx,
name,
|name| async move { important_api_call(name).await },
);
view! { cx,
view! {
<input
on:input=move |ev| {
set_name(event_target_value(&ev));
@@ -135,20 +135,20 @@ fn App(cx: Scope) -> impl IntoView {
<Suspense
// the fallback will show whenever a resource
// read "under" the suspense is loading
fallback=move || view! { cx, <p>"Loading..."</p> }
fallback=move || view! { <p>"Loading..."</p> }
>
// the children will be rendered once initially,
// and then whenever any resources has been resolved
<p>
"Your shouting name is "
{move || async_data.read(cx)}
{move || async_data.read()}
</p>
</Suspense>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(|| view! { <App/> })
}
```

View File

@@ -29,13 +29,13 @@ async fn important_api_call(id: usize) -> String {
}
#[component]
fn App(cx: Scope) -> impl IntoView {
let (tab, set_tab) = create_signal(cx, 0);
fn App() -> impl IntoView {
let (tab, set_tab) = create_signal(0);
// this will reload every time `tab` changes
let user_data = create_resource(cx, tab, |tab| async move { important_api_call(tab).await });
let user_data = create_resource(tab, |tab| async move { important_api_call(tab).await });
view! { cx,
view! {
<div class="buttons">
<button
on:click=move |_| set_tab(0)
@@ -65,17 +65,17 @@ fn App(cx: Scope) -> impl IntoView {
// the fallback will show initially
// on subsequent reloads, the current child will
// continue showing
fallback=move || view! { cx, <p>"Loading..."</p> }
fallback=move || view! { <p>"Loading..."</p> }
>
<p>
{move || user_data.read(cx)}
{move || user_data.read()}
</p>
</Transition>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(|| view! { <App/> })
}
```

View File

@@ -16,22 +16,22 @@ async fn add_todo_request(new_title: &str) -> Uuid {
}
```
`create_action` takes a reactive `Scope` and an `async` function that takes a reference to a single argument, which you could think of as its “input type.”
`create_action` takes an `async` function that takes a reference to a single argument, which you could think of as its “input type.”
> The input is always a single type. If you want to pass in multiple arguments, you can do it with a struct or tuple.
>
> ```rust
> // if there's a single argument, just use that
> let action1 = create_action(cx, |input: &String| {
> let action1 = create_action(|input: &String| {
> let input = input.clone();
> async move { todo!() }
> });
>
> // if there are no arguments, use the unit type `()`
> let action2 = create_action(cx, |input: &()| async { todo!() });
> let action2 = create_action(|input: &()| async { todo!() });
>
> // if there are multiple arguments, use a tuple
> let action3 = create_action(cx,
> let action3 = create_action(
> |input: &(usize, String)| async { todo!() }
> );
> ```
@@ -41,7 +41,7 @@ async fn add_todo_request(new_title: &str) -> Uuid {
So in this case, all we need to do to create an action is
```rust
let add_todo_action = create_action(cx, |input: &String| {
let add_todo_action = create_action(|input: &String| {
let input = input.to_owned();
async move { add_todo_request(&input).await }
});
@@ -66,9 +66,9 @@ let todo_id = add_todo_action.value(); // RwSignal<Option<Uuid>>
This makes it easy to track the current state of your request, show a loading indicator, or do “optimistic UI” based on the assumption that the submission will succeed.
```rust
let input_ref = create_node_ref::<Input>(cx);
let input_ref = create_node_ref::<Input>();
view! { cx,
view! {
<form
on:submit=move |ev| {
ev.prevent_default(); // don't reload the page...
@@ -116,10 +116,10 @@ async fn add_todo(text: &str) -> Uuid {
}
#[component]
fn App(cx: Scope) -> impl IntoView {
fn App() -> impl IntoView {
// an action takes an async function with single argument
// it can be a simple type, a struct, or ()
let add_todo = create_action(cx, |input: &String| {
let add_todo = create_action(|input: &String| {
// the input is a reference, but we need the Future to own it
// this is important: we need to clone and move into the Future
// so it has a 'static lifetime
@@ -133,9 +133,9 @@ fn App(cx: Scope) -> impl IntoView {
let pending = add_todo.pending();
let todo_id = add_todo.value();
let input_ref = create_node_ref::<Input>(cx);
let input_ref = create_node_ref::<Input>();
view! { cx,
view! {
<form
on:submit=move |ev| {
ev.prevent_default(); // don't reload the page...
@@ -168,7 +168,7 @@ fn App(cx: Scope) -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(|| view! { <App/> })
}
```

View File

@@ -7,12 +7,12 @@ As you build components you may occasionally find yourself wanting to “project
Consider the following:
```rust
pub fn LoggedIn<F, IV>(cx: Scope, fallback: F, children: ChildrenFn) -> impl IntoView
pub fn LoggedIn<F, IV>(fallback: F, children: ChildrenFn) -> impl IntoView
where
F: Fn(Scope) -> IV + 'static,
F: Fn() -> IV + 'static,
IV: IntoView,
{
view! { cx,
view! {
<Suspense
fallback=|| ()
>
@@ -22,7 +22,7 @@ where
when=move || todo!()
fallback=fallback
>
{children(cx)}
{children()}
</Show>
</Suspense>
}
@@ -50,18 +50,18 @@ If you want to really understand the issue here, it may help to look at the expa
```rust
Suspense(
cx,
::leptos::component_props_builder(&Suspense)
.fallback(|| ())
.children({
// fallback and children are moved into this closure
Box::new(move |cx| {
Box::new(move || {
{
// fallback and children captured here
leptos::Fragment::lazy(|| {
vec![
(Show(
cx,
::leptos::component_props_builder(&Show)
.when(|| true)
// but fallback is moved into Show here
@@ -70,7 +70,7 @@ Suspense(
.children(children)
.build(),
)
.into_view(cx)),
.into_view()),
]
})
}
@@ -91,22 +91,22 @@ We can solve this problem by using the [`store_value`](https://docs.rs/leptos/la
In this case, its really simple:
```rust
pub fn LoggedIn<F, IV>(cx: Scope, fallback: F, children: ChildrenFn) -> impl IntoView
pub fn LoggedIn<F, IV>(F, children: ChildrenFn) -> impl IntoView
where
F: Fn(Scope) -> IV + 'static,
F: Fn() -> IV + 'static,
IV: IntoView,
{
let fallback = store_value(cx, fallback);
let children = store_value(cx, children);
view! { cx,
let fallback = store_value(fallback);
let children = store_value(children);
view! {
<Suspense
fallback=|| ()
>
<Show
when=|| todo!()
fallback=move |cx| fallback.with_value(|fallback| fallback(cx))
fallback=move || fallback.with_value(|fallback| fallback())
>
{children.with_value(|children| children(cx))}
{children.with_value(|children| children())}
</Show>
</Suspense>
}
@@ -125,9 +125,9 @@ Consider this example
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
pub fn App() -> impl IntoView {
let name = "Alice".to_string();
view! { cx,
view! {
<Outer>
<Inner>
<Inmost name=name.clone()/>
@@ -137,18 +137,18 @@ pub fn App(cx: Scope) -> impl IntoView {
}
#[component]
pub fn Outer(cx: Scope, children: ChildrenFn) -> impl IntoView {
children(cx)
pub fn Outer(ChildrenFn) -> impl IntoView {
children()
}
#[component]
pub fn Inner(cx: Scope, children: ChildrenFn) -> impl IntoView {
children(cx)
pub fn Inner(ChildrenFn) -> impl IntoView {
children()
}
#[component]
pub fn Inmost(cx: Scope, name: String) -> impl IntoView {
view! { cx,
pub fn Inmost(ng) -> impl IntoView {
view! {
<p>{name}</p>
}
}
@@ -165,7 +165,7 @@ Its captured through multiple levels of children that need to run more than o
In this case, the `clone:` syntax comes in handy. Calling `clone:name` will clone `name` _before_ moving it into `<Inner/>`s children, which solves our ownership issue.
```rust
view! { cx,
view! {
<Outer>
<Inner clone:name>
<Inmost name=name.clone()/>

View File

@@ -14,10 +14,10 @@ This allows you to write components like this:
```rust
#[component]
fn Home(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
fn Home() -> impl IntoView {
let (count, set_count) = create_signal(0);
view! { cx,
view! {
<main class="my-0 mx-auto max-w-3xl text-center">
<h2 class="p-6 text-4xl">"Welcome to Leptos with Tailwind"</h2>
<p class="px-10 pb-10 text-left">"Tailwind will scan your Rust files for Tailwind class names and compile them into a CSS file."</p>
@@ -48,7 +48,7 @@ This allows you to write components like this:
use stylers::style;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
pub fn App() -> impl IntoView {
let styler_class = style! { "App",
#two{
color: blue;
@@ -74,7 +74,7 @@ pub fn App(cx: Scope) -> impl IntoView {
}
};
view! { cx, class = styler_class,
view! { class = styler_class,
<div class="one">
<h1 id="two">"Hello"</h1>
<h2>"World"</h2>
@@ -93,7 +93,7 @@ pub fn App(cx: Scope) -> impl IntoView {
use styled::style;
#[component]
pub fn MyComponent(cx: Scope) -> impl IntoView {
pub fn MyComponent() -> impl IntoView {
let styles = style!(
div {
background-color: red;
@@ -101,7 +101,7 @@ pub fn MyComponent(cx: Scope) -> impl IntoView {
}
);
styled::view! { cx, styles,
styled::view! { styles,
<div>"This text should be red with white text."</div>
}
}

View File

@@ -3,12 +3,13 @@
[`<ActionForm/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.ActionForm.html) is a specialized `<Form/>` that takes a server action, and automatically dispatches it on form submission. This allows you to call a server function directly from a `<form>`, even without JS/WASM.
The process is simple:
1. Define a server function using the [`#[server]` macro](https://docs.rs/leptos/latest/leptos/attr.server.html) (see [Server Functions](../server/25_server_functions.md).)
2. Create an action using [`create_server_action`](https://docs.rs/leptos/latest/leptos/fn.create_server_action.html), specifying the type of the server function youve defined.
3. Create an `<ActionForm/>`, providing the server action in the `action` prop.
4. Pass the named arguments to the server function as form fields with the same names.
> **Note:** `<ActionForm/>` only works with the default URL-encoded `POST` encoding for server functions, to ensure graceful degradation/correct behavior as an HTML form.
> **Note:** `<ActionForm/>` only works with the default URL-encoded `POST` encoding for server functions, to ensure graceful degradation/correct behavior as an HTML form.
```rust
#[server(AddTodo, "/api")]
@@ -17,14 +18,14 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
}
#[component]
fn AddTodo(cx: Scope) -> impl IntoView {
let add_todo = create_server_action::<AddTodo>(cx);
fn AddTodo() -> impl IntoView {
let add_todo = create_server_action::<AddTodo>();
// holds the latest *returned* value from the server
let value = add_todo.value();
// check if the server has returned an error
let has_error = move || value.with(|val| matches!(val, Some(Err(_))));
view! { cx,
view! {
<ActionForm action=add_todo>
<label>
"Add a Todo"
@@ -36,6 +37,7 @@ fn AddTodo(cx: Scope) -> impl IntoView {
}
}
```
Its really that easy. With JS/WASM, your form will submit without a page reload, storing its most recent submission in the `.input()` signal of the action, its pending status in `.pending()`, and so on. (See the [`Action`](https://docs.rs/leptos/latest/leptos/struct.Action.html) docs for a refresher, if you need.) Without JS/WASM, your form will submit with a page reload. If you call a `redirect` function (from `leptos_axum` or `leptos_actix`) it will redirect to the correct page. By default, it will redirect back to the page youre currently on. The power of HTML, HTTP, and isomorphic rendering mean that your `<ActionForm/>` simply works, even with no JS/WASM.
## Client-Side Validation
@@ -53,4 +55,4 @@ let on_submit = move |ev| {
ev.prevent_default();
}
}
```
```

View File

@@ -9,10 +9,10 @@ Hidden behind the whole reactive DOM renderer that weve seen so far is a func
[`create_effect`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_effect.html) takes a function as its argument. It immediately runs the function. If you access any reactive signal inside that function, it registers the fact that the effect depends on that signal with the reactive runtime. Whenever one of the signals that the effect depends on changes, the effect runs again.
```rust
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, 0);
let (a, set_a) = create_signal(0);
let (b, set_b) = create_signal(0);
create_effect(cx, move |_| {
create_effect(move |_| {
// immediately prints "Value: 0" and subscribes to `a`
log::debug!("Value: {}", a());
});
@@ -42,15 +42,15 @@ While theyre not a “zero-cost abstraction” in the most technical sense—
Imagine that Im creating some kind of chat software, and I want people to be able to display their full name, or just their first name, and to notify the server whenever their name changes:
```rust
let (first, set_first) = create_signal(cx, String::new());
let (last, set_last) = create_signal(cx, String::new());
let (use_last, set_use_last) = create_signal(cx, true);
let (first, set_first) = create_signal(String::new());
let (last, set_last) = create_signal(String::new());
let (use_last, set_use_last) = create_signal(true);
// this will add the name to the log
// any time one of the source signals changes
create_effect(cx, move |_| {
create_effect(move |_| {
log(
cx,
if use_last() {
format!("{} {}", first(), last())
} else {
@@ -77,9 +77,9 @@ If you need to synchronize some reactive value with the non-reactive world outsi
Weve managed to get this far without mentioning effects because theyre built into the Leptos DOM renderer. Weve seen that you can create a signal and pass it into the `view` macro, and it will update the relevant DOM node whenever the signal changes:
```rust
let (count, set_count) = create_signal(cx, 0);
let (count, set_count) = create_signal(0);
view! { cx,
view! {
<p>{count}</p>
}
```
@@ -87,13 +87,13 @@ view! { cx,
This works because the framework essentially creates an effect wrapping this update. You can imagine Leptos translating this view into something like this:
```rust
let (count, set_count) = create_signal(cx, 0);
let (count, set_count) = create_signal(0);
// create a DOM element
let p = create_element("p");
// create an effect to reactively update the text
create_effect(cx, move |prev_value| {
create_effect(move |prev_value| {
// first, access the signals value and convert it to a string
let text = count().to_string();
@@ -119,10 +119,10 @@ In addition to `create_effect`, Leptos provides a [`watch`](https://docs.rs/lept
Like `create_resource`, `watch` takes a first argument, which is reactively tracked, and a second, which is not. Whenever a reactive value in its `deps` argument is changed, the `callback` is run. `watch` returns a function that can be called to stop tracking the dependencies.
```rust
let (num, set_num) = create_signal(cx, 0);
let (num, set_num) = create_signal(0);
let stop = watch(
cx,
move || num.get(),
move |num, prev_num, _| {
log::debug!("Number: {}; Prev: {:?}", num, prev_num);
@@ -149,30 +149,30 @@ use leptos::html::Input;
use leptos::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
fn App() -> impl IntoView {
// Just making a visible log here
// You can ignore this...
let log = create_rw_signal::<Vec<String>>(cx, vec![]);
let log = create_rw_signal::<Vec<String>>(vec![]);
let logged = move || log().join("\n");
provide_context(cx, log);
provide_context(log);
view! { cx,
view! {
<CreateAnEffect/>
<pre>{logged}</pre>
}
}
#[component]
fn CreateAnEffect(cx: Scope) -> impl IntoView {
let (first, set_first) = create_signal(cx, String::new());
let (last, set_last) = create_signal(cx, String::new());
let (use_last, set_use_last) = create_signal(cx, true);
fn CreateAnEffect() -> impl IntoView {
let (first, set_first) = create_signal(String::new());
let (last, set_last) = create_signal(String::new());
let (use_last, set_use_last) = create_signal(true);
// this will add the name to the log
// any time one of the source signals changes
create_effect(cx, move |_| {
create_effect(move |_| {
log(
cx,
if use_last() {
format!("{} {}", first(), last())
} else {
@@ -181,7 +181,7 @@ fn CreateAnEffect(cx: Scope) -> impl IntoView {
)
});
view! { cx,
view! {
<h1><code>"create_effect"</code> " Version"</h1>
<form>
<label>
@@ -207,14 +207,14 @@ fn CreateAnEffect(cx: Scope) -> impl IntoView {
}
#[component]
fn ManualVersion(cx: Scope) -> impl IntoView {
let first = create_node_ref::<Input>(cx);
let last = create_node_ref::<Input>(cx);
let use_last = create_node_ref::<Input>(cx);
fn ManualVersion() -> impl IntoView {
let first = create_node_ref::<Input>();
let last = create_node_ref::<Input>();
let use_last = create_node_ref::<Input>();
let mut prev_name = String::new();
let on_change = move |_| {
log(cx, " listener");
log(" listener");
let first = first.get().unwrap();
let last = last.get().unwrap();
let use_last = use_last.get().unwrap();
@@ -225,12 +225,12 @@ fn ManualVersion(cx: Scope) -> impl IntoView {
};
if this_one != prev_name {
log(cx, &this_one);
log(&this_one);
prev_name = this_one;
}
};
view! { cx,
view! {
<h1>"Manual Version"</h1>
<form on:change=on_change>
<label>
@@ -257,12 +257,12 @@ fn ManualVersion(cx: Scope) -> impl IntoView {
}
#[component]
fn EffectVsDerivedSignal(cx: Scope) -> impl IntoView {
let (my_value, set_my_value) = create_signal(cx, String::new());
fn EffectVsDerivedSignal() -> impl IntoView {
let (my_value, set_my_value) = create_signal(String::new());
// Don't do this.
/*let (my_optional_value, set_optional_my_value) = create_signal(cx, Option::<String>::None);
/*let (my_optional_value, set_optional_my_value) = create_signal(Option::<String>::None);
create_effect(cx, move |_| {
create_effect(move |_| {
if !my_value.get().is_empty() {
set_optional_my_value(Some(my_value.get()));
} else {
@@ -274,7 +274,7 @@ fn EffectVsDerivedSignal(cx: Scope) -> impl IntoView {
let my_optional_value =
move || (!my_value.with(String::is_empty)).then(|| Some(my_value.get()));
view! { cx,
view! {
<input
prop:value=my_value
on:input= move |ev| set_my_value(event_target_value(&ev))
@@ -286,7 +286,7 @@ fn EffectVsDerivedSignal(cx: Scope) -> impl IntoView {
<code>
<Show
when=move || my_optional_value().is_some()
fallback=|cx| view! { cx, "None" }
fallback=|| view! { "None" }
>
"Some(\"" {my_optional_value().unwrap()} "\")"
</Show>
@@ -298,9 +298,9 @@ fn EffectVsDerivedSignal(cx: Scope) -> impl IntoView {
/*#[component]
pub fn Show<F, W, IV>(
/// The scope the component is running in
cx: Scope,
/// The components Show wraps
children: Box<dyn Fn(Scope) -> Fragment>,
children: Box<dyn Fn() -> Fragment>,
/// A closure that returns a bool that determines whether this thing runs
when: W,
/// A closure that returns what gets rendered if the when statement is false
@@ -308,24 +308,24 @@ pub fn Show<F, W, IV>(
) -> impl IntoView
where
W: Fn() -> bool + 'static,
F: Fn(Scope) -> IV + 'static,
F: Fn() -> IV + 'static,
IV: IntoView,
{
let memoized_when = create_memo(cx, move |_| when());
let memoized_when = create_memo(move |_| when());
move || match memoized_when.get() {
true => children(cx).into_view(cx),
false => fallback(cx).into_view(cx),
true => children().into_view(),
false => fallback().into_view(),
}
}*/
fn log(cx: Scope, msg: impl std::fmt::Display) {
let log = use_context::<RwSignal<Vec<String>>>(cx).unwrap();
fn log(std::fmt::Display) {
let log = use_context::<RwSignal<Vec<String>>>().unwrap();
log.update(|log| log.push(msg.to_string()));
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(|| view! { <App/> })
}
```

View File

@@ -6,7 +6,7 @@ application. It sometimes looks a little silly:
```rust
// a signal holds a value, and can be updated
let (count, set_count) = create_signal(cx, 0);
let (count, set_count) = create_signal(0);
// a derived signal is a function that accesses other signals
let double_count = move || count() * 2;
@@ -19,11 +19,11 @@ let text = move || if count_is_odd() {
// an effect automatically tracks the signals it depends on
// and reruns when they change
create_effect(cx, move |_| {
create_effect(move |_| {
log!("text = {}", text());
});
view! { cx,
view! {
<p>{move || text().to_uppercase()}</p>
}
```
@@ -53,12 +53,12 @@ Take our typical `<SimpleCounter/>` example in its simplest form:
```rust
#[component]
pub fn SimpleCounter(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, 0);
pub fn SimpleCounter() -> impl IntoView {
let (value, set_value) = create_signal(0);
let increment = move |_| set_value.update(|value| *value += 1);
view! { cx,
view! {
<button on:click=increment>
{value}
</button>

View File

@@ -14,7 +14,7 @@ There are four basic signal operations:
Calling a `ReadSignal` as a function is syntax sugar for `.get()`. Calling a `WriteSignal` as a function is syntax sugar for `.set()`. So
```rust
let (count, set_count) = create_signal(cx, 0);
let (count, set_count) = create_signal(0);
set_count(1);
log!(count());
```
@@ -22,7 +22,7 @@ log!(count());
is the same as
```rust
let (count, set_count) = create_signal(cx, 0);
let (count, set_count) = create_signal(0);
set_count.set(1);
log!(count.get());
```
@@ -36,7 +36,7 @@ However, there are some very good use cases for `.with()` and `.update()`.
For example, consider a signal that holds a `Vec<String>`.
```rust
let (names, set_names) = create_signal(cx, Vec::new());
let (names, set_names) = create_signal(Vec::new());
if names().is_empty() {
set_names(vec!["Alice".to_string()]);
}
@@ -47,7 +47,7 @@ In terms of logic, this is simple enough, but its hiding some significant ine
Likewise, `set_names` replaces the value with a whole new `Vec<_>`. This is fine, but we might as well just mutate the original `Vec<_>` in place.
```rust
let (names, set_names) = create_signal(cx, Vec::new());
let (names, set_names) = create_signal(Vec::new());
if names.with(|names| names.is_empty()) {
set_names.update(|names| names.push("Alice".to_string()));
}
@@ -70,33 +70,39 @@ After all, `.with()` simply takes a function that takes the value by reference.
Often people ask about situations in which some signal needs to change based on some other signals value. There are three good ways to do this, and one thats less than ideal but okay under controlled circumstances.
### Good Options
**1) B is a function of A.** Create a signal for A and a derived signal or memo for B.
```rust
let (count, set_count) = create_signal(cx, 1);
let (count, set_count) = create_signal(1);
let derived_signal_double_count = move || count() * 2;
let memoized_double_count = create_memo(cx, move |_| count() * 2);
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(cx, "Bridget".to_string());
let (last_name, set_last_name) = create_signal(cx, "Jones".to_string());
let (first_name, set_first_name) = create_signal("Bridget".to_string());
let (last_name, set_last_name) = create_signal("Jones".to_string());
let full_name = move || format!("{} {}", first_name(), last_name());
```
**3) A and B are independent signals, but sometimes updated at the same time.** When you make the call to update A, make a separate call to update B.
```rust
let (age, set_age) = create_signal(cx, 32);
let (favorite_number, set_favorite_number) = create_signal(cx, 42);
let (age, set_age) = create_signal(32);
let (favorite_number, set_favorite_number) = create_signal(42);
// use this to handle a click on a `Clear` button
let clear_handler = move |_| {
set_age(0);
set_favorite_number(0);
};
```
### If you really must...
**4) Create an effect to write to B whenever A changes.** This is officially discouraged, for several reasons:
a) It will always be less efficient, as it means every time A updates you do two full trips through the reactive process. (You set A, which causes the effect to run, as well as any other effects that depend on A. Then you set B, which causes any effects that depend on B to run.)
b) It increases your chances of accidentally creating things like infinite loops or over-re-running effects. This is the kind of ping-ponging, reactive spaghetti code that was common in the early 2010s and that we try to avoid with things like read-write segregation and discouraging writing to signals from effects.

View File

@@ -33,8 +33,8 @@ use leptos::*;
use leptos_router::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! { cx,
pub fn App() -> impl IntoView {
view! {
<Router>
<nav>
/* ... */
@@ -58,8 +58,8 @@ use leptos::*;
use leptos_router::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! { cx,
pub fn App() -> impl IntoView {
view! {
<Router>
<nav>
/* ... */
@@ -83,18 +83,18 @@ The `path` can include
- dynamic, named parameters beginning with a colon (`/:id`),
- and/or a wildcard beginning with an asterisk (`/user/*any`)
The `view` is a function that takes a `Scope` and returns a view.
The `view` is a function that returns a view. Any component with no props works here, as does a closure that returns some view.
```rust
<Routes>
<Route path="/" view=Home/>
<Route path="/users" view=Users/>
<Route path="/users/:id" view=UserProfile/>
<Route path="/*any" view=NotFound/>
<Route path="/*any" view=|| view! { <h1>"Not Found"</h1> }/>
</Routes>
```
> `view` takes a `Fn(Scope) -> impl IntoView`. If a component has no props, it is a function that takes `Scope` and returns `impl IntoView`, so it can be passed directly into the `view`. In this case, `view=Home` is just a shorthand for `|cx| view! { cx, <Home/> }`.
> `view` takes a `Fn() -> impl IntoView`. If a component has no props, it can be passed directly into the `view`. In this case, `view=Home` is just a shorthand for `|| view! { <Home/> }`.
Now if you navigate to `/` or to `/users` youll get the home page or the `<Users/>`. If you go to `/users/3` or `/blahblah` youll get a user profile or your 404 page (`<NotFound/>`). On every navigation, the router determines which `<Route/>` should be matched, and therefore what content should be displayed where the `<Routes/>` component is defined.

View File

@@ -96,7 +96,7 @@ You can easily define this with nested routes
<Routes>
<Route path="/contacts" view=ContactList>
<Route path=":id" view=ContactInfo/>
<Route path="" view=|cx| view! { cx,
<Route path="" view=|| view! {
<p>"Select a contact to view more info."</p>
}/>
</Route>
@@ -113,7 +113,7 @@ You can go even deeper. Say you want to have tabs for each contacts address,
<Route path="address" view=Address/>
<Route path="messages" view=Messages/>
</Route>
<Route path="" view=|cx| view! { cx,
<Route path="" view=|| view! {
<p>"Select a contact to view more info."</p>
}/>
</Route>
@@ -135,15 +135,15 @@ Thats all! But its important to know and to remember, because its a com
```rust
#[component]
pub fn ContactList(cx: Scope) -> impl IntoView {
pub fn ContactList() -> impl IntoView {
let contacts = todo!();
view! { cx,
view! {
<div style="display: flex">
// the contact list
<For each=contacts
key=|contact| contact.id
view=|cx, contact| todo!()
view=|contact| todo!()
>
// the nested child, if any
// dont forget this!
@@ -179,8 +179,8 @@ use leptos::*;
use leptos_router::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
fn App() -> impl IntoView {
view! {
<Router>
<h1>"Contact App"</h1>
// this <nav> will show on every routes,
@@ -195,28 +195,29 @@ fn App(cx: Scope) -> impl IntoView {
<main>
<Routes>
// / just has an un-nested "Home"
<Route path="/" view=|cx| view! { cx,
<Route path="/" view=|| view! {
<h3>"Home"</h3>
}/>
// /contacts has nested routes
<Route
path="/contacts"
view=ContactList
>
// if no id specified, fall back
<Route path=":id" view=ContactInfo>
<Route path="" view=|cx| view! { cx,
<Route path="" view=|| view! {
<div class="tab">
"(Contact Info)"
</div>
}/>
<Route path="conversations" view=|cx| view! { cx,
<Route path="conversations" view=|| view! {
<div class="tab">
"(Conversations)"
</div>
}/>
</Route>
// if no id specified, fall back
<Route path="" view=|cx| view! { cx,
<Route path="" view=|| view! {
<div class="select-user">
"Select a user to view contact info."
</div>
@@ -229,8 +230,8 @@ fn App(cx: Scope) -> impl IntoView {
}
#[component]
fn ContactList(cx: Scope) -> impl IntoView {
view! { cx,
fn ContactList() -> impl IntoView {
view! {
<div class="contact-list">
// here's our contact list component itself
<div class="contact-list-contacts">
@@ -249,9 +250,9 @@ fn ContactList(cx: Scope) -> impl IntoView {
}
#[component]
fn ContactInfo(cx: Scope) -> impl IntoView {
fn ContactInfo() -> impl IntoView {
// we can access the :id param reactively with `use_params_map`
let params = use_params_map(cx);
let params = use_params_map();
let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default());
// imagine we're loading data from an API here
@@ -262,7 +263,7 @@ fn ContactInfo(cx: Scope) -> impl IntoView {
_ => "User not found.",
};
view! { cx,
view! {
<div class="contact-info">
<h4>{name}</h4>
<div class="tabs">
@@ -278,7 +279,7 @@ fn ContactInfo(cx: Scope) -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(|| view! { <App/> })
}
```

View File

@@ -50,8 +50,8 @@ Now we can use them in a component. Imagine a URL that has both params and a que
The typed versions return `Memo<Result<T, _>>`. Its a Memo so it reacts to changes in the URL. Its a `Result` because the params or query need to be parsed from the URL, and may or may not be valid.
```rust
let params = use_params::<ContactParams>(cx);
let query = use_query::<ContactSearch>(cx);
let params = use_params::<ContactParams>();
let query = use_query::<ContactSearch>();
// id: || -> usize
let id = move || {
@@ -66,8 +66,8 @@ let id = move || {
The untyped versions return `Memo<ParamsMap>`. Again, its memo to react to changes in the URL. [`ParamsMap`](https://docs.rs/leptos_router/0.2.3/leptos_router/struct.ParamsMap.html) behaves a lot like any other map type, with a `.get()` method that returns `Option<&String>`.
```rust
let params = use_params_map(cx);
let query = use_query_map(cx);
let params = use_params_map();
let query = use_query_map();
// id: || -> Option<String>
let id = move || {
@@ -94,8 +94,8 @@ use leptos::*;
use leptos_router::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
fn App() -> impl IntoView {
view! {
<Router>
<h1>"Contact App"</h1>
// this <nav> will show on every routes,
@@ -110,7 +110,7 @@ fn App(cx: Scope) -> impl IntoView {
<main>
<Routes>
// / just has an un-nested "Home"
<Route path="/" view=|cx| view! { cx,
<Route path="/" view=|| view! {
<h3>"Home"</h3>
}/>
// /contacts has nested routes
@@ -120,19 +120,19 @@ fn App(cx: Scope) -> impl IntoView {
>
// if no id specified, fall back
<Route path=":id" view=ContactInfo>
<Route path="" view=|cx| view! { cx,
<Route path="" view=|| view! {
<div class="tab">
"(Contact Info)"
</div>
}/>
<Route path="conversations" view=|cx| view! { cx,
<Route path="conversations" view=|| view! {
<div class="tab">
"(Conversations)"
</div>
}/>
</Route>
// if no id specified, fall back
<Route path="" view=|cx| view! { cx,
<Route path="" view=|| view! {
<div class="select-user">
"Select a user to view contact info."
</div>
@@ -145,8 +145,8 @@ fn App(cx: Scope) -> impl IntoView {
}
#[component]
fn ContactList(cx: Scope) -> impl IntoView {
view! { cx,
fn ContactList() -> impl IntoView {
view! {
<div class="contact-list">
// here's our contact list component itself
<div class="contact-list-contacts">
@@ -165,9 +165,9 @@ fn ContactList(cx: Scope) -> impl IntoView {
}
#[component]
fn ContactInfo(cx: Scope) -> impl IntoView {
fn ContactInfo() -> impl IntoView {
// we can access the :id param reactively with `use_params_map`
let params = use_params_map(cx);
let params = use_params_map();
let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default());
// imagine we're loading data from an API here
@@ -178,7 +178,7 @@ fn ContactInfo(cx: Scope) -> impl IntoView {
_ => "User not found.",
};
view! { cx,
view! {
<div class="contact-info">
<h4>{name}</h4>
<div class="tabs">
@@ -194,7 +194,7 @@ fn ContactInfo(cx: Scope) -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(|| view! { <App/> })
}
```

View File

@@ -23,8 +23,9 @@ The router also provides an [`<A>`](https://docs.rs/leptos_router/latest/leptos_
Your most-used methods of navigating between pages should be with `<a>` and `<form>` elements or with the enhanced `<A/>` and `<Form/>` components. Using links and forms to navigate is the best solution for accessibility and graceful degradation.
On occasion, though, youll want to navigate programmatically, i.e., call a function that can navigate to a new page. In that case, you should use the [`use_navigate`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_navigate.html) function.
```rust
let navigate = leptos_router::use_navigate(cx);
let navigate = leptos_router::use_navigate();
navigate("/somewhere", Default::default());
```
@@ -46,8 +47,8 @@ use leptos::*;
use leptos_router::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
fn App() -> impl IntoView {
view! {
<Router>
<h1>"Contact App"</h1>
// this <nav> will show on every routes,
@@ -62,7 +63,7 @@ fn App(cx: Scope) -> impl IntoView {
<main>
<Routes>
// / just has an un-nested "Home"
<Route path="/" view=|cx| view! { cx,
<Route path="/" view=|| view! {
<h3>"Home"</h3>
}/>
// /contacts has nested routes
@@ -72,19 +73,19 @@ fn App(cx: Scope) -> impl IntoView {
>
// if no id specified, fall back
<Route path=":id" view=ContactInfo>
<Route path="" view=|cx| view! { cx,
<Route path="" view=|| view! {
<div class="tab">
"(Contact Info)"
</div>
}/>
<Route path="conversations" view=|cx| view! { cx,
<Route path="conversations" view=|| view! {
<div class="tab">
"(Conversations)"
</div>
}/>
</Route>
// if no id specified, fall back
<Route path="" view=|cx| view! { cx,
<Route path="" view=|| view! {
<div class="select-user">
"Select a user to view contact info."
</div>
@@ -97,8 +98,8 @@ fn App(cx: Scope) -> impl IntoView {
}
#[component]
fn ContactList(cx: Scope) -> impl IntoView {
view! { cx,
fn ContactList() -> impl IntoView {
view! {
<div class="contact-list">
// here's our contact list component itself
<div class="contact-list-contacts">
@@ -117,9 +118,9 @@ fn ContactList(cx: Scope) -> impl IntoView {
}
#[component]
fn ContactInfo(cx: Scope) -> impl IntoView {
fn ContactInfo() -> impl IntoView {
// we can access the :id param reactively with `use_params_map`
let params = use_params_map(cx);
let params = use_params_map();
let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default());
// imagine we're loading data from an API here
@@ -130,7 +131,7 @@ fn ContactInfo(cx: Scope) -> impl IntoView {
_ => "User not found.",
};
view! { cx,
view! {
<div class="contact-info">
<h4>{name}</h4>
<div class="tabs">
@@ -146,7 +147,7 @@ fn ContactInfo(cx: Scope) -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(|| view! { <App/> })
}
```

View File

@@ -24,15 +24,15 @@ async fn fetch_results() {
}
#[component]
pub fn FormExample(cx: Scope) -> impl IntoView {
pub fn FormExample() -> impl IntoView {
// reactive access to URL query strings
let query = use_query_map(cx);
let query = use_query_map();
// search stored as ?q=
let search = move || query().get("q").cloned().unwrap_or_default();
// a resource driven by the search string
let search_results = create_resource(cx, search, fetch_results);
let search_results = create_resource(search, fetch_results);
view! { cx,
view! {
<Form method="GET" action="">
<input type="search" name="search" value=search/>
<input type="submit"/>
@@ -51,7 +51,7 @@ This is a great pattern. The data flow is extremely clear: all data flows from t
We can actually take it a step further and do something kind of clever:
```rust
view! { cx,
view! {
<Form method="GET" action="">
<input type="search" name="search" value=search
oninput="this.form.requestSubmit()"
@@ -74,8 +74,8 @@ use leptos::*;
use leptos_router::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
fn App() -> impl IntoView {
view! {
<Router>
<h1><code>"<Form/>"</code></h1>
<main>
@@ -88,14 +88,14 @@ fn App(cx: Scope) -> impl IntoView {
}
#[component]
pub fn FormExample(cx: Scope) -> impl IntoView {
pub fn FormExample() -> impl IntoView {
// reactive access to URL query
let query = use_query_map(cx);
let query = use_query_map();
let name = move || query().get("name").cloned().unwrap_or_default();
let number = move || query().get("number").cloned().unwrap_or_default();
let select = move || query().get("select").cloned().unwrap_or_default();
view! { cx,
view! {
// read out the URL query strings
<table>
<tr>
@@ -172,7 +172,7 @@ pub fn FormExample(cx: Scope) -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(|| view! { <App/> })
}
```

View File

@@ -31,9 +31,9 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
}
#[component]
pub fn BusyButton(cx: Scope) -> impl IntoView {
pub fn BusyButton() -> impl IntoView {
view! {
cx,
<button on:click=move |_| {
spawn_local(async {
add_todo("So much to do!".to_string()).await;
@@ -100,9 +100,9 @@ In other words, you have two choices:
> **Why not `PUT` or `DELETE`? Why URL/form encoding, and not JSON?**
>
> These are reasonable questions. Much of the web is built on REST API patterns that encourage the use of semantic HTTP methods like `DELETE` to delete an item from a database, and many devs are accustomed to sending data to APIs in the JSON format.
>
>
> The reason we use `POST` or `GET` with URL-encoded data by default is the `<form>` support. For better or for worse, HTML forms dont support `PUT` or `DELETE`, and they dont support sending JSON. This means that if you use anything but a `GET` or `POST` request with URL-encoded data, it can only work once WASM has loaded. As well see [in a later chapter](../progressive_enhancement), this isnt always a great idea.
>
>
> The CBOR encoding is suported for historical reasons; an earlier version of server functions used a URL encoding that didnt support nested objects like structs or vectors as server function arguments, which CBOR did. But note that the CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of your app is not available.
## An Important Note on Security

View File

@@ -23,12 +23,12 @@ The [`extract` function in `leptos_actix`](https://docs.rs/leptos_actix/latest/l
```rust
#[server(ActixExtract, "/api")]
pub async fn actix_extract(cx: Scope) -> Result<String, ServerFnError> {
pub async fn actix_extract() -> Result<String, ServerFnError> {
use leptos_actix::extract;
use actix_web::dev::ConnectionInfo;
use actix_web::web::{Data, Query};
extract(cx,
extract(
|search: Query<Search>, connection: ConnectionInfo| async move {
format!(
"search = {}\nconnection = {:?}",
@@ -47,11 +47,11 @@ The syntax for the [`leptos_axum::extract`](https://docs.rs/leptos_axum/latest/l
```rust
#[server(AxumExtract, "/api")]
pub async fn axum_extract(cx: Scope) -> Result<String, ServerFnError> {
pub async fn axum_extract() -> Result<String, ServerFnError> {
use axum::{extract::Query, http::Method};
use leptos_axum::extract;
extract(cx, |method: Method, res: Query<MyQuery>| async move {
extract(|method: Method, res: Query<MyQuery>| async move {
format!("{method:?} and {}", res.q)
},
)

View File

@@ -8,12 +8,12 @@ Extractors provide an easy way to access request data inside server functions. L
```rust
#[server(TeaAndCookies)]
pub async fn tea_and_cookies(cx: Scope) -> Result<(), ServerFnError> {
pub async fn tea_and_cookies() -> Result<(), ServerFnError> {
use actix_web::{cookie::Cookie, http::header, http::header::HeaderValue};
use leptos_actix::ResponseOptions;
// pull ResponseOptions from context
let response = expect_context::<ResponseOptions>(cx);
let response = expect_context::<ResponseOptions>();
// set the HTTP status code
response.set_status(StatusCode::IM_A_TEAPOT);
@@ -35,14 +35,14 @@ Heres a simplified example from our [`session_auth_axum` example](https://git
```rust
#[server(Login, "/api")]
pub async fn login(
cx: Scope,
username: String,
password: String,
remember: Option<String>,
) -> Result<(), ServerFnError> {
// pull the DB pool and auth provider from context
let pool = pool(cx)?;
let auth = auth(cx)?;
let pool = pool()?;
let auth = auth()?;
// check whether the user exists
let user: User = User::get_from_username(username, &pool)
@@ -60,7 +60,7 @@ pub async fn login(
auth.remember_user(remember.is_some());
// and redirect to the home page
leptos_axum::redirect(cx, "/");
leptos_axum::redirect("/");
Ok(())
}
// if not, return an error

View File

@@ -110,14 +110,14 @@ With blocking resources, I can do something like this:
```rust
#[component]
pub fn BlogPost(cx: Scope) -> impl IntoView {
let post_data = create_blocking_resource(cx, /* load blog post */);
let comment_data = create_resource(cx, /* load blog post */);
view! { cx,
pub fn BlogPost() -> impl IntoView {
let post_data = create_blocking_resource(/* load blog post */);
let comment_data = create_resource(/* load blog post */);
view! {
<Suspense fallback=|| ()>
{move || {
post_data.with(cx, |data| {
view! { cx,
post_data.with(|data| {
view! {
<Title text=data.title/>
<Meta name="description" content=data.excerpt/>
<article>

View File

@@ -8,7 +8,7 @@ Put a log somewhere in your root component. (I usually call mine `<App/>`, but a
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
pub fn App() -> impl IntoView {
leptos::log!("where do I run?");
// ... whatever
}
@@ -57,15 +57,15 @@ One way to create a bug is by creating a mismatch between the HTML thats sent
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
pub fn App() -> impl IntoView {
let data = if cfg!(target_arch = "wasm32") {
vec![0, 1, 2]
} else {
vec![]
};
data.into_iter()
.map(|value| view! { cx, <span>{value}</span> })
.collect_view(cx)
.map(|value| view! { <span>{value}</span> })
.collect_view()
}
```
@@ -93,20 +93,20 @@ This is a slightly more common way to create a client/server mismatch: updating
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (loaded, set_loaded) = create_signal(cx, false);
pub fn App() -> impl IntoView {
let (loaded, set_loaded) = create_signal(false);
// create_effect only runs on the client
create_effect(cx, move |_| {
create_effect(move |_| {
// do something like reading from localStorage
set_loaded(true);
});
move || {
if loaded() {
view! { cx, <p>"Hello, world!"</p> }.into_any()
view! { <p>"Hello, world!"</p> }.into_any()
} else {
view! { cx, <div class="loading">"Loading..."</div> }.into_any()
view! { <div class="loading">"Loading..."</div> }.into_any()
}
}
}
@@ -129,7 +129,7 @@ The problem here is that `create_effect` runs **immediately** and **synchronousl
You can simply tell the effect to wait a tick before updating the signal, by using something like `request_animation_frame`, which will set a short timeout and then update the signal before the next frame.
```rust
create_effect(cx, move |_| {
create_effect(move |_| {
// do something like reading from localStorage
request_animation_frame(move || set_loaded(true));
});
@@ -163,7 +163,7 @@ For example, say that I want to store something in the browsers `localStorage
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
pub fn App() -> impl IntoView {
use gloo_storage::Storage;
let storage = gloo_storage::LocalStorage::raw();
leptos::log!("{storage:?}");
@@ -176,9 +176,9 @@ But if I wrap it in an effect...
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
pub fn App() -> impl IntoView {
use gloo_storage::Storage;
create_effect(cx, move |_| {
create_effect(move |_| {
let storage = gloo_storage::LocalStorage::raw();
leptos::log!("{storage:?}");
});

View File

@@ -14,8 +14,8 @@ For example, instead of embedding logic in a component directly like this:
```rust
#[component]
pub fn TodoApp(cx: Scope) -> impl IntoView {
let (todos, set_todos) = create_signal(cx, vec![Todo { /* ... */ }]);
pub fn TodoApp() -> impl IntoView {
let (todos, set_todos) = create_signal(vec![Todo { /* ... */ }]);
// ⚠️ this is hard to test because it's embedded in the component
let num_remaining = move || todos.with(|todos| {
todos.iter().filter(|todo| !todo.completed).sum()
@@ -43,8 +43,8 @@ mod tests {
}
#[component]
pub fn TodoApp(cx: Scope) -> impl IntoView {
let (todos, set_todos) = create_signal(cx, Todos(vec![Todo { /* ... */ }]));
pub fn TodoApp() -> impl IntoView {
let (todos, set_todos) = create_signal(Todos(vec![Todo { /* ... */ }]));
// ✅ this has a test associated with it
let num_remaining = move || todos.with(Todos::num_remaining);
}
@@ -65,7 +65,7 @@ This is a fairly simple manual testing setup that uses the [`wasm-pack test`](ht
#### Sample Test
```rust
````rust
#[wasm_bindgen_test]
fn clear() {
let document = leptos::document();
@@ -74,7 +74,7 @@ fn clear() {
mount_to(
test_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
|| view! { <SimpleCounter initial_value=10 step=1/> },
);
let div = test_wrapper.query_selector("div").unwrap().unwrap();
@@ -86,11 +86,29 @@ fn clear() {
clear.click();
assert_eq!(
div.outer_html(),
/* HTML expected */
);
```
```rust
assert_eq!(
div.outer_html(),
// here we spawn a mini reactive system to render the test case
run_scope(create_runtime(), || {
// it's as if we're creating it with a value of 0, right?
let (value, set_value) = create_signal(0);
// we can remove the event listeners because they're not rendered to HTML
view! {
<div>
<button>"Clear"</button>
<button>"-1"</button>
<span>"Value: " {value} "!"</span>
<button>"+1"</button>
</div>
}
// the view returned an HtmlElement<Div>, which is a smart pointer for
// a DOM element. So we can still just call .outer_html()
.outer_html()
})
);
````
### [`wasm-bindgen-test` with `counters_stable`](https://github.com/leptos-rs/leptos/tree/main/examples/counters_stable/tests/web)

View File

@@ -13,7 +13,7 @@ DOM, with self-contained, defined behavior. Unlike HTML elements, they are in
```rust
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(|| view! { <App/> })
}
```
@@ -22,10 +22,10 @@ Ill give you the whole thing up front, then walk through it line by line.
```rust
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
fn App() -> impl IntoView {
let (count, set_count) = create_signal(0);
view! { cx,
view! {
<button
on:click=move |_| {
set_count(3);
@@ -49,18 +49,17 @@ used as a component in your Leptos application. Well see some of the other fe
this macro in a couple chapters.
```rust
fn App(cx: Scope) -> impl IntoView
fn App() -> impl IntoView
```
Every component is a function with the following characteristics
1. It takes a reactive [`Scope`](https://docs.rs/leptos/latest/leptos/struct.Scope.html)
as its first argument. This `Scope` is our entrypoint into the reactive system.
By convention, its usually named `cx`.
2. You can include other arguments, which will be available as component “props.”
3. Component functions return `impl IntoView`, which is an opaque type that includes
1. It takes zero or more arguments of any type.
2. It returns `impl IntoView`, which is an opaque type that includes
anything you could return from a Leptos `view`.
> Component function arguments are gathered together into a single props struct which is built by the `view` macro as needed.
## The Component Body
The body of the component function is a set-up function that runs once, not a
@@ -69,7 +68,7 @@ few reactive variables, define any side effects that run in response to those va
changing, and describe the user interface.
```rust
let (count, set_count) = create_signal(cx, 0);
let (count, set_count) = create_signal(0);
```
[`create_signal`](https://docs.rs/leptos/latest/leptos/fn.create_signal.html)
@@ -85,7 +84,7 @@ current value, youll call `set_count.set(...)` (or `set_count(...)`).
Leptos defines user interfaces using a JSX-like format via the [`view`](https://docs.rs/leptos/latest/leptos/macro.view.html) macro.
```rust
view! { cx,
view! {
<button
// define an event listener with on:
on:click=move |_| {
@@ -127,7 +126,7 @@ Leptos with `nightly` Rust, signals are already functions, so the closure is unn
As a result, you can write a simpler view:
```rust
view! { cx,
view! {
<button /* ... */>
"Click me: "
// identical to {move || count.get()}
@@ -171,16 +170,16 @@ use leptos::*;
// Components are the building blocks of your user interface
// They define a reusable unit of behavior
#[component]
fn App(cx: Scope) -> impl IntoView {
fn App() -> impl IntoView {
// here we create a reactive signal
// and get a (getter, setter) pair
// signals are the basic unit of change in the framework
// we'll talk more about them later
let (count, set_count) = create_signal(cx, 0);
let (count, set_count) = create_signal(0);
// the `view` macro is how we define the user interface
// it uses an HTML-like format that can accept certain Rust values
view! { cx,
view! {
<button
// on:click will run whenever the `click` event fires
// every event handler is defined as `on:{eventname}`
@@ -221,6 +220,6 @@ fn App(cx: Scope) -> impl IntoView {
// Because we defined it as `fn App`, we can now use it in a
// template as <App/>
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(|| view! { <App/> })
}
```

View File

@@ -12,10 +12,10 @@ increment a counter.
```rust
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
fn App() -> impl IntoView {
let (count, set_count) = create_signal(0);
view! { cx,
view! {
<button
on:click=move |_| {
set_count.update(|n| *n += 1);
@@ -73,9 +73,9 @@ class=("button-20", move || count() % 2 == 1)
Individual CSS properties can be directly updated with a similar `style:` syntax.
```rust
let (x, set_x) = create_signal(cx, 0);
let (y, set_y) = create_signal(cx, 0);
view! { cx,
let (x, set_x) = create_signal(0);
let (y, set_y) = create_signal(0);
view! {
<div
style="position: absolute"
style:left=move || format!("{}px", x() + 100)
@@ -159,7 +159,7 @@ are designed to solve this problem for expensive calculations.
>
> ```rust
> let html = "<p>This HTML will be injected.</p>";
> view! { cx,
> view! {
> <div inner_html=html/>
> }
> ```
@@ -177,15 +177,15 @@ are designed to solve this problem for expensive calculations.
use leptos::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
fn App() -> impl IntoView {
let (count, set_count) = create_signal(0);
// a "derived signal" is a function that accesses other signals
// we can use this to create reactive values that depend on the
// values of one or more other signals
let double_count = move || count() * 2;
view! { cx,
view! {
<button
on:click=move |_| {
set_count.update(|n| *n += 1);
@@ -227,7 +227,7 @@ fn App(cx: Scope) -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(|| view! { <App/> })
}
```

View File

@@ -12,10 +12,10 @@ per click.
You _could_ do this by just creating two `<progress>` elements:
```rust
let (count, set_count) = create_signal(cx, 0);
let (count, set_count) = create_signal(0);
let double_count = move || count() * 2;
view! { cx,
view! {
<progress
max="50"
value=count
@@ -36,9 +36,9 @@ Instead, lets create a `<ProgressBar/>` component.
```rust
#[component]
fn ProgressBar(
cx: Scope
) -> impl IntoView {
view! { cx,
view! {
<progress
max="50"
// hmm... where will we get this from?
@@ -64,10 +64,10 @@ In Leptos, you define props by giving additional arguments to the component func
```rust
#[component]
fn ProgressBar(
cx: Scope,
progress: ReadSignal<i32>
) -> impl IntoView {
view! { cx,
view! {
<progress
max="50"
// now this works
@@ -81,9 +81,9 @@ Now we can use our component in the main `<App/>` components view.
```rust
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
view! { cx,
fn App() -> impl IntoView {
let (count, set_count) = create_signal(0);
view! {
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
"Click me"
</button>
@@ -118,14 +118,14 @@ argument to the component function with `#[prop(optional)]`.
```rust
#[component]
fn ProgressBar(
cx: Scope,
// mark this prop optional
// you can specify it or not when you use <ProgressBar/>
#[prop(optional)]
max: u16,
progress: ReadSignal<i32>
) -> impl IntoView {
view! { cx,
view! {
<progress
max=max
value=progress
@@ -149,12 +149,12 @@ with `#[prop(default = ...)`.
```rust
#[component]
fn ProgressBar(
cx: Scope,
#[prop(default = 100)]
max: u16,
progress: ReadSignal<i32>
) -> impl IntoView {
view! { cx,
view! {
<progress
max=max
value=progress
@@ -171,11 +171,11 @@ as the `progress` prop on another `<ProgressBar/>`.
```rust
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
fn App() -> impl IntoView {
let (count, set_count) = create_signal(0);
let double_count = move || count() * 2;
view! { cx,
view! {
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
"Click me"
</button>
@@ -199,7 +199,7 @@ implement the trait `Fn() -> i32`. So you could use a generic component:
```rust
#[component]
fn ProgressBar<F>(
cx: Scope,
#[prop(default = 100)]
max: u16,
progress: F
@@ -207,7 +207,7 @@ fn ProgressBar<F>(
where
F: Fn() -> i32 + 'static,
{
view! { cx,
view! {
<progress
max=max
value=progress
@@ -224,11 +224,10 @@ This generic can also be specified inline:
```rust
#[component]
fn ProgressBar<F: Fn() -> i32 + 'static>(
cx: Scope,
#[prop(default = 100)] max: u16,
progress: F,
) -> impl IntoView {
view! { cx,
view! {
<progress
max=max
value=progress
@@ -255,14 +254,14 @@ reactive value.
```rust
#[component]
fn ProgressBar(
cx: Scope,
#[prop(default = 100)]
max: u16,
#[prop(into)]
progress: Signal<i32>
) -> impl IntoView
{
view! { cx,
view! {
<progress
max=max
value=progress
@@ -271,18 +270,18 @@ fn ProgressBar(
}
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
fn App() -> impl IntoView {
let (count, set_count) = create_signal(0);
let double_count = move || count() * 2;
view! { cx,
view! {
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
"Click me"
</button>
// .into() converts `ReadSignal` to `Signal`
<ProgressBar progress=count/>
// use `Signal::derive()` to wrap a derived signal
<ProgressBar progress=Signal::derive(cx, double_count)/>
<ProgressBar progress=Signal::derive(double_count)/>
}
}
```
@@ -294,11 +293,10 @@ Note that you cant specify optional generic props for a component. Lets se
```rust,compile_fail
#[component]
fn ProgressBar<F: Fn() -> i32 + 'static>(
cx: Scope,
#[prop(optional)] progress: Option<F>,
) -> impl IntoView {
progress.map(|progress| {
view! { cx,
view! {
<progress
max=100
value=progress
@@ -308,8 +306,8 @@ fn ProgressBar<F: Fn() -> i32 + 'static>(
}
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! { cx,
pub fn App() -> impl IntoView {
view! {
<ProgressBar/>
}
}
@@ -337,11 +335,10 @@ However, you can get around this by providing a concrete type using `Box<dyn _>`
```rust
#[component]
fn ProgressBar(
cx: Scope,
#[prop(optional)] progress: Option<Box<dyn Fn() -> i32>>,
) -> impl IntoView {
progress.map(|progress| {
view! { cx,
view! {
<progress
max=100
value=progress
@@ -351,8 +348,8 @@ fn ProgressBar(
}
#[component]
pub fn App(cx: Scope) -> impl IntoView {
view! { cx,
pub fn App() -> impl IntoView {
view! {
<ProgressBar/>
}
}
@@ -376,7 +373,7 @@ component function, and each one of the props:
/// Shows progress toward a goal.
#[component]
fn ProgressBar(
cx: Scope,
/// The maximum value of the progress bar.
#[prop(default = 100)]
max: u16,
@@ -433,8 +430,6 @@ use leptos::*;
/// Shows progress toward a goal.
#[component]
fn ProgressBar(
// All components take a reactive `Scope` as the first argument
cx: Scope,
// Marks this as an optional prop. It will default to the default
// value of its type, i.e., 0.
#[prop(default = 100)]
@@ -448,7 +443,7 @@ fn ProgressBar(
/// How much progress should be displayed.
progress: Signal<i32>,
) -> impl IntoView {
view! { cx,
view! {
<progress
max={max}
value=progress
@@ -458,12 +453,12 @@ fn ProgressBar(
}
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
fn App() -> impl IntoView {
let (count, set_count) = create_signal(0);
let double_count = move || count() * 2;
view! { cx,
view! {
<button
on:click=move |_| {
set_count.update(|n| *n += 1);
@@ -481,12 +476,12 @@ fn App(cx: Scope) -> impl IntoView {
<ProgressBar progress=count/>
// Signal::derive creates a Signal wrapper from our derived signal
// using double_count means it should move twice as fast
<ProgressBar max=50 progress=Signal::derive(cx, double_count)/>
<ProgressBar max=50 progress=Signal::derive(double_count)/>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(|| view! { <App/> })
}
```

View File

@@ -19,30 +19,30 @@ any `Vec<IV> where IV: IntoView` into your view. In other words, if you can rend
```rust
let values = vec![0, 1, 2];
view! { cx,
view! {
// this will just render "012"
<p>{values.clone()}</p>
// or we can wrap them in <li>
<ul>
{values.into_iter()
.map(|n| view! { cx, <li>{n}</li>})
.map(|n| view! { <li>{n}</li>})
.collect::<Vec<_>>()}
</ul>
}
```
Leptos also provides a `.collect_view(cx)` helper function that allows you to collect any iterator of `T: IntoView` into `Vec<View>`.
Leptos also provides a `.collect_view()` helper function that allows you to collect any iterator of `T: IntoView` into `Vec<View>`.
```rust
let values = vec![0, 1, 2];
view! { cx,
view! {
// this will just render "012"
<p>{values.clone()}</p>
// or we can wrap them in <li>
<ul>
{values.into_iter()
.map(|n| view! { cx, <li>{n}</li>})
.collect_view(cx)}
.map(|n| view! { <li>{n}</li>})
.collect_view()}
</ul>
}
```
@@ -52,13 +52,13 @@ You can render dynamic items as part of a static list.
```rust
// create a list of N signals
let counters = (1..=length).map(|idx| create_signal(cx, idx));
let counters = (1..=length).map(|idx| create_signal(idx));
// each item manages a reactive view
// but the list itself will never change
let counter_buttons = counters
.map(|(count, set_count)| {
view! { cx,
view! {
<li>
<button
on:click=move |_| set_count.update(|n| *n += 1)
@@ -68,9 +68,9 @@ let counter_buttons = counters
</li>
}
})
.collect_view(cx);
.collect_view();
view! { cx,
view! {
<ul>{counter_buttons}</ul>
}
```
@@ -120,8 +120,8 @@ use leptos::*;
// 2) for lists that grow, shrink, or move items, using <For/>
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
fn App() -> impl IntoView {
view! {
<h1>"Iteration"</h1>
<h2>"Static List"</h2>
<p>"Use this pattern if the list itself is static."</p>
@@ -136,19 +136,19 @@ fn App(cx: Scope) -> impl IntoView {
/// to add or remove any.
#[component]
fn StaticList(
cx: Scope,
/// How many counters to include in this list.
length: usize,
) -> impl IntoView {
// create counter signals that start at incrementing numbers
let counters = (1..=length).map(|idx| create_signal(cx, idx));
let counters = (1..=length).map(|idx| create_signal(idx));
// when you have a list that doesn't change, you can
// manipulate it using ordinary Rust iterators
// and collect it into a Vec<_> to insert it into the DOM
let counter_buttons = counters
.map(|(count, set_count)| {
view! { cx,
view! {
<li>
<button
on:click=move |_| set_count.update(|n| *n += 1)
@@ -163,7 +163,7 @@ fn StaticList(
// Note that if `counter_buttons` were a reactive list
// and its value changed, this would be very inefficient:
// it would rerender every row every time the list changed.
view! { cx,
view! {
<ul>{counter_buttons}</ul>
}
}
@@ -172,7 +172,7 @@ fn StaticList(
/// remove counters.
#[component]
fn DynamicList(
cx: Scope,
/// The number of counters to begin with.
initial_length: usize,
) -> impl IntoView {
@@ -190,17 +190,17 @@ fn DynamicList(
// we generate an initial list as in <StaticList/>
// but this time we include the ID along with the signal
let initial_counters = (0..initial_length)
.map(|id| (id, create_signal(cx, id + 1)))
.map(|id| (id, create_signal(id + 1)))
.collect::<Vec<_>>();
// now we store that initial list in a signal
// this way, we'll be able to modify the list over time,
// adding and removing counters, and it will change reactively
let (counters, set_counters) = create_signal(cx, initial_counters);
let (counters, set_counters) = create_signal(initial_counters);
let add_counter = move |_| {
// create a signal for the new counter
let sig = create_signal(cx, next_counter_id + 1);
let sig = create_signal(next_counter_id + 1);
// add this counter to the list of counters
set_counters.update(move |counters| {
// since `.update()` gives us `&mut T`
@@ -211,7 +211,7 @@ fn DynamicList(
next_counter_id += 1;
};
view! { cx,
view! {
<div>
<button on:click=add_counter>
"Add Counter"
@@ -231,8 +231,8 @@ fn DynamicList(
key=|counter| counter.0
// the view function receives each item from your `each` iterator
// and returns a view
view=move |cx, (id, (count, set_count))| {
view! { cx,
view=move |(id, (count, set_count))| {
view! {
<li>
<button
on:click=move |_| set_count.update(|n| *n += 1)
@@ -258,7 +258,7 @@ fn DynamicList(
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(|| view! { <App/> })
}
```

View File

@@ -23,9 +23,9 @@ There are two important things to remember:
on an `<input type="checkbox">`.)
```rust
let (name, set_name) = create_signal(cx, "Controlled".to_string());
let (name, set_name) = create_signal("Controlled".to_string());
view! { cx,
view! {
<input type="text"
on:input=move |ev| {
// event_target_value is a Leptos helper function
@@ -81,9 +81,9 @@ In this example, we only notify the framework when the `<form>` fires a `submit`
event.
```rust
let (name, set_name) = create_signal(cx, "Uncontrolled".to_string());
let (name, set_name) = create_signal("Uncontrolled".to_string());
let input_element: NodeRef<Input> = create_node_ref(cx);
let input_element: NodeRef<Input> = create_node_ref();
```
`NodeRef` is a kind of reactive smart pointer: we can use it to access the
@@ -117,7 +117,7 @@ We can then call `.value()` to get the value out of the input, because `NodeRef`
gives us access to a correctly-typed HTML element.
```rust
view! { cx,
view! {
<form on:submit=on_submit>
<input type="text"
value=name
@@ -148,8 +148,8 @@ The view should be pretty self-explanatory by now. Note two things:
use leptos::{ev::SubmitEvent, *};
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
fn App() -> impl IntoView {
view! {
<h2>"Controlled Component"</h2>
<ControlledComponent/>
<h2>"Uncontrolled Component"</h2>
@@ -158,11 +158,11 @@ fn App(cx: Scope) -> impl IntoView {
}
#[component]
fn ControlledComponent(cx: Scope) -> impl IntoView {
fn ControlledComponent() -> impl IntoView {
// create a signal to hold the value
let (name, set_name) = create_signal(cx, "Controlled".to_string());
let (name, set_name) = create_signal("Controlled".to_string());
view! { cx,
view! {
<input type="text"
// fire an event whenever the input changes
on:input=move |ev| {
@@ -192,15 +192,15 @@ fn ControlledComponent(cx: Scope) -> impl IntoView {
}
#[component]
fn UncontrolledComponent(cx: Scope) -> impl IntoView {
fn UncontrolledComponent() -> impl IntoView {
// import the type for <input>
use leptos::html::Input;
let (name, set_name) = create_signal(cx, "Uncontrolled".to_string());
let (name, set_name) = create_signal("Uncontrolled".to_string());
// we'll use a NodeRef to store a reference to the input element
// this will be filled when the element is created
let input_element: NodeRef<Input> = create_node_ref(cx);
let input_element: NodeRef<Input> = create_node_ref();
// fires when the form `submit` event happens
// this will store the value of the <input> in our signal
@@ -220,7 +220,7 @@ fn UncontrolledComponent(cx: Scope) -> impl IntoView {
set_name(value);
};
view! { cx,
view! {
<form on:submit=on_submit>
<input type="text"
// here, we use the `value` *attribute* to set only
@@ -242,7 +242,7 @@ fn UncontrolledComponent(cx: Scope) -> impl IntoView {
// Because we defined it as `fn App`, we can now use it in a
// template as <App/>
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(|| view! { <App/> })
}
```

View File

@@ -38,7 +38,7 @@ special knowledge.
For example, lets start with a simple signal and derived signal:
```rust
let (value, set_value) = create_signal(cx, 0);
let (value, set_value) = create_signal(0);
let is_odd = move || value() & 1 == 1;
```
@@ -54,7 +54,7 @@ Lets say I want to render some text if the number is odd, and some other text
if its even. Well, how about this?
```rust
view! { cx,
view! {
<p>
{move || if is_odd() {
"Odd"
@@ -81,7 +81,7 @@ let message = move || {
}
};
view! { cx,
view! {
<p>{message}</p>
}
```
@@ -90,7 +90,7 @@ This works fine. We can make it a little shorter if wed like, using `bool::th
```rust
let message = move || is_odd().then(|| "Ding ding ding!");
view! { cx,
view! {
<p>{message}</p>
}
```
@@ -112,7 +112,7 @@ let message = move || {
_ => "Even"
}
};
view! { cx,
view! {
<p>{message}</p>
}
```
@@ -131,7 +131,7 @@ above, where the value switches from even to odd on every change, this is fine.
But consider the following example:
```rust
let (value, set_value) = create_signal(cx, 0);
let (value, set_value) = create_signal(0);
let message = move || if value() > 5 {
"Big"
@@ -139,7 +139,7 @@ let message = move || if value() > 5 {
"Small"
};
view! { cx,
view! {
<p>{message}</p>
}
```
@@ -194,12 +194,12 @@ the answer. You pass it a `when` condition function, a `fallback` to be shown if
the `when` function returns `false`, and children to be rendered if `when` is `true`.
```rust
let (value, set_value) = create_signal(cx, 0);
let (value, set_value) = create_signal(0);
view! { cx,
view! {
<Show
when=move || { value() > 5 }
fallback=|cx| view! { cx, <Small/> }
fallback=|| view! { <Small/> }
>
<Big/>
</Show>
@@ -228,19 +228,19 @@ can be a little annoying if youre returning different HTML elements from
different branches of a conditional:
```rust,compile_error
view! { cx,
view! {
<main>
{move || match is_odd() {
true if value() == 1 => {
// returns HtmlElement<Pre>
view! { cx, <pre>"One"</pre> }
view! { <pre>"One"</pre> }
},
false if value() == 2 => {
// returns HtmlElement<P>
view! { cx, <p>"Two"</p> }
view! { <p>"Two"</p> }
}
// returns HtmlElement<Textarea>
_ => view! { cx, <textarea>{value()}</textarea> }
_ => view! { <textarea>{value()}</textarea> }
}}
</main>
}
@@ -260,24 +260,24 @@ to get yourself out of this situation:
1. If you have multiple `HtmlElement` types, convert them to `HtmlElement<AnyElement>`
with [`.into_any()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.into_any)
2. If you have a variety of view types that are not all `HtmlElement`, convert them to
`View`s with [`.into_view(cx)`](https://docs.rs/leptos/latest/leptos/trait.IntoView.html#tymethod.into_view).
`View`s with [`.into_view()`](https://docs.rs/leptos/latest/leptos/trait.IntoView.html#tymethod.into_view).
Heres the same example, with the conversion added:
```rust,compile_error
view! { cx,
view! {
<main>
{move || match is_odd() {
true if value() == 1 => {
// returns HtmlElement<Pre>
view! { cx, <pre>"One"</pre> }.into_any()
view! { <pre>"One"</pre> }.into_any()
},
false if value() == 2 => {
// returns HtmlElement<P>
view! { cx, <p>"Two"</p> }.into_any()
view! { <p>"Two"</p> }.into_any()
}
// returns HtmlElement<Textarea>
_ => view! { cx, <textarea>{value()}</textarea> }.into_any()
_ => view! { <textarea>{value()}</textarea> }.into_any()
}}
</main>
}
@@ -294,12 +294,12 @@ view! { cx,
use leptos::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, 0);
fn App() -> impl IntoView {
let (value, set_value) = create_signal(0);
let is_odd = move || value() & 1 == 1;
let odd_text = move || if is_odd() { Some("How odd!") } else { None };
view! { cx,
view! {
<h1>"Control Flow"</h1>
// Simple UI to update and show a value
@@ -346,37 +346,37 @@ fn App(cx: Scope) -> impl IntoView {
// needed. This makes it more efficient in many cases
// than a {move || if ...} block
<Show when=is_odd
fallback=|cx| view! { cx, <p>"Even steven"</p> }
fallback=|| view! { <p>"Even steven"</p> }
>
<p>"Oddment"</p>
</Show>
// d. Because `bool::then()` converts a `bool` to
// `Option`, you can use it to create a show/hide toggled
{move || is_odd().then(|| view! { cx, <p>"Oddity!"</p> })}
{move || is_odd().then(|| view! { <p>"Oddity!"</p> })}
<h2>"Converting between Types"</h2>
// e. Note: if branches return different types,
// you can convert between them with
// `.into_any()` (for different HTML element types)
// or `.into_view(cx)` (for all view types)
// or `.into_view()` (for all view types)
{move || match is_odd() {
true if value() == 1 => {
// <pre> returns HtmlElement<Pre>
view! { cx, <pre>"One"</pre> }.into_any()
view! { <pre>"One"</pre> }.into_any()
},
false if value() == 2 => {
// <p> returns HtmlElement<P>
// so we convert into a more generic type
view! { cx, <p>"Two"</p> }.into_any()
view! { <p>"Two"</p> }.into_any()
}
_ => view! { cx, <textarea>{value()}</textarea> }.into_any()
_ => view! { <textarea>{value()}</textarea> }.into_any()
}}
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(|| view! { <App/> })
}
```

View File

@@ -10,16 +10,16 @@ Lets start with a simple component to capture a number input.
```rust
#[component]
fn NumericInput(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, Ok(0));
fn NumericInput() -> impl IntoView {
let (value, set_value) = create_signal(Ok(0));
// when input changes, try to parse a number from the input
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
view! { cx,
view! {
<label>
"Type a number (or not!)"
<input on:input=on_input/>
<input type="number" on:input=on_input/>
<p>
"You entered "
<strong>{value}</strong>
@@ -60,27 +60,27 @@ Lets add an `<ErrorBoundary/>` to this example.
```rust
#[component]
fn NumericInput(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, Ok(0));
fn NumericInput() -> impl IntoView {
let (value, set_value) = create_signal(Ok(0));
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
view! { cx,
view! {
<h1>"Error Handling"</h1>
<label>
"Type a number (or something that's not a number!)"
<input on:input=on_input/>
<input type="number" on:input=on_input/>
<ErrorBoundary
// the fallback receives a signal containing current errors
fallback=|cx, errors| view! { cx,
fallback=|errors| view! {
<div class="error">
<p>"Not a number! Errors: "</p>
// we can render a list of errors as strings, if we'd like
<ul>
{move || errors.get()
.into_iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
.collect_view(cx)
.map(|(_, e)| view! { <li>{e.to_string()}</li>})
.collect_view()
}
</ul>
</div>
@@ -121,13 +121,13 @@ an `<ErrorBoundary/>` will appear again.
use leptos::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, Ok(0));
fn App() -> impl IntoView {
let (value, set_value) = create_signal(Ok(0));
// when input changes, try to parse a number from the input
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
view! { cx,
view! {
<h1>"Error Handling"</h1>
<label>
"Type a number (or something that's not a number!)"
@@ -137,7 +137,7 @@ fn App(cx: Scope) -> impl IntoView {
// <ErrorBoundary/> will be displayed.
<ErrorBoundary
// the fallback receives a signal containing current errors
fallback=|cx, errors| view! { cx,
fallback=|errors| view! {
<div class="error">
<p>"Not a number! Errors: "</p>
// we can render a list of errors
@@ -145,7 +145,7 @@ fn App(cx: Scope) -> impl IntoView {
<ul>
{move || errors.get()
.into_iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
.map(|(_, e)| view! { <li>{e.to_string()}</li>})
.collect::<Vec<_>>()
}
</ul>
@@ -167,7 +167,7 @@ fn App(cx: Scope) -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(|| view! { <App/> })
}
```

View File

@@ -29,17 +29,17 @@ it in the child. This lets you manipulate the state of the parent from the child
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
pub fn App() -> impl IntoView {
let (toggled, set_toggled) = create_signal(false);
view! {
<p>"Toggled? " {toggled}</p>
<ButtonA setter=set_toggled/>
}
}
#[component]
pub fn ButtonA(cx: Scope, setter: WriteSignal<bool>) -> impl IntoView {
view! { cx,
pub fn ButtonA(setter: WriteSignal<bool>) -> impl IntoView {
view! {
<button
on:click=move |_| setter.update(|value| *value = !*value)
>
@@ -62,9 +62,9 @@ Another approach would be to pass a callback to the child: say, `on_click`.
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
pub fn App() -> impl IntoView {
let (toggled, set_toggled) = create_signal(false);
view! {
<p>"Toggled? " {toggled}</p>
<ButtonB on_click=move |_| set_toggled.update(|value| *value = !*value)/>
}
@@ -73,13 +73,13 @@ pub fn App(cx: Scope) -> impl IntoView {
#[component]
pub fn ButtonB<F>(
cx: Scope,
on_click: F,
) -> impl IntoView
where
F: Fn(MouseEvent) + 'static,
{
view! { cx,
view! {
<button on:click=on_click>
"Toggle"
</button>
@@ -105,9 +105,9 @@ in your `view` macro in `<App/>`.
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
pub fn App() -> impl IntoView {
let (toggled, set_toggled) = create_signal(false);
view! {
<p>"Toggled? " {toggled}</p>
// note the on:click instead of on_click
// this is the same syntax as an HTML element event listener
@@ -117,8 +117,8 @@ pub fn App(cx: Scope) -> impl IntoView {
#[component]
pub fn ButtonC(cx: Scope) -> impl IntoView {
view! { cx,
pub fn ButtonC<F>() -> impl IntoView {
view! {
<button>"Toggle"</button>
}
}
@@ -141,17 +141,17 @@ tree:
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
pub fn App() -> impl IntoView {
let (toggled, set_toggled) = create_signal(false);
view! {
<p>"Toggled? " {toggled}</p>
<Layout/>
}
}
#[component]
pub fn Layout(cx: Scope) -> impl IntoView {
view! { cx,
pub fn Layout() -> impl IntoView {
view! {
<header>
<h1>"My Page"</h1>
</header>
@@ -162,8 +162,8 @@ pub fn Layout(cx: Scope) -> impl IntoView {
}
#[component]
pub fn Content(cx: Scope) -> impl IntoView {
view! { cx,
pub fn Content() -> impl IntoView {
view! {
<div class="content">
<ButtonD/>
</div>
@@ -171,7 +171,7 @@ pub fn Content(cx: Scope) -> impl IntoView {
}
#[component]
pub fn ButtonD<F>(cx: Scope) -> impl IntoView {
pub fn ButtonD<F>() -> impl IntoView {
todo!()
}
```
@@ -182,17 +182,17 @@ pass your `WriteSignal` to its props. You could do whats sometimes called
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
view! { cx,
pub fn App() -> impl IntoView {
let (toggled, set_toggled) = create_signal(false);
view! {
<p>"Toggled? " {toggled}</p>
<Layout set_toggled/>
}
}
#[component]
pub fn Layout(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
view! { cx,
pub fn Layout(d: WriteSignal<bool>) -> impl IntoView {
view! {
<header>
<h1>"My Page"</h1>
</header>
@@ -203,8 +203,8 @@ pub fn Layout(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
}
#[component]
pub fn Content(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
view! { cx,
pub fn Content(d: WriteSignal<bool>) -> impl IntoView {
view! {
<div class="content">
<ButtonD set_toggled/>
</div>
@@ -212,7 +212,7 @@ pub fn Content(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
}
#[component]
pub fn ButtonD<F>(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
pub fn ButtonD<F>(d: WriteSignal<bool>) -> impl IntoView {
todo!()
}
```
@@ -237,13 +237,13 @@ unnecessary prop drilling.
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (toggled, set_toggled) = create_signal(cx, false);
pub fn App() -> impl IntoView {
let (toggled, set_toggled) = create_signal(false);
// share `set_toggled` with all children of this component
provide_context(cx, set_toggled);
provide_context(set_toggled);
view! { cx,
view! {
<p>"Toggled? " {toggled}</p>
<Layout/>
}
@@ -252,14 +252,14 @@ pub fn App(cx: Scope) -> impl IntoView {
// <Layout/> and <Content/> omitted
#[component]
pub fn ButtonD(cx: Scope) -> impl IntoView {
pub fn ButtonD() -> impl IntoView {
// use_context searches up the context tree, hoping to
// find a `WriteSignal<bool>`
// in this case, I .expect() because I know I provided it
let setter = use_context::<WriteSignal<bool>>(cx)
let setter = use_context::<WriteSignal<bool>>()
.expect("to have found the setter provided");
view! { cx,
view! {
<button
on:click=move |_| setter.update(|value| *value = !*value)
>
@@ -308,20 +308,20 @@ use leptos::{ev::MouseEvent, *};
struct SmallcapsContext(WriteSignal<bool>);
#[component]
pub fn App(cx: Scope) -> impl IntoView {
pub fn App() -> impl IntoView {
// just some signals to toggle three classes on our <p>
let (red, set_red) = create_signal(cx, false);
let (right, set_right) = create_signal(cx, false);
let (italics, set_italics) = create_signal(cx, false);
let (smallcaps, set_smallcaps) = create_signal(cx, false);
let (red, set_red) = create_signal(false);
let (right, set_right) = create_signal(false);
let (italics, set_italics) = create_signal(false);
let (smallcaps, set_smallcaps) = create_signal(false);
// the newtype pattern isn't *necessary* here but is a good practice
// it avoids confusion with other possible future `WriteSignal<bool>` contexts
// and makes it easier to refer to it in ButtonC
provide_context(cx, SmallcapsContext(set_smallcaps));
provide_context(SmallcapsContext(set_smallcaps));
view! {
cx,
<main>
<p
// class: attributes take F: Fn() => bool, and these signals all implement Fn()
@@ -353,12 +353,12 @@ pub fn App(cx: Scope) -> impl IntoView {
/// Button A receives a signal setter and updates the signal itself
#[component]
pub fn ButtonA(
cx: Scope,
/// Signal that will be toggled when the button is clicked.
setter: WriteSignal<bool>,
) -> impl IntoView {
view! {
cx,
<button
on:click=move |_| setter.update(|value| *value = !*value)
>
@@ -370,7 +370,7 @@ pub fn ButtonA(
/// Button B receives a closure
#[component]
pub fn ButtonB<F>(
cx: Scope,
/// Callback that will be invoked when the button is clicked.
on_click: F,
) -> impl IntoView
@@ -378,7 +378,7 @@ where
F: Fn(MouseEvent) + 'static,
{
view! {
cx,
<button
on:click=on_click
>
@@ -402,9 +402,9 @@ where
/// Button C is a dummy: it renders a button but doesn't handle
/// its click. Instead, the parent component adds an event listener.
#[component]
pub fn ButtonC(cx: Scope) -> impl IntoView {
pub fn ButtonC() -> impl IntoView {
view! {
cx,
<button>
"Toggle Italics"
</button>
@@ -414,11 +414,11 @@ pub fn ButtonC(cx: Scope) -> impl IntoView {
/// Button D is very similar to Button A, but instead of passing the setter as a prop
/// we get it from the context
#[component]
pub fn ButtonD(cx: Scope) -> impl IntoView {
let setter = use_context::<SmallcapsContext>(cx).unwrap().0;
pub fn ButtonD() -> impl IntoView {
let setter = use_context::<SmallcapsContext>().unwrap().0;
view! {
cx,
<button
on:click=move |_| setter.update(|value| *value = !*value)
>
@@ -428,7 +428,7 @@ pub fn ButtonD(cx: Scope) -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(|| view! { <App/> })
}
```

View File

@@ -5,7 +5,7 @@ children into an HTML element. For example, imagine I have a `<FancyForm/>` comp
that enhances an HTML `<form>`. I need some way to pass all its inputs.
```rust
view! { cx,
view! {
<Form>
<fieldset>
<label>
@@ -28,12 +28,12 @@ other components:
In fact, youve already seen these both in action in the [`<Show/>`](/view/06_control_flow.html#show) component:
```rust
view! { cx,
view! {
<Show
// `when` is a normal prop
when=move || value() > 5
// `fallback` is a "render prop": a function that returns a view
fallback=|cx| view! { cx, <Small/> }
fallback=|| view! { <Small/> }
>
// `<Big/>` (and anything else here)
// will be given to the `children` prop
@@ -47,7 +47,7 @@ Lets define a component that takes some children and a render prop.
```rust
#[component]
pub fn TakesChildren<F, IV>(
cx: Scope,
/// Takes a function (type F) that returns anything that can be
/// converted into a View (type IV)
render_prop: F,
@@ -58,19 +58,19 @@ where
F: Fn() -> IV,
IV: IntoView,
{
view! { cx,
view! {
<h2>"Render Prop"</h2>
{render_prop()}
<h2>"Children"</h2>
{children(cx)}
{children()}
}
}
```
`render_prop` and `children` are both functions, so we can call them to generate
the appropriate views. `children`, in particular, is an alias for
`Box<dyn FnOnce(Scope) -> Fragment>`. (Aren't you glad we named it `Children` instead?)
`Box<dyn FnOnce() -> Fragment>`. (Aren't you glad we named it `Children` instead?)
> If you need a `Fn` or `FnMut` here because you need to call `children` more than once,
> we also provide `ChildrenFn` and `ChildrenMut` aliases.
@@ -78,8 +78,8 @@ the appropriate views. `children`, in particular, is an alias for
We can use the component like this:
```rust
view! { cx,
<TakesChildren render_prop=|| view! { cx, <p>"Hi, there!"</p> }>
view! {
<TakesChildren render_prop=|| view! { <p>"Hi, there!"</p> }>
// these get passed to `children`
"Some text"
<span>"A span"</span>
@@ -97,15 +97,15 @@ a component that takes its children and turns them into an unordered list.
```rust
#[component]
pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
pub fn WrapsChildren(Children) -> impl IntoView {
// Fragment has `nodes` field that contains a Vec<View>
let children = children(cx)
let children = children()
.nodes
.into_iter()
.map(|child| view! { cx, <li>{child}</li> })
.collect_view(cx);
.map(|child| view! { <li>{child}</li> })
.collect_view();
view! { cx,
view! {
<ul>{children}</ul>
}
}
@@ -114,7 +114,7 @@ pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
Calling it like this will create a list:
```rust
view! { cx,
view! {
<WrapsChildren>
"A"
"B"
@@ -142,19 +142,19 @@ use leptos::*;
// property
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (items, set_items) = create_signal(cx, vec![0, 1, 2]);
pub fn App() -> impl IntoView {
let (items, set_items) = create_signal(vec![0, 1, 2]);
let render_prop = move || {
// items.with(...) reacts to the value without cloning
// by applying a function. Here, we pass the `len` method
// on a `Vec<_>` directly
let len = move || items.with(Vec::len);
view! { cx,
view! {
<p>"Length: " {len}</p>
}
};
view! { cx,
view! {
// This component just displays the two kinds of children,
// embedding them in some other markup
<TakesChildren
@@ -179,12 +179,12 @@ pub fn App(cx: Scope) -> impl IntoView {
/// Displays a `render_prop` and some children within markup.
#[component]
pub fn TakesChildren<F, IV>(
cx: Scope,
/// Takes a function (type F) that returns anything that can be
/// converted into a View (type IV)
render_prop: F,
/// `children` takes the `Children` type
/// this is an alias for `Box<dyn FnOnce(Scope) -> Fragment>`
/// this is an alias for `Box<dyn FnOnce() -> Fragment>`
/// ... aren't you glad we named it `Children` instead?
children: Children,
) -> impl IntoView
@@ -192,30 +192,30 @@ where
F: Fn() -> IV,
IV: IntoView,
{
view! { cx,
view! {
<h1><code>"<TakesChildren/>"</code></h1>
<h2>"Render Prop"</h2>
{render_prop()}
<hr/>
<h2>"Children"</h2>
{children(cx)}
{children()}
}
}
/// Wraps each child in an `<li>` and embeds them in a `<ul>`.
#[component]
pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
// children(cx) returns a `Fragment`, which has a
pub fn WrapsChildren(Children) -> impl IntoView {
// children() returns a `Fragment`, which has a
// `nodes` field that contains a Vec<View>
// this means we can iterate over the children
// to create something new!
let children = children(cx)
let children = children()
.nodes
.into_iter()
.map(|child| view! { cx, <li>{child}</li> })
.map(|child| view! { <li>{child}</li> })
.collect::<Vec<_>>();
view! { cx,
view! {
<h1><code>"<WrapsChildren/>"</code></h1>
// wrap our wrapped children in a UL
<ul>{children}</ul>
@@ -223,7 +223,7 @@ pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
leptos::mount_to_body(|| view! { <App/> })
}
```

View File

@@ -2,11 +2,11 @@ use core::time::Duration;
use leptos::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let show = create_rw_signal(cx, false);
pub fn App() -> impl IntoView {
let show = create_rw_signal(false);
// the CSS classes in this example are just written directly inside the `index.html`
view! { cx,
view! {
<div
class="hover-me"
on:mouseenter=move |_| show.set(true)

View File

@@ -4,9 +4,5 @@ use leptos::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! { cx,
<App />
}
})
mount_to_body(App);
}

View File

@@ -5,15 +5,14 @@ use leptos::*;
/// You can use doc comments like this to document your component.
#[component]
pub fn SimpleCounter(
cx: Scope,
/// The starting value for the counter
initial_value: i32,
/// The change that should be applied each time the button is clicked.
step: i32,
) -> impl IntoView {
let (value, set_value) = create_signal(cx, initial_value);
let (value, set_value) = create_signal(initial_value);
view! { cx,
view! {
<div>
<button on:click=move |_| set_value(0)>"Clear"</button>
<button on:click=move |_| set_value.update(|value| *value -= step)>"-1"</button>

View File

@@ -4,8 +4,8 @@ use leptos::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! { cx,
mount_to_body(|| {
view! {
<SimpleCounter
initial_value=0
step=1

View File

@@ -15,7 +15,7 @@ fn clear() {
// note that we start at the initial value of 10
mount_to(
test_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
|| view! { <SimpleCounter initial_value=10 step=1/> },
);
// now we extract the buttons by iterating over the DOM
@@ -32,16 +32,17 @@ fn clear() {
// now let's test the <div> against the expected value
// we can do this by testing its `outerHTML`
let runtime = create_runtime();
assert_eq!(
div.outer_html(),
// here we spawn a mini reactive system, just to render the
// test case
run_scope(create_runtime(), |cx| {
{
// it's as if we're creating it with a value of 0, right?
let (value, _set_value) = create_signal(cx, 0);
let (value, _set_value) = create_signal(0);
// we can remove the event listeners because they're not rendered to HTML
view! { cx,
view! {
<div>
<button>"Clear"</button>
<button>"-1"</button>
@@ -52,7 +53,7 @@ fn clear() {
// the view returned an HtmlElement<Div>, which is a smart pointer for
// a DOM element. So we can still just call .outer_html()
.outer_html()
})
}
);
// There's actually an easier way to do this...
@@ -61,10 +62,12 @@ fn clear() {
let comparison_wrapper = document.create_element("section").unwrap();
leptos::mount_to(
comparison_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
|| view! { <SimpleCounter initial_value=0 step=1/>},
);
comparison_wrapper.inner_html()
});
runtime.dispose();
}
#[wasm_bindgen_test]
@@ -75,7 +78,7 @@ fn inc() {
mount_to(
test_wrapper.clone().unchecked_into(),
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/> },
|| view! { <SimpleCounter initial_value=0 step=1/> },
);
// You can do testing with vanilla DOM operations
@@ -118,12 +121,14 @@ fn inc() {
assert_eq!(text.text_content(), Some("Value: 0!".to_string()));
let runtime = create_runtime();
// Or you can test against a sample view!
assert_eq!(
div.outer_html(),
run_scope(create_runtime(), |cx| {
let (value, _) = create_signal(cx, 0);
view! { cx,
{
let (value, _) = create_signal(0);
view! {
<div>
<button>"Clear"</button>
<button>"-1"</button>
@@ -132,17 +137,17 @@ fn inc() {
</div>
}
}
.outer_html())
.outer_html()
);
inc.click();
assert_eq!(
div.outer_html(),
run_scope(create_runtime(), |cx| {
{
// because we've clicked, it's as if the signal is starting at 1
let (value, _) = create_signal(cx, 1);
view! { cx,
let (value, _) = create_signal(1);
view! {
<div>
<button>"Clear"</button>
<button>"-1"</button>
@@ -151,6 +156,8 @@ fn inc() {
</div>
}
}
.outer_html())
.outer_html()
);
runtime.dispose();
}

View File

@@ -25,7 +25,7 @@ leptos_meta = { path = "../../meta" }
leptos_router = { path = "../../router" }
log = "0.4"
gloo-net = { git = "https://github.com/rustwasm/gloo" }
wasm-bindgen = "=0.2.87"
wasm-bindgen = "=0.2.86"
serde = { version = "1", features = ["derive"] }
[features]

View File

@@ -40,9 +40,9 @@ pub async fn clear_server_count() -> Result<i32, ServerFnError> {
Ok(0)
}
#[component]
pub fn Counters(cx: Scope) -> impl IntoView {
provide_meta_context(cx);
view! { cx,
pub fn Counters() -> impl IntoView {
provide_meta_context();
view! {
<Router>
<header>
<h1>"Server-Side Counters"</h1>
@@ -67,10 +67,24 @@ pub fn Counters(cx: Scope) -> impl IntoView {
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<main>
<Routes>
<Route path="" view=Counter/>
<Route path="form" view=FormCounter/>
<Route path="multi" view=MultiuserCounter/>
<Route path="multi" view=NotFound/>
<Route
path=""
view=|| {
view! { <Counter/> }
}
/>
<Route
path="form"
view=|| {
view! { <FormCounter/> }
}
/>
<Route
path="multi"
view=|| {
view! { <MultiuserCounter/> }
}
/>
</Routes>
</main>
</Router>
@@ -82,12 +96,11 @@ pub fn Counters(cx: Scope) -> impl IntoView {
// it's invalidated by one of the user's own actions
// This is the typical pattern for a CRUD app
#[component]
pub fn Counter(cx: Scope) -> impl IntoView {
let dec = create_action(cx, |_| adjust_server_count(-1, "decing".into()));
let inc = create_action(cx, |_| adjust_server_count(1, "incing".into()));
let clear = create_action(cx, |_| clear_server_count());
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(
cx,
move || {
(
dec.version().get(),
@@ -98,20 +111,16 @@ pub fn Counter(cx: Scope) -> impl IntoView {
|_| get_server_count(),
);
let value = move || {
counter
.read(cx)
.map(|count| count.unwrap_or(0))
.unwrap_or(0)
};
let value =
move || counter.get().map(|count| count.unwrap_or(0)).unwrap_or(0);
let error_msg = move || {
counter.read(cx).and_then(|res| match res {
counter.get().and_then(|res| match res {
Ok(_) => None,
Err(e) => Some(e),
})
};
view! { cx,
view! {
<div>
<h2>"Simple Counter"</h2>
<p>
@@ -126,7 +135,7 @@ pub fn Counter(cx: Scope) -> impl IntoView {
{move || {
error_msg()
.map(|msg| {
view! { cx, <p>"Error: " {msg.to_string()}</p> }
view! { <p>"Error: " {msg.to_string()}</p> }
})
}}
</div>
@@ -137,12 +146,11 @@ pub fn Counter(cx: Scope) -> impl IntoView {
// It uses the same invalidation pattern as the plain counter,
// but uses HTML forms to submit the actions
#[component]
pub fn FormCounter(cx: Scope) -> impl IntoView {
let adjust = create_server_action::<AdjustServerCount>(cx);
let clear = create_server_action::<ClearServerCount>(cx);
pub fn FormCounter() -> impl IntoView {
let adjust = create_server_action::<AdjustServerCount>();
let clear = create_server_action::<ClearServerCount>();
let counter = create_resource(
cx,
move || (adjust.version().get(), clear.version().get()),
|_| {
log::debug!("FormCounter running fetcher");
@@ -151,19 +159,23 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
);
let value = move || {
log::debug!("FormCounter looking for value");
counter.read(cx).and_then(|n| n.ok()).unwrap_or(0)
counter.get().and_then(|n| n.ok()).unwrap_or(0)
};
view! { cx,
view! {
<div>
<h2>"Form Counter"</h2>
<p>
"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"
</p>
<div>
// calling a server function is the same as POSTing to its API URL
// so we can just do that with a form and button
<ActionForm action=clear>
<input type="submit" value="Clear"/>
</ActionForm>
// We can submit named arguments to the server functions
// by including them as input values with the same name
<ActionForm action=adjust>
<input type="hidden" name="delta" value="-1"/>
<input type="hidden" name="msg" value="form value down"/>
@@ -185,12 +197,11 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
// Whenever another user updates the value, it will update here
// This is the primitive pattern for live chat, collaborative editing, etc.
#[component]
pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
pub fn MultiuserCounter() -> impl IntoView {
let dec =
create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
let inc =
create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
let clear = create_action(cx, |_| clear_server_count());
create_action(|_| adjust_server_count(-1, "dec dec goose".into()));
let inc = create_action(|_| adjust_server_count(1, "inc inc moose".into()));
let clear = create_action(|_| clear_server_count());
#[cfg(not(feature = "ssr"))]
let multiplayer_value = {
@@ -200,7 +211,6 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
gloo_net::eventsource::futures::EventSource::new("/api/events")
.expect("couldn't connect to SSE stream");
let s = create_signal_from_stream(
cx,
source
.subscribe("message")
.unwrap()
@@ -214,14 +224,14 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
}),
);
on_cleanup(cx, move || source.close());
on_cleanup(move || source.close());
s
};
#[cfg(feature = "ssr")]
let (multiplayer_value, _) = create_signal(cx, None::<i32>);
let (multiplayer_value, _) = create_signal(None::<i32>);
view! { cx,
view! {
<div>
<h2>"Multi-User Counter"</h2>
<p>
@@ -238,14 +248,3 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
</div>
}
}
#[component]
fn NotFound(cx: Scope) -> impl IntoView {
#[cfg(feature = "ssr")]
{
let resp = expect_context::<leptos_actix::ResponseOptions>(cx);
resp.set_status(actix_web::http::StatusCode::NOT_FOUND);
}
view! { cx, <h1>"Not Found"</h1> }
}

View File

@@ -13,8 +13,8 @@ cfg_if! {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! { cx, <Counters/> }
mount_to_body(|| {
view! { <Counters/> }
});
}
}

View File

@@ -43,7 +43,7 @@ cfg_if! {
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
let routes = generate_route_list(|cx| view! { cx, <Counters/> });
let routes = generate_route_list(|| view! { <Counters/> });
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
@@ -52,36 +52,15 @@ cfg_if! {
App::new()
.service(counter_events)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
// serve JS/WASM/CSS from `pkg`
.service(Files::new("/pkg", format!("{site_root}/pkg")))
// serve other assets from the `assets` directory
.service(Files::new("/assets", site_root))
// serve the favicon from /favicon.ico
.service(favicon)
.leptos_routes(
leptos_options.to_owned(),
routes.to_owned(),
Counters,
)
.app_data(web::Data::new(leptos_options.to_owned()))
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), || view! { <Counters/> })
.service(Files::new("/", site_root))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()
.await
}
#[actix_web::get("favicon.ico")]
async fn favicon(
leptos_options: actix_web::web::Data<leptos::LeptosOptions>,
) -> actix_web::Result<actix_files::NamedFile> {
let leptos_options = leptos_options.into_inner();
let site_root = &leptos_options.site_root;
Ok(actix_files::NamedFile::open(format!(
"{site_root}/favicon.ico"
))?)
}
}
// client-only main for Trunk
else {

View File

@@ -5,13 +5,13 @@ use leptos_router::*;
///
/// You can use doc comments like this to document your component.
#[component]
pub fn SimpleQueryCounter(cx: Scope) -> impl IntoView {
let (count, set_count) = create_query_signal::<i32>(cx, "count");
pub fn SimpleQueryCounter() -> impl IntoView {
let (count, set_count) = create_query_signal::<i32>("count");
let clear = move |_| set_count(None);
let decrement = move |_| set_count(Some(count().unwrap_or(0) - 1));
let increment = move |_| set_count(Some(count().unwrap_or(0) + 1));
let (msg, set_msg) = create_query_signal::<String>(cx, "message");
let (msg, set_msg) = create_query_signal::<String>("message");
let update_msg = move |ev| {
let new_msg = event_target_value(&ev);
if new_msg.is_empty() {
@@ -21,7 +21,7 @@ pub fn SimpleQueryCounter(cx: Scope) -> impl IntoView {
}
};
view! { cx,
view! {
<div>
<button on:click=clear>"Clear"</button>
<button on:click=decrement>"-1"</button>

View File

@@ -5,8 +5,8 @@ use leptos_router::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! { cx,
mount_to_body(|| {
view! {
<Router>
<Routes>
<Route path="" view=SimpleQueryCounter />

View File

@@ -2,40 +2,38 @@ use leptos::{ev, html::*, *};
/// A simple counter view.
// A component is really just a function call: it runs once to create the DOM and reactive system
pub fn counter(cx: Scope, initial_value: i32, step: u32) -> impl IntoView {
let (count, set_count) = create_signal(cx, Count::new(initial_value, step));
pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
let (count, set_count) = create_signal(Count::new(initial_value, step));
// elements are created by calling a function with a Scope argument
// the function name is the same as the HTML tag name
div(cx)
div()
// children can be added with .child()
// this takes any type that implements IntoView as its argument
// for example, a string or an HtmlElement<_>
.child(
button(cx)
// it can also take an array of types that impl IntoView
// or a tuple of up to 26 objects that impl IntoView
.child((
button()
// typed events found in leptos::ev
// 1) prevent typos in event names
// 2) allow for correct type inference in callbacks
.on(ev::click, move |_| set_count.update(|count| count.clear()))
.child("Clear"),
)
.child(
button(cx)
button()
.on(ev::click, move |_| {
set_count.update(|count| count.decrease())
})
.child("-1"),
)
.child(
span(cx)
span()
.child("Value: ")
// reactive values are passed to .child() as a tuple
// (Scope, [child function]) so an effect can be created
.child(move || count.get().value())
.child("!"),
)
))
.child(
button(cx)
button()
.on(ev::click, move |_| {
set_count.update(|count| count.increase())
})

View File

@@ -5,5 +5,5 @@ use leptos::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| counter(cx, 0, 1))
mount_to_body(|| counter(0, 1))
}

View File

@@ -41,7 +41,7 @@ fn should_clear_counter() {
fn open_counter() {
remove_existing_counter();
mount_to_body(move |cx| counter(cx, 0, 1));
mount_to_body(move || counter(0, 1));
}
fn remove_existing_counter() {

View File

@@ -10,14 +10,14 @@ struct CounterUpdater {
}
#[component]
pub fn Counters(cx: Scope) -> impl IntoView {
let (next_counter_id, set_next_counter_id) = create_signal(cx, 0);
let (counters, set_counters) = create_signal::<CounterHolder>(cx, vec![]);
provide_context(cx, CounterUpdater { set_counters });
pub fn Counters() -> impl IntoView {
let (next_counter_id, set_next_counter_id) = create_signal(0);
let (counters, set_counters) = create_signal::<CounterHolder>(vec![]);
provide_context(CounterUpdater { set_counters });
let add_counter = move |_| {
let id = next_counter_id();
let sig = create_signal(cx, 0);
let sig = create_signal(0);
set_counters.update(move |counters| counters.push((id, sig)));
set_next_counter_id.update(|id| *id += 1);
};
@@ -25,7 +25,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
let add_many_counters = move |_| {
let next_id = next_counter_id();
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
let signal = create_signal(cx, 0);
let signal = create_signal(0);
(id, signal)
});
@@ -37,7 +37,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
set_counters.update(|counters| counters.clear());
};
view! { cx,
view! {
<div>
<button on:click=add_counter>
"Add Counter"
@@ -65,8 +65,8 @@ pub fn Counters(cx: Scope) -> impl IntoView {
<For
each=counters
key=|counter| counter.0
view=move |cx, (id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
view! { cx,
view=move |(id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
view! {
<Counter id value set_value/>
}
}
@@ -78,12 +78,11 @@ pub fn Counters(cx: Scope) -> impl IntoView {
#[component]
fn Counter(
cx: Scope,
id: usize,
value: ReadSignal<i32>,
set_value: WriteSignal<i32>,
) -> impl IntoView {
let CounterUpdater { set_counters } = use_context(cx).unwrap();
let CounterUpdater { set_counters } = use_context().unwrap();
let input = move |ev| {
set_value(event_target_value(&ev).parse::<i32>().unwrap_or_default())
@@ -91,9 +90,9 @@ fn Counter(
// just an example of how a cleanup function works
// this will run when the scope is disposed, i.e., when this row is deleted
on_cleanup(cx, || log::debug!("deleted a row"));
on_cleanup(|| log::debug!("deleted a row"));
view! { cx,
view! {
<li>
<button on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
<input type="text"

View File

@@ -4,5 +4,5 @@ use leptos::*;
fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| view! { cx, <Counters/> })
mount_to_body(|| view! { <Counters/> })
}

View File

@@ -8,7 +8,7 @@ use web_sys::HtmlElement;
#[wasm_bindgen_test]
fn inc() {
mount_to_body(|cx| view! { cx, <Counters/> });
mount_to_body(|| view! { <Counters/> });
let document = leptos::document();
let div = document.query_selector("div").unwrap().unwrap();

View File

@@ -5,15 +5,12 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos", features = ["csr"] }
leptos_meta = { path = "../../meta", features = ["csr"] }
log = "0.4"
console_log = "1"
console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen = "0.2.87"
wasm-bindgen-test = "0.3.37"
pretty_assertions = "1.3.0"
wasm-bindgen-test = "0.3.0"
[dev-dependencies.web-sys]
features = [

View File

@@ -1,6 +1,5 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/wasm-test.toml" },
{ path = "../cargo-make/trunk_server.toml" },
{ path = "../cargo-make/playwright-test.toml" },
]

View File

@@ -1,109 +0,0 @@
use leptos::*;
use leptos_meta::*;
const MANY_COUNTERS: usize = 1000;
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
#[derive(Copy, Clone)]
struct CounterUpdater {
set_counters: WriteSignal<CounterHolder>,
}
#[component]
pub fn Counters(cx: Scope) -> impl IntoView {
let (next_counter_id, set_next_counter_id) = create_signal(cx, 0);
let (counters, set_counters) = create_signal::<CounterHolder>(cx, vec![]);
provide_context(cx, CounterUpdater { set_counters });
provide_meta_context(cx);
let add_counter = move |_| {
let id = next_counter_id.get();
let sig = create_signal(cx, 0);
set_counters.update(move |counters| counters.push((id, sig)));
set_next_counter_id.update(|id| *id += 1);
};
let add_many_counters = move |_| {
let next_id = next_counter_id.get();
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
let signal = create_signal(cx, 0);
(id, signal)
});
set_counters.update(move |counters| counters.extend(new_counters));
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
};
let clear_counters = move |_| {
set_counters.update(|counters| counters.clear());
};
view! { cx,
<Title text="Counters (Stable)" />
<div>
<button on:click=add_counter>
"Add Counter"
</button>
<button on:click=add_many_counters>
{format!("Add {MANY_COUNTERS} Counters")}
</button>
<button on:click=clear_counters>
"Clear Counters"
</button>
<p>
"Total: "
<span data-testid="total">{move ||
counters.get()
.iter()
.map(|(_, (count, _))| count.get())
.sum::<i32>()
.to_string()
}</span>
" from "
<span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
" counters."
</p>
<ul>
<For
each={move || counters.get()}
key={|counter| counter.0}
view=move |cx, (id, (value, set_value))| {
view! {
cx,
<Counter id value set_value/>
}
}
/>
</ul>
</div>
}
}
#[component]
fn Counter(
cx: Scope,
id: usize,
value: ReadSignal<i32>,
set_value: WriteSignal<i32>,
) -> impl IntoView {
let CounterUpdater { set_counters } = use_context(cx).unwrap();
let input = move |ev| {
set_value
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
};
view! { cx,
<li>
<button data-testid="decrement_count" on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
<input data-testid="counter_input" type="text"
prop:value={move || value.get().to_string()}
on:input=input
/>
<span>{value}</span>
<button data-testid="increment_count" on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
<button data-testid="remove_counter" on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
</li>
}
}

View File

@@ -1,8 +1,110 @@
use counters_stable::Counters;
use leptos::*;
fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| view! { cx, <Counters/> })
mount_to_body(|| view! { <Counters/> })
}
const MANY_COUNTERS: usize = 1000;
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
#[derive(Copy, Clone)]
struct CounterUpdater {
set_counters: WriteSignal<CounterHolder>,
}
#[component]
pub fn Counters() -> impl IntoView {
let (next_counter_id, set_next_counter_id) = create_signal(0);
let (counters, set_counters) = create_signal::<CounterHolder>(vec![]);
provide_context(CounterUpdater { set_counters });
let add_counter = move |_| {
let id = next_counter_id.get();
let sig = create_signal(0);
set_counters.update(move |counters| counters.push((id, sig)));
set_next_counter_id.update(|id| *id += 1);
};
let add_many_counters = move |_| {
let next_id = next_counter_id.get();
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
let signal = create_signal(0);
(id, signal)
});
set_counters.update(move |counters| counters.extend(new_counters));
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
};
let clear_counters = move |_| {
set_counters.update(|counters| counters.clear());
};
view! {
<div>
<button on:click=add_counter>
"Add Counter"
</button>
<button on:click=add_many_counters>
{format!("Add {MANY_COUNTERS} Counters")}
</button>
<button on:click=clear_counters>
"Clear Counters"
</button>
<p>
"Total: "
<span data-testid="total">{move ||
counters.get()
.iter()
.map(|(_, (count, _))| count.get())
.sum::<i32>()
.to_string()
}</span>
" from "
<span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
" counters."
</p>
<ul>
<For
each={move || counters.get()}
key={|counter| counter.0}
view=move |(id, (value, set_value))| {
view! {
<Counter id value set_value/>
}
}
/>
</ul>
</div>
}
}
#[component]
fn Counter(
id: usize,
value: ReadSignal<i32>,
set_value: WriteSignal<i32>,
) -> impl IntoView {
let CounterUpdater { set_counters } = use_context().unwrap();
let input = move |ev| {
set_value
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
};
view! {
<li>
<button id="decrement_count" on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
<input type="text"
prop:value={move || value.get().to_string()}
on:input=input
/>
<span>{value}</span>
<button id="increment_count" on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
<button on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
</li>
}
}

View File

@@ -1,17 +0,0 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_increase_the_number_of_counters() {
// Given
ui::view_counters();
// When
ui::add_1k_counters();
ui::add_1k_counters();
ui::add_1k_counters();
// Then
assert_eq!(ui::counters(), 3000);
}

View File

@@ -1,17 +0,0 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_increase_the_number_of_counters() {
// Given
ui::view_counters();
// When
ui::add_counter();
ui::add_counter();
ui::add_counter();
// Then
assert_eq!(ui::counters(), 3);
}

View File

@@ -1,19 +0,0 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_reset_the_counts() {
// Given
ui::view_counters();
ui::add_counter();
ui::add_counter();
ui::add_counter();
// When
ui::clear_counters();
// Then
assert_eq!(ui::total(), 0);
assert_eq!(ui::counters(), 0);
}

View File

@@ -1,18 +0,0 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_decrease_the_total_count() {
// Given
ui::view_counters();
ui::add_counter();
// When
ui::decrement_counter(1);
ui::decrement_counter(1);
ui::decrement_counter(1);
// Then
assert_eq!(ui::total(), -3);
}

View File

@@ -1,112 +0,0 @@
use counters_stable::Counters;
use leptos::*;
use wasm_bindgen::JsCast;
use web_sys::{Element, Event, EventInit, HtmlElement, HtmlInputElement};
// Actions
pub fn add_1k_counters() {
find_by_text("Add 1000 Counters").click();
}
pub fn add_counter() {
find_by_text("Add Counter").click();
}
pub fn clear_counters() {
find_by_text("Clear Counters").click();
}
pub fn decrement_counter(index: u32) {
counter_html_element(index, "decrement_count").click();
}
pub fn enter_count(index: u32, count: i32) {
let input = counter_input_element(index, "counter_input");
input.set_value(count.to_string().as_str());
let mut event_init = EventInit::new();
event_init.bubbles(true);
let event = Event::new_with_event_init_dict("input", &event_init).unwrap();
input.dispatch_event(&event).unwrap();
}
pub fn increment_counter(index: u32) {
counter_html_element(index, "increment_count").click();
}
pub fn remove_counter(index: u32) {
counter_html_element(index, "remove_counter").click();
}
pub fn view_counters() {
remove_existing_counters();
mount_to_body(|cx| view! { cx, <Counters/> });
}
// Results
pub fn counters() -> i32 {
data_test_id("counters").parse::<i32>().unwrap()
}
pub fn title() -> String {
leptos::document().title()
}
pub fn total() -> i32 {
data_test_id("total").parse::<i32>().unwrap()
}
// Internal
fn counter_element(index: u32, text: &str) -> Element {
let selector =
format!("li:nth-child({}) [data-testid=\"{}\"]", index, text);
leptos::document()
.query_selector(&selector)
.unwrap()
.unwrap()
}
fn counter_html_element(index: u32, text: &str) -> HtmlElement {
counter_element(index, text)
.dyn_into::<HtmlElement>()
.unwrap()
}
fn counter_input_element(index: u32, text: &str) -> HtmlInputElement {
counter_element(index, text)
.dyn_into::<HtmlInputElement>()
.unwrap()
}
fn data_test_id(id: &str) -> String {
let selector = format!("[data-testid=\"{}\"]", id);
leptos::document()
.query_selector(&selector)
.unwrap()
.expect("counters not found")
.text_content()
.unwrap()
}
fn find_by_text(text: &str) -> HtmlElement {
let xpath = format!("//*[text()='{}']", text);
let document = leptos::document();
document
.evaluate(&xpath, &document)
.unwrap()
.iterate_next()
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
}
fn remove_existing_counters() {
if let Some(counter) =
leptos::document().query_selector("body div").unwrap()
{
counter.remove();
}
}

View File

@@ -1 +0,0 @@
pub mod counters_page;

View File

@@ -1,18 +0,0 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_increase_the_total_count() {
// Given
ui::view_counters();
ui::add_counter();
// When
ui::increment_counter(1);
ui::increment_counter(1);
ui::increment_counter(1);
// Then
assert_eq!(ui::total(), 3);
}

View File

@@ -1,16 +0,0 @@
use wasm_bindgen_test::*;
// Test Suites
pub mod add_1k_counters;
pub mod add_counter;
pub mod clear_counters;
pub mod decrement_counter;
pub mod enter_count;
pub mod increment_counter;
pub mod remove_counter;
pub mod view_counters;
pub mod fixtures;
pub use fixtures::*;
wasm_bindgen_test_configure!(run_in_browser);

View File

@@ -1,18 +0,0 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_decrement_the_number_of_counters() {
// Given
ui::view_counters();
ui::add_counter();
ui::add_counter();
ui::add_counter();
// When
ui::remove_counter(2);
// Then
assert_eq!(ui::counters(), 2);
}

View File

@@ -1,22 +0,0 @@
use super::*;
use crate::counters_page as ui;
use pretty_assertions::assert_eq;
#[wasm_bindgen_test]
fn should_see_the_initial_counts() {
// When
ui::view_counters();
// Then
assert_eq!(ui::total(), 0);
assert_eq!(ui::counters(), 0);
}
#[wasm_bindgen_test]
fn should_see_the_title() {
// When
ui::view_counters();
// Then
assert_eq!(ui::title(), "Counters (Stable)");
}

View File

@@ -1,23 +1,23 @@
use leptos::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, Ok(0));
pub fn App() -> impl IntoView {
let (value, set_value) = create_signal(Ok(0));
// when input changes, try to parse a number from the input
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
view! { cx,
view! {
<h1>"Error Handling"</h1>
<label>
"Type a number (or something that's not a number!)"
<input on:input=on_input/>
<input type="number" on:input=on_input/>
// If an `Err(_) had been rendered inside the <ErrorBoundary/>,
// the fallback will be displayed. Otherwise, the children of the
// <ErrorBoundary/> will be displayed.
<ErrorBoundary
// the fallback receives a signal containing current errors
fallback=|cx, errors| view! { cx,
fallback=|errors| view! {
<div class="error">
<p>"Not a number! Errors: "</p>
// we can render a list of errors
@@ -25,8 +25,8 @@ pub fn App(cx: Scope) -> impl IntoView {
<ul>
{move || errors.get()
.into_iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
.collect_view(cx)
.map(|(_, e)| view! { <li>{e.to_string()}</li>})
.collect_view()
}
</ul>
</div>

View File

@@ -4,8 +4,8 @@ use leptos::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! { cx,
mount_to_body(|| {
view! {
<App/>
}
})

View File

@@ -8,12 +8,11 @@ use leptos_axum::ResponseOptions;
// Feel free to do more complicated things here than just displaying them.
#[component]
pub fn ErrorTemplate(
cx: Scope,
#[prop(optional)] outside_errors: Option<Errors>,
#[prop(optional)] errors: Option<RwSignal<Errors>>,
) -> impl IntoView {
let errors = match outside_errors {
Some(e) => create_rw_signal(cx, e),
Some(e) => create_rw_signal(e),
None => match errors {
Some(e) => e,
None => panic!("No Errors found and we expected errors!"),
@@ -23,7 +22,7 @@ pub fn ErrorTemplate(
// Get Errors from Signal
// Downcast lets us take a type that implements `std::error::Error`
let errors: Vec<AppError> = errors
.get()
.get_untracked()
.into_iter()
.filter_map(|(_, v)| v.downcast_ref::<AppError>().cloned())
.collect();
@@ -32,13 +31,13 @@ pub fn ErrorTemplate(
// Only the response code for the first error is actually sent from the server
// this may be customized by the specific application
cfg_if! { if #[cfg(feature="ssr")] {
let response = use_context::<ResponseOptions>(cx);
let response = use_context::<ResponseOptions>();
if let Some(response) = response {
response.set_status(errors[0].status_code());
}
}}
view! { cx,
view! {
<h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
@@ -46,10 +45,10 @@ pub fn ErrorTemplate(
// a unique key for each item as a reference
key=|(index, _)| *index
// renders each item to a view
view=move |cx, error| {
view=move |error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! { cx,
view! {
<h2>{error_code.to_string()}</h2>
<p>"Error: " {error_string}</p>
}

View File

@@ -22,7 +22,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
} else{
let handler = leptos_axum::render_app_to_stream(
options.to_owned(),
move |cx| view!{ cx, <App/> }
move || view!{ <App/> }
);
handler(req).await.into_response()
}

View File

@@ -14,20 +14,20 @@ pub async fn cause_internal_server_error() -> Result<(), ServerFnError> {
}
#[component]
pub fn App(cx: Scope) -> impl IntoView {
//let id = use_context::<String>(cx);
provide_meta_context(cx);
pub fn App() -> impl IntoView {
//let id = use_context::<String>();
provide_meta_context();
view! {
cx,
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/errors_axum.css"/>
<Router fallback=|cx| {
<Router fallback=|| {
let mut outside_errors = Errors::default();
outside_errors.insert_with_default_key(AppError::NotFound);
view! { cx,
view! {
<ErrorTemplate outside_errors/>
}
.into_view(cx)
.into_view()
}>
<header>
<h1>"Error Examples:"</h1>
@@ -42,11 +42,11 @@ pub fn App(cx: Scope) -> impl IntoView {
}
#[component]
pub fn ExampleErrors(cx: Scope) -> impl IntoView {
pub fn ExampleErrors() -> impl IntoView {
let generate_internal_error =
create_server_action::<CauseInternalServerError>(cx);
create_server_action::<CauseInternalServerError>();
view! { cx,
view! {
<p>
"These links will load 404 pages since they do not exist. Verify with browser development tools: " <br/>
<a href="/404">"This links to a page that does not exist"</a><br/>
@@ -63,7 +63,7 @@ pub fn ExampleErrors(cx: Scope) -> impl IntoView {
// note that the error boundaries could be placed above in the Router or lower down
// in a particular route. The generated errors on the entire page contribute to the
// final status code sent by the server when producing ssr pages.
<ErrorBoundary fallback=|cx, errors| view!{ cx, <ErrorTemplate errors=errors/>}>
<ErrorBoundary fallback=|errors| view!{ <ErrorTemplate errors=errors/>}>
<ReturnsError/>
</ErrorBoundary>
</div>
@@ -71,6 +71,6 @@ pub fn ExampleErrors(cx: Scope) -> impl IntoView {
}
#[component]
pub fn ReturnsError(_cx: Scope) -> impl IntoView {
pub fn ReturnsError() -> impl IntoView {
Err::<String, AppError>(AppError::InternalServerError)
}

View File

@@ -16,8 +16,8 @@ cfg_if! {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(|cx| {
view! { cx, <App/> }
leptos::mount_to_body(|| {
view! { <App/> }
});
}
}

View File

@@ -25,10 +25,10 @@ async fn custom_handler(
) -> Response {
let handler = leptos_axum::render_app_to_stream_with_context(
options.clone(),
move |cx| {
provide_context(cx, id.clone());
move || {
provide_context(id.clone());
},
|cx| view! { cx, <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(|cx| view! { cx, <App/> }).await;
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, |cx| view! { cx, <App/> })
.leptos_routes(&leptos_options, routes, || view! { <App/> })
.fallback(file_and_error_handler)
.with_state(leptos_options);

View File

@@ -37,26 +37,26 @@ async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
}
}
pub fn fetch_example(cx: Scope) -> impl IntoView {
let (cat_count, set_cat_count) = create_signal::<CatCount>(cx, 0);
pub fn fetch_example() -> impl IntoView {
let (cat_count, set_cat_count) = create_signal::<CatCount>(0);
// we use local_resource here because
// 1) our error type isn't serializable/deserializable
// 2) we're not doing server-side rendering in this example anyway
// (during SSR, create_resource will begin loading on the server and resolve on the client)
let cats = create_local_resource(cx, cat_count, fetch_cats);
let cats = create_local_resource(cat_count, fetch_cats);
let fallback = move |cx, errors: RwSignal<Errors>| {
let fallback = move |errors: RwSignal<Errors>| {
let error_list = move || {
errors.with(|errors| {
errors
.iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li> })
.collect_view(cx)
.map(|(_, e)| view! { <li>{e.to_string()}</li> })
.collect_view()
})
};
view! { cx,
view! {
<div class="error">
<h2>"Error"</h2>
<ul>{error_list}</ul>
@@ -67,18 +67,16 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
// the renderer can handle Option<_> and Result<_> states
// by displaying nothing for None if the resource is still loading
// and by using the ErrorBoundary fallback to catch Err(_)
// so we'll just implement our happy path and let the framework handle the rest
// so we'll just use `.and_then()` to map over the happy path
let cats_view = move || {
cats.read(cx).map(|data| {
data.map(|data| {
data.iter()
.map(|s| view! { cx, <p><img src={s}/></p> })
.collect_view(cx)
})
cats.and_then(|data| {
data.iter()
.map(|s| view! { <p><img src={s}/></p> })
.collect_view()
})
};
view! { cx,
view! {
<div>
<label>
"How many cats would you like?"
@@ -93,7 +91,7 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
</label>
<ErrorBoundary fallback>
<Transition fallback=move || {
view! { cx, <div>"Loading (Suspense Fallback)..."</div> }
view! { <div>"Loading (Suspense Fallback)..."</div> }
}>
<div>
{cats_view}

View File

@@ -1,25 +1,23 @@
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Button};
use gtk::{prelude::*, Application, ApplicationWindow, Button};
use leptos::*;
const APP_ID: &str = "dev.leptos.Counter";
// Basic GTK app setup from https://gtk-rs.org/gtk4-rs/stable/latest/book/hello_world.html
fn main() {
_ = create_scope(create_runtime(), |cx| {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
let _ = create_runtime();
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(move |app| build_ui(cx, app));
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run();
});
// Run the application
app.run();
}
fn build_ui(cx: Scope, app: &Application) {
let button = counter_button(cx);
fn build_ui(app: &Application) {
let button = counter_button();
// Create a window and set the title
let window = ApplicationWindow::builder()
@@ -32,8 +30,8 @@ fn build_ui(cx: Scope, app: &Application) {
window.present();
}
fn counter_button(cx: Scope) -> Button {
let (value, set_value) = create_signal(cx, 0);
fn counter_button() -> Button {
let (value, set_value) = create_signal(0);
// Create a button with label and margins
let button = Button::builder()
@@ -50,7 +48,7 @@ fn counter_button(cx: Scope) -> Button {
set_value.update(|value| *value += 1);
});
create_effect(cx, {
create_effect({
let button = button.clone();
move |_| {
button.set_label(&format!("Count: {}", value()));

View File

@@ -16,7 +16,7 @@ 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", "islands"] }
leptos_meta = { path = "../../meta", features = ["nightly"] }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_router = { path = "../../router", features = ["nightly"] }
@@ -41,6 +41,12 @@ ssr = [
"leptos_router/ssr",
]
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
[package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web", "leptos_actix"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
@@ -88,3 +94,5 @@ lib-features = ["hydrate"]
#
# Optional. Defaults to false.
lib-default-features = false
lib-profile-release = "wasm-release"

View File

@@ -1,4 +1,4 @@
use leptos::{Scope, Serializable};
use leptos::Serializable;
use serde::{Deserialize, Serialize};
pub fn story(path: &str) -> String {
@@ -10,7 +10,7 @@ pub fn user(path: &str) -> String {
}
#[cfg(not(feature = "ssr"))]
pub async fn fetch_api<T>(cx: Scope, path: &str) -> Option<T>
pub async fn fetch_api<T>(path: &str) -> Option<T>
where
T: Serializable,
{
@@ -19,7 +19,7 @@ where
// abort in-flight requests if the Scope is disposed
// i.e., if we've navigated away from this page
leptos::on_cleanup(cx, move || {
leptos::on_cleanup(move || {
if let Some(abort_controller) = abort_controller {
abort_controller.abort()
}
@@ -39,7 +39,7 @@ where
}
#[cfg(feature = "ssr")]
pub async fn fetch_api<T>(_cx: Scope, path: &str) -> Option<T>
pub async fn fetch_api<T>(path: &str) -> Option<T>
where
T: Serializable,
{

View File

@@ -7,18 +7,20 @@ mod routes;
use routes::{nav::*, stories::*, story::*, users::*};
#[component]
pub fn App(cx: Scope) -> impl IntoView {
provide_meta_context(cx);
let (is_routing, set_is_routing) = create_signal(cx, false);
pub fn App() -> impl IntoView {
provide_meta_context();
let (is_routing, set_is_routing) = create_signal(false);
view! { cx,
view! {
<Stylesheet id="leptos" href="/pkg/hackernews.css"/>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
// adding `set_is_routing` causes the router to wait for async data to load on new pages
<Router set_is_routing>
// shows a progress bar while async data are loading
<RoutingProgress is_routing max_time=std::time::Duration::from_millis(250)/>
<div class="routing-progress">
<RoutingProgress is_routing max_time=std::time::Duration::from_millis(250)/>
</div>
<Nav />
<main>
<Routes>
@@ -40,9 +42,7 @@ cfg_if! {
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(move |cx| {
view! { cx, <App/> }
});
leptos::mount_to_body(App);
}
}
}

View File

@@ -14,6 +14,10 @@ cfg_if! {
async fn css() -> impl Responder {
actix_files::NamedFile::open_async("./style.css").await
}
#[get("/favicon.ico")]
async fn favicon() -> impl Responder {
actix_files::NamedFile::open_async("./target/site//favicon.ico").await
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
@@ -22,48 +26,31 @@ cfg_if! {
let addr = conf.leptos_options.site_addr;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|cx| view! { cx, <App/> });
let routes = generate_route_list(App);
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
let site_root = &leptos_options.site_root;
App::new()
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.service(Files::new("/pkg", format!("{site_root}/pkg")))
.service(Files::new("/assets", site_root))
.service(favicon)
.service(css)
.leptos_routes(
leptos_options.to_owned(),
routes.to_owned(),
|cx| view! { cx, <App/> },
)
.app_data(web::Data::new(leptos_options.to_owned()))
.service(favicon)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
.service(Files::new("/", site_root))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()
.await
}
#[actix_web::get("favicon.ico")]
async fn favicon(
leptos_options: actix_web::web::Data<leptos::LeptosOptions>,
) -> actix_web::Result<actix_files::NamedFile> {
let leptos_options = leptos_options.into_inner();
let site_root = &leptos_options.site_root;
Ok(actix_files::NamedFile::open(format!(
"{site_root}/favicon.ico"
))?)
}
} else {
fn main() {
use hackernews::{App};
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| view! { cx, <App/> })
mount_to_body(App)
}
}
}

View File

@@ -1,9 +1,9 @@
use leptos::{component, view, IntoView, Scope};
use leptos::{component, view, IntoView};
use leptos_router::*;
#[component]
pub fn Nav(cx: Scope) -> impl IntoView {
view! { cx,
pub fn Nav() -> impl IntoView {
view! {
<header class="header">
<nav class="inner">
<A href="/home">

View File

@@ -13,9 +13,9 @@ fn category(from: &str) -> &'static str {
}
#[component]
pub fn Stories(cx: Scope) -> impl IntoView {
let query = use_query_map(cx);
let params = use_params_map(cx);
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()))
@@ -27,28 +27,27 @@ pub fn Stories(cx: Scope) -> impl IntoView {
.unwrap_or_else(|| "top".to_string())
};
let stories = create_resource(
cx,
move || (page(), story_type()),
move |(page, story_type)| async move {
let path = format!("{}?page={}", category(&story_type), page);
api::fetch_api::<Vec<api::Story>>(cx, &api::story(&path)).await
api::fetch_api::<Vec<api::Story>>(&api::story(&path)).await
},
);
let (pending, set_pending) = create_signal(cx, false);
let (pending, set_pending) = create_signal(false);
let hide_more_link = move |cx| {
let hide_more_link = move || {
pending()
|| stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28
|| stories.get().unwrap_or(None).unwrap_or_default().len() < 28
};
view! {
cx,
<div class="news-view">
<div class="news-list-nav">
<span>
{move || if page() > 1 {
view! {
cx,
<a class="page-link"
href=move || format!("/{}?page={}", story_type(), page() - 1)
attr:aria_label="Previous Page"
@@ -58,7 +57,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
}.into_any()
} else {
view! {
cx,
<span class="page-link disabled" aria-hidden="true">
"< prev"
</span>
@@ -67,11 +66,11 @@ pub fn Stories(cx: Scope) -> impl IntoView {
</span>
<span>"page " {page}</span>
<Transition
fallback=move || view! { cx, <p>"Loading..."</p> }
fallback=move || view! { <p>"Loading..."</p> }
>
<span class="page-link"
class:disabled=move || hide_more_link(cx)
aria-hidden=move || hide_more_link(cx)
class:disabled=hide_more_link
aria-hidden=hide_more_link
>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
@@ -84,20 +83,20 @@ pub fn Stories(cx: Scope) -> impl IntoView {
<main class="news-list">
<div>
<Transition
fallback=move || view! { cx, <p>"Loading..."</p> }
fallback=move || view! { <p>"Loading..."</p> }
set_pending=set_pending.into()
>
{move || match stories.read(cx) {
{move || match stories.get() {
None => None,
Some(None) => Some(view! { cx, <p>"Error loading stories."</p> }.into_any()),
Some(None) => Some(view! { <p>"Error loading stories."</p> }.into_any()),
Some(Some(stories)) => {
Some(view! { cx,
Some(view! {
<ul>
<For
each=move || stories.clone()
key=|story| story.id
view=move |cx, story: api::Story| {
view! { cx,
view=move |story: api::Story| {
view! {
<Story story/>
}
}
@@ -114,32 +113,32 @@ pub fn Stories(cx: Scope) -> impl IntoView {
}
#[component]
fn Story(cx: Scope, story: api::Story) -> impl IntoView {
view! { cx,
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! { cx,
view! {
<span>
<a href=story.url target="_blank" rel="noreferrer">
{story.title.clone()}
</a>
<span class="host">"("{story.domain}")"</span>
</span>
}.into_view(cx)
}.into_view()
} else {
let title = story.title.clone();
view! { cx, <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }.into_view(cx)
view! { <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }.into_view()
}}
</span>
<br />
<span class="meta">
{if story.story_type != "job" {
view! { cx,
view! {
<span>
{"by "}
{story.user.map(|user| view ! { cx, <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 {
@@ -149,13 +148,13 @@ fn Story(cx: Scope, story: api::Story) -> impl IntoView {
}}
</A>
</span>
}.into_view(cx)
}.into_view()
} else {
let title = story.title.clone();
view! { cx, <A href=format!("/item/{}", story.id)>{title.clone()}</A> }.into_view(cx)
view! { <A href=format!("/item/{}", story.id)>{title.clone()}</A> }.into_view()
}}
</span>
{(story.story_type != "link").then(|| view! { cx,
{(story.story_type != "link").then(|| view! {
" "
<span class="label">{story.story_type}</span>
})}

View File

@@ -4,37 +4,33 @@ use leptos_meta::*;
use leptos_router::*;
#[component]
pub fn Story(cx: Scope) -> impl IntoView {
let params = use_params_map(cx);
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = create_resource(
cx,
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move {
if id.is_empty() {
None
} else {
api::fetch_api::<api::Story>(
cx,
&api::story(&format!("item/{id}")),
)
.await
api::fetch_api::<api::Story>(&api::story(&format!("item/{id}")))
.await
}
},
);
let meta_description = move || {
story
.read(cx)
.get()
.and_then(|story| story.map(|story| story.title))
.unwrap_or_else(|| "Loading story...".to_string())
};
view! { cx,
view! {
<>
<Meta name="description" content=meta_description/>
<Suspense fallback=|| view! { cx, "Loading..." }>
{move || story.read(cx).map(|story| match story {
None => view! { cx, <div class="item-view">"Error loading this story."</div> },
Some(story) => view! { cx,
<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">
@@ -43,7 +39,7 @@ pub fn Story(cx: Scope) -> impl IntoView {
<span class="host">
"("{story.domain}")"
</span>
{story.user.map(|user| view! { cx, <p class="meta">
{story.user.map(|user| view! { <p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
@@ -62,7 +58,7 @@ pub fn Story(cx: Scope) -> impl IntoView {
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
view=move |cx, comment| view! { cx, <Comment comment /> }
view=move |comment| view! { <Comment comment /> }
/>
</ul>
</div>
@@ -75,10 +71,10 @@ pub fn Story(cx: Scope) -> impl IntoView {
}
#[component]
pub fn Comment(cx: Scope, comment: api::Comment) -> impl IntoView {
let (open, set_open) = create_signal(cx, true);
pub fn Comment(comment: api::Comment) -> impl IntoView {
let (open, set_open) = create_signal(true);
view! { cx,
view! {
<li class="comment">
<div class="by">
<A href=format!("/users/{}", comment.user.clone().unwrap_or_default())>{comment.user.clone()}</A>
@@ -86,7 +82,7 @@ pub fn Comment(cx: Scope, comment: api::Comment) -> impl IntoView {
</div>
<div class="text" inner_html=comment.content></div>
{(!comment.comments.is_empty()).then(|| {
view! { cx,
view! {
<div>
<div class="toggle" class:open=open>
<a on:click=move |_| set_open.update(|n| *n = !*n)>
@@ -102,12 +98,12 @@ pub fn Comment(cx: Scope, comment: api::Comment) -> impl IntoView {
</div>
{move || open().then({
let comments = comment.comments.clone();
move || view! { cx,
move || view! {
<ul class="comment-children">
<For
each=move || comments.clone()
key=|comment| comment.id
view=move |cx, comment: api::Comment| view! { cx, <Comment comment /> }
view=move |comment: api::Comment| view! { <Comment comment /> }
/>
</ul>
}

View File

@@ -3,25 +3,24 @@ use leptos::*;
use leptos_router::*;
#[component]
pub fn User(cx: Scope) -> impl IntoView {
let params = use_params_map(cx);
pub fn User() -> impl IntoView {
let params = use_params_map();
let user = create_resource(
cx,
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move {
if id.is_empty() {
None
} else {
api::fetch_api::<User>(cx, &api::user(&id)).await
api::fetch_api::<User>(&api::user(&id)).await
}
},
);
view! { cx,
view! {
<div class="user-view">
<Suspense fallback=|| view! { cx, "Loading..." }>
{move || user.read(cx).map(|user| match user {
None => view! { cx, <h1>"User not found."</h1> }.into_any(),
Some(user) => view! { cx,
<Suspense fallback=|| view! { "Loading..." }>
{move || user.get().map(|user| match user {
None => view! { <h1>"User not found."</h1> }.into_view(),
Some(user) => view! {
<div>
<h1>"User: " {&user.id}</h1>
<ul class="meta">
@@ -39,7 +38,7 @@ pub fn User(cx: Scope) -> impl IntoView {
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
</p>
</div>
}.into_any()
}.into_view()
})}
</Suspense>
</div>

View File

@@ -323,4 +323,9 @@ a {
.user-view .links a {
text-decoration: underline
}
.routing-progress, .routing-progress progress {
height: 10px;
width: 100%;
}

View File

@@ -1,4 +1,4 @@
use leptos::{Scope, Serializable};
use leptos::Serializable;
use serde::{Deserialize, Serialize};
pub fn story(path: &str) -> String {
@@ -10,7 +10,7 @@ pub fn user(path: &str) -> String {
}
#[cfg(not(feature = "ssr"))]
pub async fn fetch_api<T>(cx: Scope, path: &str) -> Option<T>
pub async fn fetch_api<T>(path: &str) -> Option<T>
where
T: Serializable,
{
@@ -19,7 +19,7 @@ where
// abort in-flight requests if the Scope is disposed
// i.e., if we've navigated away from this page
leptos::on_cleanup(cx, move || {
leptos::on_cleanup(move || {
if let Some(abort_controller) = abort_controller {
abort_controller.abort()
}
@@ -39,7 +39,7 @@ where
}
#[cfg(feature = "ssr")]
pub async fn fetch_api<T>(_cx: Scope, path: &str) -> Option<T>
pub async fn fetch_api<T>(path: &str) -> Option<T>
where
T: Serializable,
{

View File

@@ -1,13 +1,13 @@
use leptos::{view, Errors, For, IntoView, RwSignal, Scope, View};
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(cx: Scope, errors: Option<RwSignal<Errors>>) -> View {
pub fn error_template(errors: Option<RwSignal<Errors>>) -> View {
let Some(errors) = errors else {
panic!("No Errors found and we expected errors!");
};
view! {cx,
view! {
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
@@ -15,14 +15,14 @@ pub fn error_template(cx: Scope, errors: Option<RwSignal<Errors>>) -> View {
// a unique key for each item as a reference
key=|(key, _)| key.clone()
// renders each item to a view
view= move |cx, (_, error)| {
view= move | (_, error)| {
let error_string = error.to_string();
view! {
cx,
<p>"Error: " {error_string}</p>
}
}
/>
}
.into_view(cx)
.into_view()
}

View File

@@ -21,7 +21,7 @@ if #[cfg(feature = "ssr")] {
if res.status() == StatusCode::OK {
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(options.to_owned(), |cx| error_template(cx, None));
let handler = leptos_axum::render_app_to_stream(options.to_owned(), || error_template( None));
handler(req).await.into_response()
}
}

View File

@@ -1,5 +1,5 @@
use cfg_if::cfg_if;
use leptos::{component, view, IntoView, Scope};
use leptos::{component, view, IntoView};
use leptos_meta::*;
use leptos_router::*;
mod api;
@@ -10,10 +10,10 @@ mod routes;
use routes::{nav::*, stories::*, story::*, users::*};
#[component]
pub fn App(cx: Scope) -> impl IntoView {
provide_meta_context(cx);
pub fn App() -> impl IntoView {
provide_meta_context();
view! {
cx,
<>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/hackernews_axum.css"/>
@@ -41,8 +41,8 @@ cfg_if! {
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(move |cx| {
view! { cx, <App/> }
leptos::mount_to_body(move || {
view! { <App/> }
});
}
}

View File

@@ -18,14 +18,14 @@ 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(|cx| view! { cx, <App/> }).await;
let routes = generate_route_list(|| view! { <App/> }).await;
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
// build our application with a route
let app = Router::new()
.route("/favicon.ico", get(file_and_error_handler))
.leptos_routes(&leptos_options, routes, |cx| view! { cx, <App/> } )
.leptos_routes(&leptos_options, routes, || view! { <App/> } )
.fallback(file_and_error_handler)
.with_state(leptos_options);
@@ -46,8 +46,8 @@ if #[cfg(feature = "ssr")] {
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! { cx, <App/> }
mount_to_body(|| {
view! { <App/> }
});
}
}

View File

@@ -1,9 +1,9 @@
use leptos::{component, view, IntoView, Scope};
use leptos::{component, view, IntoView};
use leptos_router::*;
#[component]
pub fn Nav(cx: Scope) -> impl IntoView {
view! { cx,
pub fn Nav() -> impl IntoView {
view! {
<header class="header">
<nav class="inner">
<A href="/">

View File

@@ -13,9 +13,9 @@ fn category(from: &str) -> &'static str {
}
#[component]
pub fn Stories(cx: Scope) -> impl IntoView {
let query = use_query_map(cx);
let params = use_params_map(cx);
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()))
@@ -27,28 +27,27 @@ pub fn Stories(cx: Scope) -> impl IntoView {
.unwrap_or_else(|| "top".to_string())
};
let stories = create_resource(
cx,
move || (page(), story_type()),
move |(page, story_type)| async move {
let path = format!("{}?page={}", category(&story_type), page);
api::fetch_api::<Vec<api::Story>>(cx, &api::story(&path)).await
api::fetch_api::<Vec<api::Story>>(&api::story(&path)).await
},
);
let (pending, set_pending) = create_signal(cx, false);
let (pending, set_pending) = create_signal(false);
let hide_more_link = move |cx| {
let hide_more_link = move || {
pending()
|| stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28
|| stories.get().unwrap_or(None).unwrap_or_default().len() < 28
};
view! {
cx,
<div class="news-view">
<div class="news-list-nav">
<span>
{move || if page() > 1 {
view! {
cx,
<a class="page-link"
href=move || format!("/{}?page={}", story_type(), page() - 1)
attr:aria_label="Previous Page"
@@ -58,7 +57,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
}.into_any()
} else {
view! {
cx,
<span class="page-link disabled" aria-hidden="true">
"< prev"
</span>
@@ -67,11 +66,11 @@ pub fn Stories(cx: Scope) -> impl IntoView {
</span>
<span>"page " {page}</span>
<Transition
fallback=move || view! { cx, <p>"Loading..."</p> }
fallback=move || view! { <p>"Loading..."</p> }
>
<span class="page-link"
class:disabled=move || hide_more_link(cx)
aria-hidden=move || hide_more_link(cx)
class:disabled=hide_more_link
aria-hidden=hide_more_link
>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
@@ -84,20 +83,20 @@ pub fn Stories(cx: Scope) -> impl IntoView {
<main class="news-list">
<div>
<Transition
fallback=move || view! { cx, <p>"Loading..."</p> }
fallback=move || view! { <p>"Loading..."</p> }
set_pending=set_pending.into()
>
{move || match stories.read(cx) {
{move || match stories.get() {
None => None,
Some(None) => Some(view! { cx, <p>"Error loading stories."</p> }.into_any()),
Some(None) => Some(view! { <p>"Error loading stories."</p> }.into_any()),
Some(Some(stories)) => {
Some(view! { cx,
Some(view! {
<ul>
<For
each=move || stories.clone()
key=|story| story.id
view=move |cx, story: api::Story| {
view! { cx,
view=move | story: api::Story| {
view! {
<Story story/>
}
}
@@ -114,32 +113,32 @@ pub fn Stories(cx: Scope) -> impl IntoView {
}
#[component]
fn Story(cx: Scope, story: api::Story) -> impl IntoView {
view! { cx,
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! { cx,
view! {
<span>
<a href=story.url target="_blank" rel="noreferrer">
{story.title.clone()}
</a>
<span class="host">"("{story.domain}")"</span>
</span>
}.into_view(cx)
}.into_view()
} else {
let title = story.title.clone();
view! { cx, <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }.into_view(cx)
view! { <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }.into_view()
}}
</span>
<br />
<span class="meta">
{if story.story_type != "job" {
view! { cx,
view! {
<span>
{"by "}
{story.user.map(|user| view ! { cx, <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 {
@@ -149,13 +148,13 @@ fn Story(cx: Scope, story: api::Story) -> impl IntoView {
}}
</A>
</span>
}.into_view(cx)
}.into_view()
} else {
let title = story.title.clone();
view! { cx, <A href=format!("/item/{}", story.id)>{title.clone()}</A> }.into_view(cx)
view! { <A href=format!("/item/{}", story.id)>{title.clone()}</A> }.into_view()
}}
</span>
{(story.story_type != "link").then(|| view! { cx,
{(story.story_type != "link").then(|| view! {
" "
<span class="label">{story.story_type}</span>
})}

View File

@@ -4,37 +4,33 @@ use leptos_meta::*;
use leptos_router::*;
#[component]
pub fn Story(cx: Scope) -> impl IntoView {
let params = use_params_map(cx);
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = create_resource(
cx,
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move {
if id.is_empty() {
None
} else {
api::fetch_api::<api::Story>(
cx,
&api::story(&format!("item/{id}")),
)
.await
api::fetch_api::<api::Story>(&api::story(&format!("item/{id}")))
.await
}
},
);
let meta_description = move || {
story
.read(cx)
.get()
.and_then(|story| story.map(|story| story.title))
.unwrap_or_else(|| "Loading story...".to_string())
};
view! { cx,
view! {
<>
<Meta name="description" content=meta_description/>
<Suspense fallback=|| view! { cx, "Loading..." }>
{move || story.read(cx).map(|story| match story {
None => view! { cx, <div class="item-view">"Error loading this story."</div> },
Some(story) => view! { cx,
<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">
@@ -43,7 +39,7 @@ pub fn Story(cx: Scope) -> impl IntoView {
<span class="host">
"("{story.domain}")"
</span>
{story.user.map(|user| view! { cx, <p class="meta">
{story.user.map(|user| view! { <p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{user}")>{user.clone()}</A>
@@ -62,7 +58,7 @@ pub fn Story(cx: Scope) -> impl IntoView {
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
view=move |cx, comment| view! { cx, <Comment comment /> }
view=move | comment| view! { <Comment comment /> }
/>
</ul>
</div>
@@ -75,10 +71,10 @@ pub fn Story(cx: Scope) -> impl IntoView {
}
#[component]
pub fn Comment(cx: Scope, comment: api::Comment) -> impl IntoView {
let (open, set_open) = create_signal(cx, true);
pub fn Comment(comment: api::Comment) -> impl IntoView {
let (open, set_open) = create_signal(true);
view! { cx,
view! {
<li class="comment">
<div class="by">
<A href=format!("/users/{}", comment.user.clone().unwrap_or_default())>{comment.user.clone()}</A>
@@ -86,7 +82,7 @@ pub fn Comment(cx: Scope, comment: api::Comment) -> impl IntoView {
</div>
<div class="text" inner_html=comment.content></div>
{(!comment.comments.is_empty()).then(|| {
view! { cx,
view! {
<div>
<div class="toggle" class:open=open>
<a on:click=move |_| set_open.update(|n| *n = !*n)>
@@ -102,12 +98,12 @@ pub fn Comment(cx: Scope, comment: api::Comment) -> impl IntoView {
</div>
{move || open().then({
let comments = comment.comments.clone();
move || view! { cx,
move || view! {
<ul class="comment-children">
<For
each=move || comments.clone()
key=|comment| comment.id
view=move |cx, comment: api::Comment| view! { cx, <Comment comment /> }
view=move | comment: api::Comment| view! { <Comment comment /> }
/>
</ul>
}

View File

@@ -3,25 +3,24 @@ use leptos::*;
use leptos_router::*;
#[component]
pub fn User(cx: Scope) -> impl IntoView {
let params = use_params_map(cx);
pub fn User() -> impl IntoView {
let params = use_params_map();
let user = create_resource(
cx,
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move {
if id.is_empty() {
None
} else {
api::fetch_api::<User>(cx, &api::user(&id)).await
api::fetch_api::<User>(&api::user(&id)).await
}
},
);
view! { cx,
view! {
<div class="user-view">
<Suspense fallback=|| view! { cx, "Loading..." }>
{move || user.read(cx).map(|user| match user {
None => view! { cx, <h1>"User not found."</h1> }.into_any(),
Some(user) => view! { cx,
<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">
@@ -31,7 +30,7 @@ pub fn User(cx: Scope) -> impl IntoView {
<li>
<span class="label">"Karma: "</span> {user.karma}
</li>
{user.about.as_ref().map(|about| view! { cx, <li inner_html=about class="about"></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>

View File

@@ -0,0 +1,3 @@
[unstable]
build-std = ["std", "panic_abort", "core", "alloc"]
build-std-features = ["panic_immediate_abort"]

View File

@@ -0,0 +1 @@
target

13
examples/hackernews_islands/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
.direnv
fly.toml

View File

@@ -0,0 +1,112 @@
[package]
name = "hackernews"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
panic = "abort"
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_log = "1"
console_error_panic_hook = "0.1"
cfg-if = "1"
leptos = { git = "https://github.com/leptos-rs/leptos", branch = "islands", features = [
"islands",
"nightly",
] }
leptos_meta = { git = "https://github.com/leptos-rs/leptos", branch = "islands", features = [
"nightly",
] }
leptos_router = { git = "https://github.com/leptos-rs/leptos", branch = "islands", features = [
"nightly",
] }
leptos_actix = { git = "https://github.com/leptos-rs/leptos", branch = "islands", optional = true, features = [
#"nonce",
"islands",
] }
log = "0.4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.11", default-features = false, features = [
"rustls-tls",
"json",
] }
tracing = "0.1"
# openssl = { version = "0.10", features = ["v110"] }
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
wee_alloc = "0.4.5"
[features]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:actix-files",
"dep:actix-web",
"dep:leptos_actix",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
[package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web", "leptos_actix"]
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"
# 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 = "0.0.0.0:8080"
#site-addr = "127.0.0.1:3004"
# The port to use for automatic reload monitoring
reload-port = 3005
# [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
lib-profile-release = "wasm-release"

View File

@@ -0,0 +1,25 @@
FROM rustlang/rust:nightly-bullseye as builder
RUN wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz
#RUN tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz
#RUN cp cargo-binstall /usr/local/cargo/bin
#RUN cargo binstall cargo-leptos -y
#RUN rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu
#RUN rustup target add wasm32-unknown-unknown
RUN mkdir -p /app
WORKDIR /app
COPY . .
RUN cargo build --release --no-default-features --features=ssr
RUN ls -l /app/target
FROM rustlang/rust:nightly-bullseye as runner
COPY --from=builder /app/target/release/hackernews /app/
COPY --from=builder /app/pkg /app
COPY --from=builder /app/Cargo.toml /app/
WORKDIR /app
ENV RUST_LOG="info"
ENV LEPTOS_OUTPUT_NAME="hackernews"
ENV APP_ENVIRONMENT="production"
ENV LEPTOS_SITE_ADDR="0.0.0.0:8080"
ENV LEPTOS_SITE_ROOT="site"
EXPOSE 8080
CMD ["/app/hackernews"]

View File

@@ -0,0 +1,21 @@
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.

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