mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 11:21:55 -05:00
Compare commits
32 Commits
server-fn-
...
0.5.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bee9bd8f67 | ||
|
|
8d3874f8a9 | ||
|
|
bade16d227 | ||
|
|
e0a132bde3 | ||
|
|
4d7e1f4d26 | ||
|
|
700eee6604 | ||
|
|
694ed61e4c | ||
|
|
d7330097ba | ||
|
|
c65a3a6ca3 | ||
|
|
793c191619 | ||
|
|
6c3e2fe53e | ||
|
|
08c419e3ee | ||
|
|
736f4185b5 | ||
|
|
9cc0fc8c49 | ||
|
|
8f067dcde7 | ||
|
|
ad6eb58fe1 | ||
|
|
3f3ab1c3c8 | ||
|
|
9adae32847 | ||
|
|
b8098e7992 | ||
|
|
bef4d0dd3b | ||
|
|
a789100e22 | ||
|
|
abeca70625 | ||
|
|
cc293b1170 | ||
|
|
8ab62c17c6 | ||
|
|
cf14e857ca | ||
|
|
c322ef38fd | ||
|
|
c9cc493063 | ||
|
|
fb48f7f117 | ||
|
|
c344e54cf6 | ||
|
|
7306ecccbc | ||
|
|
b98174db7a | ||
|
|
e48f66694d |
1
.github/workflows/check-examples.yml
vendored
1
.github/workflows/check-examples.yml
vendored
@@ -26,3 +26,4 @@ jobs:
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "check"
|
||||
toolchain: nightly
|
||||
|
||||
47
.github/workflows/check-stable.yml
vendored
47
.github/workflows/check-stable.yml
vendored
@@ -2,50 +2,25 @@ name: Check stable
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
get-leptos-changed:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
|
||||
test:
|
||||
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
name: Check
|
||||
needs: [get-leptos-changed]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- stable
|
||||
os:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Add wasm32-unknown-unknown
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
|
||||
- name: Setup cargo-make
|
||||
uses: davidB/rust-cargo-make@v1
|
||||
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo check on all examples
|
||||
run: cargo make --profile=github-actions check-stable
|
||||
directory: [examples/counters_stable, examples/counter_without_macros]
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "check"
|
||||
toolchain: stable
|
||||
|
||||
1
.github/workflows/ci-changed-examples.yml
vendored
1
.github/workflows/ci-changed-examples.yml
vendored
@@ -29,3 +29,4 @@ jobs:
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly
|
||||
|
||||
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -41,3 +41,4 @@ jobs:
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly
|
||||
|
||||
34
.github/workflows/run-cargo-make-task.yml
vendored
34
.github/workflows/run-cargo-make-task.yml
vendored
@@ -9,6 +9,9 @@ on:
|
||||
cargo_make_task:
|
||||
required: true
|
||||
type: string
|
||||
toolchain:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -16,14 +19,8 @@ env:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- nightly
|
||||
os:
|
||||
- ubuntu-latest
|
||||
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Setup environment
|
||||
@@ -32,7 +29,7 @@ jobs:
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
toolchain: ${{ inputs.toolchain }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
@@ -98,14 +95,17 @@ jobs:
|
||||
|
||||
- name: Maybe install playwright browser dependencies
|
||||
run: |
|
||||
playwright_count=$(find ${{inputs.directory}} -name playwright.config.ts | wc -l)
|
||||
if [ $playwright_count -eq 1 ]; then
|
||||
echo playwright required
|
||||
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
|
||||
else
|
||||
echo playwright is not required
|
||||
fi
|
||||
for pw_path in $(find ${{inputs.directory}} -name playwright.config.ts)
|
||||
do
|
||||
pw_dir=$(dirname $pw_path)
|
||||
if [ ! -v $pw_dir ]; then
|
||||
echo "Playwright required in $pw_dir"
|
||||
cd $pw_dir
|
||||
pnpm dlx playwright install --with-deps
|
||||
else
|
||||
echo Playwright is not required
|
||||
fi
|
||||
done
|
||||
|
||||
# Run Cargo Make Task
|
||||
- name: ${{ inputs.cargo_make_task }}
|
||||
|
||||
1
.github/workflows/verify-all-examples.yml
vendored
1
.github/workflows/verify-all-examples.yml
vendored
@@ -23,3 +23,4 @@ jobs:
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,3 +9,5 @@ Cargo.lock
|
||||
.idea
|
||||
.direnv
|
||||
.envrc
|
||||
|
||||
.vscode
|
||||
28
Cargo.toml
28
Cargo.toml
@@ -26,22 +26,22 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.5.0"
|
||||
version = "0.5.0-beta2"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", version = "0.5.0" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.5.0" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.0" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.5.0" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.5.0" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.5.0" }
|
||||
server_fn = { path = "./server_fn", version = "0.5.0" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.5.0" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.0" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.5.0" }
|
||||
leptos_router = { path = "./router", version = "0.5.0" }
|
||||
leptos_meta = { path = "./meta", version = "0.5.0" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.0" }
|
||||
leptos = { path = "./leptos", version = "0.5.0-beta2" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.5.0-beta2" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.5.0-beta2" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.5.0-beta2" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.5.0-beta2" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.5.0-beta2" }
|
||||
server_fn = { path = "./server_fn", version = "0.5.0-beta2" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.5.0-beta2" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.5.0-beta2" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.5.0-beta2" }
|
||||
leptos_router = { path = "./router", version = "0.5.0-beta2" }
|
||||
leptos_meta = { path = "./meta", version = "0.5.0-beta2" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.5.0-beta2" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
- [Error Handling](./view/07_errors.md)
|
||||
- [Parent-Child Communication](./view/08_parent_child.md)
|
||||
- [Passing Children to Components](./view/09_component_children.md)
|
||||
- [No Macros: The View Builder Syntax](./view/builder.md)
|
||||
- [Reactivity](./reactivity/README.md)
|
||||
- [Working with Signals](./reactivity/working_with_signals.md)
|
||||
- [Responding to Changes with `create_effect`](./reactivity/14_create_effect.md)
|
||||
|
||||
@@ -70,6 +70,18 @@ There are a few things to note about the way you define a server function, too.
|
||||
- We provide the macro a path. This is a prefix for the path at which we’ll mount a server function handler on our server. (See examples for [Actix](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/main.rs#L44) and [Axum](https://github.com/leptos-rs/leptos/blob/598523cd9d0d775b017cb721e41ebae9349f01e2/examples/todo_app_sqlite_axum/src/main.rs#L51).)
|
||||
- You’ll need to have `serde` as a dependency with the `derive` featured enabled for the macro to work properly. You can easily add it to `Cargo.toml` with `cargo add serde --features=derive`.
|
||||
|
||||
## Server Function URL Prefixes
|
||||
|
||||
You can optionally define a specific URL prefix to be used in the definition of the server function.
|
||||
This is done by providing an optional 2nd argument to the `#[server]` macro.
|
||||
By default the URL prefix will be `/api`, if not specified.
|
||||
Here are some examples:
|
||||
|
||||
```rust
|
||||
#[server(AddTodo)] // will use the default URL prefix of `/api`
|
||||
#[server(AddTodo, "/foo")] // will use the URL prefix of `/foo`
|
||||
```
|
||||
|
||||
## Server Function Encodings
|
||||
|
||||
By default, the server function call is a `POST` request that serializes the arguments as URL-encoded form data in the body of the request. (This means that server functions can be called from HTML forms, which we’ll see in a future chapter.) But there are a few other methods supported. Optionally, we can provide another argument to the `#[server]` macro to specify an alternate encoding:
|
||||
@@ -105,6 +117,21 @@ In other words, you have two choices:
|
||||
>
|
||||
> The CBOR encoding is suported for historical reasons; an earlier version of server functions used a URL encoding that didn’t support nested objects like structs or vectors as server function arguments, which CBOR did. But note that the CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of your app is not available.
|
||||
|
||||
|
||||
## Server Functions Endpoint Paths
|
||||
|
||||
By default, a unique path will be generated. You can optionally define a specific endpoint path to be used in the URL. This is done by providing an optional 4th argument to the `#[server]` macro. Leptos will generate the complete path by concatenating the URL prefix (2nd argument) and the endpoint path (4th argument).
|
||||
For example,
|
||||
|
||||
```rust
|
||||
#[server(MyServerFnType, "/api", "Url", "hello")]
|
||||
```
|
||||
will generate a server function endpoint at `/api/hello` that accepts a POST request.
|
||||
|
||||
> **Can I use the same server function endpoint path with multiple encodings?**
|
||||
>
|
||||
> No. Different server functions must have unique paths. The `#[server]` macro automatically generates unique paths, but you need to be careful if you choose to specify the complete path manually, as the server looks up server functions by their path.
|
||||
|
||||
## An Important Note on Security
|
||||
|
||||
Server functions are a cool technology, but it’s very important to remember. **Server functions are not magic; they’re syntax sugar for defining a public API.** The _body_ of a server function is never made public; it’s just part of your server binary. But the server function is a publicly accessible API endpoint, and it’s return value is just a JSON or similar blob. You should _never_ return something sensitive from a server function.
|
||||
|
||||
98
docs/book/src/view/builder.md
Normal file
98
docs/book/src/view/builder.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# No Macros: The View Builder Syntax
|
||||
|
||||
> If you’re perfectly happy with the `view!` macro syntax described so far, you’re welcome to skip this chapter. The builder syntax described in this section is always available, but never required.
|
||||
|
||||
For one reason or another, many developers would prefer to avoid macros. Perhaps you don’t like the limited `rustfmt` support. (Although, you should check out [`leptosfmt`](https://github.com/bram209/leptosfmt), which is an excellent tool!) Perhaps you worry about the effect of macros on compile time. Perhaps you prefer the aesthetics of pure Rust syntax, or you have trouble context-switching between an HTML-like syntax and your Rust code. Or perhaps you want more flexibility in how you create and manipulate HTML elements than the `view` macro provides.
|
||||
|
||||
If you fall into any of those camps, the builder syntax may be for you.
|
||||
|
||||
The `view` macro expands an HTML-like syntax to a series of Rust functions and method calls. If you’d rather not use the `view` macro, you can simply use that expanded syntax yourself. And it’s actually pretty nice!
|
||||
|
||||
First off, if you want you can even drop the `#[component]` macro: a component is just a setup function that creates your view, so you can define a component as a simple function call:
|
||||
|
||||
```rust
|
||||
pub fn counter(initial_value: i32, step: u32) -> impl IntoView { }
|
||||
```
|
||||
|
||||
Elements are created by calling a function with the same name as the HTML element:
|
||||
|
||||
```rust
|
||||
p()
|
||||
```
|
||||
|
||||
You can add children to the element with [`.child()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.child), which takes a single child or a tuple or array of types that implement [`IntoView`](https://docs.rs/leptos/latest/leptos/trait.IntoView.html).
|
||||
|
||||
```rust
|
||||
p().child((em().child("Big, "), strong().child("bold "), "text"))
|
||||
```
|
||||
|
||||
Attributes are added with [`.attr()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.attr). This can take any of the same types that you could pass as an attribute into the view macro (types that implement [`IntoAttribute`](https://docs.rs/leptos/latest/leptos/trait.IntoAttribute.html)).
|
||||
|
||||
```rust
|
||||
p().attr("id", "foo").attr("data-count", move || count().to_string())
|
||||
```
|
||||
|
||||
Similarly, the `class:`, `prop:`, and `style:` syntaxes map directly onto [`.class()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.class), [`.prop()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.prop), and [`.style()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.style) methods.
|
||||
|
||||
Event listeners can be added with [`.on()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.on). Typed events found in [`leptos::ev`](https://docs.rs/leptos/latest/leptos/ev/index.html) prevent typos in event names and allow for correct type inference in the callback function.
|
||||
|
||||
```rust
|
||||
button()
|
||||
.on(ev::click, move |_| set_count.update(|count| count.clear()))
|
||||
.child("Clear")
|
||||
```
|
||||
|
||||
> Many additional methods can be found in the [`HtmlElement`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.child) docs, including some methods that are not directly available in the `view` macro.
|
||||
|
||||
All of this adds up to a very Rusty syntax to build full-featured views, if you prefer this style.
|
||||
|
||||
```rust
|
||||
/// A simple counter view.
|
||||
// A component is really just a function call: it runs once to create the DOM and reactive system
|
||||
pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(0);
|
||||
|
||||
div()
|
||||
.child((
|
||||
button()
|
||||
// typed events found in leptos::ev
|
||||
// 1) prevent typos in event names
|
||||
// 2) allow for correct type inference in callbacks
|
||||
.on(ev::click, move |_| set_count.update(|count| count.clear()))
|
||||
.child("Clear"),
|
||||
button()
|
||||
.on(ev::click, move |_| {
|
||||
set_count.update(|count| count.decrease())
|
||||
})
|
||||
.child("-1"),
|
||||
span().child(("Value: ", move || count.get().value(), "!")),
|
||||
button()
|
||||
.on(ev::click, move |_| {
|
||||
set_count.update(|count| count.increase())
|
||||
})
|
||||
.child("+1"),
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
This also has the benefit of being more flexible: because these are all plain Rust functions and methods, it’s easier to use them in things like iterator adapters without any additional “magic”:
|
||||
|
||||
```rust
|
||||
// take some set of attribute names and values
|
||||
let attrs: Vec<(&str, AttributeValue)> = todo!();
|
||||
// you can use the builder syntax to “spread” these onto the
|
||||
// element in a way that’s not possible with the view macro
|
||||
let p = attrs
|
||||
.into_iter()
|
||||
.fold(p(), |el, (name, value)| el.attr(name, value));
|
||||
|
||||
```
|
||||
|
||||
> ## Performance Note
|
||||
>
|
||||
> One caveat: the `view` macro applies significant optimizations in server-side-rendering (SSR) mode to improve HTML rendering performance significantly (think 2-4x faster, depending on the characteristics of any given app). It does this by analyzing your `view` at compile time and converting the static parts into simple HTML strings, rather than expanding them into the builder syntax.
|
||||
>
|
||||
> This means two things:
|
||||
>
|
||||
> 1. The builder syntax and `view` macro should not be mixed, or should only be mixed very carefully: at least in SSR mode, the output of the `view` should be treated as a “black box” that can’t have additional builder methods applied to it without causing inconsistencies.
|
||||
> 2. Using the builder syntax will result in less-than-optimal SSR performance. It won’t be slow, by any means (and it’s worth running your own benchmarks in any case), just slower than the `view`-optimized version.
|
||||
@@ -53,6 +53,8 @@ echo "CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = $examples"
|
||||
workspace = false
|
||||
description = "report ci test runners for each example - OPTION: [all]"
|
||||
script = '''
|
||||
set -emu
|
||||
|
||||
BOLD="\e[1m"
|
||||
GREEN="\e[0;32m"
|
||||
ITALIC="\e[3m"
|
||||
@@ -77,36 +79,54 @@ for path in $makefile_paths; do
|
||||
|
||||
test_runner=
|
||||
|
||||
test_count=$(grep -rl -E "#\[(test|rstest)\]" | wc -l)
|
||||
test_count=$(grep -rl -E "#\[test\]" | wc -l)
|
||||
if [ $test_count -gt 0 ]; then
|
||||
test_runner="-C"
|
||||
test_runner="T"
|
||||
fi
|
||||
|
||||
while read -r line; do
|
||||
case $line in
|
||||
*"cucumber"*)
|
||||
test_runner=$test_runner"C"
|
||||
;;
|
||||
*"rstest"*)
|
||||
test_runner=$test_runner"R"
|
||||
;;
|
||||
esac
|
||||
done <"./Cargo.toml"
|
||||
|
||||
while read -r line; do
|
||||
case $line in
|
||||
*"wasm-test.toml"*)
|
||||
test_runner=$test_runner"-W"
|
||||
test_runner=$test_runner"W"
|
||||
;;
|
||||
*"playwright-test.toml"*)
|
||||
test_runner=$test_runner"-P"
|
||||
test_runner=$test_runner"P"
|
||||
;;
|
||||
*"cargo-leptos-test.toml"*)
|
||||
test_runner=$test_runner"-L"
|
||||
test_runner=$test_runner"L"
|
||||
;;
|
||||
esac
|
||||
done <"./Makefile.toml"
|
||||
|
||||
if [ ! -z "$1" ]; then
|
||||
runners=$(echo ${test_runner} | grep -o . | sort | tr -d "\n")
|
||||
|
||||
if [ ! -z ${1+x} ]; then
|
||||
# Show all examples
|
||||
echo "$path ${BOLD}${test_runner}${RESET}"
|
||||
elif [ ! -z $test_runner ]; then
|
||||
echo "$path ➤ ${BOLD}${runners}${RESET}"
|
||||
elif [ ! -z $runners ]; then
|
||||
# Filter out examples that do not run tests in `ci`
|
||||
echo "$path ${BOLD}${test_runner}${RESET}"
|
||||
echo "$path ➤ ${BOLD}${runners}${RESET}"
|
||||
fi
|
||||
|
||||
cd ${start_path}
|
||||
done
|
||||
echo
|
||||
echo "${ITALIC}Runners: C = Cargo Test, L = Cargo Leptos Test, P = Playwright Test, W = WASM Test${RESET}"
|
||||
echo "${ITALIC}Runners: C = Cucumber, L = Cargo Leptos, P = Playwright, R = RS Test, T = Cargo, W = WASM${RESET}"
|
||||
echo
|
||||
'''
|
||||
|
||||
# ALIASES
|
||||
|
||||
[tasks.tr]
|
||||
alias = "test-runner-report"
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
[tasks.clean]
|
||||
dependencies = [
|
||||
"clean-cargo",
|
||||
"clean-trunk",
|
||||
"clean-node_modules",
|
||||
"clean-playwright",
|
||||
"clean-cargo",
|
||||
"clean-trunk",
|
||||
"clean-node_modules",
|
||||
"clean-playwright",
|
||||
]
|
||||
|
||||
[tasks.clean-cargo]
|
||||
command = "cargo"
|
||||
args = ["clean"]
|
||||
command = "rm"
|
||||
args = ["-rf", "target"]
|
||||
|
||||
[tasks.clean-trunk]
|
||||
command = "trunk"
|
||||
args = ["clean"]
|
||||
script = '''
|
||||
find . -type d -name target | xargs rm -rf
|
||||
'''
|
||||
|
||||
[tasks.clean-node_modules]
|
||||
script = '''
|
||||
|
||||
@@ -5,7 +5,6 @@ use leptos::{ev, html::*, *};
|
||||
pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(Count::new(initial_value, step));
|
||||
|
||||
// elements are created by calling a function with a Scope argument
|
||||
// the function name is the same as the HTML tag name
|
||||
div()
|
||||
// children can be added with .child()
|
||||
@@ -25,20 +24,13 @@ pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
|
||||
set_count.update(|count| count.decrease())
|
||||
})
|
||||
.child("-1"),
|
||||
span()
|
||||
.child("Value: ")
|
||||
// reactive values are passed to .child() as a tuple
|
||||
// (Scope, [child function]) so an effect can be created
|
||||
.child(move || count.get().value())
|
||||
.child("!"),
|
||||
))
|
||||
.child(
|
||||
span().child(("Value: ", move || count.get().value(), "!")),
|
||||
button()
|
||||
.on(ev::click, move |_| {
|
||||
set_count.update(|count| count.increase())
|
||||
})
|
||||
.child("+1"),
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -5,12 +5,15 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
leptos_meta = { path = "../../meta", features = ["csr"] }
|
||||
log = "0.4"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
wasm-bindgen = "0.2.87"
|
||||
wasm-bindgen-test = "0.3.37"
|
||||
pretty_assertions = "1.4.0"
|
||||
|
||||
[dev-dependencies.web-sys]
|
||||
features = [
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
{ path = "../cargo-make/playwright-test.toml" },
|
||||
]
|
||||
|
||||
@@ -1,8 +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>
|
||||
</html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs />
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
|
||||
106
examples/counters_stable/src/lib.rs
Normal file
106
examples/counters_stable/src/lib.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
|
||||
const MANY_COUNTERS: usize = 1000;
|
||||
|
||||
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct CounterUpdater {
|
||||
set_counters: WriteSignal<CounterHolder>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Counters() -> impl IntoView {
|
||||
let (next_counter_id, set_next_counter_id) = create_signal(0);
|
||||
let (counters, set_counters) = create_signal::<CounterHolder>(vec![]);
|
||||
provide_context(CounterUpdater { set_counters });
|
||||
|
||||
let add_counter = move |_| {
|
||||
let id = next_counter_id.get();
|
||||
let sig = create_signal(0);
|
||||
set_counters.update(move |counters| counters.push((id, sig)));
|
||||
set_next_counter_id.update(|id| *id += 1);
|
||||
};
|
||||
|
||||
let add_many_counters = move |_| {
|
||||
let next_id = next_counter_id.get();
|
||||
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
|
||||
let signal = create_signal(0);
|
||||
(id, signal)
|
||||
});
|
||||
|
||||
set_counters.update(move |counters| counters.extend(new_counters));
|
||||
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
|
||||
};
|
||||
|
||||
let clear_counters = move |_| {
|
||||
set_counters.update(|counters| counters.clear());
|
||||
};
|
||||
|
||||
view! {
|
||||
<Title text="Counters (Stable)" />
|
||||
<div>
|
||||
<button on:click=add_counter>
|
||||
"Add Counter"
|
||||
</button>
|
||||
<button on:click=add_many_counters>
|
||||
{format!("Add {MANY_COUNTERS} Counters")}
|
||||
</button>
|
||||
<button on:click=clear_counters>
|
||||
"Clear Counters"
|
||||
</button>
|
||||
<p>
|
||||
"Total: "
|
||||
<span data-testid="total">{move ||
|
||||
counters.get()
|
||||
.iter()
|
||||
.map(|(_, (count, _))| count.get())
|
||||
.sum::<i32>()
|
||||
.to_string()
|
||||
}</span>
|
||||
" from "
|
||||
<span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
|
||||
" counters."
|
||||
</p>
|
||||
<ul>
|
||||
<For
|
||||
each={move || counters.get()}
|
||||
key={|counter| counter.0}
|
||||
view=move |(id, (value, set_value))| {
|
||||
view! {
|
||||
<Counter id value set_value/>
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Counter(
|
||||
id: usize,
|
||||
value: ReadSignal<i32>,
|
||||
set_value: WriteSignal<i32>,
|
||||
) -> impl IntoView {
|
||||
let CounterUpdater { set_counters } = use_context().unwrap();
|
||||
|
||||
let input = move |ev| {
|
||||
set_value
|
||||
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
|
||||
};
|
||||
|
||||
view! {
|
||||
<li>
|
||||
<button data-testid="decrement_count" on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
|
||||
<input data-testid="counter_input" type="text"
|
||||
prop:value={move || value.get().to_string()}
|
||||
on:input=input
|
||||
/>
|
||||
<span>{value}</span>
|
||||
<button data-testid="increment_count" on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
|
||||
<button data-testid="remove_counter" on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use counters_stable::Counters;
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
@@ -5,106 +6,3 @@ fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|| view! { <Counters/> })
|
||||
}
|
||||
|
||||
const MANY_COUNTERS: usize = 1000;
|
||||
|
||||
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct CounterUpdater {
|
||||
set_counters: WriteSignal<CounterHolder>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Counters() -> impl IntoView {
|
||||
let (next_counter_id, set_next_counter_id) = create_signal(0);
|
||||
let (counters, set_counters) = create_signal::<CounterHolder>(vec![]);
|
||||
provide_context(CounterUpdater { set_counters });
|
||||
|
||||
let add_counter = move |_| {
|
||||
let id = next_counter_id.get();
|
||||
let sig = create_signal(0);
|
||||
set_counters.update(move |counters| counters.push((id, sig)));
|
||||
set_next_counter_id.update(|id| *id += 1);
|
||||
};
|
||||
|
||||
let add_many_counters = move |_| {
|
||||
let next_id = next_counter_id.get();
|
||||
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
|
||||
let signal = create_signal(0);
|
||||
(id, signal)
|
||||
});
|
||||
|
||||
set_counters.update(move |counters| counters.extend(new_counters));
|
||||
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
|
||||
};
|
||||
|
||||
let clear_counters = move |_| {
|
||||
set_counters.update(|counters| counters.clear());
|
||||
};
|
||||
|
||||
view! {
|
||||
<div>
|
||||
<button on:click=add_counter>
|
||||
"Add Counter"
|
||||
</button>
|
||||
<button on:click=add_many_counters>
|
||||
{format!("Add {MANY_COUNTERS} Counters")}
|
||||
</button>
|
||||
<button on:click=clear_counters>
|
||||
"Clear Counters"
|
||||
</button>
|
||||
<p>
|
||||
"Total: "
|
||||
<span data-testid="total">{move ||
|
||||
counters.get()
|
||||
.iter()
|
||||
.map(|(_, (count, _))| count.get())
|
||||
.sum::<i32>()
|
||||
.to_string()
|
||||
}</span>
|
||||
" from "
|
||||
<span data-testid="counters">{move || counters.with(|counters| counters.len()).to_string()}</span>
|
||||
" counters."
|
||||
</p>
|
||||
<ul>
|
||||
<For
|
||||
each={move || counters.get()}
|
||||
key={|counter| counter.0}
|
||||
view=move |(id, (value, set_value))| {
|
||||
view! {
|
||||
<Counter id value set_value/>
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Counter(
|
||||
id: usize,
|
||||
value: ReadSignal<i32>,
|
||||
set_value: WriteSignal<i32>,
|
||||
) -> impl IntoView {
|
||||
let CounterUpdater { set_counters } = use_context().unwrap();
|
||||
|
||||
let input = move |ev| {
|
||||
set_value
|
||||
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
|
||||
};
|
||||
|
||||
view! {
|
||||
<li>
|
||||
<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>{value}</span>
|
||||
<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>
|
||||
}
|
||||
}
|
||||
|
||||
17
examples/counters_stable/tests/web/add_1k_counters.rs
Normal file
17
examples/counters_stable/tests/web/add_1k_counters.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_increase_the_number_of_counters() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
|
||||
// When
|
||||
ui::add_1k_counters();
|
||||
ui::add_1k_counters();
|
||||
ui::add_1k_counters();
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::counters(), 3000);
|
||||
}
|
||||
17
examples/counters_stable/tests/web/add_counter.rs
Normal file
17
examples/counters_stable/tests/web/add_counter.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_increase_the_number_of_counters() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
|
||||
// When
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::counters(), 3);
|
||||
}
|
||||
19
examples/counters_stable/tests/web/clear_counters.rs
Normal file
19
examples/counters_stable/tests/web/clear_counters.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_reset_the_counts() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
|
||||
// When
|
||||
ui::clear_counters();
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::total(), 0);
|
||||
assert_eq!(ui::counters(), 0);
|
||||
}
|
||||
18
examples/counters_stable/tests/web/decrement_counter.rs
Normal file
18
examples/counters_stable/tests/web/decrement_counter.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_decrease_the_total_count() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
ui::add_counter();
|
||||
|
||||
// When
|
||||
ui::decrement_counter(1);
|
||||
ui::decrement_counter(1);
|
||||
ui::decrement_counter(1);
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::total(), -3);
|
||||
}
|
||||
112
examples/counters_stable/tests/web/fixtures/counters_page.rs
Normal file
112
examples/counters_stable/tests/web/fixtures/counters_page.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use counters_stable::Counters;
|
||||
use leptos::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{Element, Event, EventInit, HtmlElement, HtmlInputElement};
|
||||
|
||||
// Actions
|
||||
|
||||
pub fn add_1k_counters() {
|
||||
find_by_text("Add 1000 Counters").click();
|
||||
}
|
||||
|
||||
pub fn add_counter() {
|
||||
find_by_text("Add Counter").click();
|
||||
}
|
||||
|
||||
pub fn clear_counters() {
|
||||
find_by_text("Clear Counters").click();
|
||||
}
|
||||
|
||||
pub fn decrement_counter(index: u32) {
|
||||
counter_html_element(index, "decrement_count").click();
|
||||
}
|
||||
|
||||
pub fn enter_count(index: u32, count: i32) {
|
||||
let input = counter_input_element(index, "counter_input");
|
||||
input.set_value(count.to_string().as_str());
|
||||
let mut event_init = EventInit::new();
|
||||
event_init.bubbles(true);
|
||||
let event = Event::new_with_event_init_dict("input", &event_init).unwrap();
|
||||
input.dispatch_event(&event).unwrap();
|
||||
}
|
||||
|
||||
pub fn increment_counter(index: u32) {
|
||||
counter_html_element(index, "increment_count").click();
|
||||
}
|
||||
|
||||
pub fn remove_counter(index: u32) {
|
||||
counter_html_element(index, "remove_counter").click();
|
||||
}
|
||||
|
||||
pub fn view_counters() {
|
||||
remove_existing_counters();
|
||||
mount_to_body(|| view! { <Counters/> });
|
||||
}
|
||||
|
||||
// Results
|
||||
|
||||
pub fn counters() -> i32 {
|
||||
data_test_id("counters").parse::<i32>().unwrap()
|
||||
}
|
||||
|
||||
pub fn title() -> String {
|
||||
leptos::document().title()
|
||||
}
|
||||
|
||||
pub fn total() -> i32 {
|
||||
data_test_id("total").parse::<i32>().unwrap()
|
||||
}
|
||||
|
||||
// Internal
|
||||
|
||||
fn counter_element(index: u32, text: &str) -> Element {
|
||||
let selector =
|
||||
format!("li:nth-child({}) [data-testid=\"{}\"]", index, text);
|
||||
leptos::document()
|
||||
.query_selector(&selector)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn counter_html_element(index: u32, text: &str) -> HtmlElement {
|
||||
counter_element(index, text)
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn counter_input_element(index: u32, text: &str) -> HtmlInputElement {
|
||||
counter_element(index, text)
|
||||
.dyn_into::<HtmlInputElement>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn data_test_id(id: &str) -> String {
|
||||
let selector = format!("[data-testid=\"{}\"]", id);
|
||||
leptos::document()
|
||||
.query_selector(&selector)
|
||||
.unwrap()
|
||||
.expect("counters not found")
|
||||
.text_content()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn find_by_text(text: &str) -> HtmlElement {
|
||||
let xpath = format!("//*[text()='{}']", text);
|
||||
let document = leptos::document();
|
||||
document
|
||||
.evaluate(&xpath, &document)
|
||||
.unwrap()
|
||||
.iterate_next()
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn remove_existing_counters() {
|
||||
if let Some(counter) =
|
||||
leptos::document().query_selector("body div").unwrap()
|
||||
{
|
||||
counter.remove();
|
||||
}
|
||||
}
|
||||
1
examples/counters_stable/tests/web/fixtures/mod.rs
Normal file
1
examples/counters_stable/tests/web/fixtures/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod counters_page;
|
||||
18
examples/counters_stable/tests/web/increment_counter.rs
Normal file
18
examples/counters_stable/tests/web/increment_counter.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_increase_the_total_count() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
ui::add_counter();
|
||||
|
||||
// When
|
||||
ui::increment_counter(1);
|
||||
ui::increment_counter(1);
|
||||
ui::increment_counter(1);
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::total(), 3);
|
||||
}
|
||||
16
examples/counters_stable/tests/web/main.rs
Normal file
16
examples/counters_stable/tests/web/main.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
// Test Suites
|
||||
pub mod add_1k_counters;
|
||||
pub mod add_counter;
|
||||
pub mod clear_counters;
|
||||
pub mod decrement_counter;
|
||||
pub mod enter_count;
|
||||
pub mod increment_counter;
|
||||
pub mod remove_counter;
|
||||
pub mod view_counters;
|
||||
|
||||
pub mod fixtures;
|
||||
pub use fixtures::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
18
examples/counters_stable/tests/web/remove_counter.rs
Normal file
18
examples/counters_stable/tests/web/remove_counter.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_decrement_the_number_of_counters() {
|
||||
// Given
|
||||
ui::view_counters();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
ui::add_counter();
|
||||
|
||||
// When
|
||||
ui::remove_counter(2);
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::counters(), 2);
|
||||
}
|
||||
22
examples/counters_stable/tests/web/view_counters.rs
Normal file
22
examples/counters_stable/tests/web/view_counters.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use super::*;
|
||||
use crate::counters_page as ui;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_see_the_initial_counts() {
|
||||
// When
|
||||
ui::view_counters();
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::total(), 0);
|
||||
assert_eq!(ui::counters(), 0);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_see_the_title() {
|
||||
// When
|
||||
ui::view_counters();
|
||||
|
||||
// Then
|
||||
assert_eq!(ui::title(), "Counters (Stable)");
|
||||
}
|
||||
@@ -36,8 +36,8 @@ pub fn Stories() -> impl IntoView {
|
||||
let (pending, set_pending) = create_signal(false);
|
||||
|
||||
let hide_more_link = move || {
|
||||
pending()
|
||||
|| stories.get().unwrap_or(None).unwrap_or_default().len() < 28
|
||||
stories.get().unwrap_or(None).unwrap_or_default().len() < 28
|
||||
|| pending()
|
||||
};
|
||||
|
||||
view! {
|
||||
@@ -65,20 +65,16 @@ pub fn Stories() -> impl IntoView {
|
||||
}}
|
||||
</span>
|
||||
<span>"page " {page}</span>
|
||||
<Transition
|
||||
fallback=move || view! { <p>"Loading..."</p> }
|
||||
<span class="page-link"
|
||||
class:disabled=hide_more_link
|
||||
aria-hidden=hide_more_link
|
||||
>
|
||||
<span class="page-link"
|
||||
class:disabled=hide_more_link
|
||||
aria-hidden=hide_more_link
|
||||
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
aria-label="Next Page"
|
||||
>
|
||||
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
aria-label="Next Page"
|
||||
>
|
||||
"more >"
|
||||
</a>
|
||||
</span>
|
||||
</Transition>
|
||||
"more >"
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<main class="news-list">
|
||||
<div>
|
||||
|
||||
@@ -25,48 +25,45 @@ pub fn Story() -> impl IntoView {
|
||||
};
|
||||
|
||||
view! {
|
||||
<>
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
<Meta name="description" content=meta_description/>
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
{move || story.get().map(|story| match story {
|
||||
None => view! { <div class="item-view">"Error loading this story."</div> },
|
||||
Some(story) => view! {
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { <p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"No comments yet.".into()
|
||||
}}
|
||||
</p>
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
each=move || story.comments.clone().unwrap_or_default()
|
||||
key=|comment| comment.id
|
||||
view=move |comment| view! { <Comment comment /> }
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
{move || story.get().map(|story| match story {
|
||||
None => view! { <div class="item-view">"Error loading this story."</div> },
|
||||
Some(story) => view! {
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { <p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
</div>
|
||||
}})
|
||||
}
|
||||
</Suspense>
|
||||
</>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"No comments yet.".into()
|
||||
}}
|
||||
</p>
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
each=move || story.comments.clone().unwrap_or_default()
|
||||
key=|comment| comment.id
|
||||
view=move |comment| view! { <Comment comment /> }
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}})}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,12 +36,11 @@ pub fn Stories() -> impl IntoView {
|
||||
let (pending, set_pending) = create_signal(false);
|
||||
|
||||
let hide_more_link = move || {
|
||||
pending()
|
||||
|| stories.get().unwrap_or(None).unwrap_or_default().len() < 28
|
||||
stories.get().unwrap_or(None).unwrap_or_default().len() < 28
|
||||
|| pending()
|
||||
};
|
||||
|
||||
view! {
|
||||
|
||||
<div class="news-view">
|
||||
<div class="news-list-nav">
|
||||
<span>
|
||||
@@ -65,20 +64,16 @@ pub fn Stories() -> impl IntoView {
|
||||
}}
|
||||
</span>
|
||||
<span>"page " {page}</span>
|
||||
<Transition
|
||||
fallback=move || view! { <p>"Loading..."</p> }
|
||||
<span class="page-link"
|
||||
class:disabled=hide_more_link
|
||||
aria-hidden=hide_more_link
|
||||
>
|
||||
<span class="page-link"
|
||||
class:disabled=hide_more_link
|
||||
aria-hidden=hide_more_link
|
||||
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
aria-label="Next Page"
|
||||
>
|
||||
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
aria-label="Next Page"
|
||||
>
|
||||
"more >"
|
||||
</a>
|
||||
</span>
|
||||
</Transition>
|
||||
"more >"
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<main class="news-list">
|
||||
<div>
|
||||
|
||||
@@ -25,48 +25,45 @@ pub fn Story() -> impl IntoView {
|
||||
};
|
||||
|
||||
view! {
|
||||
<>
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
<Meta name="description" content=meta_description/>
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
{move || story.get().map(|story| match story {
|
||||
None => view! { <div class="item-view">"Error loading this story."</div> },
|
||||
Some(story) => view! {
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { <p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"No comments yet.".into()
|
||||
}}
|
||||
</p>
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
each=move || story.comments.clone().unwrap_or_default()
|
||||
key=|comment| comment.id
|
||||
view=move | comment| view! { <Comment comment /> }
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
{move || story.get().map(|story| match story {
|
||||
None => view! { <div class="item-view">"Error loading this story."</div> },
|
||||
Some(story) => view! {
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { <p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
</div>
|
||||
}})
|
||||
}
|
||||
</Suspense>
|
||||
</>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"No comments yet.".into()
|
||||
}}
|
||||
</p>
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
each=move || story.comments.clone().unwrap_or_default()
|
||||
key=|comment| comment.id
|
||||
view=move | comment| view! { <Comment comment /> }
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}})}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ use leptos_router::*;
|
||||
use parking_lot::RwLock;
|
||||
use regex::Regex;
|
||||
use std::{fmt::Display, future::Future, sync::Arc};
|
||||
#[cfg(debug_assertions)]
|
||||
use tracing::instrument;
|
||||
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
|
||||
/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.
|
||||
|
||||
@@ -7,14 +7,18 @@ extern crate tracing;
|
||||
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
fn autoreload(nonce_str: &str, options: &LeptosOptions) -> String {
|
||||
let site_ip = &options.site_addr.ip().to_string();
|
||||
let reload_port = options.reload_port;
|
||||
let reload_port = match options.reload_external_port {
|
||||
Some(val) => val,
|
||||
None => options.reload_port,
|
||||
};
|
||||
match std::env::var("LEPTOS_WATCH").is_ok() {
|
||||
true => format!(
|
||||
r#"
|
||||
<script crossorigin=""{nonce_str}>(function () {{
|
||||
{}
|
||||
let ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
|
||||
let protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
let host = window.location.hostname;
|
||||
let ws = new WebSocket(protocol + host + ':{reload_port}/live_reload');
|
||||
ws.onmessage = (ev) => {{
|
||||
let msg = JSON.parse(ev.data);
|
||||
if (msg.all) window.location.reload();
|
||||
|
||||
@@ -16,7 +16,8 @@ leptos_reactive = { workspace = true }
|
||||
leptos_server = { workspace = true }
|
||||
leptos_config = { workspace = true }
|
||||
tracing = "0.1"
|
||||
typed-builder = "0.14"
|
||||
typed-builder = "0.16"
|
||||
typed-builder-macro = "0.16"
|
||||
server_fn = { workspace = true }
|
||||
web-sys = { version = "0.3.63", optional = true }
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::TextProp;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A collection of additional HTML attributes to be applied to an element,
|
||||
/// each of which may or may not be reactive.
|
||||
#[derive(Default, Clone)]
|
||||
#[derive(Clone)]
|
||||
#[repr(transparent)]
|
||||
pub struct AdditionalAttributes(pub(crate) Vec<(String, TextProp)>);
|
||||
pub struct AdditionalAttributes(pub(crate) Rc<[(String, TextProp)]>);
|
||||
|
||||
impl<I, T, U> From<I> for AdditionalAttributes
|
||||
where
|
||||
@@ -22,6 +23,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AdditionalAttributes {
|
||||
fn default() -> Self {
|
||||
Self([].into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over additional HTML attributes.
|
||||
#[repr(transparent)]
|
||||
pub struct AdditionalAttributesIter<'a>(
|
||||
|
||||
@@ -175,7 +175,6 @@ pub use leptos_server::{
|
||||
ServerFnErrorErr,
|
||||
};
|
||||
pub use server_fn::{self, ServerFn as _};
|
||||
pub use typed_builder;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "template_macro"))]
|
||||
pub use {leptos_macro::template, wasm_bindgen, web_sys};
|
||||
mod error_boundary;
|
||||
@@ -195,6 +194,12 @@ pub use text_prop::TextProp;
|
||||
#[doc(hidden)]
|
||||
pub use tracing;
|
||||
pub use transition::*;
|
||||
#[doc(hidden)]
|
||||
pub use typed_builder;
|
||||
#[doc(hidden)]
|
||||
pub use typed_builder::Optional;
|
||||
#[doc(hidden)]
|
||||
pub use typed_builder_macro;
|
||||
extern crate self as leptos;
|
||||
|
||||
/// The most common type for the `children` property on components,
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use leptos_reactive::Oco;
|
||||
use std::{fmt::Debug, rc::Rc};
|
||||
|
||||
/// Describes a value that is either a static or a reactive string, i.e.,
|
||||
/// a [`String`], a [`&str`], or a reactive `Fn() -> String`.
|
||||
#[derive(Clone)]
|
||||
pub struct TextProp(Rc<dyn Fn() -> String>);
|
||||
pub struct TextProp(Rc<dyn Fn() -> Oco<'static, str>>);
|
||||
|
||||
impl TextProp {
|
||||
/// Accesses the current value of the property.
|
||||
#[inline(always)]
|
||||
pub fn get(&self) -> String {
|
||||
pub fn get(&self) -> Oco<'static, str> {
|
||||
(self.0)()
|
||||
}
|
||||
}
|
||||
@@ -21,23 +22,38 @@ impl Debug for TextProp {
|
||||
|
||||
impl From<String> for TextProp {
|
||||
fn from(s: String) -> Self {
|
||||
let s: Oco<'_, str> = Oco::Counted(Rc::from(s));
|
||||
TextProp(Rc::new(move || s.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for TextProp {
|
||||
fn from(s: &str) -> Self {
|
||||
let s = s.to_string();
|
||||
impl From<&'static str> for TextProp {
|
||||
fn from(s: &'static str) -> Self {
|
||||
let s: Oco<'_, str> = s.into();
|
||||
TextProp(Rc::new(move || s.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> From<F> for TextProp
|
||||
impl From<Rc<str>> for TextProp {
|
||||
fn from(s: Rc<str>) -> Self {
|
||||
let s: Oco<'_, str> = s.into();
|
||||
TextProp(Rc::new(move || s.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Oco<'static, str>> for TextProp {
|
||||
fn from(s: Oco<'static, str>) -> Self {
|
||||
TextProp(Rc::new(move || s.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, S> From<F> for TextProp
|
||||
where
|
||||
F: Fn() -> String + 'static,
|
||||
F: Fn() -> S + 'static,
|
||||
S: Into<Oco<'static, str>>,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn from(s: F) -> Self {
|
||||
TextProp(Rc::new(s))
|
||||
TextProp(Rc::new(move || s().into()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use leptos_dom::{Fragment, HydrationCtx, IntoView, View};
|
||||
use leptos_macro::component;
|
||||
use leptos_reactive::{
|
||||
create_isomorphic_effect, use_context, SignalGet, SignalSetter,
|
||||
SuspenseContext,
|
||||
create_isomorphic_effect, create_rw_signal, use_context, RwSignal,
|
||||
SignalGet, SignalSet, SignalSetter, SuspenseContext,
|
||||
};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
@@ -83,7 +83,7 @@ where
|
||||
{
|
||||
let prev_children = Rc::new(RefCell::new(None::<View>));
|
||||
|
||||
let first_run = Rc::new(std::cell::Cell::new(true));
|
||||
let first_run = create_rw_signal(true);
|
||||
let child_runs = Cell::new(0);
|
||||
let held_suspense_context = Rc::new(RefCell::new(None::<SuspenseContext>));
|
||||
|
||||
@@ -91,17 +91,18 @@ where
|
||||
crate::SuspenseProps::builder()
|
||||
.fallback({
|
||||
let prev_child = Rc::clone(&prev_children);
|
||||
let first_run = Rc::clone(&first_run);
|
||||
move || {
|
||||
let suspense_context = use_context::<SuspenseContext>()
|
||||
.expect("there to be a SuspenseContext");
|
||||
|
||||
let was_first_run =
|
||||
cfg!(feature = "csr") && first_run.get();
|
||||
let is_first_run =
|
||||
is_first_run(&first_run, &suspense_context);
|
||||
is_first_run(first_run, &suspense_context);
|
||||
first_run.set(false);
|
||||
|
||||
if let Some(prev_children) = &*prev_child.borrow() {
|
||||
if is_first_run {
|
||||
if is_first_run || was_first_run {
|
||||
fallback().into_view()
|
||||
} else {
|
||||
prev_children.clone()
|
||||
@@ -127,12 +128,11 @@ where
|
||||
{
|
||||
*prev_children.borrow_mut() = Some(frag.clone());
|
||||
}
|
||||
if is_first_run(&first_run, &suspense_context) {
|
||||
if is_first_run(first_run, &suspense_context) {
|
||||
let has_local_only = suspense_context.has_local_only()
|
||||
|| cfg!(feature = "csr");
|
||||
if (!has_local_only || child_runs.get() > 0)
|
||||
&& (cfg!(feature = "csr")
|
||||
|| HydrationCtx::is_hydrating())
|
||||
&& !cfg!(feature = "csr")
|
||||
{
|
||||
first_run.set(false);
|
||||
}
|
||||
@@ -152,7 +152,7 @@ where
|
||||
}
|
||||
|
||||
fn is_first_run(
|
||||
first_run: &Rc<Cell<bool>>,
|
||||
first_run: RwSignal<bool>,
|
||||
suspense_context: &SuspenseContext,
|
||||
) -> bool {
|
||||
if cfg!(feature = "csr") {
|
||||
|
||||
@@ -13,7 +13,7 @@ config = "0.13.3"
|
||||
regex = "1.7.0"
|
||||
serde = { version = "1.0.151", features = ["derive"] }
|
||||
thiserror = "1.0.38"
|
||||
typed-builder = "0.14"
|
||||
typed-builder = "0.16"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["rt", "macros"] }
|
||||
|
||||
@@ -56,6 +56,11 @@ pub struct LeptosOptions {
|
||||
#[builder(default = default_reload_port())]
|
||||
#[serde(default = "default_reload_port")]
|
||||
pub reload_port: u32,
|
||||
/// The port the Websocket watcher listens on when on the client, e.g., when behind a reverse proxy.
|
||||
/// Defaults to match reload_port
|
||||
#[builder(default)]
|
||||
#[serde(default)]
|
||||
pub reload_external_port: Option<u32>,
|
||||
}
|
||||
|
||||
impl LeptosOptions {
|
||||
@@ -84,6 +89,12 @@ impl LeptosOptions {
|
||||
.parse()?,
|
||||
reload_port: env_w_default("LEPTOS_RELOAD_PORT", "3001")?
|
||||
.parse()?,
|
||||
reload_external_port: match env_wo_default(
|
||||
"LEPTOS_RELOAD_EXTERNAL_PORT",
|
||||
)? {
|
||||
Some(val) => Some(val.parse()?),
|
||||
None => None,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -107,7 +118,13 @@ fn default_site_addr() -> SocketAddr {
|
||||
fn default_reload_port() -> u32 {
|
||||
3001
|
||||
}
|
||||
|
||||
fn env_wo_default(key: &str) -> Result<Option<String>, LeptosConfigError> {
|
||||
match std::env::var(key) {
|
||||
Ok(val) => Ok(Some(val)),
|
||||
Err(VarError::NotPresent) => Ok(None),
|
||||
Err(e) => Err(LeptosConfigError::EnvVarError(format!("{key}: {e}"))),
|
||||
}
|
||||
}
|
||||
fn env_w_default(
|
||||
key: &str,
|
||||
default: &str,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{env_w_default, from_str, Env, LeptosOptions};
|
||||
use crate::{env_w_default, env_wo_default, from_str, Env, LeptosOptions};
|
||||
use std::{net::SocketAddr, str::FromStr};
|
||||
|
||||
#[test]
|
||||
@@ -29,6 +29,17 @@ fn env_w_default_test() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_wo_default_test() {
|
||||
std::env::set_var("LEPTOS_CONFIG_ENV_TEST", "custom");
|
||||
assert_eq!(
|
||||
env_wo_default("LEPTOS_CONFIG_ENV_TEST").unwrap(),
|
||||
Some(String::from("custom"))
|
||||
);
|
||||
std::env::remove_var("LEPTOS_CONFIG_ENV_TEST");
|
||||
assert_eq!(env_wo_default("LEPTOS_CONFIG_ENV_TEST").unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_from_env_test() {
|
||||
// Test config values from environment variables
|
||||
@@ -37,6 +48,7 @@ fn try_from_env_test() {
|
||||
std::env::set_var("LEPTOS_SITE_PKG_DIR", "my_pkg");
|
||||
std::env::set_var("LEPTOS_SITE_ADDR", "0.0.0.0:80");
|
||||
std::env::set_var("LEPTOS_RELOAD_PORT", "8080");
|
||||
std::env::set_var("LEPTOS_RELOAD_EXTERNAL_PORT", "8080");
|
||||
|
||||
let config = LeptosOptions::try_from_env().unwrap();
|
||||
assert_eq!(config.output_name, "app_test");
|
||||
@@ -48,4 +60,5 @@ fn try_from_env_test() {
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
);
|
||||
assert_eq!(config.reload_port, 8080);
|
||||
assert_eq!(config.reload_external_port, Some(8080));
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ site-root = "my_target/site"
|
||||
site-pkg-dir = "my_pkg"
|
||||
site-addr = "0.0.0.0:80"
|
||||
reload-port = "8080"
|
||||
reload-external-port = "8080"
|
||||
env = "PROD"
|
||||
"#;
|
||||
|
||||
@@ -27,6 +28,7 @@ _site-root = "my_target/site"
|
||||
_site-pkg-dir = "my_pkg"
|
||||
_site-addr = "0.0.0.0:80"
|
||||
_reload-port = "8080"
|
||||
_reload-external-port = "8080"
|
||||
_env = "PROD"
|
||||
"#;
|
||||
|
||||
@@ -54,6 +56,7 @@ async fn get_configuration_from_file_ok() {
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
);
|
||||
assert_eq!(config.reload_port, 8080);
|
||||
assert_eq!(config.reload_external_port, Some(8080));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -101,6 +104,7 @@ async fn get_config_from_file_ok() {
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
);
|
||||
assert_eq!(config.reload_port, 8080);
|
||||
assert_eq!(config.reload_external_port, Some(8080));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -136,6 +140,7 @@ fn get_config_from_str_content() {
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
);
|
||||
assert_eq!(config.reload_port, 8080);
|
||||
assert_eq!(config.reload_external_port, Some(8080));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -146,6 +151,7 @@ async fn get_config_from_env() {
|
||||
std::env::set_var("LEPTOS_SITE_PKG_DIR", "my_pkg");
|
||||
std::env::set_var("LEPTOS_SITE_ADDR", "0.0.0.0:80");
|
||||
std::env::set_var("LEPTOS_RELOAD_PORT", "8080");
|
||||
std::env::set_var("LEPTOS_RELOAD_EXTERNAL_PORT", "8080");
|
||||
|
||||
let config = get_configuration(None).await.unwrap().leptos_options;
|
||||
assert_eq!(config.output_name, "app-test");
|
||||
@@ -157,12 +163,14 @@ async fn get_config_from_env() {
|
||||
SocketAddr::from_str("0.0.0.0:80").unwrap()
|
||||
);
|
||||
assert_eq!(config.reload_port, 8080);
|
||||
assert_eq!(config.reload_external_port, Some(8080));
|
||||
|
||||
// Test default config values
|
||||
std::env::remove_var("LEPTOS_SITE_ROOT");
|
||||
std::env::remove_var("LEPTOS_SITE_PKG_DIR");
|
||||
std::env::remove_var("LEPTOS_SITE_ADDR");
|
||||
std::env::remove_var("LEPTOS_RELOAD_PORT");
|
||||
std::env::set_var("LEPTOS_RELOAD_EXTERNAL_PORT", "443");
|
||||
|
||||
let config = get_configuration(None).await.unwrap().leptos_options;
|
||||
assert_eq!(config.site_root, "target/site");
|
||||
@@ -172,6 +180,7 @@ async fn get_config_from_env() {
|
||||
SocketAddr::from_str("127.0.0.1:3000").unwrap()
|
||||
);
|
||||
assert_eq!(config.reload_port, 3001);
|
||||
assert_eq!(config.reload_external_port, Some(443));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -186,4 +195,5 @@ fn leptos_options_builder_default() {
|
||||
SocketAddr::from_str("127.0.0.1:3000").unwrap()
|
||||
);
|
||||
assert_eq!(conf.reload_port, 3001);
|
||||
assert_eq!(conf.reload_external_port, None);
|
||||
}
|
||||
|
||||
@@ -14,12 +14,12 @@ pub use dyn_child::*;
|
||||
pub use each::*;
|
||||
pub use errors::*;
|
||||
pub use fragment::*;
|
||||
use leptos_reactive::untrack_with_diagnostics;
|
||||
use leptos_reactive::{untrack_with_diagnostics, Oco};
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use once_cell::unsync::OnceCell;
|
||||
use std::fmt;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use std::rc::Rc;
|
||||
use std::{borrow::Cow, fmt};
|
||||
pub use unit::*;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use wasm_bindgen::JsCast;
|
||||
@@ -55,7 +55,7 @@ pub struct ComponentRepr {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
mounted: Rc<OnceCell<()>>,
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
pub(crate) name: Cow<'static, str>,
|
||||
pub(crate) name: Oco<'static, str>,
|
||||
#[cfg(debug_assertions)]
|
||||
_opening: Comment,
|
||||
/// The children of the component.
|
||||
@@ -163,24 +163,24 @@ impl IntoView for ComponentRepr {
|
||||
impl ComponentRepr {
|
||||
/// Creates a new [`Component`].
|
||||
#[inline(always)]
|
||||
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
|
||||
pub fn new(name: impl Into<Oco<'static, str>>) -> Self {
|
||||
Self::new_with_id_concrete(name.into(), HydrationCtx::id())
|
||||
}
|
||||
|
||||
/// Creates a new [`Component`] with the given hydration ID.
|
||||
#[inline(always)]
|
||||
pub fn new_with_id(
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
name: impl Into<Oco<'static, str>>,
|
||||
id: HydrationKey,
|
||||
) -> Self {
|
||||
Self::new_with_id_concrete(name.into(), id)
|
||||
}
|
||||
|
||||
fn new_with_id_concrete(name: Cow<'static, str>, id: HydrationKey) -> Self {
|
||||
fn new_with_id_concrete(name: Oco<'static, str>, id: HydrationKey) -> Self {
|
||||
let markers = (
|
||||
Comment::new(Cow::Owned(format!("</{name}>")), &id, true),
|
||||
Comment::new(format!("</{name}>"), &id, true),
|
||||
#[cfg(debug_assertions)]
|
||||
Comment::new(Cow::Owned(format!("<{name}>")), &id, false),
|
||||
Comment::new(format!("<{name}>"), &id, false),
|
||||
);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
@@ -236,7 +236,7 @@ where
|
||||
V: IntoView,
|
||||
{
|
||||
id: HydrationKey,
|
||||
name: Cow<'static, str>,
|
||||
name: Oco<'static, str>,
|
||||
children_fn: F,
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ where
|
||||
V: IntoView,
|
||||
{
|
||||
/// Creates a new component.
|
||||
pub fn new(name: impl Into<Cow<'static, str>>, f: F) -> Self {
|
||||
pub fn new(name: impl Into<Oco<'static, str>>, f: F) -> Self {
|
||||
Self {
|
||||
id: HydrationCtx::id(),
|
||||
name: name.into(),
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
Comment, IntoView, View,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use std::{borrow::Cow, cell::RefCell, fmt, ops::Deref, rc::Rc};
|
||||
use std::{cell::RefCell, fmt, ops::Deref, rc::Rc};
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
use crate::{mount_child, prepare_to_move, unmount_child, MountKind, Mountable, Text};
|
||||
@@ -83,9 +83,9 @@ impl Mountable for DynChildRepr {
|
||||
impl DynChildRepr {
|
||||
fn new_with_id(id: HydrationKey) -> Self {
|
||||
let markers = (
|
||||
Comment::new(Cow::Borrowed("</DynChild>"), &id, true),
|
||||
Comment::new("</DynChild>", &id, true),
|
||||
#[cfg(debug_assertions)]
|
||||
Comment::new(Cow::Borrowed("<DynChild>"), &id, false),
|
||||
Comment::new("<DynChild>", &id, false),
|
||||
);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
use crate::hydration::HydrationKey;
|
||||
use crate::{hydration::HydrationCtx, Comment, CoreComponent, IntoView, View};
|
||||
use leptos_reactive::{as_child_of_current_owner, Disposer};
|
||||
use std::{borrow::Cow, cell::RefCell, fmt, hash::Hash, ops::Deref, rc::Rc};
|
||||
use std::{cell::RefCell, fmt, hash::Hash, ops::Deref, rc::Rc};
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use web::*;
|
||||
|
||||
@@ -79,9 +79,9 @@ impl Default for EachRepr {
|
||||
let id = HydrationCtx::id();
|
||||
|
||||
let markers = (
|
||||
Comment::new(Cow::Borrowed("</Each>"), &id, true),
|
||||
Comment::new("</Each>", &id, true),
|
||||
#[cfg(debug_assertions)]
|
||||
Comment::new(Cow::Borrowed("<Each>"), &id, false),
|
||||
Comment::new("<Each>", &id, false),
|
||||
);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
@@ -224,13 +224,13 @@ impl EachItem {
|
||||
|
||||
let markers = (
|
||||
if needs_closing {
|
||||
Some(Comment::new(Cow::Borrowed("</EachItem>"), &id, true))
|
||||
Some(Comment::new("</EachItem>", &id, true))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
#[cfg(debug_assertions)]
|
||||
if needs_closing {
|
||||
Some(Comment::new(Cow::Borrowed("<EachItem>"), &id, false))
|
||||
Some(Comment::new("<EachItem>", &id, false))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod typed;
|
||||
|
||||
use std::{borrow::Cow, cell::RefCell, collections::HashSet};
|
||||
use leptos_reactive::Oco;
|
||||
use std::{cell::RefCell, collections::HashSet};
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use wasm_bindgen::{
|
||||
convert::FromWasmAbi, intern, prelude::Closure, JsCast, JsValue,
|
||||
@@ -8,7 +9,7 @@ use wasm_bindgen::{
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
pub(crate) static GLOBAL_EVENTS: RefCell<HashSet<Cow<'static, str>>> = RefCell::new(HashSet::new());
|
||||
pub(crate) static GLOBAL_EVENTS: RefCell<HashSet<Oco<'static, str>>> = RefCell::new(HashSet::new());
|
||||
}
|
||||
|
||||
// Used in template macro
|
||||
@@ -47,8 +48,8 @@ pub fn add_event_helper<E: crate::ev::EventDescriptor + 'static>(
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub fn add_event_listener<E>(
|
||||
target: &web_sys::Element,
|
||||
key: Cow<'static, str>,
|
||||
event_name: Cow<'static, str>,
|
||||
key: Oco<'static, str>,
|
||||
event_name: Oco<'static, str>,
|
||||
#[cfg(debug_assertions)] mut cb: Box<dyn FnMut(E)>,
|
||||
#[cfg(not(debug_assertions))] cb: Box<dyn FnMut(E)>,
|
||||
options: &Option<web_sys::AddEventListenerOptions>,
|
||||
@@ -115,7 +116,7 @@ pub(crate) fn add_event_listener_undelegated<E>(
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn add_delegated_event_listener(
|
||||
key: &str,
|
||||
event_name: Cow<'static, str>,
|
||||
event_name: Oco<'static, str>,
|
||||
options: &Option<web_sys::AddEventListenerOptions>,
|
||||
) {
|
||||
GLOBAL_EVENTS.with(|global_events| {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Types for all DOM events.
|
||||
|
||||
use std::{borrow::Cow, marker::PhantomData};
|
||||
use leptos_reactive::Oco;
|
||||
use std::marker::PhantomData;
|
||||
use wasm_bindgen::convert::FromWasmAbi;
|
||||
|
||||
/// A trait for converting types into [web_sys events](web_sys).
|
||||
@@ -16,10 +17,10 @@ pub trait EventDescriptor: Clone {
|
||||
const BUBBLES: bool;
|
||||
|
||||
/// The name of the event, such as `click` or `mouseover`.
|
||||
fn name(&self) -> Cow<'static, str>;
|
||||
fn name(&self) -> Oco<'static, str>;
|
||||
|
||||
/// The key used for event delegation.
|
||||
fn event_delegation_key(&self) -> Cow<'static, str>;
|
||||
fn event_delegation_key(&self) -> Oco<'static, str>;
|
||||
|
||||
/// Return the options for this type. This is only used when you create a [`Custom`] event
|
||||
/// handler.
|
||||
@@ -31,7 +32,7 @@ pub trait EventDescriptor: Clone {
|
||||
|
||||
/// Overrides the [`EventDescriptor::BUBBLES`] value to always return
|
||||
/// `false`, which forces the event to not be globally delegated.
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub struct undelegated<Ev: EventDescriptor>(pub Ev);
|
||||
|
||||
@@ -39,12 +40,12 @@ impl<Ev: EventDescriptor> EventDescriptor for undelegated<Ev> {
|
||||
type EventType = Ev::EventType;
|
||||
|
||||
#[inline(always)]
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
fn name(&self) -> Oco<'static, str> {
|
||||
self.0.name()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn event_delegation_key(&self) -> Cow<'static, str> {
|
||||
fn event_delegation_key(&self) -> Oco<'static, str> {
|
||||
self.0.event_delegation_key()
|
||||
}
|
||||
|
||||
@@ -52,8 +53,9 @@ impl<Ev: EventDescriptor> EventDescriptor for undelegated<Ev> {
|
||||
}
|
||||
|
||||
/// A custom event.
|
||||
#[derive(Debug)]
|
||||
pub struct Custom<E: FromWasmAbi = web_sys::Event> {
|
||||
name: Cow<'static, str>,
|
||||
name: Oco<'static, str>,
|
||||
options: Option<web_sys::AddEventListenerOptions>,
|
||||
_event_type: PhantomData<E>,
|
||||
}
|
||||
@@ -71,11 +73,11 @@ impl<E: FromWasmAbi> Clone for Custom<E> {
|
||||
impl<E: FromWasmAbi> EventDescriptor for Custom<E> {
|
||||
type EventType = E;
|
||||
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
fn name(&self) -> Oco<'static, str> {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn event_delegation_key(&self) -> Cow<'static, str> {
|
||||
fn event_delegation_key(&self) -> Oco<'static, str> {
|
||||
format!("$$${}", self.name).into()
|
||||
}
|
||||
|
||||
@@ -91,7 +93,7 @@ impl<E: FromWasmAbi> Custom<E> {
|
||||
/// Creates a custom event type that can be used within
|
||||
/// [`HtmlElement::on`](crate::HtmlElement::on), for events
|
||||
/// which are not covered in the [`ev`](crate::ev) module.
|
||||
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
|
||||
pub fn new(name: impl Into<Oco<'static, str>>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
options: None,
|
||||
@@ -125,34 +127,255 @@ impl<E: FromWasmAbi> Custom<E> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Type that can respond to DOM events
|
||||
pub trait DOMEventResponder: Sized {
|
||||
/// Adds handler to specified event
|
||||
fn add<E: EventDescriptor + 'static>(
|
||||
self,
|
||||
event: E,
|
||||
handler: impl FnMut(E::EventType) + 'static,
|
||||
) -> Self;
|
||||
/// Same as [add](DOMEventResponder::add), but with [`EventHandler`]
|
||||
#[inline]
|
||||
fn add_handler(self, handler: impl EventHandler) -> Self {
|
||||
handler.attach(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DOMEventResponder for crate::HtmlElement<T>
|
||||
where
|
||||
T: crate::html::ElementDescriptor + 'static,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn add<E: EventDescriptor + 'static>(
|
||||
self,
|
||||
event: E,
|
||||
handler: impl FnMut(E::EventType) + 'static,
|
||||
) -> Self {
|
||||
self.on(event, handler)
|
||||
}
|
||||
}
|
||||
|
||||
impl DOMEventResponder for crate::View {
|
||||
#[inline(always)]
|
||||
fn add<E: EventDescriptor + 'static>(
|
||||
self,
|
||||
event: E,
|
||||
handler: impl FnMut(E::EventType) + 'static,
|
||||
) -> Self {
|
||||
self.on(event, handler)
|
||||
}
|
||||
}
|
||||
|
||||
/// Type that can be used to handle DOM events
|
||||
pub trait EventHandler {
|
||||
/// Attaches event listener to any target that can respond to DOM events
|
||||
fn attach<T: DOMEventResponder>(self, target: T) -> T;
|
||||
}
|
||||
|
||||
impl<T, const N: usize> EventHandler for [T; N]
|
||||
where
|
||||
T: EventHandler,
|
||||
{
|
||||
#[inline]
|
||||
fn attach<R: DOMEventResponder>(self, target: R) -> R {
|
||||
let mut target = target;
|
||||
for item in self {
|
||||
target = item.attach(target);
|
||||
}
|
||||
target
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> EventHandler for Option<T>
|
||||
where
|
||||
T: EventHandler,
|
||||
{
|
||||
#[inline]
|
||||
fn attach<R: DOMEventResponder>(self, target: R) -> R {
|
||||
match self {
|
||||
Some(event_handler) => event_handler.attach(target),
|
||||
None => target,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! tc {
|
||||
($($ty:ident),*) => {
|
||||
impl<$($ty),*> EventHandler for ($($ty,)*)
|
||||
where
|
||||
$($ty: EventHandler),*
|
||||
{
|
||||
#[inline]
|
||||
fn attach<RES: DOMEventResponder>(self, target: RES) -> RES {
|
||||
::paste::paste! {
|
||||
let (
|
||||
$(
|
||||
[<$ty:lower>],)*
|
||||
) = self;
|
||||
$(
|
||||
let target = [<$ty:lower>].attach(target);
|
||||
)*
|
||||
target
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
tc!(A);
|
||||
tc!(A, B);
|
||||
tc!(A, B, C);
|
||||
tc!(A, B, C, D);
|
||||
tc!(A, B, C, D, E);
|
||||
tc!(A, B, C, D, E, F);
|
||||
tc!(A, B, C, D, E, F, G);
|
||||
tc!(A, B, C, D, E, F, G, H);
|
||||
tc!(A, B, C, D, E, F, G, H, I);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y);
|
||||
#[rustfmt::skip]
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z);
|
||||
|
||||
macro_rules! collection_callback {
|
||||
{$(
|
||||
$collection:ident
|
||||
),* $(,)?} => {
|
||||
$(
|
||||
impl<T> EventHandler for $collection<T>
|
||||
where
|
||||
T: EventHandler
|
||||
{
|
||||
#[inline]
|
||||
fn attach<R: DOMEventResponder>(self, target: R) -> R {
|
||||
let mut target = target;
|
||||
for item in self {
|
||||
target = item.attach(target);
|
||||
}
|
||||
target
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
use std::collections::{BTreeSet, BinaryHeap, HashSet, LinkedList, VecDeque};
|
||||
|
||||
collection_callback! {
|
||||
Vec,
|
||||
BTreeSet,
|
||||
BinaryHeap,
|
||||
HashSet,
|
||||
LinkedList,
|
||||
VecDeque,
|
||||
}
|
||||
|
||||
macro_rules! generate_event_types {
|
||||
{$(
|
||||
$( #[$does_not_bubble:ident] )?
|
||||
$event:ident : $web_sys_event:ident
|
||||
$( $event:ident )+ : $web_event:ident
|
||||
),* $(,)?} => {
|
||||
|
||||
$(
|
||||
#[doc = concat!("The `", stringify!($event), "` event, which receives [", stringify!($web_sys_event), "](web_sys::", stringify!($web_sys_event), ") as its argument.")]
|
||||
#[derive(Copy, Clone)]
|
||||
::paste::paste! {
|
||||
$(
|
||||
#[doc = "The `" [< $($event)+ >] "` event, which receives [" $web_event "](web_sys::" $web_event ") as its argument."]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub struct $event;
|
||||
pub struct [<$( $event )+ >];
|
||||
|
||||
impl EventDescriptor for $event {
|
||||
type EventType = web_sys::$web_sys_event;
|
||||
impl EventDescriptor for [< $($event)+ >] {
|
||||
type EventType = web_sys::$web_event;
|
||||
|
||||
#[inline(always)]
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
stringify!($event).into()
|
||||
fn name(&self) -> Oco<'static, str> {
|
||||
stringify!([< $($event)+ >]).into()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn event_delegation_key(&self) -> Cow<'static, str> {
|
||||
concat!("$$$", stringify!($event)).into()
|
||||
fn event_delegation_key(&self) -> Oco<'static, str> {
|
||||
concat!("$$$", stringify!([< $($event)+ >])).into()
|
||||
}
|
||||
|
||||
const BUBBLES: bool = true $(&& generate_event_types!($does_not_bubble))?;
|
||||
}
|
||||
)*
|
||||
)*
|
||||
|
||||
/// An enum holding all basic event types with their respective handlers.
|
||||
///
|
||||
/// It currently omits [`Custom`] and [`undelegated`] variants.
|
||||
#[non_exhaustive]
|
||||
pub enum GenericEventHandler {
|
||||
$(
|
||||
#[doc = "Variant mapping [`struct@" [< $($event)+ >] "`] to its event handler type."]
|
||||
[< $($event:camel)+ >]([< $($event)+ >], Box<dyn FnMut($web_event) + 'static>),
|
||||
)*
|
||||
}
|
||||
|
||||
impl ::std::fmt::Debug for GenericEventHandler {
|
||||
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
|
||||
match self {
|
||||
$(
|
||||
Self::[< $($event:camel)+ >](event, _) => f
|
||||
.debug_tuple(stringify!([< $($event:camel)+ >]))
|
||||
.field(&event)
|
||||
.field(&::std::any::type_name::<Box<dyn FnMut($web_event) + 'static>>())
|
||||
.finish(),
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventHandler for GenericEventHandler {
|
||||
fn attach<T: DOMEventResponder>(self, target: T) -> T {
|
||||
match self {
|
||||
$(
|
||||
Self::[< $($event:camel)+ >](event, handler) => target.add(event, handler),
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(
|
||||
impl<F> From<([< $($event)+ >], F)> for GenericEventHandler
|
||||
where
|
||||
F: FnMut($web_event) + 'static
|
||||
{
|
||||
fn from(value: ([< $($event)+ >], F)) -> Self {
|
||||
Self::[< $($event:camel)+ >](value.0, Box::new(value.1))
|
||||
}
|
||||
}
|
||||
// NOTE: this could become legal in future and would save us from useless allocations
|
||||
//impl<F> From<([< $($event)+ >], Box<F>)> for GenericEventHandler
|
||||
//where
|
||||
// F: FnMut($web_event) + 'static
|
||||
//{
|
||||
// fn from(value: ([< $($event)+ >], Box<F>)) -> Self {
|
||||
// Self::[< $($event:camel)+ >](value.0, value.1)
|
||||
// }
|
||||
//}
|
||||
impl<F> EventHandler for ([< $($event)+ >], F)
|
||||
where
|
||||
F: FnMut($web_event) + 'static
|
||||
{
|
||||
fn attach<L: DOMEventResponder>(self, target: L) -> L {
|
||||
target.add(self.0, self.1)
|
||||
}
|
||||
}
|
||||
)*
|
||||
}
|
||||
};
|
||||
|
||||
(does_not_bubble) => { false }
|
||||
@@ -163,36 +386,36 @@ generate_event_types! {
|
||||
// WindowEventHandlersEventMap
|
||||
// =========================================================
|
||||
#[does_not_bubble]
|
||||
afterprint: Event,
|
||||
after print: Event,
|
||||
#[does_not_bubble]
|
||||
beforeprint: Event,
|
||||
before print: Event,
|
||||
#[does_not_bubble]
|
||||
beforeunload: BeforeUnloadEvent,
|
||||
before unload: BeforeUnloadEvent,
|
||||
#[does_not_bubble]
|
||||
gamepadconnected: GamepadEvent,
|
||||
gamepad connected: GamepadEvent,
|
||||
#[does_not_bubble]
|
||||
gamepaddisconnected: GamepadEvent,
|
||||
hashchange: HashChangeEvent,
|
||||
gamepad disconnected: GamepadEvent,
|
||||
hash change: HashChangeEvent,
|
||||
#[does_not_bubble]
|
||||
languagechange: Event,
|
||||
language change: Event,
|
||||
#[does_not_bubble]
|
||||
message: MessageEvent,
|
||||
#[does_not_bubble]
|
||||
messageerror: MessageEvent,
|
||||
message error: MessageEvent,
|
||||
#[does_not_bubble]
|
||||
offline: Event,
|
||||
#[does_not_bubble]
|
||||
online: Event,
|
||||
#[does_not_bubble]
|
||||
pagehide: PageTransitionEvent,
|
||||
page hide: PageTransitionEvent,
|
||||
#[does_not_bubble]
|
||||
pageshow: PageTransitionEvent,
|
||||
popstate: PopStateEvent,
|
||||
rejectionhandled: PromiseRejectionEvent,
|
||||
page show: PageTransitionEvent,
|
||||
pop state: PopStateEvent,
|
||||
rejection handled: PromiseRejectionEvent,
|
||||
#[does_not_bubble]
|
||||
storage: StorageEvent,
|
||||
#[does_not_bubble]
|
||||
unhandledrejection: PromiseRejectionEvent,
|
||||
unhandled rejection: PromiseRejectionEvent,
|
||||
#[does_not_bubble]
|
||||
unload: Event,
|
||||
|
||||
@@ -201,38 +424,38 @@ generate_event_types! {
|
||||
// =========================================================
|
||||
#[does_not_bubble]
|
||||
abort: UiEvent,
|
||||
animationcancel: AnimationEvent,
|
||||
animationend: AnimationEvent,
|
||||
animationiteration: AnimationEvent,
|
||||
animationstart: AnimationEvent,
|
||||
auxclick: MouseEvent,
|
||||
beforeinput: InputEvent,
|
||||
animation cancel: AnimationEvent,
|
||||
animation end: AnimationEvent,
|
||||
animation iteration: AnimationEvent,
|
||||
animation start: AnimationEvent,
|
||||
aux click: MouseEvent,
|
||||
before input: InputEvent,
|
||||
#[does_not_bubble]
|
||||
blur: FocusEvent,
|
||||
#[does_not_bubble]
|
||||
canplay: Event,
|
||||
can play: Event,
|
||||
#[does_not_bubble]
|
||||
canplaythrough: Event,
|
||||
can play through: Event,
|
||||
change: Event,
|
||||
click: MouseEvent,
|
||||
#[does_not_bubble]
|
||||
close: Event,
|
||||
compositionend: CompositionEvent,
|
||||
compositionstart: CompositionEvent,
|
||||
compositionupdate: CompositionEvent,
|
||||
contextmenu: MouseEvent,
|
||||
composition end: CompositionEvent,
|
||||
composition start: CompositionEvent,
|
||||
composition update: CompositionEvent,
|
||||
context menu: MouseEvent,
|
||||
#[does_not_bubble]
|
||||
cuechange: Event,
|
||||
dblclick: MouseEvent,
|
||||
cue change: Event,
|
||||
dbl click: MouseEvent,
|
||||
drag: DragEvent,
|
||||
dragend: DragEvent,
|
||||
dragenter: DragEvent,
|
||||
dragleave: DragEvent,
|
||||
dragover: DragEvent,
|
||||
dragstart: DragEvent,
|
||||
drag end: DragEvent,
|
||||
drag enter: DragEvent,
|
||||
drag leave: DragEvent,
|
||||
drag over: DragEvent,
|
||||
drag start: DragEvent,
|
||||
drop: DragEvent,
|
||||
#[does_not_bubble]
|
||||
durationchange: Event,
|
||||
duration change: Event,
|
||||
#[does_not_bubble]
|
||||
emptied: Event,
|
||||
#[does_not_bubble]
|
||||
@@ -242,110 +465,110 @@ generate_event_types! {
|
||||
#[does_not_bubble]
|
||||
focus: FocusEvent,
|
||||
#[does_not_bubble]
|
||||
focusin: FocusEvent,
|
||||
focus in: FocusEvent,
|
||||
#[does_not_bubble]
|
||||
focusout: FocusEvent,
|
||||
formdata: Event, // web_sys does not include `FormDataEvent`
|
||||
focus out: FocusEvent,
|
||||
form data: Event, // web_sys does not include `FormDataEvent`
|
||||
#[does_not_bubble]
|
||||
gotpointercapture: PointerEvent,
|
||||
got pointer capture: PointerEvent,
|
||||
input: Event,
|
||||
#[does_not_bubble]
|
||||
invalid: Event,
|
||||
keydown: KeyboardEvent,
|
||||
keypress: KeyboardEvent,
|
||||
keyup: KeyboardEvent,
|
||||
key down: KeyboardEvent,
|
||||
key press: KeyboardEvent,
|
||||
key up: KeyboardEvent,
|
||||
#[does_not_bubble]
|
||||
load: Event,
|
||||
#[does_not_bubble]
|
||||
loadeddata: Event,
|
||||
loaded data: Event,
|
||||
#[does_not_bubble]
|
||||
loadedmetadata: Event,
|
||||
loaded metadata: Event,
|
||||
#[does_not_bubble]
|
||||
loadstart: Event,
|
||||
lostpointercapture: PointerEvent,
|
||||
mousedown: MouseEvent,
|
||||
load start: Event,
|
||||
lost pointer capture: PointerEvent,
|
||||
mouse down: MouseEvent,
|
||||
#[does_not_bubble]
|
||||
mouseenter: MouseEvent,
|
||||
mouse enter: MouseEvent,
|
||||
#[does_not_bubble]
|
||||
mouseleave: MouseEvent,
|
||||
mousemove: MouseEvent,
|
||||
mouseout: MouseEvent,
|
||||
mouseover: MouseEvent,
|
||||
mouseup: MouseEvent,
|
||||
mouse leave: MouseEvent,
|
||||
mouse move: MouseEvent,
|
||||
mouse out: MouseEvent,
|
||||
mouse over: MouseEvent,
|
||||
mouse up: MouseEvent,
|
||||
#[does_not_bubble]
|
||||
pause: Event,
|
||||
#[does_not_bubble]
|
||||
play: Event,
|
||||
#[does_not_bubble]
|
||||
playing: Event,
|
||||
pointercancel: PointerEvent,
|
||||
pointerdown: PointerEvent,
|
||||
pointer cancel: PointerEvent,
|
||||
pointer down: PointerEvent,
|
||||
#[does_not_bubble]
|
||||
pointerenter: PointerEvent,
|
||||
pointer enter: PointerEvent,
|
||||
#[does_not_bubble]
|
||||
pointerleave: PointerEvent,
|
||||
pointermove: PointerEvent,
|
||||
pointerout: PointerEvent,
|
||||
pointerover: PointerEvent,
|
||||
pointerup: PointerEvent,
|
||||
pointer leave: PointerEvent,
|
||||
pointer move: PointerEvent,
|
||||
pointer out: PointerEvent,
|
||||
pointer over: PointerEvent,
|
||||
pointer up: PointerEvent,
|
||||
#[does_not_bubble]
|
||||
progress: ProgressEvent,
|
||||
#[does_not_bubble]
|
||||
ratechange: Event,
|
||||
rate change: Event,
|
||||
reset: Event,
|
||||
#[does_not_bubble]
|
||||
resize: UiEvent,
|
||||
#[does_not_bubble]
|
||||
scroll: Event,
|
||||
#[does_not_bubble]
|
||||
scrollend: Event,
|
||||
securitypolicyviolation: SecurityPolicyViolationEvent,
|
||||
scroll end: Event,
|
||||
security policy violation: SecurityPolicyViolationEvent,
|
||||
#[does_not_bubble]
|
||||
seeked: Event,
|
||||
#[does_not_bubble]
|
||||
seeking: Event,
|
||||
select: Event,
|
||||
#[does_not_bubble]
|
||||
selectionchange: Event,
|
||||
selectstart: Event,
|
||||
slotchange: Event,
|
||||
selection change: Event,
|
||||
select start: Event,
|
||||
slot change: Event,
|
||||
#[does_not_bubble]
|
||||
stalled: Event,
|
||||
submit: SubmitEvent,
|
||||
#[does_not_bubble]
|
||||
suspend: Event,
|
||||
#[does_not_bubble]
|
||||
timeupdate: Event,
|
||||
time update: Event,
|
||||
#[does_not_bubble]
|
||||
toggle: Event,
|
||||
touchcancel: TouchEvent,
|
||||
touchend: TouchEvent,
|
||||
touchmove: TouchEvent,
|
||||
touchstart: TouchEvent,
|
||||
transitioncancel: TransitionEvent,
|
||||
transitionend: TransitionEvent,
|
||||
transitionrun: TransitionEvent,
|
||||
transitionstart: TransitionEvent,
|
||||
touch cancel: TouchEvent,
|
||||
touch end: TouchEvent,
|
||||
touch move: TouchEvent,
|
||||
touch start: TouchEvent,
|
||||
transition cancel: TransitionEvent,
|
||||
transition end: TransitionEvent,
|
||||
transition run: TransitionEvent,
|
||||
transition start: TransitionEvent,
|
||||
#[does_not_bubble]
|
||||
volumechange: Event,
|
||||
volume change: Event,
|
||||
#[does_not_bubble]
|
||||
waiting: Event,
|
||||
webkitanimationend: Event,
|
||||
webkitanimationiteration: Event,
|
||||
webkitanimationstart: Event,
|
||||
webkittransitionend: Event,
|
||||
webkit animation end: Event,
|
||||
webkit animation iteration: Event,
|
||||
webkit animation start: Event,
|
||||
webkit transition end: Event,
|
||||
wheel: WheelEvent,
|
||||
|
||||
// =========================================================
|
||||
// WindowEventMap
|
||||
// =========================================================
|
||||
DOMContentLoaded: Event,
|
||||
D O M Content Loaded: Event, // Hack for correct casing
|
||||
#[does_not_bubble]
|
||||
devicemotion: DeviceMotionEvent,
|
||||
device motion: DeviceMotionEvent,
|
||||
#[does_not_bubble]
|
||||
deviceorientation: DeviceOrientationEvent,
|
||||
device orientation: DeviceOrientationEvent,
|
||||
#[does_not_bubble]
|
||||
orientationchange: Event,
|
||||
orientation change: Event,
|
||||
|
||||
// =========================================================
|
||||
// DocumentAndElementEventHandlersEventMap
|
||||
@@ -357,13 +580,13 @@ generate_event_types! {
|
||||
// =========================================================
|
||||
// DocumentEventMap
|
||||
// =========================================================
|
||||
fullscreenchange: Event,
|
||||
fullscreenerror: Event,
|
||||
pointerlockchange: Event,
|
||||
pointerlockerror: Event,
|
||||
fullscreen change: Event,
|
||||
fullscreen error: Event,
|
||||
pointer lock change: Event,
|
||||
pointer lock error: Event,
|
||||
#[does_not_bubble]
|
||||
readystatechange: Event,
|
||||
visibilitychange: Event,
|
||||
ready state change: Event,
|
||||
visibility change: Event,
|
||||
}
|
||||
|
||||
// Export `web_sys` event types
|
||||
@@ -371,7 +594,7 @@ pub use web_sys::{
|
||||
AnimationEvent, BeforeUnloadEvent, CompositionEvent, CustomEvent,
|
||||
DeviceMotionEvent, DeviceOrientationEvent, DragEvent, ErrorEvent, Event,
|
||||
FocusEvent, GamepadEvent, HashChangeEvent, InputEvent, KeyboardEvent,
|
||||
MouseEvent, PageTransitionEvent, PointerEvent, PopStateEvent,
|
||||
MessageEvent, MouseEvent, PageTransitionEvent, PointerEvent, PopStateEvent,
|
||||
ProgressEvent, PromiseRejectionEvent, SecurityPolicyViolationEvent,
|
||||
StorageEvent, SubmitEvent, TouchEvent, TransitionEvent, UiEvent,
|
||||
WheelEvent,
|
||||
|
||||
@@ -66,12 +66,13 @@ use crate::{
|
||||
macro_helpers::{IntoAttribute, IntoClass, IntoProperty, IntoStyle},
|
||||
Element, Fragment, IntoView, NodeRef, Text, View,
|
||||
};
|
||||
use std::{borrow::Cow, fmt};
|
||||
use leptos_reactive::Oco;
|
||||
use std::fmt;
|
||||
|
||||
/// Trait which allows creating an element tag.
|
||||
pub trait ElementDescriptor: ElementDescriptorBounds {
|
||||
/// The name of the element, i.e., `div`, `p`, `custom-element`.
|
||||
fn name(&self) -> Cow<'static, str>;
|
||||
fn name(&self) -> Oco<'static, str>;
|
||||
|
||||
/// Determines if the tag is void, i.e., `<input>` and `<br>`.
|
||||
#[inline(always)]
|
||||
@@ -126,7 +127,7 @@ where
|
||||
/// Represents potentially any element.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AnyElement {
|
||||
pub(crate) name: Cow<'static, str>,
|
||||
pub(crate) name: Oco<'static, str>,
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) element: web_sys::HtmlElement,
|
||||
pub(crate) is_void: bool,
|
||||
@@ -159,7 +160,7 @@ impl std::convert::AsRef<web_sys::HtmlElement> for AnyElement {
|
||||
}
|
||||
|
||||
impl ElementDescriptor for AnyElement {
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
fn name(&self) -> Oco<'static, str> {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
@@ -178,7 +179,7 @@ impl ElementDescriptor for AnyElement {
|
||||
/// Represents a custom HTML element, such as `<my-element>`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Custom {
|
||||
name: Cow<'static, str>,
|
||||
name: Oco<'static, str>,
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
element: web_sys::HtmlElement,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
@@ -187,7 +188,7 @@ pub struct Custom {
|
||||
|
||||
impl Custom {
|
||||
/// Creates a new custom element with the given tag name.
|
||||
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
|
||||
pub fn new(name: impl Into<Oco<'static, str>>) -> Self {
|
||||
let name = name.into();
|
||||
let id = HydrationCtx::id();
|
||||
|
||||
@@ -266,7 +267,7 @@ impl std::convert::AsRef<web_sys::HtmlElement> for Custom {
|
||||
}
|
||||
|
||||
impl ElementDescriptor for Custom {
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
fn name(&self) -> Oco<'static, str> {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
@@ -294,12 +295,12 @@ cfg_if! {
|
||||
#[derive(educe::Educe, Clone)]
|
||||
#[educe(Debug)]
|
||||
pub struct HtmlElement<El: ElementDescriptor> {
|
||||
pub(crate) element: El,
|
||||
pub(crate) attrs: SmallVec<[(Cow<'static, str>, Cow<'static, str>); 4]>,
|
||||
#[educe(Debug(ignore))]
|
||||
pub(crate) children: ElementChildren,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) view_marker: Option<String>
|
||||
pub(crate) element: El,
|
||||
pub(crate) attrs: SmallVec<[(Oco<'static, str>, Oco<'static, str>); 4]>,
|
||||
#[educe(Debug(ignore))]
|
||||
pub(crate) children: ElementChildren,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) view_marker: Option<String>
|
||||
}
|
||||
|
||||
#[derive(Clone, educe::Educe, PartialEq, Eq)]
|
||||
@@ -308,14 +309,14 @@ cfg_if! {
|
||||
#[educe(Default)]
|
||||
Empty,
|
||||
Children(Vec<View>),
|
||||
InnerHtml(Cow<'static, str>),
|
||||
InnerHtml(Oco<'static, str>),
|
||||
Chunks(Vec<StringOrView>)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Clone)]
|
||||
pub enum StringOrView {
|
||||
String(Cow<'static, str>),
|
||||
String(Oco<'static, str>),
|
||||
View(std::rc::Rc<dyn Fn() -> View>)
|
||||
}
|
||||
|
||||
@@ -445,7 +446,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
/// Adds an `id` to the element.
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn id(self, id: impl Into<Cow<'static, str>>) -> Self {
|
||||
pub fn id(self, id: impl Into<Oco<'static, str>>) -> Self {
|
||||
let id = id.into();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
@@ -575,7 +576,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
#[cfg_attr(all(target_arch = "wasm32", feature = "web"), inline(always))]
|
||||
pub fn attr(
|
||||
self,
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
name: impl Into<Oco<'static, str>>,
|
||||
attr: impl IntoAttribute,
|
||||
) -> Self {
|
||||
let name = name.into();
|
||||
@@ -634,7 +635,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
#[track_caller]
|
||||
pub fn class(
|
||||
self,
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
name: impl Into<Oco<'static, str>>,
|
||||
class: impl IntoClass,
|
||||
) -> Self {
|
||||
let name = name.into();
|
||||
@@ -686,7 +687,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
/// Adds a list of classes separated by ASCII whitespace to an element.
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn classes(self, classes: impl Into<Cow<'static, str>>) -> Self {
|
||||
pub fn classes(self, classes: impl Into<Oco<'static, str>>) -> Self {
|
||||
self.classes_inner(&classes.into())
|
||||
}
|
||||
|
||||
@@ -698,7 +699,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = C>,
|
||||
C: Into<Cow<'static, str>>,
|
||||
C: Into<Oco<'static, str>>,
|
||||
{
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
@@ -708,12 +709,12 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
|
||||
leptos_reactive::create_effect(
|
||||
move |prev_classes: Option<
|
||||
SmallVec<[Cow<'static, str>; 4]>,
|
||||
SmallVec<[Oco<'static, str>; 4]>,
|
||||
>| {
|
||||
let classes = classes_signal()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<SmallVec<[Cow<'static, str>; 4]>>(
|
||||
.collect::<SmallVec<[Oco<'static, str>; 4]>>(
|
||||
);
|
||||
|
||||
let new_classes = classes
|
||||
@@ -797,7 +798,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
#[track_caller]
|
||||
pub fn style(
|
||||
self,
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
name: impl Into<Oco<'static, str>>,
|
||||
style: impl IntoStyle,
|
||||
) -> Self {
|
||||
let name = name.into();
|
||||
@@ -856,7 +857,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
#[track_caller]
|
||||
pub fn prop(
|
||||
self,
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
name: impl Into<Oco<'static, str>>,
|
||||
value: impl IntoProperty,
|
||||
) -> Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
@@ -1016,7 +1017,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
/// sanitize the input to avoid a cross-site scripting (XSS)
|
||||
/// vulnerability.
|
||||
#[inline(always)]
|
||||
pub fn inner_html(self, html: impl Into<Cow<'static, str>>) -> Self {
|
||||
pub fn inner_html(self, html: impl Into<Oco<'static, str>>) -> Self {
|
||||
let html = html.into();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
@@ -1103,7 +1104,7 @@ pub fn custom<El: ElementDescriptor>(el: El) -> HtmlElement<Custom> {
|
||||
|
||||
/// Creates a text node.
|
||||
#[inline(always)]
|
||||
pub fn text(text: impl Into<Cow<'static, str>>) -> Text {
|
||||
pub fn text(text: impl Into<Oco<'static, str>>) -> Text {
|
||||
Text::new(text.into())
|
||||
}
|
||||
|
||||
@@ -1190,7 +1191,7 @@ macro_rules! generate_html_tags {
|
||||
|
||||
impl ElementDescriptor for [<$tag:camel $($trailing_)?>] {
|
||||
#[inline(always)]
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
fn name(&self) -> Oco<'static, str> {
|
||||
stringify!($tag).into()
|
||||
}
|
||||
|
||||
|
||||
@@ -28,12 +28,13 @@ use cfg_if::cfg_if;
|
||||
pub use components::*;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub use events::add_event_helper;
|
||||
pub use events::typed as ev;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use events::{add_event_listener, add_event_listener_undelegated};
|
||||
pub use events::{typed as ev, typed::EventHandler};
|
||||
pub use html::HtmlElement;
|
||||
use html::{AnyElement, ElementDescriptor};
|
||||
pub use hydration::{HydrationCtx, HydrationKey};
|
||||
use leptos_reactive::Oco;
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
use leptos_reactive::{
|
||||
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
|
||||
@@ -240,7 +241,7 @@ cfg_if! {
|
||||
pub struct Element {
|
||||
#[doc(hidden)]
|
||||
#[cfg(debug_assertions)]
|
||||
pub name: Cow<'static, str>,
|
||||
pub name: Oco<'static, str>,
|
||||
#[doc(hidden)]
|
||||
pub element: web_sys::HtmlElement,
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -261,9 +262,9 @@ cfg_if! {
|
||||
/// HTML element.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Element {
|
||||
name: Cow<'static, str>,
|
||||
name: Oco<'static, str>,
|
||||
is_void: bool,
|
||||
attrs: SmallVec<[(Cow<'static, str>, Cow<'static, str>); 4]>,
|
||||
attrs: SmallVec<[(Oco<'static, str>, Oco<'static, str>); 4]>,
|
||||
children: ElementChildren,
|
||||
id: HydrationKey,
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -396,13 +397,13 @@ impl Element {
|
||||
struct Comment {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
node: web_sys::Node,
|
||||
content: Cow<'static, str>,
|
||||
content: Oco<'static, str>,
|
||||
}
|
||||
|
||||
impl Comment {
|
||||
#[inline]
|
||||
fn new(
|
||||
content: impl Into<Cow<'static, str>>,
|
||||
content: impl Into<Oco<'static, str>>,
|
||||
id: &HydrationKey,
|
||||
closing: bool,
|
||||
) -> Self {
|
||||
@@ -410,7 +411,7 @@ impl Comment {
|
||||
}
|
||||
|
||||
fn new_inner(
|
||||
content: Cow<'static, str>,
|
||||
content: Oco<'static, str>,
|
||||
id: &HydrationKey,
|
||||
closing: bool,
|
||||
) -> Self {
|
||||
@@ -466,12 +467,13 @@ pub struct Text {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
node: web_sys::Node,
|
||||
/// The current contents of the text node.
|
||||
pub content: Cow<'static, str>,
|
||||
pub content: Oco<'static, str>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Text {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "\"{}\"", self.content)
|
||||
fmt::Debug::fmt(&self.content, f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,7 +486,7 @@ impl IntoView for Text {
|
||||
|
||||
impl Text {
|
||||
/// Creates a new [`Text`].
|
||||
pub fn new(content: Cow<'static, str>) -> Self {
|
||||
pub fn new(content: Oco<'static, str>) -> Self {
|
||||
Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
node: crate::document()
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
use leptos_reactive::Oco;
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
use leptos_reactive::{
|
||||
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
|
||||
};
|
||||
use std::{borrow::Cow, rc::Rc};
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
@@ -10,11 +15,11 @@ use wasm_bindgen::UnwrapThrowExt;
|
||||
#[derive(Clone)]
|
||||
pub enum Attribute {
|
||||
/// A plain string value.
|
||||
String(Cow<'static, str>),
|
||||
String(Oco<'static, str>),
|
||||
/// A (presumably reactive) function, which will be run inside an effect to do targeted updates to the attribute.
|
||||
Fn(Rc<dyn Fn() -> Attribute>),
|
||||
/// An optional string value, which sets the attribute to the value if `Some` and removes the attribute if `None`.
|
||||
Option(Option<Cow<'static, str>>),
|
||||
Option(Option<Oco<'static, str>>),
|
||||
/// A boolean attribute, which sets the attribute if `true` and removes the attribute if `false`.
|
||||
Bool(bool),
|
||||
}
|
||||
@@ -25,7 +30,7 @@ impl Attribute {
|
||||
pub fn as_value_string(
|
||||
&self,
|
||||
attr_name: &'static str,
|
||||
) -> Cow<'static, str> {
|
||||
) -> Oco<'static, str> {
|
||||
match self {
|
||||
Attribute::String(value) => {
|
||||
format!("{attr_name}=\"{value}\"").into()
|
||||
@@ -42,14 +47,14 @@ impl Attribute {
|
||||
.map(|value| format!("{attr_name}=\"{value}\"").into())
|
||||
.unwrap_or_default(),
|
||||
Attribute::Bool(include) => {
|
||||
Cow::Borrowed(if *include { attr_name } else { "" })
|
||||
Oco::Borrowed(if *include { attr_name } else { "" })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the attribute to its HTML value at that moment, not including
|
||||
/// the attribute name, so it can be rendered on the server.
|
||||
pub fn as_nameless_value_string(&self) -> Option<Cow<'static, str>> {
|
||||
pub fn as_nameless_value_string(&self) -> Option<Oco<'static, str>> {
|
||||
match self {
|
||||
Attribute::String(value) => Some(value.clone()),
|
||||
Attribute::Fn(f) => {
|
||||
@@ -144,7 +149,7 @@ impl IntoAttribute for Option<Attribute> {
|
||||
impl IntoAttribute for String {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::String(Cow::Owned(self))
|
||||
Attribute::String(Oco::Owned(self))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
@@ -153,13 +158,22 @@ impl IntoAttribute for String {
|
||||
impl IntoAttribute for &'static str {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::String(Cow::Borrowed(self))
|
||||
Attribute::String(Oco::Borrowed(self))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Cow<'static, str> {
|
||||
impl IntoAttribute for Rc<str> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::String(Oco::Counted(self))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Oco<'static, str> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::String(self)
|
||||
@@ -180,7 +194,7 @@ impl IntoAttribute for bool {
|
||||
impl IntoAttribute for Option<String> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::Option(self.map(Cow::Owned))
|
||||
Attribute::Option(self.map(Oco::Owned))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
@@ -189,13 +203,31 @@ impl IntoAttribute for Option<String> {
|
||||
impl IntoAttribute for Option<&'static str> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::Option(self.map(Cow::Borrowed))
|
||||
Attribute::Option(self.map(Oco::Borrowed))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<Rc<str>> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::Option(self.map(Oco::Counted))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<Cow<'static, str>> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::Option(self.map(Oco::from))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<Oco<'static, str>> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::Option(self)
|
||||
@@ -263,6 +295,41 @@ macro_rules! attr_type {
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! attr_signal_type {
|
||||
($signal_type:ty) => {
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T> IntoAttribute for $signal_type
|
||||
where
|
||||
T: IntoAttribute + Clone,
|
||||
{
|
||||
fn into_attribute(self) -> Attribute {
|
||||
let modified_fn = Rc::new(move || self.get().into_attribute());
|
||||
Attribute::Fn(modified_fn)
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! attr_signal_type_optional {
|
||||
($signal_type:ty) => {
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T> IntoAttribute for $signal_type
|
||||
where
|
||||
T: Clone,
|
||||
Option<T>: IntoAttribute,
|
||||
{
|
||||
fn into_attribute(self) -> Attribute {
|
||||
let modified_fn = Rc::new(move || self.get().into_attribute());
|
||||
Attribute::Fn(modified_fn)
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
attr_type!(&String);
|
||||
attr_type!(usize);
|
||||
attr_type!(u8);
|
||||
@@ -280,12 +347,19 @@ attr_type!(f32);
|
||||
attr_type!(f64);
|
||||
attr_type!(char);
|
||||
|
||||
attr_signal_type!(ReadSignal<T>);
|
||||
attr_signal_type!(RwSignal<T>);
|
||||
attr_signal_type!(Memo<T>);
|
||||
attr_signal_type!(Signal<T>);
|
||||
attr_signal_type!(MaybeSignal<T>);
|
||||
attr_signal_type_optional!(MaybeProp<T>);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[doc(hidden)]
|
||||
#[inline(never)]
|
||||
pub fn attribute_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
name: Oco<'static, str>,
|
||||
value: Attribute,
|
||||
) {
|
||||
use leptos_reactive::create_render_effect;
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
use leptos_reactive::{
|
||||
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
|
||||
};
|
||||
|
||||
/// Represents the different possible values a single class on an element could have,
|
||||
/// allowing you to do fine-grained updates to single items
|
||||
/// in [`Element.classList`](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList).
|
||||
@@ -60,14 +65,14 @@ impl Class {
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use std::borrow::Cow;
|
||||
use leptos_reactive::Oco;
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[doc(hidden)]
|
||||
#[inline(never)]
|
||||
pub fn class_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
name: Oco<'static, str>,
|
||||
value: Class,
|
||||
) {
|
||||
use leptos_reactive::create_render_effect;
|
||||
@@ -113,3 +118,36 @@ pub(crate) fn class_expression(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! class_signal_type {
|
||||
($signal_type:ty) => {
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl IntoClass for $signal_type {
|
||||
#[inline(always)]
|
||||
fn into_class(self) -> Class {
|
||||
let modified_fn = Box::new(move || self.get());
|
||||
Class::Fn(modified_fn)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! class_signal_type_optional {
|
||||
($signal_type:ty) => {
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl IntoClass for $signal_type {
|
||||
#[inline(always)]
|
||||
fn into_class(self) -> Class {
|
||||
let modified_fn = Box::new(move || self.get().unwrap_or(false));
|
||||
Class::Fn(modified_fn)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class_signal_type!(ReadSignal<bool>);
|
||||
class_signal_type!(RwSignal<bool>);
|
||||
class_signal_type!(Memo<bool>);
|
||||
class_signal_type!(Signal<bool>);
|
||||
class_signal_type!(MaybeSignal<bool>);
|
||||
class_signal_type_optional!(MaybeProp<bool>);
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
use leptos_reactive::{
|
||||
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
|
||||
};
|
||||
use wasm_bindgen::JsValue;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
@@ -52,6 +56,37 @@ macro_rules! prop_type {
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! prop_signal_type {
|
||||
($signal_type:ty) => {
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T> IntoProperty for $signal_type
|
||||
where
|
||||
T: Into<JsValue> + Clone,
|
||||
{
|
||||
fn into_property(self) -> Property {
|
||||
let modified_fn = Box::new(move || self.get().into());
|
||||
Property::Fn(modified_fn)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! prop_signal_type_optional {
|
||||
($signal_type:ty) => {
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T> IntoProperty for $signal_type
|
||||
where
|
||||
T: Clone,
|
||||
Option<T>: Into<JsValue>,
|
||||
{
|
||||
fn into_property(self) -> Property {
|
||||
let modified_fn = Box::new(move || self.get().into());
|
||||
Property::Fn(modified_fn)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
prop_type!(JsValue);
|
||||
prop_type!(String);
|
||||
prop_type!(&String);
|
||||
@@ -72,14 +107,21 @@ prop_type!(f32);
|
||||
prop_type!(f64);
|
||||
prop_type!(bool);
|
||||
|
||||
prop_signal_type!(ReadSignal<T>);
|
||||
prop_signal_type!(RwSignal<T>);
|
||||
prop_signal_type!(Memo<T>);
|
||||
prop_signal_type!(Signal<T>);
|
||||
prop_signal_type!(MaybeSignal<T>);
|
||||
prop_signal_type_optional!(MaybeProp<T>);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use std::borrow::Cow;
|
||||
use leptos_reactive::Oco;
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[inline(never)]
|
||||
pub(crate) fn property_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
name: Oco<'static, str>,
|
||||
value: Property,
|
||||
) {
|
||||
use leptos_reactive::create_render_effect;
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
use leptos_reactive::Oco;
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
use leptos_reactive::{
|
||||
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
|
||||
};
|
||||
use std::{borrow::Cow, rc::Rc};
|
||||
|
||||
/// todo docs
|
||||
#[derive(Clone)]
|
||||
pub enum Style {
|
||||
/// A plain string value.
|
||||
Value(Cow<'static, str>),
|
||||
Value(Oco<'static, str>),
|
||||
/// An optional string value, which sets the property to the value if `Some` and removes the property if `None`.
|
||||
Option(Option<Cow<'static, str>>),
|
||||
Option(Option<Oco<'static, str>>),
|
||||
/// A (presumably reactive) function, which will be run inside an effect to update the style.
|
||||
Fn(Rc<dyn Fn() -> Style>),
|
||||
}
|
||||
@@ -41,28 +46,70 @@ pub trait IntoStyle {
|
||||
impl IntoStyle for &'static str {
|
||||
#[inline(always)]
|
||||
fn into_style(self) -> Style {
|
||||
Style::Value(self.into())
|
||||
Style::Value(Oco::Borrowed(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for String {
|
||||
#[inline(always)]
|
||||
fn into_style(self) -> Style {
|
||||
Style::Value(Oco::Owned(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for Rc<str> {
|
||||
#[inline(always)]
|
||||
fn into_style(self) -> Style {
|
||||
Style::Value(Oco::Counted(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for Cow<'static, str> {
|
||||
#[inline(always)]
|
||||
fn into_style(self) -> Style {
|
||||
Style::Value(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for Oco<'static, str> {
|
||||
#[inline(always)]
|
||||
fn into_style(self) -> Style {
|
||||
Style::Value(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for Option<&'static str> {
|
||||
#[inline(always)]
|
||||
fn into_style(self) -> Style {
|
||||
Style::Option(self.map(Cow::Borrowed))
|
||||
Style::Option(self.map(Oco::Borrowed))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for Option<String> {
|
||||
#[inline(always)]
|
||||
fn into_style(self) -> Style {
|
||||
Style::Option(self.map(Cow::Owned))
|
||||
Style::Option(self.map(Oco::Owned))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for Option<Rc<str>> {
|
||||
#[inline(always)]
|
||||
fn into_style(self) -> Style {
|
||||
Style::Option(self.map(Oco::Counted))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for Option<Cow<'static, str>> {
|
||||
#[inline(always)]
|
||||
fn into_style(self) -> Style {
|
||||
Style::Option(self.map(Oco::from))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for Option<Oco<'static, str>> {
|
||||
#[inline(always)]
|
||||
fn into_style(self) -> Style {
|
||||
Style::Option(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +130,7 @@ impl Style {
|
||||
pub fn as_value_string(
|
||||
&self,
|
||||
style_name: &'static str,
|
||||
) -> Option<Cow<'static, str>> {
|
||||
) -> Option<Oco<'static, str>> {
|
||||
match self {
|
||||
Style::Value(value) => {
|
||||
Some(format!("{style_name}: {value};").into())
|
||||
@@ -107,10 +154,11 @@ impl Style {
|
||||
#[inline(never)]
|
||||
pub fn style_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
name: Oco<'static, str>,
|
||||
value: Style,
|
||||
) {
|
||||
use leptos_reactive::create_render_effect;
|
||||
use std::ops::Deref;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
let el = el.unchecked_ref::<web_sys::HtmlElement>();
|
||||
@@ -128,16 +176,16 @@ pub fn style_helper(
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if old.as_ref() != Some(&new) {
|
||||
style_expression(&style_list, &name, new.as_ref(), true)
|
||||
style_expression(&style_list, &name, new.as_deref(), true)
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
Style::Value(value) => {
|
||||
style_expression(&style_list, &name, Some(&value), false)
|
||||
style_expression(&style_list, &name, Some(value.deref()), false)
|
||||
}
|
||||
Style::Option(value) => {
|
||||
style_expression(&style_list, &name, value.as_ref(), false)
|
||||
style_expression(&style_list, &name, value.as_deref(), false)
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -147,7 +195,7 @@ pub fn style_helper(
|
||||
pub(crate) fn style_expression(
|
||||
style_list: &web_sys::CssStyleDeclaration,
|
||||
style_name: &str,
|
||||
value: Option<&Cow<'static, str>>,
|
||||
value: Option<&str>,
|
||||
force: bool,
|
||||
) {
|
||||
use crate::HydrationCtx;
|
||||
@@ -156,7 +204,7 @@ pub(crate) fn style_expression(
|
||||
let style_name = wasm_bindgen::intern(style_name);
|
||||
|
||||
if let Some(value) = value {
|
||||
if let Err(e) = style_list.set_property(style_name, &value) {
|
||||
if let Err(e) = style_list.set_property(style_name, value) {
|
||||
crate::error!("[HtmlElement::style()] {e:?}");
|
||||
}
|
||||
} else {
|
||||
@@ -183,6 +231,37 @@ macro_rules! style_type {
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! style_signal_type {
|
||||
($signal_type:ty) => {
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T> IntoStyle for $signal_type
|
||||
where
|
||||
T: IntoStyle + Clone,
|
||||
{
|
||||
fn into_style(self) -> Style {
|
||||
let modified_fn = Rc::new(move || self.get().into_style());
|
||||
Style::Fn(modified_fn)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! style_signal_type_optional {
|
||||
($signal_type:ty) => {
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T> IntoStyle for $signal_type
|
||||
where
|
||||
T: Clone,
|
||||
Option<T>: IntoStyle,
|
||||
{
|
||||
fn into_style(self) -> Style {
|
||||
let modified_fn = Rc::new(move || self.get().into_style());
|
||||
Style::Fn(modified_fn)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
style_type!(&String);
|
||||
style_type!(usize);
|
||||
style_type!(u8);
|
||||
@@ -199,3 +278,10 @@ style_type!(i128);
|
||||
style_type!(f32);
|
||||
style_type!(f64);
|
||||
style_type!(char);
|
||||
|
||||
style_signal_type!(ReadSignal<T>);
|
||||
style_signal_type!(RwSignal<T>);
|
||||
style_signal_type!(Memo<T>);
|
||||
style_signal_type!(Signal<T>);
|
||||
style_signal_type!(MaybeSignal<T>);
|
||||
style_signal_type_optional!(MaybeProp<T>);
|
||||
|
||||
@@ -5,7 +5,7 @@ use wasm_bindgen::UnwrapThrowExt;
|
||||
macro_rules! tracing_props {
|
||||
() => {
|
||||
::leptos::leptos_dom::tracing::span!(
|
||||
::leptos::leptos_dom::tracing::Level::DEBUG,
|
||||
::leptos::leptos_dom::tracing::Level::TRACE,
|
||||
"leptos_dom::tracing_props",
|
||||
props = String::from("[]")
|
||||
);
|
||||
@@ -24,7 +24,7 @@ macro_rules! tracing_props {
|
||||
props.pop();
|
||||
props.push(']');
|
||||
::leptos::leptos_dom::tracing::span!(
|
||||
::leptos::leptos_dom::tracing::Level::DEBUG,
|
||||
::leptos::leptos_dom::tracing::Level::TRACE,
|
||||
"leptos_dom::tracing_props",
|
||||
props
|
||||
);
|
||||
@@ -48,10 +48,21 @@ impl<T: serde::Serialize> SerializeMatch for &Match<&T> {
|
||||
type Return = String;
|
||||
fn spez(&self) -> Self::Return {
|
||||
let name = self.name;
|
||||
serde_json::to_string(self.value.get().unwrap_throw()).map_or_else(
|
||||
|err| format!(r#"{{"name": "{name}", "error": "{err}"}}"#),
|
||||
|value| format!(r#"{{"name": "{name}", "value": {value}}}"#),
|
||||
)
|
||||
|
||||
// suppresses warnings when serializing signals into props
|
||||
#[cfg(debug_assertions)]
|
||||
let prev = leptos_reactive::SpecialNonReactiveZone::enter();
|
||||
|
||||
let value = serde_json::to_string(self.value.get().unwrap_throw())
|
||||
.map_or_else(
|
||||
|err| format!(r#"{{"name": "{name}", "error": "{err}"}}"#),
|
||||
|value| format!(r#"{{"name": "{name}", "value": {value}}}"#),
|
||||
);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
leptos_reactive::SpecialNonReactiveZone::exit(prev);
|
||||
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,9 +74,7 @@ impl<T> DefaultMatch for Match<&T> {
|
||||
type Return = String;
|
||||
fn spez(&self) -> Self::Return {
|
||||
let name = self.name;
|
||||
format!(
|
||||
r#"{{"name": "{name}", "error": "The trait `serde::Serialize` is not implemented"}}"#
|
||||
)
|
||||
format!(r#"{{"name": "{name}", "value": "[unserializable value]"}}"#)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +156,8 @@ fn match_serialize() {
|
||||
|
||||
#[test]
|
||||
fn match_no_serialize() {
|
||||
#![allow(clippy::needless_borrow)]
|
||||
|
||||
struct CustomStruct {
|
||||
field: &'static str,
|
||||
}
|
||||
@@ -159,7 +170,7 @@ fn match_no_serialize() {
|
||||
.spez();
|
||||
assert_eq!(
|
||||
prop,
|
||||
r#"{"name": "test", "error": "The trait `serde::Serialize` is not implemented"}"#
|
||||
r#"{"name": "test", "value": "[unserializable value]"}"#
|
||||
);
|
||||
// Verification of ownership
|
||||
assert_eq!(test.field, "field");
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use super::{ElementDescriptor, HtmlElement};
|
||||
use crate::HydrationCtx;
|
||||
use cfg_if::cfg_if;
|
||||
use std::borrow::Cow;
|
||||
use leptos_reactive::Oco;
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
use once_cell::unsync::Lazy as LazyCell;
|
||||
@@ -145,7 +145,7 @@ macro_rules! generate_math_tags {
|
||||
}
|
||||
|
||||
impl ElementDescriptor for [<$tag:camel $($second:camel $($third:camel)?)?>] {
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
fn name(&self) -> Oco<'static, str> {
|
||||
stringify!($tag).into()
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ use crate::{
|
||||
use cfg_if::cfg_if;
|
||||
use futures::{stream::FuturesUnordered, Future, Stream, StreamExt};
|
||||
use itertools::Itertools;
|
||||
use leptos_reactive::*;
|
||||
use std::{borrow::Cow, pin::Pin};
|
||||
use leptos_reactive::{Oco, *};
|
||||
use std::pin::Pin;
|
||||
|
||||
type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
|
||||
|
||||
@@ -30,7 +30,7 @@ type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", skip_all,)
|
||||
)]
|
||||
pub fn render_to_string<F, N>(f: F) -> String
|
||||
pub fn render_to_string<F, N>(f: F) -> Oco<'static, str>
|
||||
where
|
||||
F: FnOnce() -> N + 'static,
|
||||
N: IntoView,
|
||||
@@ -42,7 +42,7 @@ where
|
||||
|
||||
runtime.dispose();
|
||||
|
||||
html.into()
|
||||
html
|
||||
}
|
||||
|
||||
/// Renders a function to a stream of HTML strings.
|
||||
@@ -87,7 +87,7 @@ pub fn render_to_stream(
|
||||
)]
|
||||
pub fn render_to_stream_with_prefix(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
prefix: impl FnOnce() -> Cow<'static, str> + 'static,
|
||||
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
|
||||
) -> impl Stream<Item = String> {
|
||||
let (stream, runtime) =
|
||||
render_to_stream_with_prefix_undisposed(view, prefix);
|
||||
@@ -116,7 +116,7 @@ pub fn render_to_stream_with_prefix(
|
||||
)]
|
||||
pub fn render_to_stream_with_prefix_undisposed(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
prefix: impl FnOnce() -> Cow<'static, str> + 'static,
|
||||
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
|
||||
) -> (impl Stream<Item = String>, RuntimeId) {
|
||||
render_to_stream_with_prefix_undisposed_with_context(view, prefix, || {})
|
||||
}
|
||||
@@ -142,7 +142,7 @@ pub fn render_to_stream_with_prefix_undisposed(
|
||||
)]
|
||||
pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
prefix: impl FnOnce() -> Cow<'static, str> + 'static,
|
||||
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
|
||||
additional_context: impl FnOnce() + 'static,
|
||||
) -> (impl Stream<Item = String>, RuntimeId) {
|
||||
render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
|
||||
@@ -179,7 +179,7 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
)]
|
||||
pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
prefix: impl FnOnce() -> Cow<'static, str> + 'static,
|
||||
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
|
||||
additional_context: impl FnOnce() + 'static,
|
||||
replace_blocks: bool,
|
||||
) -> (impl Stream<Item = String>, RuntimeId) {
|
||||
@@ -363,7 +363,7 @@ impl View {
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "info", skip_all,)
|
||||
)]
|
||||
pub fn render_to_string(self) -> Cow<'static, str> {
|
||||
pub fn render_to_string(self) -> Oco<'static, str> {
|
||||
#[cfg(all(feature = "web", feature = "ssr"))]
|
||||
crate::console_error(
|
||||
"\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \
|
||||
@@ -381,7 +381,7 @@ impl View {
|
||||
pub(crate) fn render_to_string_helper(
|
||||
self,
|
||||
dont_escape_text: bool,
|
||||
) -> Cow<'static, str> {
|
||||
) -> Oco<'static, str> {
|
||||
match self {
|
||||
View::Text(node) => {
|
||||
if dont_escape_text {
|
||||
@@ -450,7 +450,7 @@ impl View {
|
||||
)
|
||||
.into()
|
||||
})
|
||||
as Box<dyn FnOnce() -> Cow<'static, str>>,
|
||||
as Box<dyn FnOnce() -> Oco<'static, str>>,
|
||||
),
|
||||
CoreComponent::DynChild(node) => {
|
||||
let child = node.child.take();
|
||||
@@ -500,7 +500,7 @@ impl View {
|
||||
"".into()
|
||||
}
|
||||
})
|
||||
as Box<dyn FnOnce() -> Cow<'static, str>>,
|
||||
as Box<dyn FnOnce() -> Oco<'static, str>>,
|
||||
)
|
||||
}
|
||||
CoreComponent::Each(node) => {
|
||||
@@ -554,7 +554,7 @@ impl View {
|
||||
.join("")
|
||||
.into()
|
||||
})
|
||||
as Box<dyn FnOnce() -> Cow<'static, str>>,
|
||||
as Box<dyn FnOnce() -> Oco<'static, str>>,
|
||||
)
|
||||
}
|
||||
};
|
||||
@@ -598,15 +598,15 @@ impl View {
|
||||
.join("")
|
||||
.into()
|
||||
} else {
|
||||
let tag_name = el.name;
|
||||
let tag_name: Oco<'_, str> = el.name;
|
||||
|
||||
let mut inner_html = None;
|
||||
let mut inner_html: Option<Oco<'_, str>> = None;
|
||||
|
||||
let attrs = el
|
||||
.attrs
|
||||
.into_iter()
|
||||
.filter_map(
|
||||
|(name, value)| -> Option<Cow<'static, str>> {
|
||||
|(name, value)| -> Option<Oco<'static, str>> {
|
||||
if value.is_empty() {
|
||||
Some(format!(" {name}").into())
|
||||
} else if name == "inner_html" {
|
||||
@@ -615,9 +615,9 @@ impl View {
|
||||
} else {
|
||||
Some(
|
||||
format!(
|
||||
" {name}=\"{}\"",
|
||||
html_escape::encode_double_quoted_attribute(&value)
|
||||
)
|
||||
" {name}=\"{}\"",
|
||||
html_escape::encode_double_quoted_attribute(&value)
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
@@ -729,9 +729,9 @@ pub(crate) fn render_serializers(
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn escape_attr<T>(value: &T) -> Cow<'_, str>
|
||||
pub fn escape_attr<T>(value: &T) -> Oco<'_, str>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
html_escape::encode_double_quoted_attribute(value)
|
||||
html_escape::encode_double_quoted_attribute(value).into()
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use cfg_if::cfg_if;
|
||||
use futures::{channel::mpsc::UnboundedSender, Stream, StreamExt};
|
||||
use itertools::Itertools;
|
||||
use leptos_reactive::{
|
||||
create_runtime, suspense::StreamChunk, RuntimeId, SharedContext,
|
||||
create_runtime, suspense::StreamChunk, Oco, RuntimeId, SharedContext,
|
||||
};
|
||||
use std::{borrow::Cow, collections::VecDeque};
|
||||
|
||||
@@ -23,10 +23,17 @@ pub async fn render_to_string_async(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
) -> String {
|
||||
let mut buf = String::new();
|
||||
let mut stream = Box::pin(render_to_stream_in_order(view));
|
||||
let (stream, runtime) =
|
||||
render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
view,
|
||||
|| "".into(),
|
||||
|| {},
|
||||
);
|
||||
let mut stream = Box::pin(stream);
|
||||
while let Some(chunk) = stream.next().await {
|
||||
buf.push_str(&chunk);
|
||||
}
|
||||
runtime.dispose();
|
||||
buf
|
||||
}
|
||||
|
||||
@@ -52,7 +59,7 @@ pub fn render_to_stream_in_order(
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
pub fn render_to_stream_in_order_with_prefix(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
prefix: impl FnOnce() -> Cow<'static, str> + 'static,
|
||||
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
|
||||
) -> impl Stream<Item = String> {
|
||||
#[cfg(all(feature = "web", feature = "ssr"))]
|
||||
crate::console_error(
|
||||
@@ -82,7 +89,7 @@ pub fn render_to_stream_in_order_with_prefix(
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
prefix: impl FnOnce() -> Cow<'static, str> + 'static,
|
||||
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
|
||||
additional_context: impl FnOnce() + 'static,
|
||||
) -> (impl Stream<Item = String>, RuntimeId) {
|
||||
HydrationCtx::reset_id();
|
||||
@@ -280,12 +287,11 @@ impl View {
|
||||
StringOrView::String(string) => {
|
||||
chunks.push_back(StreamChunk::Sync(string))
|
||||
}
|
||||
StringOrView::View(view) => {
|
||||
view().into_stream_chunks_helper(
|
||||
StringOrView::View(view) => view()
|
||||
.into_stream_chunks_helper(
|
||||
chunks,
|
||||
is_script_or_style,
|
||||
);
|
||||
}
|
||||
),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -306,9 +312,9 @@ impl View {
|
||||
} else {
|
||||
Some(
|
||||
format!(
|
||||
" {name}=\"{}\"",
|
||||
html_escape::encode_double_quoted_attribute(&value)
|
||||
)
|
||||
" {name}=\"{}\"",
|
||||
html_escape::encode_double_quoted_attribute(&value)
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
@@ -343,7 +349,7 @@ impl View {
|
||||
}
|
||||
}
|
||||
ElementChildren::InnerHtml(inner_html) => {
|
||||
chunks.push_back(StreamChunk::Sync(inner_html));
|
||||
chunks.push_back(StreamChunk::Sync(inner_html))
|
||||
}
|
||||
// handled above
|
||||
ElementChildren::Chunks(_) => unreachable!(),
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
use super::{html::HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG, HydrationKey};
|
||||
use super::{ElementDescriptor, HtmlElement};
|
||||
use crate::HydrationCtx;
|
||||
use leptos_reactive::Oco;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use once_cell::unsync::Lazy as LazyCell;
|
||||
use std::borrow::Cow;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
@@ -142,7 +142,7 @@ macro_rules! generate_svg_tags {
|
||||
}
|
||||
|
||||
impl ElementDescriptor for [<$tag:camel $($second:camel $($third:camel)?)?>] {
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
fn name(&self) -> Oco<'static, str> {
|
||||
stringify!($tag).into()
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ tracing = "0.1.37"
|
||||
|
||||
[dev-dependencies]
|
||||
log = "0.4"
|
||||
typed-builder = "0.14"
|
||||
typed-builder = "0.16"
|
||||
trybuild = "1"
|
||||
leptos = { path = "../leptos" }
|
||||
insta = "1.29"
|
||||
|
||||
@@ -252,8 +252,9 @@ impl ToTokens for Model {
|
||||
#[doc = ""]
|
||||
#docs
|
||||
#component_fn_prop_docs
|
||||
#[derive(::leptos::typed_builder::TypedBuilder)]
|
||||
#[builder(doc)]
|
||||
#[derive(::leptos::typed_builder_macro::TypedBuilder)]
|
||||
//#[builder(doc)]
|
||||
#[builder(crate_module_path=::leptos::typed_builder)]
|
||||
#vis struct #props_name #impl_generics #where_clause {
|
||||
#prop_builder_fields
|
||||
}
|
||||
@@ -554,7 +555,11 @@ impl ToTokens for TypedBuilderOpts {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let output = quote! { #[builder(#default #setter)] };
|
||||
let output = if !default.is_empty() || !setter.is_empty() {
|
||||
quote! { #[builder(#default #setter)] }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
tokens.append_all(output);
|
||||
}
|
||||
|
||||
@@ -818,6 +818,10 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// - Your server must be ready to handle the server functions at the API prefix you list. The easiest way to do this
|
||||
/// is to use the `handle_server_fns` function from [`leptos_actix`](https://docs.rs/leptos_actix/latest/leptos_actix/fn.handle_server_fns.html)
|
||||
/// or [`leptos_axum`](https://docs.rs/leptos_axum/latest/leptos_axum/fn.handle_server_fns.html).
|
||||
/// - **Server functions must have unique paths**. Unique paths are automatically generated for each
|
||||
/// server function. If you choose to specify a path in the fourth argument, you must ensure that these
|
||||
/// are unique. You cannot define two server functions with the same URL prefix and endpoint path,
|
||||
/// even if they have different URL encodings, e.g. a POST method at `/api/foo` and a GET method at `/api/foo`.
|
||||
///
|
||||
/// ## Server Function Encodings
|
||||
///
|
||||
|
||||
@@ -19,7 +19,7 @@ pub fn impl_params(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
|
||||
let span = field.span();
|
||||
|
||||
quote_spanned! {
|
||||
span => #ident: <#ty>::into_param(map.get(#field_name_string).map(|n| n.as_str()), #field_name_string)?
|
||||
span => #ident: <#ty>::into_param(map.get(#field_name_string).map(::std::string::String::as_str), #field_name_string)?
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -36,12 +36,12 @@ pub fn server_impl(
|
||||
};
|
||||
// default to PascalCase version of function name if no struct name given
|
||||
if args.struct_name.is_none() {
|
||||
let upper_cammel_case_name = Converter::new()
|
||||
let upper_camel_case_name = Converter::new()
|
||||
.from_case(Case::Snake)
|
||||
.to_case(Case::UpperCamel)
|
||||
.convert(sig.ident.to_string());
|
||||
args.struct_name =
|
||||
Some(Ident::new(&upper_cammel_case_name, sig.ident.span()));
|
||||
Some(Ident::new(&upper_camel_case_name, sig.ident.span()));
|
||||
}
|
||||
// default to "/api" if no prefix given
|
||||
if args.prefix.is_none() {
|
||||
@@ -76,7 +76,7 @@ impl ToTokens for ServerFnArgs {
|
||||
self.struct_name.as_ref().map(|s| quote::quote! { #s, });
|
||||
let prefix = self.prefix.as_ref().map(|p| quote::quote! { #p, });
|
||||
let encoding = self.encoding.as_ref().map(|e| quote::quote! { #e, });
|
||||
let fn_path = self.fn_path.as_ref().map(|f| quote::quote! { #f, });
|
||||
let fn_path = self.fn_path.as_ref().map(|f| quote::quote! { #f });
|
||||
tokens.extend(quote::quote! {
|
||||
#struct_name
|
||||
#prefix
|
||||
|
||||
@@ -79,8 +79,8 @@ impl ToTokens for Model {
|
||||
#[doc = ""]
|
||||
#docs
|
||||
#prop_docs
|
||||
#[derive(::leptos::typed_builder::TypedBuilder)]
|
||||
#[builder(doc)]
|
||||
#[derive(::leptos::typed_builder_macro::TypedBuilder)]
|
||||
#[builder(doc, crate_module_path=::leptos::typed_builder)]
|
||||
#vis struct #name #generics #where_clause {
|
||||
#prop_builder_fields
|
||||
}
|
||||
@@ -191,7 +191,11 @@ impl ToTokens for TypedBuilderOpts {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let output = quote! { #[builder(#default #setter)] };
|
||||
let output = if !default.is_empty() || !setter.is_empty() {
|
||||
quote! { #[builder(#default #setter)] }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
tokens.append_all(output);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#[cfg(debug_assertions)]
|
||||
use super::ident_from_tag_name;
|
||||
use super::{
|
||||
client_builder::{fragment_to_tokens, TagType},
|
||||
event_from_attribute_node, ident_from_tag_name,
|
||||
event_from_attribute_node,
|
||||
};
|
||||
use proc_macro2::{Ident, TokenStream, TokenTree};
|
||||
use quote::{format_ident, quote};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{with_runtime, Runtime};
|
||||
use crate::{node::NodeId, with_runtime, Disposer, Runtime, SignalDispose};
|
||||
use cfg_if::cfg_if;
|
||||
use std::{any::Any, cell::RefCell, marker::PhantomData, rc::Rc};
|
||||
|
||||
@@ -57,21 +57,23 @@ use std::{any::Any, cell::RefCell, marker::PhantomData, rc::Rc};
|
||||
)]
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn create_effect<T>(f: impl Fn(Option<T>) -> T + 'static)
|
||||
pub fn create_effect<T>(f: impl Fn(Option<T>) -> T + 'static) -> Effect
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
cfg_if! {
|
||||
if #[cfg(not(feature = "ssr"))] {
|
||||
let runtime = Runtime::current();
|
||||
let e = runtime.create_effect(f);
|
||||
let id = runtime.create_effect(f);
|
||||
//crate::macros::debug_warn!("creating effect {e:?}");
|
||||
_ = with_runtime( |runtime| {
|
||||
runtime.update_if_necessary(e);
|
||||
runtime.update_if_necessary(id);
|
||||
});
|
||||
Effect { id }
|
||||
} else {
|
||||
// clear warnings
|
||||
_ = f;
|
||||
Effect::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,16 +116,19 @@ where
|
||||
)]
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn create_isomorphic_effect<T>(f: impl Fn(Option<T>) -> T + 'static)
|
||||
pub fn create_isomorphic_effect<T>(
|
||||
f: impl Fn(Option<T>) -> T + 'static,
|
||||
) -> Effect
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
let runtime = Runtime::current();
|
||||
let e = runtime.create_effect(f);
|
||||
let id = runtime.create_effect(f);
|
||||
//crate::macros::debug_warn!("creating effect {e:?}");
|
||||
_ = with_runtime(|runtime| {
|
||||
runtime.update_if_necessary(e);
|
||||
runtime.update_if_necessary(id);
|
||||
});
|
||||
Effect { id }
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
@@ -145,7 +150,25 @@ where
|
||||
create_effect(f);
|
||||
}
|
||||
|
||||
pub(crate) struct Effect<T, F>
|
||||
/// A handle to an effect, can be used to explicitly dispose of the effect.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub struct Effect {
|
||||
pub(crate) id: NodeId,
|
||||
}
|
||||
|
||||
impl From<Effect> for Disposer {
|
||||
fn from(effect: Effect) -> Self {
|
||||
Disposer(effect.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl SignalDispose for Effect {
|
||||
fn dispose(self) {
|
||||
drop(Disposer::from(self));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct EffectState<T, F>
|
||||
where
|
||||
T: 'static,
|
||||
F: Fn(Option<T>) -> T,
|
||||
@@ -160,7 +183,7 @@ pub(crate) trait AnyComputation {
|
||||
fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool;
|
||||
}
|
||||
|
||||
impl<T, F> AnyComputation for Effect<T, F>
|
||||
impl<T, F> AnyComputation for EffectState<T, F>
|
||||
where
|
||||
T: 'static,
|
||||
F: Fn(Option<T>) -> T,
|
||||
|
||||
@@ -9,6 +9,8 @@ use std::collections::{HashMap, HashSet, VecDeque};
|
||||
/// Hydration data and other context that is shared between the server
|
||||
/// and the client.
|
||||
pub struct SharedContext {
|
||||
/// Resources that initially needed to resolve from the server.
|
||||
pub server_resources: HashSet<ResourceId>,
|
||||
/// Resources that have not yet resolved.
|
||||
pub pending_resources: HashSet<ResourceId>,
|
||||
/// Resources that have already resolved.
|
||||
@@ -201,24 +203,27 @@ impl Default for SharedContext {
|
||||
let pending_resources: HashSet<ResourceId> = pending_resources
|
||||
.map_err(|_| ())
|
||||
.and_then(|pr| serde_wasm_bindgen::from_value(pr).map_err(|_| ()))
|
||||
.unwrap_or_default();
|
||||
.unwrap();
|
||||
|
||||
let resolved_resources = js_sys::Reflect::get(
|
||||
&web_sys::window().unwrap(),
|
||||
&wasm_bindgen::JsValue::from_str("__LEPTOS_RESOLVED_RESOURCES"),
|
||||
)
|
||||
.unwrap_or(wasm_bindgen::JsValue::NULL);
|
||||
.unwrap(); // unwrap_or(wasm_bindgen::JsValue::NULL);
|
||||
|
||||
let resolved_resources =
|
||||
serde_wasm_bindgen::from_value(resolved_resources).unwrap_or_default();
|
||||
serde_wasm_bindgen::from_value(resolved_resources).unwrap();
|
||||
|
||||
|
||||
Self {
|
||||
server_resources: pending_resources.clone(),
|
||||
pending_resources,
|
||||
resolved_resources,
|
||||
pending_fragments: Default::default(),
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
server_resources: Default::default(),
|
||||
pending_resources: Default::default(),
|
||||
resolved_resources: Default::default(),
|
||||
pending_fragments: Default::default(),
|
||||
|
||||
@@ -85,6 +85,7 @@ mod effect;
|
||||
mod hydration;
|
||||
mod memo;
|
||||
mod node;
|
||||
pub mod oco;
|
||||
mod resource;
|
||||
mod runtime;
|
||||
mod selector;
|
||||
@@ -107,6 +108,7 @@ pub use effect::*;
|
||||
pub use hydration::{FragmentData, SharedContext};
|
||||
pub use memo::*;
|
||||
pub use node::Disposer;
|
||||
pub use oco::*;
|
||||
pub use resource::*;
|
||||
use runtime::*;
|
||||
pub use runtime::{
|
||||
|
||||
@@ -206,7 +206,9 @@ fn forward_ref_to<T, O, F: FnOnce(&T) -> O>(
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> SignalGetUntracked<T> for Memo<T> {
|
||||
impl<T: Clone> SignalGetUntracked for Memo<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -257,7 +259,9 @@ impl<T: Clone> SignalGetUntracked<T> for Memo<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SignalWithUntracked<T> for Memo<T> {
|
||||
impl<T> SignalWithUntracked for Memo<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -324,7 +328,9 @@ impl<T> SignalWithUntracked<T> for Memo<T> {
|
||||
/// # runtime.dispose();
|
||||
/// #
|
||||
/// ```
|
||||
impl<T: Clone> SignalGet<T> for Memo<T> {
|
||||
impl<T: Clone> SignalGet for Memo<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -364,7 +370,9 @@ impl<T: Clone> SignalGet<T> for Memo<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SignalWith<T> for Memo<T> {
|
||||
impl<T> SignalWith for Memo<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
|
||||
681
leptos_reactive/src/oco.rs
Normal file
681
leptos_reactive/src/oco.rs
Normal file
@@ -0,0 +1,681 @@
|
||||
//! This module contains the `Oco` (Owned Clones Once) smart pointer,
|
||||
//! which is used to store immutable references to values.
|
||||
//! This is useful for storing, for example, strings.
|
||||
|
||||
use std::{
|
||||
borrow::{Borrow, Cow},
|
||||
ffi::{CStr, OsStr},
|
||||
fmt,
|
||||
hash::Hash,
|
||||
ops::{Add, Deref},
|
||||
path::Path,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
/// "Owned Clones Once" - a smart pointer that can be either a reference,
|
||||
/// an owned value, or a reference counted pointer. This is useful for
|
||||
/// storing immutable values, such as strings, in a way that is cheap to
|
||||
/// clone and pass around.
|
||||
///
|
||||
/// The `Clone` implementation is amortized `O(1)`. Cloning the [`Oco::Borrowed`]
|
||||
/// variant simply copies the references (`O(1)`). Cloning the [`Oco::Counted`]
|
||||
/// variant increments a reference count (`O(1)`). Cloning the [`Oco::Owned`]
|
||||
/// variant upgrades it to [`Oco::Counted`], which requires an `O(n)` clone of the
|
||||
/// data, but all subsequent clones will be `O(1)`.
|
||||
pub enum Oco<'a, T: ?Sized + ToOwned + 'a> {
|
||||
/// A static reference to a value.
|
||||
Borrowed(&'a T),
|
||||
/// A reference counted pointer to a value.
|
||||
Counted(Rc<T>),
|
||||
/// An owned value.
|
||||
Owned(<T as ToOwned>::Owned),
|
||||
}
|
||||
|
||||
impl<T: ?Sized + ToOwned> Oco<'_, T> {
|
||||
/// Converts the value into an owned value.
|
||||
pub fn into_owned(self) -> <T as ToOwned>::Owned {
|
||||
match self {
|
||||
Oco::Borrowed(v) => v.to_owned(),
|
||||
Oco::Counted(v) => v.as_ref().to_owned(),
|
||||
Oco::Owned(v) => v,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the value is [`Oco::Borrowed`].
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use std::rc::Rc;
|
||||
/// # use leptos_reactive::oco::Oco;
|
||||
/// assert!(Oco::<str>::Borrowed("Hello").is_borrowed());
|
||||
/// assert!(!Oco::<str>::Counted(Rc::from("Hello")).is_borrowed());
|
||||
/// assert!(!Oco::<str>::Owned("Hello".to_string()).is_borrowed());
|
||||
/// ```
|
||||
pub const fn is_borrowed(&self) -> bool {
|
||||
matches!(self, Oco::Borrowed(_))
|
||||
}
|
||||
|
||||
/// Checks if the value is [`Oco::Counted`].
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use std::rc::Rc;
|
||||
/// # use leptos_reactive::oco::Oco;
|
||||
/// assert!(Oco::<str>::Counted(Rc::from("Hello")).is_counted());
|
||||
/// assert!(!Oco::<str>::Borrowed("Hello").is_counted());
|
||||
/// assert!(!Oco::<str>::Owned("Hello".to_string()).is_counted());
|
||||
/// ```
|
||||
pub const fn is_counted(&self) -> bool {
|
||||
matches!(self, Oco::Counted(_))
|
||||
}
|
||||
|
||||
/// Checks if the value is [`Oco::Owned`].
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use std::rc::Rc;
|
||||
/// # use leptos_reactive::oco::Oco;
|
||||
/// assert!(Oco::<str>::Owned("Hello".to_string()).is_owned());
|
||||
/// assert!(!Oco::<str>::Borrowed("Hello").is_owned());
|
||||
/// assert!(!Oco::<str>::Counted(Rc::from("Hello")).is_owned());
|
||||
/// ```
|
||||
pub const fn is_owned(&self) -> bool {
|
||||
matches!(self, Oco::Owned(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized + ToOwned> Deref for Oco<'_, T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &T {
|
||||
match self {
|
||||
Oco::Borrowed(v) => v,
|
||||
Oco::Owned(v) => v.borrow(),
|
||||
Oco::Counted(v) => v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized + ToOwned> Borrow<T> for Oco<'_, T> {
|
||||
#[inline(always)]
|
||||
fn borrow(&self) -> &T {
|
||||
self.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized + ToOwned> AsRef<T> for Oco<'_, T> {
|
||||
#[inline(always)]
|
||||
fn as_ref(&self) -> &T {
|
||||
self.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for Oco<'_, str> {
|
||||
#[inline(always)]
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.as_str().as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for Oco<'_, OsStr> {
|
||||
#[inline(always)]
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.as_os_str().as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------
|
||||
// pub fn as_{slice}(&self) -> &{slice}
|
||||
// --------------------------------------
|
||||
|
||||
impl Oco<'_, str> {
|
||||
/// Returns a `&str` slice of this [`Oco`].
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use leptos_reactive::oco::Oco;
|
||||
/// let oco = Oco::<str>::Borrowed("Hello");
|
||||
/// let s: &str = oco.as_str();
|
||||
/// assert_eq!(s, "Hello");
|
||||
/// ```
|
||||
#[inline(always)]
|
||||
pub fn as_str(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Oco<'_, CStr> {
|
||||
/// Returns a `&CStr` slice of this [`Oco`].
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use leptos_reactive::oco::Oco;
|
||||
/// use std::ffi::CStr;
|
||||
///
|
||||
/// let oco =
|
||||
/// Oco::<CStr>::Borrowed(CStr::from_bytes_with_nul(b"Hello\0").unwrap());
|
||||
/// let s: &CStr = oco.as_c_str();
|
||||
/// assert_eq!(s, CStr::from_bytes_with_nul(b"Hello\0").unwrap());
|
||||
/// ```
|
||||
#[inline(always)]
|
||||
pub fn as_c_str(&self) -> &CStr {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Oco<'_, OsStr> {
|
||||
/// Returns a `&OsStr` slice of this [`Oco`].
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use leptos_reactive::oco::Oco;
|
||||
/// use std::ffi::OsStr;
|
||||
///
|
||||
/// let oco = Oco::<OsStr>::Borrowed(OsStr::new("Hello"));
|
||||
/// let s: &OsStr = oco.as_os_str();
|
||||
/// assert_eq!(s, OsStr::new("Hello"));
|
||||
/// ```
|
||||
#[inline(always)]
|
||||
pub fn as_os_str(&self) -> &OsStr {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Oco<'_, Path> {
|
||||
/// Returns a `&Path` slice of this [`Oco`].
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use leptos_reactive::oco::Oco;
|
||||
/// use std::path::Path;
|
||||
///
|
||||
/// let oco = Oco::<Path>::Borrowed(Path::new("Hello"));
|
||||
/// let s: &Path = oco.as_path();
|
||||
/// assert_eq!(s, Path::new("Hello"));
|
||||
/// ```
|
||||
#[inline(always)]
|
||||
pub fn as_path(&self) -> &Path {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Oco<'_, [T]>
|
||||
where
|
||||
[T]: ToOwned,
|
||||
{
|
||||
/// Returns a `&[T]` slice of this [`Oco`].
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use leptos_reactive::oco::Oco;
|
||||
/// let oco = Oco::<[u8]>::Borrowed(b"Hello");
|
||||
/// let s: &[u8] = oco.as_slice();
|
||||
/// assert_eq!(s, b"Hello");
|
||||
/// ```
|
||||
#[inline(always)]
|
||||
pub fn as_slice(&self) -> &[T] {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------
|
||||
// Cloning (has to be implemented manually because of the `Rc<T>: From<&<T as ToOwned>::Owned>` bound)
|
||||
// ------------------------------------------------------------------------------------------------------
|
||||
|
||||
impl Clone for Oco<'_, str> {
|
||||
/// Returns a new [`Oco`] with the same value as this one.
|
||||
/// If the value is [`Oco::Owned`], this will convert it into
|
||||
/// [`Oco::Counted`], so that the next clone will be O(1).
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use leptos_reactive::oco::Oco;
|
||||
/// let oco = Oco::<str>::Owned("Hello".to_string());
|
||||
/// let oco2 = oco.clone();
|
||||
/// assert_eq!(oco, oco2);
|
||||
/// assert!(oco2.is_counted());
|
||||
/// ```
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Oco::Borrowed(v) => Oco::Borrowed(v),
|
||||
Oco::Counted(v) => Oco::Counted(v.clone()),
|
||||
Oco::Owned(v) => Oco::Counted(Rc::<str>::from(v.as_str())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Oco<'_, CStr> {
|
||||
/// Returns a new [`Oco`] with the same value as this one.
|
||||
/// If the value is [`Oco::Owned`], this will convert it into
|
||||
/// [`Oco::Counted`], so that the next clone will be O(1).
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use leptos_reactive::oco::Oco;
|
||||
/// use std::ffi::CStr;
|
||||
///
|
||||
/// let oco = Oco::<CStr>::Owned(
|
||||
/// CStr::from_bytes_with_nul(b"Hello\0").unwrap().to_owned(),
|
||||
/// );
|
||||
/// let oco2 = oco.clone();
|
||||
/// assert_eq!(oco, oco2);
|
||||
/// assert!(oco2.is_counted());
|
||||
/// ```
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Oco::Borrowed(v) => Oco::Borrowed(v),
|
||||
Oco::Counted(v) => Oco::Counted(v.clone()),
|
||||
Oco::Owned(v) => Oco::Counted(Rc::<CStr>::from(v.as_c_str())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Oco<'_, OsStr> {
|
||||
/// Returns a new [`Oco`] with the same value as this one.
|
||||
/// If the value is [`Oco::Owned`], this will convert it into
|
||||
/// [`Oco::Counted`], so that the next clone will be O(1).
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use leptos_reactive::oco::Oco;
|
||||
/// use std::ffi::OsStr;
|
||||
///
|
||||
/// let oco = Oco::<OsStr>::Owned(OsStr::new("Hello").to_owned());
|
||||
/// let oco2 = oco.clone();
|
||||
/// assert_eq!(oco, oco2);
|
||||
/// assert!(oco2.is_counted());
|
||||
/// ```
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Oco::Borrowed(v) => Oco::Borrowed(v),
|
||||
Oco::Counted(v) => Oco::Counted(v.clone()),
|
||||
Oco::Owned(v) => Oco::Counted(Rc::<OsStr>::from(v.as_os_str())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Oco<'_, Path> {
|
||||
/// Returns a new [`Oco`] with the same value as this one.
|
||||
/// If the value is [`Oco::Owned`], this will convert it into
|
||||
/// [`Oco::Counted`], so that the next clone will be O(1).
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use leptos_reactive::oco::Oco;
|
||||
/// use std::path::Path;
|
||||
///
|
||||
/// let oco = Oco::<Path>::Owned(Path::new("Hello").to_owned());
|
||||
/// let oco2 = oco.clone();
|
||||
/// assert_eq!(oco, oco2);
|
||||
/// assert!(oco2.is_counted());
|
||||
/// ```
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Oco::Borrowed(v) => Oco::Borrowed(v),
|
||||
Oco::Counted(v) => Oco::Counted(v.clone()),
|
||||
Oco::Owned(v) => Oco::Counted(Rc::<Path>::from(v.as_path())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Clone for Oco<'_, [T]>
|
||||
where
|
||||
[T]: ToOwned<Owned = Vec<T>>,
|
||||
{
|
||||
/// Returns a new [`Oco`] with the same value as this one.
|
||||
/// If the value is [`Oco::Owned`], this will convert it into
|
||||
/// [`Oco::Counted`], so that the next clone will be O(1).
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use leptos_reactive::oco::Oco;
|
||||
/// let oco = Oco::<[i32]>::Owned(vec![1, 2, 3]);
|
||||
/// let oco2 = oco.clone();
|
||||
/// assert_eq!(oco, oco2);
|
||||
/// assert!(oco2.is_counted());
|
||||
/// ```
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Oco::Borrowed(v) => Oco::Borrowed(v),
|
||||
Oco::Counted(v) => Oco::Counted(v.clone()),
|
||||
Oco::Owned(v) => Oco::Counted(Rc::<[T]>::from(v.as_slice())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Default for Oco<'_, T>
|
||||
where
|
||||
T: ToOwned,
|
||||
T::Owned: Default,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Oco::Owned(T::Owned::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, A: ?Sized, B: ?Sized> PartialEq<Oco<'b, B>> for Oco<'a, A>
|
||||
where
|
||||
A: PartialEq<B>,
|
||||
A: ToOwned,
|
||||
B: ToOwned,
|
||||
{
|
||||
fn eq(&self, other: &Oco<'b, B>) -> bool {
|
||||
**self == **other
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized + ToOwned + Eq> Eq for Oco<'_, T> {}
|
||||
|
||||
impl<'a, 'b, A: ?Sized, B: ?Sized> PartialOrd<Oco<'b, B>> for Oco<'a, A>
|
||||
where
|
||||
A: PartialOrd<B>,
|
||||
A: ToOwned,
|
||||
B: ToOwned,
|
||||
{
|
||||
fn partial_cmp(&self, other: &Oco<'b, B>) -> Option<std::cmp::Ordering> {
|
||||
(**self).partial_cmp(&**other)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized + Ord> Ord for Oco<'_, T>
|
||||
where
|
||||
T: ToOwned,
|
||||
{
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
(**self).cmp(&**other)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized + Hash> Hash for Oco<'_, T>
|
||||
where
|
||||
T: ToOwned,
|
||||
{
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
(**self).hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized + fmt::Debug> fmt::Debug for Oco<'_, T>
|
||||
where
|
||||
T: ToOwned,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
(**self).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized + fmt::Display> fmt::Display for Oco<'_, T>
|
||||
where
|
||||
T: ToOwned,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
(**self).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: ?Sized> From<&'a T> for Oco<'a, T>
|
||||
where
|
||||
T: ToOwned,
|
||||
{
|
||||
fn from(v: &'a T) -> Self {
|
||||
Oco::Borrowed(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: ?Sized> From<Cow<'a, T>> for Oco<'a, T>
|
||||
where
|
||||
T: ToOwned,
|
||||
{
|
||||
fn from(v: Cow<'a, T>) -> Self {
|
||||
match v {
|
||||
Cow::Borrowed(v) => Oco::Borrowed(v),
|
||||
Cow::Owned(v) => Oco::Owned(v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: ?Sized> From<Oco<'a, T>> for Cow<'a, T>
|
||||
where
|
||||
T: ToOwned,
|
||||
{
|
||||
fn from(value: Oco<'a, T>) -> Self {
|
||||
match value {
|
||||
Oco::Borrowed(v) => Cow::Borrowed(v),
|
||||
Oco::Owned(v) => Cow::Owned(v),
|
||||
Oco::Counted(v) => Cow::Owned(v.as_ref().to_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> From<Rc<T>> for Oco<'_, T>
|
||||
where
|
||||
T: ToOwned,
|
||||
{
|
||||
fn from(v: Rc<T>) -> Self {
|
||||
Oco::Counted(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> From<Box<T>> for Oco<'_, T>
|
||||
where
|
||||
T: ToOwned,
|
||||
{
|
||||
fn from(v: Box<T>) -> Self {
|
||||
Oco::Counted(v.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Oco<'_, str> {
|
||||
fn from(v: String) -> Self {
|
||||
Oco::Owned(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Oco<'_, str>> for String {
|
||||
fn from(v: Oco<'_, str>) -> Self {
|
||||
match v {
|
||||
Oco::Borrowed(v) => v.to_owned(),
|
||||
Oco::Counted(v) => v.as_ref().to_owned(),
|
||||
Oco::Owned(v) => v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Vec<T>> for Oco<'_, [T]>
|
||||
where
|
||||
[T]: ToOwned<Owned = Vec<T>>,
|
||||
{
|
||||
fn from(v: Vec<T>) -> Self {
|
||||
Oco::Owned(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, const N: usize> From<&'a [T; N]> for Oco<'a, [T]>
|
||||
where
|
||||
[T]: ToOwned,
|
||||
{
|
||||
fn from(v: &'a [T; N]) -> Self {
|
||||
Oco::Borrowed(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Oco<'a, str>> for Oco<'a, [u8]> {
|
||||
fn from(v: Oco<'a, str>) -> Self {
|
||||
match v {
|
||||
Oco::Borrowed(v) => Oco::Borrowed(v.as_bytes()),
|
||||
Oco::Owned(v) => Oco::Owned(v.into_bytes()),
|
||||
Oco::Counted(v) => Oco::Counted(v.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned from [`Oco::try_from`] for unsuccessful
|
||||
/// conversion from `Oco<'_, [u8]>` to `Oco<'_, str>`.
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
#[error("invalid utf-8 sequence: {_0}")]
|
||||
pub enum FromUtf8Error {
|
||||
/// Error for conversion of [`Oco::Borrowed`] and [`Oco::Counted`] variants
|
||||
/// (`&[u8]` to `&str`).
|
||||
#[error("{_0}")]
|
||||
StrFromBytes(
|
||||
#[source]
|
||||
#[from]
|
||||
std::str::Utf8Error,
|
||||
),
|
||||
/// Error for conversion of [`Oco::Owned`] variant (`Vec<u8>` to `String`).
|
||||
#[error("{_0}")]
|
||||
StringFromBytes(
|
||||
#[source]
|
||||
#[from]
|
||||
std::string::FromUtf8Error,
|
||||
),
|
||||
}
|
||||
|
||||
macro_rules! impl_slice_eq {
|
||||
([$($g:tt)*] $((where $($w:tt)+))?, $lhs:ty, $rhs: ty) => {
|
||||
impl<$($g)*> PartialEq<$rhs> for $lhs
|
||||
$(where
|
||||
$($w)*)?
|
||||
{
|
||||
#[inline]
|
||||
fn eq(&self, other: &$rhs) -> bool {
|
||||
PartialEq::eq(&self[..], &other[..])
|
||||
}
|
||||
}
|
||||
|
||||
impl<$($g)*> PartialEq<$lhs> for $rhs
|
||||
$(where
|
||||
$($w)*)?
|
||||
{
|
||||
#[inline]
|
||||
fn eq(&self, other: &$lhs) -> bool {
|
||||
PartialEq::eq(&self[..], &other[..])
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_slice_eq!([], Oco<'_, str>, str);
|
||||
impl_slice_eq!(['a, 'b], Oco<'a, str>, &'b str);
|
||||
impl_slice_eq!([], Oco<'_, str>, String);
|
||||
impl_slice_eq!(['a, 'b], Oco<'a, str>, Cow<'b, str>);
|
||||
|
||||
impl_slice_eq!([T: PartialEq] (where [T]: ToOwned), Oco<'_, [T]>, [T]);
|
||||
impl_slice_eq!(['a, 'b, T: PartialEq] (where [T]: ToOwned), Oco<'a, [T]>, &'b [T]);
|
||||
impl_slice_eq!([T: PartialEq] (where [T]: ToOwned), Oco<'_, [T]>, Vec<T>);
|
||||
impl_slice_eq!(['a, 'b, T: PartialEq] (where [T]: ToOwned), Oco<'a, [T]>, Cow<'b, [T]>);
|
||||
|
||||
impl<'a, 'b> Add<&'b str> for Oco<'a, str> {
|
||||
type Output = Oco<'static, str>;
|
||||
|
||||
fn add(self, rhs: &'b str) -> Self::Output {
|
||||
Oco::Owned(String::from(self) + rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Add<Cow<'b, str>> for Oco<'a, str> {
|
||||
type Output = Oco<'static, str>;
|
||||
|
||||
fn add(self, rhs: Cow<'b, str>) -> Self::Output {
|
||||
Oco::Owned(String::from(self) + rhs.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Add<Oco<'b, str>> for Oco<'a, str> {
|
||||
type Output = Oco<'static, str>;
|
||||
|
||||
fn add(self, rhs: Oco<'b, str>) -> Self::Output {
|
||||
Oco::Owned(String::from(self) + rhs.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromIterator<Oco<'a, str>> for String {
|
||||
fn from_iter<T: IntoIterator<Item = Oco<'a, str>>>(iter: T) -> Self {
|
||||
iter.into_iter().fold(String::new(), |mut acc, item| {
|
||||
acc.push_str(item.as_ref());
|
||||
acc
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn debug_fmt_should_display_quotes_for_strings() {
|
||||
let s: Oco<str> = Oco::Borrowed("hello");
|
||||
assert_eq!(format!("{:?}", s), "\"hello\"");
|
||||
let s: Oco<str> = Oco::Counted(Rc::from("hello"));
|
||||
assert_eq!(format!("{:?}", s), "\"hello\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_eq_should_compare_str_to_str() {
|
||||
let s: Oco<str> = Oco::Borrowed("hello");
|
||||
assert_eq!(s, "hello");
|
||||
assert_eq!("hello", s);
|
||||
assert_eq!(s, String::from("hello"));
|
||||
assert_eq!(String::from("hello"), s);
|
||||
assert_eq!(s, Cow::from("hello"));
|
||||
assert_eq!(Cow::from("hello"), s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_eq_should_compare_slice_to_slice() {
|
||||
let s: Oco<[i32]> = Oco::Borrowed([1, 2, 3].as_slice());
|
||||
assert_eq!(s, [1, 2, 3].as_slice());
|
||||
assert_eq!([1, 2, 3].as_slice(), s);
|
||||
assert_eq!(s, vec![1, 2, 3]);
|
||||
assert_eq!(vec![1, 2, 3], s);
|
||||
assert_eq!(s, Cow::<'_, [i32]>::Borrowed(&[1, 2, 3]));
|
||||
assert_eq!(Cow::<'_, [i32]>::Borrowed(&[1, 2, 3]), s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_should_concatenate_strings() {
|
||||
let s: Oco<str> = Oco::Borrowed("hello");
|
||||
assert_eq!(s.clone() + " world", "hello world");
|
||||
assert_eq!(s.clone() + Cow::from(" world"), "hello world");
|
||||
assert_eq!(s + Oco::from(" world"), "hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_str_should_return_a_str() {
|
||||
let s: Oco<str> = Oco::Borrowed("hello");
|
||||
assert_eq!(s.as_str(), "hello");
|
||||
let s: Oco<str> = Oco::Counted(Rc::from("hello"));
|
||||
assert_eq!(s.as_str(), "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_slice_should_return_a_slice() {
|
||||
let s: Oco<[i32]> = Oco::Borrowed([1, 2, 3].as_slice());
|
||||
assert_eq!(s.as_slice(), [1, 2, 3].as_slice());
|
||||
let s: Oco<[i32]> = Oco::Counted(Rc::from([1, 2, 3]));
|
||||
assert_eq!(s.as_slice(), [1, 2, 3].as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_for_str_should_return_an_empty_string() {
|
||||
let s: Oco<str> = Default::default();
|
||||
assert!(s.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_for_slice_should_return_an_empty_slice() {
|
||||
let s: Oco<[i32]> = Default::default();
|
||||
assert!(s.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_for_any_option_should_return_none() {
|
||||
let s: Oco<Option<i32>> = Default::default();
|
||||
assert!(s.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cloned_owned_string_should_become_counted_str() {
|
||||
let s: Oco<str> = Oco::Owned(String::from("hello"));
|
||||
assert!(s.clone().is_counted());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cloned_borrowed_str_should_remain_borrowed_str() {
|
||||
let s: Oco<str> = Oco::Borrowed("hello");
|
||||
assert!(s.clone().is_borrowed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cloned_counted_str_should_remain_counted_str() {
|
||||
let s: Oco<str> = Oco::Counted(Rc::from("hello"));
|
||||
assert!(s.clone().is_counted());
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
#[cfg(debug_assertions)]
|
||||
use crate::SpecialNonReactiveZone;
|
||||
use crate::{
|
||||
create_effect, create_isomorphic_effect, create_memo, create_signal,
|
||||
queue_microtask, runtime::with_runtime, serialization::Serializable,
|
||||
signal_prelude::format_signal_warning, spawn::spawn_local, use_context,
|
||||
GlobalSuspenseContext, Memo, ReadSignal, ScopeProperty, SignalDispose,
|
||||
SignalGet, SignalGetUntracked, SignalSet, SignalUpdate, SignalWith,
|
||||
SpecialNonReactiveZone, SuspenseContext, WriteSignal,
|
||||
GlobalSuspenseContext, Memo, ReadSignal, ScopeProperty, Signal,
|
||||
SignalDispose, SignalGet, SignalGetUntracked, SignalSet, SignalUpdate,
|
||||
SignalWith, SuspenseContext, WriteSignal,
|
||||
};
|
||||
use std::{
|
||||
any::Any,
|
||||
@@ -503,16 +505,52 @@ where
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn loading(&self) -> ReadSignal<bool> {
|
||||
with_runtime(|runtime| {
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
|
||||
resource.loading
|
||||
})
|
||||
pub fn loading(&self) -> Signal<bool> {
|
||||
#[allow(unused_variables)]
|
||||
let (loading, is_from_server) = with_runtime(|runtime| {
|
||||
let loading = runtime
|
||||
.resource(self.id, |resource: &ResourceState<S, T>| {
|
||||
resource.loading
|
||||
});
|
||||
#[cfg(feature = "hydrate")]
|
||||
let is_from_server = runtime
|
||||
.shared_context
|
||||
.borrow()
|
||||
.server_resources
|
||||
.contains(&self.id);
|
||||
|
||||
#[cfg(not(feature = "hydrate"))]
|
||||
let is_from_server = false;
|
||||
(loading, is_from_server)
|
||||
})
|
||||
.expect(
|
||||
"tried to call Resource::loading() in a runtime that has already \
|
||||
been disposed.",
|
||||
)
|
||||
);
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
{
|
||||
// if the loading signal is read outside Suspense
|
||||
// in hydrate mode, there will be a mismatch on first render
|
||||
// unless we delay a tick
|
||||
let (initial, set_initial) = create_signal(true);
|
||||
queue_microtask(move || set_initial.set(false));
|
||||
Signal::derive(move || {
|
||||
if is_from_server
|
||||
&& initial.get()
|
||||
&& use_context::<SuspenseContext>().is_none()
|
||||
{
|
||||
true
|
||||
} else {
|
||||
loading.get()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "hydrate"))]
|
||||
{
|
||||
loading.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-runs the async function with the current source data.
|
||||
@@ -592,7 +630,9 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> SignalUpdate<Option<T>> for Resource<S, T> {
|
||||
impl<S, T> SignalUpdate for Resource<S, T> {
|
||||
type Value = Option<T>;
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
@@ -648,11 +688,13 @@ impl<S, T> SignalUpdate<Option<T>> for Resource<S, T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> SignalWith<Option<T>> for Resource<S, T>
|
||||
impl<S, T> SignalWith for Resource<S, T>
|
||||
where
|
||||
S: Clone,
|
||||
T: Clone,
|
||||
{
|
||||
type Value = Option<T>;
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
@@ -714,11 +756,13 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> SignalGet<Option<T>> for Resource<S, T>
|
||||
impl<S, T> SignalGet for Resource<S, T>
|
||||
where
|
||||
S: Clone,
|
||||
T: Clone,
|
||||
{
|
||||
type Value = Option<T>;
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
@@ -751,6 +795,7 @@ where
|
||||
)
|
||||
)]
|
||||
#[inline(always)]
|
||||
#[track_caller]
|
||||
fn try_get(&self) -> Option<Option<T>> {
|
||||
let location = std::panic::Location::caller();
|
||||
with_runtime(|runtime| {
|
||||
@@ -762,7 +807,9 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> SignalSet<T> for Resource<S, T> {
|
||||
impl<S, T> SignalSet for Resource<S, T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
@@ -1248,3 +1295,29 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "nightly")]
|
||||
impl<S: Clone, T: Clone> FnOnce<()> for Resource<S, T> {
|
||||
type Output = Option<T>;
|
||||
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "nightly")]
|
||||
impl<S: Clone, T: Clone> FnMut<()> for Resource<S, T> {
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "nightly")]
|
||||
impl<S: Clone, T: Clone> Fn<()> for Resource<S, T> {
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
#[cfg(debug_assertions)]
|
||||
use crate::SpecialNonReactiveZone;
|
||||
use crate::{
|
||||
hydration::SharedContext,
|
||||
node::{
|
||||
Disposer, NodeId, ReactiveNode, ReactiveNodeState, ReactiveNodeType,
|
||||
},
|
||||
AnyComputation, AnyResource, Effect, Memo, MemoState, ReadSignal,
|
||||
ResourceId, ResourceState, RwSignal, SerializableResource,
|
||||
SpecialNonReactiveZone, StoredValueId, Trigger, UnserializableResource,
|
||||
WriteSignal,
|
||||
AnyComputation, AnyResource, EffectState, Memo, MemoState, ReadSignal,
|
||||
ResourceId, ResourceState, RwSignal, SerializableResource, StoredValueId,
|
||||
Trigger, UnserializableResource, WriteSignal,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use core::hash::BuildHasherDefault;
|
||||
@@ -771,7 +772,7 @@ impl RuntimeId {
|
||||
pub(crate) fn untrack<T>(
|
||||
self,
|
||||
f: impl FnOnce() -> T,
|
||||
diagnostics: bool,
|
||||
#[allow(unused)] diagnostics: bool,
|
||||
) -> T {
|
||||
with_runtime(|runtime| {
|
||||
let untracked_result;
|
||||
@@ -938,7 +939,7 @@ impl RuntimeId {
|
||||
{
|
||||
self.create_concrete_effect(
|
||||
Rc::new(RefCell::new(None::<T>)),
|
||||
Rc::new(Effect {
|
||||
Rc::new(EffectState {
|
||||
f,
|
||||
ty: PhantomData,
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
@@ -1002,7 +1003,7 @@ impl RuntimeId {
|
||||
|
||||
let id = self.create_concrete_effect(
|
||||
Rc::new(RefCell::new(None::<()>)),
|
||||
Rc::new(Effect {
|
||||
Rc::new(EffectState {
|
||||
f: effect_fn,
|
||||
ty: PhantomData,
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
|
||||
@@ -105,35 +105,41 @@ pub mod prelude {
|
||||
|
||||
/// This trait allows getting an owned value of the signals
|
||||
/// inner type.
|
||||
pub trait SignalGet<T> {
|
||||
pub trait SignalGet {
|
||||
/// The value held by the signal.
|
||||
type Value;
|
||||
|
||||
/// Clones and returns the current value of the signal, and subscribes
|
||||
/// the running effect to this signal.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a signal that is owned by a reactive node that has been disposed.
|
||||
#[track_caller]
|
||||
fn get(&self) -> T;
|
||||
fn get(&self) -> Self::Value;
|
||||
|
||||
/// Clones and returns the signal value, returning [`Some`] if the signal
|
||||
/// is still alive, and [`None`] otherwise.
|
||||
fn try_get(&self) -> Option<T>;
|
||||
fn try_get(&self) -> Option<Self::Value>;
|
||||
}
|
||||
|
||||
/// This trait allows obtaining an immutable reference to the signal's
|
||||
/// inner type.
|
||||
pub trait SignalWith<T> {
|
||||
pub trait SignalWith {
|
||||
/// The value held by the signal.
|
||||
type Value;
|
||||
|
||||
/// Applies a function to the current value of the signal, and subscribes
|
||||
/// the running effect to this signal.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a signal that is owned by a reactive node that has been disposed.
|
||||
#[track_caller]
|
||||
fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O;
|
||||
fn with<O>(&self, f: impl FnOnce(&Self::Value) -> O) -> O;
|
||||
|
||||
/// Applies a function to the current value of the signal, and subscribes
|
||||
/// the running effect to this signal. Returns [`Some`] if the signal is
|
||||
/// valid and the function ran, otherwise returns [`None`].
|
||||
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O>;
|
||||
fn try_with<O>(&self, f: impl FnOnce(&Self::Value) -> O) -> Option<O>;
|
||||
|
||||
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
|
||||
fn track(&self) {
|
||||
@@ -142,31 +148,37 @@ pub trait SignalWith<T> {
|
||||
}
|
||||
|
||||
/// This trait allows setting the value of a signal.
|
||||
pub trait SignalSet<T> {
|
||||
pub trait SignalSet {
|
||||
/// The value held by the signal.
|
||||
type Value;
|
||||
|
||||
/// Sets the signal’s value and notifies subscribers.
|
||||
///
|
||||
/// **Note:** `set()` does not auto-memoize, i.e., it will notify subscribers
|
||||
/// even if the value has not actually changed.
|
||||
#[track_caller]
|
||||
fn set(&self, new_value: T);
|
||||
fn set(&self, new_value: Self::Value);
|
||||
|
||||
/// Sets the signal’s value and notifies subscribers. Returns [`None`]
|
||||
/// if the signal is still valid, [`Some(T)`] otherwise.
|
||||
///
|
||||
/// **Note:** `set()` does not auto-memoize, i.e., it will notify subscribers
|
||||
/// even if the value has not actually changed.
|
||||
fn try_set(&self, new_value: T) -> Option<T>;
|
||||
fn try_set(&self, new_value: Self::Value) -> Option<Self::Value>;
|
||||
}
|
||||
|
||||
/// This trait allows updating the inner value of a signal.
|
||||
pub trait SignalUpdate<T> {
|
||||
pub trait SignalUpdate {
|
||||
/// The value held by the signal.
|
||||
type Value;
|
||||
|
||||
/// Applies a function to the current value to mutate it in place
|
||||
/// and notifies subscribers that the signal has changed.
|
||||
///
|
||||
/// **Note:** `update()` does not auto-memoize, i.e., it will notify subscribers
|
||||
/// even if the value has not actually changed.
|
||||
#[track_caller]
|
||||
fn update(&self, f: impl FnOnce(&mut T));
|
||||
fn update(&self, f: impl FnOnce(&mut Self::Value));
|
||||
|
||||
/// Applies a function to the current value to mutate it in place
|
||||
/// and notifies subscribers that the signal has changed. Returns
|
||||
@@ -174,45 +186,55 @@ pub trait SignalUpdate<T> {
|
||||
///
|
||||
/// **Note:** `update()` does not auto-memoize, i.e., it will notify subscribers
|
||||
/// even if the value has not actually changed.
|
||||
fn try_update<O>(&self, f: impl FnOnce(&mut T) -> O) -> Option<O>;
|
||||
fn try_update<O>(&self, f: impl FnOnce(&mut Self::Value) -> O)
|
||||
-> Option<O>;
|
||||
}
|
||||
|
||||
/// Trait implemented for all signal types which you can `get` a value
|
||||
/// from, such as [`ReadSignal`],
|
||||
/// [`Memo`](crate::Memo), etc., which allows getting the inner value without
|
||||
/// subscribing to the current scope.
|
||||
pub trait SignalGetUntracked<T> {
|
||||
pub trait SignalGetUntracked {
|
||||
/// The value held by the signal.
|
||||
type Value;
|
||||
|
||||
/// Gets the signal's value without creating a dependency on the
|
||||
/// current scope.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a signal that is owned by a reactive node that has been disposed.
|
||||
#[track_caller]
|
||||
fn get_untracked(&self) -> T;
|
||||
fn get_untracked(&self) -> Self::Value;
|
||||
|
||||
/// Gets the signal's value without creating a dependency on the
|
||||
/// current scope. Returns [`Some(T)`] if the signal is still
|
||||
/// valid, [`None`] otherwise.
|
||||
fn try_get_untracked(&self) -> Option<T>;
|
||||
fn try_get_untracked(&self) -> Option<Self::Value>;
|
||||
}
|
||||
|
||||
/// This trait allows getting a reference to the signals inner value
|
||||
/// without creating a dependency on the signal.
|
||||
pub trait SignalWithUntracked<T> {
|
||||
pub trait SignalWithUntracked {
|
||||
/// The value held by the signal.
|
||||
type Value;
|
||||
|
||||
/// Runs the provided closure with a reference to the current
|
||||
/// value without creating a dependency on the current scope.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if you try to access a signal that is owned by a reactive node that has been disposed.
|
||||
#[track_caller]
|
||||
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O;
|
||||
fn with_untracked<O>(&self, f: impl FnOnce(&Self::Value) -> O) -> O;
|
||||
|
||||
/// Runs the provided closure with a reference to the current
|
||||
/// value without creating a dependency on the current scope.
|
||||
/// Returns [`Some(O)`] if the signal is still valid, [`None`]
|
||||
/// otherwise.
|
||||
#[track_caller]
|
||||
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O>;
|
||||
fn try_with_untracked<O>(
|
||||
&self,
|
||||
f: impl FnOnce(&Self::Value) -> O,
|
||||
) -> Option<O>;
|
||||
}
|
||||
|
||||
/// Trait implemented for all signal types which you can `set` the inner
|
||||
@@ -419,7 +441,9 @@ where
|
||||
pub(crate) defined_at: &'static std::panic::Location<'static>,
|
||||
}
|
||||
|
||||
impl<T: Clone> SignalGetUntracked<T> for ReadSignal<T> {
|
||||
impl<T: Clone> SignalGetUntracked for ReadSignal<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -470,7 +494,9 @@ impl<T: Clone> SignalGetUntracked<T> for ReadSignal<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SignalWithUntracked<T> for ReadSignal<T> {
|
||||
impl<T> SignalWithUntracked for ReadSignal<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -533,7 +559,9 @@ impl<T> SignalWithUntracked<T> for ReadSignal<T> {
|
||||
/// assert_eq!(first_char(), 'B');
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
impl<T> SignalWith<T> for ReadSignal<T> {
|
||||
impl<T> SignalWith for ReadSignal<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -600,7 +628,9 @@ impl<T> SignalWith<T> for ReadSignal<T> {
|
||||
/// // assert_eq!(count.get(), 0);
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
impl<T: Clone> SignalGet<T> for ReadSignal<T> {
|
||||
impl<T: Clone> SignalGet for ReadSignal<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -928,7 +958,9 @@ impl<T> SignalUpdateUntracked<T> for WriteSignal<T> {
|
||||
/// assert_eq!(count.get(), 1);
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
impl<T> SignalUpdate<T> for WriteSignal<T> {
|
||||
impl<T> SignalUpdate for WriteSignal<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -1000,7 +1032,9 @@ impl<T> SignalUpdate<T> for WriteSignal<T> {
|
||||
/// assert_eq!(count.get(), 1);
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
impl<T> SignalSet<T> for WriteSignal<T> {
|
||||
impl<T> SignalSet for WriteSignal<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -1217,7 +1251,9 @@ impl<T> From<T> for RwSignal<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> SignalGetUntracked<T> for RwSignal<T> {
|
||||
impl<T: Clone> SignalGetUntracked for RwSignal<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -1279,7 +1315,9 @@ impl<T: Clone> SignalGetUntracked<T> for RwSignal<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SignalWithUntracked<T> for RwSignal<T> {
|
||||
impl<T> SignalWithUntracked for RwSignal<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -1456,7 +1494,9 @@ impl<T> SignalUpdateUntracked<T> for RwSignal<T> {
|
||||
/// # runtime.dispose();
|
||||
/// #
|
||||
/// ```
|
||||
impl<T> SignalWith<T> for RwSignal<T> {
|
||||
impl<T> SignalWith for RwSignal<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -1524,7 +1564,9 @@ impl<T> SignalWith<T> for RwSignal<T> {
|
||||
/// # runtime.dispose();
|
||||
/// #
|
||||
/// ```
|
||||
impl<T: Clone> SignalGet<T> for RwSignal<T> {
|
||||
impl<T: Clone> SignalGet for RwSignal<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -1604,7 +1646,9 @@ impl<T: Clone> SignalGet<T> for RwSignal<T> {
|
||||
/// assert_eq!(count.get(), 1);
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
impl<T> SignalUpdate<T> for RwSignal<T> {
|
||||
impl<T> SignalUpdate for RwSignal<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -1671,7 +1715,9 @@ impl<T> SignalUpdate<T> for RwSignal<T> {
|
||||
/// assert_eq!(count.get(), 1);
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
impl<T> SignalSet<T> for RwSignal<T> {
|
||||
impl<T> SignalSet for RwSignal<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
|
||||
@@ -28,12 +28,12 @@ where
|
||||
/// function call, `with()`, and `get()` APIs as other signals.
|
||||
///
|
||||
/// ## Core Trait Implementations
|
||||
/// - [`.get()`](#impl-SignalGet<T>-for-Signal<T>) (or calling the signal as a function) clones the current
|
||||
/// - [`.get()`](#impl-SignalGet-for-Signal<T>) (or calling the signal as a function) clones the current
|
||||
/// value of the signal. If you call it within an effect, it will cause that effect
|
||||
/// to subscribe to the signal, and to re-run whenever the value of the signal changes.
|
||||
/// - [`.get_untracked()`](#impl-SignalGetUntracked<T>-for-Signal<T>) clones the value of the signal
|
||||
/// without reactively tracking it.
|
||||
/// - [`.with()`](#impl-SignalWith<T>-for-Signal<T>) allows you to reactively access the signal’s value without
|
||||
/// - [`.with()`](#impl-SignalWith-for-Signal<T>) allows you to reactively access the signal’s value without
|
||||
/// cloning by applying a callback function.
|
||||
/// - [`.with_untracked()`](#impl-SignalWithUntracked<T>-for-Signal<T>) allows you to access the signal’s
|
||||
/// value without reactively tracking it.
|
||||
@@ -97,7 +97,9 @@ impl<T> PartialEq for Signal<T> {
|
||||
/// Please note that using `Signal::with_untracked` still clones the inner value,
|
||||
/// so there's no benefit to using it as opposed to calling
|
||||
/// `Signal::get_untracked`.
|
||||
impl<T: Clone> SignalGetUntracked<T> for Signal<T> {
|
||||
impl<T: Clone> SignalGetUntracked for Signal<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -141,7 +143,9 @@ impl<T: Clone> SignalGetUntracked<T> for Signal<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SignalWithUntracked<T> for Signal<T> {
|
||||
impl<T> SignalWithUntracked for Signal<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -218,7 +222,9 @@ impl<T> SignalWithUntracked<T> for Signal<T> {
|
||||
/// assert_eq!(memoized_lower.get(), "alice");
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
impl<T> SignalWith<T> for Signal<T> {
|
||||
impl<T> SignalWith for Signal<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -280,7 +286,9 @@ impl<T> SignalWith<T> for Signal<T> {
|
||||
/// assert_eq!(above_3(&memoized_double_count.into()), true);
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
impl<T: Clone> SignalGet<T> for Signal<T> {
|
||||
impl<T: Clone> SignalGet for Signal<T> {
|
||||
type Value = T;
|
||||
|
||||
fn get(&self) -> T {
|
||||
match self.inner {
|
||||
SignalTypes::ReadSignal(r) => r.get(),
|
||||
@@ -475,12 +483,12 @@ impl<T> Eq for SignalTypes<T> where T: PartialEq {}
|
||||
/// of the same type. This is especially useful for component properties.
|
||||
///
|
||||
/// ## Core Trait Implementations
|
||||
/// - [`.get()`](#impl-SignalGet<T>-for-MaybeSignal<T>) (or calling the signal as a function) clones the current
|
||||
/// - [`.get()`](#impl-SignalGet-for-MaybeSignal<T>) (or calling the signal as a function) clones the current
|
||||
/// value of the signal. If you call it within an effect, it will cause that effect
|
||||
/// to subscribe to the signal, and to re-run whenever the value of the signal changes.
|
||||
/// - [`.get_untracked()`](#impl-SignalGetUntracked<T>-for-MaybeSignal<T>) clones the value of the signal
|
||||
/// without reactively tracking it.
|
||||
/// - [`.with()`](#impl-SignalWith<T>-for-MaybeSignal<T>) allows you to reactively access the signal’s value without
|
||||
/// - [`.with()`](#impl-SignalWith-for-MaybeSignal<T>) allows you to reactively access the signal’s value without
|
||||
/// cloning by applying a callback function.
|
||||
/// - [`.with_untracked()`](#impl-SignalWithUntracked<T>-for-MaybeSignal<T>) allows you to access the signal’s
|
||||
/// value without reactively tracking it.
|
||||
@@ -557,7 +565,9 @@ impl<T: Default> Default for MaybeSignal<T> {
|
||||
/// assert_eq!(above_3(&static_value.into()), true);
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
impl<T: Clone> SignalGet<T> for MaybeSignal<T> {
|
||||
impl<T: Clone> SignalGet for MaybeSignal<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -624,7 +634,9 @@ impl<T: Clone> SignalGet<T> for MaybeSignal<T> {
|
||||
/// assert_eq!(static_value.get(), "Bob");
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
impl<T> SignalWith<T> for MaybeSignal<T> {
|
||||
impl<T> SignalWith for MaybeSignal<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -658,7 +670,9 @@ impl<T> SignalWith<T> for MaybeSignal<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SignalWithUntracked<T> for MaybeSignal<T> {
|
||||
impl<T> SignalWithUntracked for MaybeSignal<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -692,7 +706,9 @@ impl<T> SignalWithUntracked<T> for MaybeSignal<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> SignalGetUntracked<T> for MaybeSignal<T> {
|
||||
impl<T: Clone> SignalGetUntracked for MaybeSignal<T> {
|
||||
type Value = T;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -831,12 +847,12 @@ impl From<&str> for MaybeSignal<String> {
|
||||
/// This creates an extremely flexible type for component libraries, etc.
|
||||
///
|
||||
/// ## Core Trait Implementations
|
||||
/// - [`.get()`](#impl-SignalGet<T>-for-MaybeProp<T>) (or calling the signal as a function) clones the current
|
||||
/// - [`.get()`](#impl-SignalGet-for-MaybeProp<T>) (or calling the signal as a function) clones the current
|
||||
/// value of the signal. If you call it within an effect, it will cause that effect
|
||||
/// to subscribe to the signal, and to re-run whenever the value of the signal changes.
|
||||
/// - [`.get_untracked()`](#impl-SignalGetUntracked<T>-for-MaybeProp<T>) clones the value of the signal
|
||||
/// without reactively tracking it.
|
||||
/// - [`.with()`](#impl-SignalWith<T>-for-MaybeProp<T>) allows you to reactively access the signal’s value without
|
||||
/// - [`.with()`](#impl-SignalWith-for-MaybeProp<T>) allows you to reactively access the signal’s value without
|
||||
/// cloning by applying a callback function.
|
||||
/// - [`.with_untracked()`](#impl-SignalWithUntracked<T>-for-MaybeProp<T>) allows you to access the signal’s
|
||||
/// value without reactively tracking it.
|
||||
@@ -902,7 +918,9 @@ impl<T> Default for MaybeProp<T> {
|
||||
/// assert_eq!(above_3(&memoized_double_count.into()), true);
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
impl<T: Clone> SignalGet<Option<T>> for MaybeProp<T> {
|
||||
impl<T: Clone> SignalGet for MaybeProp<T> {
|
||||
type Value = Option<T>;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
@@ -1042,7 +1060,9 @@ impl<T> MaybeProp<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> SignalGetUntracked<Option<T>> for MaybeProp<T> {
|
||||
impl<T: Clone> SignalGetUntracked for MaybeProp<T> {
|
||||
type Value = Option<T>;
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(
|
||||
|
||||
@@ -77,7 +77,9 @@ impl<T: Default + 'static> Default for SignalSetter<T> {
|
||||
|
||||
impl<T> Copy for SignalSetter<T> {}
|
||||
|
||||
impl<T> SignalSet<T> for SignalSetter<T> {
|
||||
impl<T> SignalSet for SignalSetter<T> {
|
||||
type Value = T;
|
||||
|
||||
fn set(&self, new_value: T) {
|
||||
match self.inner {
|
||||
SignalSetterTypes::Default => {}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
//! Types that handle asynchronous data loading via `<Suspense/>`.
|
||||
|
||||
use crate::{
|
||||
create_isomorphic_effect, create_rw_signal, create_signal, queue_microtask,
|
||||
signal::SignalGet, store_value, ReadSignal, RwSignal, SignalSet,
|
||||
SignalUpdate, StoredValue, WriteSignal,
|
||||
create_isomorphic_effect, create_rw_signal, create_signal, oco::Oco,
|
||||
queue_microtask, signal::SignalGet, store_value, ReadSignal, RwSignal,
|
||||
SignalSet, SignalUpdate, StoredValue, WriteSignal,
|
||||
};
|
||||
use futures::Future;
|
||||
use std::{
|
||||
borrow::Cow, cell::RefCell, collections::VecDeque, pin::Pin, rc::Rc,
|
||||
};
|
||||
use std::{cell::RefCell, collections::VecDeque, pin::Pin, rc::Rc};
|
||||
|
||||
/// Tracks [`Resource`](crate::Resource)s that are read under a suspense context,
|
||||
/// i.e., within a [`Suspense`](https://docs.rs/leptos_core/latest/leptos_core/fn.Suspense.html) component.
|
||||
@@ -77,7 +75,7 @@ impl SuspenseContext {
|
||||
if pending_resources.get() == 0 {
|
||||
_ = tx.borrow_mut().try_send(());
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
async move {
|
||||
rx.next().await;
|
||||
@@ -172,7 +170,7 @@ impl Default for SuspenseContext {
|
||||
/// Represents a chunk in a stream of HTML.
|
||||
pub enum StreamChunk {
|
||||
/// A chunk of synchronous HTML.
|
||||
Sync(Cow<'static, str>),
|
||||
Sync(Oco<'static, str>),
|
||||
/// A future that resolves to be a list of additional chunks.
|
||||
Async {
|
||||
/// The HTML chunks this contains.
|
||||
|
||||
@@ -97,7 +97,9 @@ pub fn create_trigger() -> Trigger {
|
||||
Runtime::current().create_trigger()
|
||||
}
|
||||
|
||||
impl SignalGet<()> for Trigger {
|
||||
impl SignalGet for Trigger {
|
||||
type Value = ();
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
@@ -134,7 +136,9 @@ impl SignalGet<()> for Trigger {
|
||||
}
|
||||
}
|
||||
|
||||
impl SignalUpdate<()> for Trigger {
|
||||
impl SignalUpdate for Trigger {
|
||||
type Value = ();
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
@@ -181,7 +185,9 @@ impl SignalUpdate<()> for Trigger {
|
||||
}
|
||||
}
|
||||
|
||||
impl SignalSet<()> for Trigger {
|
||||
impl SignalSet for Trigger {
|
||||
type Value = ();
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.5.0"
|
||||
version = "0.5.0-beta2"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -51,7 +51,6 @@ use leptos::{
|
||||
*,
|
||||
};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::{Cell, RefCell},
|
||||
fmt::Debug,
|
||||
rc::Rc,
|
||||
@@ -100,7 +99,7 @@ pub struct MetaTagsContext {
|
||||
els: Rc<
|
||||
RefCell<
|
||||
IndexMap<
|
||||
Cow<'static, str>,
|
||||
Oco<'static, str>,
|
||||
(HtmlElement<AnyElement>, Option<web_sys::Element>),
|
||||
>,
|
||||
>,
|
||||
@@ -130,7 +129,7 @@ impl MetaTagsContext {
|
||||
pub fn register(
|
||||
&self,
|
||||
|
||||
id: Cow<'static, str>,
|
||||
id: Oco<'static, str>,
|
||||
builder_el: HtmlElement<AnyElement>,
|
||||
) {
|
||||
cfg_if! {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::use_head;
|
||||
use leptos::{nonce::use_nonce, *};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Injects an [HTMLLinkElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document
|
||||
/// head, accepting any of the valid attributes for that tag.
|
||||
@@ -28,62 +27,62 @@ use std::borrow::Cow;
|
||||
pub fn Link(
|
||||
/// The [`id`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-id) attribute.
|
||||
#[prop(optional, into)]
|
||||
id: Option<Cow<'static, str>>,
|
||||
id: Option<Oco<'static, str>>,
|
||||
/// The [`as`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-as) attribute.
|
||||
#[prop(optional, into)]
|
||||
as_: Option<Cow<'static, str>>,
|
||||
as_: Option<Oco<'static, str>>,
|
||||
/// The [`crossorigin`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-crossorigin) attribute.
|
||||
#[prop(optional, into)]
|
||||
crossorigin: Option<Cow<'static, str>>,
|
||||
crossorigin: Option<Oco<'static, str>>,
|
||||
/// The [`disabled`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-disabled) attribute.
|
||||
#[prop(optional, into)]
|
||||
disabled: Option<bool>,
|
||||
/// The [`fetchpriority`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-fetchpriority) attribute.
|
||||
#[prop(optional, into)]
|
||||
fetchpriority: Option<Cow<'static, str>>,
|
||||
fetchpriority: Option<Oco<'static, str>>,
|
||||
/// The [`href`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-href) attribute.
|
||||
#[prop(optional, into)]
|
||||
href: Option<Cow<'static, str>>,
|
||||
href: Option<Oco<'static, str>>,
|
||||
/// The [`hreflang`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-hreflang) attribute.
|
||||
#[prop(optional, into)]
|
||||
hreflang: Option<Cow<'static, str>>,
|
||||
hreflang: Option<Oco<'static, str>>,
|
||||
/// The [`imagesizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-imagesizes) attribute.
|
||||
#[prop(optional, into)]
|
||||
imagesizes: Option<Cow<'static, str>>,
|
||||
imagesizes: Option<Oco<'static, str>>,
|
||||
/// The [`imagesrcset`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-imagesrcset) attribute.
|
||||
#[prop(optional, into)]
|
||||
imagesrcset: Option<Cow<'static, str>>,
|
||||
imagesrcset: Option<Oco<'static, str>>,
|
||||
/// The [`integrity`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-integrity) attribute.
|
||||
#[prop(optional, into)]
|
||||
integrity: Option<Cow<'static, str>>,
|
||||
integrity: Option<Oco<'static, str>>,
|
||||
/// The [`media`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-media) attribute.
|
||||
#[prop(optional, into)]
|
||||
media: Option<Cow<'static, str>>,
|
||||
media: Option<Oco<'static, str>>,
|
||||
/// The [`prefetch`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-prefetch) attribute.
|
||||
#[prop(optional, into)]
|
||||
prefetch: Option<Cow<'static, str>>,
|
||||
prefetch: Option<Oco<'static, str>>,
|
||||
/// The [`referrerpolicy`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-referrerpolicy) attribute.
|
||||
#[prop(optional, into)]
|
||||
referrerpolicy: Option<Cow<'static, str>>,
|
||||
referrerpolicy: Option<Oco<'static, str>>,
|
||||
/// The [`rel`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-rel) attribute.
|
||||
#[prop(optional, into)]
|
||||
rel: Option<Cow<'static, str>>,
|
||||
rel: Option<Oco<'static, str>>,
|
||||
/// The [`sizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-sizes) attribute.
|
||||
#[prop(optional, into)]
|
||||
sizes: Option<Cow<'static, str>>,
|
||||
sizes: Option<Oco<'static, str>>,
|
||||
/// The [`title`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-title) attribute.
|
||||
#[prop(optional, into)]
|
||||
title: Option<Cow<'static, str>>,
|
||||
title: Option<Oco<'static, str>>,
|
||||
/// The [`type`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-type) attribute.
|
||||
#[prop(optional, into)]
|
||||
type_: Option<Cow<'static, str>>,
|
||||
type_: Option<Oco<'static, str>>,
|
||||
/// The [`blocking`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-blocking) attribute.
|
||||
#[prop(optional, into)]
|
||||
blocking: Option<Cow<'static, str>>,
|
||||
blocking: Option<Oco<'static, str>>,
|
||||
) -> impl IntoView {
|
||||
let meta = use_head();
|
||||
let next_id = meta.tags.get_next_id();
|
||||
let id: Cow<'static, str> =
|
||||
let id: Oco<'static, str> =
|
||||
id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0).into());
|
||||
|
||||
let builder_el = leptos::leptos_dom::html::as_meta_tag({
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::use_head;
|
||||
use leptos::{nonce::use_nonce, *};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Injects an [HTMLScriptElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement) into the document
|
||||
/// head, accepting any of the valid attributes for that tag.
|
||||
@@ -25,47 +24,47 @@ use std::borrow::Cow;
|
||||
pub fn Script(
|
||||
/// An ID for the `<script>` tag.
|
||||
#[prop(optional, into)]
|
||||
id: Option<Cow<'static, str>>,
|
||||
id: Option<Oco<'static, str>>,
|
||||
/// The [`async`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-async) attribute.
|
||||
#[prop(optional, into)]
|
||||
async_: Option<Cow<'static, str>>,
|
||||
async_: Option<Oco<'static, str>>,
|
||||
/// The [`crossorigin`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-crossorigin) attribute.
|
||||
#[prop(optional, into)]
|
||||
crossorigin: Option<Cow<'static, str>>,
|
||||
crossorigin: Option<Oco<'static, str>>,
|
||||
/// The [`defer`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-defer) attribute.
|
||||
#[prop(optional, into)]
|
||||
defer: Option<Cow<'static, str>>,
|
||||
defer: Option<Oco<'static, str>>,
|
||||
/// The [`fetchpriority `](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-fetchpriority ) attribute.
|
||||
#[prop(optional, into)]
|
||||
fetchpriority: Option<Cow<'static, str>>,
|
||||
fetchpriority: Option<Oco<'static, str>>,
|
||||
/// The [`integrity`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-integrity) attribute.
|
||||
#[prop(optional, into)]
|
||||
integrity: Option<Cow<'static, str>>,
|
||||
integrity: Option<Oco<'static, str>>,
|
||||
/// The [`nomodule`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-nomodule) attribute.
|
||||
#[prop(optional, into)]
|
||||
nomodule: Option<Cow<'static, str>>,
|
||||
nomodule: Option<Oco<'static, str>>,
|
||||
/// The [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-nonce) attribute.
|
||||
#[prop(optional, into)]
|
||||
nonce: Option<Cow<'static, str>>,
|
||||
nonce: Option<Oco<'static, str>>,
|
||||
/// The [`referrerpolicy`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-referrerpolicy) attribute.
|
||||
#[prop(optional, into)]
|
||||
referrerpolicy: Option<Cow<'static, str>>,
|
||||
referrerpolicy: Option<Oco<'static, str>>,
|
||||
/// The [`src`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-src) attribute.
|
||||
#[prop(optional, into)]
|
||||
src: Option<Cow<'static, str>>,
|
||||
src: Option<Oco<'static, str>>,
|
||||
/// The [`type`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type) attribute.
|
||||
#[prop(optional, into)]
|
||||
type_: Option<Cow<'static, str>>,
|
||||
type_: Option<Oco<'static, str>>,
|
||||
/// The [`blocking`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-blocking) attribute.
|
||||
#[prop(optional, into)]
|
||||
blocking: Option<Cow<'static, str>>,
|
||||
blocking: Option<Oco<'static, str>>,
|
||||
/// The content of the `<script>` tag.
|
||||
#[prop(optional)]
|
||||
children: Option<Box<dyn FnOnce() -> Fragment>>,
|
||||
) -> impl IntoView {
|
||||
let meta = use_head();
|
||||
let next_id = meta.tags.get_next_id();
|
||||
let id: Cow<'static, str> =
|
||||
let id: Oco<'static, str> =
|
||||
id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0).into());
|
||||
|
||||
let builder_el = leptos::leptos_dom::html::as_meta_tag({
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::use_head;
|
||||
use leptos::{nonce::use_nonce, *};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Injects an [HTMLStyleElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLStyleElement) into the document
|
||||
/// head, accepting any of the valid attributes for that tag.
|
||||
@@ -25,26 +24,26 @@ use std::borrow::Cow;
|
||||
pub fn Style(
|
||||
/// An ID for the `<script>` tag.
|
||||
#[prop(optional, into)]
|
||||
id: Option<Cow<'static, str>>,
|
||||
id: Option<Oco<'static, str>>,
|
||||
/// The [`media`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style#attr-media) attribute.
|
||||
#[prop(optional, into)]
|
||||
media: Option<Cow<'static, str>>,
|
||||
media: Option<Oco<'static, str>>,
|
||||
/// The [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style#attr-nonce) attribute.
|
||||
#[prop(optional, into)]
|
||||
nonce: Option<Cow<'static, str>>,
|
||||
nonce: Option<Oco<'static, str>>,
|
||||
/// The [`title`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style#attr-title) attribute.
|
||||
#[prop(optional, into)]
|
||||
title: Option<Cow<'static, str>>,
|
||||
title: Option<Oco<'static, str>>,
|
||||
/// The [`blocking`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style#attr-blocking) attribute.
|
||||
#[prop(optional, into)]
|
||||
blocking: Option<Cow<'static, str>>,
|
||||
blocking: Option<Oco<'static, str>>,
|
||||
/// The content of the `<style>` tag.
|
||||
#[prop(optional)]
|
||||
children: Option<Box<dyn FnOnce() -> Fragment>>,
|
||||
) -> impl IntoView {
|
||||
let meta = use_head();
|
||||
let next_id = meta.tags.get_next_id();
|
||||
let id: Cow<'static, str> =
|
||||
let id: Oco<'static, str> =
|
||||
id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0).into());
|
||||
|
||||
let builder_el = leptos::leptos_dom::html::as_meta_tag({
|
||||
|
||||
@@ -16,11 +16,11 @@ pub struct TitleContext {
|
||||
|
||||
impl TitleContext {
|
||||
/// Converts the title into a string that can be used as the text content of a `<title>` tag.
|
||||
pub fn as_string(&self) -> Option<String> {
|
||||
let title = self.text.borrow().as_ref().map(|f| f.get());
|
||||
pub fn as_string(&self) -> Option<Oco<'static, str>> {
|
||||
let title = self.text.borrow().as_ref().map(TextProp::get);
|
||||
title.map(|title| {
|
||||
if let Some(formatter) = &*self.formatter.borrow() {
|
||||
(formatter.0)(title)
|
||||
(formatter.0)(title.into_owned()).into()
|
||||
} else {
|
||||
title
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_router"
|
||||
version = "0.5.0"
|
||||
version = "0.5.0-beta2"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -409,6 +409,13 @@ where
|
||||
|
||||
let on_response = Rc::new(move |resp: &web_sys::Response| {
|
||||
let resp = resp.clone().expect("couldn't get Response");
|
||||
|
||||
// If the response was redirected then a JSON will not be available in the response, instead
|
||||
// it will be an actual page, so we don't want to try to parse it.
|
||||
if resp.redirected() {
|
||||
return;
|
||||
}
|
||||
|
||||
spawn_local(async move {
|
||||
let body = JsFuture::from(
|
||||
resp.text().expect("couldn't get .text() from Response"),
|
||||
|
||||
@@ -24,6 +24,20 @@ impl ToHref for String {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToHref for Cow<'_, str> {
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
|
||||
let s = self.to_string();
|
||||
Box::new(move || s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToHref for Oco<'_, str> {
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
|
||||
let s = self.to_string();
|
||||
Box::new(move || s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> ToHref for F
|
||||
where
|
||||
F: Fn() -> String + 'static,
|
||||
@@ -65,7 +79,7 @@ pub fn A<H>(
|
||||
/// `[aria-current=page]` selector, you should prefer that, as it enables significant
|
||||
/// SSR optimizations.
|
||||
#[prop(optional, into)]
|
||||
active_class: Option<Cow<'static, str>>,
|
||||
active_class: Option<Oco<'static, str>>,
|
||||
/// An object of any type that will be pushed to router state
|
||||
#[prop(optional)]
|
||||
state: Option<State>,
|
||||
@@ -78,7 +92,7 @@ pub fn A<H>(
|
||||
class: Option<AttributeValue>,
|
||||
/// Sets the `id` attribute on the underlying `<a>` tag, making it easier to target.
|
||||
#[prop(optional, into)]
|
||||
id: Option<String>,
|
||||
id: Option<Oco<'static, str>>,
|
||||
/// The nodes or elements to be shown inside the link.
|
||||
children: Children,
|
||||
) -> impl IntoView
|
||||
@@ -95,37 +109,35 @@ where
|
||||
#[allow(unused)] state: Option<State>,
|
||||
#[allow(unused)] replace: bool,
|
||||
class: Option<AttributeValue>,
|
||||
#[allow(unused)] active_class: Option<Cow<'static, str>>,
|
||||
id: Option<String>,
|
||||
#[allow(unused)] active_class: Option<Oco<'static, str>>,
|
||||
id: Option<Oco<'static, str>>,
|
||||
children: Children,
|
||||
) -> View {
|
||||
#[cfg(not(any(feature = "hydrate", feature = "csr")))]
|
||||
{
|
||||
_ = state;
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "hydrate", feature = "csr")))]
|
||||
{
|
||||
_ = replace;
|
||||
}
|
||||
|
||||
let location = use_location();
|
||||
let is_active = create_memo(move |_| match href.get() {
|
||||
None => false,
|
||||
|
||||
Some(to) => {
|
||||
let path = to
|
||||
.split(['?', '#'])
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
let loc = location.pathname.get().to_lowercase();
|
||||
if exact {
|
||||
loc == path
|
||||
} else {
|
||||
loc.starts_with(&path)
|
||||
}
|
||||
}
|
||||
let is_active = create_memo(move |_| {
|
||||
href.with(|href| {
|
||||
href.as_deref().is_some_and(|to| {
|
||||
let path = to
|
||||
.split(['?', '#'])
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
location.pathname.with(|loc| {
|
||||
let loc = loc.to_lowercase();
|
||||
if exact {
|
||||
loc == path
|
||||
} else {
|
||||
loc.starts_with(&path)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::{
|
||||
use leptos::{leptos_dom::Transparent, *};
|
||||
use std::{
|
||||
any::Any,
|
||||
borrow::Cow,
|
||||
cell::{Cell, RefCell},
|
||||
rc::Rc,
|
||||
};
|
||||
@@ -309,7 +310,7 @@ impl RouteContext {
|
||||
|
||||
pub(crate) fn resolve_path_tracked(&self, to: &str) -> Option<String> {
|
||||
resolve_path(&self.inner.base_path, to, Some(&self.inner.path.get()))
|
||||
.map(String::from)
|
||||
.map(Cow::into_owned)
|
||||
}
|
||||
|
||||
/// The nested child route, if any.
|
||||
|
||||
@@ -544,6 +544,7 @@ pub(crate) fn create_branch(routes: &[RouteData], index: usize) -> Branch {
|
||||
score: routes.last().unwrap().score() * 10000 - (index as i32),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "info", skip_all,)
|
||||
@@ -567,7 +568,7 @@ fn create_routes(route_def: &RouteDefinition, base: &str) -> Vec<RouteData> {
|
||||
id: route_def.id,
|
||||
matcher: Matcher::new_with_partial(&pattern, !is_leaf),
|
||||
pattern,
|
||||
original_path: original_path.to_string(),
|
||||
original_path: original_path.into_owned(),
|
||||
});
|
||||
}
|
||||
acc
|
||||
|
||||
@@ -4,9 +4,9 @@ use crate::{
|
||||
};
|
||||
use leptos::{
|
||||
create_memo, request_animation_frame, signal_prelude::*, use_context, Memo,
|
||||
Oco,
|
||||
};
|
||||
use std::{borrow::Cow, rc::Rc, str::FromStr};
|
||||
|
||||
use std::{rc::Rc, str::FromStr};
|
||||
/// Constructs a signal synchronized with a specific URL query parameter.
|
||||
///
|
||||
/// The function creates a bidirectional sync mechanism between the state encapsulated in a signal and a URL query parameter.
|
||||
@@ -45,7 +45,7 @@ use std::{borrow::Cow, rc::Rc, str::FromStr};
|
||||
/// ```
|
||||
#[track_caller]
|
||||
pub fn create_query_signal<T>(
|
||||
key: impl Into<Cow<'static, str>>,
|
||||
key: impl Into<Oco<'static, str>>,
|
||||
) -> (Memo<Option<T>>, SignalSetter<Option<T>>)
|
||||
where
|
||||
T: FromStr + ToString + PartialEq,
|
||||
@@ -163,7 +163,7 @@ pub fn use_resolved_path(
|
||||
if path.starts_with('/') {
|
||||
Some(path)
|
||||
} else {
|
||||
route.resolve_path_tracked(&path).map(String::from)
|
||||
route.resolve_path_tracked(&path)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -190,6 +190,7 @@ pub fn use_navigate() -> impl Fn(&str, NavigateOptions) {
|
||||
let to = to.to_string();
|
||||
if cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
request_animation_frame(move || {
|
||||
#[allow(unused_variables)]
|
||||
if let Err(e) = router.navigate_from_route(&to, &options) {
|
||||
leptos::debug_warn!("use_navigate error: {e:?}");
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::borrow::Cow;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn expand_optionals(pattern: &str) -> Vec<Cow<str>> {
|
||||
pub fn expand_optionals(pattern: &str) -> Vec<Cow<'_, str>> {
|
||||
use js_sys::RegExp;
|
||||
use once_cell::unsync::Lazy;
|
||||
use wasm_bindgen::JsValue;
|
||||
@@ -58,7 +58,7 @@ pub fn expand_optionals(pattern: &str) -> Vec<Cow<str>> {
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn expand_optionals(pattern: &str) -> Vec<Cow<str>> {
|
||||
pub fn expand_optionals(pattern: &str) -> Vec<Cow<'_, str>> {
|
||||
use regex::Regex;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
|
||||
@@ -555,14 +555,22 @@ where
|
||||
let status = resp.status();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let status = status.as_u16();
|
||||
if (500..=599).contains(&status) {
|
||||
if (400..=599).contains(&status) {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let status_text = resp.status_text();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let status_text = status.to_string();
|
||||
return Err(serde_json::from_str(&text)
|
||||
.unwrap_or(ServerFnError::ServerError(status_text)));
|
||||
return Err(match serde_json::from_str(&text) {
|
||||
Ok(e) => e,
|
||||
Err(_) => {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let status_text = resp.status_text();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let status_text = status.to_string();
|
||||
ServerFnError::ServerError(if text.is_empty() {
|
||||
format!("{} {}", status, status_text)
|
||||
} else {
|
||||
format!("{} {}: {}", status, status_text, text)
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Decoding the body of the request
|
||||
@@ -582,12 +590,6 @@ where
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let binary = binary.as_ref();
|
||||
|
||||
if status == 400 {
|
||||
return Err(ServerFnError::ServerError(
|
||||
"No server function was found at this URL.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
ciborium::de::from_reader(binary)
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
} else {
|
||||
@@ -596,10 +598,6 @@ where
|
||||
.await
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
|
||||
|
||||
if status == 400 {
|
||||
return Err(ServerFnError::ServerError(text));
|
||||
}
|
||||
|
||||
let mut deserializer = JSONDeserializer::from_str(&text);
|
||||
T::deserialize(&mut deserializer)
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
|
||||
Reference in New Issue
Block a user