Compare commits

...

17 Commits

Author SHA1 Message Date
Greg Johnston
7376b76558 what I'm talking about 2023-07-19 21:32:22 -04:00
Greg Johnston
579abc586c wtf? 2023-07-19 21:31:16 -04:00
Ari Seyhun
b2c75d215b chore: remove unnecessary string allocation in TryFrom for Url (#1376) 2023-07-19 07:04:06 -04:00
Andrew Grande
951607de74 docs: typos
* Fixed wording

* Update ARCHITECTURE.md

Fixed superfluous whitespace
2023-07-19 07:03:50 -04:00
Greg Johnston
c1c49ce53b v0.4.5 2023-07-18 14:02:56 -04:00
Greg Johnston
e8aa9b24f1 fix: memory leak in leptos_axum (#1374) 2023-07-17 21:59:20 -04:00
Greg Johnston
3036cd223e v0.4.4 2023-07-17 17:33:44 -04:00
Greg Johnston
1ae5150b08 fix: release lock on stored values during update/set (#1373) 2023-07-17 14:19:03 -04:00
Greg Johnston
47148f2033 perf: exclude hydration code in CSR mode (#1372) 2023-07-17 12:20:33 -04:00
Sebastian Probst Eide
4d4d15436b fix: incorrect tree walker filter in hot reloading (closes #1355) (#1368) 2023-07-17 08:44:32 -04:00
Jack DeVries
7f4741b3a3 doc: add previews to backup CodeSandbox (#1169) 2023-07-17 08:40:30 -04:00
Hans Baker
85644a7c1c Fix broken link to example code in testing book page (#1365) 2023-07-16 20:00:56 -04:00
Greg Johnston
f40ae6af30 fix: correctly show fallback for Transition on first load even if not hydrating (#1362) 2023-07-16 15:19:51 -04:00
Greg Johnston
708e1a5aab docs: wasm-pack instructions missing --debug for Tailwind examples 2023-07-16 12:28:20 -04:00
Joseph Cruz
55613c9a31 ci(check-examples): only run on source change (#1356)
* ci(check-examples): only run on source change

* ci(check-examples):  simulate source change

* ci(check-examples): fix expression

* ci(check-examples): simulate source change

* ci(check-examples): test change against files

* ci(check-examples): adjust expression

* ci(check-examples): remove quotes

* ci(check-examples): use from json

* ci(check-examples): set output value

* ci(check-examples): remove simulated change
2023-07-15 19:09:54 -04:00
Joseph Cruz
e6590c7d31 ci(ci): only run on source change (#1357)
* ci(ci): only run on source change

* ci(ci): simulate source change

* ci(ci): remove simulated source change
2023-07-15 19:09:33 -04:00
Greg Johnston
5af2f4e98d docs/warning: fix <ActionForm/> docs and add runtime warning for incorrect encodings (#1360) 2023-07-15 19:09:03 -04:00
42 changed files with 2638 additions and 456 deletions

View File

@@ -14,6 +14,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
source_changed: ${{ steps.set-source-changed.outputs.source_changed }}
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -34,9 +35,36 @@ jobs:
echo "Example Directories: $examples"
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
- name: Get source files that changed
id: changed-source
uses: tj-actions/changed-files@v36
with:
files: |
integrations
leptos
leptos_config
leptos_dom
leptos_hot_reload
leptos_macro
leptos_reactive
leptos_server
meta
router
server_fn
server_fn_macro
- name: List source files that changed
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
- name: Set source_changed
id: set-source-changed
run: |
echo "source_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
matrix-job:
name: Check
needs: [setup]
if: needs.setup.outputs.source_changed == 'true'
strategy:
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
fail-fast: false

View File

@@ -9,8 +9,45 @@ on:
- main
jobs:
setup:
name: Detect Changes
runs-on: ubuntu-latest
outputs:
source_changed: ${{ steps.set-source-changed.outputs.source_changed }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Get source files that changed
id: changed-source
uses: tj-actions/changed-files@v36
with:
files: |
integrations
leptos
leptos_config
leptos_dom
leptos_hot_reload
leptos_macro
leptos_reactive
leptos_server
meta
router
server_fn
server_fn_macro
- name: List source files that changed
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
- name: Set source_changed
id: set-source-changed
run: |
echo "source_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
matrix-job:
name: CI
needs: [setup]
if: needs.setup.outputs.source_changed == 'true'
strategy:
matrix:
directory:

View File

@@ -220,8 +220,8 @@ for reference: they include large amounts of manual SSR route handling, etc.
## `cargo-leptos` helpers
`leptos_config` and `leptos_hot_reload` exist to support two different features
of `cargo-leptos`, namely its configuration and its view-patching/hot-
reloading features.
of `cargo-leptos`, namely its configuration and its view-patching/hot-reloading
features.
Its important to say that the main feature `cargo-leptos` remains its ability
to conveniently tie together different build tooling, compiling your app to

View File

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

View File

@@ -183,3 +183,222 @@ data flow and of fine-grained reactive updates.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs)
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
// So far, we've only been working with local state in components
// We've only seen how to communicate between parent and child components
// But there are also more general ways to manage global state
//
// The three best approaches to global state are
// 1. Using the router to drive global state via the URL
// 2. Passing signals through context
// 3. Creating a global state struct and creating lenses into it with `create_slice`
//
// Option #1: URL as Global State
// The next few sections of the tutorial will be about the router.
// So for now, we'll just look at options #2 and #3.
// Option #2: Pass Signals through Context
//
// In virtual DOM libraries like React, using the Context API to manage global
// state is a bad idea: because the entire app exists in a tree, changing
// some value provided high up in the tree can cause the whole app to render.
//
// In fine-grained reactive libraries like Leptos, this is simply not the case.
// You can create a signal in the root of your app and pass it down to other
// 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 {
// here we create a signal in the root that can be consumed
// anywhere in the app.
let (count, set_count) = create_signal(cx, 0);
// we'll pass the setter to specific components,
// but provide the count itself to the whole app via context
provide_context(cx, count);
view! { cx,
<h1>"Option 2: Passing Signals"</h1>
// SetterButton is allowed to modify the count
<SetterButton set_count/>
// These consumers can only read from it
// But we could give them write access by passing `set_count` if we wanted
<div style="display: flex">
<FancyMath/>
<ListItems/>
</div>
}
}
/// A button that increments our global counter.
#[component]
fn SetterButton(cx: Scope, set_count: WriteSignal<u32>) -> impl IntoView {
view! { cx,
<div class="provider red">
<button on:click=move |_| set_count.update(|count| *count += 1)>
"Increment Global Count"
</button>
</div>
}
}
/// A component that does some "fancy" math with the global count
#[component]
fn FancyMath(cx: Scope) -> impl IntoView {
// here we consume the global count signal with `use_context`
let count = use_context::<ReadSignal<u32>>(cx)
// 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,
<div class="consumer blue">
"The number "
<strong>{count}</strong>
{move || if is_even() {
" is"
} else {
" is not"
}}
" even."
</div>
}
}
/// A component that shows a list of items generated from the global count.
#[component]
fn ListItems(cx: Scope) -> 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 squares = move || {
(0..count())
.map(|n| view! { cx, <li>{n}<sup>"2"</sup> " is " {n * n}</li> })
.collect::<Vec<_>>()
};
view! { cx,
<div class="consumer green">
<ul>{squares}</ul>
</div>
}
}
// Option #3: Create a Global State Struct
//
// You can use this approach to build a single global data structure
// that holds the state for your whole app, and then access it by
// taking fine-grained slices using `create_slice` or `create_memo`,
// so that changing one part of the state doesn't cause parts of your
// app that depend on other parts of the state to change.
#[derive(Default, Clone, Debug)]
struct GlobalState {
count: u32,
name: String,
}
#[component]
fn Option3(cx: Scope) -> 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);
view! { cx,
<h1>"Option 3: Passing Signals"</h1>
<div class="red consumer" style="width: 100%">
<h2>"Current Global State"</h2>
<pre>
{move || {
format!("{:#?}", state.get())
}}
</pre>
</div>
<div style="display: flex">
<GlobalStateCounter/>
<GlobalStateInput/>
</div>
}
}
/// 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");
// `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
|state| state.count,
// our setter describes how to mutate that slice, given a new value
|state, n| state.count = n,
);
view! { cx,
<div class="consumer blue">
<button
on:click=move |_| {
set_count(count() + 1);
}
>
"Increment Global Count"
</button>
<br/>
<span>"Count is: " {count}</span>
</div>
}
}
/// 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");
// 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
|state| state.name.clone(),
// our setter describes how to mutate that slice, given a new value
|state, n| state.name = n,
);
view! { cx,
<div class="consumer green">
<input
type="text"
prop:value=name
on:input=move |ev| {
set_name(event_target_value(&ev));
}
/>
<br/>
<span>"Name is: " {name}</span>
</div>
}
}
// This `main` function is the entry point into the app
// It just mounts our component to the <body>
// 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/> })
}
```
</details>
</preview>

View File

@@ -53,3 +53,89 @@ Resources also provide a `refetch()` method that allows you to manually reload t
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
```rust
use gloo_timers::future::TimeoutFuture;
use leptos::*;
// Here we define an async function
// This could be anything: a network request, database read, etc.
// Here, we just multiply a number by 10
async fn load_data(value: i32) -> i32 {
// fake a one-second delay
TimeoutFuture::new(1_000).await;
value * 10
}
#[component]
fn App(cx: Scope) -> impl IntoView {
// this count is our synchronous, local state
let (count, set_count) = create_signal(cx, 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
// it takes the source signal's value as its argument
// and does some async work
|value| async move { load_data(value).await },
);
// whenever the source signal changes, the loader reloads
// 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 });
// 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)
.map(|value| format!("Server returned {value:?}"))
// This loading state will only show before the first load
.unwrap_or_else(|| "Loading...".into())
};
// the resource's loading() method gives us a
// signal to indicate whether it's currently loading
let loading = async_data.loading();
let is_loading = move || if loading() { "Loading..." } else { "Idle." };
view! { cx,
<button
on:click=move |_| {
set_count.update(|n| *n += 1);
}
>
"Click me"
</button>
<p>
<code>"stable"</code>": " {move || stable.read(cx)}
</p>
<p>
<code>"count"</code>": " {count}
</p>
<p>
<code>"async_value"</code>": "
{async_result}
<br/>
{is_loading}
</p>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -72,3 +72,58 @@ This inversion of the flow of control makes it easier to add or remove individua
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs)
<iframe src="https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
```rust
use gloo_timers::future::TimeoutFuture;
use leptos::*;
async fn important_api_call(name: String) -> String {
TimeoutFuture::new(1_000).await;
name.to_ascii_uppercase()
}
#[component]
fn App(cx: Scope) -> impl IntoView {
let (name, set_name) = create_signal(cx, "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,
<input
on:input=move |ev| {
set_name(event_target_value(&ev));
}
prop:value=name
/>
<p><code>"name:"</code> {name}</p>
<Suspense
// the fallback will show whenever a resource
// read "under" the suspense is loading
fallback=move || view! { cx, <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)}
</p>
</Suspense>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -9,3 +9,76 @@ This example shows how you can create a simple tabbed contact list with `<Transi
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs)
<iframe src="https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
```rust
use gloo_timers::future::TimeoutFuture;
use leptos::*;
async fn important_api_call(id: usize) -> String {
TimeoutFuture::new(1_000).await;
match id {
0 => "Alice",
1 => "Bob",
2 => "Carol",
_ => "User not found",
}
.to_string()
}
#[component]
fn App(cx: Scope) -> impl IntoView {
let (tab, set_tab) = create_signal(cx, 0);
// this will reload every time `tab` changes
let user_data = create_resource(cx, tab, |tab| async move { important_api_call(tab).await });
view! { cx,
<div class="buttons">
<button
on:click=move |_| set_tab(0)
class:selected=move || tab() == 0
>
"Tab A"
</button>
<button
on:click=move |_| set_tab(1)
class:selected=move || tab() == 1
>
"Tab B"
</button>
<button
on:click=move |_| set_tab(2)
class:selected=move || tab() == 2
>
"Tab C"
</button>
{move || if user_data.loading().get() {
"Loading..."
} else {
""
}}
</div>
<Transition
// the fallback will show initially
// on subsequent reloads, the current child will
// continue showing
fallback=move || view! { cx, <p>"Loading..."</p> }
>
<p>
{move || user_data.read(cx)}
</p>
</Transition>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -94,3 +94,83 @@ Now, theres a chance this all seems a little over-complicated, or maybe too r
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/10-async-resources-forked-hgpfp0?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A4%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A4%7D%5D&file=%2Fsrc%2Fmain.rs)
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-forked-hgpfp0?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A4%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A4%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
```rust
use gloo_timers::future::TimeoutFuture;
use leptos::{html::Input, *};
use uuid::Uuid;
// Here we define an async function
// This could be anything: a network request, database read, etc.
// Think of it as a mutation: some imperative async action you run,
// whereas a resource would be some async data you load
async fn add_todo(text: &str) -> Uuid {
_ = text;
// fake a one-second delay
TimeoutFuture::new(1_000).await;
// pretend this is a post ID or something
Uuid::new_v4()
}
#[component]
fn App(cx: Scope) -> 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| {
// 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
let input = input.to_owned();
async move { add_todo(&input).await }
});
// actions provide a bunch of synchronous, reactive variables
// that tell us different things about the state of the action
let submitted = add_todo.input();
let pending = add_todo.pending();
let todo_id = add_todo.value();
let input_ref = create_node_ref::<Input>(cx);
view! { cx,
<form
on:submit=move |ev| {
ev.prevent_default(); // don't reload the page...
let input = input_ref.get().expect("input to exist");
add_todo.dispatch(input.value());
}
>
<label>
"What do you need to do?"
<input type="text"
node_ref=input_ref
/>
</label>
<button type="submit">"Add Todo"</button>
</form>
<p>{move || pending().then(|| "Loading...")}</p>
<p>
"Submitted: "
<code>{move || format!("{:#?}", submitted())}</code>
</p>
<p>
"Pending: "
<code>{move || format!("{:#?}", pending())}</code>
</p>
<p>
"Todo ID: "
<code>{move || format!("{:#?}", todo_id())}</code>
</p>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -112,3 +112,195 @@ Every time `count` is updated, this effect wil rerun. This is what allows reacti
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/serene-thompson-40974n?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/serene-thompson-40974n?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::html::Input;
use leptos::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
// Just making a visible log here
// You can ignore this...
let log = create_rw_signal::<Vec<String>>(cx, vec![]);
let logged = move || log().join("\n");
provide_context(cx, log);
view! { cx,
<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);
// this will add the name to the log
// any time one of the source signals changes
create_effect(cx, move |_| {
log(
cx,
if use_last() {
format!("{} {}", first(), last())
} else {
first()
},
)
});
view! { cx,
<h1><code>"create_effect"</code> " Version"</h1>
<form>
<label>
"First Name"
<input type="text" name="first" prop:value=first
on:change=move |ev| set_first(event_target_value(&ev))
/>
</label>
<label>
"Last Name"
<input type="text" name="last" prop:value=last
on:change=move |ev| set_last(event_target_value(&ev))
/>
</label>
<label>
"Show Last Name"
<input type="checkbox" name="use_last" prop:checked=use_last
on:change=move |ev| set_use_last(event_target_checked(&ev))
/>
</label>
</form>
}
}
#[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);
let mut prev_name = String::new();
let on_change = move |_| {
log(cx, " listener");
let first = first.get().unwrap();
let last = last.get().unwrap();
let use_last = use_last.get().unwrap();
let this_one = if use_last.checked() {
format!("{} {}", first.value(), last.value())
} else {
first.value()
};
if this_one != prev_name {
log(cx, &this_one);
prev_name = this_one;
}
};
view! { cx,
<h1>"Manual Version"</h1>
<form on:change=on_change>
<label>
"First Name"
<input type="text" name="first"
node_ref=first
/>
</label>
<label>
"Last Name"
<input type="text" name="last"
node_ref=last
/>
</label>
<label>
"Show Last Name"
<input type="checkbox" name="use_last"
checked
node_ref=use_last
/>
</label>
</form>
}
}
#[component]
fn EffectVsDerivedSignal(cx: Scope) -> impl IntoView {
let (my_value, set_my_value) = create_signal(cx, String::new());
// Don't do this.
/*let (my_optional_value, set_optional_my_value) = create_signal(cx, Option::<String>::None);
create_effect(cx, move |_| {
if !my_value.get().is_empty() {
set_optional_my_value(Some(my_value.get()));
} else {
set_optional_my_value(None);
}
});*/
// Do this
let my_optional_value =
move || (!my_value.with(String::is_empty)).then(|| Some(my_value.get()));
view! { cx,
<input
prop:value=my_value
on:input= move |ev| set_my_value(event_target_value(&ev))
/>
<p>
<code>"my_optional_value"</code>
" is "
<code>
<Show
when=move || my_optional_value().is_some()
fallback=|cx| view! { cx, "None" }
>
"Some(\"" {my_optional_value().unwrap()} "\")"
</Show>
</code>
</p>
}
}
/*#[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>,
/// 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
fallback: F,
) -> impl IntoView
where
W: Fn() -> bool + 'static,
F: Fn(Scope) -> IV + 'static,
IV: IntoView,
{
let memoized_when = create_memo(cx, move |_| when());
move || match memoized_when.get() {
true => children(cx).into_view(cx),
false => fallback(cx).into_view(cx),
}
}*/
fn log(cx: Scope, msg: impl std::fmt::Display) {
let log = use_context::<RwSignal<Vec<String>>>(cx).unwrap();
log.update(|log| log.push(msg.to_string()));
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -68,7 +68,7 @@ pub fn SimpleCounter(cx: Scope) -> impl IntoView {
The `SimpleCounter` function itself runs once. The `value` signal is created once. The framework hands off the `increment` function to the browser as an event listener. When you click the button, the browser calls `increment`, which updates `value` via `set_value`. And that updates the single text node represented in our view by `{value}`.
Closures are key to reactivity. They provide the framework with the ability to rerun the smallest possible unit of your application in responsive to a change.
Closures are key to reactivity. They provide the framework with the ability to rerun the smallest possible unit of your application in response to a change.
So remember two things:

View File

@@ -170,3 +170,121 @@ In fact, in this case, we dont even need to rerender the `<Contact/>` compone
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
use leptos_router::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
<Router>
<h1>"Contact App"</h1>
// this <nav> will show on every routes,
// because it's outside the <Routes/>
// note: we can just use normal <a> tags
// and the router will use client-side navigation
<nav>
<h2>"Navigation"</h2>
<a href="/">"Home"</a>
<a href="/contacts">"Contacts"</a>
</nav>
<main>
<Routes>
// / just has an un-nested "Home"
<Route path="/" view=|cx| view! { cx,
<h3>"Home"</h3>
}/>
// /contacts has nested routes
<Route
path="/contacts"
view=|cx| view! { cx, <ContactList/> }
>
// if no id specified, fall back
<Route path=":id" view=|cx| view! { cx,
<ContactInfo/>
}>
<Route path="" view=|cx| view! { cx,
<div class="tab">
"(Contact Info)"
</div>
}/>
<Route path="conversations" view=|cx| view! { cx,
<div class="tab">
"(Conversations)"
</div>
}/>
</Route>
// if no id specified, fall back
<Route path="" view=|cx| view! { cx,
<div class="select-user">
"Select a user to view contact info."
</div>
}/>
</Route>
</Routes>
</main>
</Router>
}
}
#[component]
fn ContactList(cx: Scope) -> impl IntoView {
view! { cx,
<div class="contact-list">
// here's our contact list component itself
<div class="contact-list-contacts">
<h3>"Contacts"</h3>
<A href="alice">"Alice"</A>
<A href="bob">"Bob"</A>
<A href="steve">"Steve"</A>
</div>
// <Outlet/> will show the nested child route
// we can position this outlet wherever we want
// within the layout
<Outlet/>
</div>
}
}
#[component]
fn ContactInfo(cx: Scope) -> impl IntoView {
// we can access the :id param reactively with `use_params_map`
let params = use_params_map(cx);
let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default());
// imagine we're loading data from an API here
let name = move || match id().as_str() {
"alice" => "Alice",
"bob" => "Bob",
"steve" => "Steve",
_ => "User not found.",
};
view! { cx,
<div class="contact-info">
<h4>{name}</h4>
<div class="tabs">
<A href="" exact=true>"Contact Info"</A>
<A href="conversations">"Conversations"</A>
</div>
// <Outlet/> here is the tabs that are nested
// underneath the /contacts/:id route
<Outlet/>
</div>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -77,3 +77,121 @@ This can get a little messy: deriving a signal that wraps an `Option<_>` or `Res
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
use leptos_router::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
<Router>
<h1>"Contact App"</h1>
// this <nav> will show on every routes,
// because it's outside the <Routes/>
// note: we can just use normal <a> tags
// and the router will use client-side navigation
<nav>
<h2>"Navigation"</h2>
<a href="/">"Home"</a>
<a href="/contacts">"Contacts"</a>
</nav>
<main>
<Routes>
// / just has an un-nested "Home"
<Route path="/" view=|cx| view! { cx,
<h3>"Home"</h3>
}/>
// /contacts has nested routes
<Route
path="/contacts"
view=|cx| view! { cx, <ContactList/> }
>
// if no id specified, fall back
<Route path=":id" view=|cx| view! { cx,
<ContactInfo/>
}>
<Route path="" view=|cx| view! { cx,
<div class="tab">
"(Contact Info)"
</div>
}/>
<Route path="conversations" view=|cx| view! { cx,
<div class="tab">
"(Conversations)"
</div>
}/>
</Route>
// if no id specified, fall back
<Route path="" view=|cx| view! { cx,
<div class="select-user">
"Select a user to view contact info."
</div>
}/>
</Route>
</Routes>
</main>
</Router>
}
}
#[component]
fn ContactList(cx: Scope) -> impl IntoView {
view! { cx,
<div class="contact-list">
// here's our contact list component itself
<div class="contact-list-contacts">
<h3>"Contacts"</h3>
<A href="alice">"Alice"</A>
<A href="bob">"Bob"</A>
<A href="steve">"Steve"</A>
</div>
// <Outlet/> will show the nested child route
// we can position this outlet wherever we want
// within the layout
<Outlet/>
</div>
}
}
#[component]
fn ContactInfo(cx: Scope) -> impl IntoView {
// we can access the :id param reactively with `use_params_map`
let params = use_params_map(cx);
let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default());
// imagine we're loading data from an API here
let name = move || match id().as_str() {
"alice" => "Alice",
"bob" => "Bob",
"steve" => "Steve",
_ => "User not found.",
};
view! { cx,
<div class="contact-info">
<h4>{name}</h4>
<div class="tabs">
<A href="" exact=true>"Contact Info"</A>
<A href="conversations">"Conversations"</A>
</div>
// <Outlet/> here is the tabs that are nested
// underneath the /contacts/:id route
<Outlet/>
</div>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -21,3 +21,121 @@ The router also provides an [`<A>`](https://docs.rs/leptos_router/latest/leptos_
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
use leptos_router::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
<Router>
<h1>"Contact App"</h1>
// this <nav> will show on every routes,
// because it's outside the <Routes/>
// note: we can just use normal <a> tags
// and the router will use client-side navigation
<nav>
<h2>"Navigation"</h2>
<a href="/">"Home"</a>
<a href="/contacts">"Contacts"</a>
</nav>
<main>
<Routes>
// / just has an un-nested "Home"
<Route path="/" view=|cx| view! { cx,
<h3>"Home"</h3>
}/>
// /contacts has nested routes
<Route
path="/contacts"
view=|cx| view! { cx, <ContactList/> }
>
// if no id specified, fall back
<Route path=":id" view=|cx| view! { cx,
<ContactInfo/>
}>
<Route path="" view=|cx| view! { cx,
<div class="tab">
"(Contact Info)"
</div>
}/>
<Route path="conversations" view=|cx| view! { cx,
<div class="tab">
"(Conversations)"
</div>
}/>
</Route>
// if no id specified, fall back
<Route path="" view=|cx| view! { cx,
<div class="select-user">
"Select a user to view contact info."
</div>
}/>
</Route>
</Routes>
</main>
</Router>
}
}
#[component]
fn ContactList(cx: Scope) -> impl IntoView {
view! { cx,
<div class="contact-list">
// here's our contact list component itself
<div class="contact-list-contacts">
<h3>"Contacts"</h3>
<A href="alice">"Alice"</A>
<A href="bob">"Bob"</A>
<A href="steve">"Steve"</A>
</div>
// <Outlet/> will show the nested child route
// we can position this outlet wherever we want
// within the layout
<Outlet/>
</div>
}
}
#[component]
fn ContactInfo(cx: Scope) -> impl IntoView {
// we can access the :id param reactively with `use_params_map`
let params = use_params_map(cx);
let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default());
// imagine we're loading data from an API here
let name = move || match id().as_str() {
"alice" => "Alice",
"bob" => "Bob",
"steve" => "Steve",
_ => "User not found.",
};
view! { cx,
<div class="contact-info">
<h4>{name}</h4>
<div class="tabs">
<A href="" exact=true>"Contact Info"</A>
<A href="conversations">"Conversations"</A>
</div>
// <Outlet/> here is the tabs that are nested
// underneath the /contacts/:id route
<Outlet/>
</div>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -65,3 +65,117 @@ Youll notice that this version drops the `Submit` button. Instead, we add an
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-forked-hrrt3h?file=%2Fsrc%2Fmain.rs)
<iframe src="https://codesandbox.io/p/sandbox/16-router-forked-hrrt3h?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
use leptos_router::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
<Router>
<h1><code>"<Form/>"</code></h1>
<main>
<Routes>
<Route path="" view=|cx| view! { cx, <FormExample/> }/>
</Routes>
</main>
</Router>
}
}
#[component]
pub fn FormExample(cx: Scope) -> impl IntoView {
// reactive access to URL query
let query = use_query_map(cx);
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,
// read out the URL query strings
<table>
<tr>
<td><code>"name"</code></td>
<td>{name}</td>
</tr>
<tr>
<td><code>"number"</code></td>
<td>{number}</td>
</tr>
<tr>
<td><code>"select"</code></td>
<td>{select}</td>
</tr>
</table>
// <Form/> will navigate whenever submitted
<h2>"Manual Submission"</h2>
<Form method="GET" action="">
// input names determine query string key
<input type="text" name="name" value=name/>
<input type="number" name="number" value=number/>
<select name="select">
// `selected` will set which starts as selected
<option selected=move || select() == "A">
"A"
</option>
<option selected=move || select() == "B">
"B"
</option>
<option selected=move || select() == "C">
"C"
</option>
</select>
// submitting should cause a client-side
// navigation, not a full reload
<input type="submit"/>
</Form>
// This <Form/> uses some JavaScript to submit
// on every input
<h2>"Automatic Submission"</h2>
<Form method="GET" action="">
<input
type="text"
name="name"
value=name
// this oninput attribute will cause the
// form to submit on every input to the field
oninput="this.form.requestSubmit()"
/>
<input
type="number"
name="number"
value=number
oninput="this.form.requestSubmit()"
/>
<select name="select"
onchange="this.form.requestSubmit()"
>
<option selected=move || select() == "A">
"A"
</option>
<option selected=move || select() == "B">
"B"
</option>
<option selected=move || select() == "C">
"C"
</option>
</select>
// submitting should cause a client-side
// navigation, not a full reload
<input type="submit"/>
</Form>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -76,7 +76,7 @@ wasm-pack test --firefox
### Writing Your Tests
Most tests will involve some combination of vanilla DOM manipulation and comparison to a `view`. For example, heres a test [for the
`counter` example](https://github.com/leptos-rs/leptos/blob/main/examples/counter/tests/mod.rs).
`counter` example](https://github.com/leptos-rs/leptos/blob/main/examples/counter/tests/web.rs).
First, we set up the testing environment.

View File

@@ -160,3 +160,67 @@ Other Previews > 8080.` Hover over any of the variables to show Rust-Analyzer de
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A31%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A31%2C%22startLineNumber%22%3A19%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A31%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A31%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
// The #[component] macro marks a function as a reusable component
// Components are the building blocks of your user interface
// They define a reusable unit of behavior
#[component]
fn App(cx: Scope) -> 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);
// the `view` macro is how we define the user interface
// it uses an HTML-like format that can accept certain Rust values
view! { cx,
<button
// on:click will run whenever the `click` event fires
// every event handler is defined as `on:{eventname}`
// we're able to move `set_count` into the closure
// because signals are Copy and 'static
on:click=move |_| {
set_count.update(|n| *n += 1);
}
>
// text nodes in RSX should be wrapped in quotes,
// like a normal Rust string
"Click me"
</button>
<p>
<strong>"Reactive: "</strong>
// you can insert Rust expressions as values in the DOM
// by wrapping them in curly braces
// if you pass in a function, it will reactively update
{move || count.get()}
</p>
<p>
<strong>"Reactive shorthand: "</strong>
// signals are functions, so we can remove the wrapping closure
{count}
</p>
<p>
<strong>"Not reactive: "</strong>
// NOTE: if you write {count()}, this will *not* be reactive
// it simply gets the value of count once
{count()}
</p>
}
}
// This `main` function is the entry point into the app
// It just mounts our component to the <body>
// 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/> })
}
```

View File

@@ -152,3 +152,67 @@ places in your application with minimal overhead.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>Code Sandbox Source</summary>
```rust
use leptos::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 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,
<button
on:click=move |_| {
set_count.update(|n| *n += 1);
}
// the class: syntax reactively updates a single class
// here, we'll set the `red` class when `count` is odd
class:red=move || count() % 2 == 1
>
"Click me"
</button>
// NOTE: self-closing tags like <br> need an explicit /
<br/>
// We'll update this progress bar every time `count` changes
<progress
// static attributes work as in HTML
max="50"
// passing a function to an attribute
// reactively sets that attribute
// signals are functions, so this <=> `move || count.get()`
value=count
>
</progress>
<br/>
// This progress bar will use `double_count`
// so it should move twice as fast!
<progress
max="50"
// derived signals are functions, so they can also
// reactive update the DOM
value=double_count
>
</progress>
<p>"Count: " {count}</p>
<p>"Double Count: " {double_count}</p>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -309,3 +309,77 @@ and see the power of the `#[component]` macro combined with rust-analyzer here.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
// Composing different components together is how we build
// user interfaces. Here, we'll define a resuable <ProgressBar/>.
// You'll see how doc comments can be used to document components
// and their properties.
/// 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)]
/// The maximum value of the progress bar.
max: u16,
// Will run `.into()` on the value passed into the prop.
#[prop(into)]
// `Signal<T>` is a wrapper for several reactive types.
// It can be helpful in component APIs like this, where we
// might want to take any kind of reactive value
/// How much progress should be displayed.
progress: Signal<i32>,
) -> impl IntoView {
view! { cx,
<progress
max={max}
value=progress
/>
<br/>
}
}
#[component]
fn App(cx: Scope) -> impl IntoView {
let (count, set_count) = create_signal(cx, 0);
let double_count = move || count() * 2;
view! { cx,
<button
on:click=move |_| {
set_count.update(|n| *n += 1);
}
>
"Click me"
</button>
<br/>
// If you have this open in CodeSandbox or an editor with
// rust-analyzer support, try hovering over `ProgressBar`,
// `max`, or `progress` to see the docs we defined above
<ProgressBar max=50 progress=count/>
// Let's use the default max value on this one
// the default is 100, so it should move half as fast
<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)/>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -106,3 +106,162 @@ Check out the `<DynamicList/>` component below for an example.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
// Iteration is a very common task in most applications.
// So how do you take a list of data and render it in the DOM?
// This example will show you the two ways:
// 1) for mostly-static lists, using Rust iterators
// 2) for lists that grow, shrink, or move items, using <For/>
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
<h1>"Iteration"</h1>
<h2>"Static List"</h2>
<p>"Use this pattern if the list itself is static."</p>
<StaticList length=5/>
<h2>"Dynamic List"</h2>
<p>"Use this pattern if the rows in your list will change."</p>
<DynamicList initial_length=5/>
}
}
/// A list of counters, without the ability
/// 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));
// 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,
<li>
<button
on:click=move |_| set_count.update(|n| *n += 1)
>
{count}
</button>
</li>
}
})
.collect::<Vec<_>>();
// 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,
<ul>{counter_buttons}</ul>
}
}
/// A list of counters that allows you to add or
/// remove counters.
#[component]
fn DynamicList(
cx: Scope,
/// The number of counters to begin with.
initial_length: usize,
) -> impl IntoView {
// This dynamic list will use the <For/> component.
// <For/> is a keyed list. This means that each row
// has a defined key. If the key does not change, the row
// will not be re-rendered. When the list changes, only
// the minimum number of changes will be made to the DOM.
// `next_counter_id` will let us generate unique IDs
// we do this by simply incrementing the ID by one
// each time we create a counter
let mut next_counter_id = initial_length;
// 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)))
.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 add_counter = move |_| {
// create a signal for the new counter
let sig = create_signal(cx, next_counter_id + 1);
// add this counter to the list of counters
set_counters.update(move |counters| {
// since `.update()` gives us `&mut T`
// we can just use normal Vec methods like `push`
counters.push((next_counter_id, sig))
});
// increment the ID so it's always unique
next_counter_id += 1;
};
view! { cx,
<div>
<button on:click=add_counter>
"Add Counter"
</button>
<ul>
// The <For/> component is central here
// This allows for efficient, key list rendering
<For
// `each` takes any function that returns an iterator
// this should usually be a signal or derived signal
// if it's not reactive, just render a Vec<_> instead of <For/>
each=counters
// the key should be unique and stable for each row
// using an index is usually a bad idea, unless your list
// can only grow, because moving items around inside the list
// means their indices will change and they will all rerender
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,
<li>
<button
on:click=move |_| set_count.update(|n| *n += 1)
>
{count}
</button>
<button
on:click=move |_| {
set_counters.update(|counters| {
counters.retain(|(counter_id, _)| counter_id != &id)
});
}
>
"Remove"
</button>
</li>
}
}
/>
</ul>
</div>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -112,3 +112,112 @@ The view should be pretty self-explanatory by now. Note two things:
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::{ev::SubmitEvent, *};
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
<h2>"Controlled Component"</h2>
<ControlledComponent/>
<h2>"Uncontrolled Component"</h2>
<UncontrolledComponent/>
}
}
#[component]
fn ControlledComponent(cx: Scope) -> impl IntoView {
// create a signal to hold the value
let (name, set_name) = create_signal(cx, "Controlled".to_string());
view! { cx,
<input type="text"
// fire an event whenever the input changes
on:input=move |ev| {
// event_target_value is a Leptos helper function
// it functions the same way as event.target.value
// in JavaScript, but smooths out some of the typecasting
// necessary to make this work in Rust
set_name(event_target_value(&ev));
}
// the `prop:` syntax lets you update a DOM property,
// rather than an attribute.
//
// IMPORTANT: the `value` *attribute* only sets the
// initial value, until you have made a change.
// The `value` *property* sets the current value.
// This is a quirk of the DOM; I didn't invent it.
// Other frameworks gloss this over; I think it's
// more important to give you access to the browser
// as it really works.
//
// tl;dr: use prop:value for form inputs
prop:value=name
/>
<p>"Name is: " {name}</p>
}
}
#[component]
fn UncontrolledComponent(cx: Scope) -> impl IntoView {
// import the type for <input>
use leptos::html::Input;
let (name, set_name) = create_signal(cx, "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);
// fires when the form `submit` event happens
// this will store the value of the <input> in our signal
let on_submit = move |ev: SubmitEvent| {
// stop the page from reloading!
ev.prevent_default();
// here, we'll extract the value from the input
let value = input_element()
// event handlers can only fire after the view
// is mounted to the DOM, so the `NodeRef` will be `Some`
.expect("<input> to exist")
// `NodeRef` implements `Deref` for the DOM element type
// this means we can call`HtmlInputElement::value()`
// to get the current value of the input
.value();
set_name(value);
};
view! { cx,
<form on:submit=on_submit>
<input type="text"
// here, we use the `value` *attribute* to set only
// the initial value, letting the browser maintain
// the state after that
value=name
// store a reference to this input in `input_element`
node_ref=input_element
/>
<input type="submit" value="Submit"/>
</form>
<p>"Name is: " {name}</p>
}
}
// This `main` function is the entry point into the app
// It just mounts our component to the <body>
// 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/> })
}
```
</details>
</preview>

View File

@@ -285,3 +285,100 @@ view! { cx,
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, 0);
let is_odd = move || value() & 1 == 1;
let odd_text = move || if is_odd() { Some("How odd!") } else { None };
view! { cx,
<h1>"Control Flow"</h1>
// Simple UI to update and show a value
<button on:click=move |_| set_value.update(|n| *n += 1)>
"+1"
</button>
<p>"Value is: " {value}</p>
<hr/>
<h2><code>"Option<T>"</code></h2>
// For any `T` that implements `IntoView`,
// so does `Option<T>`
<p>{odd_text}</p>
// This means you can use `Option` methods on it
<p>{move || odd_text().map(|text| text.len())}</p>
<h2>"Conditional Logic"</h2>
// You can do dynamic conditional if-then-else
// logic in several ways
//
// a. An "if" expression in a function
// This will simply re-render every time the value
// changes, which makes it good for lightweight UI
<p>
{move || if is_odd() {
"Odd"
} else {
"Even"
}}
</p>
// b. Toggling some kind of class
// This is smart for an element that's going to
// toggled often, because it doesn't destroy
// it in between states
// (you can find the `hidden` class in `index.html`)
<p class:hidden=is_odd>"Appears if even."</p>
// c. The <Show/> component
// This only renders the fallback and the child
// once, lazily, and toggles between them when
// 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> }
>
<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> })}
<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)
{move || match is_odd() {
true if value() == 1 => {
// <pre> returns HtmlElement<Pre>
view! { cx, <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! { cx, <textarea>{value()}</textarea> }.into_any()
}}
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -113,3 +113,64 @@ an `<ErrorBoundary/>` will appear again.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, 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,
<h1>"Error Handling"</h1>
<label>
"Type a number (or something that's not a number!)"
<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,
<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::<Vec<_>>()
}
</ul>
</div>
}
>
<p>
"You entered "
// because `value` is `Result<i32, _>`,
// it will render the `i32` if it is `Ok`,
// and render nothing and trigger the error boundary
// if it is `Err`. It's a signal, so this will dynamically
// update when `value` changes
<strong>{value}</strong>
</p>
</ErrorBoundary>
</label>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -288,3 +288,150 @@ signals and effects, all the way down.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::{ev::MouseEvent, *};
// This highlights four different ways that child components can communicate
// with their parent:
// 1) <ButtonA/>: passing a WriteSignal as one of the child component props,
// for the child component to write into and the parent to read
// 2) <ButtonB/>: passing a closure as one of the child component props, for
// the child component to call
// 3) <ButtonC/>: adding an `on:` event listener to a component
// 4) <ButtonD/>: providing a context that is used in the component (rather than prop drilling)
#[derive(Copy, Clone)]
struct SmallcapsContext(WriteSignal<bool>);
#[component]
pub fn App(cx: Scope) -> 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);
// 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));
view! {
cx,
<main>
<p
// class: attributes take F: Fn() => bool, and these signals all implement Fn()
class:red=red
class:right=right
class:italics=italics
class:smallcaps=smallcaps
>
"Lorem ipsum sit dolor amet."
</p>
// Button A: pass the signal setter
<ButtonA setter=set_red/>
// Button B: pass a closure
<ButtonB on_click=move |_| set_right.update(|value| *value = !*value)/>
// Button B: use a regular event listener
// setting an event listener on a component like this applies it
// to each of the top-level elements the component returns
<ButtonC on:click=move |_| set_italics.update(|value| *value = !*value)/>
// Button D gets its setter from context rather than props
<ButtonD/>
</main>
}
}
/// 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)
>
"Toggle Red"
</button>
}
}
/// 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
where
F: Fn(MouseEvent) + 'static,
{
view! {
cx,
<button
on:click=on_click
>
"Toggle Right"
</button>
}
// just a note: in an ordinary function ButtonB could take on_click: impl Fn(MouseEvent) + 'static
// and save you from typing out the generic
// the component macro actually expands to define a
//
// struct ButtonBProps<F> where F: Fn(MouseEvent) + 'static {
// on_click: F
// }
//
// this is what allows us to have named props in our component invocation,
// instead of an ordered list of function arguments
// if Rust ever had named function arguments we could drop this requirement
}
/// 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 {
view! {
cx,
<button>
"Toggle Italics"
</button>
}
}
/// 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;
view! {
cx,
<button
on:click=move |_| setter.update(|value| *value = !*value)
>
"Toggle Small Caps"
</button>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -126,3 +126,107 @@ view! { cx,
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/9-component-children-2wrdfd?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A12%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A12%2C%22startLineNumber%22%3A19%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/9-component-children-2wrdfd?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A12%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A12%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
// Often, you want to pass some kind of child view to another
// component. There are two basic patterns for doing this:
// - "render props": creating a component prop that takes a function
// that creates a view
// - the `children` prop: a special property that contains content
// passed as the children of a component in your view, not as a
// property
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (items, set_items) = create_signal(cx, 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,
<p>"Length: " {len}</p>
}
};
view! { cx,
// This component just displays the two kinds of children,
// embedding them in some other markup
<TakesChildren
// for component props, you can shorthand
// `render_prop=render_prop` => `render_prop`
// (this doesn't work for HTML element attributes)
render_prop
>
// these look just like the children of an HTML element
<p>"Here's a child."</p>
<p>"Here's another child."</p>
</TakesChildren>
<hr/>
// This component actually iterates over and wraps the children
<WrapsChildren>
<p>"Here's a child."</p>
<p>"Here's another child."</p>
</WrapsChildren>
}
}
/// 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>`
/// ... aren't you glad we named it `Children` instead?
children: Children,
) -> impl IntoView
where
F: Fn() -> IV,
IV: IntoView,
{
view! { cx,
<h1><code>"<TakesChildren/>"</code></h1>
<h2>"Render Prop"</h2>
{render_prop()}
<hr/>
<h2>"Children"</h2>
{children(cx)}
}
}
/// 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
// `nodes` field that contains a Vec<View>
// this means we can iterate over the children
// to create something new!
let children = children(cx)
.nodes
.into_iter()
.map(|child| view! { cx, <li>{child}</li> })
.collect::<Vec<_>>();
view! { cx,
<h1><code>"<WrapsChildren/>"</code></h1>
// wrap our wrapped children in a UL
<ul>{children}</ul>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -80,7 +80,7 @@ This crate can be run without `cargo-leptos`, using `wasm-pack` and `cargo`. To
To run it as a server side app with hydration, first you should run
```bash
wasm-pack build --target=web --no-default-features --features=hydrate
wasm-pack build --target=web --debug --no-default-features --features=hydrate
```
to generate the WebAssembly to hydrate the HTML delivered from the server.
@@ -99,4 +99,4 @@ cargo run --no-default-features --features=ssr
You'll need to install trunk to client side render this bundle.
1. `cargo install trunk`
Then the site can be served with `trunk serve --open`
Then the site can be served with `trunk serve --open`

View File

@@ -90,8 +90,8 @@ By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If
## Alternatives to cargo-leptos
This crate can be run without `cargo-leptos`, using `wasm-pack` and `cargo`. To do so, you'll need to install some other tools.
0. `cargo install wasm-pack`
This crate can be run without `cargo-leptos`, using `wasm-pack` and `cargo`. To do so, you'll need to install some other tools. 0. `cargo install wasm-pack`
1. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
### Server Side Rendering With Hydration
@@ -99,7 +99,7 @@ This crate can be run without `cargo-leptos`, using `wasm-pack` and `cargo`. To
To run it as a server side app with hydration, first you should run
```bash
wasm-pack build --target=web --no-default-features --features=hydrate
wasm-pack build --target=web --debug --no-default-features --features=hydrate
```
to generate the WebAssembly to hydrate the HTML delivered from the server.

View File

@@ -605,7 +605,8 @@ where
let res_options3 = default_res_options.clone();
let local_pool = get_leptos_pool();
let (tx, rx) = futures::channel::mpsc::channel(8);
let (runtime_tx, runtime_rx) = futures::channel::oneshot::channel();
let (complete_tx, complete_rx) =
futures::channel::oneshot::channel();
let current_span = tracing::Span::current();
local_pool.spawn_pinned(move || async move {
@@ -630,17 +631,16 @@ where
replace_blocks
);
runtime_tx.send(runtime).expect("should be able to send runtime");
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
complete_rx.await.expect("could not receive completion message");
eprintln!("\n\n\n\nreceived completion message");
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
runtime.dispose();
}.instrument(current_span));
async move {
let runtime = runtime_rx
.await
.expect("runtime should be sent by renderer");
generate_response(res_options3, rx, runtime).await
}
generate_response(res_options3, rx, complete_tx)
})
}
}
@@ -649,7 +649,7 @@ where
async fn generate_response(
res_options: ResponseOptions,
rx: Receiver<String>,
runtime: RuntimeId,
complete_tx: futures::channel::oneshot::Sender<()>,
) -> Response<StreamBody<PinnedHtmlStream>> {
let mut stream = Box::pin(rx.map(|html| Ok(Bytes::from(html))));
@@ -664,8 +664,10 @@ async fn generate_response(
futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()])
.chain(stream)
.chain(futures::stream::once(async move {
runtime.dispose();
Ok(Default::default())
complete_tx
.send(())
.expect("could not send completion message");
Ok(Bytes::from(String::new()))
}));
let mut res = Response::new(StreamBody::new(
@@ -781,7 +783,7 @@ where
let full_path = format!("http://leptos.dev{path}");
let (tx, rx) = futures::channel::mpsc::channel(8);
let (runtime_tx, runtime_rx) =
let (complete_tx, complete_rx) =
futures::channel::oneshot::channel();
let local_pool = get_leptos_pool();
let current_span = tracing::Span::current();
@@ -802,15 +804,16 @@ where
add_context,
);
runtime_tx.send(runtime).expect("should be able to send runtime");
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
complete_rx.await.expect("could not receive completion message");
eprintln!("\n\n\nreceived completion message");
runtime.dispose();
}.instrument(current_span));
let runtime = runtime_rx
.await
.expect("runtime should be sent by renderer");
generate_response(res_options3, rx, runtime).await
generate_response(res_options3, rx, complete_tx).await
}
})
}

View File

@@ -28,13 +28,13 @@ leptos = { path = "." }
default = ["serde"]
template_macro = ["leptos_dom/web", "web-sys", "wasm-bindgen"]
csr = [
"leptos_dom/web",
"leptos_dom/csr",
"leptos_macro/csr",
"leptos_reactive/csr",
"leptos_server/csr",
]
hydrate = [
"leptos_dom/web",
"leptos_dom/hydrate",
"leptos_macro/hydrate",
"leptos_reactive/hydrate",
"leptos_server/hydrate",

View File

@@ -1,4 +1,4 @@
use leptos_dom::{Fragment, IntoView, View};
use leptos_dom::{Fragment, HydrationCtx, IntoView, View};
use leptos_macro::component;
use leptos_reactive::{
create_isomorphic_effect, use_context, Scope, SignalGet, SignalSetter,
@@ -99,7 +99,7 @@ where
let is_first_run =
is_first_run(&first_run, &suspense_context);
first_run.set(is_first_run);
first_run.set(false);
if let Some(prev_children) = &*prev_child.borrow() {
if is_first_run {
@@ -127,7 +127,10 @@ where
if is_first_run(&first_run, &suspense_context) {
let has_local_only = suspense_context.has_local_only()
|| cfg!(feature = "csr");
if !has_local_only || child_runs.get() > 0 {
if (!has_local_only || child_runs.get() > 0)
&& (cfg!(feature = "csr")
|| HydrationCtx::is_hydrating())
{
first_run.set(false);
}
}
@@ -163,7 +166,7 @@ fn is_first_run(
// SSR but with only local resources (so, has not streamed)
(_, false, true) => true,
// hydrate: it's the first run
(_, true, _) => true,
(first_run, true, _) => HydrationCtx::is_hydrating() || first_run,
}
}
}

View File

@@ -164,7 +164,9 @@ features = [
[features]
default = []
web = ["leptos_reactive/csr"]
web = []
csr = ["leptos_reactive/csr", "web"]
hydrate = ["leptos_reactive/hydrate", "web"]
ssr = ["leptos_reactive/ssr"]
nightly = ["leptos_reactive/nightly"]
nonce = ["dep:base64", "dep:getrandom", "dep:rand"]

View File

@@ -1,17 +1,16 @@
use cfg_if::cfg_if;
use std::{cell::RefCell, fmt::Display};
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
#[cfg(all(target_arch = "wasm32", feature = "hydrate"))]
mod hydration {
use once_cell::unsync::Lazy as LazyCell;
use std::collections::HashMap;
use std::{cell::RefCell, collections::HashMap};
use wasm_bindgen::JsCast;
// We can tell if we start in hydration mode by checking to see if the
// id "_0-1" is present in the DOM. If it is, we know we are hydrating from
// the server, if not, we are starting off in CSR
thread_local! {
static HYDRATION_COMMENTS: LazyCell<HashMap<String, web_sys::Comment>> = LazyCell::new(|| {
pub static HYDRATION_COMMENTS: LazyCell<HashMap<String, web_sys::Comment>> = LazyCell::new(|| {
let document = crate::document();
let body = document.body().unwrap();
let walker = document
@@ -31,7 +30,7 @@ cfg_if! {
});
#[cfg(debug_assertions)]
pub(crate) static VIEW_MARKERS: LazyCell<HashMap<String, web_sys::Comment>> = LazyCell::new(|| {
pub static VIEW_MARKERS: LazyCell<HashMap<String, web_sys::Comment>> = LazyCell::new(|| {
let document = crate::document();
let body = document.body().unwrap();
let walker = document
@@ -48,7 +47,7 @@ cfg_if! {
map
});
static IS_HYDRATING: RefCell<LazyCell<bool>> = RefCell::new(LazyCell::new(|| {
pub static IS_HYDRATING: RefCell<LazyCell<bool>> = RefCell::new(LazyCell::new(|| {
#[cfg(debug_assertions)]
return crate::document().get_element_by_id("_0-1").is_some()
|| crate::document().get_element_by_id("_0-1o").is_some()
@@ -60,12 +59,14 @@ cfg_if! {
}));
}
pub(crate) fn get_marker(id: &str) -> Option<web_sys::Comment> {
HYDRATION_COMMENTS.with(|comments| comments.get(id).cloned())
pub fn get_marker(id: &str) -> Option<web_sys::Comment> {
HYDRATION_COMMENTS.with(|comments| comments.get(id).cloned())
}
}
}
#[cfg(all(target_arch = "wasm32", feature = "hydrate"))]
pub(crate) use hydration::*;
/// A stable identifier within the server-rendering or hydration process.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
pub struct HydrationKey {
@@ -125,16 +126,27 @@ impl HydrationCtx {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn stop_hydrating() {
IS_HYDRATING.with(|is_hydrating| {
std::mem::take(&mut *is_hydrating.borrow_mut());
})
#[cfg(feature = "hydrate")]
{
IS_HYDRATING.with(|is_hydrating| {
std::mem::take(&mut *is_hydrating.borrow_mut());
})
}
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn is_hydrating() -> bool {
IS_HYDRATING.with(|is_hydrating| **is_hydrating.borrow())
/// Whether the UI is currently in the process of hydrating from the server-sent HTML.
pub fn is_hydrating() -> bool {
#[cfg(all(target_arch = "wasm32", feature = "hydrate"))]
{
IS_HYDRATING.with(|is_hydrating| **is_hydrating.borrow())
}
#[cfg(not(all(target_arch = "wasm32", feature = "hydrate")))]
{
false
}
}
#[allow(dead_code)] // not used in CSR
pub(crate) fn to_string(id: &HydrationKey, closing: bool) -> String {
#[cfg(debug_assertions)]
return format!("_{id}{}", if closing { 'c' } else { 'o' });

View File

@@ -416,11 +416,18 @@ impl Comment {
Self { content }
} else {
#[cfg(not(feature = "hydrate"))]
{
_ = id;
_ = closing;
}
let node = COMMENT.with(|comment| comment.clone_node().unwrap());
#[cfg(debug_assertions)]
node.set_text_content(Some(&format!(" {content} ")));
#[cfg(feature = "hydrate")]
if HydrationCtx::is_hydrating() {
let id = HydrationCtx::to_string(id, closing);

View File

@@ -189,6 +189,7 @@ pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacemen
// create the runtime
let runtime = create_runtime();
eprintln!("\n\ncreated runtime {runtime:?}");
let ((shell, pending_resources, pending_fragments, serializers), scope, _) =
run_scope_undisposed(runtime, {

View File

@@ -1,386 +1,397 @@
console.log("[HOT RELOADING] Connected to server.");
function patch(json) {
try {
const views = JSON.parse(json);
for (const [id, patches] of views) {
console.log("[HOT RELOAD]", id, patches);
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT),
open = `leptos-view|${id}|open`,
close = `leptos-view|${id}|close`;
let start, end;
const instances = [];
while (walker.nextNode()) {
if (walker.currentNode.textContent == open) {
start = walker.currentNode;
} else if (walker.currentNode.textContent == close) {
end = walker.currentNode;
instances.push([start, end]);
start = undefined;
end = undefined;
}
}
try {
const views = JSON.parse(json);
for (const [id, patches] of views) {
console.log("[HOT RELOAD]", id, patches);
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT),
open = `leptos-view|${id}|open`,
close = `leptos-view|${id}|close`;
let start, end;
const instances = [];
while (walker.nextNode()) {
if (walker.currentNode.textContent == open) {
start = walker.currentNode;
} else if (walker.currentNode.textContent == close) {
end = walker.currentNode;
instances.push([start, end]);
start = undefined;
end = undefined;
}
}
for(const [start, end] of instances) {
// build tree of current actual children
const actualChildren = childrenFromRange(start.parentElement, start, end);
const actions = [];
for (const [start, end] of instances) {
// build tree of current actual children
const actualChildren = childrenFromRange(start.parentElement, start, end);
const actions = [];
// build up the set of actions
for (const patch of patches) {
const child = childAtPath(
actualChildren.length > 1 ? { children: actualChildren } : actualChildren[0],
patch.path
);
const action = patch.action;
if (action == "ClearChildren") {
actions.push(() => {
console.log("[HOT RELOAD] > ClearChildren", child.node);
child.node.textContent = ""
});
} else if (action.ReplaceWith) {
actions.push(() => {
console.log("[HOT RELOAD] > ReplaceWith", child, action.ReplaceWith);
const replacement = fromReplacementNode(action.ReplaceWith, actualChildren);
if (child.node) {
child.node.replaceWith(replacement)
} else {
const range = new Range();
range.setStartAfter(child.start);
range.setEndAfter(child.end);
range.deleteContents();
child.start.replaceWith(replacement);
}
});
} else if (action.ChangeTagName) {
const oldNode = child.node;
actions.push(() => {
console.log("[HOT RELOAD] > ChangeTagName", child.node, action.ChangeTagName);
const newElement = document.createElement(action.ChangeTagName);
for (const attr of oldNode.attributes) {
newElement.setAttribute(attr.name, attr.value);
}
for (const childNode of child.node.childNodes) {
newElement.appendChild(childNode);
}
child.node.replaceWith(newElement)
});
} else if (action.RemoveAttribute) {
actions.push(() => {
console.log("[HOT RELOAD] > RemoveAttribute", child.node, action.RemoveAttribute);
child.node.removeAttribute(action.RemoveAttribute);
});
} else if (action.SetAttribute) {
const [name, value] = action.SetAttribute;
actions.push(() => {
console.log("[HOT RELOAD] > SetAttribute", child.node, action.SetAttribute);
child.node.setAttribute(name, value);
});
} else if (action.SetText) {
const node = child.node;
actions.push(() => {
console.log("[HOT RELOAD] > SetText", child.node, action.SetText);
node.textContent = action.SetText
});
} else if (action.AppendChildren) {
actions.push(() => {
console.log("[HOT RELOAD] > AppendChildren", child.node, action.AppendChildren);
const newChildren = fromReplacementNode(action.AppendChildren, actualChildren);
child.node.append(newChildren);
});
} else if (action.RemoveChild) {
actions.push(() => {
console.log("[HOT RELOAD] > RemoveChild", child.node, child.children, action.RemoveChild);
const toRemove = child.children[action.RemoveChild.at];
let toRemoveNode = toRemove.node;
if (!toRemoveNode) {
const range = new Range();
range.setStartBefore(toRemove.start);
range.setEndAfter(toRemove.end);
toRemoveNode = range.deleteContents();
} else {
toRemoveNode.parentNode.removeChild(toRemoveNode);
}
})
} else if (action.InsertChild) {
const newChild = fromReplacementNode(action.InsertChild.child, actualChildren);
let children = [];
if(child.children) {
children = child.children;
} else if (child.start && child.end) {
children = childrenFromRange(child.node || child.start.parentElement, start, end);
} else {
console.warn("InsertChildAfter could not build children.");
}
const before = children[action.InsertChild.before];
actions.push(() => {
console.log("[HOT RELOAD] > InsertChild", child, child.node, action.InsertChild, " before ", before);
if (!before && child.node) {
child.node.appendChild(newChild);
} else {
let node = child.node || child.end.parentElement;
const reference = before ? before.node || before.start : child.end;
node.insertBefore(newChild, reference);
}
})
} else if (action.InsertChildAfter) {
const newChild = fromReplacementNode(action.InsertChildAfter.child, actualChildren);
let children = [];
if(child.children) {
children = child.children;
} else if (child.start && child.end) {
children = childrenFromRange(child.node || child.start.parentElement, start, end);
} else {
console.warn("InsertChildAfter could not build children.");
}
const after = children[action.InsertChildAfter.after];
actions.push(() => {
console.log("[HOT RELOAD] > InsertChildAfter", child, child.node, action.InsertChildAfter, " after ", after);
if (child.node && (!after || !(after.node || after.start).nextSibling)) {
child.node.appendChild(newChild);
} else {
const node = child.node || child.end;
const parent = node.nodeType === Node.COMMENT_NODE ? node.parentNode : node;
if(!after) {
parent.appendChild(newChild);
} else {
parent.insertBefore(newChild, (after.node || after.start).nextSibling);
}
}
})
} else {
console.warn("[HOT RELOADING] Unmatched action", action);
}
}
// build up the set of actions
for (const patch of patches) {
const child = childAtPath(
actualChildren.length > 1 ? { children: actualChildren } : actualChildren[0],
patch.path
);
const action = patch.action;
if (action == "ClearChildren") {
actions.push(() => {
console.log("[HOT RELOAD] > ClearChildren", child.node);
child.node.textContent = "";
});
} else if (action.ReplaceWith) {
actions.push(() => {
console.log("[HOT RELOAD] > ReplaceWith", child, action.ReplaceWith);
const replacement = fromReplacementNode(action.ReplaceWith, actualChildren);
if (child.node) {
child.node.replaceWith(replacement);
} else {
const range = new Range();
range.setStartAfter(child.start);
range.setEndAfter(child.end);
range.deleteContents();
child.start.replaceWith(replacement);
}
});
} else if (action.ChangeTagName) {
const oldNode = child.node;
actions.push(() => {
console.log("[HOT RELOAD] > ChangeTagName", child.node, action.ChangeTagName);
const newElement = document.createElement(action.ChangeTagName);
for (const attr of oldNode.attributes) {
newElement.setAttribute(attr.name, attr.value);
}
for (const childNode of child.node.childNodes) {
newElement.appendChild(childNode);
}
// actually run the actions
// the reason we delay them is so that children aren't moved before other children are found, etc.
for (const action of actions) {
action();
}
child.node.replaceWith(newElement);
});
} else if (action.RemoveAttribute) {
actions.push(() => {
console.log("[HOT RELOAD] > RemoveAttribute", child.node, action.RemoveAttribute);
child.node.removeAttribute(action.RemoveAttribute);
});
} else if (action.SetAttribute) {
const [name, value] = action.SetAttribute;
actions.push(() => {
console.log("[HOT RELOAD] > SetAttribute", child.node, action.SetAttribute);
child.node.setAttribute(name, value);
});
} else if (action.SetText) {
const node = child.node;
actions.push(() => {
console.log("[HOT RELOAD] > SetText", child.node, action.SetText);
node.textContent = action.SetText;
});
} else if (action.AppendChildren) {
actions.push(() => {
console.log("[HOT RELOAD] > AppendChildren", child.node, action.AppendChildren);
const newChildren = fromReplacementNode(action.AppendChildren, actualChildren);
child.node.append(newChildren);
});
} else if (action.RemoveChild) {
actions.push(() => {
console.log("[HOT RELOAD] > RemoveChild", child.node, child.children, action.RemoveChild);
const toRemove = child.children[action.RemoveChild.at];
let toRemoveNode = toRemove.node;
if (!toRemoveNode) {
const range = new Range();
range.setStartBefore(toRemove.start);
range.setEndAfter(toRemove.end);
toRemoveNode = range.deleteContents();
} else {
toRemoveNode.parentNode.removeChild(toRemoveNode);
}
});
} else if (action.InsertChild) {
const newChild = fromReplacementNode(action.InsertChild.child, actualChildren);
let children = [];
if (child.children) {
children = child.children;
} else if (child.start && child.end) {
children = childrenFromRange(child.node || child.start.parentElement, start, end);
} else {
console.warn("InsertChildAfter could not build children.");
}
}
} catch (e) {
console.warn("[HOT RELOADING] Error: ", e);
}
const before = children[action.InsertChild.before];
actions.push(() => {
console.log("[HOT RELOAD] > InsertChild", child, child.node, action.InsertChild, " before ", before);
if (!before && child.node) {
child.node.appendChild(newChild);
} else {
let node = child.node || child.end.parentElement;
const reference = before ? before.node || before.start : child.end;
node.insertBefore(newChild, reference);
}
});
} else if (action.InsertChildAfter) {
const newChild = fromReplacementNode(action.InsertChildAfter.child, actualChildren);
let children = [];
if (child.children) {
children = child.children;
} else if (child.start && child.end) {
children = childrenFromRange(child.node || child.start.parentElement, start, end);
} else {
console.warn("InsertChildAfter could not build children.");
}
const after = children[action.InsertChildAfter.after];
actions.push(() => {
console.log(
"[HOT RELOAD] > InsertChildAfter",
child,
child.node,
action.InsertChildAfter,
" after ",
after
);
if (child.node && (!after || !(after.node || after.start).nextSibling)) {
child.node.appendChild(newChild);
} else {
const node = child.node || child.end;
const parent = node.nodeType === Node.COMMENT_NODE ? node.parentNode : node;
if (!after) {
parent.appendChild(newChild);
} else {
parent.insertBefore(newChild, (after.node || after.start).nextSibling);
}
}
});
} else {
console.warn("[HOT RELOADING] Unmatched action", action);
}
}
function fromReplacementNode(node, actualChildren) {
if (node.Html) {
return fromHTML(node.Html);
}
else if (node.Fragment) {
const frag = document.createDocumentFragment();
for (const child of node.Fragment) {
frag.appendChild(fromReplacementNode(child, actualChildren));
}
return frag;
}
else if (node.Element) {
const element = document.createElement(node.Element.name);
for (const [name, value] of node.Element.attrs) {
element.setAttribute(name, value);
}
for (const child of node.Element.children) {
element.appendChild(fromReplacementNode(child, actualChildren));
}
return element;
}
else {
const child = childAtPath(
actualChildren.length > 1 ? { children: actualChildren } : actualChildren[0],
node.Path
);
if (child) {
let childNode = child.node;
if (!childNode) {
const range = new Range();
range.setStartBefore(child.start);
range.setEndAfter(child.end);
// okay this is somewhat silly
// if we do cloneContents() here to return it,
// we strip away the event listeners
// if we're moving just one object, this is less than ideal
// so I'm actually going to *extract* them, then clone and reinsert
/* const toReinsert = range.cloneContents();
// actually run the actions
// the reason we delay them is so that children aren't moved before other children are found, etc.
for (const action of actions) {
action();
}
}
}
} catch (e) {
console.warn("[HOT RELOADING] Error: ", e);
}
function fromReplacementNode(node, actualChildren) {
if (node.Html) {
return fromHTML(node.Html);
} else if (node.Fragment) {
const frag = document.createDocumentFragment();
for (const child of node.Fragment) {
frag.appendChild(fromReplacementNode(child, actualChildren));
}
return frag;
} else if (node.Element) {
const element = document.createElement(node.Element.name);
for (const [name, value] of node.Element.attrs) {
element.setAttribute(name, value);
}
for (const child of node.Element.children) {
element.appendChild(fromReplacementNode(child, actualChildren));
}
return element;
} else {
const child = childAtPath(
actualChildren.length > 1 ? { children: actualChildren } : actualChildren[0],
node.Path
);
if (child) {
let childNode = child.node;
if (!childNode) {
const range = new Range();
range.setStartBefore(child.start);
range.setEndAfter(child.end);
// okay this is somewhat silly
// if we do cloneContents() here to return it,
// we strip away the event listeners
// if we're moving just one object, this is less than ideal
// so I'm actually going to *extract* them, then clone and reinsert
/* const toReinsert = range.cloneContents();
if (child.end.nextSibling) {
child.end.parentNode.insertBefore(toReinsert, child.end.nextSibling);
} else {
child.end.parentNode.appendChild(toReinsert);
} */
childNode = range.cloneContents();
}
return childNode;
} else {
console.warn("[HOT RELOADING] Could not find replacement node at ", node.Path);
return undefined;
}
}
}
function buildActualChildren(element, range) {
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT,
{
acceptNode(node) {
return node.parentNode == element && (!range || range.isPointInRange(node, 0));
}
}
);
const actualChildren = [],
elementCount = {};
while (walker.nextNode()) {
if (walker.currentNode.nodeType == Node.ELEMENT_NODE) {
if (elementCount[walker.currentNode.nodeName]) {
elementCount[walker.currentNode.nodeName] += 1;
} else {
elementCount[walker.currentNode.nodeName] = 0;
}
elementCount[walker.currentNode.nodeName];
actualChildren.push({
type: "element",
name: walker.currentNode.nodeName,
number: elementCount[walker.currentNode.nodeName],
node: walker.currentNode,
children: buildActualChildren(walker.currentNode)
});
} else if (walker.currentNode.nodeType == Node.TEXT_NODE) {
actualChildren.push({
type: "text",
node: walker.currentNode
});
} else if (walker.currentNode.nodeType == Node.COMMENT_NODE) {
if (walker.currentNode.textContent.trim().startsWith("leptos-view")) {
if (walker.currentNode.textContent.trim().endsWith("-children|open")) {
const startingName = walker.currentNode.textContent.trim();
const componentName = startingName.replace("-children|open").replace("leptos-view|");
const endingName = `leptos-view|${componentName}-children|close`;
let start = walker.currentNode;
let depth = 1;
while (walker.nextNode()) {
if (walker.currentNode.textContent.trim() == endingName) {
depth--;
} else if (walker.currentNode.textContent.trim() == startingName) {
depth++;
}
if(depth == 0) {
break;
}
}
let end = walker.currentNode;
actualChildren.push({
type: "fragment",
start: start.nextSibling,
end: end.previousSibling,
children: childrenFromRange(start.parentElement, start.nextSibling, end.previousSibling)
});
}
} else if (walker.currentNode.textContent.trim() == "<() />") {
actualChildren.push({
type: "unit",
node: walker.currentNode
});
} else if (walker.currentNode.textContent.trim() == "<DynChild>") {
let start = walker.currentNode;
let depth = 1;
while (walker.nextNode()) {
if (walker.currentNode.textContent.trim() == "</DynChild>") {
depth--;
} else if (walker.currentNode.textContent.trim() == "<DynChild>") {
depth++;
}
if(depth == 0) {
break;
}
}
let end = walker.currentNode;
actualChildren.push({
type: "dyn-child",
start, end
});
} else if (walker.currentNode.textContent.trim() == "<>") {
let start = walker.currentNode;
let depth = 1;
while (walker.nextNode()) {
if (walker.currentNode.textContent.trim() == "</>") {
depth--;
} else if (walker.currentNode.textContent.trim() == "<>") {
depth++;
}
if(depth == 0) {
break;
}
}
let end = walker.currentNode;
actualChildren.push({
type: "fragment",
children: childrenFromRange(start.parentElement, start, end),
start, end
});
} else if (walker.currentNode.textContent.trim().startsWith("<")) {
let componentName = walker.currentNode.textContent.trim();
let endMarker = componentName.replace("<", "</");
let depth = 1;
let start = walker.currentNode;
while (walker.nextNode()) {
if (walker.currentNode.textContent.trim() == endMarker) {
depth--;
} else if (walker.currentNode.textContent.trim() == componentName) {
depth++;
}
if(depth == 0) {
break;
}
}
let end = walker.currentNode;
actualChildren.push({
type: "component",
start, end
});
}
} else {
console.warn("[HOT RELOADING] Building children, encountered", walker.currentNode);
}
}
return actualChildren;
}
function childAtPath(element, path) {
if (path.length == 0) {
return element;
} else if (element.children) {
const next = element.children[path[0]],
rest = path.slice(1);
return childAtPath(next, rest);
} else if (path == [0]) {
return element;
} else if (element.start && element.end) {
const actualChildren = childrenFromRange(element.node || element.start.parentElement, element.start, element.end);
return childAtPath({ children: actualChildren }, path);
} else {
console.warn("[HOT RELOADING] Child at ", path, "not found in ", element);
return element;
}
}
function childrenFromRange(parent, start, end) {
const range = new Range();
range.setStartAfter(start);
range.setEndBefore(end);
return buildActualChildren(parent, range);
childNode = range.cloneContents();
}
return childNode;
} else {
console.warn("[HOT RELOADING] Could not find replacement node at ", node.Path);
return undefined;
}
}
}
function fromHTML(html) {
const template = document.createElement("template");
template.innerHTML = html;
return template.content.cloneNode(true);
}
function buildActualChildren(element, range) {
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT,
{
acceptNode(node) {
if (node.parentNode == element && (!range || range.isPointInRange(node, 0))) {
return NodeFilter.FILTER_ACCEPT;
} else {
return NodeFilter.FILTER_REJECT;
}
},
}
);
const actualChildren = [],
elementCount = {};
while (walker.nextNode()) {
if (walker.currentNode.nodeType == Node.ELEMENT_NODE) {
if (elementCount[walker.currentNode.nodeName]) {
elementCount[walker.currentNode.nodeName] += 1;
} else {
elementCount[walker.currentNode.nodeName] = 0;
}
elementCount[walker.currentNode.nodeName];
actualChildren.push({
type: "element",
name: walker.currentNode.nodeName,
number: elementCount[walker.currentNode.nodeName],
node: walker.currentNode,
children: buildActualChildren(walker.currentNode),
});
} else if (walker.currentNode.nodeType == Node.TEXT_NODE) {
actualChildren.push({
type: "text",
node: walker.currentNode,
});
} else if (walker.currentNode.nodeType == Node.COMMENT_NODE) {
if (walker.currentNode.textContent.trim().startsWith("leptos-view")) {
if (walker.currentNode.textContent.trim().endsWith("-children|open")) {
const startingName = walker.currentNode.textContent.trim();
const componentName = startingName.replace("-children|open").replace("leptos-view|");
const endingName = `leptos-view|${componentName}-children|close`;
let start = walker.currentNode;
let depth = 1;
while (walker.nextNode()) {
if (walker.currentNode.textContent.trim() == endingName) {
depth--;
} else if (walker.currentNode.textContent.trim() == startingName) {
depth++;
}
if (depth == 0) {
break;
}
}
let end = walker.currentNode;
actualChildren.push({
type: "fragment",
start: start.nextSibling,
end: end.previousSibling,
children: childrenFromRange(start.parentElement, start.nextSibling, end.previousSibling),
});
}
} else if (walker.currentNode.textContent.trim() == "<() />") {
actualChildren.push({
type: "unit",
node: walker.currentNode,
});
} else if (walker.currentNode.textContent.trim() == "<DynChild>") {
let start = walker.currentNode;
let depth = 1;
while (walker.nextNode()) {
if (walker.currentNode.textContent.trim() == "</DynChild>") {
depth--;
} else if (walker.currentNode.textContent.trim() == "<DynChild>") {
depth++;
}
if (depth == 0) {
break;
}
}
let end = walker.currentNode;
actualChildren.push({
type: "dyn-child",
start,
end,
});
} else if (walker.currentNode.textContent.trim() == "<>") {
let start = walker.currentNode;
let depth = 1;
while (walker.nextNode()) {
if (walker.currentNode.textContent.trim() == "</>") {
depth--;
} else if (walker.currentNode.textContent.trim() == "<>") {
depth++;
}
if (depth == 0) {
break;
}
}
let end = walker.currentNode;
actualChildren.push({
type: "fragment",
children: childrenFromRange(start.parentElement, start, end),
start,
end,
});
} else if (walker.currentNode.textContent.trim().startsWith("<")) {
let componentName = walker.currentNode.textContent.trim();
let endMarker = componentName.replace("<", "</");
let depth = 1;
let start = walker.currentNode;
while (walker.nextNode()) {
if (walker.currentNode.textContent.trim() == endMarker) {
depth--;
} else if (walker.currentNode.textContent.trim() == componentName) {
depth++;
}
if (depth == 0) {
break;
}
}
let end = walker.currentNode;
actualChildren.push({
type: "component",
start,
end,
});
}
} else {
console.warn("[HOT RELOADING] Building children, encountered", walker.currentNode);
}
}
return actualChildren;
}
function childAtPath(element, path) {
if (path.length == 0) {
return element;
} else if (element.children) {
const next = element.children[path[0]],
rest = path.slice(1);
return childAtPath(next, rest);
} else if (path == [0]) {
return element;
} else if (element.start && element.end) {
const actualChildren = childrenFromRange(element.node || element.start.parentElement, element.start, element.end);
return childAtPath({ children: actualChildren }, path);
} else {
console.warn("[HOT RELOADING] Child at ", path, "not found in ", element);
return element;
}
}
function childrenFromRange(parent, start, end) {
const range = new Range();
range.setStartAfter(start);
range.setEndBefore(end);
return buildActualChildren(parent, range);
}
function fromHTML(html) {
const template = document.createElement("template");
template.innerHTML = html;
return template.content.cloneNode(true);
}
}

View File

@@ -415,11 +415,14 @@ pub struct RuntimeId;
impl RuntimeId {
/// Removes the runtime, disposing all its child [`Scope`](crate::Scope)s.
pub fn dispose(self) {
cfg_if! {
if #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
let runtime = RUNTIMES.with(move |runtimes| runtimes.borrow_mut().remove(self));
drop(runtime);
}
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
{
eprintln!("\n\ndisposing of {self:?}");
let runtime = RUNTIMES.with(move |runtimes| runtimes.borrow_mut().remove(self))
.expect("Attempted to dispose of a reactive runtime that was not found. This suggests \
a possible memory leak. Please open an issue with details at https://github.com/leptos-rs/leptos");
drop(runtime);
}
}

View File

@@ -137,6 +137,7 @@ impl<T> StoredValue<T> {
/// Same as [`StoredValue::with_value`] but returns [`Some(O)]` only if
/// the stored value has not yet been disposed. [`None`] otherwise.
pub fn try_with_value<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
eprintln!("\n\nlooking in {:?}", self.runtime);
with_runtime(self.runtime, |runtime| {
let value = {
let values = runtime.stored_values.borrow();
@@ -194,8 +195,10 @@ impl<T> StoredValue<T> {
/// stored value has not yet been disposed, [`None`] otherwise.
pub fn try_update_value<O>(self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
with_runtime(self.runtime, |runtime| {
let values = runtime.stored_values.borrow();
let value = values.get(self.id)?;
let value = {
let values = runtime.stored_values.borrow();
values.get(self.id)?.clone()
};
let mut value = value.borrow_mut();
let value = value.downcast_mut::<T>()?;
Some(f(value))
@@ -228,13 +231,20 @@ impl<T> StoredValue<T> {
/// stored value has not yet been disposed, [`Some(T)`] otherwise.
pub fn try_set_value(&self, value: T) -> Option<T> {
with_runtime(self.runtime, |runtime| {
let values = runtime.stored_values.borrow();
let n = values.get(self.id);
let mut n = n.map(|n| n.borrow_mut());
let n = n.as_mut().and_then(|n| n.downcast_mut::<T>());
let n = {
let values = runtime.stored_values.borrow();
values.get(self.id).map(Rc::clone)
};
if let Some(n) = n {
*n = value;
None
let mut n = n.borrow_mut();
let n = n.downcast_mut::<T>();
if let Some(n) = n {
*n = value;
None
} else {
Some(value)
}
} else {
Some(value)
}
@@ -284,6 +294,7 @@ pub fn store_value<T>(cx: Scope, value: T) -> StoredValue<T>
where
T: 'static,
{
eprintln!("\nstoring in {:?}", cx.runtime);
let id = with_runtime(cx.runtime, |runtime| {
runtime
.stored_values

View File

@@ -135,6 +135,7 @@ where
/// The URL associated with the action (typically as part of a server function.)
/// This enables integration with the `ActionForm` component in `leptos_router`.
pub fn url(&self) -> Option<String> {
eprintln!("action = {:?}", self.0);
self.0.with_value(|a| a.url.as_ref().cloned())
}

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.4.3"
version = "0.4.5"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -342,8 +342,7 @@ fn current_window_origin() -> String {
///
/// ## Encoding
/// **Note:** `<ActionForm/>` only works with server functions that use the
/// default `Url` encoding or the `GetJSON` encoding, not with `CBOR` or other
/// encoding schemes. This is to ensure that `<ActionForm/>` works correctly
/// default `Url` encoding. This is to ensure that `<ActionForm/>` works correctly
/// both before and after WASM has loaded.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
@@ -493,6 +492,19 @@ where
});
});
let class = class.map(|bx| bx.into_attribute_boxed(cx));
#[cfg(debug_assertions)]
{
if I::encoding() != server_fn::Encoding::Url {
leptos::warn!(
"<ActionForm/> only supports the `Url` encoding for server \
functions, but {} uses {:?}.",
std::any::type_name::<I>(),
I::encoding()
);
}
}
let mut props = FormProps::builder()
.action(action_url)
.version(version)

View File

@@ -36,9 +36,8 @@ impl TryFrom<&str> for Url {
type Error = String;
fn try_from(url: &str) -> Result<Self, Self::Error> {
let fake_host = String::from("http://leptos");
let url =
web_sys::Url::new_with_base(url, &fake_host).map_js_error()?;
let fake_host = "http://leptos";
let url = web_sys::Url::new_with_base(url, fake_host).map_js_error()?;
Ok(Self {
origin: url.origin(),
pathname: url.pathname(),