mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 11:21:55 -05:00
Compare commits
20 Commits
accidental
...
v040
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ac55266f8 | ||
|
|
33c83c3e62 | ||
|
|
171adcd09e | ||
|
|
13f7cb9a9a | ||
|
|
ee7dbafc85 | ||
|
|
f5cfe4e8a2 | ||
|
|
c3e45d19d7 | ||
|
|
966100c2d6 | ||
|
|
bce1dea11b | ||
|
|
c55067ab7c | ||
|
|
9da4084561 | ||
|
|
1d7235d4ca | ||
|
|
2cb8171105 | ||
|
|
bbc7799b7c | ||
|
|
a9cbcce8b2 | ||
|
|
3531ca64bb | ||
|
|
e402b85dd6 | ||
|
|
8ae5cf0ccf | ||
|
|
5c34c3fc77 | ||
|
|
3a570dc0d9 |
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -16,10 +16,10 @@ Please copy and paste the Leptos dependencies and features from your `Cargo.toml
|
||||
|
||||
For example:
|
||||
```toml
|
||||
leptos = { version = "0.3", default-features = false, features = ["serde"] }
|
||||
leptos = { version = "0.3", features = ["serde"] }
|
||||
leptos_axum = { version = "0.3", optional = true }
|
||||
leptos_meta = { version = "0.3", default-features = false }
|
||||
leptos_router = { version = "0.3", default-features = false }
|
||||
leptos_meta = { version = "0.3"}
|
||||
leptos_router = { version = "0.3"}
|
||||
```
|
||||
|
||||
**To Reproduce**
|
||||
|
||||
5
.github/workflows/run-example-task.yml
vendored
5
.github/workflows/run-example-task.yml
vendored
@@ -26,6 +26,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
# Setup environment
|
||||
- name: Install playwright browser dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libegl1 libvpx7 libevent-2.1-7 libopus0 libopengl0 libwoff1 libharfbuzz-icu0 libgstreamer-plugins-base1.0-0 libgstreamer-gl1.0-0 libhyphen0 libmanette-0.2-0 libgles2 gstreamer1.0-libav
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
_This Code of Conduct is based on the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct)
|
||||
and the [Bevy Code of Conduct](https://raw.githubusercontent.com/bevyengine/bevy/main/CODE_OF_CONDUCT.md),
|
||||
which are adapted from the [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling)
|
||||
which are adapted from the [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling)
|
||||
and the [Contributor Covenant](https://www.contributor-covenant.org)._
|
||||
|
||||
## Our Pledge
|
||||
|
||||
@@ -22,7 +22,7 @@ Leptos, as a framework, reflects certain technical values:
|
||||
- **Expose primitives rather than imposing patterns.** Provide building blocks
|
||||
that users can combine together to build up more complex behavior, rather than
|
||||
requiring users follow certain templates, file formats, etc. e.g., components
|
||||
are defined as functions, rather than a bespoke single-file comonent format.
|
||||
are defined as functions, rather than a bespoke single-file component format.
|
||||
The reactive system feeds into the rendering system, rather than being defined
|
||||
by it.
|
||||
- **Bottom-up over top-down.** If you envision a user’s application as a tree
|
||||
@@ -42,7 +42,7 @@ Leptos, as a framework, reflects certain technical values:
|
||||
- **Embrace Rust semantics.** Especially in things like UI templating, use Rust
|
||||
semantics or extend them in a predictable way with control-flow components
|
||||
rather than overloading the meaning of Rust terms like `if` or `for` in a
|
||||
framework-speciic way.
|
||||
framework-specific way.
|
||||
- **Enhance ergonomics without obfuscating what’s happening.** This is by far
|
||||
the hardest to achieve. It’s often the case that adding additional layers to
|
||||
improve DX (like a custom build tool and starter templates) comes across as
|
||||
@@ -67,7 +67,7 @@ are a few guidelines that will make it a better experience for everyone:
|
||||
- Our CI tests every PR against all the existing examples, sometimes requiring
|
||||
compilation for both server and client side, etc. It’s thorough but slow. If
|
||||
you want to run CI locally to reduce frustration, you can do that by installing
|
||||
`cargo-make` and using `cargo make check && cargo make test && cargo make
|
||||
`cargo-make` and using `cargo make check && cargo make test && cargo make
|
||||
check-examples`.
|
||||
|
||||
## Architecture
|
||||
|
||||
30
Cargo.toml
30
Cargo.toml
@@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
resolver="2"
|
||||
resolver = "2"
|
||||
members = [
|
||||
# core
|
||||
"leptos",
|
||||
@@ -26,22 +26,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.3.0" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.3.0" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.3.0" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.3.0" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.3.0" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.3.0" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.3.0" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.3.0" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.3.0" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.3.0" }
|
||||
leptos_router = { path = "./router", version = "0.3.0" }
|
||||
leptos_meta = { path = "./meta", default-features = false, version = "0.3.0" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.3.0" }
|
||||
leptos = { path = "./leptos", version = "0.4.0" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.4.0" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.4.0" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.4.0" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.4.0" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.4.0" }
|
||||
server_fn = { path = "./server_fn", version = "0.4.0" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.4.0" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.4.0" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.4.0" }
|
||||
leptos_router = { path = "./router", version = "0.4.0" }
|
||||
leptos_meta = { path = "./meta", version = "0.4.0" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.4.0" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
11
README.md
11
README.md
@@ -8,7 +8,6 @@
|
||||
[](https://discord.gg/YdRAhS7eQB)
|
||||
[](https://matrix.to/#/#leptos:matrix.org)
|
||||
|
||||
|
||||
[Website](https://leptos.dev) | [Book](https://leptos-rs.github.io/leptos/) | [Docs.rs](https://docs.rs/leptos/latest/leptos/) | [Playground](https://codesandbox.io/p/sandbox/leptos-rtfggt?file=%2Fsrc%2Fmain.rs%3A1%2C1) | [Discord](https://discord.gg/YdRAhS7eQB)
|
||||
|
||||
# Leptos
|
||||
@@ -69,7 +68,7 @@ Here are some resources for learning more about Leptos:
|
||||
|
||||
## `nightly` Note
|
||||
|
||||
Most of the examples assume you’re using `nightly` version of Rust. For this, you can either set your toolchain globally or on per-project basis.
|
||||
Most of the examples assume you’re using `nightly` version of Rust and the `nightly` feature of Leptos. To use `nightly` Rust, you can either set your toolchain globally or on per-project basis.
|
||||
|
||||
To set `nightly` as a default toolchain for all projects (and add the ability to compile Rust to WebAssembly, if you haven’t already):
|
||||
|
||||
@@ -87,13 +86,9 @@ channel = "nightly"
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
```
|
||||
|
||||
If you’re on `stable`, note the following:
|
||||
The `nightly` feature enables the function call syntax for accessing and setting signals, as opposed to `.get()` and `.set()`. This leads to a consistent mental model in which accessing a reactive value of any kind (a signal, memo, or derived signal) is always represented as a function call. This is only possible with nightly Rust and the `nightly` feature.
|
||||
|
||||
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)
|
||||
for examples of the correct API.
|
||||
> Note: The `nightly` feature is present on the main branch version right now, but not in 0.3.x. For 0.3.x, nightly is the default and `stable` has a special feature.
|
||||
|
||||
## `cargo-leptos`
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
l021 = { package = "leptos", version = "0.2.1" }
|
||||
leptos = { path = "../leptos", default-features = false, features = ["ssr"] }
|
||||
leptos = { path = "../leptos", features = ["ssr"] }
|
||||
sycamore = { version = "0.8", features = ["ssr"] }
|
||||
yew = { git = "https://github.com/yewstack/yew", features = ["ssr"] }
|
||||
tokio-test = "0.4"
|
||||
|
||||
@@ -29,9 +29,9 @@ where
|
||||
}
|
||||
```
|
||||
|
||||
This is pretty straightforward: when the user is logged in, we want to show `children`. Until if the user is not logged in, we want to show `fallback`. And while we’re waiting to find out, we just render `()`, i.e., nothing.
|
||||
This is pretty straightforward: when the user is logged in, we want to show `children`. If the user is not logged in, we want to show `fallback`. And while we’re waiting to find out, we just render `()`, i.e., nothing.
|
||||
|
||||
In other words, we want to pass the children of `<WhenLoaded/>` _through_ the `<Suspense/>` component to become the children of the `<Show/>`. This is what I mean by “projection.”
|
||||
In other words, we want to pass the children of `<LoggedIn/>` _through_ the `<Suspense/>` component to become the children of the `<Show/>`. This is what I mean by “projection.”
|
||||
|
||||
This won’t compile.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ That’s where the [`leptos_meta`](https://docs.rs/leptos_meta/latest/leptos_met
|
||||
|
||||
`leptos_meta` also provides a [`<Script/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Script.html) component, and it’s worth pausing here for a second. All of the other components we’ve considered inject `<head>`-only elements in the `<head>`. But a `<script>` can also be included in the body.
|
||||
|
||||
There’s a very simple way to determine whether you should use a capital-S `<Script/>` component or a lowercase-s `<script>` element: the `<Script/>` component will be rendered in the `<head>`, and the `<script>` element will be rendered wherever in your the `<body>` of your user interface you put in, alongside other normal HTML elements. These cause JavaScript to load and run at different times, so use whichever is appropriate to your needs.
|
||||
There’s a very simple way to determine whether you should use a capital-S `<Script/>` component or a lowercase-s `<script>` element: the `<Script/>` component will be rendered in the `<head>`, and the `<script>` element will be rendered wherever in the `<body>` of your user interface you put it in, alongside other normal HTML elements. These cause JavaScript to load and run at different times, so use whichever is appropriate to your needs.
|
||||
|
||||
## `<Body/>` and `<Html/>`
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ There are four basic signal operations:
|
||||
1. [`.get()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#impl-SignalGet%3CT%3E-for-ReadSignal%3CT%3E) clones the current value of the signal and tracks any future changes to the value reactively.
|
||||
2. [`.with()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#impl-SignalWith%3CT%3E-for-ReadSignal%3CT%3E) takes a function, which receives the current value of the signal by reference (`&T`), and tracks any future changes.
|
||||
3. [`.set()`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#impl-SignalSet%3CT%3E-for-WriteSignal%3CT%3E) replaces the current value of the signal and notifies any subscribers that they need to update.
|
||||
4. [`.update()`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#impl-SignalUpdate%3CT%3E-for-WriteSignal%3CT%3E) takes a function, which receives a mutable reference to the current value of the signal (`&T`), and notifies any subscribers that they need to update. (`.update()` doesn’t return the value returned by the closure, but you can use [`.try_update()`](https://docs.rs/leptos/latest/leptos/trait.SignalUpdate.html#tymethod.try_update) if you need to; for example, if you’re removing an item from a `Vec<_>` and want the removed item.)
|
||||
4. [`.update()`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#impl-SignalUpdate%3CT%3E-for-WriteSignal%3CT%3E) takes a function, which receives a mutable reference to the current value of the signal (`&mut T`), and notifies any subscribers that they need to update. (`.update()` doesn’t return the value returned by the closure, but you can use [`.try_update()`](https://docs.rs/leptos/latest/leptos/trait.SignalUpdate.html#tymethod.try_update) if you need to; for example, if you’re removing an item from a `Vec<_>` and want the removed item.)
|
||||
|
||||
Calling a `ReadSignal` as a function is syntax sugar for `.get()`. Calling a `WriteSignal` as a function is syntax sugar for `.set()`. So
|
||||
|
||||
@@ -99,7 +99,7 @@ let clear_handler = move |_| {
|
||||
### If you really must...
|
||||
**4) Create an effect to write to B whenever A changes.** This is officially discouraged, for several reasons:
|
||||
a) It will always be less efficient, as it means every time A updates you do two full trips through the reactive process. (You set A, which causes the effect to run, as well as any other effects that depend on A. Then you set B, which causes any effects that depend on B to run.)
|
||||
b) It increases your chances of accidentally creating things like infinite loops or over-re-running effects. This is the kind of ping-ponging, reactive spaghetti code that was common in the early 2010s and that we try to avoid with things like read-write segregation and discouraging writing to signals frome effects.
|
||||
b) It increases your chances of accidentally creating things like infinite loops or over-re-running effects. This is the kind of ping-ponging, reactive spaghetti code that was common in the early 2010s and that we try to avoid with things like read-write segregation and discouraging writing to signals from effects.
|
||||
|
||||
In most situations, it’s best to rewrite things such that there’s a clear, top-down data flow based on derived signals or memos. But this isn’t the end of the world.
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
|
||||
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 URL consists of many parts. For example, the URL `https://my-cool-blog.com/blog/search?q=Search#results` consists of
|
||||
|
||||
- a _scheme_: `https`
|
||||
- a _domain_: `leptos.dev`
|
||||
- a _domain_: `my-cool-blog.com`
|
||||
- a **path**: `/blog/search`
|
||||
- a **query** (or **search**): `?q=Search`
|
||||
- a _hash_: `#results`
|
||||
|
||||
@@ -28,7 +28,7 @@ fn App(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<button
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
set_count(3);
|
||||
}
|
||||
>
|
||||
"Click me: "
|
||||
@@ -142,7 +142,7 @@ in a function, telling the framework to update the view every time `count` chang
|
||||
`{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!
|
||||
|
||||
Let’s make one final change. `set_count(3)` is a pretty useless thing for a click handler to do. Let’s replacing “set this value to 3” with “increment this value by 1”:
|
||||
Let’s make one final change. `set_count(3)` is a pretty useless thing for a click handler to do. Let’s replace “set this value to 3” with “increment this value by 1”:
|
||||
|
||||
```rust
|
||||
move |_| {
|
||||
|
||||
@@ -98,18 +98,6 @@ notice that you can easily tell the difference between an element and a componen
|
||||
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:
|
||||
>
|
||||
> `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
|
||||
|
||||
@@ -34,7 +34,7 @@ category = "Cleanup"
|
||||
script = '''
|
||||
for pw_dir in $(find . -name playwright.config.ts | xargs dirname)
|
||||
do
|
||||
rm -rf $pw_dir/playwright-report
|
||||
rm -rf $pw_dir/playwright-report pw_dir/playwright pw_dir/test-results
|
||||
done
|
||||
'''
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
|
||||
[tasks.ci]
|
||||
alias = "verify-flow"
|
||||
|
||||
[tasks.verify-flow]
|
||||
description = "Provides pre and post hooks for verify"
|
||||
dependencies = ["pre-verify", "verify", "post-verify"]
|
||||
|
||||
7
examples/cargo-make/playwright-test.toml
Normal file
7
examples/cargo-make/playwright-test.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
extend = [{ path = "../cargo-make/playwright.toml" }]
|
||||
|
||||
[tasks.test-e2e]
|
||||
dependencies = ["setup-node", "test-playwright-autostart"]
|
||||
|
||||
[tasks.clean-all]
|
||||
dependencies = ["clean-cargo", "clean-node_modules", "clean-playwright"]
|
||||
119
examples/cargo-make/playwright.toml
Normal file
119
examples/cargo-make/playwright.toml
Normal file
@@ -0,0 +1,119 @@
|
||||
[tasks.clean-playwright]
|
||||
description = "Delete playwright directories"
|
||||
category = "Cleanup"
|
||||
script = '''
|
||||
for pw_dir in $(find . -name playwright.config.ts | xargs dirname)
|
||||
do
|
||||
rm -rf $pw_dir/playwright-report pw_dir/playwright pw_dir/test-results
|
||||
done
|
||||
'''
|
||||
|
||||
[tasks.test-playwright-autostart]
|
||||
description = "Run playwright test with server autostart"
|
||||
category = "Test"
|
||||
command = "npm"
|
||||
args = ["run", "e2e:auto-start"]
|
||||
|
||||
[tasks.test-playwright]
|
||||
description = "Run playwright test"
|
||||
category = "Test"
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
RED="\e[0;31m"
|
||||
RESET="\e[0m"
|
||||
|
||||
project_dir=$CARGO_MAKE_WORKING_DIRECTORY
|
||||
|
||||
# Discover commands
|
||||
if command -v pnpm; then
|
||||
PLAYWRIGHT_CMD=pnpm
|
||||
elif command -v npm; then
|
||||
PLAYWRIGHT_CMD=npx
|
||||
else
|
||||
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run playwright command
|
||||
for pw_path in $(find . -name playwright.config.ts)
|
||||
do
|
||||
pw_dir=$(dirname $pw_path)
|
||||
cd $pw_dir
|
||||
${PLAYWRIGHT_CMD} playwright test
|
||||
cd $project_dir
|
||||
done
|
||||
'''
|
||||
|
||||
[tasks.test-playwright-ui]
|
||||
description = "Run playwright test --ui"
|
||||
category = "Test"
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
RED="\e[0;31m"
|
||||
RESET="\e[0m"
|
||||
|
||||
project_dir=$CARGO_MAKE_WORKING_DIRECTORY
|
||||
|
||||
# Discover commands
|
||||
if command -v pnpm; then
|
||||
PLAYWRIGHT_CMD=pnpm
|
||||
elif command -v npm; then
|
||||
PLAYWRIGHT_CMD=npx
|
||||
else
|
||||
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run playwright command
|
||||
for pw_path in $(find . -name playwright.config.ts)
|
||||
do
|
||||
pw_dir=$(dirname $pw_path)
|
||||
cd $pw_dir
|
||||
${PLAYWRIGHT_CMD} playwright test --ui
|
||||
cd $project_dir
|
||||
done
|
||||
'''
|
||||
|
||||
[tasks.test-playwright-report]
|
||||
description = "Run playwright show-report"
|
||||
category = "Test"
|
||||
script = '''
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
RED="\e[0;31m"
|
||||
RESET="\e[0m"
|
||||
|
||||
project_dir=$CARGO_MAKE_WORKING_DIRECTORY
|
||||
|
||||
# Discover commands
|
||||
if command -v pnpm; then
|
||||
PLAYWRIGHT_CMD=pnpm
|
||||
elif command -v npm; then
|
||||
PLAYWRIGHT_CMD=npx
|
||||
else
|
||||
echo "${RED}${BOLD}ERROR${RESET} - pnpm or npm is required by this task"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run playwright command
|
||||
for pw_path in $(find . -name playwright.config.ts)
|
||||
do
|
||||
pw_dir=$(dirname $pw_path)
|
||||
cd $pw_dir
|
||||
${PLAYWRIGHT_CMD} playwright show-report
|
||||
cd $project_dir
|
||||
done
|
||||
'''
|
||||
|
||||
# ALIASES
|
||||
|
||||
[tasks.pw]
|
||||
dependencies = ["test-playwright"]
|
||||
|
||||
[tasks.pw-ui]
|
||||
dependencies = ["test-playwright-ui"]
|
||||
|
||||
[tasks.pw-report]
|
||||
dependencies = ["test-playwright-report"]
|
||||
22
examples/cargo-make/trunk_server.toml
Normal file
22
examples/cargo-make/trunk_server.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[tasks.build]
|
||||
command = "trunk"
|
||||
args = ["build"]
|
||||
|
||||
[tasks.clean-trunk]
|
||||
command = "trunk"
|
||||
args = ["clean"]
|
||||
|
||||
[tasks.start-trunk]
|
||||
command = "trunk"
|
||||
args = ["serve", "--open"]
|
||||
|
||||
[tasks.stop-trunk]
|
||||
script = '''
|
||||
pkill -f "cargo-make"
|
||||
pkill -f "trunk"
|
||||
'''
|
||||
|
||||
# ALIASES
|
||||
|
||||
[tasks.dev]
|
||||
dependencies = ["start-trunk"]
|
||||
@@ -8,7 +8,7 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -19,19 +19,17 @@ console_error_panic_hook = "0.1"
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4"
|
||||
gloo-net = { git = "https://github.com/rustwasm/gloo" }
|
||||
wasm-bindgen = "=0.2.86"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["nightly"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
@@ -41,10 +39,10 @@ ssr = [
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
stable = ["leptos/stable", "leptos_router/stable"]
|
||||
nightly = ["leptos/nightly", "leptos_router/nightly"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["actix-files", "actix-web", "leptos_actix", "stable"]
|
||||
denylist = ["actix-files", "actix-web", "leptos_actix", "nightly"]
|
||||
skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
|
||||
@@ -8,7 +8,7 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["stable"] }
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
log = "0.4"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
20
examples/counters_stable/.gitignore
vendored
Normal file
20
examples/counters_stable/.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# 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
|
||||
|
||||
# Support playwright testing
|
||||
node_modules/
|
||||
test-results/
|
||||
end2end/playwright-report/
|
||||
playwright/.cache/
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Support trunk
|
||||
dist
|
||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["stable"] }
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
log = "0.4"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
extend = [{ path = "../cargo-make/main.toml" }]
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
{ path = "../cargo-make/playwright-test.toml" },
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
|
||||
4
examples/counters_stable/e2e/.gitignore
vendored
Normal file
4
examples/counters_stable/e2e/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
83
examples/counters_stable/e2e/package-lock.json
generated
Normal file
83
examples/counters_stable/e2e/package-lock.json
generated
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"name": "grip",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "grip",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.35.1"
|
||||
}
|
||||
},
|
||||
"node_modules/.pnpm/@playwright+test@1.33.0": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/.pnpm/@types+node@20.2.1/node_modules/@types/node": {
|
||||
"version": "20.2.1",
|
||||
"extraneous": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/.pnpm/playwright-core@1.33.0/node_modules/playwright-core": {
|
||||
"version": "1.33.0",
|
||||
"extraneous": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.35.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz",
|
||||
"integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.35.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz",
|
||||
"integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.35.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz",
|
||||
"integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
examples/counters_stable/e2e/package.json
Normal file
7
examples/counters_stable/e2e/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"private": "true",
|
||||
"scripts": {},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.35.1"
|
||||
}
|
||||
}
|
||||
77
examples/counters_stable/e2e/playwright.config.ts
Normal file
77
examples/counters_stable/e2e/playwright.config.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
/* 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.DEV,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.DEV ? 0 : 10,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.DEV ? 1 : 1,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [["html", { open: "never" }], ["list"]],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://127.0.0.1:8080",
|
||||
|
||||
/* 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: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: "cd ../ && trunk serve",
|
||||
// url: "http://127.0.0.1:8080",
|
||||
// reuseExistingServer: false, //!process.env.CI,
|
||||
// },
|
||||
});
|
||||
20
examples/counters_stable/e2e/tests/add_1k_counters.spec.ts
Normal file
20
examples/counters_stable/e2e/tests/add_1k_counters.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./counters_page";
|
||||
|
||||
test.describe("Add 1000 Counters", () => {
|
||||
test("should increment the total count by 1K", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
|
||||
await Promise.all([
|
||||
await ui.goto(),
|
||||
await ui.addOneThousandCountersButton.waitFor(),
|
||||
]);
|
||||
|
||||
await ui.addOneThousandCounters();
|
||||
await ui.addOneThousandCounters();
|
||||
await ui.addOneThousandCounters();
|
||||
|
||||
await expect(ui.total).toHaveText("0");
|
||||
await expect(ui.counters).toHaveText("3000");
|
||||
});
|
||||
});
|
||||
16
examples/counters_stable/e2e/tests/add_counter.spec.ts
Normal file
16
examples/counters_stable/e2e/tests/add_counter.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./counters_page";
|
||||
|
||||
test.describe("Add Counter", () => {
|
||||
test("should increment the total count", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await expect(ui.total).toHaveText("0");
|
||||
await expect(ui.counters).toHaveText("3");
|
||||
});
|
||||
});
|
||||
18
examples/counters_stable/e2e/tests/clear_counters.spec.ts
Normal file
18
examples/counters_stable/e2e/tests/clear_counters.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./counters_page";
|
||||
|
||||
test.describe("Clear Counters", () => {
|
||||
test("should reset the counts", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.clearCounters();
|
||||
|
||||
await expect(ui.total).toHaveText("0");
|
||||
await expect(ui.counters).toHaveText("0");
|
||||
});
|
||||
});
|
||||
75
examples/counters_stable/e2e/tests/counters_page.ts
Normal file
75
examples/counters_stable/e2e/tests/counters_page.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { expect, Locator, Page } from "@playwright/test";
|
||||
|
||||
export class CountersPage {
|
||||
readonly page: Page;
|
||||
readonly addCounterButton: Locator;
|
||||
readonly addOneThousandCountersButton: Locator;
|
||||
readonly clearCountersButton: Locator;
|
||||
readonly decrementCountButton: Locator;
|
||||
readonly incrementCountButton: Locator;
|
||||
|
||||
readonly total: Locator;
|
||||
readonly counters: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
this.addCounterButton = page.locator("button", { hasText: "Add Counter" });
|
||||
|
||||
this.addOneThousandCountersButton = page.locator("button", {
|
||||
hasText: "Add 1000 Counters",
|
||||
});
|
||||
|
||||
this.clearCountersButton = page.locator("button", {
|
||||
hasText: "Clear Counters",
|
||||
});
|
||||
|
||||
this.decrementCountButton = page.locator("button", {
|
||||
hasText: "-1",
|
||||
});
|
||||
|
||||
this.incrementCountButton = page.locator("button", {
|
||||
hasText: "+1",
|
||||
});
|
||||
|
||||
this.total = page.getByTestId("total");
|
||||
|
||||
this.counters = page.getByTestId("counters");
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto("/");
|
||||
}
|
||||
|
||||
async addCounter() {
|
||||
await Promise.all([
|
||||
this.addCounterButton.waitFor(),
|
||||
this.addCounterButton.click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async addOneThousandCounters() {
|
||||
this.addOneThousandCountersButton.click();
|
||||
}
|
||||
|
||||
async decrementCount() {
|
||||
await Promise.all([
|
||||
this.decrementCountButton.waitFor(),
|
||||
this.decrementCountButton.click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async incrementCount() {
|
||||
await Promise.all([
|
||||
this.incrementCountButton.waitFor(),
|
||||
this.incrementCountButton.click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async clearCounters() {
|
||||
await Promise.all([
|
||||
this.clearCountersButton.waitFor(),
|
||||
this.clearCountersButton.click(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
17
examples/counters_stable/e2e/tests/decrement_count.spec.ts
Normal file
17
examples/counters_stable/e2e/tests/decrement_count.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./counters_page";
|
||||
|
||||
test.describe("Decrement Count", () => {
|
||||
test("should decrement the total count", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.decrementCount();
|
||||
await ui.decrementCount();
|
||||
await ui.decrementCount();
|
||||
|
||||
await expect(ui.total).toHaveText("-3");
|
||||
await expect(ui.counters).toHaveText("1");
|
||||
});
|
||||
});
|
||||
17
examples/counters_stable/e2e/tests/increment_count.spec.ts
Normal file
17
examples/counters_stable/e2e/tests/increment_count.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./counters_page";
|
||||
|
||||
test.describe("Increment Count", () => {
|
||||
test("should increment the total count", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.incrementCount();
|
||||
await ui.incrementCount();
|
||||
await ui.incrementCount();
|
||||
|
||||
await expect(ui.total).toHaveText("3");
|
||||
await expect(ui.counters).toHaveText("1");
|
||||
});
|
||||
});
|
||||
19
examples/counters_stable/e2e/tests/view_counters.spec.ts
Normal file
19
examples/counters_stable/e2e/tests/view_counters.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./counters_page";
|
||||
|
||||
test.describe("View Counters", () => {
|
||||
test("should_see_the_title", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await expect(page).toHaveTitle("Counters (Stable)");
|
||||
});
|
||||
|
||||
test("should see the initial counts", async ({ page }) => {
|
||||
const counters = new CountersPage(page);
|
||||
await counters.goto();
|
||||
|
||||
await expect(counters.total).toHaveText("0");
|
||||
await expect(counters.counters).toHaveText("0");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Counters (Stable)</title>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs/>
|
||||
</head>
|
||||
<body></body>
|
||||
|
||||
11
examples/counters_stable/package.json
Normal file
11
examples/counters_stable/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"private": "true",
|
||||
"scripts": {
|
||||
"start-server": "trunk serve",
|
||||
"e2e": "cargo make test-playwright",
|
||||
"e2e:auto-start": "start-server-and-test start-server http://127.0.0.1:8080 e2e"
|
||||
},
|
||||
"devDependencies": {
|
||||
"start-server-and-test": "^1.15.4"
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
</button>
|
||||
<p>
|
||||
"Total: "
|
||||
<span>{move ||
|
||||
<span id="total" data-testid="total">{move ||
|
||||
counters.get()
|
||||
.iter()
|
||||
.map(|(_, (count, _))| count.get())
|
||||
@@ -64,7 +64,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
.to_string()
|
||||
}</span>
|
||||
" from "
|
||||
<span>{move || counters.with(|counters| counters.len()).to_string()}</span>
|
||||
<span id="counters" data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
|
||||
" counters."
|
||||
</p>
|
||||
<ul>
|
||||
@@ -99,13 +99,13 @@ fn Counter(
|
||||
|
||||
view! { cx,
|
||||
<li>
|
||||
<button on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
|
||||
<button id="decrement_count" on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
|
||||
<input type="text"
|
||||
prop:value={move || value.get().to_string()}
|
||||
on:input=input
|
||||
/>
|
||||
<span>{move || value.get().to_string()}</span>
|
||||
<button on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
|
||||
<button id="increment_count" on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
|
||||
<button on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
|
||||
</li>
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -10,12 +10,10 @@ crate-type = ["cdylib", "rlib"]
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
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 = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4.17"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
simple_logger = "4.0.0"
|
||||
|
||||
@@ -34,10 +34,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<ExampleErrors/>
|
||||
}/>
|
||||
<Route path="" view=|cx| view! { cx, <ExampleErrors/> }/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -66,7 +63,7 @@ pub fn ExampleErrors(cx: Scope) -> impl IntoView {
|
||||
// note that the error boundaries could be placed above in the Router or lower down
|
||||
// in a particular route. The generated errors on the entire page contribute to the
|
||||
// final status code sent by the server when producing ssr pages.
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
<ErrorBoundary fallback=|cx, errors| view!{ cx, <ErrorTemplate errors=errors/>}>
|
||||
<ReturnsError/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
reqwasm = "0.5"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
log = "0.4"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::{error::Result, *};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -8,30 +8,24 @@ pub struct Cat {
|
||||
}
|
||||
|
||||
#[derive(Error, Clone, Debug)]
|
||||
pub enum FetchError {
|
||||
pub enum CatError {
|
||||
#[error("Please request more than zero cats.")]
|
||||
NonZeroCats,
|
||||
#[error("Error loading data from serving.")]
|
||||
Request,
|
||||
#[error("Error deserializaing cat data from request.")]
|
||||
Json,
|
||||
}
|
||||
|
||||
type CatCount = usize;
|
||||
|
||||
async fn fetch_cats(count: CatCount) -> Result<Vec<String>, FetchError> {
|
||||
async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
|
||||
if count > 0 {
|
||||
// make the request
|
||||
let res = reqwasm::http::Request::get(&format!(
|
||||
"https://api.thecatapi.com/v1/images/search?limit={count}",
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| FetchError::Request)?
|
||||
.await?
|
||||
// convert it to JSON
|
||||
.json::<Vec<Cat>>()
|
||||
.await
|
||||
.map_err(|_| FetchError::Json)?
|
||||
.await?
|
||||
// extract the URL field for each cat
|
||||
.into_iter()
|
||||
.take(count)
|
||||
@@ -39,7 +33,7 @@ async fn fetch_cats(count: CatCount) -> Result<Vec<String>, FetchError> {
|
||||
.collect::<Vec<_>>();
|
||||
Ok(res)
|
||||
} else {
|
||||
Err(FetchError::NonZeroCats)
|
||||
Err(CatError::NonZeroCats.into())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
gtk = { version = "0.5.0", package = "gtk4" }
|
||||
@@ -16,12 +16,10 @@ actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1"
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_actix = { path = "../../integrations/actix", default-features = false, optional = true }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
|
||||
@@ -14,12 +14,10 @@ lto = true
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
|
||||
@@ -8,7 +8,7 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features=["template_macro"] }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly", "template_macro"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
# used in rand, but we need to enable js feature
|
||||
|
||||
@@ -11,10 +11,10 @@ axum = { version = "0.6.18", optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_log = "1"
|
||||
cfg-if = "1"
|
||||
leptos = { version = "0.3", default-features = false, features = ["serde"] }
|
||||
leptos_axum = { version = "0.3", optional = true }
|
||||
leptos_meta = { version = "0.3", default-features = false }
|
||||
leptos_router = { version = "0.3", default-features = false }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4"
|
||||
tokio = { version = "1.28.1", optional = true }
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
use cfg_if::cfg_if;
|
||||
use http::status::StatusCode;
|
||||
use leptos::*;
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_axum::ResponseOptions;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
pub enum AppError {
|
||||
#[error("Not Found")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
impl AppError {
|
||||
pub fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
AppError::NotFound => StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A basic function to display errors served by the error boundaries.
|
||||
// Feel free to do more complicated things here than just displaying the error.
|
||||
#[component]
|
||||
pub fn ErrorTemplate(
|
||||
cx: Scope,
|
||||
#[prop(optional)] outside_errors: Option<Errors>,
|
||||
#[prop(optional)] errors: Option<RwSignal<Errors>>,
|
||||
) -> impl IntoView {
|
||||
let errors = match outside_errors {
|
||||
Some(e) => create_rw_signal(cx, e),
|
||||
None => match errors {
|
||||
Some(e) => e,
|
||||
None => panic!("No Errors found and we expected errors!"),
|
||||
},
|
||||
};
|
||||
// Get Errors from Signal
|
||||
let errors = errors.get();
|
||||
|
||||
// Downcast lets us take a type that implements `std::error::Error`
|
||||
let errors: Vec<AppError> = errors
|
||||
.into_iter()
|
||||
.filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned())
|
||||
.collect();
|
||||
println!("Errors: {errors:#?}");
|
||||
|
||||
// Only the response code for the first error is actually sent from the server
|
||||
// this may be customized by the specific application
|
||||
cfg_if! { if #[cfg(feature="ssr")] {
|
||||
let response = use_context::<ResponseOptions>(cx);
|
||||
if let Some(response) = response {
|
||||
response.set_status(errors[0].status_code());
|
||||
}
|
||||
}}
|
||||
|
||||
view! {cx,
|
||||
<h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1>
|
||||
<For
|
||||
// a function that returns the items we're iterating over; a signal is fine
|
||||
each= move || {errors.clone().into_iter().enumerate()}
|
||||
// a unique key for each item as a reference
|
||||
key=|(index, _error)| *index
|
||||
// renders each item to a view
|
||||
view= move |cx, error| {
|
||||
let error_string = error.1.to_string();
|
||||
let error_code= error.1.status_code();
|
||||
view! {
|
||||
cx,
|
||||
<h2>{error_code.to_string()}</h2>
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
}
|
||||
@@ -3,29 +3,27 @@ use cfg_if::cfg_if;
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use axum::response::Response as AxumResponse;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use std::sync::Arc;
|
||||
use leptos::*;
|
||||
use crate::error_template::ErrorTemplate;
|
||||
use crate::error_template::AppError;
|
||||
use leptos::{LeptosOptions, view};
|
||||
use crate::app::App;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let mut errors = Errors::default();
|
||||
errors.insert_with_default_key(AppError::NotFound);
|
||||
let handler = leptos_axum::render_app_to_stream(options.to_owned(), move |cx| view!{cx, <ErrorTemplate outside_errors=errors.clone()/>});
|
||||
res.into_response()
|
||||
} else{
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
move |cx| view!{ cx, <App/> }
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
use cfg_if::cfg_if;
|
||||
pub mod app;
|
||||
pub mod error_template;
|
||||
pub mod fileserv;
|
||||
pub mod fallback;
|
||||
|
||||
cfg_if! { if #[cfg(feature = "hydrate")] {
|
||||
use leptos::*;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use axum::{extract::Extension, routing::post, Router};
|
||||
use axum::{routing::post, Router};
|
||||
use leptos::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use leptos_tailwind::{app::*, fileserv::file_and_error_handler};
|
||||
use leptos_tailwind::{app::*, fallback::file_and_error_handler};
|
||||
use log::info;
|
||||
use std::sync::Arc;
|
||||
|
||||
simple_logger::init_with_level(log::Level::Info)
|
||||
.expect("couldn't initialize logging");
|
||||
@@ -17,20 +16,17 @@ async fn main() {
|
||||
// Alternately a file can be specified such as Some("Cargo.toml")
|
||||
// The file would need to be included with the executable when moved to deployment
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.leptos_routes(
|
||||
leptos_options.clone(),
|
||||
routes,
|
||||
|cx| view! { cx, <App/> },
|
||||
)
|
||||
.leptos_routes(&leptos_options, routes, |cx| view! { cx, <App/> })
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(Extension(Arc::new(leptos_options)));
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
|
||||
@@ -6,6 +6,6 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[patch.crates-io]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_router = { path = "../../router" }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
api-boundary = { path = "api-boundary" }
|
||||
|
||||
@@ -7,8 +7,9 @@ publish = false
|
||||
[dependencies]
|
||||
api-boundary = "*"
|
||||
|
||||
leptos = { version = "0.2.0-alpha2", features = ["stable"] }
|
||||
leptos_router = { version = "0.2.0-alpha2", features = ["stable", "csr"] }
|
||||
leptos = { path = "../../../leptos", features = ["csr"] }
|
||||
leptos_meta = { path = "../../../meta", features = ["csr"] }
|
||||
leptos_router = { path = "../../../router", features = ["csr"] }
|
||||
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1"
|
||||
|
||||
@@ -8,7 +8,7 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -45,7 +45,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
// Button B: pass a closure
|
||||
<ButtonB on_click=move |_| set_right.update(|value| *value = !*value)/>
|
||||
|
||||
// Button B: use a regular event listener
|
||||
// Button C: use a regular event listener
|
||||
// setting an event listener on a component like this applies it
|
||||
// to each of the top-level elements the component returns
|
||||
<ButtonC on:click=move |_| set_italics.update(|value| *value = !*value)/>
|
||||
|
||||
@@ -10,12 +10,12 @@ lto = true
|
||||
[dependencies]
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_router = { path = "../../router", features = ["csr"] }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["csr", "nightly"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
futures = "0.3"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
leptos_meta = { path = "../../meta", features = ["csr"] }
|
||||
leptos_meta = { path = "../../meta", features = ["csr", "nightly"] }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
|
||||
@@ -13,12 +13,10 @@ 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 = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
|
||||
@@ -163,12 +163,11 @@ pub async fn login(
|
||||
|
||||
let user: User = User::get_from_username(username, &pool)
|
||||
.await
|
||||
.ok_or("User does not exist.")
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
|
||||
.ok_or_else(|| {
|
||||
ServerFnError::ServerError("User does not exist.".into())
|
||||
})?;
|
||||
|
||||
match verify(password, &user.password)
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
|
||||
{
|
||||
match verify(password, &user.password)? {
|
||||
true => {
|
||||
auth.login_user(user.id);
|
||||
auth.remember_user(remember.is_some());
|
||||
@@ -204,13 +203,16 @@ pub async fn signup(
|
||||
.bind(username.clone())
|
||||
.bind(password_hashed)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
|
||||
.await?;
|
||||
|
||||
let user = User::get_from_username(username, &pool)
|
||||
.await
|
||||
.ok_or("Signup failed: User does not exist.")
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
|
||||
let user =
|
||||
User::get_from_username(username, &pool)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
ServerFnError::ServerError(
|
||||
"Signup failed: User does not exist.".into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
auth.login_user(user.id);
|
||||
auth.remember_user(remember.is_some());
|
||||
|
||||
@@ -21,14 +21,12 @@ if #[cfg(feature = "ssr")] {
|
||||
|
||||
pub fn pool(cx: Scope) -> Result<SqlitePool, ServerFnError> {
|
||||
use_context::<SqlitePool>(cx)
|
||||
.ok_or("Pool missing.")
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
.ok_or_else(|| ServerFnError::ServerError("Pool missing.".into()))
|
||||
}
|
||||
|
||||
pub fn auth(cx: Scope) -> Result<AuthSession, ServerFnError> {
|
||||
use_context::<AuthSession>(cx)
|
||||
.ok_or("Auth session missing.")
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
.ok_or_else(|| ServerFnError::ServerError("Auth session missing.".into()))
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Clone)]
|
||||
@@ -64,11 +62,7 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
let mut rows =
|
||||
sqlx::query_as::<_, SqlTodo>("SELECT * FROM todos").fetch(&pool);
|
||||
|
||||
while let Some(row) = rows
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
|
||||
{
|
||||
while let Some(row) = rows.try_next().await? {
|
||||
todos.push(row);
|
||||
}
|
||||
|
||||
@@ -117,12 +111,11 @@ pub async fn add_todo(cx: Scope, title: String) -> Result<(), ServerFnError> {
|
||||
pub async fn delete_todo(cx: Scope, id: u16) -> Result<(), ServerFnError> {
|
||||
let pool = pool(cx)?;
|
||||
|
||||
sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
Ok(sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
.map(|_| ())?)
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -177,12 +170,7 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
<hr/>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
<Todos/>
|
||||
</ErrorBoundary>
|
||||
}/> //Route
|
||||
<Route path="" view=|cx| view! { cx, <Todos/> }/> //Route
|
||||
<Route path="signup" view=move |cx| view! {
|
||||
cx,
|
||||
<Signup action=signup/>
|
||||
@@ -226,69 +214,71 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
<input type="submit" value="Add"/>
|
||||
</MultiActionForm>
|
||||
<Transition fallback=move || view! {cx, <p>"Loading..."</p> }>
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
|
||||
}
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
.map(move |todo| {
|
||||
view! {
|
||||
cx,
|
||||
<li>
|
||||
{todo.title}
|
||||
": Created at "
|
||||
{todo.created_at}
|
||||
" by "
|
||||
{
|
||||
todo.user.unwrap_or_default().username
|
||||
}
|
||||
<ActionForm action=delete_todo>
|
||||
<input type="hidden" name="id" value={todo.id}/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
<ErrorBoundary fallback=|cx, errors| view!{ cx, <ErrorTemplate errors=errors/>}>
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
cx,
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
.map(move |todo| {
|
||||
view! {
|
||||
cx,
|
||||
<li>
|
||||
{todo.title}
|
||||
": Created at "
|
||||
{todo.created_at}
|
||||
" by "
|
||||
{
|
||||
todo.user.unwrap_or_default().username
|
||||
}
|
||||
<ActionForm action=delete_todo>
|
||||
<input type="hidden" name="id" value={todo.id}/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
};
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<ul>
|
||||
{existing_todos}
|
||||
{pending_todos}
|
||||
</ul>
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
cx,
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<ul>
|
||||
{existing_todos}
|
||||
{pending_todos}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</ErrorBoundary>
|
||||
</Transition>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -13,12 +13,10 @@ console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_actix = { path = "../../integrations/actix", default-features = false, optional = true }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
thiserror = "1"
|
||||
|
||||
@@ -11,12 +11,10 @@ console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
thiserror = "1"
|
||||
|
||||
@@ -87,22 +87,27 @@ fn Post(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
});
|
||||
|
||||
let post_view = move || {
|
||||
post.with(cx, |post| {
|
||||
post.clone().map(|post| {
|
||||
view! { cx,
|
||||
// render content
|
||||
<h1>{&post.title}</h1>
|
||||
<p>{&post.content}</p>
|
||||
// this view needs to take the `Scope` from the `<Suspense/>`, not
|
||||
// from the parent component, so we take that as an argument and
|
||||
// pass it in under the `<Suspense/>` so that it is correct
|
||||
let post_view = move |cx| {
|
||||
move || {
|
||||
post.with(cx, |post| {
|
||||
post.clone().map(|post| {
|
||||
view! { cx,
|
||||
// render content
|
||||
<h1>{&post.title}</h1>
|
||||
<p>{&post.content}</p>
|
||||
|
||||
// since we're using async rendering for this page,
|
||||
// this metadata should be included in the actual HTML <head>
|
||||
// when it's first served
|
||||
<Title text=post.title/>
|
||||
<Meta name="description" content=post.content/>
|
||||
}
|
||||
// since we're using async rendering for this page,
|
||||
// this metadata should be included in the actual HTML <head>
|
||||
// when it's first served
|
||||
<Title text=post.title/>
|
||||
<Meta name="description" content=post.content/>
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
@@ -121,7 +126,7 @@ fn Post(cx: Scope) -> impl IntoView {
|
||||
</div>
|
||||
}
|
||||
}>
|
||||
{post_view}
|
||||
{post_view(cx)}
|
||||
</ErrorBoundary>
|
||||
</Suspense>
|
||||
}
|
||||
|
||||
@@ -10,12 +10,10 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
log = "0.4"
|
||||
cfg-if = "1.0"
|
||||
|
||||
@@ -4,12 +4,9 @@ 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"] }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
leptos_meta = { path = "../../meta", features = ["csr", "nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["csr", "nightly"] }
|
||||
log = "0.4"
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -16,12 +16,10 @@ console_error_panic_hook = "0.1.7"
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
gloo = { git = "https://github.com/rustwasm/gloo" }
|
||||
@@ -32,7 +30,6 @@ sqlx = { version = "0.6.2", features = [
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
default = ["ssr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
|
||||
@@ -9,7 +9,7 @@ cfg_if! {
|
||||
use sqlx::{Connection, SqliteConnection};
|
||||
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
|
||||
@@ -43,11 +43,7 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
let mut todos = Vec::new();
|
||||
let mut rows =
|
||||
sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
while let Some(row) = rows
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
|
||||
{
|
||||
while let Some(row) = rows.try_next().await? {
|
||||
todos.push(row);
|
||||
}
|
||||
|
||||
@@ -76,12 +72,11 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
Ok(sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
.map(|_| ())?)
|
||||
}
|
||||
|
||||
#[component]
|
||||
|
||||
@@ -11,12 +11,10 @@ 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",
|
||||
] }
|
||||
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 = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
@@ -33,8 +31,6 @@ thiserror = "1.0.38"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
|
||||
@@ -11,7 +11,7 @@ cfg_if! {
|
||||
// use http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode};
|
||||
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
|
||||
@@ -47,11 +47,7 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
let mut todos = Vec::new();
|
||||
let mut rows =
|
||||
sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
while let Some(row) = rows
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
|
||||
{
|
||||
while let Some(row) = rows.try_next().await? {
|
||||
todos.push(row);
|
||||
}
|
||||
|
||||
@@ -93,12 +89,11 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
Ok(sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
.map(|_| ())?)
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -115,9 +110,7 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<Todos/>
|
||||
}/>
|
||||
<Route path="" view=|cx| view! { cx, <Todos/> }/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -11,13 +11,11 @@ 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",
|
||||
] }
|
||||
leptos_viz = { path = "../../integrations/viz", 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 }
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_viz = { path = "../../integrations/viz", optional = true }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
leptos_reactive = { path = "../../leptos_reactive", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
@@ -32,7 +30,6 @@ thiserror = "1.0.38"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
|
||||
@@ -11,7 +11,7 @@ cfg_if! {
|
||||
// use http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode};
|
||||
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
Ok(SqliteConnection::connect("sqlite:Todos.db").await?)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
|
||||
@@ -47,11 +47,7 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
let mut todos = Vec::new();
|
||||
let mut rows =
|
||||
sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
while let Some(row) = rows
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
|
||||
{
|
||||
while let Some(row) = rows.try_next().await? {
|
||||
todos.push(row);
|
||||
}
|
||||
|
||||
@@ -93,12 +89,11 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
Ok(sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
.map(|_| ())?)
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -117,9 +112,7 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
<Todos/>
|
||||
</ErrorBoundary>
|
||||
}/> //Route
|
||||
</Routes>
|
||||
</main>
|
||||
@@ -151,63 +144,65 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
||||
<input type="submit" value="Add"/>
|
||||
</MultiActionForm>
|
||||
<Transition fallback=move || view! {cx, <p>"Loading..."</p> }>
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
|
||||
}
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
.map(move |todo| {
|
||||
view! {
|
||||
cx,
|
||||
<li>
|
||||
{todo.title}
|
||||
<ActionForm action=delete_todo>
|
||||
<input type="hidden" name="id" value={todo.id}/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors/>}>
|
||||
{move || {
|
||||
let existing_todos = {
|
||||
move || {
|
||||
todos.read(cx)
|
||||
.map(move |todos| match todos {
|
||||
Err(e) => {
|
||||
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
cx,
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
.map(move |todo| {
|
||||
view! {
|
||||
cx,
|
||||
<li>
|
||||
{todo.title}
|
||||
<ActionForm action=delete_todo>
|
||||
<input type="hidden" name="id" value={todo.id}/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
};
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<ul>
|
||||
{existing_todos}
|
||||
{pending_todos}
|
||||
</ul>
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
cx,
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect_view(cx)
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<ul>
|
||||
{existing_todos}
|
||||
{pending_todos}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</ErrorBoundary>
|
||||
</Transition>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", default-features = false }
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
log = "0.4"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -13,19 +13,19 @@ cfg-if = "1"
|
||||
leptos_dom = { workspace = true }
|
||||
leptos_macro = { workspace = true }
|
||||
leptos_reactive = { workspace = true }
|
||||
leptos_server = { workspace = true, default-features = false }
|
||||
leptos_server = { workspace = true}
|
||||
leptos_config = { workspace = true }
|
||||
tracing = "0.1"
|
||||
typed-builder = "0.14"
|
||||
server_fn = { workspace = true, default-features = false }
|
||||
server_fn = { workspace = true}
|
||||
web-sys = { version = "0.3.63", optional = true }
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = ".", default-features = false }
|
||||
leptos = { path = "."}
|
||||
|
||||
[features]
|
||||
default = ["csr", "serde"]
|
||||
default = ["serde"]
|
||||
template_macro = ["leptos_dom/web", "web-sys", "wasm-bindgen"]
|
||||
csr = [
|
||||
"leptos_dom/web",
|
||||
@@ -47,11 +47,11 @@ ssr = [
|
||||
"leptos_reactive/ssr",
|
||||
"leptos_server/ssr",
|
||||
]
|
||||
stable = [
|
||||
"leptos_dom/stable",
|
||||
"leptos_macro/stable",
|
||||
"leptos_reactive/stable",
|
||||
"leptos_server/stable",
|
||||
nightly = [
|
||||
"leptos_dom/nightly",
|
||||
"leptos_macro/nightly",
|
||||
"leptos_reactive/nightly",
|
||||
"leptos_server/nightly",
|
||||
]
|
||||
serde = ["leptos_reactive/serde"]
|
||||
serde-lite = ["leptos_reactive/serde-lite"]
|
||||
@@ -60,7 +60,7 @@ rkyv = ["leptos_reactive/rkyv"]
|
||||
tracing = ["leptos_macro/tracing"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable", "tracing", "template_macro", "rustls", "default-tls", "web-sys", "wasm-bindgen"]
|
||||
denylist = ["nightly", "tracing", "template_macro", "rustls", "default-tls", "web-sys", "wasm-bindgen"]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"csr",
|
||||
|
||||
@@ -16,18 +16,36 @@ use leptos_reactive::{
|
||||
/// # use leptos_dom::*; use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// let (value, set_value) = create_signal(cx, Ok(0));
|
||||
/// let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
|
||||
/// let on_input = move |ev| set_value.set(event_target_value(&ev).parse::<i32>());
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <input type="text" on:input=on_input/>
|
||||
/// <ErrorBoundary
|
||||
/// fallback=move |_, _| view! { cx, <p class="error">"Enter a valid number."</p>}
|
||||
/// >
|
||||
/// <p>"Value is: " {value}</p>
|
||||
/// <p>"Value is: " {move || value.get()}</p>
|
||||
/// </ErrorBoundary>
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// ## Interaction with `<Suspense/>`
|
||||
/// If you use this with a `<Suspense/>` or `<Transition/>` component, note that the
|
||||
/// `<ErrorBoundary/>` should go inside the `<Suspense/>`, not the other way around,
|
||||
/// if there’s a chance that the `<ErrorBoundary/>` will begin in the error state.
|
||||
/// This is a limitation of the current design of the two components and the way they
|
||||
/// hydrate. Placing the `<ErrorBoundary/>` outside the `<Suspense/>` means that
|
||||
/// it is rendered on the server without any knowledge of the suspended view, so it
|
||||
/// will always be rendered on the server as if there were no errors, but might need
|
||||
/// to be hydrated with errors, depending on the actual result.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// view! { cx,
|
||||
/// <Suspense fallback=move || view! {cx, <p>"Loading..."</p> }>
|
||||
/// <ErrorBoundary fallback=|cx, errors| view!{ cx, <ErrorTemplate errors=errors/>}>
|
||||
/// {move || {
|
||||
/// /* etc. */
|
||||
/// ```
|
||||
#[component]
|
||||
pub fn ErrorBoundary<F, IV>(
|
||||
cx: Scope,
|
||||
@@ -46,7 +64,25 @@ where
|
||||
provide_context(cx, errors);
|
||||
|
||||
// Run children so that they render and execute resources
|
||||
let children = children(cx).into_view(cx);
|
||||
let children = children(cx);
|
||||
|
||||
#[cfg(all(debug_assertions, feature = "hydrate"))]
|
||||
{
|
||||
use leptos_dom::View;
|
||||
if children.nodes.iter().any(|child| {
|
||||
matches!(child, View::Suspense(_, _))
|
||||
|| matches!(child, View::Component(repr) if repr.name() == "Transition")
|
||||
}) {
|
||||
crate::debug_warn!("You are using a <Suspense/> or \
|
||||
<Transition/> as the direct child of an <ErrorBoundary/>. To ensure correct \
|
||||
hydration, these should be reorganized so that the <ErrorBoundary/> is a child \
|
||||
of the <Suspense/> or <Transition/> instead: \n\
|
||||
\nview! {{ cx,\
|
||||
\n <Suspense fallback=todo!()>\n <ErrorBoundary fallback=todo!()>\n {{move || {{ /* etc. */")
|
||||
}
|
||||
}
|
||||
|
||||
let children = children.into_view(cx);
|
||||
let errors_empty = create_memo(cx, move |_| errors.with(Errors::is_empty));
|
||||
|
||||
move || {
|
||||
|
||||
@@ -26,7 +26,7 @@ use std::hash::Hash;
|
||||
/// <div>
|
||||
/// <For
|
||||
/// // a function that returns the items we're iterating over; a signal is fine
|
||||
/// each=counters
|
||||
/// each=move || counters.get()
|
||||
/// // a unique key for each item
|
||||
/// key=|counter| counter.id
|
||||
/// // renders each item to a view
|
||||
|
||||
@@ -16,14 +16,6 @@
|
||||
//! Join us on our [Discord Channel](https://discord.gg/v38Eef6sWG) to see what the community is building.
|
||||
//! Explore our [Examples](https://github.com/leptos-rs/leptos/tree/main/examples) to see Leptos in action.
|
||||
//!
|
||||
//! # `nightly` Note
|
||||
//! Most of the examples assume you’re using `nightly` Rust. If you’re on stable, note the following:
|
||||
//! 1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.0", 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)
|
||||
//! for examples of the correct API.
|
||||
//!
|
||||
//! # Learning by Example
|
||||
//!
|
||||
//! If you want to see what Leptos is capable of, check out
|
||||
@@ -84,12 +76,10 @@
|
||||
//! - **Server Functions**: the [server](crate::leptos_server) macro, [create_action], and [create_server_action]
|
||||
//!
|
||||
//! # Feature Flags
|
||||
//! - `csr` (*Default*) Client-side rendering: Generate DOM nodes in the browser
|
||||
//! - `nightly`: On `nightly` Rust, enables the function-call syntax for signal getters and setters.
|
||||
//! - `csr` Client-side rendering: Generate DOM nodes in the browser
|
||||
//! - `ssr` Server-side rendering: Generate an HTML string (typically on the server)
|
||||
//! - `hydrate` Hydration: use this to add interactivity to an SSRed Leptos app
|
||||
//! - `stable` By default, Leptos requires `nightly` Rust, which is what allows the ergonomics
|
||||
//! of calling signals as functions. If you need to use `stable`, you will need to call `.get()`
|
||||
//! and `.set()` manually.
|
||||
//! - `serde` (*Default*) In SSR/hydrate mode, uses [serde](https://docs.rs/serde/latest/serde/) to serialize resources and send them
|
||||
//! from the server to the client.
|
||||
//! - `serde-lite` In SSR/hydrate mode, uses [serde-lite](https://docs.rs/serde-lite/latest/serde_lite/) to serialize resources and send them
|
||||
@@ -122,7 +112,7 @@
|
||||
//! <div>
|
||||
//! <button on:click=clear>"Clear"</button>
|
||||
//! <button on:click=decrement>"-1"</button>
|
||||
//! <span>"Value: " {move || value().to_string()} "!"</span>
|
||||
//! <span>"Value: " {move || value.get().to_string()} "!"</span>
|
||||
//! <button on:click=increment>"+1"</button>
|
||||
//! </div>
|
||||
//! }
|
||||
@@ -171,6 +161,11 @@ pub use leptos_dom::{
|
||||
Class, CollectView, Errors, Fragment, HtmlElement, IntoAttribute,
|
||||
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
|
||||
};
|
||||
|
||||
/// Types to make it easier to handle errors in your application.
|
||||
pub mod error {
|
||||
pub use server_fn::error::{Error, Result};
|
||||
}
|
||||
#[cfg(not(any(target_arch = "wasm32", feature = "template_macro")))]
|
||||
pub use leptos_macro::view as template;
|
||||
pub use leptos_macro::{component, server, slot, view, Params};
|
||||
@@ -178,6 +173,7 @@ pub use leptos_reactive::*;
|
||||
pub use leptos_server::{
|
||||
self, create_action, create_multi_action, create_server_action,
|
||||
create_server_multi_action, Action, MultiAction, ServerFn, ServerFnError,
|
||||
ServerFnErrorErr,
|
||||
};
|
||||
pub use server_fn::{self, ServerFn as _};
|
||||
pub use typed_builder;
|
||||
|
||||
@@ -21,7 +21,7 @@ use std::{cell::RefCell, rc::Rc};
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <Show
|
||||
/// when=move || value() < 5
|
||||
/// when=move || value.get() < 5
|
||||
/// fallback=|cx| view! { cx, "Big number!" }
|
||||
/// >
|
||||
/// "Small number!"
|
||||
|
||||
@@ -21,7 +21,7 @@ use std::rc::Rc;
|
||||
///
|
||||
/// let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
|
||||
///
|
||||
/// let cats = create_resource(cx, cat_count, |count| fetch_cats(count));
|
||||
/// let cats = create_resource(cx, move || cat_count.get(), |count| fetch_cats(count));
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <div>
|
||||
|
||||
@@ -32,7 +32,8 @@ use std::{
|
||||
/// let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
|
||||
/// let (pending, set_pending) = create_signal(cx, false);
|
||||
///
|
||||
/// let cats = create_resource(cx, cat_count, |count| fetch_cats(count));
|
||||
/// let cats =
|
||||
/// create_resource(cx, move || cat_count.get(), |count| fetch_cats(count));
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <div>
|
||||
@@ -64,7 +65,7 @@ use std::{
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "info", skip_all)
|
||||
)]
|
||||
#[component]
|
||||
#[component(transparent)]
|
||||
pub fn Transition<F, E>(
|
||||
cx: Scope,
|
||||
/// Will be displayed while resources are pending.
|
||||
|
||||
@@ -12,12 +12,12 @@ actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../../..", default-features = false, features = ["serde"] }
|
||||
leptos = { path = "../../..", features = ["serde"] }
|
||||
leptos_actix = { path = "../../../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../../../router", default-features = false }
|
||||
leptos_router = { path = "../../../../router"}
|
||||
log = "0.4"
|
||||
simple_logger = "4"
|
||||
wasm-bindgen = "0.2.85"
|
||||
wasm-bindgen = "0.2.87"
|
||||
serde = "1.0.159"
|
||||
tokio = { version = "1.27.0", features = ["time"], optional = true }
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
>
|
||||
<Route path="" view=|cx| view! { cx, <Nested/> }/>
|
||||
<Route path="inside" view=|cx| view! { cx, <NestedResourceInside/> }/>
|
||||
<Route path="single" view=|cx| view! { cx, <Single/> }/>
|
||||
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
|
||||
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
|
||||
@@ -69,6 +70,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
>
|
||||
<Route path="" view=|cx| view! { cx, <Nested/> }/>
|
||||
<Route path="inside" view=|cx| view! { cx, <NestedResourceInside/> }/>
|
||||
<Route path="single" view=|cx| view! { cx, <Single/> }/>
|
||||
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
|
||||
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
|
||||
@@ -85,6 +87,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
>
|
||||
<Route path="" view=|cx| view! { cx, <Nested/> }/>
|
||||
<Route path="inside" view=|cx| view! { cx, <NestedResourceInside/> }/>
|
||||
<Route path="single" view=|cx| view! { cx, <Single/> }/>
|
||||
<Route path="parallel" view=|cx| view! { cx, <Parallel/> }/>
|
||||
<Route path="inside-component" view=|cx| view! { cx, <InsideComponent/> }/>
|
||||
@@ -101,6 +104,7 @@ fn SecondaryNav(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<nav>
|
||||
<A href="" exact=true>"Nested"</A>
|
||||
<A href="inside" exact=true>"Nested (resource created inside)"</A>
|
||||
<A href="single">"Single"</A>
|
||||
<A href="parallel">"Parallel"</A>
|
||||
<A href="inside-component">"Inside Component"</A>
|
||||
@@ -139,6 +143,43 @@ fn Nested(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn NestedResourceInside(cx: Scope) -> impl IntoView {
|
||||
let one_second = create_resource(cx, || (), one_second_fn);
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
view! { cx,
|
||||
<div>
|
||||
<Suspense fallback=|| "Loading 1...">
|
||||
"One Second: "
|
||||
{move || {
|
||||
one_second.read(cx).map(|_| {
|
||||
let two_second = create_resource(cx, || (), move |_| async move {
|
||||
leptos::log!("creating two_second resource");
|
||||
two_second_fn(()).await
|
||||
});
|
||||
view! { cx,
|
||||
<p>{move || one_second.read(cx).map(|_| "Loaded 1!")}</p>
|
||||
<Suspense fallback=|| "Loading 2...">
|
||||
"Two Second: "
|
||||
{move || {
|
||||
two_second.read(cx).map(|x| view! { cx,
|
||||
"Loaded 2 (created inside first suspense)!: "
|
||||
{format!("{x:?}")}
|
||||
<button on:click=move |_| set_count.update(|n| *n += 1)>
|
||||
{count}
|
||||
</button>
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
}
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Parallel(cx: Scope) -> impl IntoView {
|
||||
let one_second = create_resource(cx, || (), one_second_fn);
|
||||
|
||||
@@ -18,6 +18,7 @@ indexmap = "1.9"
|
||||
itertools = "0.10"
|
||||
js-sys = "0.3"
|
||||
leptos_reactive = { workspace = true }
|
||||
server_fn = { workspace = true }
|
||||
once_cell = "1"
|
||||
pad-adapter = "0.1"
|
||||
paste = "1"
|
||||
@@ -158,8 +159,8 @@ features = [
|
||||
default = []
|
||||
web = ["leptos_reactive/csr"]
|
||||
ssr = ["leptos_reactive/ssr"]
|
||||
stable = ["leptos_reactive/stable"]
|
||||
nightly = ["leptos_reactive/nightly"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable"]
|
||||
denylist = ["nightly"]
|
||||
skip_feature_sets = [["web", "ssr"]]
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../../leptos", default-features = false }
|
||||
leptos = { path = "../../../leptos", features = ["nightly"] }
|
||||
actix-web = { version = "4", optional = true }
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
|
||||
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1"
|
||||
gloo = { version = "0.8", features = ["futures"] }
|
||||
leptos = { path = "../../../leptos", features = ["tracing"] }
|
||||
leptos = { path = "../../../leptos", features = ["nightly", "csr", "tracing"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
tracing-subscriber-wasm = "0.1"
|
||||
|
||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos = { path = "../../leptos", features = ["csr", "nightly"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -221,6 +221,12 @@ impl ComponentRepr {
|
||||
view_marker: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
/// Returns the name of the component.
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
|
||||
/// A user-defined `leptos` component.
|
||||
|
||||
@@ -653,6 +653,7 @@ fn apply_opts<K: Eq + Hash>(
|
||||
&& cmds.moved.is_empty()
|
||||
{
|
||||
cmds.clear = true;
|
||||
cmds.removed.clear();
|
||||
|
||||
cmds.added
|
||||
.iter_mut()
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use crate::{HydrationCtx, IntoView};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos_reactive::{signal_prelude::*, use_context, RwSignal};
|
||||
use std::{borrow::Cow, collections::HashMap, error::Error, sync::Arc};
|
||||
use server_fn::error::Error;
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
/// A struct to hold all the possible errors that could be provided by child Views
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[repr(transparent)]
|
||||
pub struct Errors(HashMap<ErrorKey, Arc<dyn Error + Send + Sync>>);
|
||||
pub struct Errors(HashMap<ErrorKey, Error>);
|
||||
|
||||
/// A unique key for an error that occurs at a particular location in the user interface.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
|
||||
@@ -24,7 +25,7 @@ where
|
||||
}
|
||||
|
||||
impl IntoIterator for Errors {
|
||||
type Item = (ErrorKey, Arc<dyn Error + Send + Sync>);
|
||||
type Item = (ErrorKey, Error);
|
||||
type IntoIter = IntoIter;
|
||||
|
||||
#[inline(always)]
|
||||
@@ -35,15 +36,10 @@ impl IntoIterator for Errors {
|
||||
|
||||
/// An owning iterator over all the errors contained in the [Errors] struct.
|
||||
#[repr(transparent)]
|
||||
pub struct IntoIter(
|
||||
std::collections::hash_map::IntoIter<
|
||||
ErrorKey,
|
||||
Arc<dyn Error + Send + Sync>,
|
||||
>,
|
||||
);
|
||||
pub struct IntoIter(std::collections::hash_map::IntoIter<ErrorKey, Error>);
|
||||
|
||||
impl Iterator for IntoIter {
|
||||
type Item = (ErrorKey, Arc<dyn Error + Send + Sync>);
|
||||
type Item = (ErrorKey, Error);
|
||||
|
||||
#[inline(always)]
|
||||
fn next(
|
||||
@@ -55,16 +51,10 @@ impl Iterator for IntoIter {
|
||||
|
||||
/// An iterator over all the errors contained in the [Errors] struct.
|
||||
#[repr(transparent)]
|
||||
pub struct Iter<'a>(
|
||||
std::collections::hash_map::Iter<
|
||||
'a,
|
||||
ErrorKey,
|
||||
Arc<dyn Error + Send + Sync>,
|
||||
>,
|
||||
);
|
||||
pub struct Iter<'a>(std::collections::hash_map::Iter<'a, ErrorKey, Error>);
|
||||
|
||||
impl<'a> Iterator for Iter<'a> {
|
||||
type Item = (&'a ErrorKey, &'a Arc<dyn Error + Send + Sync>);
|
||||
type Item = (&'a ErrorKey, &'a Error);
|
||||
|
||||
#[inline(always)]
|
||||
fn next(
|
||||
@@ -77,7 +67,7 @@ impl<'a> Iterator for Iter<'a> {
|
||||
impl<T, E> IntoView for Result<T, E>
|
||||
where
|
||||
T: IntoView + 'static,
|
||||
E: Error + Send + Sync + 'static,
|
||||
E: Into<Error>,
|
||||
{
|
||||
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
|
||||
let id = ErrorKey(HydrationCtx::peek().fragment.to_string().into());
|
||||
@@ -92,6 +82,7 @@ where
|
||||
stuff.into_view(cx)
|
||||
}
|
||||
Err(error) => {
|
||||
let error = error.into();
|
||||
match errors {
|
||||
Some(errors) => {
|
||||
errors.update({
|
||||
@@ -133,6 +124,7 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Errors {
|
||||
/// Returns `true` if there are no errors.
|
||||
#[inline(always)]
|
||||
@@ -143,24 +135,21 @@ impl Errors {
|
||||
/// Add an error to Errors that will be processed by `<ErrorBoundary/>`
|
||||
pub fn insert<E>(&mut self, key: ErrorKey, error: E)
|
||||
where
|
||||
E: Error + Send + Sync + 'static,
|
||||
E: Into<Error>,
|
||||
{
|
||||
self.0.insert(key, Arc::new(error));
|
||||
self.0.insert(key, error.into());
|
||||
}
|
||||
|
||||
/// Add an error with the default key for errors outside the reactive system
|
||||
pub fn insert_with_default_key<E>(&mut self, error: E)
|
||||
where
|
||||
E: Error + Send + Sync + 'static,
|
||||
E: Into<Error>,
|
||||
{
|
||||
self.0.insert(Default::default(), Arc::new(error));
|
||||
self.0.insert(Default::default(), error.into());
|
||||
}
|
||||
|
||||
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
|
||||
pub fn remove(
|
||||
&mut self,
|
||||
key: &ErrorKey,
|
||||
) -> Option<Arc<dyn Error + Send + Sync>> {
|
||||
pub fn remove(&mut self, key: &ErrorKey) -> Option<Error> {
|
||||
self.0.remove(key)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ cfg_if! {
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
// We can tell if we start in hydration mode by checking to see if the
|
||||
// id "_0-0-0" is present in the DOM. If it is, we know we are hydrating from
|
||||
// id "_0-1" is present in the DOM. If it is, we know we are hydrating from
|
||||
// the server, if not, we are starting off in CSR
|
||||
thread_local! {
|
||||
static HYDRATION_COMMENTS: LazyCell<HashMap<String, web_sys::Comment>> = LazyCell::new(|| {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#![deny(missing_docs)]
|
||||
#![forbid(unsafe_code)]
|
||||
#![cfg_attr(not(feature = "stable"), feature(fn_traits))]
|
||||
#![cfg_attr(not(feature = "stable"), feature(unboxed_closures))]
|
||||
#![cfg_attr(feature = "nightly", feature(fn_traits))]
|
||||
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
|
||||
|
||||
//! The DOM implementation for `leptos`.
|
||||
|
||||
@@ -1125,7 +1125,7 @@ viewable_primitive![
|
||||
];
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(not(feature = "stable"))] {
|
||||
if #[cfg(feature = "nightly")] {
|
||||
viewable_primitive! {
|
||||
std::backtrace::Backtrace
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ impl<T: ElementDescriptor> Clone for NodeRef<T> {
|
||||
impl<T: ElementDescriptor + 'static> Copy for NodeRef<T> {}
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(not(feature = "stable"))] {
|
||||
if #[cfg(feature = "nightly")] {
|
||||
impl<T: Clone + ElementDescriptor + 'static> FnOnce<()> for NodeRef<T> {
|
||||
type Output = Option<HtmlElement<T>>;
|
||||
|
||||
|
||||
@@ -221,66 +221,105 @@ pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacemen
|
||||
blocking_fragments
|
||||
.push(async move { (fragment_id, data.out_of_order.await) });
|
||||
} else {
|
||||
fragments
|
||||
.push(async move { (fragment_id, data.out_of_order.await) });
|
||||
fragments.push(Box::pin(async move {
|
||||
(fragment_id.clone(), data.out_of_order.await)
|
||||
})
|
||||
as Pin<Box<dyn Future<Output = (String, String)>>>);
|
||||
}
|
||||
}
|
||||
|
||||
let stream = futures::stream::once(
|
||||
// HTML for the view function and script to store resources
|
||||
async move {
|
||||
let resolvers = format!(
|
||||
"<script>__LEPTOS_PENDING_RESOURCES = \
|
||||
{pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \
|
||||
Map();__LEPTOS_RESOURCE_RESOLVERS = new Map();</script>"
|
||||
);
|
||||
|
||||
if replace_blocks {
|
||||
let mut blocks = Vec::with_capacity(blocking_fragments.len());
|
||||
while let Some((blocked_id, blocked_fragment)) =
|
||||
blocking_fragments.next().await
|
||||
{
|
||||
blocks.push((blocked_id, blocked_fragment));
|
||||
}
|
||||
|
||||
let prefix = prefix(cx);
|
||||
|
||||
let mut shell = shell;
|
||||
|
||||
for (blocked_id, blocked_fragment) in blocks {
|
||||
let open = format!("<!--suspense-open-{blocked_id}-->");
|
||||
let close = format!("<!--suspense-close-{blocked_id}-->");
|
||||
let (first, rest) =
|
||||
shell.split_once(&open).unwrap_or_default();
|
||||
let (_fallback, rest) =
|
||||
rest.split_once(&close).unwrap_or_default();
|
||||
|
||||
shell = format!("{first}{blocked_fragment}{rest}").into();
|
||||
}
|
||||
|
||||
format!("{prefix}{shell}{resolvers}")
|
||||
} else {
|
||||
let mut blocking = String::new();
|
||||
let mut blocking_fragments =
|
||||
fragments_to_chunks(blocking_fragments);
|
||||
|
||||
while let Some(fragment) = blocking_fragments.next().await {
|
||||
blocking.push_str(&fragment);
|
||||
}
|
||||
let prefix = prefix(cx);
|
||||
format!("{prefix}{shell}{resolvers}{blocking}")
|
||||
}
|
||||
},
|
||||
)
|
||||
.chain(ooo_body_stream_recurse(cx, fragments, serializers));
|
||||
|
||||
(stream, runtime, scope)
|
||||
}
|
||||
|
||||
fn ooo_body_stream_recurse(
|
||||
cx: Scope,
|
||||
fragments: FuturesUnordered<PinnedFuture<(String, String)>>,
|
||||
serializers: FuturesUnordered<PinnedFuture<(ResourceId, String)>>,
|
||||
) -> Pin<Box<dyn Stream<Item = String>>> {
|
||||
// resources and fragments
|
||||
// stream HTML for each <Suspense/> as it resolves
|
||||
let fragments = fragments_to_chunks(fragments);
|
||||
// stream data for each Resource as it resolves
|
||||
let resources = render_serializers(serializers);
|
||||
|
||||
// HTML for the view function and script to store resources
|
||||
let stream = futures::stream::once(async move {
|
||||
let resolvers = format!(
|
||||
"<script>__LEPTOS_PENDING_RESOURCES = \
|
||||
{pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \
|
||||
Map();__LEPTOS_RESOURCE_RESOLVERS = new Map();</script>"
|
||||
);
|
||||
|
||||
if replace_blocks {
|
||||
let mut blocks = Vec::with_capacity(blocking_fragments.len());
|
||||
while let Some((blocked_id, blocked_fragment)) =
|
||||
blocking_fragments.next().await
|
||||
{
|
||||
blocks.push((blocked_id, blocked_fragment));
|
||||
}
|
||||
|
||||
let prefix = prefix(cx);
|
||||
|
||||
let mut shell = shell;
|
||||
|
||||
for (blocked_id, blocked_fragment) in blocks {
|
||||
let open = format!("<!--suspense-open-{blocked_id}-->");
|
||||
let close = format!("<!--suspense-close-{blocked_id}-->");
|
||||
let (first, rest) = shell.split_once(&open).unwrap_or_default();
|
||||
let (_fallback, rest) =
|
||||
rest.split_once(&close).unwrap_or_default();
|
||||
|
||||
shell = format!("{first}{blocked_fragment}{rest}").into();
|
||||
}
|
||||
|
||||
format!("{prefix}{shell}{resolvers}")
|
||||
} else {
|
||||
let mut blocking = String::new();
|
||||
let mut blocking_fragments =
|
||||
fragments_to_chunks(blocking_fragments);
|
||||
|
||||
while let Some(fragment) = blocking_fragments.next().await {
|
||||
blocking.push_str(&fragment);
|
||||
}
|
||||
let prefix = prefix(cx);
|
||||
format!("{prefix}{shell}{resolvers}{blocking}")
|
||||
}
|
||||
})
|
||||
// TODO these should be combined again in a way that chains them appropriately
|
||||
// such that individual resources can resolve before all fragments are done
|
||||
.chain(fragments)
|
||||
.chain(resources);
|
||||
|
||||
(stream, runtime, scope)
|
||||
Box::pin(
|
||||
// TODO these should be combined again in a way that chains them appropriately
|
||||
// such that individual resources can resolve before all fragments are done
|
||||
fragments.chain(resources).chain(
|
||||
futures::stream::once(async move {
|
||||
let pending = cx.pending_fragments();
|
||||
if pending.len() > 0 {
|
||||
let fragments = FuturesUnordered::new();
|
||||
let serializers = cx.serialization_resolvers();
|
||||
for (fragment_id, data) in pending {
|
||||
fragments.push(Box::pin(async move {
|
||||
(fragment_id.clone(), data.out_of_order.await)
|
||||
})
|
||||
as Pin<Box<dyn Future<Output = (String, String)>>>);
|
||||
}
|
||||
Box::pin(ooo_body_stream_recurse(
|
||||
cx,
|
||||
fragments,
|
||||
serializers,
|
||||
))
|
||||
as Pin<Box<dyn Stream<Item = String>>>
|
||||
} else {
|
||||
Box::pin(futures::stream::once(async move {
|
||||
Default::default()
|
||||
}))
|
||||
}
|
||||
})
|
||||
.flatten(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
|
||||
@@ -94,13 +94,7 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
let runtime = create_runtime();
|
||||
|
||||
let (
|
||||
(
|
||||
blocking_fragments_ready,
|
||||
chunks,
|
||||
prefix,
|
||||
pending_resources,
|
||||
serializers,
|
||||
),
|
||||
(blocking_fragments_ready, chunks, prefix, pending_resources),
|
||||
scope_id,
|
||||
_,
|
||||
) = run_scope_undisposed(runtime, |cx| {
|
||||
@@ -115,7 +109,6 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
view.into_stream_chunks(cx),
|
||||
prefix,
|
||||
serde_json::to_string(&cx.pending_resources()).unwrap(),
|
||||
cx.serialization_resolvers(),
|
||||
)
|
||||
});
|
||||
let cx = Scope {
|
||||
@@ -130,7 +123,7 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
let remaining_chunks = handle_blocking_chunks(tx.clone(), chunks).await;
|
||||
let prefix = prefix(cx);
|
||||
prefix_tx.send(prefix).expect("to send prefix");
|
||||
handle_chunks(tx, remaining_chunks).await;
|
||||
handle_chunks(cx, tx, remaining_chunks).await;
|
||||
});
|
||||
|
||||
let stream = futures::stream::once(async move {
|
||||
@@ -147,7 +140,13 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
)
|
||||
})
|
||||
.chain(rx)
|
||||
.chain(render_serializers(serializers));
|
||||
.chain(
|
||||
futures::stream::once(async move {
|
||||
let serializers = cx.serialization_resolvers();
|
||||
render_serializers(serializers)
|
||||
})
|
||||
.flatten(),
|
||||
);
|
||||
|
||||
(stream, runtime, scope_id)
|
||||
}
|
||||
@@ -196,6 +195,7 @@ async fn handle_blocking_chunks(
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
#[async_recursion(?Send)]
|
||||
async fn handle_chunks(
|
||||
cx: Scope,
|
||||
tx: UnboundedSender<String>,
|
||||
chunks: VecDeque<StreamChunk>,
|
||||
) {
|
||||
@@ -210,7 +210,7 @@ async fn handle_chunks(
|
||||
|
||||
// send the inner stream
|
||||
let suspended = chunks.await;
|
||||
handle_chunks(tx.clone(), suspended).await;
|
||||
handle_chunks(cx, tx.clone(), suspended).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,9 @@ default = ["ssr"]
|
||||
csr = []
|
||||
hydrate = []
|
||||
ssr = []
|
||||
stable = ["server_fn_macro/stable"]
|
||||
nightly = ["server_fn_macro/nightly"]
|
||||
tracing = []
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable", "tracing"]
|
||||
denylist = ["nightly", "tracing"]
|
||||
skip_feature_sets = [["csr", "hydrate"], ["hydrate", "csr"], ["hydrate", "ssr"]]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![cfg_attr(not(feature = "stable"), feature(proc_macro_span))]
|
||||
#![cfg_attr(feature = "nightly", feature(proc_macro_span))]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
#[macro_use]
|
||||
@@ -94,7 +94,7 @@ mod template;
|
||||
/// Attributes can take a wide variety of primitive types that can be converted to strings. They can also
|
||||
/// take an `Option`, in which case `Some` sets the attribute and `None` removes the attribute.
|
||||
///
|
||||
/// ```rust
|
||||
/// ```rust,ignore
|
||||
/// # use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
@@ -102,11 +102,11 @@ mod template;
|
||||
///
|
||||
/// view! {
|
||||
/// cx,
|
||||
/// // ❌ not like this: `count()` returns an `i32`, not a function
|
||||
/// <p>{count()}</p>
|
||||
/// // ❌ not like this: `count.get()` returns an `i32`, not a function
|
||||
/// <p>{count.get()}</p>
|
||||
/// // ✅ this is good: Leptos sees the function and knows it's a dynamic value
|
||||
/// <p>{move || count.get()}</p>
|
||||
/// // 🔥 `count` is itself a function, so you can pass it directly (unless you're on `stable`)
|
||||
/// // 🔥 with the `nightly` feature, `count` is a function, so `count` itself can be passed directly into the view
|
||||
/// <p>{count}</p>
|
||||
/// }
|
||||
/// # ;
|
||||
@@ -147,9 +147,9 @@ mod template;
|
||||
/// <input
|
||||
/// type="text"
|
||||
/// name="user_name"
|
||||
/// value={name} // this only sets the default value!
|
||||
/// prop:value={name} // here's how you update values. Sorry, I didn’t invent the DOM.
|
||||
/// on:click=move |ev| set_name(event_target_value(&ev)) // `event_target_value` is a useful little Leptos helper
|
||||
/// value={move || name.get()} // this only sets the default value!
|
||||
/// prop:value={move || name.get()} // here's how you update values. Sorry, I didn’t invent the DOM.
|
||||
/// on:click=move |ev| set_name.set(event_target_value(&ev)) // `event_target_value` is a useful little Leptos helper
|
||||
/// />
|
||||
/// }
|
||||
/// # ;
|
||||
@@ -163,7 +163,7 @@ mod template;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// let (count, set_count) = create_signal(cx, 2);
|
||||
/// view! { cx, <div class:hidden-div={move || count() < 3}>"Now you see me, now you don’t."</div> }
|
||||
/// view! { cx, <div class:hidden-div={move || count.get() < 3}>"Now you see me, now you don’t."</div> }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # });
|
||||
@@ -176,7 +176,7 @@ mod template;
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// let (count, set_count) = create_signal(cx, 2);
|
||||
/// // `hidden-div-25` is invalid at the moment
|
||||
/// view! { cx, <div class:hidden-div-25={move || count() < 3}>"Now you see me, now you don’t."</div> }
|
||||
/// view! { cx, <div class:hidden-div-25={move || count.get() < 3}>"Now you see me, now you don’t."</div> }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # });
|
||||
@@ -191,7 +191,7 @@ mod template;
|
||||
/// // this allows you to use CSS frameworks that include complex class names
|
||||
/// view! { cx,
|
||||
/// <div
|
||||
/// class=("is-[this_-_really]-necessary-42", move || count() < 3)
|
||||
/// class=("is-[this_-_really]-necessary-42", move || count.get() < 3)
|
||||
/// >
|
||||
/// "Now you see me, now you don’t."
|
||||
/// </div>
|
||||
@@ -211,9 +211,9 @@ mod template;
|
||||
/// view! { cx,
|
||||
/// <div
|
||||
/// style="position: absolute"
|
||||
/// style:left=move || format!("{}px", x())
|
||||
/// style:top=move || format!("{}px", y())
|
||||
/// style=("background-color", move || format!("rgb({}, {}, 100)", x(), y()))
|
||||
/// style:left=move || format!("{}px", x.get())
|
||||
/// style:top=move || format!("{}px", y.get())
|
||||
/// style=("background-color", move || format!("rgb({}, {}, 100)", x.get(), y.get()))
|
||||
/// >
|
||||
/// "Moves when coordinates change"
|
||||
/// </div>
|
||||
@@ -285,7 +285,7 @@ mod template;
|
||||
///
|
||||
/// // create event handlers for our buttons
|
||||
/// // note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
|
||||
/// let clear = move |_ev| set_value(0);
|
||||
/// let clear = move |_ev| set_value.set(0);
|
||||
/// let decrement = move |_ev| set_value.update(|value| *value -= 1);
|
||||
/// let increment = move |_ev| set_value.update(|value| *value += 1);
|
||||
///
|
||||
@@ -295,7 +295,7 @@ mod template;
|
||||
/// <div>
|
||||
/// <button on:click=clear>"Clear"</button>
|
||||
/// <button on:click=decrement>"-1"</button>
|
||||
/// <span>"Value: " {move || value().to_string()} "!"</span>
|
||||
/// <span>"Value: " {move || value.get().to_string()} "!"</span>
|
||||
/// <button on:click=increment>"+1"</button>
|
||||
/// </div>
|
||||
/// }
|
||||
@@ -381,7 +381,7 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
|
||||
fn normalized_call_site(site: proc_macro::Span) -> Option<String> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(all(debug_assertions, not(feature = "stable")))] {
|
||||
if #[cfg(all(debug_assertions, feature = "nightly"))] {
|
||||
Some(leptos_hot_reload::span_to_stable_id(
|
||||
site.source_file().path(),
|
||||
site.into()
|
||||
@@ -467,7 +467,7 @@ pub fn template(tokens: TokenStream) -> TokenStream {
|
||||
/// // return the user interface, which will be automatically updated
|
||||
/// // when signal values change
|
||||
/// view! { cx,
|
||||
/// <p>"Your name is " {name} " and you are " {age} " years old."</p>
|
||||
/// <p>"Your name is " {name} " and you are " {move || age.get()} " years old."</p>
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
|
||||
@@ -581,7 +581,7 @@ fn attribute_to_tokens_ssr<'a>(
|
||||
|| name.strip_prefix("style:").is_some()
|
||||
{
|
||||
// ignore props for SSR
|
||||
// ignore classes and sdtyles: we'll handle these separately
|
||||
// ignore classes and styles: we'll handle these separately
|
||||
} else if name == "inner_html" {
|
||||
return attr.value();
|
||||
} else {
|
||||
@@ -606,7 +606,9 @@ fn attribute_to_tokens_ssr<'a>(
|
||||
if let Some(value) = value_to_string(value) {
|
||||
template.push_str(&name);
|
||||
template.push_str("=\"");
|
||||
template.push_str(&value);
|
||||
template.push_str(&html_escape::encode_quoted_attribute(
|
||||
&value,
|
||||
));
|
||||
template.push('"');
|
||||
} else {
|
||||
template.push_str("{}");
|
||||
@@ -729,7 +731,9 @@ fn set_class_attribute_ssr(
|
||||
{
|
||||
template.push_str(" class=\"");
|
||||
|
||||
template.push_str(&static_class_attr);
|
||||
template.push_str(&html_escape::encode_quoted_attribute(
|
||||
&static_class_attr,
|
||||
));
|
||||
|
||||
for (_span, value) in dyn_class_attr {
|
||||
if let Some(value) = value {
|
||||
|
||||
@@ -68,14 +68,14 @@ hydrate = [
|
||||
"dep:web-sys",
|
||||
]
|
||||
ssr = ["dep:tokio"]
|
||||
stable = []
|
||||
nightly = []
|
||||
serde = []
|
||||
serde-lite = ["dep:serde-lite"]
|
||||
miniserde = ["dep:miniserde"]
|
||||
rkyv = ["dep:rkyv", "dep:bytecheck"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable"]
|
||||
denylist = ["nightly"]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"csr",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user