Compare commits

..

32 Commits

Author SHA1 Message Date
Greg Johnston
bee9bd8f67 0.5.0-beta2 2023-08-29 21:23:59 -04:00
Greg Johnston
8d3874f8a9 cargo fmt 2023-08-29 21:19:24 -04:00
Einherjar
bade16d227 docs: discuss unique paths for #[server] functions (#1610) 2023-08-29 20:49:31 -04:00
Jon Cahill
e0a132bde3 fix: don't try to parse as JSON the result from a server function redirect (#1604) 2023-08-29 20:42:19 -04:00
Daniel Oliveira
4d7e1f4d26 feat: improve server function client side error handling (#1597)
Handle all error codes 401-499 in addition to the
400 and 500-599 that were already handled.

In addition, handle them all in the same way
and improve the error message.
2023-08-29 20:40:03 -04:00
Maneren
700eee6604 fix(macro/params): clippy warning (#1612) 2023-08-29 20:31:54 -04:00
Joseph Cruz
694ed61e4c fix(ci): add new webkit dependency (#1607) (#1608) 2023-08-28 11:08:47 -04:00
Joseph Cruz
d7330097ba chore(examples): improve cucumber support #1598 (#1599)
* chore(examples): add cucumber runner

* chore(examples): clean cargo recursively
2023-08-28 11:08:22 -04:00
Greg Johnston
c65a3a6ca3 docs: add docs for builder syntax (#1603) 2023-08-28 11:08:07 -04:00
Danik Vitek
793c191619 feat: Oco (Owned Clones Once) smart pointer (#1480) 2023-08-26 11:43:51 -04:00
Greg Johnston
6c3e2fe53e feat: update to typed-builder 0.16 (closes #1455) (#1590) 2023-08-26 10:10:42 -04:00
Greg Johnston
08c419e3ee fix: broken test with untrack in tracing props (#1593) 2023-08-26 09:20:14 -04:00
Greg Johnston
736f4185b5 Merge pull request #1588 from leptos-rs/1457
Some resource and transition fixes
2023-08-26 07:34:21 -04:00
Greg Johnston
9cc0fc8c49 fix: adjust tracing properties 2023-08-26 07:24:52 -04:00
Greg Johnston
8f067dcde7 chore: clear release-mode warnings 2023-08-25 17:16:00 -04:00
Greg Johnston
ad6eb58fe1 fix: <Transition/> fallback in CSR 2023-08-25 17:12:01 -04:00
Greg Johnston
3f3ab1c3c8 remove unnecessary parens 2023-08-25 16:49:26 -04:00
Greg Johnston
9adae32847 examples: improve hackernews behavior 2023-08-25 16:00:47 -04:00
Greg Johnston
b8098e7992 fix: <Transition/> fallback on non-initial page loads 2023-08-25 16:00:47 -04:00
Greg Johnston
bef4d0dd3b fix: resource loading signal pattern for subsequent hydration page loads 2023-08-25 16:00:47 -04:00
Matt Cuneo
a789100e22 feat: allow autoreload websocket connection to work outside of localhost (#1548)
* Updated client reloading to use window.location.protocol/host to determine websocket connection. Added optional config reload_external_port to provide further control of the client websocket connection. These changes allow reloading while accessing the served site from outside of localhost.
2023-08-25 15:54:22 -04:00
Greg Johnston
abeca70625 fix: correct logic for resource loading signal when read outside suspense (#1586) 2023-08-25 11:46:54 -04:00
rkuklik
cc293b1170 feat: generic event handler types to make it easier to create collections of event handlers (#1444) 2023-08-25 11:41:16 -04:00
Greg Johnston
8ab62c17c6 feat: add Fn traits for resources on nightly (#1587) 2023-08-25 11:20:29 -04:00
Joseph Cruz
cf14e857ca refactor(check-stable): use matrix (#1543) (#1583)
* refactor(check-stable): use matrix

* chore: simulate leptos change

* chore: remove simulated change
2023-08-25 10:30:00 -04:00
Greg Johnston
c322ef38fd feat: signal traits should take associated types instead of generics (#1578) 2023-08-25 10:29:24 -04:00
Greg Johnston
c9cc493063 fix: fourth argument to server functions (#1585) 2023-08-25 10:28:54 -04:00
Joseph Cruz
fb48f7f117 fix(counters_stable): restore wasm tests (#1581) (#1582) 2023-08-24 16:33:01 -04:00
尹吉峰
c344e54cf6 feat: return an Effect from create_effect that can be disposed (#1571) 2023-08-24 10:24:10 -04:00
Greg Johnston
7306ecccbc feat: make struct name and path optional for server functions (#1573) 2023-08-24 10:22:35 -04:00
Greg Johnston
b98174db7a feat: support passing signals directly as attributes, classes, styles, and props on stable (#1577) 2023-08-24 10:22:14 -04:00
Greg Johnston
e48f66694d fix: runtime disposal time in render_to_string_async (#1574) 2023-08-24 10:22:00 -04:00
93 changed files with 2616 additions and 792 deletions

View File

@@ -26,3 +26,4 @@ jobs:
with:
directory: ${{ matrix.directory }}
cargo_make_task: "check"
toolchain: nightly

View File

@@ -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

View File

@@ -29,3 +29,4 @@ jobs:
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly

View File

@@ -41,3 +41,4 @@ jobs:
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly

View File

@@ -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 }}

View File

@@ -23,3 +23,4 @@ jobs:
with:
directory: ${{ matrix.directory }}
cargo_make_task: "ci"
toolchain: nightly

2
.gitignore vendored
View File

@@ -9,3 +9,5 @@ Cargo.lock
.idea
.direnv
.envrc
.vscode

View File

@@ -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

View File

@@ -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)

View File

@@ -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 well 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).)
- Youll 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 well 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 didnt 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 its very important to remember. **Server functions are not magic; theyre syntax sugar for defining a public API.** The _body_ of a server function is never made public; its just part of your server binary. But the server function is a publicly accessible API endpoint, and its return value is just a JSON or similar blob. You should _never_ return something sensitive from a server function.

View File

@@ -0,0 +1,98 @@
# No Macros: The View Builder Syntax
> If youre perfectly happy with the `view!` macro syntax described so far, youre 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 dont 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 youd rather not use the `view` macro, you can simply use that expanded syntax yourself. And its 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, its 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 thats 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 cant have additional builder methods applied to it without causing inconsistencies.
> 2. Using the builder syntax will result in less-than-optimal SSR performance. It wont be slow, by any means (and its worth running your own benchmarks in any case), just slower than the `view`-optimized version.

View File

@@ -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"

View File

@@ -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 = '''

View File

@@ -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)]

View File

@@ -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 = [

View File

@@ -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" },
]

View File

@@ -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>

View 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>
}
}

View File

@@ -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>
}
}

View 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);
}

View 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);
}

View 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);
}

View 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);
}

View 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();
}
}

View File

@@ -0,0 +1 @@
pub mod counters_page;

View 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);
}

View 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);

View 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);
}

View 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)");
}

View File

@@ -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>

View File

@@ -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>
}
}

View File

@@ -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>

View File

@@ -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>
}
}

View File

@@ -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.

View File

@@ -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();

View File

@@ -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 }

View File

@@ -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>(

View File

@@ -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,

View File

@@ -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()))
}
}

View File

@@ -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") {

View File

@@ -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"] }

View File

@@ -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,

View File

@@ -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));
}

View File

@@ -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);
}

View File

@@ -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(),

View File

@@ -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"))]

View File

@@ -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
},

View File

@@ -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| {

View File

@@ -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,

View File

@@ -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()
}

View File

@@ -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()

View File

@@ -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;

View File

@@ -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>);

View File

@@ -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;

View File

@@ -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>);

View File

@@ -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");

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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!(),

View File

@@ -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()
}

View File

@@ -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"

View File

@@ -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);
}

View File

@@ -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
///

View File

@@ -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()

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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};

View File

@@ -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,

View File

@@ -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(),

View File

@@ -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::{

View File

@@ -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
View 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());
}
}

View File

@@ -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()
}
}

View File

@@ -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"))]

View File

@@ -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 signals 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 signals 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(

View File

@@ -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 signals value without
/// - [`.with()`](#impl-SignalWith-for-Signal<T>) allows you to reactively access the signals value without
/// cloning by applying a callback function.
/// - [`.with_untracked()`](#impl-SignalWithUntracked<T>-for-Signal<T>) allows you to access the signals
/// 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 signals value without
/// - [`.with()`](#impl-SignalWith-for-MaybeSignal<T>) allows you to reactively access the signals value without
/// cloning by applying a callback function.
/// - [`.with_untracked()`](#impl-SignalWithUntracked<T>-for-MaybeSignal<T>) allows you to access the signals
/// 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 signals value without
/// - [`.with()`](#impl-SignalWith-for-MaybeProp<T>) allows you to reactively access the signals value without
/// cloning by applying a callback function.
/// - [`.with_untracked()`](#impl-SignalWithUntracked<T>-for-MaybeProp<T>) allows you to access the signals
/// 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(

View File

@@ -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 => {}

View File

@@ -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.

View File

@@ -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(

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.5.0"
version = "0.5.0-beta2"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -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! {

View File

@@ -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({

View File

@@ -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({

View File

@@ -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({

View File

@@ -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
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.5.0"
version = "0.5.0-beta2"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -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"),

View File

@@ -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")]

View File

@@ -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.

View File

@@ -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

View File

@@ -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:?}");
}

View File

@@ -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! {

View File

@@ -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()))