mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 09:02:37 -05:00
Compare commits
105 Commits
v0.2.2
...
optional-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95eb6b1bd9 | ||
|
|
e81f14a794 | ||
|
|
0e8125abde | ||
|
|
8bb2ee4569 | ||
|
|
5dab35447a | ||
|
|
63be819533 | ||
|
|
af8afb1204 | ||
|
|
2170be8e01 | ||
|
|
1187a506dd | ||
|
|
ff5ceddbe2 | ||
|
|
41a5e09caa | ||
|
|
9478245986 | ||
|
|
4c1c12734a | ||
|
|
5d3a360456 | ||
|
|
4e7a0db950 | ||
|
|
cee6ed9a9f | ||
|
|
fa1013f7c3 | ||
|
|
8b57ba7aa8 | ||
|
|
ea638e37f6 | ||
|
|
4342d45a2f | ||
|
|
fe4d2382b8 | ||
|
|
2a13609eff | ||
|
|
c2ff1cabf1 | ||
|
|
e72ed26809 | ||
|
|
64e056ffa9 | ||
|
|
db9b7db53d | ||
|
|
a9e6590b5e | ||
|
|
b67121b755 | ||
|
|
7bce4de682 | ||
|
|
8bdb427133 | ||
|
|
4c23f3c478 | ||
|
|
9502de561b | ||
|
|
210c11a733 | ||
|
|
6917027204 | ||
|
|
e78ce7e6b9 | ||
|
|
a3327f8841 | ||
|
|
f727dd773b | ||
|
|
952646f066 | ||
|
|
1e037ecb60 | ||
|
|
c9f75d82d6 | ||
|
|
de3849c20c | ||
|
|
c391c2e938 | ||
|
|
1cde4b1f8a | ||
|
|
42360d109b | ||
|
|
7aa4d9e6db | ||
|
|
9ed3390b81 | ||
|
|
1ff56f7bfd | ||
|
|
16917997cd | ||
|
|
f42568d262 | ||
|
|
97bbdf561a | ||
|
|
f4043cbd9f | ||
|
|
e9ff26abb4 | ||
|
|
e6b1298915 | ||
|
|
98a9ec8335 | ||
|
|
5329561687 | ||
|
|
89ca047f2f | ||
|
|
a94711fcf0 | ||
|
|
97d88c65ae | ||
|
|
e482e3748d | ||
|
|
8ab9c08448 | ||
|
|
56de70b714 | ||
|
|
38d97babd8 | ||
|
|
4cfecb5d82 | ||
|
|
08b5970b2b | ||
|
|
af20f80b2b | ||
|
|
c2fdd2cd70 | ||
|
|
286f3eebe4 | ||
|
|
509223ab2e | ||
|
|
665b0b8ed2 | ||
|
|
508ad52582 | ||
|
|
cfd5c98f97 | ||
|
|
2e63bb1f50 | ||
|
|
982c8f6b5a | ||
|
|
12c4c115f3 | ||
|
|
d4d20ecdb0 | ||
|
|
b78919c6ed | ||
|
|
abb9320e31 | ||
|
|
875d2d5a3a | ||
|
|
42a58855a0 | ||
|
|
9d142758ec | ||
|
|
2faddd85cb | ||
|
|
ddd463748d | ||
|
|
71ee4cd09d | ||
|
|
08c56f7d6c | ||
|
|
e1ba26b62c | ||
|
|
309f0bf826 | ||
|
|
1698ffa7db | ||
|
|
556955cf1a | ||
|
|
a9f778459a | ||
|
|
f2ac412253 | ||
|
|
3bd52fcc9d | ||
|
|
b9bd1e103c | ||
|
|
55f9081465 | ||
|
|
0bac16dba0 | ||
|
|
a8a9c575b5 | ||
|
|
15ec855db5 | ||
|
|
b8f79a7e56 | ||
|
|
b988ee85f4 | ||
|
|
d6e166f105 | ||
|
|
53ceca8ff8 | ||
|
|
f2f9759138 | ||
|
|
817152ff39 | ||
|
|
38daaf3b72 | ||
|
|
666d53e2ba | ||
|
|
b55e9a9e64 |
5
.github/workflows/check-examples.yml
vendored
5
.github/workflows/check-examples.yml
vendored
@@ -11,7 +11,7 @@ env:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test on ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -39,9 +39,6 @@ jobs:
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- name: Run Rustfmt
|
||||
run: cargo fmt -- --check
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo check on all examples
|
||||
|
||||
45
.github/workflows/check-stable.yml
vendored
Normal file
45
.github/workflows/check-stable.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- stable
|
||||
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 examples
|
||||
run: cargo make --profile=github-actions check-stable
|
||||
7
.github/workflows/check.yml
vendored
7
.github/workflows/check.yml
vendored
@@ -11,7 +11,7 @@ env:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test on ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
name: Run `cargo check` ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -39,10 +39,7 @@ jobs:
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- name: Run Rustfmt
|
||||
run: cargo fmt -- --check
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo check on all libraries
|
||||
run: cargo make check
|
||||
run: cargo make --profile=github-actions check
|
||||
|
||||
34
.github/workflows/fmt.yml
vendored
Normal file
34
.github/workflows/fmt.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Test
|
||||
|
||||
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
|
||||
37
.github/workflows/publish-book.yml
vendored
Normal file
37
.github/workflows/publish-book.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Deploy book
|
||||
on:
|
||||
push:
|
||||
paths: ['docs/book/**']
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # To push a branch
|
||||
pull-requests: write # To create a PR from that branch
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install mdbook
|
||||
run: |
|
||||
mkdir mdbook
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.27/mdbook-v0.4.27-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=./mdbook
|
||||
echo `pwd`/mdbook >> $GITHUB_PATH
|
||||
- name: Deploy GitHub Pages
|
||||
run: |
|
||||
cd docs/book
|
||||
mdbook build
|
||||
git worktree add gh-pages
|
||||
git config user.name "Deploy book from CI"
|
||||
git config user.email ""
|
||||
cd gh-pages
|
||||
# Delete the ref to avoid keeping history.
|
||||
git update-ref -d refs/heads/gh-pages
|
||||
rm -rf *
|
||||
mv ../book/* .
|
||||
git add .
|
||||
git commit -m "Deploy book $GITHUB_SHA to gh-pages"
|
||||
git push --force --set-upstream origin gh-pages
|
||||
7
.github/workflows/test.yml
vendored
7
.github/workflows/test.yml
vendored
@@ -11,7 +11,7 @@ env:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test on ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
name: Run tests ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -39,10 +39,7 @@ jobs:
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- name: Run Rustfmt
|
||||
run: cargo fmt -- --check
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run tests with all features
|
||||
run: cargo make test
|
||||
run: cargo make --profile=github-actions test
|
||||
|
||||
28
Cargo.toml
28
Cargo.toml
@@ -25,22 +25,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.2"
|
||||
version = "0.2.5"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.2.2" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.2" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.2" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.2" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.2" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.2" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.2.2" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.2" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.2" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.2" }
|
||||
leptos_router = { path = "./router", version = "0.2.2" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.2" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.2" }
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.2.5" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.5" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.5" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.5" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.5" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.5" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.2.5" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.5" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.5" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.5" }
|
||||
leptos_router = { path = "./router", version = "0.2.5" }
|
||||
leptos_meta = { path = "./meta", default-features = false, version = "0.2.5" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.5" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
# make tasks run at the workspace root
|
||||
default_to_workspace = false
|
||||
|
||||
[tasks.ci]
|
||||
dependencies = ["check", "check-examples", "test"]
|
||||
|
||||
[tasks.check]
|
||||
clear = true
|
||||
dependencies = ["check-all", "check-wasm"]
|
||||
dependencies = [
|
||||
"check-all",
|
||||
"check-wasm",
|
||||
"check-all-release",
|
||||
"check-wasm-release",
|
||||
]
|
||||
|
||||
[tasks.check-all]
|
||||
command = "cargo"
|
||||
@@ -23,14 +25,21 @@ install_crate = "cargo-all-features"
|
||||
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/counter_without_macros" },
|
||||
{ name = "check", path = "examples/counters" },
|
||||
{ name = "check", path = "examples/counters_stable" },
|
||||
{ name = "check", path = "examples/error_boundary" },
|
||||
{ name = "check", path = "examples/errors_axum" },
|
||||
{ name = "check", path = "examples/fetch" },
|
||||
@@ -43,12 +52,20 @@ dependencies = [
|
||||
{ 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/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]
|
||||
clear = true
|
||||
dependencies = [
|
||||
{ name = "check", path = "examples/counter_without_macros" },
|
||||
{ name = "check", path = "examples/counters_stable" },
|
||||
]
|
||||
|
||||
[tasks.test]
|
||||
clear = true
|
||||
dependencies = ["test-all"]
|
||||
@@ -57,3 +74,9 @@ dependencies = ["test-all"]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "test-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[env]
|
||||
RUSTFLAGS=""
|
||||
|
||||
[env.github-actions]
|
||||
RUSTFLAGS="-D warnings"
|
||||
33
README.md
33
README.md
@@ -24,8 +24,7 @@ pub fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
|
||||
let increment = move |_| set_value.update(|value| *value += 1);
|
||||
|
||||
// create user interfaces with the declarative `view!` macro
|
||||
view! {
|
||||
cx,
|
||||
view! { cx,
|
||||
<div>
|
||||
<button on:click=clear>"Clear"</button>
|
||||
<button on:click=decrement>"-1"</button>
|
||||
@@ -48,21 +47,21 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
|
||||
|
||||
## What does that mean?
|
||||
|
||||
- **Full-stack**: Leptos can be used to build apps that run in the browser (_client-side rendering_), on the server (_server-side rendering_), or by rendering HTML on the server and then adding interactivity in the browser (_hydration_). This includes support for _HTTP streaming_ of both data (`Resource`s) and HTML (out-of-order streaming of `<Suspense/>` components.)
|
||||
- **Isomorphic**: Leptos provides primitives to write isomorphic server functions, i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser.
|
||||
- **Web**: Leptos is built on the Web platform and Web standards. The router is designed to use Web fundamentals (like links and forms) and build on top of them rather than trying to replace them.
|
||||
- **Full-stack**: Leptos can be used to build apps that run in the browser (client-side rendering), on the server (server-side rendering), or by rendering HTML on the server and then adding interactivity in the browser (server-side rendering with hydration). This includes support for HTTP streaming of both data ([`Resource`s](https://docs.rs/leptos/latest/leptos/struct.Resource.html)) and HTML (out-of-order or in-order streaming of [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) components.)
|
||||
- **Isomorphic**: Leptos provides primitives to write isomorphic [server functions](https://docs.rs/leptos_server/0.2.5/leptos_server/index.html), i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser, without needing to create and maintain a separate REST or other API.
|
||||
- **Web**: Leptos is built on the Web platform and Web standards. The [router](https://docs.rs/leptos_router/latest/leptos_router/) is designed to use Web fundamentals (like links and forms) and build on top of them rather than trying to replace them.
|
||||
- **Framework**: Leptos provides most of what you need to build a modern web app: a reactive system, templating library, and a router that works on both the server and client side.
|
||||
- **Fine-grained reactivity**: The entire framework is built from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signal’s value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (_So, no virtual DOM!_)
|
||||
- **Fine-grained reactivity**: The entire framework is built from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signal’s value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (So, no virtual DOM overhead!)
|
||||
- **Declarative**: Tell Leptos how you want the page to look, and let the framework tell the browser how to do it.
|
||||
|
||||
## Learn more
|
||||
|
||||
Here are some resources for learning more about Leptos:
|
||||
|
||||
- [Book](https://leptos-rs.github.io/leptos/) (work in progress)
|
||||
- [Examples](https://github.com/leptos-rs/leptos/tree/main/examples)
|
||||
- [API Documentation](https://docs.rs/leptos/latest/leptos/)
|
||||
- [Common Bugs](https://github.com/leptos-rs/leptos/tree/main/docs/COMMON_BUGS.md) (and how to fix them!)
|
||||
- Leptos Guide (in progress)
|
||||
|
||||
## `nightly` Note
|
||||
|
||||
@@ -78,7 +77,7 @@ rustup target add wasm32-unknown-unknown
|
||||
|
||||
If you’re on `stable`, note the following:
|
||||
|
||||
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.1.0", features = ["stable"] }`
|
||||
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.2", features = ["stable"] }`
|
||||
2. `nightly` enables the function call syntax for accessing and setting signals. If you’re using `stable`,
|
||||
you’ll just call `.get()`, `.set()`, or `.update()` manually. Check out the
|
||||
[`counters_stable` example](https://github.com/leptos-rs/leptos/blob/main/examples/counters_stable/src/main.rs)
|
||||
@@ -86,7 +85,7 @@ If you’re on `stable`, note the following:
|
||||
|
||||
## `cargo-leptos`
|
||||
|
||||
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our [starter template](https://github.com/leptos-rs/start).
|
||||
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our starter templates for [Actix](https://github.com/leptos-rs/start) or [Axum](https://github.com/leptos-rs/start-axum).
|
||||
|
||||
```bash
|
||||
cargo install cargo-leptos
|
||||
@@ -95,13 +94,13 @@ cd [your project name]
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
Open browser to [http://localhost:3000/](http://localhost:3000/).
|
||||
|
||||
## FAQs
|
||||
|
||||
### What’s up with the name?
|
||||
|
||||
*Leptos* (λεπτός) is an ancient Greek word meaning “thin, light, refine, fine-grained.” To me, a classicist and not a dog owner, it evokes the lightweight reactive system that powers the framework. I've since learned the same word is at the root of the medical term “leptospirosis,” a blood infection that affects humans and animals... My bad. No dogs were harmed in the creation of this framework.
|
||||
_Leptos_ (λεπτός) is an ancient Greek word meaning “thin, light, refine, fine-grained.” To me, a classicist and not a dog owner, it evokes the lightweight reactive system that powers the framework. I've since learned the same word is at the root of the medical term “leptospirosis,” a blood infection that affects humans and animals... My bad. No dogs were harmed in the creation of this framework.
|
||||
|
||||
### Is it production ready?
|
||||
|
||||
@@ -109,7 +108,7 @@ People usually mean one of three things by this question.
|
||||
|
||||
1. **Are the APIs stable?** i.e., will I have to rewrite my whole app from Leptos 0.1 to 0.2 to 0.3 to 0.4, or can I write it now and benefit from new features and updates as new versions come?
|
||||
|
||||
With 0.1 the APIs are basically settled. We’re adding new features, but we’re very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to, for example, a 0.2.0 release.
|
||||
The APIs are basically settled. We’re adding new features, but we’re very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to future releases. The sorts of breaking changes that we discuss are things like “Oh yeah, that function should probably take `cx` as its argument...” not major changes to the way you write your application.
|
||||
|
||||
2. **Are there bugs?**
|
||||
|
||||
@@ -119,7 +118,7 @@ Yes, I’m sure there are. You can see from the state of our issue tracker over
|
||||
|
||||
This may be the big one: “production ready” implies a certain orientation to a library: that you can basically use it, without any special knowledge of its internals or ability to contribute. Everyone has this at some level in their stack: for example I (@gbj) don’t have the capacity or knowledge to contribute to something like `wasm-bindgen` at this point: I simply rely on it to work.
|
||||
|
||||
There are several people in this community using Leptos right now for internal apps at work, who have also become significant contributors. I think this is the right level of production use for now. There may be missing features that you need, and you may end up building them! But for internal apps, if you’re willing to build and contribute missing pieces along the way, the framework is definitely usable right now.
|
||||
There are several people in the community using Leptos right now for internal apps at work, who have also become significant contributors. I think this is the right level of production use for now. There may be missing features that you need, and you may end up building them! But for internal apps, if you’re willing to build and contribute missing pieces along the way, the framework is definitely usable right now.
|
||||
|
||||
### Can I use this for native GUI?
|
||||
|
||||
@@ -137,8 +136,8 @@ I've put together a [very simple GTK example](https://github.com/leptos-rs/lepto
|
||||
On the surface level, these libraries may seem similar. Yew is, of course, the most mature Rust library for web UI development and has a huge ecosystem. Dioxus is similar in many ways, being heavily inspired by React. Here are some conceptual differences between Leptos and these frameworks:
|
||||
|
||||
- **VDOM vs. fine-grained:** Yew is built on the virtual DOM (VDOM) model: state changes cause components to re-render, generating a new virtual DOM tree. Yew diffs this against the previous VDOM, and applies those patches to the actual DOM. Component functions rerun whenever state changes. Leptos takes an entirely different approach. Components run once, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes.
|
||||
- **Performance:** This has huge performance implications: Leptos is simply _much_ faster at both creating and updating the UI than Yew is.
|
||||
- **Mental model:** Adopting fine-grained reactivity also tends to simplify the mental model. There are no surprising component re-renders because there are no re-renders. Your app can be divided into components based on what makes sense for your app, because they have no performance implications.
|
||||
- **Performance:** This has huge performance implications: Leptos is simply much faster at both creating and updating the UI than Yew is. (Dioxus has made huge advances in performance with its recent 0.3 release, and is now roughly on par with Leptos.)
|
||||
- **Mental model:** Adopting fine-grained reactivity also tends to simplify the mental model. There are no surprising component re-renders because there are no re-renders. You can call functions, create timeouts, etc. within the body of your component functions because they won’t be re-run. You don’t need to think about manual dependency tracking for effects; fine-grained reactivity tracks dependencies automatically.
|
||||
|
||||
### How is this different from Sycamore?
|
||||
|
||||
@@ -146,9 +145,9 @@ Conceptually, these two frameworks are very similar: because both are built on f
|
||||
|
||||
There are some practical differences that make a significant difference:
|
||||
|
||||
- **Maturity:** Sycamore is obviously a much more mature and stable library with a larger ecosystem.
|
||||
- **Templating:** Leptos uses a JSX-like template format (built on [syn-rsx](https://github.com/stoically/syn-rsx)) for its `view` macro. Sycamore offers the choice of its own templating DSL or a builder syntax.
|
||||
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use `create_rw_signal` to give a unified read/write signal.)_
|
||||
- **Server integration:** Leptos provides primitives that encourage HTML streaming and allow for easy async integration and RPC calls, even without WASM enabled, making it easy to opt into integrations between your frontend and backend code without pushing you toward any particular metaframework patterns.
|
||||
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html) to give a unified read/write signal.)_
|
||||
- **Signals are functions:** In Leptos, you can call a signal to access it rather than calling a specific method (so, `count()` instead of `count.get()`) This creates a more consistent mental model: accessing a reactive value is always a matter of calling a function. For example:
|
||||
|
||||
```rust
|
||||
|
||||
@@ -4,6 +4,7 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
l021 = { package = "leptos", version = "0.2.1" }
|
||||
leptos = { path = "../leptos", default-features = false, features = ["ssr"] }
|
||||
sycamore = { version = "0.8", features = ["ssr"] }
|
||||
yew = { git = "https://github.com/yewstack/yew", features = ["ssr"] }
|
||||
@@ -16,15 +17,11 @@ lazy_static = "1"
|
||||
log = "0.4"
|
||||
strum = "0.24"
|
||||
strum_macros = "0.24"
|
||||
serde = { version = "1", features = ["derive", "rc"]}
|
||||
serde = { version = "1", features = ["derive", "rc"] }
|
||||
serde_json = "1"
|
||||
tera = "1"
|
||||
reactive-signals = "0.1.0-alpha.4"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"Window",
|
||||
"Document",
|
||||
"HtmlElement",
|
||||
"HtmlInputElement"
|
||||
]
|
||||
features = ["Window", "Document", "HtmlElement", "HtmlInputElement"]
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
extern crate test;
|
||||
|
||||
//mod reactive;
|
||||
mod ssr;
|
||||
//åmod reactive;
|
||||
//mod ssr;
|
||||
mod todomvc;
|
||||
|
||||
@@ -1,35 +1,114 @@
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
use test::Bencher;
|
||||
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
#[bench]
|
||||
fn leptos_create_1000_signals(b: &mut Bencher) {
|
||||
use leptos::{create_isomorphic_effect, create_memo, create_scope, create_signal};
|
||||
fn leptos_deep_creation(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(|cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = (0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_deep_update(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_narrowing_down(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| reads.iter().map(|r| r.get()).sum::<i32>());
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_create_and_update_1000_signals(b: &mut Bencher) {
|
||||
use leptos::{create_isomorphic_effect, create_memo, create_scope, create_signal};
|
||||
fn leptos_fanning_out(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(|cx| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sig = create_rw_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move |_| sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_narrowing_update(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = (0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| reads.iter().map(|r| r.get()).sum::<i32>());
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
create_isomorphic_effect(cx, {
|
||||
let acc = Rc::clone(&acc);
|
||||
@@ -48,17 +127,20 @@ fn leptos_create_and_update_1000_signals(b: &mut Bencher) {
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_create_and_dispose_1000_scopes(b: &mut Bencher) {
|
||||
use leptos::{create_isomorphic_effect, create_scope, create_signal};
|
||||
fn leptos_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let disposers = (0..1000)
|
||||
.map(|_| {
|
||||
create_scope({
|
||||
create_scope(runtime, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move |cx| {
|
||||
let (r, w) = create_signal(cx, 0);
|
||||
@@ -76,16 +158,254 @@ fn leptos_create_and_dispose_1000_scopes(b: &mut Bencher) {
|
||||
disposer.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_create_1000_signals(b: &mut Bencher) {
|
||||
use sycamore::reactive::{create_effect, create_memo, create_scope, create_signal};
|
||||
fn rs_deep_update(b: &mut Bencher) {
|
||||
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
|
||||
|
||||
let sc = ClientRuntime::new_root_scope();
|
||||
b.iter(|| {
|
||||
let signal = signal!(sc, 0);
|
||||
let mut memos = Vec::<Signal<Func<i32>, ClientRuntime>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(signal!(sc, move || prev.get() + 1))
|
||||
} else {
|
||||
memos.push(signal!(sc, move || signal.get() + 1))
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn rs_fanning_out(b: &mut Bencher) {
|
||||
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
|
||||
let cx = ClientRuntime::new_root_scope();
|
||||
|
||||
b.iter(|| {
|
||||
let sig = signal!(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| signal!(cx, move || sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn rs_narrowing_update(b: &mut Bencher) {
|
||||
use reactive_signals::{Scope, Signal, signal, runtimes::ClientRuntime, types::Func};
|
||||
let cx = ClientRuntime::new_root_scope();
|
||||
|
||||
b.iter(|| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| signal!(cx, n)).collect::<Vec<_>>();
|
||||
let memo = signal!(cx, {
|
||||
let sigs = sigs.clone();
|
||||
move || {
|
||||
sigs.iter().map(|r| r.get()).sum::<i32>()
|
||||
}
|
||||
});
|
||||
assert_eq!(memo.get(), 499500);
|
||||
signal!(cx, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move || {
|
||||
acc.set(memo.get());
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(acc.get(), 499500);
|
||||
|
||||
sigs[1].update(|n| *n += 1);
|
||||
sigs[10].update(|n| *n += 1);
|
||||
sigs[100].update(|n| *n += 1);
|
||||
|
||||
assert_eq!(acc.get(), 499503);
|
||||
assert_eq!(memo.get(), 499503);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_deep_creation(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_deep_update(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_narrowing_down(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_fanning_out(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sig = create_rw_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move |_| sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
#[bench]
|
||||
fn l021_narrowing_update(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
create_isomorphic_effect(cx, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move |_| {
|
||||
acc.set(memo());
|
||||
}
|
||||
});
|
||||
assert_eq!(acc.get(), 499500);
|
||||
|
||||
writes[1].update(|n| *n += 1);
|
||||
writes[10].update(|n| *n += 1);
|
||||
writes[100].update(|n| *n += 1);
|
||||
|
||||
assert_eq!(acc.get(), 499503);
|
||||
assert_eq!(memo(), 499503);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let disposers = (0..1000)
|
||||
.map(|_| {
|
||||
create_scope(runtime, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move |cx| {
|
||||
let (r, w) = create_signal(cx, 0);
|
||||
create_isomorphic_effect(cx, {
|
||||
move |_| {
|
||||
acc.set(r());
|
||||
}
|
||||
});
|
||||
w.update(|n| *n += 1);
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for disposer in disposers {
|
||||
disposer.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_narrowing_down(b: &mut Bencher) {
|
||||
use sycamore::reactive::{
|
||||
create_effect, create_memo, create_scope, create_signal,
|
||||
};
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = Rc::new((0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>());
|
||||
let sigs = Rc::new(
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>(),
|
||||
);
|
||||
let memo = create_memo(cx, {
|
||||
let sigs = Rc::clone(&sigs);
|
||||
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
|
||||
@@ -97,13 +417,80 @@ fn sycamore_create_1000_signals(b: &mut Bencher) {
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_create_and_update_1000_signals(b: &mut Bencher) {
|
||||
use sycamore::reactive::{create_effect, create_memo, create_scope, create_signal};
|
||||
fn sycamore_fanning_out(b: &mut Bencher) {
|
||||
use sycamore::reactive::{
|
||||
create_effect, create_memo, create_scope, create_signal,
|
||||
};
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let sig = create_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move || sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| *(*m.get())).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| *(*m.get())).sum::<i32>(), 1000);
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_deep_creation(b: &mut Bencher) {
|
||||
use sycamore::reactive::*;
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let signal = create_signal(cx, 0);
|
||||
let mut memos = Vec::<&ReadSignal<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move || *prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move || *signal.get() + 1));
|
||||
}
|
||||
}
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_deep_update(b: &mut Bencher) {
|
||||
use sycamore::reactive::*;
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let signal = create_signal(cx, 0);
|
||||
let mut memos = Vec::<&ReadSignal<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move || *prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move || *signal.get() + 1));
|
||||
}
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(*memos[999].get(), 1001);
|
||||
});
|
||||
unsafe { d.dispose() };
|
||||
});
|
||||
}
|
||||
#[bench]
|
||||
fn sycamore_narrowing_update(b: &mut Bencher) {
|
||||
use sycamore::reactive::{
|
||||
create_effect, create_memo, create_scope, create_signal,
|
||||
};
|
||||
|
||||
b.iter(|| {
|
||||
let d = create_scope(|cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs = Rc::new((0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>());
|
||||
let sigs = Rc::new(
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>(),
|
||||
);
|
||||
let memo = create_memo(cx, {
|
||||
let sigs = Rc::clone(&sigs);
|
||||
move || sigs.iter().map(|r| *r.get()).sum::<i32>()
|
||||
@@ -129,7 +516,7 @@ fn sycamore_create_and_update_1000_signals(b: &mut Bencher) {
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_create_and_dispose_1000_scopes(b: &mut Bencher) {
|
||||
fn sycamore_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
use sycamore::reactive::{create_effect, create_scope, create_signal};
|
||||
|
||||
b.iter(|| {
|
||||
|
||||
@@ -4,7 +4,7 @@ use test::Bencher;
|
||||
fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
b.iter(|| {
|
||||
use leptos::*;
|
||||
HydrationCtx::reset_id();
|
||||
leptos_dom::HydrationCtx::reset_id();
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
#[component]
|
||||
fn Counter(cx: Scope, initial: i32) -> impl IntoView {
|
||||
@@ -32,7 +32,8 @@ fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
|
||||
assert_eq!(
|
||||
rendered,
|
||||
"<main id=\"_0-1\"><h1 id=\"_0-2\">Welcome to our benchmark page.</h1><p id=\"_0-3\">Here's some introductory text.</p><div id=\"_0-3-1\"><button id=\"_0-3-2\">-1</button><span id=\"_0-3-3\">Value: <!>1<!--hk=_0-3-4-->!</span><button id=\"_0-3-5\">+1</button></div><!--hk=_0-3-0--><div id=\"_0-3-5-1\"><button id=\"_0-3-5-2\">-1</button><span id=\"_0-3-5-3\">Value: <!>2<!--hk=_0-3-5-4-->!</span><button id=\"_0-3-5-5\">+1</button></div><!--hk=_0-3-5-0--><div id=\"_0-3-5-5-1\"><button id=\"_0-3-5-5-2\">-1</button><span id=\"_0-3-5-5-3\">Value: <!>3<!--hk=_0-3-5-5-4-->!</span><button id=\"_0-3-5-5-5\">+1</button></div><!--hk=_0-3-5-5-0--></main>" );
|
||||
"<main id=\"_0-1\"><h1 id=\"_0-2\">Welcome to our benchmark page.</h1><p id=\"_0-3\">Here's some introductory text.</p><div id=\"_0-3-1\"><button id=\"_0-3-2\">-1</button><span id=\"_0-3-3\">Value: <!>1<!--hk=_0-3-4-->!</span><button id=\"_0-3-5\">+1</button></div><!--hk=_0-3-0--><div id=\"_0-3-5-1\"><button id=\"_0-3-5-2\">-1</button><span id=\"_0-3-5-3\">Value: <!>2<!--hk=_0-3-5-4-->!</span><button id=\"_0-3-5-5\">+1</button></div><!--hk=_0-3-5-0--><div id=\"_0-3-5-5-1\"><button id=\"_0-3-5-5-2\">-1</button><span id=\"_0-3-5-5-3\">Value: <!>3<!--hk=_0-3-5-5-4-->!</span><button id=\"_0-3-5-5-5\">+1</button></div><!--hk=_0-3-5-5-0--></main>"
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub use leptos::*;
|
||||
use miniserde::*;
|
||||
use web_sys::HtmlInputElement;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Todos(pub Vec<Todo>);
|
||||
@@ -110,10 +111,6 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
provide_context(cx, set_todos);
|
||||
|
||||
let (mode, set_mode) = create_signal(cx, Mode::All);
|
||||
window_event_listener("hashchange", move |_| {
|
||||
let new_mode = location_hash().map(|hash| route(&hash)).unwrap_or_default();
|
||||
set_mode(new_mode);
|
||||
});
|
||||
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
let target = event_target::<HtmlInputElement>(&ev);
|
||||
@@ -167,57 +164,79 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<main>
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>"todos"</h1>
|
||||
<input class="new-todo" placeholder="What needs to be done?" autofocus="" on:keydown=add_todo />
|
||||
</header>
|
||||
<section class="main" class:hidden={move || todos.with(|t| t.is_empty())}>
|
||||
<input id="toggle-all" class="toggle-all" type="checkbox"
|
||||
prop:checked={move || todos.with(|t| t.remaining() > 0)}
|
||||
on:input=move |_| set_todos.update(|t| t.toggle_all())
|
||||
/>
|
||||
<label for="toggle-all">"Mark all as complete"</label>
|
||||
<ul class="todo-list">
|
||||
<For
|
||||
each=filtered_todos
|
||||
key=|todo| todo.id
|
||||
view=move |todo: Todo| view! { cx, <Todo todo=todo.clone() /> }
|
||||
/>
|
||||
</ul>
|
||||
</section>
|
||||
<footer class="footer" class:hidden={move || todos.with(|t| t.is_empty())}>
|
||||
<span class="todo-count">
|
||||
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
|
||||
{move || if todos.with(|t| t.remaining()) == 1 {
|
||||
" item"
|
||||
} else {
|
||||
" items"
|
||||
}}
|
||||
" left"
|
||||
</span>
|
||||
<ul class="filters">
|
||||
<li><a href="#/" class="selected" class:selected={move || mode() == Mode::All}>"All"</a></li>
|
||||
<li><a href="#/active" class:selected={move || mode() == Mode::Active}>"Active"</a></li>
|
||||
<li><a href="#/completed" class:selected={move || mode() == Mode::Completed}>"Completed"</a></li>
|
||||
</ul>
|
||||
<button
|
||||
class="clear-completed hidden"
|
||||
class:hidden={move || todos.with(|t| t.completed() == 0)}
|
||||
on:click=move |_| set_todos.update(|t| t.clear_completed())
|
||||
>
|
||||
"Clear completed"
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
<footer class="info">
|
||||
<p>"Double-click to edit a todo"</p>
|
||||
<p>"Created by "<a href="http://todomvc.com">"Greg Johnston"</a></p>
|
||||
<p>"Part of "<a href="http://todomvc.com">"TodoMVC"</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
}.into_view(cx)
|
||||
<main>
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>"todos"</h1>
|
||||
<input
|
||||
class="new-todo"
|
||||
placeholder="What needs to be done?"
|
||||
autofocus=""
|
||||
on:keydown=add_todo
|
||||
/>
|
||||
</header>
|
||||
<section class="main" class:hidden=move || todos.with(|t| t.is_empty())>
|
||||
<input
|
||||
id="toggle-all"
|
||||
class="toggle-all"
|
||||
type="checkbox"
|
||||
prop:checked=move || todos.with(|t| t.remaining() > 0)
|
||||
on:input=move |_| set_todos.update(|t| t.toggle_all())
|
||||
/>
|
||||
<label for="toggle-all">"Mark all as complete"</label>
|
||||
<ul class="todo-list">
|
||||
<For
|
||||
each=filtered_todos
|
||||
key=|todo| todo.id
|
||||
view=move |cx, todo: Todo| {
|
||||
view! { cx, <Todo todo=todo.clone()/> }
|
||||
}
|
||||
/>
|
||||
</ul>
|
||||
</section>
|
||||
<footer class="footer" class:hidden=move || todos.with(|t| t.is_empty())>
|
||||
<span class="todo-count">
|
||||
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
|
||||
{move || if todos.with(|t| t.remaining()) == 1 { " item" } else { " items" }}
|
||||
" left"
|
||||
</span>
|
||||
<ul class="filters">
|
||||
<li>
|
||||
<a
|
||||
href="#/"
|
||||
class="selected"
|
||||
class:selected=move || mode() == Mode::All
|
||||
>
|
||||
"All"
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#/active" class:selected=move || mode() == Mode::Active>
|
||||
"Active"
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#/completed" class:selected=move || mode() == Mode::Completed>
|
||||
"Completed"
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
class="clear-completed hidden"
|
||||
class:hidden=move || todos.with(|t| t.completed() == 0)
|
||||
on:click=move |_| set_todos.update(|t| t.clear_completed())
|
||||
>
|
||||
"Clear completed"
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
<footer class="info">
|
||||
<p>"Double-click to edit a todo"</p>
|
||||
<p>"Created by " <a href="http://todomvc.com">"Greg Johnston"</a></p>
|
||||
<p>"Part of " <a href="http://todomvc.com">"TodoMVC"</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
}.into_view(cx)
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -237,41 +256,36 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<li
|
||||
class="todo"
|
||||
class:editing={editing}
|
||||
class:completed={move || (todo.completed)()}
|
||||
//_ref=input
|
||||
>
|
||||
<li class="todo" class:editing=editing class:completed=move || (todo.completed)()>
|
||||
<div class="view">
|
||||
<input
|
||||
class="toggle"
|
||||
type="checkbox"
|
||||
prop:checked={move || (todo.completed)()}
|
||||
|
||||
/>
|
||||
<label on:dblclick=move |_| set_editing(true)>
|
||||
{move || todo.title.get()}
|
||||
</label>
|
||||
<button class="destroy" on:click=move |_| set_todos.update(|t| t.remove(todo.id))/>
|
||||
<input class="toggle" type="checkbox" prop:checked=move || (todo.completed)()/>
|
||||
<label on:dblclick=move |_| set_editing(true)>{move || todo.title.get()}</label>
|
||||
<button
|
||||
class="destroy"
|
||||
on:click=move |_| set_todos.update(|t| t.remove(todo.id))
|
||||
></button>
|
||||
</div>
|
||||
{move || editing().then(|| view! { cx,
|
||||
<input
|
||||
class="edit"
|
||||
class:hidden={move || !(editing)()}
|
||||
prop:value={move || todo.title.get()}
|
||||
on:focusout=move |ev| save(&event_target_value(&ev))
|
||||
on:keyup={move |ev| {
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
save(&event_target_value(&ev));
|
||||
} else if key_code == ESCAPE_KEY {
|
||||
set_editing(false);
|
||||
{move || {
|
||||
editing()
|
||||
.then(|| {
|
||||
view! { cx,
|
||||
<input
|
||||
class="edit"
|
||||
class:hidden=move || !(editing)()
|
||||
prop:value=move || todo.title.get()
|
||||
on:focusout=move |ev| save(&event_target_value(&ev))
|
||||
on:keyup=move |ev| {
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
save(&event_target_value(&ev));
|
||||
} else if key_code == ESCAPE_KEY {
|
||||
set_editing(false);
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
}}
|
||||
/>
|
||||
})
|
||||
}
|
||||
})
|
||||
}}
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,19 +7,15 @@ mod yew;
|
||||
|
||||
#[bench]
|
||||
fn leptos_todomvc_ssr(b: &mut Bencher) {
|
||||
use ::leptos::*;
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
use crate::todomvc::leptos::*;
|
||||
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let rendered = view! {
|
||||
cx,
|
||||
<TodoMVC todos=Todos::new(cx)/>
|
||||
}
|
||||
.into_view(cx)
|
||||
.render_to_string(cx);
|
||||
|
||||
assert!(rendered.len() > 1);
|
||||
let html = ::leptos::ssr::render_to_string(|cx| {
|
||||
view! { cx, <TodoMVC todos=Todos::new(cx)/> }
|
||||
});
|
||||
assert!(html.len() > 1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,21 +53,20 @@ fn yew_todomvc_ssr(b: &mut Bencher) {
|
||||
});
|
||||
});
|
||||
}
|
||||
/*
|
||||
|
||||
#[bench]
|
||||
fn leptos_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
b.iter(|| {
|
||||
use self::leptos::*;
|
||||
use ::leptos::*;
|
||||
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let rendered = view! {
|
||||
let html = ::leptos::ssr::render_to_string(|cx| {
|
||||
view! {
|
||||
cx,
|
||||
<TodoMVC todos=Todos::new_with_1000(cx)/>
|
||||
}.into_view(cx).render_to_string(cx);
|
||||
|
||||
assert!(rendered.len() > 1);
|
||||
}
|
||||
});
|
||||
assert!(html.len() > 1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -108,5 +103,4 @@ fn yew_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
assert!(rendered.len() > 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -174,4 +174,4 @@ fn tera_todomvc_1000(b: &mut Bencher) {
|
||||
|
||||
let _ = TERA.render("template.html", &ctx).unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -61,3 +61,19 @@ view! {
|
||||
<input prop:value=a on:input=on_input />
|
||||
}
|
||||
```
|
||||
|
||||
## Build configuration
|
||||
|
||||
### Cargo feature resolution in workspaces
|
||||
|
||||
A new [version](https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions) of Cargo's feature resolver was introduced for the 2021 edition of Rust.
|
||||
For single crate projects it will select a resolver version based on the Rust edition in `Cargo.toml`. As there is no Rust edition present for `Cargo.toml` in a workspace, Cargo will default to the pre 2021 edition resolver.
|
||||
This can cause issues resulting in non WASM compatible code being built for a WASM target. Seeing `mio` failing to build is often a sign that none WASM compatible code is being included in the build.
|
||||
|
||||
The resolver version can be set in the workspace `Cargo.toml` to remedy this issue.
|
||||
|
||||
```toml
|
||||
[workspace]
|
||||
members = ["member1", "member2"]
|
||||
resolver = "2"
|
||||
```
|
||||
|
||||
@@ -60,7 +60,7 @@ Your directory structure should now look something like this
|
||||
leptos_tutorial
|
||||
├── src
|
||||
│ └── main.rs
|
||||
├── Cargo.html
|
||||
├── Cargo.toml
|
||||
├── index.html
|
||||
```
|
||||
|
||||
|
||||
@@ -107,4 +107,6 @@ create_effect(cx, move |prev_value| {
|
||||
|
||||
Every time `count` is updated, this effect wil rerun. This is what allows reactive, fine-grained updates to the DOM.
|
||||
|
||||
<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"></iframe>
|
||||
[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>
|
||||
|
||||
@@ -84,7 +84,7 @@ fn FancyMath(cx: Scope) -> impl IntoView {
|
||||
This kind of “provide a signal in a parent, consume it in a child” should be familiar
|
||||
from the chapter on [parent-child interactions](./view/08_parent_child.md). The same
|
||||
pattern you use to communicate between parents and children works for grandparents and
|
||||
grandchildren, or any ancestors and descendents: in other words, between “global” state
|
||||
grandchildren, or any ancestors and descendants: in other words, between “global” state
|
||||
in the root component of your app and any other components anywhere else in the app.
|
||||
|
||||
Because of the fine-grained nature of updates, this is usually all you need. However,
|
||||
@@ -122,6 +122,7 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
provide_context(cx, state);
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Then child components can access “slices” of that state with fine-grained
|
||||
@@ -168,4 +169,6 @@ somewhere else that only takes `state.name`, clicking the button won’t cause
|
||||
that other slice to update. This allows you to combine the benefits of a top-down
|
||||
data flow and of fine-grained reactive updates.
|
||||
|
||||
<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">
|
||||
[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">
|
||||
|
||||
@@ -21,11 +21,12 @@
|
||||
- [Actions](./async/13_actions.md)
|
||||
- [Responding to Changes with `create_effect`](./14_create_effect.md)
|
||||
- [Global State Management](./15_global_state.md)
|
||||
- [Router]()
|
||||
- [Fundamentals]()
|
||||
- [defining `<Routes/>`]()
|
||||
- [`<A/>`]()
|
||||
- [`<Form/>`]()
|
||||
- [Router](./router/README.md)
|
||||
- [Defining `<Routes/>`](./router/16_routes.md)
|
||||
- [Nested Routing](./router/17_nested_routing.md)
|
||||
- [Params and Queries](./router/18_params_and_queries.md)
|
||||
- [`<A/>`](./router/19_a.md)
|
||||
- [`<Form/>`](./router/20_form.md)
|
||||
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
|
||||
- [Metadata]()
|
||||
- [SSR]()
|
||||
@@ -33,6 +34,9 @@
|
||||
- [`cargo-leptos`]()
|
||||
- [Hydration Footguns]()
|
||||
- [Request/Response]()
|
||||
- [Extractors]()
|
||||
- [Axum]()
|
||||
- [Actix]()
|
||||
- [Headers]()
|
||||
- [Cookies]()
|
||||
- [Server Functions]()
|
||||
@@ -41,3 +45,4 @@
|
||||
- [`<ActionForm/>`s]()
|
||||
- [Turning off WebAssembly]()
|
||||
- [Advanced Reactivity]()
|
||||
- [Appendix: Optimizing WASM Binary Size]()
|
||||
|
||||
@@ -50,4 +50,6 @@ view! { cx,
|
||||
|
||||
Resources also provide a `refetch()` method that allows you to manually reload the data (for example, in response to a button click) and a `loading()` method that returns a `ReadSignal<bool>` indicating whether the resource is currently loading or not.
|
||||
|
||||
<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"></iframe>
|
||||
[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>
|
||||
|
||||
@@ -69,4 +69,6 @@ Every time one of the resources is reloading, the `"Loading..."` fallback will s
|
||||
|
||||
This inversion of the flow of control makes it easier to add or remove individual resources, as you don’t need to handle the matching yourself. It also unlocks some massive performance improvements during server-side rendering, which we’ll talk about during a later chapter.
|
||||
|
||||
<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"></iframe>
|
||||
[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>
|
||||
|
||||
@@ -4,6 +4,8 @@ You’ll notice in the `<Suspense/>` example that if you keep reloading the data
|
||||
|
||||
`<Transition/>` behaves exactly the same as `<Suspense/>`, but instead of falling back every time, it only shows the fallback the first time. On all subsequent loads, it continues showing the old data until the new data are ready. This can be really handy to prevent the flickering effect, and to allow users to continue interacting with your application.
|
||||
|
||||
This example shows how you can create a simple tabbed contact list with `<Transition/>`. When you select a new tab, it continues showing the current contact until the new data laods. This can be a much better user experience than constantly falling back to a loading message.
|
||||
This example shows how you can create a simple tabbed contact list with `<Transition/>`. When you select a new tab, it continues showing the current contact until the new data loads. This can be a much better user experience than constantly falling back to a loading message.
|
||||
|
||||
<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"></iframe>
|
||||
[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>
|
||||
|
||||
@@ -91,4 +91,6 @@ view! { cx,
|
||||
|
||||
Now, there’s a chance this all seems a little over-complicated, or maybe too restricted. I wanted to include actions here, alongside resources, as the missing piece of the puzzle. In a real Leptos app, you’ll actually most often use actions alongside server functions, [`create_server_action`](https://docs.rs/leptos/latest/leptos/fn.create_server_action.html), and the [`<ActionForm/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.ActionForm.html) component to create really powerful progressively-enhanced forms. So if this primitive seems useless to you... Don’t worry! Maybe it will make sense later. (Or check out our [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/todo.rs) example now.)
|
||||
|
||||
<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"></iframe>
|
||||
[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>
|
||||
|
||||
101
docs/book/src/router/16_routes.md
Normal file
101
docs/book/src/router/16_routes.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Defining Routes
|
||||
|
||||
## Getting Started
|
||||
|
||||
It’s easy to get started with the router.
|
||||
|
||||
First things first, make sure you’ve added the `leptos_router` package to your dependencies.
|
||||
|
||||
> It’s important that the router is a separate package from `leptos` itself. This means that everything in the router can be defined in user-land code. If you want to create your own router, or use no router, you’re completely free to do that!
|
||||
|
||||
And import the relevant types from the router, either with something like
|
||||
|
||||
```rust
|
||||
use leptos_router::{Route, RouteProps, Router, RouterProps, Routes, RoutesProps};
|
||||
```
|
||||
|
||||
or simply
|
||||
|
||||
```rust
|
||||
use leptos_router::*;
|
||||
```
|
||||
|
||||
## Providing the `<Router/>`
|
||||
|
||||
Routing behavior is provided by the [`<Router/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Router.html) component. This should usually be somewhere near the root of your application, the rest of the app.
|
||||
|
||||
> You shouldn’t try to use multiple `<Router/>`s in your app. Remember that the router drives global state: if you have multiple routers, which ones decides what to do when the URL changes?
|
||||
|
||||
Let’s start with a simple `<App/>` component using the router:
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<nav>
|
||||
/* ... */
|
||||
</nav>
|
||||
<main>
|
||||
/* ... */
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Defining `<Routes/>`
|
||||
|
||||
The [`<Routes/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Routes.html) component is where you define all the routes to which a user can navigate in your application. Each possible route is defined by a [`<Route/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Route.html) component.
|
||||
|
||||
You should place the `<Routes/>` component at the location within your app where you want routes to be rendered. Everything outside `<Routes/>` will be present on every page, so you can leave things like a navigation bar or menu outside the `<Routes/>`.
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<nav>
|
||||
/* ... */
|
||||
</nav>
|
||||
<main>
|
||||
// all our routes will appear inside <main>
|
||||
<Routes>
|
||||
/* ... */
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Individual routes are defined by providing children to `<Routes/>` with the `<Route/>` component. `<Route/>` takes a `path` and a `view`. When the current location matches `path`, the `view` will be created and displayed.
|
||||
|
||||
The `path` can include
|
||||
|
||||
- a static path (`/users`),
|
||||
- dynamic, named parameters beginning with a colon (`/:id`),
|
||||
- and/or a wildcard beginning with an asterisk (`/user/*any`)
|
||||
|
||||
The `view` is a function that takes a `Scope` and returns a view.
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/" view=|cx| view! { cx, <Home/> }/>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users/> }/>
|
||||
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile/> }/>
|
||||
<Route path="/*any" view=|cx| view! { cx, <NotFound/> }/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
> The router scores each route to see how good a match it is, so you can define your routes in any order.
|
||||
|
||||
Now if you navigate to `/` or to `/users` you’ll get the home page or the `<Users/>`. If you go to `/users/3` or `/blahblah` you’ll get a user profile or your 404 page (`<NotFound/>`). On every navigation, the router determines which `<Route/>` should be matched, and therefore what content should be displayed where the `<Routes/>` component is defined.
|
||||
|
||||
Simple enough?
|
||||
172
docs/book/src/router/17_nested_routing.md
Normal file
172
docs/book/src/router/17_nested_routing.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# Nested Routing
|
||||
|
||||
We just defined the following set of routes:
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/" view=|cx| view! { cx, <Home /> }/>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }/>
|
||||
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
<Route path="/*any" view=|cx| view! { cx, <NotFound /> }/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
There’s a certain amount of duplication here: `/users` and `/users/:id`. This is fine for a small app, but you can probably already tell it won’t scale well. Wouldn’t it be nice if we could nest these routes?
|
||||
|
||||
Well... you can!
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/" view=|cx| view! { cx, <Home /> }/>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
</Route>
|
||||
<Route path="/*any" view=|cx| view! { cx, <NotFound /> }/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
But wait. We’ve just subtly changed what our application does.
|
||||
|
||||
The next section is one of the most important in this entire routing section of the guide. Read it carefully, and feel free to ask questions if there’s anything you don’t understand.
|
||||
|
||||
# Nested Routes as Layout
|
||||
|
||||
Nested routes are a form of layout, not a method of route definition.
|
||||
|
||||
Let me put that another way: The goal of defining nested routes is not primarily to avoid repeating yourself when typing out the paths in your route definitions. It is actually to tell the router to display multiple `<Route/>`s on the page at the same time, side by side.
|
||||
|
||||
Let’s look back at our practical example.
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }/>
|
||||
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
This means:
|
||||
|
||||
- If I go to `/users`, I get the `<Users/>` component.
|
||||
- If I go to `/users/3`, I get the `<UserProfile/>` component (with the parameter `id` set to `3`; more on that later)
|
||||
|
||||
Let’s say I use nested routes instead:
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
This means:
|
||||
|
||||
- If I go to `/users/3`, the path matches two `<Route/>`s: `<Users/>` and `<UserProfile/>`.
|
||||
- If I go to `/users`, the path is not matched.
|
||||
|
||||
I actually need to add a fallback route
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
<Route path="" view=|cx| view! { cx, <NoUser /> }/>
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
Now:
|
||||
|
||||
- If I go to `/users/3`, the path matches `<Users/>` and `<UserProfile/>`.
|
||||
- If I go to `/users`, the path matches `<Users/>` and `<NoUser/>`.
|
||||
|
||||
When I use nested routes, in other words, each **path** can match multiple **routes**: each URL can render the views provided by multiple `<Route/>` components, at the same time, on the same page.
|
||||
|
||||
This may be counter-intuitive, but it’s very powerful, for reasons you’ll hopefully see in a few minutes.
|
||||
|
||||
## Why Nested Routing?
|
||||
|
||||
Why bother with this?
|
||||
|
||||
Most web applications contain levels of navigation that correspond to different parts of the layout. For example, in an email app you might have a URL like `/contacts/greg`, which shows a list of contacts on the left of the screen, and contact details for Greg on the right of the screen. The contact list and the contact details should always appear on the screen at the same time. If there’s no contact selected, maybe you want to show a little instructional text.
|
||||
|
||||
You can easily define this with nested routes
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/contacts" view=|cx| view! { cx, <ContactList/> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <ContactInfo/> }/>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<p>"Select a contact to view more info."</p>
|
||||
}/>
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
You can go even deeper. Say you want to have tabs for each contact’s address, email/phone, and your conversations with them. You can add _another_ set of nested routes inside `:id`:
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/contacts" view=|cx| view! { cx, <ContactList/> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <ContactInfo/> }>
|
||||
<Route path="" view=|cx| view! { cx, <EmailAndPhone/> }/>
|
||||
<Route path="address" view=|cx| view! { cx, <Address/> }/>
|
||||
<Route path="messages" view=|cx| view! { cx, <Messages/> }/>
|
||||
</Route>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<p>"Select a contact to view more info."</p>
|
||||
}/>
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
> The main page of the [Remix website](https://remix.run/), a React framework from the creators of React Router, has a great visual example if you scroll down, with three levels of nested routing: Sales > Invoices > an invoice.
|
||||
|
||||
## `<Outlet/>`
|
||||
|
||||
Parent routes do not automatically render their nested routes. After all, they are just components; they don’t 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:
|
||||
|
||||
- if there is no nested route that has been matched, it shows nothing
|
||||
- if there is a nested route that has been matched, it shows its `view`
|
||||
|
||||
That’s all! But it’s important to know and to remember, because it’s a common source of “Why isn’t this working?” frustration. If you don’t provide an `<Outlet/>`, the nested route won’t be displayed.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn ContactList(cx: Scope) -> impl IntoView {
|
||||
let contacts = todo!();
|
||||
|
||||
view! { cx,
|
||||
<div style="display: flex">
|
||||
// the contact list
|
||||
<For each=contacts
|
||||
key=|contact| contact.id
|
||||
view=|cx, contact| todo!()
|
||||
>
|
||||
// the nested child, if any
|
||||
// don’t forget this!
|
||||
<Outlet/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Nested Routing and Performance
|
||||
|
||||
All of this is nice, conceptually, but again—what’s the big deal?
|
||||
|
||||
Performance.
|
||||
|
||||
In a fine-grained reactive library like Leptos, it’s always important to do the least amount of rendering work you can. Because we’re working with real DOM nodes and not diffing a virtual DOM, we want to “rerender” components as infrequently as possible. Nested routing makes this extremely easy.
|
||||
|
||||
Imagine my contact list example. If I navigate from Greg to Alice to Bob and back to Greg, the contact information needs to change on each navigation. But the `<ContactList/>` should never be rerendered. Not only does this save on rendering performance, it also maintains state in the UI. For example, if I have a search bar at the top of `<ContactList/>`, navigating from Greg to Alice to Bob won’t clear the search.
|
||||
|
||||
In fact, in this case, we don’t even need to rerender the `<Contact/>` component when moving between contacts. The router will just reactively update the `:id` parameter as we navigate, allowing us to make fine-grained updates. As we navigate between contacts, we’ll update single text nodes to change the contact’s name, address, and so on, without doing _any_ additional rerendering.
|
||||
|
||||
> This sandbox includes a couple features (like nested routing) discussed in this section and the previous one, and a couple we’ll cover in the rest of this chapter. The router is such an integrated system that it makes sense to provide a single example, so don’t be surprised if there’s anything you don’t understand.
|
||||
|
||||
[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>
|
||||
79
docs/book/src/router/18_params_and_queries.md
Normal file
79
docs/book/src/router/18_params_and_queries.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Params and Queries
|
||||
|
||||
Static paths are useful for distinguishing between different pages, but almost every application wants to pass data through the URL at some point.
|
||||
|
||||
There are two ways you can do this:
|
||||
|
||||
1. named route **params** like `id` in `/users/:id`
|
||||
2. named route **queries** like `q` in `/search?q=Foo`
|
||||
|
||||
Because of the way URLs are built, you can access the query from _any_ `<Route/>` view. You can access route params from the `<Route/>` that defines them or any of its nested children.
|
||||
|
||||
Accessing params and queries is pretty simple with a couple of hooks:
|
||||
|
||||
- [`use_query`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query.html) or [`use_query_map`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query_map.html)
|
||||
- [`use_params`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_params.html) or [`use_params_map`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query_map.html)
|
||||
|
||||
Each of these comes with a typed option (`use_query` and `use_params`) and an untyped option (`use_query_map` and `use_params_map`).
|
||||
|
||||
The untyped versions hold a simple key-value map. To use the typed versions, derive the [`Params`](https://docs.rs/leptos_router/0.2.3/leptos_router/trait.Params.html) trait on a struct.
|
||||
|
||||
> `Params` is a very lightweight trait to convert a flat key-value map of strings into a struct by applying `FromStr` to each field. Because of the flat structure of route params and URL queries, it’s significantly less flexible than something like `serde`; it also adds much less weight to your binary.
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[derive(Params)]
|
||||
struct ContactParams {
|
||||
id: usize
|
||||
}
|
||||
|
||||
#[derive(Params)]
|
||||
struct ContactSearch {
|
||||
q: String
|
||||
}
|
||||
```
|
||||
|
||||
> Note: The `Params` derive macro is located at `leptos::Params`, and the `Params` trait is at `leptos_router::Params`. If you avoid using glob imports like `use leptos::*;`, make sure you’re importing the right one for the derive macro.
|
||||
|
||||
Now we can use them in a component. Imagine a URL that has both params and a query, like `/contacts/:id?q=Search`.
|
||||
|
||||
The typed versions return `Memo<Result<T>, _>`. It’s a Memo so it reacts to changes in the URL. It’s a `Result` because the params or query need to be parsed from the URL, and may or may not be valid.
|
||||
|
||||
```rust
|
||||
let params = use_params::<ContactParams>(cx);
|
||||
let query = use_query::<ContactSearch>(cx);
|
||||
|
||||
// id: || -> usize
|
||||
let id = move || {
|
||||
params.with(|params| {
|
||||
params
|
||||
.map(|params| params.id)
|
||||
.unwrap_or_default()
|
||||
})
|
||||
};
|
||||
```
|
||||
|
||||
The untyped versions return `Memo<ParamsMap>`. Again, it’s memo to react to changes in the URL. [`ParamsMap`](https://docs.rs/leptos_router/0.2.3/leptos_router/struct.ParamsMap.html) behaves a lot like any other map type, with a `.get()` method that returns `Option<&String>`.
|
||||
|
||||
```rust
|
||||
let params = use_params_map(cx);
|
||||
let query = use_query_map(cx);
|
||||
|
||||
// id: || -> Option<String>
|
||||
let id = move || {
|
||||
params.with(|params| params.get("id").cloned())
|
||||
};
|
||||
```
|
||||
|
||||
This can get a little messy: deriving a signal that wraps an `Option<_>` or `Result<_>` can involve a couple steps. But it’s worth doing this for two reasons:
|
||||
|
||||
1. It’s correct, i.e., it forces you to consider the cases, “What if the user doesn’t pass a value for this query field? What if they pass an invalid value?”
|
||||
2. It’s 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 haven’t 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>
|
||||
23
docs/book/src/router/19_a.md
Normal file
23
docs/book/src/router/19_a.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# The `<A/>` Component
|
||||
|
||||
Client-side navigation works perfectly fine with ordinary HTML `<a>` elements. The router adds a listener that handles every click on a `<a>` element and tries to handle it on the client side, i.e., without doing another round trip to the server to request HTML. This is what enables the snappy “single-page app” navigations you’re probably familiar with from most modern web apps.
|
||||
|
||||
The router will bail out of handling an `<a>` click under a number of situations
|
||||
|
||||
- the click event has had `prevent_default()` called on it
|
||||
- the <kbd>Meta</kbd>, <kbd>Alt</kbd>, <kbd>Ctrl</kbd>, or <kbd>Shift</kbd> keys were held during click
|
||||
- the `<a>` has a `target` or `download` attribute, or `rel="external"`
|
||||
- the link has a different origin from the current location
|
||||
|
||||
In other words, the router will only try to do a client-side navigation when it’s pretty sure it can handle it, and it will upgrade every `<a>` element to get this special behavior.
|
||||
|
||||
The router also provides an [`<A>`](https://docs.rs/leptos_router/latest/leptos_router/fn.A.html) component, which does two additional things:
|
||||
|
||||
1. Correctly resolves relative nested routes. Relative routing with ordinary `<a>` tags can be tricky. For example, if you have a route like `/post/:id`, `<A href="1">` will generate the correct relative route, but `<a href="1">` likely will not (depending on where it appears in your view.) `<A/>` resolves routes relative to the path of the nested route within which it appears.
|
||||
2. Sets the `aria-current` attribute to `page` if this link is the active link (i.e., it’s a link to the page you’re on). This is helpful for accessibility and for styling. For example, if you want to set the link a different color if it’s a link to the page you’re currently on, you can match this attribute with a CSS selector.
|
||||
|
||||
> Once again, this is the same example. Check out the relative `<A/>` components, and take a look at the CSS in `index.html` to see the ARIA-based styling.
|
||||
|
||||
[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>
|
||||
69
docs/book/src/router/20_form.md
Normal file
69
docs/book/src/router/20_form.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# The `<Form/>` Component
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
The router provides a [`<Form>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Form.html) component, which works like the HTML `<form>` element, but uses client-side navigations instead of full page reloads. `<Form/>` works with both `GET` and `POST` requests. With `method="GET"`, it will navigate to the URL encoded in the form data. With `method="POST"` it will make a `POST` request and handle the server’s response.
|
||||
|
||||
`<Form/>` provides the basis for some components like `<ActionForm/>` and `<MultiActionForm/>` that we’ll see in later chapters. But it also enables some powerful patterns of its own.
|
||||
|
||||
For example, imagine that you want to create a search field that updates search results in real time as the user searches, without a page reload, but that also stores the search in the URL so a user can copy and paste it to share results with someone else.
|
||||
|
||||
It turns out that the patterns we’ve learned so far make this easy to implement.
|
||||
|
||||
```rust
|
||||
async fn fetch_results() {
|
||||
// some async function to fetch our search results
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Search(cx: Scope) -> impl IntoView {
|
||||
#[component]
|
||||
pub fn FormExample(cx: Scope) -> impl IntoView {
|
||||
// reactive access to URL query strings
|
||||
let query = use_query_map(cx);
|
||||
// search stored as ?q=
|
||||
let search = move || query().get("q").cloned().unwrap_or_default();
|
||||
// a resource driven by the search string
|
||||
let search_results = create_resource(cx, search, fetch_results);
|
||||
|
||||
view! { cx,
|
||||
<Form method="GET" action="">
|
||||
<input type="search" name="search" value=search/>
|
||||
<input type="submit"/>
|
||||
</Form>
|
||||
<Transition fallback=move || ()>
|
||||
/* render search results */
|
||||
</Transition>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Whenever you click `Submit`, the `<Form/>` will “navigate” to `?q={search}`. But because this navigation is done on the client side, there’s no page flicker or reload. The URL query string changes, which triggers `search` to update. Because `search` is the source signal for the `search_results` resource, this triggers `search_results` to reload its resource. The `<Transition/>` continues displaying the current search results until the new ones have loaded. When they are complete, it switches to displaying the new result.
|
||||
|
||||
This is a great pattern. The data flow is extremely clear: all data flows from the URL to the resource into the UI. The current state of the application is stored in the URL, which means you can refresh the page or text the link to a friend and it will show exactly what you’re expecting. And once we introduce server rendering, this pattern will prove to be really fault-tolerant, too: because it uses a `<form>` element and URLs under the hood, it actually works really well without even loading your WASM on the client.
|
||||
|
||||
We can actually take it a step further and do something kind of clever:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<Form method="GET" action="">
|
||||
<input type="search" name="search" value=search
|
||||
oninput="this.form.requestSubmit()"
|
||||
/>
|
||||
</Form>
|
||||
}
|
||||
```
|
||||
|
||||
You’ll notice that this version drops the `Submit` button. Instead, we add an `oninput` attribute to the input. Note that this is _not_ `on:input`, which would listen for the `input` event and run some Rust code. Without the colon, `oninput` is the plain HTML attribute. So the string is actually a JavaScript string. `this.form` gives us the form the input is attached to. `requestSubmit()` fires the `submit` event on the `<form>`, which is caught by `<Form/>` just as if we had clicked a `Submit` button. Now the form will “navigate” on every keystroke or input to keep the URL (and therefore the search) perfectly in sync with the user’s input as they type.
|
||||
|
||||
[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>
|
||||
23
docs/book/src/router/README.md
Normal file
23
docs/book/src/router/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Routing
|
||||
|
||||
## The Basics
|
||||
|
||||
Routing drives most websites. A router is the answer to the question, “Given this URL, what should appear on the page?”
|
||||
|
||||
A URL consists of many parts. For example, the URL `https://leptos.dev/blog/search?q=Search#results` consists of
|
||||
|
||||
- a _scheme_: `https`
|
||||
- a _domain_: `leptos.dev`
|
||||
- a **path**: `/blog/search`
|
||||
- a **query** (or **search**): `?q=Search`
|
||||
- a _hash_: `#results`
|
||||
|
||||
The Leptos Router works with the path and query (`/blog/search?q=Search`). Given this piece of the URL, what should the app render on the page?
|
||||
|
||||
## The Philosophy
|
||||
|
||||
In most cases, the path should drive what is displayed on the page. From the user’s perspective, for most applications, most major changes in the state of the app should be reflected in the URL. If you copy and paste the URL and open it in another tab, you should find yourself more or less in the same place.
|
||||
|
||||
In this sense, the router is really at the heart of the global state management for your application. More than anything else, it drives what is displayed on the page.
|
||||
|
||||
The router handles most of this work for you by mapping the current location to particular components.
|
||||
@@ -107,27 +107,28 @@ fn clear() {
|
||||
test_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
We’ll use some manual DOM operations to grab the `<div>` that wraps
|
||||
the whole component, as well as the `clear` button.
|
||||
|
||||
```rust
|
||||
// now we extract the buttons by iterating over the DOM
|
||||
// this would be easier if they had IDs
|
||||
let div = test_wrapper.query_selector("div").unwrap().unwrap();
|
||||
let clear = test_wrapper
|
||||
.query_selector("button")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::HtmlElement>();
|
||||
// now we extract the buttons by iterating over the DOM
|
||||
// this would be easier if they had IDs
|
||||
let div = test_wrapper.query_selector("div").unwrap().unwrap();
|
||||
let clear = test_wrapper
|
||||
.query_selector("button")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::HtmlElement>();
|
||||
```
|
||||
|
||||
Now we can use ordinary DOM APIs to simulate user interaction.
|
||||
|
||||
```rust
|
||||
// now let's click the `clear` button
|
||||
clear.click();
|
||||
// now let's click the `clear` button
|
||||
clear.click();
|
||||
```
|
||||
|
||||
You can test individual DOM element attributes or text node values. Sometimes
|
||||
@@ -135,27 +136,27 @@ I like to test the whole view at once. We can do this by testing the element’s
|
||||
`outerHTML` against our expectations.
|
||||
|
||||
```rust
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
// here we spawn a mini reactive system to render the test case
|
||||
run_scope(create_runtime(), |cx| {
|
||||
// it's as if we're creating it with a value of 0, right?
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
// here we spawn a mini reactive system to render the test case
|
||||
run_scope(create_runtime(), |cx| {
|
||||
// it's as if we're creating it with a value of 0, right?
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
|
||||
// we can remove the event listeners because they're not rendered to HTML
|
||||
view! { cx,
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
<button>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
// the view returned an HtmlElement<Div>, which is a smart pointer for
|
||||
// a DOM element. So we can still just call .outer_html()
|
||||
.outer_html()
|
||||
})
|
||||
);
|
||||
// we can remove the event listeners because they're not rendered to HTML
|
||||
view! { cx,
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
<button>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
// the view returned an HtmlElement<Div>, which is a smart pointer for
|
||||
// a DOM element. So we can still just call .outer_html()
|
||||
.outer_html()
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
That test involved us manually replicating the `view` that’s inside the component.
|
||||
@@ -164,15 +165,14 @@ with the initial value `0`. This is where our wrapping element comes in: I’ll
|
||||
the wrapper’s `innerHTML` against another comparison case.
|
||||
|
||||
```rust
|
||||
assert_eq!(test_wrapper.inner_html(), {
|
||||
let comparison_wrapper = document.create_element("section").unwrap();
|
||||
leptos::mount_to(
|
||||
comparison_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
|
||||
);
|
||||
comparison_wrapper.inner_html()
|
||||
});
|
||||
}
|
||||
assert_eq!(test_wrapper.inner_html(), {
|
||||
let comparison_wrapper = document.create_element("section").unwrap();
|
||||
leptos::mount_to(
|
||||
comparison_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
|
||||
);
|
||||
comparison_wrapper.inner_html()
|
||||
});
|
||||
```
|
||||
|
||||
This is only a very limited introduction to testing. But I hope it’s useful as you begin to build applications.
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# A Basic Component
|
||||
|
||||
That “Hello, world!” was a *very* simple example. Let’s move on to something a
|
||||
That “Hello, world!” was a _very_ simple example. Let’s move on to something a
|
||||
little more like an ordinary app.
|
||||
|
||||
First, let’s edit the `main` function so that, instead of rendering the whole
|
||||
app, it just renders an `<App/>` component. Components are the basic unit of
|
||||
composition and design in most web frameworks, and Leptos is no exception.
|
||||
Conceptually, they are similar to HTML elements: they represent a section of the
|
||||
DOM, with self-contained, defined behavior. Unlike HTML elements, they are in
|
||||
`PascalCase`, so most Leptos applications will start with something like an
|
||||
composition and design in most web frameworks, and Leptos is no exception.
|
||||
Conceptually, they are similar to HTML elements: they represent a section of the
|
||||
DOM, with self-contained, defined behavior. Unlike HTML elements, they are in
|
||||
`PascalCase`, so most Leptos applications will start with something like an
|
||||
`<App/>` component.
|
||||
|
||||
```rust
|
||||
@@ -39,11 +39,12 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
```
|
||||
|
||||
## The Component Signature
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
```
|
||||
|
||||
Like all component definitions, this begins with the [`#[component]`](https://docs.rs/leptos/latest/leptos/attr.component.html) macro. `#[component]` annotates a function so it can be
|
||||
Like all component definitions, this begins with the [`#[component]`](https://docs.rs/leptos/latest/leptos/attr.component.html) macro. `#[component]` annotates a function so it can be
|
||||
used as a component in your Leptos application. We’ll see some of the other features of
|
||||
this macro in a couple chapters.
|
||||
|
||||
@@ -52,6 +53,7 @@ fn App(cx: Scope) -> impl IntoView
|
||||
```
|
||||
|
||||
Every component is a function with the following characteristics
|
||||
|
||||
1. It takes a reactive [`Scope`](https://docs.rs/leptos/latest/leptos/struct.Scope.html)
|
||||
as its first argument. This `Scope` is our entrypoint into the reactive system.
|
||||
By convention, it’s usually named `cx`.
|
||||
@@ -60,7 +62,8 @@ Every component is a function with the following characteristics
|
||||
anything you could return from a Leptos `view`.
|
||||
|
||||
## The Component Body
|
||||
The body of the component function is a set-up function that runs once, not a
|
||||
|
||||
The body of the component function is a set-up function that runs once, not a
|
||||
render function that reruns multiple times. You’ll typically use it to create a
|
||||
few reactive variables, define any side effects that run in response to those values
|
||||
changing, and describe the user interface.
|
||||
@@ -68,16 +71,17 @@ changing, and describe the user interface.
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
```
|
||||
|
||||
[`create_signal`](https://docs.rs/leptos/latest/leptos/fn.create_signal.html)
|
||||
creates a signal, the basic unit of reactive change and state management in Leptos.
|
||||
This returns a `(getter, setter)` tuple. To access the current value, you’ll
|
||||
use `count.get()` (or, on `nightly` Rust, the shorthand `count()`). To set the
|
||||
This returns a `(getter, setter)` tuple. To access the current value, you’ll
|
||||
use `count.get()` (or, on `nightly` Rust, the shorthand `count()`). To set the
|
||||
current value, you’ll call `set_count.set(...)` (or `set_count(...)`).
|
||||
|
||||
> `.get()` clones the value and `.set()` overwrites it. In many cases, it’s more
|
||||
efficient to use `.with()` or `.update()`; check out the docs for [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) and [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) if you’d like to learn more about those trade-offs at this point.
|
||||
> `.get()` clones the value and `.set()` overwrites it. In many cases, it’s more
|
||||
> efficient to use `.with()` or `.update()`; check out the docs for [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) and [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) if you’d like to learn more about those trade-offs at this point.
|
||||
|
||||
## The View
|
||||
## The View
|
||||
|
||||
Leptos defines user interfaces using a JSX-like format via the [`view`](https://docs.rs/leptos/latest/leptos/macro.view.html) macro.
|
||||
|
||||
@@ -100,25 +104,28 @@ view! { cx,
|
||||
This should mostly be easy to understand: it looks like HTML, with a special
|
||||
`on:click` to define a `click` event listener, a text node that’s formatted like
|
||||
a Rust string, and then...
|
||||
|
||||
```rust
|
||||
{move || count.get()}
|
||||
```
|
||||
|
||||
whatever that is.
|
||||
|
||||
People sometimes joke that they use more closures in their first Leptos application
|
||||
than they’ve ever used in their lives. And fair enough. Basically, passing a function
|
||||
People sometimes joke that they use more closures in their first Leptos application
|
||||
than they’ve ever used in their lives. And fair enough. Basically, passing a function
|
||||
into the view tells the framework: “Hey, this is something that might change.”
|
||||
|
||||
When we click the button and call `set_count`, the `count` signal is updated. This
|
||||
`move || count.get()` closure, whose value depends on the value of `count`, reruns,
|
||||
and the framework makes a targeted update to that one specific text node, touching
|
||||
When we click the button and call `set_count`, the `count` signal is updated. This
|
||||
`move || count.get()` closure, whose value depends on the value of `count`, reruns,
|
||||
and the framework makes a targeted update to that one specific text node, touching
|
||||
nothing else in your application. This is what allows for extremely efficient updates
|
||||
to the DOM.
|
||||
|
||||
Now, if you have Clippy on—or if you have a particularly sharp eye—you might notice
|
||||
that this closure is redundant, at least if you’re in `nightly` Rust. If you’re using
|
||||
that this closure is redundant, at least if you’re in `nightly` Rust. If you’re using
|
||||
Leptos with `nightly` Rust, signals are already functions, so the closure is unnecessary.
|
||||
As a result, you can write a simpler view:
|
||||
As a result, you can write a simpler view:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<button /* ... */>
|
||||
@@ -129,15 +136,17 @@ view! { cx,
|
||||
}
|
||||
```
|
||||
|
||||
Remember—and this is *very important*—only functions are reactive. This means that
|
||||
`{count}` and `{count()}` do very different things in your view. `{count}` passes
|
||||
Remember—and this is _very important_—only functions are reactive. This means that
|
||||
`{count}` and `{count()}` do very different things in your view. `{count}` passes
|
||||
in a function, telling the framework to update the view every time `count` changes.
|
||||
`{count()}` access the value of `count` once, and passes an `i32` into the view,
|
||||
`{count()}` access the value of `count` once, and passes an `i32` into the view,
|
||||
rendering it once, unreactively. You can see the difference in the CodeSandbox below!
|
||||
|
||||
> Throughout this tutorial, we’ll use CodeSandbox to show interactive examples. To
|
||||
show the browser in the sandbox, you may need to click `Add DevTools >
|
||||
Other Previews > 8080.` Hover over any of the variables to show Rust-Analyzer details
|
||||
and docs for what’s going on. Feel free to fork the examples to play with them yourself!
|
||||
> Throughout this tutorial, we’ll use CodeSandbox to show interactive examples. To
|
||||
> show the browser in the sandbox, you may need to click `Add DevTools >
|
||||
Other Previews > 8080.` Hover over any of the variables to show Rust-Analyzer details
|
||||
> and docs for what’s going on. Feel free to fork the examples to play with them yourself!
|
||||
|
||||
<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"></iframe>
|
||||
[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>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# `view`: Dynamic Attributes and Classes
|
||||
|
||||
So far we’ve seen how to use the `view` macro to create event listeners and to
|
||||
So far we’ve seen how to use the `view` macro to create event listeners and to
|
||||
create dynamic text by passing a function (such as a signal) into the view.
|
||||
|
||||
But of course there are other things you might want to update in your user interface.
|
||||
In this section, we’ll look at how to update attributes and classes dynamically,
|
||||
In this section, we’ll look at how to update attributes and classes dynamically,
|
||||
and we’ll introduce the concept of a **derived signal**.
|
||||
|
||||
Let’s start with a simple component that should be familiar: click a button to
|
||||
Let’s start with a simple component that should be familiar: click a button to
|
||||
increment a counter.
|
||||
|
||||
```rust
|
||||
@@ -20,6 +20,12 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
}
|
||||
>
|
||||
"Click me: "
|
||||
{move || count()}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
So far, this is just the example from the last chapter.
|
||||
@@ -28,27 +34,31 @@ So far, this is just the example from the last chapter.
|
||||
|
||||
Now let’s say I’d like to update the list of CSS classes on this element dynamically.
|
||||
For example, let’s say I want to add the class `red` when the count is odd. I can
|
||||
do this using the `class:` syntax.
|
||||
do this using the `class:` syntax.
|
||||
|
||||
```rust
|
||||
class:red=move || count() & 1 == 1
|
||||
class:red=move || count() % 2 == 1
|
||||
```
|
||||
|
||||
`class:` attributes take
|
||||
|
||||
1. the class name, following the colon (`red`)
|
||||
2. a value, which can be a `bool` or a function that returns a `bool`
|
||||
|
||||
When the value is `true`, the class is added. When the value is `false`, the class
|
||||
is removed. And if the value is a function that accesses a signal, the class will
|
||||
is removed. And if the value is a function that accesses a signal, the class will
|
||||
reactively update when the signal changes.
|
||||
|
||||
Now every time I click the button, the text should toggle between red and black as
|
||||
Now every time I click the button, the text should toggle between red and black as
|
||||
the number switches between even and odd.
|
||||
|
||||
## Dynamic Attributes
|
||||
|
||||
The same applies to plain attributes. Passing a plain string or primitive value to
|
||||
an attribute gives it a static value. Passing a function (including a signal) to
|
||||
an attribute causes it to update its value reactively. Let’s add another element
|
||||
an attribute causes it to update its value reactively. Let’s add another element
|
||||
to our view:
|
||||
|
||||
```rust
|
||||
<progress
|
||||
max="50"
|
||||
@@ -57,17 +67,18 @@ to our view:
|
||||
/>
|
||||
```
|
||||
|
||||
Now every time we set the count, not only will the `class` of the `<button>` be
|
||||
toggled, but the `value` of the `<progress>` bar will increase, which means that
|
||||
Now every time we set the count, not only will the `class` of the `<button>` be
|
||||
toggled, but the `value` of the `<progress>` bar will increase, which means that
|
||||
our progress bar will move forward.
|
||||
|
||||
## Derived Signals
|
||||
## Derived Signals
|
||||
|
||||
Let’s go one layer deeper, just for fun.
|
||||
|
||||
You already know that we create reactive interfaces just by passing functions into
|
||||
You already know that we create reactive interfaces just by passing functions into
|
||||
the `view`. This means that we can easily change our progress bar. For example,
|
||||
suppose we want it to move twice as fast:
|
||||
|
||||
```rust
|
||||
<progress
|
||||
max="50"
|
||||
@@ -77,28 +88,31 @@ suppose we want it to move twice as fast:
|
||||
|
||||
But imagine we want to reuse that calculation in more than one place. You can do this
|
||||
using a **derived signal**: a closure that accesses a signal.
|
||||
|
||||
```rust
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
/* insert the rest of the view */
|
||||
/* insert the rest of the view */
|
||||
<progress
|
||||
max="50"
|
||||
// we use it once here
|
||||
value=double_count
|
||||
/>
|
||||
<p>
|
||||
"Double Count: "
|
||||
"Double Count: "
|
||||
// and again here
|
||||
{double_count}
|
||||
</p>
|
||||
```
|
||||
|
||||
Derived signals let you create reactive computed values that can be used in multiple
|
||||
Derived signals let you create reactive computed values that can be used in multiple
|
||||
places in your application with minimal overhead.
|
||||
|
||||
> Note: Using a derived signal like this means that the calculation runs once per
|
||||
signal change per place we access `double_count`; in other words, twice. This is a
|
||||
very cheap calculation, so that’s fine. We’ll look at memos in a later chapter, which
|
||||
are designed to solve this problem for expensive calculations.
|
||||
> Note: Using a derived signal like this means that the calculation runs once per
|
||||
> signal change per place we access `double_count`; in other words, twice. This is a
|
||||
> very cheap calculation, so that’s fine. We’ll look at memos in a later chapter, which
|
||||
> are designed to solve this problem for expensive calculations.
|
||||
|
||||
<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"></iframe>
|
||||
[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>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Components and Props
|
||||
|
||||
So far, we’ve been building our whole application in a single component. This
|
||||
is fine for really tiny examples, but in any real application you’ll need to
|
||||
break the user interface out into multiple components, so you can break your
|
||||
So far, we’ve been building our whole application in a single component. This
|
||||
is fine for really tiny examples, but in any real application you’ll need to
|
||||
break the user interface out into multiple components, so you can break your
|
||||
interface down into smaller, reusable, composable chunks.
|
||||
|
||||
Let’s take our progress bar example. Imagine that you want two progress bars
|
||||
Let’s take our progress bar example. Imagine that you want two progress bars
|
||||
instead of one: one that advances one tick per click, one that advances two ticks
|
||||
per click.
|
||||
|
||||
@@ -15,7 +15,7 @@ You _could_ do this by just creating two `<progress>` elements:
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
view! {
|
||||
view! {
|
||||
<progress
|
||||
max="50"
|
||||
value=count
|
||||
@@ -24,10 +24,11 @@ view! {
|
||||
max="50"
|
||||
value=double_count
|
||||
/>
|
||||
}
|
||||
```
|
||||
|
||||
But of course, this doesn’t scale very well. If you want to add a third progress
|
||||
bar, you need to add this code another time. And if you want to edit anything
|
||||
bar, you need to add this code another time. And if you want to edit anything
|
||||
about it, you need to edit it in triplicate.
|
||||
|
||||
Instead, let’s create a `<ProgressBar/>` component.
|
||||
@@ -47,15 +48,15 @@ fn ProgressBar(
|
||||
}
|
||||
```
|
||||
|
||||
There’s just one problem: `progress` is not defined. Where should it come from?
|
||||
When we were defining everything manually, we just used the local variable names.
|
||||
There’s just one problem: `progress` is not defined. Where should it come from?
|
||||
When we were defining everything manually, we just used the local variable names.
|
||||
Now we need some way to pass an argument into the component.
|
||||
|
||||
## Component Props
|
||||
## Component Props
|
||||
|
||||
We do this using component properties, or “props.” If you’ve used another frontend
|
||||
framework, this is probably a familiar idea. Basically, properties are to components
|
||||
as attributes are to HTML elements: they let you pass additional information into
|
||||
framework, this is probably a familiar idea. Basically, properties are to components
|
||||
as attributes are to HTML elements: they let you pass additional information into
|
||||
the component.
|
||||
|
||||
In Leptos, you define props by giving additional arguments to the component function.
|
||||
@@ -69,7 +70,7 @@ fn ProgressBar(
|
||||
view! { cx,
|
||||
<progress
|
||||
max="50"
|
||||
// now this works
|
||||
// now this works
|
||||
value=progress
|
||||
/>
|
||||
}
|
||||
@@ -92,41 +93,45 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
```
|
||||
|
||||
Using a component in the view looks a lot like using an HTML element. You’ll
|
||||
notice that you can easily tell the difference between an element and a component
|
||||
because components always have `PascalCase` names. You pass the `progress` prop
|
||||
Using a component in the view looks a lot like using an HTML element. You’ll
|
||||
notice that you can easily tell the difference between an element and a component
|
||||
because components always have `PascalCase` names. You pass the `progress` prop
|
||||
in as if it were an HTML element attribute. Simple.
|
||||
|
||||
> ### Important Note
|
||||
> For every `Component`, Leptos generates a corresponding `ComponentProps` type. This
|
||||
is what allows us to have named props, when Rust does not have named function parameters.
|
||||
If you’re defining a component in one module and importing it into another, make
|
||||
sure you include this `ComponentProps` type:
|
||||
> ### Important Note
|
||||
>
|
||||
> For every `Component`, Leptos generates a corresponding `ComponentProps` type. This
|
||||
> is what allows us to have named props, when Rust does not have named function parameters.
|
||||
> If you’re defining a component in one module and importing it into another, make
|
||||
> sure you include this `ComponentProps` type:
|
||||
>
|
||||
> `use progress_bar::{ProgressBar, ProgressBarProps};`
|
||||
>
|
||||
> **Note**: This is still true as of `0.2.5`, but the requirement has been removed on `main`
|
||||
> and will not apply to later versions.
|
||||
|
||||
### Reactive and Static Props
|
||||
|
||||
You’ll notice that throughout this example, `progress` takes a reactive
|
||||
You’ll notice that throughout this example, `progress` takes a reactive
|
||||
`ReadSignal<i32>`, and not a plain `i32`. This is **very important**.
|
||||
|
||||
Component props have no special meaning attached to them. A component is simply
|
||||
a function that runs once to set up the user interface. The only way to tell the
|
||||
interface to respond to changing is to pass it a signal type. So if you have a
|
||||
component property that will change over time, like our `progress`, it should
|
||||
Component props have no special meaning attached to them. A component is simply
|
||||
a function that runs once to set up the user interface. The only way to tell the
|
||||
interface to respond to changing is to pass it a signal type. So if you have a
|
||||
component property that will change over time, like our `progress`, it should
|
||||
be a signal.
|
||||
|
||||
### `optional` Props
|
||||
### `optional` Props
|
||||
|
||||
Right now the `max` setting is hard-coded. Let’s take that as a prop too. But
|
||||
let’s add a catch: let’s make this prop optional by annotating the particular
|
||||
Right now the `max` setting is hard-coded. Let’s take that as a prop too. But
|
||||
let’s add a catch: let’s make this prop optional by annotating the particular
|
||||
argument to the component function with `#[prop(optional)]`.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
// mark this prop optional
|
||||
// mark this prop optional
|
||||
// you can specify it or not when you use <ProgressBar/>
|
||||
#[prop(optional)]
|
||||
max: u16,
|
||||
@@ -142,7 +147,7 @@ fn ProgressBar(
|
||||
```
|
||||
|
||||
Now, we can use `<ProgressBar max=50 value=count/>`, or we can omit `max`
|
||||
to use the default value (i.e., `<ProgressBar value=count/>`). The default value
|
||||
to use the default value (i.e., `<ProgressBar value=count/>`). The default value
|
||||
on an `optional` is its `Default::default()` value, which for a `u16` is going to
|
||||
be `0`. In the case of a progress bar, a max value of `0` is not very useful.
|
||||
|
||||
@@ -187,20 +192,20 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
"Click me"
|
||||
</button>
|
||||
<ProgressBar progress=count/>
|
||||
// add a second progress bar
|
||||
// add a second progress bar
|
||||
<ProgressBar progress=double_count/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Hm... this won’t compile. It should be pretty easy to understand why: we’ve declared
|
||||
that the `progress` prop takes `ReadSignal<i32>`, and `double_count` is not
|
||||
`ReadSignal<i32>`. As rust-analyzer will tell you, its type is `|| -> i32`, i.e.,
|
||||
that the `progress` prop takes `ReadSignal<i32>`, and `double_count` is not
|
||||
`ReadSignal<i32>`. As rust-analyzer will tell you, its type is `|| -> i32`, i.e.,
|
||||
it’s a closure that returns an `i32`.
|
||||
|
||||
There are a couple ways to handle this. One would be to say: “Well, I know that
|
||||
a `ReadSignal` is a function, and I know that a closure is a function; maybe I
|
||||
could just take any function?” If you’re savvy, you may know that both these
|
||||
There are a couple ways to handle this. One would be to say: “Well, I know that
|
||||
a `ReadSignal` is a function, and I know that a closure is a function; maybe I
|
||||
could just take any function?” If you’re savvy, you may know that both these
|
||||
implement the trait `Fn() -> i32`. So you could use a generic component:
|
||||
|
||||
```rust
|
||||
@@ -210,8 +215,8 @@ fn ProgressBar<F>(
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
progress: F
|
||||
) -> impl IntoView
|
||||
where
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn() -> i32 + 'static,
|
||||
{
|
||||
view! { cx,
|
||||
@@ -223,27 +228,26 @@ where
|
||||
}
|
||||
```
|
||||
|
||||
This is a perfectly reasonable way to write this component: `progress` now takes
|
||||
This is a perfectly reasonable way to write this component: `progress` now takes
|
||||
any value that implements this `Fn()` trait.
|
||||
|
||||
> Note that generic component props _cannot_ be specified inline (as `<F: Fn() -> i32>`)
|
||||
or as `progress: impl Fn() -> i32 + 'static,`, in part because they’re actually used to generate
|
||||
a `struct ProgressBarProps`, and struct fields cannot be `impl` types.
|
||||
> or as `progress: impl Fn() -> i32 + 'static,`, in part because they’re actually used to generate
|
||||
> a `struct ProgressBarProps`, and struct fields cannot be `impl` types.
|
||||
|
||||
### `into` Props
|
||||
### `into` Props
|
||||
|
||||
There’s one more way we could implement this, and it would be to use `#[prop(into)]`.
|
||||
There’s one more way we could implement this, and it would be to use `#[prop(into)]`.
|
||||
This attribute automatically calls `.into()` on the values you pass as props,
|
||||
which allows you to easily pass props with different values.
|
||||
|
||||
In this case, it’s helpful to know about the
|
||||
In this case, it’s helpful to know about the
|
||||
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html) type. `Signal`
|
||||
is a enumerated type that represents any kind of readable reactive signal. It can
|
||||
be useful when defining APIs for components you’ll want to reuse while passing
|
||||
different sorts of signals. The [`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) type is useful when you want to be able to take either a static or
|
||||
is an enumerated type that represents any kind of readable reactive signal. It can
|
||||
be useful when defining APIs for components you’ll want to reuse while passing
|
||||
different sorts of signals. The [`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) type is useful when you want to be able to take either a static or
|
||||
reactive value.
|
||||
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
@@ -252,7 +256,7 @@ fn ProgressBar(
|
||||
max: u16,
|
||||
#[prop(into)]
|
||||
progress: Signal<i32>
|
||||
) -> impl IntoView
|
||||
) -> impl IntoView
|
||||
{
|
||||
view! { cx,
|
||||
<progress
|
||||
@@ -281,12 +285,12 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
|
||||
## Documenting Components
|
||||
|
||||
This is one of the least essential but most important sections of this book.
|
||||
It’s not strictly necessary to document your components and their props. It may
|
||||
be very important, depending on the size of your team and your app. But it’s very
|
||||
This is one of the least essential but most important sections of this book.
|
||||
It’s not strictly necessary to document your components and their props. It may
|
||||
be very important, depending on the size of your team and your app. But it’s very
|
||||
easy, and bears immediate fruit.
|
||||
|
||||
To document a component and its props, you can simply add doc comments on the
|
||||
To document a component and its props, you can simply add doc comments on the
|
||||
component function, and each one of the props:
|
||||
|
||||
```rust
|
||||
@@ -309,9 +313,11 @@ That’s all you need to do. These behave like ordinary Rust doc comments, excep
|
||||
that you can document individual component props, which can’t be done with Rust
|
||||
function arguments.
|
||||
|
||||
This will automatically generate documentation for your component, its `Props`
|
||||
type, and each of the fields used to add props. It can be a little hard to
|
||||
understand how powerful this is until you hover over the component name or props
|
||||
This will automatically generate documentation for your component, its `Props`
|
||||
type, and each of the fields used to add props. It can be a little hard to
|
||||
understand how powerful this is until you hover over the component name or props
|
||||
and see the power of the `#[component]` macro combined with rust-analyzer here.
|
||||
|
||||
<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"></iframe>
|
||||
[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>
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
# Iteration
|
||||
|
||||
Whether you’re listing todos, displaying a table, or showing product images,
|
||||
Whether you’re listing todos, displaying a table, or showing product images,
|
||||
iterating over a list of items is a common task in web applications. Reconciling
|
||||
the differences between changing sets of items can also be one of the trickiest
|
||||
tasks for a framework to handle well.
|
||||
|
||||
Leptos supports to two different patterns for iterating over items:
|
||||
|
||||
1. For static views: `Vec<_>`
|
||||
2. For dynamic lists: `<For/>`
|
||||
|
||||
## Static Views with `Vec<_>`
|
||||
|
||||
Sometimes you need to show an item repeatedly, but the list you’re drawing from
|
||||
does not often change. In this case, it’s important to know that you can insert
|
||||
Sometimes you need to show an item repeatedly, but the list you’re drawing from
|
||||
does not often change. In this case, it’s important to know that you can insert
|
||||
any `Vec<IV> where IV: IntoView` into your view. In other words, if you can render
|
||||
`T`, you can render `Vec<T>`.
|
||||
|
||||
@@ -58,31 +59,34 @@ view! { cx,
|
||||
}
|
||||
```
|
||||
|
||||
You _can_ render a `Fn() -> Vec<_>` reactively as well. But note that every time
|
||||
You _can_ render a `Fn() -> Vec<_>` reactively as well. But note that every time
|
||||
it changes, this will rerender every item in the list. This is quite inefficient!
|
||||
Fortunately, there’s a better way.
|
||||
|
||||
## Dynamic Rendering with the `<For/>` Component
|
||||
|
||||
The [`<For/>`](https://docs.rs/leptos/latest/leptos/fn.For.html) component is a
|
||||
The [`<For/>`](https://docs.rs/leptos/latest/leptos/fn.For.html) component is a
|
||||
keyed dynamic list. It takes three props:
|
||||
|
||||
- `each`: a function (such as a signal) that returns the items `T` to be iterated over
|
||||
- `key`: a key function that takes `&T` and returns a stable, unique key or ID
|
||||
- `view`: renders each `T` into a view
|
||||
- `view`: renders each `T` into a view
|
||||
|
||||
`key` is, well, the key. You can add, remove, and move items within the list. As
|
||||
long as each item’s key is stable over time, the framework does not need to rerender
|
||||
any of the items, unless they are new additions, and it can very efficiently add,
|
||||
remove, and move items as they change. This allows for extremely efficient updates
|
||||
remove, and move items as they change. This allows for extremely efficient updates
|
||||
to the list as it changes, with minimal additional work.
|
||||
|
||||
Creating a good `key` can be a little tricky. You generally do _not_ want to use
|
||||
an index for this purpose, as it is not stable—if you remove or move items, their
|
||||
Creating a good `key` can be a little tricky. You generally do _not_ want to use
|
||||
an index for this purpose, as it is not stable—if you remove or move items, their
|
||||
indices change.
|
||||
|
||||
But it’s a great idea to do something like generating a unique ID for each row as
|
||||
But it’s a great idea to do something like generating a unique ID for each row as
|
||||
it is generated, and using that as an ID for the key function.
|
||||
|
||||
Check out the `<DynamicList/>` component below for an example.
|
||||
|
||||
<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"></iframe>
|
||||
[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>
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
# Forms and Inputs
|
||||
|
||||
Forms and form inputs are an important part of interactive apps. There are two
|
||||
Forms and form inputs are an important part of interactive apps. There are two
|
||||
basic patterns for interacting with inputs in Leptos, which you may recognize
|
||||
if you’re familiar with React, SolidJS, or a similar framework: using **controlled**
|
||||
or **uncontrolled** inputs.
|
||||
|
||||
## Controlled Inputs
|
||||
|
||||
In a "controlled input," the framework controls the state of the input
|
||||
element. On every `input` event, it updates a local signal that holds the current
|
||||
In a "controlled input," the framework controls the state of the input
|
||||
element. On every `input` event, it updates a local signal that holds the current
|
||||
state, which in turn updates the `value` prop of the input.
|
||||
|
||||
There are two important things to remember:
|
||||
1. The `input` event fires on (almost) every change to the element, while the
|
||||
`change` event fires (more or less) when you unfocus the input. You probably
|
||||
|
||||
1. The `input` event fires on (almost) every change to the element, while the
|
||||
`change` event fires (more or less) when you unfocus the input. You probably
|
||||
want `on:input`, but we give you the freedom to choose.
|
||||
2. The `value` *attribute* only sets the initial value of the input, i.e., it
|
||||
only updates the input up to the point that you begin typing. The `value`
|
||||
*property* continues updating the input after that. You usually want to set
|
||||
2. The `value` _attribute_ only sets the initial value of the input, i.e., it
|
||||
only updates the input up to the point that you begin typing. The `value`
|
||||
_property_ continues updating the input after that. You usually want to set
|
||||
`prop:value` for this reason.
|
||||
|
||||
```rust
|
||||
@@ -41,14 +42,14 @@ view! { cx,
|
||||
}
|
||||
```
|
||||
|
||||
## Uncontrolled Inputs
|
||||
## Uncontrolled Inputs
|
||||
|
||||
In an "uncontrolled input," the browser controls the state of the input element.
|
||||
Rather than continuously updating a signal to hold its value, we use a
|
||||
[`NodeRef`](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to access
|
||||
In an "uncontrolled input," the browser controls the state of the input element.
|
||||
Rather than continuously updating a signal to hold its value, we use a
|
||||
[`NodeRef`](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to access
|
||||
the input once when we want to get its value.
|
||||
|
||||
In this example, we only notify the framework when the `<form>` fires a `submit`
|
||||
In this example, we only notify the framework when the `<form>` fires a `submit`
|
||||
event.
|
||||
|
||||
```rust
|
||||
@@ -56,7 +57,8 @@ let (name, set_name) = create_signal(cx, "Uncontrolled".to_string());
|
||||
|
||||
let input_element: NodeRef<Input> = create_node_ref(cx);
|
||||
```
|
||||
`NodeRef` is a kind of reactive smart pointer: we can use it to access the
|
||||
|
||||
`NodeRef` is a kind of reactive smart pointer: we can use it to access the
|
||||
underlying DOM node. Its value will be set when the element is rendered.
|
||||
|
||||
```rust
|
||||
@@ -76,13 +78,14 @@ let on_submit = move |ev: SubmitEvent| {
|
||||
set_name(value);
|
||||
};
|
||||
```
|
||||
|
||||
Our `on_submit` handler will access the input’s value and use it to call `set_name`.
|
||||
To access the DOM node stored in the `NodeRef`, we can simply call it as a function
|
||||
(or using `.get()`). This will return `Option<web_sys::HtmlInputElement>`, but we
|
||||
know it will already have been filled when we rendered the view, so it’s safe to
|
||||
(or using `.get()`). This will return `Option<web_sys::HtmlInputElement>`, but we
|
||||
know it will already have been filled when we rendered the view, so it’s safe to
|
||||
unwrap here.
|
||||
|
||||
We can then call `.value()` to get the value out of the input, because `NodeRef`
|
||||
We can then call `.value()` to get the value out of the input, because `NodeRef`
|
||||
gives us access to a correctly-typed HTML element.
|
||||
|
||||
```rust
|
||||
@@ -97,11 +100,15 @@ view! { cx,
|
||||
<p>"Name is: " {name}</p>
|
||||
}
|
||||
```
|
||||
|
||||
The view should be pretty self-explanatory by now. Note two things:
|
||||
|
||||
1. Unlike in the controlled input example, we use `value` (not `prop:value`).
|
||||
This is because we’re just setting the initial value of the input, and letting
|
||||
This is because we’re just setting the initial value of the input, and letting
|
||||
the browser control its state. (We could use `prop:value` instead.)
|
||||
2. We use `node_ref` to fill the `NodeRef`. (Older examples sometimes use `_ref`.
|
||||
They are the same thing, but `node_ref` has better rust-analyzer support.)
|
||||
|
||||
<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"></iframe>
|
||||
[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>
|
||||
|
||||
@@ -282,4 +282,6 @@ view! { cx,
|
||||
}
|
||||
```
|
||||
|
||||
<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"></iframe>
|
||||
[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>
|
||||
|
||||
@@ -110,4 +110,6 @@ Not a number! Errors:
|
||||
If you fix the error, the error message will disappear and the content you’re wrapping in
|
||||
an `<ErrorBoundary/>` will appear again.
|
||||
|
||||
<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"></iframe>
|
||||
[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>
|
||||
|
||||
@@ -285,4 +285,6 @@ in `<ButtonD/>` and a single text node in `<App/>`. It’s as if the components
|
||||
themselves don’t exist at all. And, well... at runtime, they don’t. It’s just
|
||||
signals and effects, all the way down.
|
||||
|
||||
<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"></iframe>
|
||||
[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>
|
||||
|
||||
@@ -123,4 +123,6 @@ view! { cx,
|
||||
}
|
||||
```
|
||||
|
||||
<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"></iframe>
|
||||
[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>
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use counter::*;
|
||||
use counter::SimpleCounter;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
|
||||
@@ -10,9 +10,8 @@ crate-type = ["cdylib", "rlib"]
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
broadcaster = "1"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
@@ -23,9 +22,9 @@ leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4"
|
||||
simple_logger = "4.0.0"
|
||||
gloo-net = { git = "https://github.com/rustwasm/gloo" }
|
||||
wasm-bindgen = "0.2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -198,13 +198,13 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
|
||||
let s = create_signal_from_stream(
|
||||
cx,
|
||||
source.subscribe("message").unwrap().map(|value| {
|
||||
value
|
||||
.expect("no message event")
|
||||
.1
|
||||
.data()
|
||||
.as_string()
|
||||
.expect("expected string value")
|
||||
}),
|
||||
match value {
|
||||
Ok(value) => {
|
||||
value.1.data().as_string().expect("expected string value")
|
||||
},
|
||||
Err(_) => "0".to_string(),
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
on_cleanup(cx, move || source.close());
|
||||
|
||||
@@ -10,7 +10,6 @@ cfg_if! {
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["stable"] }
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
args = ["+stable", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
args = ["+stable", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,48 +1,44 @@
|
||||
use leptos::{ev, html::*, *};
|
||||
|
||||
pub struct Props {
|
||||
/// The starting value for the counter
|
||||
pub initial_value: i32,
|
||||
/// The change that should be applied each time the button is clicked.
|
||||
pub step: i32,
|
||||
}
|
||||
|
||||
/// A simple counter view.
|
||||
pub fn view(cx: Scope, props: Props) -> impl IntoView {
|
||||
let Props {
|
||||
initial_value,
|
||||
step,
|
||||
} = props;
|
||||
// A component is really just a function call: it runs once to create the DOM and reactive system
|
||||
pub fn counter(cx: Scope, initial_value: i32, step: i32) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
|
||||
// elements are created by calling a function with a Scope argument
|
||||
// the function name is the same as the HTML tag name
|
||||
div(cx)
|
||||
.child((
|
||||
cx,
|
||||
// children can be added with .child()
|
||||
// this takes any type that implements IntoView as its argument
|
||||
// for example, a string or an HtmlElement<_>
|
||||
.child(
|
||||
button(cx)
|
||||
// typed events found in leptos::ev
|
||||
// 1) prevent typos in event names
|
||||
// 2) allow for correct type inference in callbacks
|
||||
.on(ev::click, move |_| set_value.update(|value| *value = 0))
|
||||
.child((cx, "Clear")),
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
.child("Clear"),
|
||||
)
|
||||
.child(
|
||||
button(cx)
|
||||
.on(ev::click, move |_| {
|
||||
set_value.update(|value| *value -= step)
|
||||
})
|
||||
.child((cx, "-1")),
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
.child("-1"),
|
||||
)
|
||||
.child(
|
||||
span(cx)
|
||||
.child((cx, "Value: "))
|
||||
.child("Value: ")
|
||||
// reactive values are passed to .child() as a tuple
|
||||
// (Scope, [child function]) so an effect can be created
|
||||
.child((cx, move || value.get()))
|
||||
.child((cx, "!")),
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
.child("!"),
|
||||
)
|
||||
.child(
|
||||
button(cx)
|
||||
.on(ev::click, move |_| {
|
||||
set_value.update(|value| *value += step)
|
||||
})
|
||||
.child((cx, "+1")),
|
||||
))
|
||||
.child("+1"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
use counter_without_macros as counter;
|
||||
use counter_without_macros::counter;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
counter::view(
|
||||
cx,
|
||||
counter::Props {
|
||||
initial_value: 0,
|
||||
step: 1,
|
||||
},
|
||||
)
|
||||
})
|
||||
mount_to_body(|cx| counter(cx, 0, 1))
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
log = "0.4"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use counters::{Counters, CountersProps};
|
||||
use counters::Counters;
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| view! { cx, <Counters/> })
|
||||
|
||||
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["stable"] }
|
||||
log = "0.4"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| view! { cx, <Counters/> })
|
||||
|
||||
@@ -5,6 +5,6 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -7,10 +7,8 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
console_log = "0.2.0"
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
|
||||
"serde",
|
||||
@@ -18,20 +16,15 @@ leptos = { path = "../../../leptos/leptos", default-features = false, features =
|
||||
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../../leptos/meta", default-features = false }
|
||||
leptos_router = { path = "../../../leptos/router", default-features = false }
|
||||
leptos_reactive = { path = "../../../leptos/leptos_reactive", default-features = false }
|
||||
log = "0.4.17"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8" }
|
||||
thiserror = "1.0.38"
|
||||
tracing = "0.1.37"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -70,8 +70,8 @@ pub fn ExampleErrors(cx: Scope) -> impl IntoView {
|
||||
</p>
|
||||
<p>"The following <div> will always contain an error and cause this page to produce status 500. Check browser dev tools. "</p>
|
||||
<div>
|
||||
// note that the error boundries could be placed above in the Router or lower down
|
||||
// in a particular route. The generated errors on the entire page contribue to the
|
||||
// note that the error boundaries could be placed above in the Router or lower down
|
||||
// in a particular route. The generated errors on the entire page contribute to the
|
||||
// final status code sent by the server when producing ssr pages.
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
<ReturnsError/>
|
||||
|
||||
@@ -13,7 +13,6 @@ cfg_if! {
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ leptos = { path = "../../leptos" }
|
||||
reqwasm = "0.5.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
log = "0.4"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -2,7 +2,6 @@ use fetch::fetch_example;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(fetch_example)
|
||||
|
||||
@@ -7,12 +7,10 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1"
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
@@ -21,15 +19,13 @@ leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_actix = { path = "../../integrations/actix", default-features = false, optional = true }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
tracing = "0.1"
|
||||
# openssl = { version = "0.10", features = ["v110"] }
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
|
||||
tracing = "0.1"
|
||||
|
||||
[features]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
|
||||
@@ -38,7 +38,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
|
||||
let (pending, set_pending) = create_signal(cx, false);
|
||||
|
||||
let hide_more_link =
|
||||
move || pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
|
||||
move |cx| pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
|
||||
|
||||
view! {
|
||||
cx,
|
||||
@@ -65,16 +65,20 @@ pub fn Stories(cx: Scope) -> impl IntoView {
|
||||
}}
|
||||
</span>
|
||||
<span>"page " {page}</span>
|
||||
<span class="page-link"
|
||||
class:disabled=hide_more_link
|
||||
aria-hidden=hide_more_link
|
||||
<Transition
|
||||
fallback=move || view! { cx, <p>"Loading..."</p> }
|
||||
>
|
||||
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
aria-label="Next Page"
|
||||
<span class="page-link"
|
||||
class:disabled=move || hide_more_link(cx)
|
||||
aria-hidden=move || hide_more_link(cx)
|
||||
>
|
||||
"more >"
|
||||
</a>
|
||||
</span>
|
||||
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
aria-label="Next Page"
|
||||
>
|
||||
"more >"
|
||||
</a>
|
||||
</span>
|
||||
</Transition>
|
||||
</div>
|
||||
<main class="news-list">
|
||||
<div>
|
||||
|
||||
@@ -7,10 +7,8 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
console_log = "0.2.0"
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
@@ -21,7 +19,7 @@ leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
tracing = "0.1"
|
||||
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
@@ -31,7 +29,6 @@ tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8", optional = true }
|
||||
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
|
||||
wasm-bindgen = "0.2"
|
||||
tracing = "0.1"
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
|
||||
@@ -46,7 +46,6 @@ if #[cfg(feature = "ssr")] {
|
||||
use hackernews_axum::*;
|
||||
|
||||
pub fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
|
||||
@@ -12,7 +12,7 @@ leptos_router = { version = "0.2.0-alpha2", features = ["stable", "csr"] }
|
||||
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
gloo-net = "0.2"
|
||||
gloo-storage = "0.2"
|
||||
serde = "1.0"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
[[proxy]]
|
||||
rewrite = "/api/"
|
||||
backend = "http://0.0.0.0:3000/"
|
||||
backend = "http://127.0.0.1:3000/"
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
web-sys = "0.3"
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_router = { path = "../../router", features = ["csr"] }
|
||||
|
||||
@@ -28,19 +28,7 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
|
||||
</nav>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route
|
||||
path=""
|
||||
view=move |cx| view! { cx, <ContactList/> }
|
||||
>
|
||||
<Route
|
||||
path=":id"
|
||||
view=move |cx| view! { cx, <Contact/> }
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
view=move |_| view! { cx, <p>"Select a contact."</p> }
|
||||
/>
|
||||
</Route>
|
||||
<ContactRoutes/>
|
||||
<Route
|
||||
path="about"
|
||||
view=move |cx| view! { cx, <About/> }
|
||||
@@ -59,6 +47,27 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
// You can define other routes in their own component.
|
||||
// Use a #[component(transparent)] that returns a <Route/>.
|
||||
#[component(transparent)]
|
||||
pub fn ContactRoutes(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<Route
|
||||
path=""
|
||||
view=move |cx| view! { cx, <ContactList/> }
|
||||
>
|
||||
<Route
|
||||
path=":id"
|
||||
view=move |cx| view! { cx, <Contact/> }
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
view=move |_| view! { cx, <p>"Select a contact."</p> }
|
||||
/>
|
||||
</Route>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ContactList(cx: Scope) -> impl IntoView {
|
||||
log::debug!("rendering <ContactList/>");
|
||||
@@ -127,6 +136,10 @@ pub fn Contact(cx: Scope) -> impl IntoView {
|
||||
get_contact,
|
||||
);
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
log!("params = {:#?}", params.get());
|
||||
});
|
||||
|
||||
let contact_display = move || match contact.read(cx) {
|
||||
// None => loading, but will be caught by Suspense fallback
|
||||
// I'm only doing this explicitly for the example
|
||||
|
||||
@@ -2,7 +2,6 @@ use leptos::*;
|
||||
use router::*;
|
||||
|
||||
pub fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| view! { cx, <RouterExample/> })
|
||||
|
||||
@@ -8,24 +8,20 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
console_log = "0.2.0"
|
||||
console_log = "1.0.0"
|
||||
rand = { version = "0.8.5", features = ["min_const_gen"], optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { version = "0.2.0", default-features = false, features = [
|
||||
"serde",
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_meta = { version = "0.2.0", default-features = false }
|
||||
leptos_axum = { version = "0.2.0", optional = true }
|
||||
leptos_router = { version = "0.2.0", default-features = false }
|
||||
leptos_reactive = { version = "0.2.0", default-features = false }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
@@ -36,12 +32,15 @@ sqlx = { version = "0.6.2", features = [
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
tracing = "0.1.37"
|
||||
wasm-bindgen = "0.2"
|
||||
axum_sessions_auth = { version = "7.0.0", features = [ "sqlite-rustls" ], optional = true }
|
||||
axum_database_sessions = { version = "7.0.0", features = [ "sqlite-rustls" ], optional = true }
|
||||
axum_sessions_auth = { version = "7.0.0", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
axum_database_sessions = { version = "7.0.0", features = [
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
bcrypt = { version = "0.14", optional = true }
|
||||
async-trait = {version = "0.1.64", optional = true }
|
||||
async-trait = { version = "0.1.64", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
|
||||
@@ -15,7 +15,6 @@ cfg_if! {
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
response::{Response, IntoResponse},
|
||||
routing::{post, get},
|
||||
extract::{Path, Extension},
|
||||
extract::{Path, Extension, RawQuery},
|
||||
http::{Request, header::HeaderMap},
|
||||
body::Body as AxumBody,
|
||||
Router,
|
||||
@@ -22,11 +22,12 @@ if #[cfg(feature = "ssr")] {
|
||||
use axum_database_sessions::{SessionConfig, SessionLayer, SessionStore};
|
||||
use axum_sessions_auth::{AuthSessionLayer, AuthConfig, SessionSqlitePool};
|
||||
|
||||
async fn server_fn_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, path: Path<String>, headers: HeaderMap, request: Request<AxumBody>) -> impl IntoResponse {
|
||||
async fn server_fn_handler(Extension(pool): Extension<SqlitePool>, auth_session: AuthSession, path: Path<String>, headers: HeaderMap, raw_query: RawQuery,
|
||||
request: Request<AxumBody>) -> impl IntoResponse {
|
||||
|
||||
log!("{:?}", path);
|
||||
|
||||
handle_server_fns_with_context(path, headers, move |cx| {
|
||||
handle_server_fns_with_context(path, headers, raw_query, move |cx| {
|
||||
provide_context(cx, auth_session.clone());
|
||||
provide_context(cx, pool.clone());
|
||||
}, request).await
|
||||
@@ -73,7 +74,7 @@ if #[cfg(feature = "ssr")] {
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(server_fn_handler))
|
||||
.route("/api/*fn_name", get(server_fn_handler).post(server_fn_handler))
|
||||
.leptos_routes_with_handler(routes, get(leptos_routes_handler) )
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(Some(pool.clone()))
|
||||
|
||||
@@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
@@ -21,7 +21,6 @@ leptos_actix = { path = "../../integrations/actix", default-features = false, op
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
simple_logger = "4"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["time"] }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "0.2"
|
||||
console_log = "1"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
@@ -19,12 +19,11 @@ leptos_axum = { path = "../../integrations/axum", default-features = false, opti
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
simple_logger = "4"
|
||||
thiserror = "1"
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
tokio = { version = "1", features = ["time"], optional = true}
|
||||
tokio = { version = "1", features = ["time"], optional = true }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -22,7 +22,7 @@ cfg-if = "1.0"
|
||||
|
||||
# dependecies for client (enable when csr or hydrate set)
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
console_log = { version = "0.2", optional = true }
|
||||
console_log = { version = "1", optional = true }
|
||||
console_error_panic_hook = { version = "0.1", optional = true }
|
||||
|
||||
# dependecies for server (enable when ssr set)
|
||||
|
||||
@@ -9,14 +9,14 @@ cfg_if! {
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
log!("hydrate mode - hydrating");
|
||||
log!("hydrate mode - hydrating");
|
||||
|
||||
leptos::mount_to_body(|cx| {
|
||||
view! { cx, <App/> }
|
||||
});
|
||||
leptos::mount_to_body(|cx| {
|
||||
view! { cx, <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
else if #[cfg(feature = "csr")] {
|
||||
@@ -35,5 +35,5 @@ cfg_if! {
|
||||
view! { cx, <App /> }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
examples/tailwind_csr_trunk/.gitignore
vendored
Normal file
10
examples/tailwind_csr_trunk/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
21
examples/tailwind_csr_trunk/Cargo.toml
Normal file
21
examples/tailwind_csr_trunk/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "tailwind-csr-trunk"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { version = "0.2", features = [
|
||||
"serde",
|
||||
"csr",
|
||||
] }
|
||||
leptos_meta = { version = "0.2", features = ["csr"] }
|
||||
leptos_router = { version = "0.2", features = ["csr"] }
|
||||
log = "0.4"
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
|
||||
|
||||
# dependecies for client (enable when csr or hydrate set)
|
||||
wasm-bindgen = { version = "0.2" }
|
||||
console_log = { version = "1"}
|
||||
console_error_panic_hook = { version = "0.1"}
|
||||
|
||||
21
examples/tailwind_csr_trunk/LICENSE
Normal file
21
examples/tailwind_csr_trunk/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 henrik
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
9
examples/tailwind_csr_trunk/Makefile.toml
Normal file
9
examples/tailwind_csr_trunk/Makefile.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
70
examples/tailwind_csr_trunk/README.md
Normal file
70
examples/tailwind_csr_trunk/README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Leptos Starter Template
|
||||
|
||||
This is a template demonstrating how to integrate [TailwindCSS](https://tailwindcss.com/) with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [trunk](https://github.com/thedodd/trunk) tool.
|
||||
|
||||
|
||||
Install Tailwind and build the CSS:
|
||||
|
||||
`npx tailwindcss -i ./input.css -o ./style/output.css --watch`
|
||||
|
||||
Install trunk to client side render this bundle.
|
||||
|
||||
`cargo install trunk`
|
||||
Then the site can be served with `trunk serve --open`
|
||||
|
||||
The browser will automatically open [http://127.0.0.1:8080//](http://127.0.0.1:8080//)
|
||||
|
||||
You can begin editing your app at `src/app.rs`.
|
||||
|
||||
## Installing Tailwind
|
||||
|
||||
You can install Tailwind using `npm`:
|
||||
|
||||
```bash
|
||||
npm install -D tailwindcss
|
||||
```
|
||||
|
||||
If you'd rather not use `npm`, you can install the Tailwind binary [here](https://github.com/tailwindlabs/tailwindcss/releases).
|
||||
|
||||
## Setting up with VS Code and Additional Tools
|
||||
|
||||
If you're using VS Code, add the following to your `settings.json`
|
||||
|
||||
```json
|
||||
"emmet.includeLanguages": {
|
||||
"rust": "html",
|
||||
"*.rs": "html"
|
||||
},
|
||||
"tailwindCSS.includeLanguages": {
|
||||
"rust": "html",
|
||||
"*.rs": "html"
|
||||
},
|
||||
"files.associations": {
|
||||
"*.rs": "rust"
|
||||
},
|
||||
"editor.quickSuggestions": {
|
||||
"other": "on",
|
||||
"comments": "on",
|
||||
"strings": true
|
||||
},
|
||||
"css.validate": false,
|
||||
```
|
||||
|
||||
Install [Tailwind CSS Intellisense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss).
|
||||
|
||||
Install "VS Browser" extension, a browser at the right window.
|
||||
Allow vscode Ports forward: 3000, 3001.
|
||||
|
||||
## Notes about Tooling
|
||||
|
||||
By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools.
|
||||
|
||||
1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly
|
||||
2. `rustup default nightly` - setup nightly as default, or you can use rust-toolchain file later on
|
||||
3. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly
|
||||
4. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future)
|
||||
5. `npm install -g sass` - install `dart-sass` (should be optional in future
|
||||
|
||||
|
||||
## Attribution
|
||||
This is based on the original Tailwind example (../examples/tailwind)
|
||||
74
examples/tailwind_csr_trunk/end2end/package-lock.json
generated
Normal file
74
examples/tailwind_csr_trunk/end2end/package-lock.json
generated
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"name": "end2end",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "end2end",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.28.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
|
||||
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.28.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.11.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
|
||||
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
|
||||
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/test": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
|
||||
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.28.0"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "18.11.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
|
||||
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
|
||||
"dev": true
|
||||
},
|
||||
"playwright-core": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
|
||||
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
13
examples/tailwind_csr_trunk/end2end/package.json
Normal file
13
examples/tailwind_csr_trunk/end2end/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "end2end",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.28.0"
|
||||
}
|
||||
}
|
||||
107
examples/tailwind_csr_trunk/end2end/playwright.config.ts
Normal file
107
examples/tailwind_csr_trunk/end2end/playwright.config.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import type { PlaywrightTestConfig } from "@playwright/test";
|
||||
import { devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: "./tests",
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 30 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 5000,
|
||||
},
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://localhost:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "firefox",
|
||||
use: {
|
||||
...devices["Desktop Firefox"],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "webkit",
|
||||
use: {
|
||||
...devices["Desktop Safari"],
|
||||
},
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: {
|
||||
// ...devices['Pixel 5'],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: {
|
||||
// ...devices['iPhone 12'],
|
||||
// },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: {
|
||||
// channel: 'msedge',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: {
|
||||
// channel: 'chrome',
|
||||
// },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
// outputDir: 'test-results/',
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// port: 3000,
|
||||
// },
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("homepage has title and links to intro page", async ({ page }) => {
|
||||
await page.goto("http://localhost:8080/");
|
||||
|
||||
await expect(page).toHaveTitle("Leptos • Counter with Tailwind");
|
||||
|
||||
await expect(page.locator("h2")).toHaveText("Welcome to Leptos with Tailwind");
|
||||
});
|
||||
14
examples/tailwind_csr_trunk/index.html
Normal file
14
examples/tailwind_csr_trunk/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" />
|
||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico" />
|
||||
<link data-trunk rel="css" href="/style/output.css" />
|
||||
<title>Leptos • Counter with Tailwind</title>
|
||||
</head>
|
||||
|
||||
<body></body>
|
||||
|
||||
</html>
|
||||
3
examples/tailwind_csr_trunk/input.css
Normal file
3
examples/tailwind_csr_trunk/input.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
BIN
examples/tailwind_csr_trunk/public/favicon.ico
Normal file
BIN
examples/tailwind_csr_trunk/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
43
examples/tailwind_csr_trunk/src/app.rs
Normal file
43
examples/tailwind_csr_trunk/src/app.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
provide_meta_context(cx);
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<Stylesheet id="leptos" href="/pkg/tailwind.css"/>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="" view= move |cx| view! { cx, <Home/> }/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Home(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
view! { cx,
|
||||
<div class="my-0 mx-auto max-w-3xl text-center">
|
||||
<h2 class="p-6 text-4xl">"Welcome to Leptos with Tailwind"</h2>
|
||||
<p class="px-10 pb-10 text-left">"Tailwind will scan your Rust files for Tailwind class names and compile them into a CSS file."</p>
|
||||
<button
|
||||
class="bg-amber-600 hover:bg-sky-700 px-5 py-3 text-white rounded-lg"
|
||||
on:click=move |_| set_count.update(|count| *count += 1)
|
||||
>
|
||||
"Something's here | "
|
||||
{move || if count() == 0 {
|
||||
"Click me!".to_string()
|
||||
} else {
|
||||
count().to_string()
|
||||
}}
|
||||
" | Some more text"
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
15
examples/tailwind_csr_trunk/src/main.rs
Normal file
15
examples/tailwind_csr_trunk/src/main.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
mod app;
|
||||
|
||||
use app::*;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
log!("csr mode - mounting to body");
|
||||
|
||||
mount_to_body(|cx| {
|
||||
view! { cx, <App /> }
|
||||
});
|
||||
}
|
||||
583
examples/tailwind_csr_trunk/style/output.css
Normal file
583
examples/tailwind_csr_trunk/style/output.css
Normal file
@@ -0,0 +1,583 @@
|
||||
/*
|
||||
! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com
|
||||
*/
|
||||
|
||||
/*
|
||||
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||
*/
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
/* 1 */
|
||||
border-width: 0;
|
||||
/* 2 */
|
||||
border-style: solid;
|
||||
/* 2 */
|
||||
border-color: #e5e7eb;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
::before,
|
||||
::after {
|
||||
--tw-content: '';
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use a consistent sensible line-height in all browsers.
|
||||
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
3. Use a more readable tab size.
|
||||
4. Use the user's configured `sans` font-family by default.
|
||||
5. Use the user's configured `sans` font-feature-settings by default.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.5;
|
||||
/* 1 */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
/* 2 */
|
||||
-moz-tab-size: 4;
|
||||
/* 3 */
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
/* 3 */
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
/* 4 */
|
||||
font-feature-settings: normal;
|
||||
/* 5 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove the margin in all browsers.
|
||||
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Add the correct height in Firefox.
|
||||
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||
3. Ensure horizontal rules are visible by default.
|
||||
*/
|
||||
|
||||
hr {
|
||||
height: 0;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 2 */
|
||||
border-top-width: 1px;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
abbr:where([title]) {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the default font size and weight for headings.
|
||||
*/
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset links to optimize for opt-in styling instead of opt-out.
|
||||
*/
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font weight in Edge and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use the user's configured `mono` font family by default.
|
||||
2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
/* 1 */
|
||||
font-size: 1em;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||
3. Remove gaps between table borders by default.
|
||||
*/
|
||||
|
||||
table {
|
||||
text-indent: 0;
|
||||
/* 1 */
|
||||
border-color: inherit;
|
||||
/* 2 */
|
||||
border-collapse: collapse;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Change the font styles in all browsers.
|
||||
2. Remove the margin in Firefox and Safari.
|
||||
3. Remove default padding in all browsers.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
/* 1 */
|
||||
font-size: 100%;
|
||||
/* 1 */
|
||||
font-weight: inherit;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 1 */
|
||||
margin: 0;
|
||||
/* 2 */
|
||||
padding: 0;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inheritance of text transform in Edge and Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Remove default button styles.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type='button'],
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
background-color: transparent;
|
||||
/* 2 */
|
||||
background-image: none;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Use the modern Firefox focus style for all focusable elements.
|
||||
*/
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||
*/
|
||||
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct vertical alignment in Chrome and Firefox.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/*
|
||||
Correct the cursor style of increment and decrement buttons in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the odd appearance in Chrome and Safari.
|
||||
2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type='search'] {
|
||||
-webkit-appearance: textfield;
|
||||
/* 1 */
|
||||
outline-offset: -2px;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
font: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct display in Chrome and Safari.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/*
|
||||
Removes the default spacing and border for appropriate elements.
|
||||
*/
|
||||
|
||||
blockquote,
|
||||
dl,
|
||||
dd,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hr,
|
||||
figure,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
menu {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent resizing textareas horizontally by default.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||
*/
|
||||
|
||||
input::-moz-placeholder, textarea::-moz-placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Set the default cursor for buttons.
|
||||
*/
|
||||
|
||||
button,
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*
|
||||
Make sure disabled buttons don't get the pointer cursor.
|
||||
*/
|
||||
|
||||
:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||
*/
|
||||
|
||||
img,
|
||||
svg,
|
||||
video,
|
||||
canvas,
|
||||
audio,
|
||||
iframe,
|
||||
embed,
|
||||
object {
|
||||
display: block;
|
||||
/* 1 */
|
||||
vertical-align: middle;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
*/
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Make elements with the HTML hidden attribute stay hidden by default */
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
*, ::before, ::after {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
::backdrop {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
}
|
||||
|
||||
.mx-auto {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.my-0 {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.max-w-3xl {
|
||||
max-width: 48rem;
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.bg-amber-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(217 119 6 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.px-10 {
|
||||
padding-left: 2.5rem;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.px-5 {
|
||||
padding-left: 1.25rem;
|
||||
padding-right: 1.25rem;
|
||||
}
|
||||
|
||||
.py-3 {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.pb-10 {
|
||||
padding-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-4xl {
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
|
||||
.text-white {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-sky-700:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(3 105 161 / var(--tw-bg-opacity));
|
||||
}
|
||||
10
examples/tailwind_csr_trunk/tailwind.config.js
Normal file
10
examples/tailwind_csr_trunk/tailwind.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: {
|
||||
files: ["*.html", "./src/**/*.rs"],
|
||||
},
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -11,7 +11,7 @@ actix-files = { version = "0.6.2", optional = true }
|
||||
actix-web = { version = "4.2.1", optional = true, features = ["macros"] }
|
||||
anyhow = "1.0.68"
|
||||
broadcaster = "1.0.0"
|
||||
console_log = "0.2.0"
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
futures = "0.3.25"
|
||||
|
||||
@@ -4,6 +4,15 @@ args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
clear = true
|
||||
dependencies = ["check-debug", "check-release"]
|
||||
|
||||
[tasks.check-debug]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check-release]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features", "--release"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -7,8 +7,7 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
console_log = "0.2.0"
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
@@ -18,13 +17,9 @@ leptos = { path = "../../leptos", default-features = false, features = [
|
||||
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos_reactive = { path = "../../leptos_reactive", default-features = false }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = ["fs"], optional = true }
|
||||
@@ -35,7 +30,6 @@ sqlx = { version = "0.6.2", features = [
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
tracing = "0.1.37"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -13,7 +13,6 @@ cfg_if! {
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
|
||||
@@ -7,13 +7,12 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
console_log = "0.2.0"
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
"serde",
|
||||
] }
|
||||
leptos_viz = { path = "../../integrations/viz", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
@@ -21,19 +20,15 @@ leptos_router = { path = "../../router", default-features = false }
|
||||
leptos_reactive = { path = "../../leptos_reactive", default-features = false }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
viz = { version = "0.4.8", features = ["serve"], optional = true }
|
||||
tokio = { version = "1.25.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8" }
|
||||
sqlx = { version = "0.6.2", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
tracing = "0.1.37"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
@@ -47,7 +42,7 @@ ssr = [
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:leptos_viz"
|
||||
"dep:leptos_viz",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
|
||||
@@ -13,7 +13,6 @@ cfg_if! {
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
|
||||
@@ -51,7 +51,8 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
let mut todos = Vec::new();
|
||||
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
let mut rows =
|
||||
sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
while let Some(row) = rows
|
||||
.try_next()
|
||||
.await
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user