mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 15:44:42 -05:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7376b76558 | ||
|
|
579abc586c | ||
|
|
b2c75d215b | ||
|
|
951607de74 | ||
|
|
c1c49ce53b | ||
|
|
e8aa9b24f1 | ||
|
|
3036cd223e | ||
|
|
1ae5150b08 | ||
|
|
47148f2033 | ||
|
|
4d4d15436b | ||
|
|
7f4741b3a3 | ||
|
|
85644a7c1c | ||
|
|
f40ae6af30 | ||
|
|
708e1a5aab | ||
|
|
55613c9a31 | ||
|
|
e6590c7d31 | ||
|
|
5af2f4e98d |
28
.github/workflows/check-examples.yml
vendored
28
.github/workflows/check-examples.yml
vendored
@@ -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
|
||||
|
||||
37
.github/workflows/ci.yml
vendored
37
.github/workflows/ci.yml
vendored
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
It’s important to say that the main feature `cargo-leptos` remains its ability
|
||||
to conveniently tie together different build tooling, compiling your app to
|
||||
|
||||
28
Cargo.toml
28
Cargo.toml
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -94,3 +94,83 @@ Now, there’s 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -170,3 +170,121 @@ In fact, in this case, we don’t 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -65,3 +65,117 @@ You’ll 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>
|
||||
|
||||
@@ -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, here’s 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.
|
||||
|
||||
|
||||
@@ -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/> })
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.4.3"
|
||||
version = "0.4.5"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.4.3"
|
||||
version = "0.4.5"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user