Compare commits

..

1 Commits

Author SHA1 Message Date
Greg Johnston
8943539043 docs: clarify WASM target 2023-07-09 17:05:37 -04:00
117 changed files with 1252 additions and 3841 deletions

View File

@@ -14,7 +14,6 @@ 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
@@ -35,40 +34,13 @@ 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
uses: ./.github/workflows/run-cargo-make-task.yml
uses: ./.github/workflows/run-example-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "check"

46
.github/workflows/check.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Check
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
jobs:
test:
name: Run `cargo check` ${{ matrix.os }} (using rustc ${{ matrix.rust }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- nightly
os:
- ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt
- name: Add wasm32-unknown-unknown
run: rustup target add wasm32-unknown-unknown
- name: Setup cargo-make
uses: davidB/rust-cargo-make@v1
- name: Cargo generate-lockfile
run: cargo generate-lockfile
- uses: Swatinem/rust-cache@v2
- name: Run cargo check on all libraries
run: cargo make --profile=github-actions check

View File

@@ -1,75 +0,0 @@
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- 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:
[
integrations/actix,
integrations/axum,
integrations/viz,
integrations/utils,
leptos,
leptos_config,
leptos_dom,
leptos_hot_reload,
leptos_macro,
leptos_reactive,
leptos_server,
meta,
router,
server_fn,
server_fn/server_fn_macro_default,
server_fn_macro,
]
uses: ./.github/workflows/run-cargo-make-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"

34
.github/workflows/fmt.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: Format
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
jobs:
test:
name: Run rustfmt
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- nightly
os:
- ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt
- name: Run Rustfmt
run: cargo fmt -- --check

View File

@@ -1,4 +1,4 @@
name: Run Task
name: Run Example Task
on:
workflow_call:
@@ -27,9 +27,9 @@ jobs:
steps:
# Setup environment
- name: Install playwright browser dependencies
run: |
run: |
sudo apt-get update
sudo apt-get install libegl1 libvpx7 libevent-2.1-7 libopus0 libopengl0 libwoff1 libharfbuzz-icu0 libgstreamer-plugins-base1.0-0 libgstreamer-gl1.0-0 libhyphen0 libmanette-0.2-0 libgles2 gstreamer1.0-libav
sudo apt-get install libegl1 libvpx7 libevent-2.1-7 libopus0 libopengl0 libwoff1 libharfbuzz-icu0 libgstreamer-plugins-base1.0-0 libgstreamer-gl1.0-0 libhyphen0 libmanette-0.2-0 libgles2 gstreamer1.0-libav
- uses: actions/checkout@v3
@@ -84,7 +84,7 @@ jobs:
restore-keys: |
${{ runner.os }}-pnpm-store-
# Run Cargo Make Task
# Verify project
- name: ${{ inputs.cargo_make_task }}
run: |
if [ "${{ inputs.directory }}" = "INTERNAL" ]; then

46
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Test
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
jobs:
test:
name: Run tests ${{ matrix.os }} (using rustc ${{ matrix.rust }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust:
- nightly
os:
- ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt
- name: Add wasm32-unknown-unknown
run: rustup target add wasm32-unknown-unknown
- name: Setup cargo-make
uses: davidB/rust-cargo-make@v1
- name: Cargo generate-lockfile
run: cargo generate-lockfile
- uses: Swatinem/rust-cache@v2
- name: Run tests with all features
run: cargo make --profile=github-actions test

View File

@@ -41,7 +41,7 @@ jobs:
strategy:
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-cargo-make-task.yml
uses: ./.github/workflows/run-example-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "verify-flow"

View File

@@ -65,7 +65,7 @@ jobs:
strategy:
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
fail-fast: false
uses: ./.github/workflows/run-cargo-make-task.yml
uses: ./.github/workflows/run-example-task.yml
with:
directory: ${{ matrix.directory }}
cargo_make_task: "verify-flow"

View File

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

View File

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

View File

@@ -3,25 +3,122 @@
# cargo install --force cargo-make
############
[env]
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
[config]
# make tasks run at the workspace root
default_to_workspace = false
[tasks.check]
clear = true
dependencies = [
"check-all",
"check-wasm",
"check-all-release",
"check-wasm-release",
]
[tasks.check-all]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"
[tasks.check-wasm]
clear = true
dependencies = [{ name = "check-wasm", path = "leptos" }]
[tasks.check-all-release]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"
[tasks.check-wasm-release]
clear = true
dependencies = [{ name = "check-wasm-release", path = "leptos" }]
[tasks.check-examples]
clear = true
dependencies = [
{ name = "check", path = "examples/counter" },
{ name = "check", path = "examples/counter_isomorphic" },
{ name = "check", path = "examples/counters" },
{ name = "check", path = "examples/error_boundary" },
{ name = "check", path = "examples/errors_axum" },
{ name = "check", path = "examples/fetch" },
{ name = "check", path = "examples/hackernews" },
{ name = "check", path = "examples/hackernews_axum" },
{ name = "check", path = "examples/js-framework-benchmark" },
{ name = "check", path = "examples/leptos-tailwind-axum" },
{ name = "check", path = "examples/login_with_token_csr_only" },
{ name = "check", path = "examples/parent_child" },
{ name = "check", path = "examples/router" },
{ name = "check", path = "examples/session_auth_axum" },
{ name = "check", path = "examples/slots" },
{ name = "check", path = "examples/ssr_modes" },
{ name = "check", path = "examples/ssr_modes_axum" },
{ name = "check", path = "examples/tailwind" },
{ name = "check", path = "examples/tailwind_csr_trunk" },
{ name = "check", path = "examples/timer" },
{ name = "check", path = "examples/todo_app_sqlite" },
{ name = "check", path = "examples/todo_app_sqlite_axum" },
{ name = "check", path = "examples/todo_app_sqlite_viz" },
{ name = "check", path = "examples/todomvc" },
]
[tasks.check-stable]
workspace = false
clear = true
dependencies = [
{ name = "check", path = "examples/counter_without_macros" },
{ name = "check", path = "examples/counters_stable" },
]
[tasks.ci-examples]
workspace = false
[tasks.test]
clear = true
dependencies = [
"test-all",
"test-leptos_macro-example",
"doc-leptos_macro-example",
]
[tasks.test-all]
command = "cargo"
args = ["+nightly", "test-all-features"]
install_crate = "cargo-all-features"
[tasks.test-leptos_macro-example]
description = "Tests the leptos_macro/example to check if macro handles doc comments correctly"
command = "cargo"
args = ["+nightly", "test", "--doc"]
cwd = "leptos_macro/example"
install_crate = false
[tasks.doc-leptos_macro-example]
description = "Docs the leptos_macro/example to check if macro handles doc comments correctly"
command = "cargo"
args = ["+nightly", "doc"]
cwd = "leptos_macro/example"
install_crate = false
[tasks.test-examples]
description = "Run all unit and web tests for examples"
cwd = "examples"
command = "cargo"
args = ["make", "ci-clean"]
args = ["make", "test-unit-and-web"]
[tasks.verify-examples]
description = "Run all quality checks and tests for examples"
env = { CLEAN_AFTER_VERIFY = "true" }
cwd = "examples"
command = "cargo"
args = ["make", "verify-flow"]
[tasks.clean-examples]
workspace = false
description = "Clean all example projects"
cwd = "examples"
command = "cargo"
args = ["make", "clean"]
args = ["make", "clean-all"]
[env]
RUSTFLAGS = ""
LEPTOS_OUTPUT_NAME = "ci" # allows examples to check/build without cargo-leptos
[env.github-actions]
RUSTFLAGS = "-D warnings"

View File

@@ -1,7 +0,0 @@
[tasks.check]
alias = "check-all"
[tasks.check-all]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -1,9 +0,0 @@
[tasks.pre-clippy]
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
[tasks.check-style]
dependencies = ["check-format-flow", "clippy-flow"]
[tasks.check-format]
env = { LEPTOS_PROJECT_DIRECTORY = "../" }
args = ["fmt", "--", "--check", "--config-path", "${LEPTOS_PROJECT_DIRECTORY}"]

View File

@@ -1,18 +0,0 @@
extend = [
{ path = "./check.toml" },
{ path = "./lint.toml" },
{ path = "./test.toml" },
]
[env]
RUSTFLAGS = ""
LEPTOS_OUTPUT_NAME = "ci" # allows examples to check/build without cargo-leptos
[env.github-actions]
RUSTFLAGS = "-D warnings"
[tasks.ci]
dependencies = ["lint", "test"]
[tasks.lint]
dependencies = ["check-format-flow"]

View File

@@ -1,7 +0,0 @@
[tasks.test]
alias = "test-all"
[tasks.test-all]
command = "cargo"
args = ["+nightly", "test-all-features"]
install_crate = "cargo-all-features"

View File

@@ -1,2 +0,0 @@
[output.html.playground]
runnable = false

View File

@@ -14,40 +14,31 @@ If you dont already have it installed, you can install Trunk by running
cargo install trunk
```
Create a basic Rust project
Create a basic Rust binary project
```bash
cargo init leptos-tutorial
```
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
```bash
cargo add leptos --features=csr,nightly
```
Or you can leave off `nighly` if you're using stable Rust
```bash
cargo add leptos --features=csr
```
> Using `nightly` Rust, and the `nightly` feature in Leptos enables the function-call syntax for signal getters and setters that is used in most of this book.
>
> To use `nightly` Rust, you can run
> We recommend using `nightly` Rust, as it enables [a few nice features](https://github.com/leptos-rs/leptos#nightly-note). To use `nightly` Rust with WebAssembly, you can run
>
> ```bash
> rustup toolchain install nightly
> rustup default nightly
> ```
>
> If youd rather use stable Rust with Leptos, you can do that too. In the guide and examples, youll just use the [`ReadSignal::get()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#impl-SignalGet%3CT%3E-for-ReadSignal%3CT%3E) and [`WriteSignal::set()`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#impl-SignalGet%3CT%3E-for-ReadSignal%3CT%3E) methods instead of calling signal getters and setters as functions.
Make sure you've added the `wasm32-unknown-unknown` target so that Rust can compile your code to WebAssembly to run in the browser.
Make sure you've added the `wasm32-unknown-unknown` target do that Rust can compile your code to WebAssembly to run in the browser.
```bash
rustup target add wasm32-unknown-unknown
```
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
```bash
cargo add leptos --features=csr,nightly # or just csr if you're using stable Rust
```
Create a simple `index.html` in the root of the `leptos-tutorial` directory
```html

View File

@@ -0,0 +1 @@
# Responding to Changes with create_effect

View File

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

View File

@@ -23,6 +23,7 @@
- [Transition](./async/12_transition.md)
- [Actions](./async/13_actions.md)
- [Interlude: Projecting Children](./interlude_projecting_children.md)
- [Responding to Changes with `create_effect`](./14_create_effect.md)
- [Global State Management](./15_global_state.md)
- [Router](./router/README.md)
- [Defining `<Routes/>`](./router/16_routes.md)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -70,128 +70,10 @@ let id = move || {
This can get a little messy: deriving a signal that wraps an `Option<_>` or `Result<_>` can involve a couple steps. But its worth doing this for two reasons:
1. Its correct, i.e., it forces you to consider the cases, “What if the user doesnt pass a value for this query field? What if they pass an invalid value?”
2. Its performant. Specifically, when you navigate between different paths that match the same `<Route/>` with only params or the query changing, you can get fine-grained updates to different parts of your app without rerendering. For example, navigating between different contacts in our contact-list example does a targeted update to the name field (and eventually contact info) without needing to replace or rerender the wrapping `<Contact/>`. This is what fine-grained reactivity is for.
2. Its performant. Specifically, when you navigate between different paths that match the same `<Route/>` with only params or the query changing, you can get fine-grained updates to different parts of your app without rerendering. For example, navigating between different contacts in our contact-list example does a targeted update to the name field (and eventually contact info) without needing to replacing or rerender the wrapping `<Contact/>`. This is what fine-grained reactivity is for.
> This is the same example from the previous section. The router is such an integrated system that it makes sense to provide a single example highlighting multiple features, even if we havent explained them all yet.
> This is the same example from the previous section. The router is such an integrated system that it makes sense to provide a single example highlighting multiple features, even if we havent explain them all yet.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
use leptos_router::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
<Router>
<h1>"Contact App"</h1>
// this <nav> will show on every routes,
// because it's outside the <Routes/>
// note: we can just use normal <a> tags
// and the router will use client-side navigation
<nav>
<h2>"Navigation"</h2>
<a href="/">"Home"</a>
<a href="/contacts">"Contacts"</a>
</nav>
<main>
<Routes>
// / just has an un-nested "Home"
<Route path="/" view=|cx| view! { cx,
<h3>"Home"</h3>
}/>
// /contacts has nested routes
<Route
path="/contacts"
view=|cx| view! { cx, <ContactList/> }
>
// if no id specified, fall back
<Route path=":id" view=|cx| view! { cx,
<ContactInfo/>
}>
<Route path="" view=|cx| view! { cx,
<div class="tab">
"(Contact Info)"
</div>
}/>
<Route path="conversations" view=|cx| view! { cx,
<div class="tab">
"(Conversations)"
</div>
}/>
</Route>
// if no id specified, fall back
<Route path="" view=|cx| view! { cx,
<div class="select-user">
"Select a user to view contact info."
</div>
}/>
</Route>
</Routes>
</main>
</Router>
}
}
#[component]
fn ContactList(cx: Scope) -> impl IntoView {
view! { cx,
<div class="contact-list">
// here's our contact list component itself
<div class="contact-list-contacts">
<h3>"Contacts"</h3>
<A href="alice">"Alice"</A>
<A href="bob">"Bob"</A>
<A href="steve">"Steve"</A>
</div>
// <Outlet/> will show the nested child route
// we can position this outlet wherever we want
// within the layout
<Outlet/>
</div>
}
}
#[component]
fn ContactInfo(cx: Scope) -> impl IntoView {
// we can access the :id param reactively with `use_params_map`
let params = use_params_map(cx);
let id = move || params.with(|params| params.get("id").cloned().unwrap_or_default());
// imagine we're loading data from an API here
let name = move || match id().as_str() {
"alice" => "Alice",
"bob" => "Bob",
"steve" => "Steve",
_ => "User not found.",
};
view! { cx,
<div class="contact-info">
<h4>{name}</h4>
<div class="tabs">
<A href="" exact=true>"Contact Info"</A>
<A href="conversations">"Conversations"</A>
</div>
// <Outlet/> here is the tabs that are nested
// underneath the /contacts/:id route
<Outlet/>
</div>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

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

View File

@@ -1,12 +1,12 @@
# The `<Form/>` Component
Links and forms sometimes seem completely unrelated. But, in fact, they work in very similar ways.
Links and forms sometimes seem completely unrelated. But in fact, they work in very similar ways.
In plain HTML, there are three ways to navigate to another page:
1. An `<a>` element that links to another page: Navigates to the URL in its `href` attribute with the `GET` HTTP method.
2. A `<form method="GET">`: Navigates to the URL in its `action` attribute with the `GET` HTTP method and the form data from its inputs encoded in the URL query string.
3. A `<form method="POST">`: Navigates to the URL in its `action` attribute with the `POST` HTTP method and the form data from its inputs encoded in the body of the request.
1. An `<a>` element that links to another page. Navigates to the URL in its `href` attribute with the `GET` HTTP method.
2. A `<form method="GET">`. Navigates to the URL in its `action` attribute with the `GET` HTTP method and the form data from its inputs encoded in the URL query string.
3. A `<form method="POST">`. Navigates to the URL in its `action` attribute with the `POST` HTTP method and the form data from its inputs encoded in the body of the request.
Since we have a client-side router, we can do client-side link navigations without reloading the page, i.e., without a full round-trip to the server and back. It makes sense that we can do client-side form navigations in the same way.
@@ -65,117 +65,3 @@ Youll notice that this version drops the `Submit` button. Instead, we add an
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/16-router-forked-hrrt3h?file=%2Fsrc%2Fmain.rs)
<iframe src="https://codesandbox.io/p/sandbox/16-router-forked-hrrt3h?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
use leptos_router::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
view! { cx,
<Router>
<h1><code>"<Form/>"</code></h1>
<main>
<Routes>
<Route path="" view=|cx| view! { cx, <FormExample/> }/>
</Routes>
</main>
</Router>
}
}
#[component]
pub fn FormExample(cx: Scope) -> impl IntoView {
// reactive access to URL query
let query = use_query_map(cx);
let name = move || query().get("name").cloned().unwrap_or_default();
let number = move || query().get("number").cloned().unwrap_or_default();
let select = move || query().get("select").cloned().unwrap_or_default();
view! { cx,
// read out the URL query strings
<table>
<tr>
<td><code>"name"</code></td>
<td>{name}</td>
</tr>
<tr>
<td><code>"number"</code></td>
<td>{number}</td>
</tr>
<tr>
<td><code>"select"</code></td>
<td>{select}</td>
</tr>
</table>
// <Form/> will navigate whenever submitted
<h2>"Manual Submission"</h2>
<Form method="GET" action="">
// input names determine query string key
<input type="text" name="name" value=name/>
<input type="number" name="number" value=number/>
<select name="select">
// `selected` will set which starts as selected
<option selected=move || select() == "A">
"A"
</option>
<option selected=move || select() == "B">
"B"
</option>
<option selected=move || select() == "C">
"C"
</option>
</select>
// submitting should cause a client-side
// navigation, not a full reload
<input type="submit"/>
</Form>
// This <Form/> uses some JavaScript to submit
// on every input
<h2>"Automatic Submission"</h2>
<Form method="GET" action="">
<input
type="text"
name="name"
value=name
// this oninput attribute will cause the
// form to submit on every input to the field
oninput="this.form.requestSubmit()"
/>
<input
type="number"
name="number"
value=number
oninput="this.form.requestSubmit()"
/>
<select name="select"
onchange="this.form.requestSubmit()"
>
<option selected=move || select() == "A">
"A"
</option>
<option selected=move || select() == "B">
"B"
</option>
<option selected=move || select() == "C">
"C"
</option>
</select>
// submitting should cause a client-side
// navigation, not a full reload
<input type="submit"/>
</Form>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

@@ -74,11 +74,11 @@ In other words, if this is being compiled to WASM, it has three items; otherwise
When I load the page in the browser, I see nothing. If I open the console I see a bunch of warnings:
```
element with id 0-3 not found, ignoring it for hydration
element with id 0-4 not found, ignoring it for hydration
element with id 0-5 not found, ignoring it for hydration
component with id _0-6c not found, ignoring it for hydration
component with id _0-6o not found, ignoring it for hydration
element with id 0-0-1 not found, ignoring it for hydration
element with id 0-0-2 not found, ignoring it for hydration
element with id 0-0-3 not found, ignoring it for hydration
component with id _0-0-4c not found, ignoring it for hydration
component with id _0-0-4o not found, ignoring it for hydration
```
The WASM version of your app, running in the browser, expects to find three items; but the HTML has none.
@@ -87,56 +87,6 @@ The WASM version of your app, running in the browser, expects to find three item
Its pretty rare that you do this intentionally, but it could happen from somehow running different logic on the server and in the browser. If youre seeing warnings like this and you dont think its your fault, its much more likely that its a bug with `<Suspense/>` or something. Feel free to go ahead and open an [issue](https://github.com/leptos-rs/leptos/issues) or [discussion](https://github.com/leptos-rs/leptos/discussions) on GitHub for help.
### Mutating the DOM during rendering
This is a slightly more common way to create a client/server mismatch: updating a signal _during rendering_ in a way that mutates the view.
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let (loaded, set_loaded) = create_signal(cx, false);
// create_effect only runs on the client
create_effect(cx, move |_| {
// do something like reading from localStorage
set_loaded(true);
});
move || {
if loaded() {
view! { cx, <p>"Hello, world!"</p> }.into_any()
} else {
view! { cx, <div class="loading">"Loading..."</div> }.into_any()
}
}
}
```
This one gives us the scary panic
```
panicked at 'assertion failed: `(left == right)`
left: `"DIV"`,
right: `"P"`: SSR and CSR elements have the same hydration key but different node kinds.
```
And a handy link to this page!
The problem here is that `create_effect` runs **immediately** and **synchronously**, but only in the browser. As a result, on the server, `loaded` is false, and a `<div>` is rendered. But on the browser, by the time the view is being rendered, `loaded` has already been set to `true`, and the browser is expecting to find a `<p>`.
#### Solution
You can simply tell the effect to wait a tick before updating the signal, by using something like `request_animation_frame`, which will set a short timeout and then update the signal before the next frame.
```rust
create_effect(cx, move |_| {
// do something like reading from localStorage
request_animation_frame(move || set_loaded(true));
});
```
This allows the browser to hydrate with the correct, matching state (`loaded` is `false` when it reaches the view), then immediately update it to `true` once hydration is complete.
### Not all client code can run on the server
Imagine you happily import a dependency like `gloo-net` that youve been used to using to make requests in the browser, and use it in a `create_resource` in a server-rendered app.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -113,64 +113,3 @@ an `<ErrorBoundary/>` will appear again.
[Click to open CodeSandbox.](https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D)
<iframe src="https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px" style="max-height: 100vh"></iframe>
<details>
<summary>CodeSandbox Source</summary>
```rust
use leptos::*;
#[component]
fn App(cx: Scope) -> impl IntoView {
let (value, set_value) = create_signal(cx, Ok(0));
// when input changes, try to parse a number from the input
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
view! { cx,
<h1>"Error Handling"</h1>
<label>
"Type a number (or something that's not a number!)"
<input type="number" on:input=on_input/>
// If an `Err(_) had been rendered inside the <ErrorBoundary/>,
// the fallback will be displayed. Otherwise, the children of the
// <ErrorBoundary/> will be displayed.
<ErrorBoundary
// the fallback receives a signal containing current errors
fallback=|cx, errors| view! { cx,
<div class="error">
<p>"Not a number! Errors: "</p>
// we can render a list of errors
// as strings, if we'd like
<ul>
{move || errors.get()
.into_iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
.collect::<Vec<_>>()
}
</ul>
</div>
}
>
<p>
"You entered "
// because `value` is `Result<i32, _>`,
// it will render the `i32` if it is `Ok`,
// and render nothing and trigger the error boundary
// if it is `Err`. It's a signal, so this will dynamically
// update when `value` changes
<strong>{value}</strong>
</p>
</ErrorBoundary>
</label>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}
```
</details>
</preview>

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
[tasks.integration-test]
dependencies = ["cargo-leptos-e2e"]
[tasks.test-e2e]
dependencies = ["setup-node", "cargo-leptos-e2e"]
[tasks.cargo-leptos-e2e]
command = "cargo"
args = ["leptos", "end-to-end"]
[tasks.clean-all]
dependencies = ["clean-cargo", "clean-node_modules", "clean-playwright"]

View File

@@ -1,28 +0,0 @@
[tasks.clean]
dependencies = [
"clean-cargo",
"clean-trunk",
"clean-node_modules",
"clean-playwright",
]
[tasks.clean-cargo]
command = "cargo"
args = ["clean"]
[tasks.clean-trunk]
command = "trunk"
args = ["clean"]
[tasks.clean-node_modules]
script = '''
project_dir=${PWD##*/}
if [ "$project_dir" != "todomvc" ]; then
find . -type d -name node_modules | xargs rm -rf
fi
'''
[tasks.clean-playwright]
script = '''
find . -name playwright-report -name playwright -name test-results | xargs rm -rf
'''

View File

@@ -0,0 +1,98 @@
[tasks.pre-clippy]
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
[tasks.check-style]
description = "Check for style violations"
dependencies = ["check-format-flow", "clippy-flow"]
[tasks.check-format]
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }
args = ["fmt", "--", "--check", "--config-path", "${LEPTOS_PROJECT_DIRECTORY}"]
[tasks.clean-cargo]
description = "Runs the cargo clean command."
category = "Cleanup"
command = "cargo"
args = ["clean"]
[tasks.clean-trunk]
description = "Runs the trunk clean command."
category = "Cleanup"
command = "trunk"
args = ["clean"]
[tasks.clean-node_modules]
description = "Delete all node_modules directories"
category = "Cleanup"
script = '''
find . -type d -name node_modules | xargs rm -rf
'''
[tasks.clean-playwright]
description = "Delete playwright directories"
category = "Cleanup"
script = '''
for pw_dir in $(find . -name playwright.config.ts | xargs dirname)
do
rm -rf $pw_dir/playwright-report pw_dir/playwright pw_dir/test-results
done
'''
[tasks.clean-all]
description = "Delete all temporary directories"
category = "Cleanup"
dependencies = ["clean-cargo"]
[tasks.test-wasm]
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
command = "cargo"
args = ["make", "wasm-pack-test"]
[tasks.cargo-leptos-e2e]
description = "Runs end to end tests with cargo leptos"
command = "cargo"
args = ["leptos", "end-to-end"]
[tasks.setup-node]
description = "Install node dependencies and playwright browsers"
env = { PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1" }
script = '''
BOLD="\e[1m"
GREEN="\e[0;32m"
RED="\e[0;31m"
RESET="\e[0m"
project_dir=$CARGO_MAKE_WORKING_DIRECTORY
# Discover commands
if command -v pnpm; then
NODE_CMD=pnpm
PLAYWRIGHT_CMD=pnpm
elif command -v npm; then
NODE_CMD=npm
PLAYWRIGHT_CMD=npx
else
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
exit 1
fi
# Install node dependencies
for node_path in $(find . -name package.json -not -path '*/node_modules/*')
do
node_dir=$(dirname $node_path)
echo Install node dependencies for $node_dir
cd $node_dir
${NODE_CMD} install
cd ${project_dir}
done
# Install playwright browsers
for pw_path in $(find . -name playwright.config.ts)
do
pw_dir=$(dirname $pw_path)
echo Install playwright browsers for $pw_dir
cd $pw_dir
${PLAYWRIGHT_CMD} playwright install
cd $project_dir
done
'''

View File

@@ -1,9 +0,0 @@
[tasks.pre-clippy]
env = { CARGO_MAKE_CLIPPY_ARGS = "--all-targets --all-features -- -D warnings" }
[tasks.check-style]
dependencies = ["check-format-flow", "clippy-flow"]
[tasks.check-format]
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }
args = ["fmt", "--", "--check", "--config-path", "${LEPTOS_PROJECT_DIRECTORY}"]

View File

@@ -1,32 +1,35 @@
extend = [
{ path = "../cargo-make/clean.toml" },
{ path = "../cargo-make/lint.toml" },
{ path = "../cargo-make/node.toml" },
]
# CI Stages
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.ci]
dependencies = ["prepare", "lint", "build", "test-flow", "integration-test"]
[tasks.ci-clean]
dependencies = ["ci", "clean"]
[tasks.prepare]
dependencies = ["setup-node"]
[tasks.lint]
dependencies = ["check-style"]
[tasks.integration-test]
# ALIASES
alias = "verify-flow"
[tasks.verify-flow]
alias = "ci"
description = "Provides pre and post hooks for verify"
dependencies = ["pre-verify", "verify", "post-verify"]
[tasks.t]
dependencies = ["test-flow"]
[tasks.verify]
description = "Run all quality checks and tests"
dependencies = ["check-style", "test-unit-and-e2e"]
[tasks.it]
alias = "integration-test"
[tasks.test-unit-and-e2e]
description = "Run all unit and e2e tests"
dependencies = ["test-flow", "test-e2e-flow"]
[tasks.pre-verify]
[tasks.post-verify]
dependencies = ["maybe-clean-all"]
[tasks.maybe-clean-all]
description = "Used to clean up locally after call to verify-examples"
condition = { env_true = ["CLEAN_AFTER_VERIFY"] }
[tasks.test-e2e-flow]
description = "Provides pre and post hooks for test-e2e"
dependencies = ["pre-test-e2e", "test-e2e", "post-test-e2e"]
[tasks.pre-test-e2e]
[tasks.test-e2e]
[tasks.post-test-e2e]

View File

@@ -1,43 +0,0 @@
[tasks.setup-node]
description = "Install node dependencies and playwright browsers"
env = { PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1" }
script = '''
BOLD="\e[1m"
GREEN="\e[0;32m"
RED="\e[0;31m"
RESET="\e[0m"
project_dir=$CARGO_MAKE_WORKING_DIRECTORY
# Discover commands
if command -v pnpm; then
NODE_CMD=pnpm
PLAYWRIGHT_CMD=pnpm
elif command -v npm; then
NODE_CMD=npm
PLAYWRIGHT_CMD=npx
else
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
exit 1
fi
# Install node dependencies
for node_path in $(find . -name package.json -not -path '*/node_modules/*')
do
node_dir=$(dirname $node_path)
echo Install node dependencies for $node_dir
cd $node_dir
${NODE_CMD} install
cd ${project_dir}
done
# Install playwright browsers
for pw_path in $(find . -name playwright.config.ts)
do
pw_dir=$(dirname $pw_path)
echo Install playwright browsers for $pw_dir
cd $pw_dir
${PLAYWRIGHT_CMD} playwright install
cd $project_dir
done
'''

View File

@@ -1,4 +1,7 @@
extend = [{ path = "../cargo-make/playwright.toml" }]
[tasks.integration-test]
dependencies = ["test-playwright-autostart"]
[tasks.test-e2e]
dependencies = ["setup-node", "test-playwright-autostart"]
[tasks.clean-all]
dependencies = ["clean-cargo", "clean-node_modules", "clean-playwright"]

View File

@@ -1,8 +1,22 @@
[tasks.clean-playwright]
description = "Delete playwright directories"
category = "Cleanup"
script = '''
for pw_dir in $(find . -name playwright.config.ts | xargs dirname)
do
rm -rf $pw_dir/playwright-report pw_dir/playwright pw_dir/test-results
done
'''
[tasks.test-playwright-autostart]
description = "Run playwright test with server autostart"
category = "Test"
command = "npm"
args = ["run", "e2e:auto-start"]
[tasks.test-playwright]
description = "Run playwright test"
category = "Test"
script = '''
BOLD="\e[1m"
GREEN="\e[0;32m"
@@ -32,6 +46,8 @@ done
'''
[tasks.test-playwright-ui]
description = "Run playwright test --ui"
category = "Test"
script = '''
BOLD="\e[1m"
GREEN="\e[0;32m"
@@ -61,6 +77,8 @@ done
'''
[tasks.test-playwright-report]
description = "Run playwright show-report"
category = "Test"
script = '''
BOLD="\e[1m"
GREEN="\e[0;32m"

View File

@@ -2,9 +2,13 @@
command = "trunk"
args = ["build"]
[tasks.clean-trunk]
command = "trunk"
args = ["clean"]
[tasks.start-trunk]
command = "trunk"
args = ["serve", "${@}"]
args = ["serve", "--open"]
[tasks.stop-trunk]
script = '''

View File

@@ -5,7 +5,5 @@ condition = { env_true = ["RUN_CARGO_TEST"] }
[tasks.post-test]
dependencies = ["test-wasm"]
[tasks.test-wasm]
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
command = "cargo"
args = ["make", "wasm-pack-test"]
[tasks.clean-all]
dependencies = ["clean-cargo", "clean-trunk"]

View File

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

View File

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

View File

@@ -4,14 +4,6 @@ use leptos_meta::*;
use leptos_router::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct Todo {
id: u16,
title: String,
completed: bool,
}
cfg_if! {
if #[cfg(feature = "ssr")] {
use sqlx::{Connection, SqliteConnection};
@@ -19,6 +11,20 @@ cfg_if! {
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
pub struct Todo {
id: u16,
title: String,
completed: bool,
}
} else {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Todo {
id: u16,
title: String,
completed: bool,
}
}
}

View File

@@ -5,14 +5,6 @@ use leptos_meta::*;
use leptos_router::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct Todo {
id: u16,
title: String,
completed: bool,
}
cfg_if! {
if #[cfg(feature = "ssr")] {
use sqlx::{Connection, SqliteConnection};
@@ -21,6 +13,20 @@ cfg_if! {
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
pub struct Todo {
id: u16,
title: String,
completed: bool,
}
} else {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Todo {
id: u16,
title: String,
completed: bool,
}
}
}

View File

@@ -5,14 +5,6 @@ use leptos_meta::*;
use leptos_router::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
pub struct Todo {
id: u16,
title: String,
completed: bool,
}
cfg_if! {
if #[cfg(feature = "ssr")] {
use sqlx::{Connection, SqliteConnection};
@@ -21,6 +13,20 @@ cfg_if! {
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
pub struct Todo {
id: u16,
title: String,
completed: bool,
}
} else {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Todo {
id: u16,
title: String,
completed: bool,
}
}
}

View File

@@ -19,6 +19,3 @@ serde_json = "1"
parking_lot = "0.12.1"
regex = "1.7.0"
tracing = "0.1.37"
[features]
nonce = ["leptos/nonce"]

View File

@@ -1,4 +0,0 @@
extend = { path = "../../cargo-make/main.toml" }
[tasks.check-format]
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }

View File

@@ -223,30 +223,26 @@ pub fn handle_server_fns_with_context(
let res_options =
use_context::<ResponseOptions>(cx).unwrap();
let mut res: HttpResponseBuilder =
HttpResponse::Ok();
let mut res: HttpResponseBuilder;
let res_parts = res_options.0.write();
// if accept_header isn't set to one of these, it's a form submit
// redirect back to the referrer if not redirect has been set
if accept_header != Some("application/json")
&& accept_header
!= Some("application/x-www-form-urlencoded")
&& accept_header != Some("application/cbor")
if accept_header == Some("application/json")
|| accept_header
== Some("application/x-www-form-urlencoded")
|| accept_header == Some("application/cbor")
{
// Location will already be set if redirect() has been used
let has_location_set =
res_parts.headers.get("Location").is_some();
if !has_location_set {
let referer = req
.headers()
.get("Referer")
.and_then(|value| value.to_str().ok())
.unwrap_or("/");
res = HttpResponse::SeeOther();
res.insert_header(("Location", referer))
.content_type("application/json");
}
res = HttpResponse::Ok();
}
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
else {
let referer = req
.headers()
.get("Referer")
.and_then(|value| value.to_str().ok())
.unwrap_or("/");
res = HttpResponse::SeeOther();
res.insert_header(("Location", referer))
.content_type("application/json");
};
// Override StatusCode if it was set in a Resource or Element
if let Some(status) = res_parts.status {
@@ -292,12 +288,7 @@ pub fn handle_server_fns_with_context(
} else {
HttpResponse::BadRequest().body(format!(
"Could not find a server function at the route {:?}. \
\n\nIt's likely that either
1. The API prefix you specify in the `#[server]` \
macro doesn't match the prefix at which your server \
function handler is mounted, or \n2. You are on a \
platform that doesn't support automatic server \
function registration and you need to call \
\n\nIt's likely that you need to call \
ServerFn::register_explicit() on the server function \
type, somewhere in your `main` function.",
req.path()
@@ -733,8 +724,6 @@ fn provide_contexts(
provide_context(cx, res_options);
provide_context(cx, req.clone());
provide_server_redirect(cx, move |path| redirect(cx, path));
#[cfg(feature = "nonce")]
leptos::nonce::provide_nonce(cx);
}
fn leptos_corrected_path(req: &HttpRequest) -> String {
@@ -799,11 +788,8 @@ async fn build_stream_response(
// wait for any blocking resources to load before pulling metadata
let first_app_chunk = stream.next().await.unwrap_or_default();
let (head, tail) = html_parts_separated(
cx,
options,
use_context::<MetaContext>(cx).as_ref(),
);
let (head, tail) =
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
let mut stream = Box::pin(
futures::stream::once(async move { head.clone() })

View File

@@ -19,9 +19,6 @@ leptos_integration_utils = { workspace = true }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
parking_lot = "0.12.1"
tokio-util = { version = "0.7.7", features = ["rt"] }
tokio-util = {version = "0.7.7", features = ["rt"] }
tracing = "0.1.37"
once_cell = "1.17"
[features]
nonce = ["leptos/nonce"]

View File

@@ -1,4 +0,0 @@
extend = { path = "../../cargo-make/main.toml" }
[tasks.check-format]
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }

View File

@@ -335,14 +335,9 @@ async fn handle_server_fns_inner(
Response::builder().status(StatusCode::BAD_REQUEST).body(
Full::from(format!(
"Could not find a server function at the route \
{fn_name}. \n\nIt's likely that either
1. The API prefix you specify in the `#[server]` \
macro doesn't match the prefix at which your server \
function handler is mounted, or \n2. You are on a \
platform that doesn't support automatic server \
function registration and you need to call \
{fn_name}. \n\nIt's likely that you need to call \
ServerFn::register_explicit() on the server function \
type, somewhere in your `main` function.",
type, somewhere in your `main` function."
)),
)
}
@@ -605,8 +600,7 @@ where
let res_options3 = default_res_options.clone();
let local_pool = get_leptos_pool();
let (tx, rx) = futures::channel::mpsc::channel(8);
let (complete_tx, complete_rx) =
futures::channel::oneshot::channel();
let (runtime_tx, runtime_rx) = futures::channel::oneshot::channel();
let current_span = tracing::Span::current();
local_pool.spawn_pinned(move || async move {
@@ -631,16 +625,17 @@ 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));
generate_response(res_options3, rx, complete_tx)
async move {
let runtime = runtime_rx
.await
.expect("runtime should be sent by renderer");
generate_response(res_options3, rx, runtime).await
}
})
}
}
@@ -649,7 +644,7 @@ where
async fn generate_response(
res_options: ResponseOptions,
rx: Receiver<String>,
complete_tx: futures::channel::oneshot::Sender<()>,
runtime: RuntimeId,
) -> Response<StreamBody<PinnedHtmlStream>> {
let mut stream = Box::pin(rx.map(|html| Ok(Bytes::from(html))));
@@ -664,10 +659,8 @@ async fn generate_response(
futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()])
.chain(stream)
.chain(futures::stream::once(async move {
complete_tx
.send(())
.expect("could not send completion message");
Ok(Bytes::from(String::new()))
runtime.dispose();
Ok(Default::default())
}));
let mut res = Response::new(StreamBody::new(
@@ -695,11 +688,8 @@ async fn forward_stream(
let mut shell = Box::pin(bundle);
let first_app_chunk = shell.next().await.unwrap_or_default();
let (head, tail) = html_parts_separated(
cx,
options,
use_context::<MetaContext>(cx).as_ref(),
);
let (head, tail) =
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
_ = tx.send(head).await;
_ = tx.send(first_app_chunk).await;
@@ -783,7 +773,7 @@ where
let full_path = format!("http://leptos.dev{path}");
let (tx, rx) = futures::channel::mpsc::channel(8);
let (complete_tx, complete_rx) =
let (runtime_tx, runtime_rx) =
futures::channel::oneshot::channel();
let local_pool = get_leptos_pool();
let current_span = tracing::Span::current();
@@ -804,16 +794,15 @@ 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));
generate_response(res_options3, rx, complete_tx).await
let runtime = runtime_rx
.await
.expect("runtime should be sent by renderer");
generate_response(res_options3, rx, runtime).await
}
})
}
@@ -834,8 +823,6 @@ fn provide_contexts(
provide_context(cx, extractor);
provide_context(cx, default_res_options);
provide_server_redirect(cx, move |path| redirect(cx, path));
#[cfg(feature = "nonce")]
leptos::nonce::provide_nonce(cx);
}
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries

View File

@@ -1,4 +0,0 @@
extend = { path = "../../cargo-make/main.toml" }
[tasks.check-format]
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }

View File

@@ -1,18 +1,18 @@
use futures::{Stream, StreamExt};
use leptos::{nonce::use_nonce, use_context, RuntimeId, Scope, ScopeId};
use leptos::{use_context, RuntimeId, ScopeId};
use leptos_config::LeptosOptions;
use leptos_meta::MetaContext;
extern crate tracing;
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn autoreload(nonce_str: &str, options: &LeptosOptions) -> String {
fn autoreload(options: &LeptosOptions) -> String {
let site_ip = &options.site_addr.ip().to_string();
let reload_port = options.reload_port;
match std::env::var("LEPTOS_WATCH").is_ok() {
true => format!(
r#"
<script crossorigin=""{nonce_str}>(function () {{
<script crossorigin="">(function () {{
{}
var ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
ws.onmessage = (ev) => {{
@@ -42,8 +42,6 @@ fn autoreload(nonce_str: &str, options: &LeptosOptions) -> String {
false => "".to_string(),
}
}
#[deprecated = "Use html_parts_separated."]
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn html_parts(
options: &LeptosOptions,
@@ -60,7 +58,7 @@ pub fn html_parts(
wasm_output_name.push_str("_bg");
}
let leptos_autoreload = autoreload("".into(), options);
let leptos_autoreload = autoreload(options);
let html_metadata =
meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
@@ -82,27 +80,21 @@ pub fn html_parts(
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn html_parts_separated(
cx: Scope,
options: &LeptosOptions,
meta: Option<&MetaContext>,
) -> (String, &'static str) {
let pkg_path = &options.site_pkg_dir;
let output_name = &options.output_name;
let nonce = use_nonce(cx);
let nonce = nonce
.as_ref()
.map(|nonce| format!(" nonce=\"{nonce}\""))
.unwrap_or_default();
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME at compile time
// Otherwise we need to add _bg because wasm_pack always does.
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
let mut wasm_output_name = output_name.clone();
if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() {
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
wasm_output_name.push_str("_bg");
}
let leptos_autoreload = autoreload(&nonce, options);
let leptos_autoreload = autoreload(options);
let html_metadata =
meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
@@ -117,9 +109,9 @@ pub fn html_parts_separated(
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
{head}
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js"{nonce}>
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin=""{nonce}>
<script type="module"{nonce}>import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js">
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
{leptos_autoreload}
"#
);
@@ -141,14 +133,15 @@ pub async fn build_async_response(
}
let cx = leptos::Scope { runtime, id: scope };
let (head, tail) = html_parts_separated(
cx,
options,
use_context::<MetaContext>(cx).as_ref(),
);
let (head, tail) =
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
// in async, we load the meta content *now*, after the suspenses have resolved
let meta = use_context::<MetaContext>(cx);
let head_meta = meta
.as_ref()
.map(|meta| meta.dehydrate())
.unwrap_or_default();
let body_meta = meta
.as_ref()
.and_then(|meta| meta.body.as_string())
@@ -156,5 +149,5 @@ pub async fn build_async_response(
runtime.dispose();
format!("{head}</head><body{body_meta}>{buf}{tail}")
format!("{head}{head_meta}</head><body{body_meta}>{buf}{tail}")
}

View File

@@ -19,6 +19,3 @@ leptos_integration_utils = { workspace = true }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
parking_lot = "0.12.1"
[features]
nonce = ["leptos/nonce"]

View File

@@ -1,4 +0,0 @@
extend = { path = "../../cargo-make/main.toml" }
[tasks.check-format]
env = { LEPTOS_PROJECT_DIRECTORY = "../../" }

View File

@@ -312,17 +312,10 @@ async fn handle_server_fns_inner(
.body(Body::from(format!(
"Could not find a server function at the \
route {fn_name}. \n\nIt's likely that \
either
1. The API prefix you specify in the \
`#[server]` macro doesn't match the \
prefix at which your server function \
handler is mounted, or \n2. You are on a \
platform that doesn't support automatic \
server function registration and you \
need to call \
you need to call \
ServerFn::register_explicit() on the \
server function type, somewhere in your \
`main` function.",
`main` function."
)))
}
.expect("could not build Response");
@@ -655,11 +648,8 @@ async fn forward_stream(
mut tx: Sender<String>,
) {
let cx = Scope { runtime, id: scope };
let (head, tail) = html_parts_separated(
cx,
options,
use_context::<MetaContext>(cx).as_ref(),
);
let (head, tail) =
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
_ = tx.send(head).await;
let mut shell = Box::pin(bundle);
@@ -796,8 +786,6 @@ fn provide_contexts(
provide_context(cx, req_parts);
provide_context(cx, default_res_options);
provide_server_redirect(cx, move |path| redirect(cx, path));
#[cfg(feature = "nonce")]
leptos::nonce::provide_nonce(cx);
}
/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries

View File

@@ -13,28 +13,28 @@ cfg-if = "1"
leptos_dom = { workspace = true }
leptos_macro = { workspace = true }
leptos_reactive = { workspace = true }
leptos_server = { workspace = true }
leptos_server = { workspace = true}
leptos_config = { workspace = true }
tracing = "0.1"
typed-builder = "0.14"
server_fn = { workspace = true }
server_fn = { workspace = true}
web-sys = { version = "0.3.63", optional = true }
wasm-bindgen = { version = "0.2", optional = true }
[dev-dependencies]
leptos = { path = "." }
leptos = { path = "."}
[features]
default = ["serde"]
template_macro = ["leptos_dom/web", "web-sys", "wasm-bindgen"]
csr = [
"leptos_dom/csr",
"leptos_dom/web",
"leptos_macro/csr",
"leptos_reactive/csr",
"leptos_server/csr",
]
hydrate = [
"leptos_dom/hydrate",
"leptos_dom/web",
"leptos_macro/hydrate",
"leptos_reactive/hydrate",
"leptos_server/hydrate",
@@ -58,18 +58,9 @@ serde-lite = ["leptos_reactive/serde-lite"]
miniserde = ["leptos_reactive/miniserde"]
rkyv = ["leptos_reactive/rkyv"]
tracing = ["leptos_macro/tracing"]
nonce = ["leptos_dom/nonce"]
[package.metadata.cargo-all-features]
denylist = [
"nightly",
"tracing",
"template_macro",
"rustls",
"default-tls",
"web-sys",
"wasm-bindgen",
]
denylist = ["nightly", "tracing", "template_macro", "rustls", "default-tls", "web-sys", "wasm-bindgen"]
skip_feature_sets = [
[
"csr",

View File

@@ -1,14 +1,3 @@
extend = { path = "../cargo-make/main.toml" }
[tasks.check]
clear = true
dependencies = [
"check-all",
"check-wasm",
"check-release",
"check-wasm-release",
]
[tasks.check-wasm]
clear = true
dependencies = ["check-hydrate", "check-csr"]
@@ -54,7 +43,3 @@ args = [
"--features=csr",
"--target=wasm32-unknown-unknown",
]
[tasks.check-release]
command = "cargo"
args = ["check", "--release"]

View File

@@ -58,16 +58,13 @@ where
F: Fn(Scope, RwSignal<Errors>) -> IV + 'static,
IV: IntoView,
{
let before_children = HydrationCtx::next_component();
_ = HydrationCtx::next_component();
let errors: RwSignal<Errors> = create_rw_signal(cx, Errors::default());
provide_context(cx, errors);
// Run children so that they render and execute resources
_ = HydrationCtx::next_component();
let children = children(cx);
HydrationCtx::continue_from(before_children);
#[cfg(all(debug_assertions, feature = "hydrate"))]
{

View File

@@ -84,18 +84,11 @@
//! from the server to the client.
//! - `serde-lite` In SSR/hydrate mode, uses [serde-lite](https://docs.rs/serde-lite/latest/serde_lite/) to serialize resources and send them
//! from the server to the client.
//! - `rkyv` In SSR/hydrate mode, uses [rkyv](https://docs.rs/rkyv/latest/rkyv/) to serialize resources and send them
//! from the server to the client.
//! - `miniserde` In SSR/hydrate mode, uses [miniserde](https://docs.rs/miniserde/latest/miniserde/) to serialize resources and send them
//! from the server to the client.
//! - `tracing` Adds additional support for [`tracing`](https://docs.rs/tracing/latest/tracing/) to components.
//! - `default-tls` Use default native TLS support. (Only applies when using server functions with a non-WASM client like a desktop app.)
//! - `rustls` Use `rustls`. (Only applies when using server functions with a non-WASM client like a desktop app.)
//! - `template_macro` Enables the [`template!`](leptos_macro::template) macro, which offers faster DOM node creation for some use cases in `csr`.
//!
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
//! which mode your app is operating in. You should only enable one of these per build target,
//! i.e., you should not have both `hydrate` and `ssr` enabled for your server binary, only `ssr`.
//! which mode your app is operating in.
//!
//! # A Simple Counter
//!
@@ -164,10 +157,9 @@ pub use leptos_dom::{
set_interval_with_handle, set_timeout, set_timeout_with_handle,
window_event_listener, window_event_listener_untyped,
},
html, log, math, mount_to, mount_to_body, nonce, svg, warn, window,
Attribute, Class, CollectView, Errors, Fragment, HtmlElement,
IntoAttribute, IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef,
Property, View,
html, log, math, mount_to, mount_to_body, svg, warn, window, Attribute,
Class, CollectView, Errors, Fragment, HtmlElement, IntoAttribute,
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
};
/// Types to make it easier to handle errors in your application.

View File

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

View File

@@ -289,4 +289,4 @@ fn None(cx: Scope) -> impl IntoView {
</div>
</div>
}
}
}

View File

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

View File

@@ -14,7 +14,6 @@ use typed_builder::TypedBuilder;
/// A Struct to allow us to parse LeptosOptions from the file. Not really needed, most interactions should
/// occur with LeptosOptions
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ConfFile {
pub leptos_options: LeptosOptions,
}
@@ -24,37 +23,31 @@ pub struct ConfFile {
/// correct path for WASM, JS, and Websockets, as well as other configuration tasks.
/// It shares keys with cargo-leptos, to allow for easy interoperability
#[derive(TypedBuilder, Debug, Clone, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct LeptosOptions {
/// The name of the WASM and JS files generated by wasm-bindgen. Defaults to the crate name with underscores instead of dashes
#[builder(setter(into))]
pub output_name: String,
/// The path of the all the files generated by cargo-leptos. This defaults to '.' for convenience when integrating with other
/// tools.
#[builder(setter(into), default=default_site_root())]
#[serde(default = "default_site_root")]
#[builder(setter(into), default=".".to_string())]
pub site_root: String,
/// The path of the WASM and JS files generated by wasm-bindgen from the root of your app
/// By default, wasm-bindgen puts them in `pkg`.
#[builder(setter(into), default=default_site_pkg_dir())]
#[serde(default = "default_site_pkg_dir")]
#[builder(setter(into), default="pkg".to_string())]
pub site_pkg_dir: String,
/// Used to configure the running environment of Leptos. Can be used to load dev constants and keys v prod, or change
/// things based on the deployment environment
/// I recommend passing in the result of `env::var("LEPTOS_ENV")`
#[builder(setter(into), default=default_env())]
#[serde(default = "default_env")]
#[builder(setter(into), default=Env::DEV)]
pub env: Env,
/// Provides a way to control the address leptos is served from.
/// Using an env variable here would allow you to run the same code in dev and prod
/// Defaults to `127.0.0.1:3000`
#[builder(setter(into), default=default_site_addr())]
#[serde(default = "default_site_addr")]
#[builder(setter(into), default=SocketAddr::from(([127,0,0,1], 3000)))]
pub site_addr: SocketAddr,
/// The port the Websocket watcher listens on. Should match the `reload_port` in cargo-leptos(if using).
/// Defaults to `3001`
#[builder(default = default_reload_port())]
#[serde(default = "default_reload_port")]
#[builder(default = 3001)]
pub reload_port: u32,
}
@@ -88,26 +81,6 @@ impl LeptosOptions {
}
}
fn default_site_root() -> String {
".".to_string()
}
fn default_site_pkg_dir() -> String {
"pkg".to_string()
}
fn default_env() -> Env {
Env::DEV
}
fn default_site_addr() -> SocketAddr {
SocketAddr::from(([127, 0, 0, 1], 3000))
}
fn default_reload_port() -> u32 {
3001
}
fn env_w_default(
key: &str,
default: &str,
@@ -122,7 +95,7 @@ fn env_w_default(
/// An enum that can be used to define the environment Leptos is running in.
/// Setting this to the `PROD` variant will not include the WebSocket code for `cargo-leptos` watch mode.
/// Defaults to `DEV`.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, serde::Deserialize)]
pub enum Env {
PROD,
DEV,
@@ -202,7 +175,9 @@ pub fn get_config_from_str(text: &str) -> Result<ConfFile, LeptosConfigError> {
// so that serde error messages have right line number
let newlines = text[..start].matches('\n').count();
let input = "\n".repeat(newlines) + &text[start..];
let toml = input.replace(metadata_name, "[leptos-options]");
let toml = input
.replace(metadata_name, "[leptos_options]")
.replace('-', "_");
let settings = Config::builder()
// Read the "default" configuration file
.add_source(File::from_str(&toml, FileFormat::Toml))

View File

@@ -46,7 +46,7 @@ async fn get_configuration_from_file_ok() {
.unwrap()
.leptos_options;
assert_eq!(config.output_name, "app-test");
assert_eq!(config.output_name, "app_test");
assert_eq!(config.site_root, "my_target/site");
assert_eq!(config.site_pkg_dir, "my_pkg");
assert_eq!(
@@ -93,7 +93,7 @@ async fn get_config_from_file_ok() {
.unwrap()
.leptos_options;
assert_eq!(config.output_name, "app-test");
assert_eq!(config.output_name, "app_test");
assert_eq!(config.site_root, "my_target/site");
assert_eq!(config.site_pkg_dir, "my_pkg");
assert_eq!(
@@ -128,7 +128,7 @@ fn get_config_from_str_content() {
let config = get_config_from_str(CARGO_TOML_CONTENT_OK)
.unwrap()
.leptos_options;
assert_eq!(config.output_name, "app-test");
assert_eq!(config.output_name, "app_test");
assert_eq!(config.site_root, "my_target/site");
assert_eq!(config.site_pkg_dir, "my_pkg");
assert_eq!(
@@ -141,14 +141,14 @@ fn get_config_from_str_content() {
#[tokio::test]
async fn get_config_from_env() {
// Test config values from environment variables
std::env::set_var("LEPTOS_OUTPUT_NAME", "app-test");
std::env::set_var("LEPTOS_OUTPUT_NAME", "app_test");
std::env::set_var("LEPTOS_SITE_ROOT", "my_target/site");
std::env::set_var("LEPTOS_SITE_PKG_DIR", "my_pkg");
std::env::set_var("LEPTOS_SITE_ADDR", "0.0.0.0:80");
std::env::set_var("LEPTOS_RELOAD_PORT", "8080");
let config = get_configuration(None).await.unwrap().leptos_options;
assert_eq!(config.output_name, "app-test");
assert_eq!(config.output_name, "app_test");
assert_eq!(config.site_root, "my_target/site");
assert_eq!(config.site_pkg_dir, "my_pkg");
@@ -176,8 +176,8 @@ async fn get_config_from_env() {
#[test]
fn leptos_options_builder_default() {
let conf = LeptosOptions::builder().output_name("app-test").build();
assert_eq!(conf.output_name, "app-test");
let conf = LeptosOptions::builder().output_name("app_test").build();
assert_eq!(conf.output_name, "app_test");
assert!(matches!(conf.env, Env::DEV));
assert_eq!(conf.site_pkg_dir, "pkg");
assert_eq!(conf.site_root, ".");

View File

@@ -9,14 +9,12 @@ description = "DOM operations for the Leptos web framework."
[dependencies]
async-recursion = "1"
base64 = { version = "0.21", optional = true }
cfg-if = "1"
drain_filter_polyfill = "0.1"
educe = "0.4"
futures = "0.3"
getrandom = { version = "0.2", optional = true }
html-escape = "0.2"
indexmap = "2"
indexmap = "1.9"
itertools = "0.10"
js-sys = "0.3"
leptos_reactive = { workspace = true }
@@ -24,7 +22,6 @@ server_fn = { workspace = true }
once_cell = "1"
pad-adapter = "0.1"
paste = "1"
rand = { version = "0.8", optional = true }
rustc-hash = "1.1.0"
serde_json = "1"
smallvec = "1"
@@ -32,9 +29,6 @@ tracing = "0.1"
wasm-bindgen = { version = "0.2", features = ["enable-interning"] }
wasm-bindgen-futures = "0.4.31"
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2", features = ["js"] }
[dev-dependencies]
leptos = { path = "../leptos" }
@@ -55,7 +49,6 @@ features = [
"Range",
"Text",
"HtmlCollection",
"ShadowRoot",
"TreeWalker",
# Events we cast to in leptos_macro -- added here so we don't force users to import them
@@ -164,12 +157,9 @@ features = [
[features]
default = []
web = []
csr = ["leptos_reactive/csr", "web"]
hydrate = ["leptos_reactive/hydrate", "web"]
web = ["leptos_reactive/csr"]
ssr = ["leptos_reactive/ssr"]
nightly = ["leptos_reactive/nightly"]
nonce = ["dep:base64", "dep:getrandom", "dep:rand"]
[package.metadata.cargo-all-features]
denylist = ["nightly"]

View File

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

View File

@@ -158,13 +158,19 @@ pub(crate) fn add_delegated_event_listener(
}
// navigate up tree
if let Some(parent) =
node.unchecked_ref::<web_sys::Node>().parent_node()
let host =
js_sys::Reflect::get(&node, &JsValue::from_str("host"))
.unwrap_throw();
if host.is_truthy()
&& host != node
&& host.dyn_ref::<web_sys::Node>().is_some()
{
node = host;
} else if let Some(parent) =
node.unchecked_into::<web_sys::Node>().parent_node()
{
node = parent.into()
} else if let Some(root) = node.dyn_ref::<web_sys::ShadowRoot>() {
node = root.host().unchecked_into();
} else {
} else {
node = JsValue::null()
}
}

View File

@@ -204,9 +204,11 @@ impl Custom {
assert_eq!(
el.node_name().to_ascii_uppercase(),
name.to_ascii_uppercase(),
"SSR and CSR elements have the same hydration key but \
different node kinds. Check out the docs for information \
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
"SSR and CSR elements have the same `TopoId` but \
different node kinds. This is either a discrepancy \
between SSR and CSR rendering
logic, which is considered a bug, or it can also be a \
leptos hydration issue."
);
el.remove_attribute("id").unwrap();
@@ -219,9 +221,11 @@ impl Custom {
assert_eq!(
el.node_name().to_ascii_uppercase(),
name.to_ascii_uppercase(),
"SSR and CSR elements have the same hydration key but \
different node kinds. Check out the docs for information \
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
"SSR and CSR elements have the same `TopoId` but \
different node kinds. This is either a discrepancy \
between SSR and CSR rendering
logic, which is considered a bug, or it can also be a \
leptos hydration issue."
);
el.remove_attribute("leptos-hk").unwrap();
@@ -1266,9 +1270,11 @@ fn create_leptos_element(
assert_eq!(
&el.node_name().to_ascii_uppercase(),
tag,
"SSR and CSR elements have the same hydration key but \
different node kinds. Check out the docs for information \
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
"SSR and CSR elements have the same `TopoId` but different \
node kinds. This is either a discrepancy between SSR and CSR \
rendering
logic, which is considered a bug, or it can also be a leptos \
hydration issue."
);
el.remove_attribute("id").unwrap();
@@ -1281,9 +1287,11 @@ fn create_leptos_element(
assert_eq!(
el.node_name().to_ascii_uppercase(),
tag,
"SSR and CSR elements have the same hydration key but \
different node kinds. Check out the docs for information \
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
"SSR and CSR elements have the same `TopoId` but different \
node kinds. This is either a discrepancy between SSR and CSR \
rendering
logic, which is considered a bug, or it can also be a leptos \
hydration issue."
);
el.remove_attribute("leptos-hk").unwrap();

View File

@@ -1,16 +1,17 @@
use cfg_if::cfg_if;
use std::{cell::RefCell, fmt::Display};
#[cfg(all(target_arch = "wasm32", feature = "hydrate"))]
mod hydration {
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use once_cell::unsync::Lazy as LazyCell;
use std::{cell::RefCell, collections::HashMap};
use std::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! {
pub static HYDRATION_COMMENTS: LazyCell<HashMap<String, web_sys::Comment>> = LazyCell::new(|| {
static HYDRATION_COMMENTS: LazyCell<HashMap<String, web_sys::Comment>> = LazyCell::new(|| {
let document = crate::document();
let body = document.body().unwrap();
let walker = document
@@ -30,7 +31,7 @@ mod hydration {
});
#[cfg(debug_assertions)]
pub static VIEW_MARKERS: LazyCell<HashMap<String, web_sys::Comment>> = LazyCell::new(|| {
pub(crate) static VIEW_MARKERS: LazyCell<HashMap<String, web_sys::Comment>> = LazyCell::new(|| {
let document = crate::document();
let body = document.body().unwrap();
let walker = document
@@ -47,7 +48,7 @@ mod hydration {
map
});
pub static IS_HYDRATING: RefCell<LazyCell<bool>> = RefCell::new(LazyCell::new(|| {
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()
@@ -59,14 +60,12 @@ mod hydration {
}));
}
pub fn get_marker(id: &str) -> Option<web_sys::Comment> {
HYDRATION_COMMENTS.with(|comments| comments.get(id).cloned())
pub(crate) 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 {
@@ -126,27 +125,16 @@ impl HydrationCtx {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn stop_hydrating() {
#[cfg(feature = "hydrate")]
{
IS_HYDRATING.with(|is_hydrating| {
std::mem::take(&mut *is_hydrating.borrow_mut());
})
}
IS_HYDRATING.with(|is_hydrating| {
std::mem::take(&mut *is_hydrating.borrow_mut());
})
}
/// 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
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn is_hydrating() -> bool {
IS_HYDRATING.with(|is_hydrating| **is_hydrating.borrow())
}
#[allow(dead_code)] // not used in CSR
pub(crate) fn to_string(id: &HydrationKey, closing: bool) -> String {
#[cfg(debug_assertions)]
return format!("_{id}{}", if closing { 'c' } else { 'o' });

View File

@@ -18,8 +18,6 @@ mod logging;
mod macro_helpers;
pub mod math;
mod node_ref;
/// Utilities for exporting nonces to be used for a Content Security Policy.
pub mod nonce;
pub mod ssr;
pub mod ssr_in_order;
pub mod svg;
@@ -416,18 +414,11 @@ 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);
@@ -741,7 +732,7 @@ impl View {
c.children.iter().cloned().for_each(|c| {
let event_handler = event_handler.clone();
_ = c.on(event.clone(), Box::new(move |e| event_handler.borrow_mut()(e)));
c.on(event.clone(), Box::new(move |e| event_handler.borrow_mut()(e)));
});
}
Self::CoreComponent(c) => match c {
@@ -850,7 +841,9 @@ where
crate::console_warn(
"You have both `csr` and `ssr` or `hydrate` and `ssr` enabled as \
features, which may cause issues like <Suspense/>` failing to work \
silently.",
silently. `csr` is enabled by default on `leptos`, and can be \
disabled by adding `default-features = false` to your `leptos` \
dependency.",
);
cfg_if! {

View File

@@ -67,9 +67,11 @@ macro_rules! generate_math_tags {
assert_eq!(
el.node_name().to_ascii_uppercase(),
stringify!([<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]),
"SSR and CSR elements have the same hydration key but \
different node kinds. Check out the docs for information \
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
"SSR and CSR elements have the same `TopoId` \
but different node kinds. This is either a \
discrepancy between SSR and CSR rendering
logic, which is considered a bug, or it \
can also be a leptos hydration issue."
);
el.remove_attribute("id").unwrap();
@@ -82,9 +84,11 @@ macro_rules! generate_math_tags {
assert_eq!(
el.node_name().to_ascii_uppercase(),
stringify!([<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]),
"SSR and CSR elements have the same hydration key but \
different node kinds. Check out the docs for information \
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
"SSR and CSR elements have the same `TopoId` \
but different node kinds. This is either a \
discrepancy between SSR and CSR rendering
logic, which is considered a bug, or it \
can also be a leptos hydration issue."
);
el.remove_attribute("leptos-hk").unwrap();

View File

@@ -1,158 +0,0 @@
use crate::{Attribute, IntoAttribute};
use leptos_reactive::{use_context, Scope};
use std::{fmt::Display, ops::Deref};
/// A nonce a cryptographic nonce ("number used once") which can be
/// used by Content Security Policy to determine whether or not a given
/// resource will be allowed to load.
///
/// When the `nonce` feature is enabled on one of the server integrations,
/// a nonce is generated during server rendering and added to all inline
/// scripts used for HTML streaming and resource loading.
///
/// The nonce being used during the current server response can be
/// accessed using [`use_nonce`](use_nonce).
///
/// ```rust,ignore
/// #[component]
/// pub fn App(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
///
/// view! { cx,
/// // use `leptos_meta` to insert a <meta> tag with the CSP
/// <Meta
/// http_equiv="Content-Security-Policy"
/// content=move || {
/// // this will insert the CSP with nonce on the server, be empty on client
/// use_nonce(cx)
/// .map(|nonce| {
/// format!(
/// "default-src 'self'; script-src 'strict-dynamic' 'nonce-{nonce}' \
/// 'wasm-unsafe-eval'; style-src 'nonce-{nonce}';"
/// )
/// })
/// .unwrap_or_default()
/// }
/// />
/// // manually insert nonce during SSR on inline script
/// <script nonce=use_nonce(cx)>"console.log('Hello, world!');"</script>
/// // leptos_meta <Style/> and <Script/> automatically insert the nonce
/// <Style>"body { color: blue; }"</Style>
/// <p>"Test"</p>
/// }
/// }
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Nonce(pub(crate) String);
impl Deref for Nonce {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Display for Nonce {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl IntoAttribute for Nonce {
fn into_attribute(self, _cx: Scope) -> Attribute {
Attribute::String(self.0.into())
}
fn into_attribute_boxed(self: Box<Self>, _cx: Scope) -> Attribute {
Attribute::String(self.0.into())
}
}
impl IntoAttribute for Option<Nonce> {
fn into_attribute(self, cx: Scope) -> Attribute {
Attribute::Option(cx, self.map(|n| n.0.into()))
}
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
Attribute::Option(cx, self.map(|n| n.0.into()))
}
}
/// Accesses the nonce that has been generated during the current
/// server response. This can be added to inline `<script>` and
/// `<style>` tags for compatibility with a Content Security Policy.
///
/// ```rust,ignore
/// #[component]
/// pub fn App(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
///
/// view! { cx,
/// // use `leptos_meta` to insert a <meta> tag with the CSP
/// <Meta
/// http_equiv="Content-Security-Policy"
/// content=move || {
/// // this will insert the CSP with nonce on the server, be empty on client
/// use_nonce(cx)
/// .map(|nonce| {
/// format!(
/// "default-src 'self'; script-src 'strict-dynamic' 'nonce-{nonce}' \
/// 'wasm-unsafe-eval'; style-src 'nonce-{nonce}';"
/// )
/// })
/// .unwrap_or_default()
/// }
/// />
/// // manually insert nonce during SSR on inline script
/// <script nonce=use_nonce(cx)>"console.log('Hello, world!');"</script>
/// // leptos_meta <Style/> and <Script/> automatically insert the nonce
/// <Style>"body { color: blue; }"</Style>
/// <p>"Test"</p>
/// }
/// }
/// ```
pub fn use_nonce(cx: Scope) -> Option<Nonce> {
use_context::<Nonce>(cx)
}
#[cfg(all(feature = "ssr", feature = "nonce"))]
pub use generate::*;
#[cfg(all(feature = "ssr", feature = "nonce"))]
mod generate {
use super::Nonce;
use base64::{
alphabet,
engine::{self, general_purpose},
Engine,
};
use leptos_reactive::{provide_context, Scope};
use rand::{thread_rng, RngCore};
const NONCE_ENGINE: engine::GeneralPurpose = engine::GeneralPurpose::new(
&alphabet::URL_SAFE,
general_purpose::NO_PAD,
);
impl Nonce {
/// Generates a new nonce from 16 bytes (128 bits) of random data.
pub fn new() -> Self {
let mut thread_rng = thread_rng();
let mut bytes = [0; 16];
thread_rng.fill_bytes(&mut bytes);
Nonce(NONCE_ENGINE.encode(bytes))
}
}
impl Default for Nonce {
fn default() -> Self {
Self::new()
}
}
/// Generates a nonce and provides it during server rendering.
pub fn provide_nonce(cx: Scope) {
provide_context(cx, Nonce::new())
}
}

View File

@@ -189,7 +189,6 @@ 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, {
@@ -213,9 +212,6 @@ pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacemen
}
});
let cx = Scope { runtime, id: scope };
let nonce_str = crate::nonce::use_nonce(cx)
.map(|nonce| format!(" nonce=\"{nonce}\""))
.unwrap_or_default();
let mut blocking_fragments = FuturesUnordered::new();
let fragments = FuturesUnordered::new();
@@ -234,110 +230,91 @@ pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacemen
let stream = futures::stream::once(
// HTML for the view function and script to store resources
{
let nonce_str = nonce_str.clone();
async move {
let resolvers = format!(
"<script{nonce_str}>__LEPTOS_PENDING_RESOURCES = \
{pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \
Map();__LEPTOS_RESOURCE_RESOLVERS = new Map();</script>"
);
async move {
let resolvers = format!(
"<script>__LEPTOS_PENDING_RESOURCES = \
{pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \
Map();__LEPTOS_RESOURCE_RESOLVERS = new Map();</script>"
);
if replace_blocks {
let mut blocks =
Vec::with_capacity(blocking_fragments.len());
while let Some((blocked_id, blocked_fragment)) =
blocking_fragments.next().await
{
blocks.push((blocked_id, blocked_fragment));
}
let prefix = prefix(cx);
let mut shell = shell;
for (blocked_id, blocked_fragment) in blocks {
let open = format!("<!--suspense-open-{blocked_id}-->");
let close =
format!("<!--suspense-close-{blocked_id}-->");
let (first, rest) =
shell.split_once(&open).unwrap_or_default();
let (_fallback, rest) =
rest.split_once(&close).unwrap_or_default();
shell =
format!("{first}{blocked_fragment}{rest}").into();
}
format!("{prefix}{shell}{resolvers}")
} else {
let mut blocking = String::new();
let mut blocking_fragments = fragments_to_chunks(
nonce_str.clone(),
blocking_fragments,
);
while let Some(fragment) = blocking_fragments.next().await {
blocking.push_str(&fragment);
}
let prefix = prefix(cx);
format!("{prefix}{shell}{resolvers}{blocking}")
if replace_blocks {
let mut blocks = Vec::with_capacity(blocking_fragments.len());
while let Some((blocked_id, blocked_fragment)) =
blocking_fragments.next().await
{
blocks.push((blocked_id, blocked_fragment));
}
let prefix = prefix(cx);
let mut shell = shell;
for (blocked_id, blocked_fragment) in blocks {
let open = format!("<!--suspense-open-{blocked_id}-->");
let close = format!("<!--suspense-close-{blocked_id}-->");
let (first, rest) =
shell.split_once(&open).unwrap_or_default();
let (_fallback, rest) =
rest.split_once(&close).unwrap_or_default();
shell = format!("{first}{blocked_fragment}{rest}").into();
}
format!("{prefix}{shell}{resolvers}")
} else {
let mut blocking = String::new();
let mut blocking_fragments =
fragments_to_chunks(blocking_fragments);
while let Some(fragment) = blocking_fragments.next().await {
blocking.push_str(&fragment);
}
let prefix = prefix(cx);
format!("{prefix}{shell}{resolvers}{blocking}")
}
},
)
.chain(ooo_body_stream_recurse(
cx,
nonce_str,
fragments,
serializers,
));
.chain(ooo_body_stream_recurse(cx, fragments, serializers));
(stream, runtime, scope)
}
fn ooo_body_stream_recurse(
cx: Scope,
nonce_str: String,
fragments: FuturesUnordered<PinnedFuture<(String, String)>>,
serializers: FuturesUnordered<PinnedFuture<(ResourceId, String)>>,
) -> Pin<Box<dyn Stream<Item = String>>> {
// resources and fragments
// stream HTML for each <Suspense/> as it resolves
let fragments = fragments_to_chunks(nonce_str.clone(), fragments);
let fragments = fragments_to_chunks(fragments);
// stream data for each Resource as it resolves
let resources = render_serializers(nonce_str.clone(), serializers);
let resources = render_serializers(serializers);
Box::pin(
// TODO these should be combined again in a way that chains them appropriately
// such that individual resources can resolve before all fragments are done
fragments.chain(resources).chain(
futures::stream::once({
async move {
let pending = cx.pending_fragments();
if !pending.is_empty() {
let fragments = FuturesUnordered::new();
let serializers = cx.serialization_resolvers();
for (fragment_id, data) in pending {
fragments.push(Box::pin(async move {
(fragment_id.clone(), data.out_of_order.await)
})
as Pin<
Box<dyn Future<Output = (String, String)>>,
>);
}
Box::pin(ooo_body_stream_recurse(
cx,
nonce_str.clone(),
fragments,
serializers,
))
as Pin<Box<dyn Stream<Item = String>>>
} else {
Box::pin(futures::stream::once(async move {
Default::default()
}))
futures::stream::once(async move {
let pending = cx.pending_fragments();
if !pending.is_empty() {
let fragments = FuturesUnordered::new();
let serializers = cx.serialization_resolvers();
for (fragment_id, data) in pending {
fragments.push(Box::pin(async move {
(fragment_id.clone(), data.out_of_order.await)
})
as Pin<Box<dyn Future<Output = (String, String)>>>);
}
Box::pin(ooo_body_stream_recurse(
cx,
fragments,
serializers,
))
as Pin<Box<dyn Stream<Item = String>>>
} else {
Box::pin(futures::stream::once(async move {
Default::default()
}))
}
})
.flatten(),
@@ -350,14 +327,13 @@ fn ooo_body_stream_recurse(
instrument(level = "trace", skip_all,)
)]
fn fragments_to_chunks(
nonce_str: String,
fragments: impl Stream<Item = (String, String)>,
) -> impl Stream<Item = String> {
fragments.map(move |(fragment_id, html)| {
fragments.map(|(fragment_id, html)| {
format!(
r#"
<template id="{fragment_id}f">{html}</template>
<script{nonce_str}>
<script>
var id = "{fragment_id}";
var open = undefined;
var close = undefined;
@@ -392,7 +368,9 @@ impl View {
crate::console_error(
"\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \
enabled as features, which may cause issues like <Suspense/>` \
failing to work silently.\n",
failing to work silently. `csr` is enabled by default on \
`leptos`, and can be disabled by adding `default-features = \
false` to your `leptos` dependency.\n",
);
self.render_to_string_helper(false)
@@ -703,14 +681,13 @@ pub(crate) fn to_kebab_case(name: &str) -> String {
instrument(level = "trace", skip_all,)
)]
pub(crate) fn render_serializers(
nonce_str: String,
serializers: FuturesUnordered<PinnedFuture<(ResourceId, String)>>,
) -> impl Stream<Item = String> {
serializers.map(move |(id, json)| {
serializers.map(|(id, json)| {
let id = serde_json::to_string(&id).unwrap();
let json = json.replace('<', "\\u003c");
format!(
r#"<script{nonce_str}>
r#"<script>
var val = {json:?};
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
__LEPTOS_RESOURCE_RESOLVERS.get({id})(val)

View File

@@ -59,7 +59,9 @@ pub fn render_to_stream_in_order_with_prefix(
crate::console_error(
"\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \
enabled as features, which may cause issues like <Suspense/>` \
failing to work silently.\n",
failing to work silently. `csr` is enabled by default on `leptos`, \
and can be disabled by adding `default-features = false` to your \
`leptos` dependency.\n",
);
let (stream, runtime, _) =
@@ -124,33 +126,24 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
handle_chunks(cx, tx, remaining_chunks).await;
});
let nonce = crate::nonce::use_nonce(cx);
let nonce_str = nonce
.as_ref()
.map(|nonce| format!(" nonce=\"{nonce}\""))
.unwrap_or_default();
let stream = futures::stream::once({
let nonce_str = nonce_str.clone();
async move {
let prefix = prefix_rx.await.expect("to receive prefix");
format!(
r#"
let stream = futures::stream::once(async move {
let prefix = prefix_rx.await.expect("to receive prefix");
format!(
r#"
{prefix}
<script{nonce_str}>
<script>
__LEPTOS_PENDING_RESOURCES = {pending_resources};
__LEPTOS_RESOLVED_RESOURCES = new Map();
__LEPTOS_RESOURCE_RESOLVERS = new Map();
</script>
"#
)
}
)
})
.chain(rx)
.chain(
futures::stream::once(async move {
let serializers = cx.serialization_resolvers();
render_serializers(nonce_str, serializers)
render_serializers(serializers)
})
.flatten(),
);

View File

@@ -64,9 +64,11 @@ macro_rules! generate_svg_tags {
assert_eq!(
el.node_name().to_ascii_uppercase(),
stringify!([<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]),
"SSR and CSR elements have the same hydration key but \
different node kinds. Check out the docs for information \
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
"SSR and CSR elements have the same `TopoId` \
but different node kinds. This is either a \
discrepancy between SSR and CSR rendering
logic, which is considered a bug, or it \
can also be a leptos hydration issue."
);
el.remove_attribute("id").unwrap();
@@ -79,9 +81,11 @@ macro_rules! generate_svg_tags {
assert_eq!(
el.node_name().to_ascii_uppercase(),
stringify!([<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]),
"SSR and CSR elements have the same hydration key but \
different node kinds. Check out the docs for information \
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
"SSR and CSR elements have the same `TopoId` \
but different node kinds. This is either a \
discrepancy between SSR and CSR rendering
logic, which is considered a bug, or it \
can also be a leptos hydration issue."
);
el.remove_attribute("leptos-hk").unwrap();

View File

@@ -24,4 +24,4 @@ proc-macro2 = { version = "1", features = ["span-locations", "nightly"] }
parking_lot = "0.12"
walkdir = "2"
camino = "1.1.3"
indexmap = "2"
indexmap = "1.9.2"

View File

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

View File

@@ -1,397 +1,386 @@
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);
// 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);
}
}
}
});
} else {
console.warn("[HOT RELOADING] Unmatched action", action);
}
}
// 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);
}
// 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();
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;
}
}
}
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) {
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;
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 {
elementCount[walker.currentNode.nodeName] = 0;
}
elementCount[walker.currentNode.nodeName];
console.warn("[HOT RELOADING] Child at ", path, "not found in ", element);
return element;
}
}
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);
}
function childrenFromRange(parent, start, end) {
const range = new Range();
range.setStartAfter(start);
range.setEndBefore(end);
return buildActualChildren(parent, range);
}
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);
}
function fromHTML(html) {
const template = document.createElement("template");
template.innerHTML = html;
return template.content.cloneNode(true);
}
}

View File

@@ -1,23 +0,0 @@
extend = { path = "../cargo-make/main.toml" }
[tasks.test]
clear = true
dependencies = [
"test-all",
"test-leptos_macro-example",
"doc-leptos_macro-example",
]
[tasks.test-leptos_macro-example]
description = "Tests the leptos_macro/example to check if macro handles doc comments correctly"
command = "cargo"
args = ["+nightly", "test", "--doc"]
cwd = "example"
install_crate = false
[tasks.doc-leptos_macro-example]
description = "Docs the leptos_macro/example to check if macro handles doc comments correctly"
command = "cargo"
args = ["+nightly", "doc"]
cwd = "example"
install_crate = false

View File

@@ -1553,7 +1553,6 @@ pub(crate) fn component_to_tokens(
global_class: Option<&TokenTree>,
) -> TokenStream {
let name = node.name();
#[cfg(debug_assertions)]
let component_name = ident_from_tag_name(node.name());
let span = node.name().span();
@@ -1686,7 +1685,6 @@ pub(crate) fn component_to_tokens(
}
});
#[allow(unused_mut)] // used in debug
let mut component = quote! {
::leptos::component_view(
&#name,
@@ -2122,7 +2120,6 @@ impl IdeTagHelper {
/// open_tag(open_tag.props().slots().children().build())
/// }
/// ```
#[cfg(debug_assertions)]
pub fn add_component_completion(
component: &mut TokenStream,
node: &NodeElement,

View File

@@ -41,7 +41,7 @@ web-sys = { version = "0.3", optional = true, features = [
"Window",
] }
cfg-if = "1"
indexmap = "2"
indexmap = "1"
self_cell = "1.0.0"
[dev-dependencies]

View File

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

View File

@@ -863,21 +863,6 @@ where
}
}
// on cleanup of this component, remove this read from parent `<Suspense/>`
// it will be added back in when this is rendered again
if let Some(s) = suspense_cx {
crate::on_cleanup(cx, {
let suspense_contexts = Rc::clone(&suspense_contexts);
move || {
if let Ok(ref mut contexts) =
suspense_contexts.try_borrow_mut()
{
contexts.remove(&s);
}
}
});
}
let increment = move |_: Option<()>| {
if let Some(s) = &suspense_cx {
if let Ok(ref mut contexts) = suspense_contexts.try_borrow_mut()
@@ -981,8 +966,10 @@ where
if version == last_version.get() {
resolved.set(true);
set_value.try_update(|n| *n = Some(res));
set_loading.try_update(|n| *n = false);
set_value.update(|n| *n = Some(res));
set_loading.update(|n| *n = false);
for suspense_context in
suspense_contexts.borrow().iter()

View File

@@ -415,14 +415,11 @@ pub struct RuntimeId;
impl RuntimeId {
/// Removes the runtime, disposing all its child [`Scope`](crate::Scope)s.
pub fn dispose(self) {
#[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);
cfg_if! {
if #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
let runtime = RUNTIMES.with(move |runtimes| runtimes.borrow_mut().remove(self));
drop(runtime);
}
}
}

View File

@@ -893,12 +893,8 @@ where
)
)]
fn set_untracked(&self, new_value: T) {
self.id.update_with_no_effect(
self.runtime,
|v| *v = new_value,
#[cfg(debug_assertions)]
Some(self.defined_at),
);
self.id
.update_with_no_effect(self.runtime, |v| *v = new_value);
}
#[cfg_attr(
@@ -917,12 +913,8 @@ where
fn try_set_untracked(&self, new_value: T) -> Option<T> {
let mut new_value = Some(new_value);
self.id.update(
self.runtime,
|t| *t = new_value.take().unwrap(),
#[cfg(debug_assertions)]
None,
);
self.id
.update(self.runtime, |t| *t = new_value.take().unwrap());
new_value
}
@@ -944,12 +936,7 @@ impl<T> SignalUpdateUntracked<T> for WriteSignal<T> {
)]
#[inline(always)]
fn update_untracked(&self, f: impl FnOnce(&mut T)) {
self.id.update_with_no_effect(
self.runtime,
f,
#[cfg(debug_assertions)]
Some(self.defined_at),
);
self.id.update_with_no_effect(self.runtime, f);
}
#[inline(always)]
@@ -957,12 +944,7 @@ impl<T> SignalUpdateUntracked<T> for WriteSignal<T> {
&self,
f: impl FnOnce(&mut T) -> O,
) -> Option<O> {
self.id.update_with_no_effect(
self.runtime,
f,
#[cfg(debug_assertions)]
None,
)
self.id.update_with_no_effect(self.runtime, f)
}
}
@@ -998,16 +980,7 @@ impl<T> SignalUpdate<T> for WriteSignal<T> {
)]
#[inline(always)]
fn update(&self, f: impl FnOnce(&mut T)) {
if self
.id
.update(
self.runtime,
f,
#[cfg(debug_assertions)]
Some(self.defined_at),
)
.is_none()
{
if self.id.update(self.runtime, f).is_none() {
warn_updating_dead_signal(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
@@ -1030,12 +1003,7 @@ impl<T> SignalUpdate<T> for WriteSignal<T> {
)]
#[inline(always)]
fn try_update<O>(&self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
self.id.update(
self.runtime,
f,
#[cfg(debug_assertions)]
None,
)
self.id.update(self.runtime, f)
}
}
@@ -1071,12 +1039,7 @@ impl<T> SignalSet<T> for WriteSignal<T> {
)
)]
fn set(&self, new_value: T) {
self.id.update(
self.runtime,
|n| *n = new_value,
#[cfg(debug_assertions)]
Some(self.defined_at),
);
self.id.update(self.runtime, |n| *n = new_value);
}
#[cfg_attr(
@@ -1095,12 +1058,8 @@ impl<T> SignalSet<T> for WriteSignal<T> {
fn try_set(&self, new_value: T) -> Option<T> {
let mut new_value = Some(new_value);
self.id.update(
self.runtime,
|t| *t = new_value.take().unwrap(),
#[cfg(debug_assertions)]
None,
);
self.id
.update(self.runtime, |t| *t = new_value.take().unwrap());
new_value
}
@@ -1381,12 +1340,8 @@ impl<T> SignalSetUntracked<T> for RwSignal<T> {
)
)]
fn set_untracked(&self, new_value: T) {
self.id.update_with_no_effect(
self.runtime,
|v| *v = new_value,
#[cfg(debug_assertions)]
Some(self.defined_at),
);
self.id
.update_with_no_effect(self.runtime, |v| *v = new_value);
}
#[cfg_attr(
@@ -1405,12 +1360,8 @@ impl<T> SignalSetUntracked<T> for RwSignal<T> {
fn try_set_untracked(&self, new_value: T) -> Option<T> {
let mut new_value = Some(new_value);
self.id.update(
self.runtime,
|t| *t = new_value.take().unwrap(),
#[cfg(debug_assertions)]
None,
);
self.id
.update(self.runtime, |t| *t = new_value.take().unwrap());
new_value
}
@@ -1432,12 +1383,7 @@ impl<T> SignalUpdateUntracked<T> for RwSignal<T> {
)]
#[inline(always)]
fn update_untracked(&self, f: impl FnOnce(&mut T)) {
self.id.update_with_no_effect(
self.runtime,
f,
#[cfg(debug_assertions)]
Some(self.defined_at),
);
self.id.update_with_no_effect(self.runtime, f);
}
#[cfg_attr(
@@ -1458,12 +1404,7 @@ impl<T> SignalUpdateUntracked<T> for RwSignal<T> {
&self,
f: impl FnOnce(&mut T) -> O,
) -> Option<O> {
self.id.update_with_no_effect(
self.runtime,
f,
#[cfg(debug_assertions)]
None,
)
self.id.update_with_no_effect(self.runtime, f)
}
}
@@ -1654,16 +1595,7 @@ impl<T> SignalUpdate<T> for RwSignal<T> {
)]
#[inline(always)]
fn update(&self, f: impl FnOnce(&mut T)) {
if self
.id
.update(
self.runtime,
f,
#[cfg(debug_assertions)]
Some(self.defined_at),
)
.is_none()
{
if self.id.update(self.runtime, f).is_none() {
warn_updating_dead_signal(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
@@ -1686,12 +1618,7 @@ impl<T> SignalUpdate<T> for RwSignal<T> {
)]
#[inline(always)]
fn try_update<O>(&self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
self.id.update(
self.runtime,
f,
#[cfg(debug_assertions)]
None,
)
self.id.update(self.runtime, f)
}
}
@@ -1722,12 +1649,7 @@ impl<T> SignalSet<T> for RwSignal<T> {
)
)]
fn set(&self, value: T) {
self.id.update(
self.runtime,
|n| *n = value,
#[cfg(debug_assertions)]
Some(self.defined_at),
);
self.id.update(self.runtime, |n| *n = value);
}
#[cfg_attr(
@@ -1746,12 +1668,8 @@ impl<T> SignalSet<T> for RwSignal<T> {
fn try_set(&self, new_value: T) -> Option<T> {
let mut new_value = Some(new_value);
self.id.update(
self.runtime,
|t| *t = new_value.take().unwrap(),
#[cfg(debug_assertions)]
None,
);
self.id
.update(self.runtime, |t| *t = new_value.take().unwrap());
new_value
}
@@ -2032,9 +1950,6 @@ impl NodeId {
&self,
runtime: RuntimeId,
f: impl FnOnce(&mut T) -> U,
#[cfg(debug_assertions)] defined_at: Option<
&'static std::panic::Location<'static>,
>,
) -> Option<U>
where
T: 'static,
@@ -2053,21 +1968,14 @@ impl NodeId {
None
}
} else {
#[cfg(debug_assertions)]
{
if let Some(defined_at) = defined_at {
debug_warn!(
"[Signal::update] Youre trying to update a \
Signal<{}> (defined at {defined_at}) that has \
already been disposed of. This is probably \
either a logic error in a component that creates \
and disposes of scopes. If it does cause cause \
any issues, it is safe to ignore this warning, \
which occurs only in debug mode.",
std::any::type_name::<T>()
);
}
}
debug_warn!(
"[Signal::update] Youre trying to update a Signal<{}> \
that has already been disposed of. This is probably \
either a logic error in a component that creates and \
disposes of scopes, or a Resource resolving after its \
scope has been dropped without having been cleaned up.",
std::any::type_name::<T>()
);
None
}
})
@@ -2079,9 +1987,6 @@ impl NodeId {
&self,
runtime_id: RuntimeId,
f: impl FnOnce(&mut T) -> U,
#[cfg(debug_assertions)] defined_at: Option<
&'static std::panic::Location<'static>,
>,
) -> Option<U>
where
T: 'static,
@@ -2100,21 +2005,14 @@ impl NodeId {
None
}
} else {
#[cfg(debug_assertions)]
{
if let Some(defined_at) = defined_at {
debug_warn!(
"[Signal::update] Youre trying to update a \
Signal<{}> (defined at {defined_at}) that has \
already been disposed of. This is probably \
either a logic error in a component that creates \
and disposes of scopes. If it does cause cause \
any issues, it is safe to ignore this warning, \
which occurs only in debug mode.",
std::any::type_name::<T>()
);
}
}
debug_warn!(
"[Signal::update] Youre trying to update a Signal<{}> \
that has already been disposed of. This is probably \
either a logic error in a component that creates and \
disposes of scopes, or a Resource resolving after its \
scope has been dropped without having been cleaned up.",
std::any::type_name::<T>()
);
None
};
@@ -2136,20 +2034,12 @@ impl NodeId {
&self,
runtime: RuntimeId,
f: impl FnOnce(&mut T) -> U,
#[cfg(debug_assertions)] defined_at: Option<
&'static std::panic::Location<'static>,
>,
) -> Option<U>
where
T: 'static,
{
// update the value
self.update_value(
runtime,
f,
#[cfg(debug_assertions)]
defined_at,
)
self.update_value(runtime, f)
}
}

View File

@@ -137,7 +137,6 @@ 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();
@@ -195,10 +194,8 @@ 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 value = {
let values = runtime.stored_values.borrow();
values.get(self.id)?.clone()
};
let values = runtime.stored_values.borrow();
let value = values.get(self.id)?;
let mut value = value.borrow_mut();
let value = value.downcast_mut::<T>()?;
Some(f(value))
@@ -231,20 +228,13 @@ 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 n = {
let values = runtime.stored_values.borrow();
values.get(self.id).map(Rc::clone)
};
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>());
if let Some(n) = n {
let mut n = n.borrow_mut();
let n = n.downcast_mut::<T>();
if let Some(n) = n {
*n = value;
None
} else {
Some(value)
}
*n = value;
None
} else {
Some(value)
}
@@ -294,7 +284,6 @@ pub fn store_value<T>(cx: Scope, value: T) -> StoredValue<T>
where
T: 'static,
{
eprintln!("\nstoring in {:?}", cx.runtime);
let id = with_runtime(cx.runtime, |runtime| {
runtime
.stored_values

View File

@@ -10,7 +10,7 @@ readme = "../README.md"
[dependencies]
leptos_reactive = { workspace = true }
server_fn = { workspace = true }
server_fn = { workspace = true}
lazy_static = "1"
serde = { version = "1", features = ["derive"] }
thiserror = "1"
@@ -21,6 +21,7 @@ inventory = "0.3"
leptos = { path = "../leptos" }
[features]
default = ["default-tls"]
csr = ["leptos_reactive/csr"]
default-tls = ["server_fn/default-tls"]
hydrate = ["leptos_reactive/hydrate"]

View File

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

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