Compare commits

..

35 Commits

Author SHA1 Message Date
Greg Johnston
b0f745f4a9 fix: escape </script> and other HTML tags in serialized resources 2023-03-28 20:34:54 -04:00
Ben Wishovich
e6b1298915 feat: add property field to Meta component (#759) 2023-03-28 09:10:00 -04:00
Igor Shevchenko
98a9ec8335 chore(docs): fix a few typos (#756) 2023-03-27 20:06:34 -04:00
jquesada2016
5329561687 feat: add is_mounted and dyn_classes (#714) 2023-03-27 19:03:59 -04:00
Greg Johnston
89ca047f2f examples: improve counter_without_macros (#751) 2023-03-27 12:50:01 -04:00
Greg Johnston
a94711fcf0 fix: correct typecast on Memo::get_untracked (closes issue #754) (#755) 2023-03-27 11:28:40 -04:00
Greg Johnston
97d88c65ae docs: warn when reading resource outside <Suspense/> (closes issue #742) (#743) 2023-03-25 14:22:22 -04:00
Jessie Chatham Spencer
e482e3748d docs: document cargo workspace feature resolver footgun (#745)
Due to no rust edition being present in a workspac's Cargo.toml, non
WASM compatible code can end up being built for a WASM target.

This commit documents this error and how to resolve it.
2023-03-25 07:34:28 -04:00
István Donkó
8ab9c08448 docs: fix typo in server_fn docs (#740) 2023-03-24 21:42:27 -04:00
Lachlan Wilger
56de70b714 docs: fix typo (#739)
There was a typo in the section of the docs that pointed towards the hackernews example, so I fixed it by add the word "application."
2023-03-24 21:41:59 -04:00
Greg Johnston
38d97babd8 fix: always run dynamic classes after static classes (closes #735) (#738) 2023-03-24 17:38:34 -04:00
martin frances
4cfecb5d82 chore: bump serde-lite from 0.3 to 0.4. (#737) 2023-03-24 16:54:20 -04:00
Michael Clayton
08b5970b2b check EventSource value for Ok to avoid unwrap panic (#732) 2023-03-23 18:41:18 -04:00
Greg Johnston
af20f80b2b docs: fix typo in router docs (#730) 2023-03-22 20:44:58 -04:00
Andrew Chang-DeWitt
c2fdd2cd70 fix: include missing query params in navigation when <ActionForm/> receives a redirect (#728)
Previous solution in #727 included manually inserted `?` when a leading
`?` is present automatically in `Url.search`.
2023-03-22 20:05:21 -04:00
Greg Johnston
286f3eebe4 fix: relative routing should update when navigating between <Outlet/>s (closes issue #725) (#729)
* clear some cruft out of the navigation code
* fix issue #725 (correctly reactively resolving paths)
2023-03-22 19:59:08 -04:00
Álvaro Mondéjar
509223ab2e chore: Upgrade console_log to stable (#724) 2023-03-22 18:21:53 -04:00
Greg Johnston
665b0b8ed2 chore: make wasm-bindgen dependency optional in leptos_reactive (#723) 2023-03-22 17:56:52 -04:00
Greg Johnston
508ad52582 chore: fix clippy warnings (#721)
* `v0.2.4`

* chore: fix clippy warnings
2023-03-21 18:20:29 -04:00
martin frances
cfd5c98f97 clippy: simplify Box::pin() call. (#718) 2023-03-21 09:06:31 -04:00
Greg Johnston
2e63bb1f50 fix: <Transition/> behavior (#717) 2023-03-21 09:05:41 -04:00
Greg Johnston
982c8f6b5a docs: small fixes (#715) 2023-03-20 20:43:04 -04:00
Greg Johnston
12c4c115f3 docs: make is_odd less pretentious 2023-03-20 20:42:46 -04:00
Carlton Gibson
d4d20ecdb0 Used modulo rather than bitwise & for is_odd check.
The modulo operator is less of a head-scratcher for folks coming through here. The bitwise & is equally correct (clearly) but is likely to cause confusion if folks don't immediately see what's going on.
2023-03-20 20:09:02 +01:00
Greg Johnston
b78919c6ed Merge pull request #712 from leptos-rs/warnings 2023-03-20 10:49:00 -04:00
Greg Johnston
abb9320e31 chore: clear warning and add exports of helpers with handles 2023-03-20 09:36:14 -04:00
Greg Johnston
875d2d5a3a chore: handle unbounded_send warnings 2023-03-20 09:33:58 -04:00
Greg Johnston
42a58855a0 feat: add Scope::batch() (#711) 2023-03-20 08:29:18 -04:00
Greg Johnston
9d142758ec feat: allow manual signal disposal before the scope is disposed (#710) 2023-03-19 21:40:16 -04:00
Greg Johnston
2faddd85cb feat: add set_interval_with_handle and deprecate set_interval (#709) 2023-03-19 16:45:22 -04:00
martin frances
ddd463748d clippy: less .clone() calls, simpler pointer passing. (#707) 2023-03-19 15:30:12 -04:00
Alexis Fontaine
71ee4cd09d fix: view! macro not compiling with a non-default scope name (#704) 2023-03-19 13:14:47 -04:00
Greg Johnston
08c56f7d6c feat: add a debounce helper for event listeners (#691) 2023-03-19 07:10:56 -04:00
Elliot Waite
e1ba26b62c feat: add request_animation_frame_with_handle and request_idle_callback_with_handle (#698) 2023-03-18 19:09:36 -04:00
Greg Johnston
309f0bf826 fix: ignore view markers in DynChild hydration (closes issue #697) (#703) 2023-03-18 19:01:56 -04:00
73 changed files with 733 additions and 248 deletions

View File

@@ -25,22 +25,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.2.3"
version = "0.2.4"
[workspace.dependencies]
leptos = { path = "./leptos", default-features = false, version = "0.2.3" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.3" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.3" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.3" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.3" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.3" }
server_fn = { path = "./server_fn", default-features = false, version = "0.2.3" }
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.3" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.3" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.3" }
leptos_router = { path = "./router", version = "0.2.3" }
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.3" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.3" }
leptos = { path = "./leptos", default-features = false, version = "0.2.4" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.4" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.4" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.4" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.4" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.4" }
server_fn = { path = "./server_fn", default-features = false, version = "0.2.4" }
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.4" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.4" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.4" }
leptos_router = { path = "./router", version = "0.2.4" }
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.4" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.4" }
[profile.release]
codegen-units = 1

View File

@@ -61,3 +61,19 @@ view! {
<input prop:value=a on:input=on_input />
}
```
## Build configuration
### Cargo feature resolution in workspaces
A new [version](https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions) of Cargo's feature resolver was introduced for the 2021 edition of Rust.
For single crate projects it will select a resolver version based on the Rust edition in `Cargo.toml`. As there is no Rust edition present for `Cargo.toml` in a workspace, Cargo will default to the pre 2021 edition resolver.
This can cause issues resulting in non WASM compatible code being built for a WASM target. Seeing `mio` failing to build is often a sign that none WASM compatible code is being included in the build.
The resolver version can be set in the workspace `Cargo.toml` to remedy this issue.
```toml
[workspace]
members = ["member1", "member2"]
resolver = "2"
```

View File

@@ -84,7 +84,7 @@ fn FancyMath(cx: Scope) -> impl IntoView {
This kind of “provide a signal in a parent, consume it in a child” should be familiar
from the chapter on [parent-child interactions](./view/08_parent_child.md). The same
pattern you use to communicate between parents and children works for grandparents and
grandchildren, or any ancestors and descendents: in other words, between “global” state
grandchildren, or any ancestors and descendants: in other words, between “global” state
in the root component of your app and any other components anywhere else in the app.
Because of the fine-grained nature of updates, this is usually all you need. However,

View File

@@ -34,6 +34,9 @@
- [`cargo-leptos`]()
- [Hydration Footguns]()
- [Request/Response]()
- [Extractors]()
- [Axum]()
- [Actix]()
- [Headers]()
- [Cookies]()
- [Server Functions]()
@@ -42,3 +45,4 @@
- [`<ActionForm/>`s]()
- [Turning off WebAssembly]()
- [Advanced Reactivity]()
- [Appendix: Optimizing WASM Binary Size]()

View File

@@ -69,4 +69,4 @@ Every time one of the resources is reloading, the `"Loading..."` fallback will s
This inversion of the flow of control makes it easier to add or remove individual resources, as you dont need to handle the matching yourself. It also unlocks some massive performance improvements during server-side rendering, which well talk about during a later chapter.
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
<iframe src="https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>

View File

@@ -4,6 +4,6 @@ Youll notice in the `<Suspense/>` example that if you keep reloading the data
`<Transition/>` behaves exactly the same as `<Suspense/>`, but instead of falling back every time, it only shows the fallback the first time. On all subsequent loads, it continues showing the old data until the new data are ready. This can be really handy to prevent the flickering effect, and to allow users to continue interacting with your application.
This example shows how you can create a simple tabbed contact list with `<Transition/>`. When you select a new tab, it continues showing the current contact until the new data laods. This can be a much better user experience than constantly falling back to a loading message.
This example shows how you can create a simple tabbed contact list with `<Transition/>`. When you select a new tab, it continues showing the current contact until the new data loads. This can be a much better user experience than constantly falling back to a loading message.
<iframe src="https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>

View File

@@ -58,8 +58,8 @@ let id = move || {
The untyped versions return `Memo<ParamsMap>`. Again, its memo to react to changes in the URL. [`ParamsMap`](https://docs.rs/leptos_router/0.2.3/leptos_router/struct.ParamsMap.html) behaves a lot like any other map type, with a `.get()` method that returns `Option<&String>`.
```rust
let params = use_params::<ContactParams>(cx);
let query = use_query::<ContactSearch>(cx);
let params = use_params_map(cx);
let query = use_query_map(cx);
// id: || -> Option<String>
let id = move || {

View File

@@ -16,7 +16,7 @@ The Leptos Router works with the path and query (`/blog/search?q=Search`). Given
## The Philosophy
In most cases, the path should drive what is displayed on the page. From the users perspective, for most appliations, most major changes in the state of the app should be reflected in the URL. If you copy and paste the URL and open it in another tab, you should find yourself more or less in the same place.
In most cases, the path should drive what is displayed on the page. From the users perspective, for most applications, most major changes in the state of the app should be reflected in the URL. If you copy and paste the URL and open it in another tab, you should find yourself more or less in the same place.
In this sense, the router is really at the heart of the global state management for your application. More than anything else, it drives what is displayed on the page.

View File

@@ -30,7 +30,7 @@ Now lets say Id like to update the list of CSS classes on this element dyn
For example, lets say I want to add the class `red` when the count is odd. I can
do this using the `class:` syntax.
```rust
class:red=move || count() & 1 == 1
class:red=move || count() % 2 == 1
```
`class:` attributes take
1. the class name, following the colon (`red`)

View File

@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos" }
console_log = "0.2"
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"

View File

@@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
broadcaster = "1"
console_log = "0.2"
console_log = "1"
console_error_panic_hook = "0.1"
futures = "0.3"
cfg-if = "1"

View File

@@ -198,13 +198,13 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
let s = create_signal_from_stream(
cx,
source.subscribe("message").unwrap().map(|value| {
value
.expect("no message event")
.1
.data()
.as_string()
.expect("expected string value")
}),
match value {
Ok(value) => {
value.1.data().as_string().expect("expected string value")
},
Err(_) => "0".to_string(),
}
})
);
on_cleanup(cx, move || source.close());

View File

@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos", features = ["stable"] }
console_log = "0.2"
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"

View File

@@ -1,48 +1,44 @@
use leptos::{ev, html::*, *};
pub struct Props {
/// The starting value for the counter
pub initial_value: i32,
/// The change that should be applied each time the button is clicked.
pub step: i32,
}
/// A simple counter view.
pub fn view(cx: Scope, props: Props) -> impl IntoView {
let Props {
initial_value,
step,
} = props;
// A component is really just a function call: it runs once to create the DOM and reactive system
pub fn counter(cx: Scope, initial_value: i32, step: i32) -> impl IntoView {
let (value, set_value) = create_signal(cx, initial_value);
// elements are created by calling a function with a Scope argument
// the function name is the same as the HTML tag name
div(cx)
.child((
cx,
// children can be added with .child()
// this takes any type that implements IntoView as its argument
// for example, a string or an HtmlElement<_>
.child(
button(cx)
// typed events found in leptos::ev
// 1) prevent typos in event names
// 2) allow for correct type inference in callbacks
.on(ev::click, move |_| set_value.update(|value| *value = 0))
.child((cx, "Clear")),
))
.child((
cx,
.child("Clear"),
)
.child(
button(cx)
.on(ev::click, move |_| {
set_value.update(|value| *value -= step)
})
.child((cx, "-1")),
))
.child((
cx,
.child("-1"),
)
.child(
span(cx)
.child((cx, "Value: "))
.child("Value: ")
// reactive values are passed to .child() as a tuple
// (Scope, [child function]) so an effect can be created
.child((cx, move || value.get()))
.child((cx, "!")),
))
.child((
cx,
.child("!"),
)
.child(
button(cx)
.on(ev::click, move |_| {
set_value.update(|value| *value += step)
})
.child((cx, "+1")),
))
.child("+1"),
)
}

View File

@@ -1,16 +1,8 @@
use counter_without_macros as counter;
use counter_without_macros::counter;
use leptos::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
counter::view(
cx,
counter::Props {
initial_value: 0,
step: 1,
},
)
})
mount_to_body(|cx| counter(cx, 0, 1))
}

View File

@@ -6,7 +6,7 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos" }
log = "0.4"
console_log = "0.2"
console_log = "1"
console_error_panic_hook = "0.1.7"
[dev-dependencies]

View File

@@ -6,7 +6,7 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos", features = ["stable"] }
log = "0.4"
console_log = "0.2"
console_log = "1"
console_error_panic_hook = "0.1.7"
[dev-dependencies]

View File

@@ -5,6 +5,6 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos" }
console_log = "0.2"
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"

View File

@@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "0.2.0"
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
leptos = { path = "../../../leptos/leptos", default-features = false, features = [

View File

@@ -70,8 +70,8 @@ pub fn ExampleErrors(cx: Scope) -> impl IntoView {
</p>
<p>"The following <div> will always contain an error and cause this page to produce status 500. Check browser dev tools. "</p>
<div>
// note that the error boundries could be placed above in the Router or lower down
// in a particular route. The generated errors on the entire page contribue to the
// note that the error boundaries could be placed above in the Router or lower down
// in a particular route. The generated errors on the entire page contribute to the
// final status code sent by the server when producing ssr pages.
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
<ReturnsError/>

View File

@@ -9,7 +9,7 @@ leptos = { path = "../../leptos" }
reqwasm = "0.5.0"
serde = { version = "1", features = ["derive"] }
log = "0.4"
console_log = "0.2"
console_log = "1"
console_error_panic_hook = "0.1.7"
[dev-dependencies]

View File

@@ -9,7 +9,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_log = "0.2"
console_log = "1"
console_error_panic_hook = "0.1"
cfg-if = "1"
leptos = { path = "../../leptos", default-features = false, features = [

View File

@@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "0.2.0"
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
cfg-if = "1.0.0"
leptos = { path = "../../leptos", default-features = false, features = [

View File

@@ -12,7 +12,7 @@ leptos_router = { version = "0.2.0-alpha2", features = ["stable", "csr"] }
log = "0.4"
console_error_panic_hook = "0.1"
console_log = "0.2"
console_log = "1"
gloo-net = "0.2"
gloo-storage = "0.2"
serde = "1.0"

View File

@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos" }
console_log = "0.2"
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"
web-sys = "0.3"

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
console_log = "0.2"
console_log = "1"
log = "0.4"
leptos = { path = "../../leptos" }
leptos_router = { path = "../../router", features = ["csr"] }

View File

@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1.0.66"
console_log = "0.2.0"
console_log = "1.0.0"
rand = { version = "0.8.5", features = ["min_const_gen"], optional = true }
console_error_panic_hook = "0.1.7"
futures = "0.3.25"

View File

@@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["macros"] }
console_error_panic_hook = "0.1"
console_log = "0.2"
console_log = "1"
cfg-if = "1"
lazy_static = "1"
leptos = { path = "../../leptos", default-features = false, features = [

View File

@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
console_error_panic_hook = "0.1"
console_log = "0.2"
console_log = "1"
cfg-if = "1"
lazy_static = "1"
leptos = { path = "../../leptos", default-features = false, features = [

View File

@@ -22,7 +22,7 @@ cfg-if = "1.0"
# dependecies for client (enable when csr or hydrate set)
wasm-bindgen = { version = "0.2", optional = true }
console_log = { version = "0.2", optional = true }
console_log = { version = "1", optional = true }
console_error_panic_hook = { version = "0.1", optional = true }
# dependecies for server (enable when ssr set)

View File

@@ -16,6 +16,6 @@ gloo-net = { version = "0.2", features = ["http"] }
# dependecies for client (enable when csr or hydrate set)
wasm-bindgen = { version = "0.2" }
console_log = { version = "0.2"}
console_log = { version = "1"}
console_error_panic_hook = { version = "0.1"}

View File

@@ -11,7 +11,7 @@ actix-files = { version = "0.6.2", optional = true }
actix-web = { version = "4.2.1", optional = true, features = ["macros"] }
anyhow = "1.0.68"
broadcaster = "1.0.0"
console_log = "0.2.0"
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
serde = { version = "1.0.152", features = ["derive"] }
futures = "0.3.25"

View File

@@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "0.2.0"
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
cfg-if = "1.0.0"

View File

@@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
console_log = "0.2.0"
console_log = "1.0.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
cfg-if = "1.0.0"

View File

@@ -6,7 +6,7 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos", default-features = false }
log = "0.4"
console_log = "0.2"
console_log = "1"
console_error_panic_hook = "0.1.7"
uuid = { version = "1", features = ["v4", "js", "serde"] }
serde = { version = "1", features = ["derive"] }

View File

@@ -94,7 +94,7 @@ impl ResponseOptions {
}
}
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`,
/// it sets a [StatusCode] of 302 and a [LOCATION](header::LOCATION) header with the provided value.
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead.
pub fn redirect(cx: leptos::Scope, path: &str) {
@@ -340,7 +340,7 @@ where
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before

View File

@@ -95,7 +95,7 @@ impl ResponseOptions {
}
}
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`,
/// it sets a StatusCode of 302 and a LOCATION header with the provided value.
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead
pub fn redirect(cx: leptos::Scope, path: &str) {
@@ -128,7 +128,7 @@ pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
/// Decomposes an HTTP request into its parts, allowing you to read its headers
/// and other data without consuming the body. Creates a new Request from the
/// original parts for further processsing
/// original parts for further processing
pub async fn generate_request_and_parts(
req: Request<Body>,
) -> (Request<Body>, RequestParts) {
@@ -148,7 +148,7 @@ pub async fn generate_request_and_parts(
}
/// A struct to hold the http::request::Request and allow users to take ownership of it
/// Requred by Request not being Clone. See this issue for eventual resolution: https://github.com/hyperium/http/pull/574
/// Required by Request not being Clone. See this issue for eventual resolution: https://github.com/hyperium/http/pull/574
#[derive(Debug, Default)]
pub struct LeptosRequest<B>(Arc<RwLock<Option<Request<B>>>>);
@@ -198,8 +198,8 @@ impl<B> LeptosRequest<B> {
}
}
/// Generate a wrapper for the http::Request::Request type that allows one to
/// processs it, access the body, and use axum Extractors on it.
/// Requred by Request not being Clone. See this issue for eventual resolution: https://github.com/hyperium/http/pull/574
/// process it, access the body, and use axum Extractors on it.
/// Required by Request not being Clone. See this issue for eventual resolution: https://github.com/hyperium/http/pull/574
pub async fn generate_leptos_request<B>(req: Request<B>) -> LeptosRequest<B>
where
B: Default + std::fmt::Debug,
@@ -495,7 +495,7 @@ where
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
@@ -735,7 +735,7 @@ async fn forward_stream(
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network

View File

@@ -10,7 +10,7 @@ pub fn html_parts(
let pkg_path = &options.site_pkg_dir;
let output_name = &options.output_name;
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to maintain compatibility with it's default options
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
let mut wasm_output_name = output_name.clone();

View File

@@ -90,7 +90,7 @@ impl ResponseOptions {
}
}
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
/// Provides an easy way to redirect the user from within a server function. Mimicking the Remix `redirect()`,
/// it sets a StatusCode of 302 and a LOCATION header with the provided value.
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead
pub fn redirect(cx: leptos::Scope, path: &str) {
@@ -385,7 +385,7 @@ where
/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application.
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
@@ -617,7 +617,7 @@ async fn forward_stream(
/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an in-order HTML stream of your application.
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve befores
/// This stream will pause at each `<Suspense/>` node and wait for it to resolve before
/// sending down its HTML. The app will become interactive once it has fully loaded.
///
/// This version allows us to pass Viz State/Extractor or other infro from Viz or network

View File

@@ -56,7 +56,7 @@
//! - [`hackernews`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews)
//! and [`hackernews_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews_axum)
//! integrate calls to a real external REST API, routing, server-side rendering and hydration to create
//! a fully-functional that works as intended even before WASM has loaded and begun to run.
//! a fully-functional application that works as intended even before WASM has loaded and begun to run.
//! - [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite),
//! [`todo_app_sqlite_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_axum), and
//! [`todo_app_sqlite_viz`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_viz)
@@ -150,12 +150,15 @@ pub use leptos_config::{self, get_configuration, LeptosOptions};
pub mod ssr {
pub use leptos_dom::{ssr::*, ssr_in_order::*};
}
#[allow(deprecated)]
pub use leptos_dom::{
self, create_node_ref, debug_warn, document, error, ev,
helpers::{
event_target, event_target_checked, event_target_value,
request_animation_frame, request_idle_callback, set_interval,
set_timeout, window_event_listener,
request_animation_frame, request_animation_frame_with_handle,
request_idle_callback, request_idle_callback_with_handle, set_interval,
set_interval_with_handle, set_timeout, set_timeout_with_handle,
window_event_listener,
},
html, log, math, mount_to, mount_to_body, svg, warn, window, Attribute,
Class, Errors, Fragment, HtmlElement, IntoAttribute, IntoClass,

View File

@@ -126,7 +126,7 @@ where
}
);
// return the fallback for now, wrapped in fragment identifer
// return the fallback for now, wrapped in fragment identifier
fallback().into_view(cx)
}
};

View File

@@ -116,9 +116,12 @@ where
let suspense_context = use_context::<SuspenseContext>(cx)
.expect("there to be a SuspenseContext");
if is_first_run(&first_run, &suspense_context) {
let has_local_only = suspense_context.has_local_only();
if cfg!(feature = "hydrate") || !first_run.get() {
*prev_children.borrow_mut() = Some(frag.nodes.clone());
}
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 {
first_run.set(false);
}
@@ -138,17 +141,21 @@ fn is_first_run(
first_run: &Rc<Cell<bool>>,
suspense_context: &SuspenseContext,
) -> bool {
match (
first_run.get(),
cfg!(feature = "hydrate"),
suspense_context.has_local_only(),
) {
(false, _, _) => false,
// is in hydrate mode, and has non-local resources (so, has streamed)
(_, false, false) => false,
// is in hydrate mode, but with only local resources (so, has not streamed)
(_, false, true) => true,
// either SSR or client mode: it's the first run
(_, true, _) => true,
if cfg!(feature = "csr") {
false
} else {
match (
first_run.get(),
cfg!(feature = "hydrate"),
suspense_context.has_local_only(),
) {
(false, _, _) => false,
// SSR and has non-local resources (so, has streamed)
(_, false, false) => false,
// SSR but with only local resources (so, has not streamed)
(_, false, true) => true,
// hydrate: it's the first run
(_, true, _) => true,
}
}
}

View File

@@ -34,6 +34,11 @@ leptos = { path = "../leptos" }
[dependencies.web-sys]
version = "0.3"
features = [
"DocumentFragment",
"Element",
"HtmlTemplateElement",
"NodeList",
"Window",
"console",
"Comment",
"Document",

View File

@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
leptos = { path = "../../leptos" }
console_log = "0.2"
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"

View File

@@ -1,6 +1,7 @@
//! A variety of DOM utility functions.
use crate::{is_server, window};
use leptos_reactive::{on_cleanup, Scope};
use std::time::Duration;
use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};
@@ -80,10 +81,33 @@ pub fn event_target_checked(ev: &web_sys::Event) -> bool {
.checked()
}
/// Runs the given function between the next repaint
/// using [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
/// Handle that is generated by [request_animation_frame_with_handle] and can
/// be used to cancel the animation frame request.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct AnimationFrameRequestHandle(i32);
impl AnimationFrameRequestHandle {
/// Cancels the animation frame request to which this refers.
/// See [`cancelAnimationFrame()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelAnimationFrame)
pub fn cancel(&self) {
_ = window().cancel_animation_frame(self.0);
}
}
/// Runs the given function between the next repaint using
/// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
_ = request_animation_frame_with_handle(cb);
}
/// Runs the given function between the next repaint using
/// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame),
/// returning a cancelable handle.
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
pub fn request_animation_frame_with_handle(
cb: impl FnOnce() + 'static,
) -> Result<AnimationFrameRequestHandle, JsValue> {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
@@ -95,13 +119,38 @@ pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
}
let cb = Closure::once_into_js(cb);
_ = window().request_animation_frame(cb.as_ref().unchecked_ref());
window()
.request_animation_frame(cb.as_ref().unchecked_ref())
.map(AnimationFrameRequestHandle)
}
/// Queues the given function during an idle period
/// using [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback).
/// Handle that is generated by [request_idle_callback_with_handle] and can be
/// used to cancel the idle callback.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct IdleCallbackHandle(u32);
impl IdleCallbackHandle {
/// Cancels the idle callback to which this refers.
/// See [`cancelAnimationFrame()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelIdleCallback)
pub fn cancel(&self) {
window().cancel_idle_callback(self.0);
}
}
/// Queues the given function during an idle period using
/// [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback).
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
pub fn request_idle_callback(cb: impl Fn() + 'static) {
_ = request_idle_callback_with_handle(cb);
}
/// Queues the given function during an idle period using
/// [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback),
/// returning a cancelable handle.
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
pub fn request_idle_callback_with_handle(
cb: impl Fn() + 'static,
) -> Result<IdleCallbackHandle, JsValue> {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
@@ -113,7 +162,21 @@ pub fn request_idle_callback(cb: impl Fn() + 'static) {
}
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
_ = window().request_idle_callback(cb.as_ref().unchecked_ref());
window()
.request_idle_callback(cb.as_ref().unchecked_ref())
.map(IdleCallbackHandle)
}
/// Handle that is generated by [set_timeout_with_handle] and can be used to clear the timeout.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct TimeoutHandle(i32);
impl TimeoutHandle {
/// Cancels the timeout to which this refers.
/// See [`clearTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout)
pub fn clear(&self) {
window().clear_timeout_with_handle(self.0);
}
}
/// Executes the given function after the given duration of time has passed.
@@ -123,6 +186,19 @@ pub fn request_idle_callback(cb: impl Fn() + 'static) {
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
_ = set_timeout_with_handle(cb, duration);
}
/// Executes the given function after the given duration of time has passed, returning a cancelable handle.
/// [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout).
#[cfg_attr(
debug_assertions,
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
pub fn set_timeout_with_handle(
cb: impl FnOnce() + 'static,
duration: Duration,
) -> Result<TimeoutHandle, JsValue> {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
@@ -134,10 +210,83 @@ pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
}
let cb = Closure::once_into_js(Box::new(cb) as Box<dyn FnOnce()>);
_ = window().set_timeout_with_callback_and_timeout_and_arguments_0(
cb.as_ref().unchecked_ref(),
duration.as_millis().try_into().unwrap_throw(),
);
window()
.set_timeout_with_callback_and_timeout_and_arguments_0(
cb.as_ref().unchecked_ref(),
duration.as_millis().try_into().unwrap_throw(),
)
.map(TimeoutHandle)
}
/// "Debounce" a callback function. This will cause it to wait for a period of `delay`
/// after it is called. If it is called again during that period, it will wait
/// `delay` before running, and so on. This can be used, for example, to wrap event
/// listeners to prevent them from firing constantly as you type.
///
/// ```
/// use leptos::{leptos_dom::helpers::debounce, *};
///
/// #[component]
/// fn DebouncedButton(cx: Scope) -> impl IntoView {
/// let delay = std::time::Duration::from_millis(250);
/// let on_click = debounce(cx, delay, move |_| {
/// log!("...so many clicks!");
/// });
///
/// view! { cx,
/// <button on:click=on_click>"Click me"</button>
/// }
/// }
/// ```
pub fn debounce<T: 'static>(
cx: Scope,
delay: Duration,
mut cb: impl FnMut(T) + 'static,
) -> impl FnMut(T) {
use std::{
cell::{Cell, RefCell},
rc::Rc,
};
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move |value| {
let _guard = span.enter();
cb(value);
};
}
}
let cb = Rc::new(RefCell::new(cb));
let timer = Rc::new(Cell::new(None::<TimeoutHandle>));
on_cleanup(cx, {
let timer = Rc::clone(&timer);
move || {
if let Some(timer) = timer.take() {
timer.clear();
}
}
});
move |arg| {
if let Some(timer) = timer.take() {
timer.clear();
}
let handle = set_timeout_with_handle(
{
let cb = Rc::clone(&cb);
move || {
cb.borrow_mut()(arg);
}
},
delay,
);
if let Ok(handle) = handle {
timer.set(Some(handle));
}
}
}
/// Handle that is generated by [set_interval] and can be used to clear the interval.
@@ -152,12 +301,16 @@ impl IntervalHandle {
}
}
/// Repeatedly calls the given function, with a delay of the given duration between calls.
/// Repeatedly calls the given function, with a delay of the given duration between calls,
/// returning a cancelable handle.
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
#[cfg_attr(
debug_assertions,
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
#[deprecated = "use set_interval_with_handle() instead. In the future, \
set_interval() will no longer return a handle, for consistency \
with other timer helper functions."]
pub fn set_interval(
cb: impl Fn() + 'static,
duration: Duration,
@@ -181,6 +334,36 @@ pub fn set_interval(
Ok(IntervalHandle(handle))
}
/// Repeatedly calls the given function, with a delay of the given duration between calls,
/// returning a cancelable handle.
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
#[cfg_attr(
debug_assertions,
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
pub fn set_interval_with_handle(
cb: impl Fn() + 'static,
duration: Duration,
) -> Result<IntervalHandle, JsValue> {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
let _guard = span.enter();
cb();
};
}
}
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
let handle = window()
.set_interval_with_callback_and_timeout_and_arguments_0(
cb.as_ref().unchecked_ref(),
duration.as_millis().try_into().unwrap_throw(),
)?;
Ok(IntervalHandle(handle))
}
/// Adds an event listener to the `Window`.
#[cfg_attr(
debug_assertions,

View File

@@ -74,13 +74,13 @@ pub trait ElementDescriptor: ElementDescriptorBounds {
/// The name of the element, i.e., `div`, `p`, `custom-element`.
fn name(&self) -> Cow<'static, str>;
/// Determains if the tag is void, i.e., `<input>` and `<br>`.
/// Determines if the tag is void, i.e., `<input>` and `<br>`.
fn is_void(&self) -> bool {
false
}
/// A unique `id` that should be generated for each new instance of
/// this element, and be consistant for both SSR and CSR.
/// this element, and be consistent for both SSR and CSR.
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
fn hydration_id(&self) -> &HydrationKey;
}
@@ -573,6 +573,23 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
self
}
/// Checks to see if this element is mounted to the DOM as a child
/// of `body`.
///
/// This method will always return [`None`] on non-wasm CSR targets.
pub fn is_mounted(&self) -> bool {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
crate::document()
.body()
.unwrap()
.contains(Some(self.element.as_ref()))
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
false
}
/// Adds an attribute to this element.
#[track_caller]
pub fn attr(
@@ -679,6 +696,108 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
this
}
/// Sets the class on the element as the class signal changes.
#[track_caller]
pub fn dyn_classes<I, C>(
self,
classes_signal: impl Fn() -> I + 'static,
) -> Self
where
I: IntoIterator<Item = C>,
C: Into<Cow<'static, str>>,
{
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
use smallvec::SmallVec;
let class_list = self.element.as_ref().class_list();
leptos_reactive::create_effect(
self.cx,
move |prev_classes: Option<
SmallVec<[Cow<'static, str>; 4]>,
>| {
let classes = classes_signal()
.into_iter()
.map(Into::into)
.collect::<SmallVec<[Cow<'static, str>; 4]>>(
);
let mut new_classes = classes
.iter()
.flat_map(|classes| classes.split_whitespace());
if let Some(prev_classes) = prev_classes {
let mut old_classes = prev_classes
.iter()
.flat_map(|classes| classes.split_whitespace());
// Remove old classes
for prev_class in old_classes.clone() {
if !new_classes.any(|c| c == prev_class) {
class_list.remove_1(prev_class).unwrap_or_else(
|err| {
panic!(
"failed to add class \
`{prev_class}`, error: {err:#?}"
)
},
);
}
}
// Add new classes
for class in new_classes {
if !old_classes.any(|c| c == class) {
class_list.add_1(class).unwrap_or_else(|err| {
panic!(
"failed to remove class `{class}`, \
error: {err:#?}"
)
});
}
}
} else {
let new_classes = new_classes
.map(ToOwned::to_owned)
.collect::<SmallVec<[_; 4]>>();
for class in &new_classes {
class_list.add_1(class).unwrap_or_else(|err| {
panic!(
"failed to add class `{class}`, error: \
{err:#?}"
)
});
}
}
classes
},
);
self
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
let mut this = self;
let this = classes_signal()
.into_iter()
.map(Into::into)
.flat_map(|classes| {
classes
.split_whitespace()
.map(ToString::to_string)
.collect::<SmallVec<[_; 4]>>()
})
.fold(this, |this, class| this.class(class, true));
this
}
}
/// Sets a property on an element.
#[track_caller]
pub fn prop(

View File

@@ -322,7 +322,7 @@ impl Element {
cx,
element,
attrs,
children: children.clone(),
children,
#[cfg(debug_assertions)]
view_marker,
}

View File

@@ -509,12 +509,14 @@ pub(crate) fn render_serializers(
) -> impl Stream<Item = String> {
serializers.map(|(id, json)| {
let id = serde_json::to_string(&id).unwrap();
let json = json.replace('<', "\\u003c");
format!(
r#"<script>
var val = {json:?};
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
__LEPTOS_RESOURCE_RESOLVERS.get({id})({json:?})
__LEPTOS_RESOURCE_RESOLVERS.get({id})(val)
}} else {{
__LEPTOS_RESOLVED_RESOURCES.set({id}, {json:?});
__LEPTOS_RESOLVED_RESOURCES.set({id}, val);
}}
</script>"#,
)

View File

@@ -126,17 +126,15 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
}
#[async_recursion(?Send)]
async fn handle_chunks(
mut tx: UnboundedSender<String>,
chunks: Vec<StreamChunk>,
) {
async fn handle_chunks(tx: UnboundedSender<String>, chunks: Vec<StreamChunk>) {
let mut buffer = String::new();
for chunk in chunks {
match chunk {
StreamChunk::Sync(sync) => buffer.push_str(&sync),
StreamChunk::Async(suspended) => {
// add static HTML before the Suspense and stream it down
tx.unbounded_send(std::mem::take(&mut buffer));
tx.unbounded_send(std::mem::take(&mut buffer))
.expect("failed to send async HTML chunk");
// send the inner stream
let suspended = suspended.await;
@@ -145,7 +143,8 @@ async fn handle_chunks(
}
}
// send final sync chunk
tx.unbounded_send(std::mem::take(&mut buffer));
tx.unbounded_send(std::mem::take(&mut buffer))
.expect("failed to send final HTML chunk");
}
impl View {

View File

@@ -130,6 +130,7 @@ impl ToTokens for Model {
let mut body = body.to_owned();
body.sig.ident = format_ident!("__{}", body.sig.ident);
#[allow(clippy::redundant_clone)] // false positive
let body_name = body.sig.ident.clone();
let (_, generics, where_clause) = body.sig.generics.split_for_impl();
@@ -153,6 +154,7 @@ impl ToTokens for Model {
if cfg!(feature = "tracing") {
(
quote! {
#[allow(clippy::let_with_type_underscore)]
#[cfg_attr(
debug_assertions,
::leptos::leptos_dom::tracing::instrument(level = "trace", name = #trace_name, skip_all)

View File

@@ -351,7 +351,7 @@ fn root_element_to_tokens_ssr(
quote! {
{
#(#exprs_for_compiler)*
::leptos::HtmlElement::from_chunks(cx, #full_name, [#(#chunks),*])#view_marker
::leptos::HtmlElement::from_chunks(#cx, #full_name, [#(#chunks),*])#view_marker
}
}
}
@@ -365,6 +365,7 @@ enum SsrElementChunks {
View(TokenStream),
}
#[allow(clippy::too_many_arguments)]
fn element_to_tokens_ssr(
cx: &Ident,
node: &NodeElement,
@@ -384,7 +385,7 @@ fn element_to_tokens_ssr(
})
}
chunks.push(SsrElementChunks::View(quote! {
{#component}.into_view(cx)
{#component}.into_view(#cx)
}));
} else {
let tag_name = node
@@ -441,7 +442,7 @@ fn element_to_tokens_ssr(
let value = inner_html.as_ref();
holes.push(quote! {
(#value).into_attribute(cx).as_nameless_value_string().unwrap_or_default()
(#value).into_attribute(#cx).as_nameless_value_string().unwrap_or_default()
})
} else {
for child in &node.children {
@@ -485,7 +486,7 @@ fn element_to_tokens_ssr(
})
}
chunks.push(SsrElementChunks::View(quote! {
{#value}.into_view(cx)
{#value}.into_view(#cx)
}));
}
}
@@ -667,7 +668,7 @@ fn set_class_attribute_ssr(
template.push_str(" {}");
let value = value.as_ref();
holes.push(quote! {
&(cx, #value).into_attribute(#cx).as_nameless_value_string()
&(#cx, #value).into_attribute(#cx).as_nameless_value_string()
.map(|a| leptos::leptos_dom::ssr::escape_attr(&a).to_string())
.unwrap_or_default()
});
@@ -677,7 +678,7 @@ fn set_class_attribute_ssr(
for (_span, name, value) in &class_attrs {
template.push_str(" {}");
holes.push(quote! {
(cx, #value).into_class(#cx).as_value_string(#name)
(#cx, #value).into_class(#cx).as_value_string(#name)
});
}
@@ -817,7 +818,22 @@ fn element_to_tokens(
};
let attrs = node.attributes.iter().filter_map(|node| {
if let Node::Attribute(node) = node {
Some(attribute_to_tokens(cx, node))
if node.key.to_string().trim().starts_with("class:") {
None
} else {
Some(attribute_to_tokens(cx, node))
}
} else {
None
}
});
let class_attrs = node.attributes.iter().filter_map(|node| {
if let Node::Attribute(node) = node {
if node.key.to_string().trim().starts_with("class:") {
Some(attribute_to_tokens(cx, node))
} else {
None
}
} else {
None
}
@@ -875,6 +891,7 @@ fn element_to_tokens(
quote! {
#name
#(#attrs)*
#(#class_attrs)*
#global_class_expr
#(#children)*
#view_marker

View File

@@ -20,7 +20,7 @@ fn Component(
#[test]
fn component() {
let cp = ComponentProps::builder().into("").strip_option(9).build();
assert_eq!(cp.optional, false);
assert!(!cp.optional);
assert_eq!(cp.optional_no_strip, None);
assert_eq!(cp.strip_option, Some(9));
assert_eq!(cp.default, NonZeroUsize::new(10).unwrap());

View File

@@ -10,9 +10,9 @@ description = "Reactive system for the Leptos web framework."
[dependencies]
slotmap = { version = "1", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde-lite = { version = "0.3", optional = true }
serde-lite = { version = "0.4", optional = true }
futures = { version = "0.3" }
js-sys = "0.3"
js-sys = { version = "0.3", optional = true }
miniserde = { version = "0.1", optional = true }
rkyv = { version = "0.7.39", features = [
"validation",
@@ -31,17 +31,17 @@ base64 = "0.21"
thiserror = "1"
tokio = { version = "1", features = ["rt"], optional = true }
tracing = "0.1"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3", features = [
wasm-bindgen = { version = "0.2", optional = true }
wasm-bindgen-futures = { version = "0.4", optional = true }
web-sys = { version = "0.3", optional = true, features = [
"DocumentFragment",
"Element",
"HtmlTemplateElement",
"NodeList",
"Window",
] }
cfg-if = "1.0.0"
indexmap = "1.9.2"
cfg-if = "1"
indexmap = "1"
[dev-dependencies]
log = "0.4"
@@ -50,8 +50,18 @@ leptos = { path = "../leptos" }
[features]
default = []
csr = []
hydrate = []
csr = [
"dep:js-sys",
"dep:wasm-bindgen",
"dep:wasm-bindgen-futures",
"dep:web-sys",
]
hydrate = [
"dep:js-sys",
"dep:wasm-bindgen",
"dep:wasm-bindgen-futures",
"dep:web-sys",
]
ssr = ["dep:tokio"]
stable = []
serde = []

View File

@@ -1,8 +1,8 @@
#![forbid(unsafe_code)]
use crate::{
create_effect, node::NodeId, on_cleanup, with_runtime, AnyComputation,
RuntimeId, Scope, SignalGet, SignalGetUntracked, SignalStream, SignalWith,
SignalWithUntracked,
RuntimeId, Scope, SignalDispose, SignalGet, SignalGetUntracked,
SignalStream, SignalWith, SignalWithUntracked,
};
use std::{any::Any, cell::RefCell, fmt::Debug, marker::PhantomData, rc::Rc};
@@ -191,7 +191,8 @@ impl<T: Clone> SignalGetUntracked<T> for Memo<T> {
)]
fn get_untracked(&self) -> T {
with_runtime(self.runtime, move |runtime| {
match self.id.try_with_no_subscription(runtime, T::clone) {
let f = move |maybe_value: &Option<T>| maybe_value.clone().unwrap();
match self.id.try_with_no_subscription(runtime, f) {
Ok(t) => t,
Err(_) => panic_getting_dead_memo(
#[cfg(debug_assertions)]
@@ -412,6 +413,12 @@ impl<T: Clone> SignalStream<T> for Memo<T> {
}
}
impl<T> SignalDispose for Memo<T> {
fn dispose(self) {
_ = with_runtime(self.runtime, |runtime| runtime.dispose_node(self.id));
}
}
impl_get_fn_traits![Memo];
pub(crate) struct MemoState<T, F>

View File

@@ -15,6 +15,7 @@ use std::{
fmt::Debug,
future::Future,
marker::PhantomData,
panic::Location,
pin::Pin,
rc::Rc,
};
@@ -377,13 +378,15 @@ where
///
/// If you want to get the value without cloning it, use [Resource::with].
/// (`value.read(cx)` is equivalent to `value.with(cx, T::clone)`.)
#[track_caller]
pub fn read(&self, cx: Scope) -> Option<T>
where
T: Clone,
{
let location = std::panic::Location::caller();
with_runtime(self.runtime, |runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
resource.read(cx)
resource.read(cx, location)
})
})
.ok()
@@ -397,10 +400,12 @@ where
///
/// If you want to get the value by cloning it, you can use
/// [Resource::read].
#[track_caller]
pub fn with<U>(&self, cx: Scope, f: impl FnOnce(&T) -> U) -> Option<U> {
let location = std::panic::Location::caller();
with_runtime(self.runtime, |runtime| {
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
resource.with(cx, f)
resource.with(cx, f, location)
})
})
.ok()
@@ -563,14 +568,25 @@ where
S: Clone + 'static,
T: 'static,
{
pub fn read(&self, cx: Scope) -> Option<T>
#[track_caller]
pub fn read(
&self,
cx: Scope,
location: &'static Location<'static>,
) -> Option<T>
where
T: Clone,
{
self.with(cx, T::clone)
self.with(cx, T::clone, location)
}
pub fn with<U>(&self, cx: Scope, f: impl FnOnce(&T) -> U) -> Option<U> {
#[track_caller]
pub fn with<U>(
&self,
cx: Scope,
f: impl FnOnce(&T) -> U,
location: &'static Location<'static>,
) -> Option<U> {
let suspense_cx = use_context::<SuspenseContext>(cx);
let v = self
@@ -587,6 +603,19 @@ where
if serializable {
suspense_cx.has_local_only.set_value(false);
}
} else {
#[cfg(all(feature = "hydrate", debug_assertions))]
crate::macros::debug_warn!(
"At {location}, you are reading a resource in `hydrate` mode \
outside a <Suspense/> or <Transition/>. This can cause \
hydration mismatch errors and loses out on a significant \
performance optimization. To fix this issue, you can either: \
\n1. Wrap the place where you read the resource in a \
<Suspense/> or <Transition/> component, or \n2. Switch to \
using create_local_resource(), which will wait to load the \
resource until the app is hydrated on the client side. (This \
will have worse performance in most cases.)",
);
}
let increment = move |_: Option<()>| {

View File

@@ -62,6 +62,7 @@ pub(crate) struct Runtime {
RefCell<SecondaryMap<NodeId, RefCell<FxIndexSet<NodeId>>>>,
pub pending_effects: RefCell<Vec<NodeId>>,
pub resources: RefCell<SlotMap<ResourceId, AnyResource>>,
pub batching: Cell<bool>,
}
// This core Runtime impl block handles all the work of marking and updating
@@ -186,12 +187,12 @@ impl Runtime {
let current_observer = self.observer.get();
// mark self dirty
if let Some(mut current_node) = nodes.get_mut(node) {
if let Some(current_node) = nodes.get_mut(node) {
Runtime::mark(
node,
&mut current_node,
current_node,
ReactiveNodeState::Dirty,
&mut *pending_effects,
&mut pending_effects,
current_observer,
);
@@ -200,10 +201,10 @@ impl Runtime {
let mut descendants = Default::default();
Runtime::gather_descendants(&subscribers, node, &mut descendants);
for descendant in descendants {
if let Some(mut node) = nodes.get_mut(descendant) {
if let Some(node) = nodes.get_mut(descendant) {
Runtime::mark(
descendant,
&mut node,
node,
ReactiveNodeState::Check,
&mut pending_effects,
current_observer,
@@ -248,12 +249,22 @@ impl Runtime {
pub(crate) fn run_effects(runtime_id: RuntimeId) {
_ = with_runtime(runtime_id, |runtime| {
let effects = runtime.pending_effects.take();
for effect_id in effects {
runtime.update_if_necessary(effect_id);
}
runtime.run_your_effects();
});
}
pub(crate) fn run_your_effects(&self) {
let effects = self.pending_effects.take();
for effect_id in effects {
self.update_if_necessary(effect_id);
}
}
pub(crate) fn dispose_node(&self, node: NodeId) {
self.node_sources.borrow_mut().remove(node);
self.node_subscribers.borrow_mut().remove(node);
self.nodes.borrow_mut().remove(node);
}
}
impl Debug for Runtime {

View File

@@ -414,7 +414,7 @@ impl Scope {
/// The set of all HTML fragments currently pending.
///
/// The keys are hydration IDs. Valeus are tuples of two pinned
/// The keys are hydration IDs. Values are tuples of two pinned
/// `Future`s that return content for out-of-order and in-order streaming, respectively.
pub fn pending_fragments(
&self,
@@ -442,6 +442,25 @@ impl Scope {
.ok()
.flatten()
}
/// Batches any reactive updates, preventing effects from running until the whole
/// function has run. This allows you to prevent rerunning effects if multiple
/// signal updates might cause the same effect to run.
///
/// # Panics
/// Panics if the runtime this scope belongs to has already been disposed.
pub fn batch<T>(&self, f: impl FnOnce() -> T) -> T {
with_runtime(self.runtime, move |runtime| {
runtime.batching.set(true);
let val = f();
runtime.batching.set(false);
runtime.run_your_effects();
val
})
.expect(
"tried to run a batched update in a runtime that has been disposed",
)
}
}
impl fmt::Debug for ScopeDisposer {

View File

@@ -274,6 +274,16 @@ pub trait SignalStream<T> {
fn to_stream(&self, cx: Scope) -> Pin<Box<dyn Stream<Item = T>>>;
}
/// This trait allows disposing a signal before its [Scope] has been disposed.
pub trait SignalDispose {
/// Disposes of the signal. This:
/// 1. Detaches the signal from the reactive graph, preventing it from triggering
/// further updates; and
/// 2. Drops the value contained in the signal.
#[track_caller]
fn dispose(self);
}
/// Creates a signal, the basic reactive primitive.
///
/// A signal is a piece of data that may change over time,
@@ -725,6 +735,12 @@ impl<T: Clone> SignalStream<T> for ReadSignal<T> {
}
}
impl<T> SignalDispose for ReadSignal<T> {
fn dispose(self) {
_ = with_runtime(self.runtime, |runtime| runtime.dispose_node(self.id));
}
}
impl<T> ReadSignal<T>
where
T: 'static,
@@ -1025,6 +1041,12 @@ impl<T> SignalSet<T> for WriteSignal<T> {
}
}
impl<T> SignalDispose for WriteSignal<T> {
fn dispose(self) {
_ = with_runtime(self.runtime, |runtime| runtime.dispose_node(self.id));
}
}
impl<T> Clone for WriteSignal<T> {
fn clone(&self) -> Self {
Self {
@@ -1598,6 +1620,12 @@ impl<T: Clone> SignalStream<T> for RwSignal<T> {
}
}
impl<T> SignalDispose for RwSignal<T> {
fn dispose(self) {
_ = with_runtime(self.runtime, |runtime| runtime.dispose_node(self.id));
}
}
impl<T> RwSignal<T> {
/// Returns a read-only handle to the signal.
///
@@ -1884,7 +1912,7 @@ impl NodeId {
runtime.mark_dirty(*self);
// notify subscribers
if updated.is_some() {
if updated.is_some() && !runtime.batching.get() {
Runtime::run_effects(runtime_id);
};
updated

View File

@@ -1,7 +1,7 @@
#[cfg(not(feature = "stable"))]
use leptos_reactive::{
create_isomorphic_effect, create_memo, create_runtime, create_scope,
create_signal,
create_isomorphic_effect, create_memo, create_runtime, create_rw_signal,
create_scope, create_signal, SignalSet,
};
#[cfg(not(feature = "stable"))]
@@ -91,3 +91,45 @@ fn untrack_mutes_effect() {
})
.dispose()
}
#[cfg(not(feature = "stable"))]
#[test]
fn batching_actually_batches() {
use std::{cell::Cell, rc::Rc};
create_scope(create_runtime(), |cx| {
let first_name = create_rw_signal(cx, "Greg".to_string());
let last_name = create_rw_signal(cx, "Johnston".to_string());
// simulate an arbitrary side effect
let count = Rc::new(Cell::new(0));
create_isomorphic_effect(cx, {
let count = count.clone();
move |_| {
_ = first_name();
_ = last_name();
count.set(count.get() + 1);
}
});
// runs once initially
assert_eq!(count.get(), 1);
// individual updates run effect once each
first_name.set("Alice".to_string());
assert_eq!(count.get(), 2);
last_name.set("Smith".to_string());
assert_eq!(count.get(), 3);
// batched effect only runs twice
cx.batch(move || {
first_name.set("Bob".to_string());
last_name.set("Williams".to_string());
});
assert_eq!(count.get(), 4);
})
.dispose()
}

View File

@@ -286,7 +286,7 @@ where
let pending = create_rw_signal(cx, false);
let action_fn = Rc::new(move |input: &I| {
let fut = action_fn(input);
Box::pin(async move { fut.await }) as Pin<Box<dyn Future<Output = O>>>
Box::pin(fut) as Pin<Box<dyn Future<Output = O>>>
});
Action(store_value(

View File

@@ -48,7 +48,7 @@
//! # run_scope(create_runtime(), |cx| {
//! spawn_local(async {
//! let posts = read_posts(3, "my search".to_string()).await;
//! log::debug!("posts = {posts{:#?}");
//! log::debug!("posts = {posts:#?}");
//! })
//! # });
//!

View File

@@ -297,7 +297,7 @@ where
let submissions = create_rw_signal(cx, Vec::new());
let action_fn = Rc::new(move |input: &I| {
let fut = action_fn(input);
Box::pin(async move { fut.await }) as Pin<Box<dyn Future<Output = O>>>
Box::pin(fut) as Pin<Box<dyn Future<Output = O>>>
});
MultiAction(store_value(

View File

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

View File

@@ -30,6 +30,9 @@ pub fn Meta(
/// The [`name`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-name) attribute.
#[prop(optional, into)]
name: Option<TextProp>,
/// The [`property`](https://ogp.me/) attribute.
#[prop(optional, into)]
property: Option<TextProp>,
/// The [`http-equiv`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-http-equiv) attribute.
#[prop(optional, into)]
http_equiv: Option<TextProp>,
@@ -45,6 +48,7 @@ pub fn Meta(
leptos::leptos_dom::html::meta(cx)
.attr("charset", move || charset.as_ref().map(|v| v.get()))
.attr("name", move || name.as_ref().map(|v| v.get()))
.attr("property", move || property.as_ref().map(|v| v.get()))
.attr("http-equiv", move || http_equiv.as_ref().map(|v| v.get()))
.attr("content", move || content.as_ref().map(|v| v.get()))
});

View File

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

View File

@@ -120,7 +120,10 @@ where
Ok(url) => {
request_animation_frame(move || {
if let Err(e) = navigate(
&url.pathname,
&format!(
"{}{}",
url.pathname, url.search,
),
Default::default(),
) {
warn!("{}", e);

View File

@@ -3,10 +3,7 @@ use crate::{
ParamsMap, RouterContext, SsrMode,
};
use leptos::{leptos_dom::Transparent, *};
use std::{
cell::{Cell, RefCell},
rc::Rc,
};
use std::{cell::Cell, rc::Rc};
thread_local! {
static ROUTE_ID: Cell<usize> = Cell::new(0);
@@ -121,7 +118,7 @@ impl RouteContext {
id,
base_path: base,
child: Box::new(child),
path: RefCell::new(path),
path: create_rw_signal(cx, path),
original_path: route.original_path.to_string(),
params,
outlet: Box::new(move |cx| Some(element(cx))),
@@ -144,11 +141,11 @@ impl RouteContext {
/// e.g., this will return `/article/0` rather than `/article/:id`.
/// For the opposite behavior, see [RouteContext::original_path].
pub fn path(&self) -> String {
self.inner.path.borrow().to_string()
self.inner.path.get_untracked()
}
pub(crate) fn set_path(&mut self, path: String) {
*self.inner.path.borrow_mut() = path;
pub(crate) fn set_path(&self, path: String) {
self.inner.path.set(path);
}
/// Returns the original URL path of the current route,
@@ -176,7 +173,7 @@ impl RouteContext {
id: 0,
base_path: path.to_string(),
child: Box::new(|_| None),
path: RefCell::new(path.to_string()),
path: create_rw_signal(cx, path.to_string()),
original_path: path.to_string(),
params: create_memo(cx, |_| ParamsMap::new()),
outlet: Box::new(move |cx| {
@@ -188,7 +185,16 @@ impl RouteContext {
/// Resolves a relative route, relative to the current route's path.
pub fn resolve_path(&self, to: &str) -> Option<String> {
resolve_path(&self.inner.base_path, to, Some(&self.inner.path.borrow()))
resolve_path(
&self.inner.base_path,
to,
Some(&self.inner.path.get_untracked()),
)
.map(String::from)
}
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)
}
@@ -208,7 +214,7 @@ pub(crate) struct RouteContextInner {
base_path: String,
pub(crate) id: usize,
pub(crate) child: Box<dyn Fn(Scope) -> Option<RouteContext>>,
pub(crate) path: RefCell<String>,
pub(crate) path: RwSignal<String>,
pub(crate) original_path: String,
pub(crate) params: Memo<ParamsMap>,
pub(crate) outlet: Box<dyn Fn(Scope) -> Option<View>>,

View File

@@ -221,55 +221,36 @@ impl RouterContextInner {
if resolved_to != this.reference.get()
|| options.state != (this.state).get()
{
if cfg!(feature = "server") {
self.history.navigate(&LocationChange {
value: resolved_to,
{
self.referrers.borrow_mut().push(LocationChange {
value: self.reference.get(),
replace: options.replace,
scroll: options.scroll,
state: options.state.clone(),
state: self.state.get(),
});
} else {
{
self.referrers.borrow_mut().push(
LocationChange {
value: self.reference.get(),
replace: options.replace,
scroll: options.scroll,
state: self.state.get(),
},
);
}
let len = self.referrers.borrow().len();
}
let len = self.referrers.borrow().len();
#[cfg(feature = "transition")]
let transition = use_transition(self.cx);
//transition.start({
let set_reference = self.set_reference;
let set_state = self.set_state;
let referrers = self.referrers.clone();
let this = Rc::clone(&self);
//move || {
let set_reference = self.set_reference;
let set_state = self.set_state;
let referrers = self.referrers.clone();
let this = Rc::clone(&self);
let resolved = resolved_to.to_string();
let state = options.state.clone();
queue_microtask(move || {
set_reference.update(move |r| *r = resolved);
let resolved = resolved_to.to_string();
let state = options.state.clone();
set_reference.update(move |r| *r = resolved);
set_state.update({
let next_state = state.clone();
move |state| *state = next_state
});
if referrers.borrow().len() == len {
this.navigate_end(LocationChange {
value: resolved_to.to_string(),
replace: false,
scroll: true,
state,
})
//}
}
});
//});
set_state.update({
let next_state = state.clone();
move |state| *state = next_state
});
if referrers.borrow().len() == len {
this.navigate_end(LocationChange {
value: resolved_to,
replace: false,
scroll: true,
state,
})
}
}

View File

@@ -88,7 +88,7 @@ pub fn Routes(
if next_match.route.key == prev_match.route.key
&& next_match.route.id == prev_match.route.id =>
{
let mut prev_one = { prev.borrow()[i].clone() };
let prev_one = { prev.borrow()[i].clone() };
if next_match.path_match.path != prev_one.path() {
prev_one
.set_path(next_match.path_match.path.clone());

View File

@@ -70,7 +70,7 @@ pub fn use_resolved_path(
if path.starts_with('/') {
Some(path)
} else {
route.resolve_path(&path).map(String::from)
route.resolve_path_tracked(&path).map(String::from)
}
})
}

View File

@@ -29,7 +29,7 @@ cfg_if! {
}
#[test]
fn test_complext_query_string() {
fn test_complex_query_string() {
let url = Url::try_from("http://leptos.com?data=Data%3A+%24+%26+%2B%2B+7").unwrap();
assert_params_map!{
["data" => "Data: $ & ++ 7"],

View File

@@ -49,7 +49,7 @@
//! # async fn main() {
//! async {
//! let posts = read_posts(3, "my search".to_string()).await;
//! log::debug!("posts = {posts{:#?}");
//! log::debug!("posts = {posts:#?}");
//! }
//! # }
//!

View File

@@ -14,7 +14,7 @@ use syn::{
*,
};
/// Discribes the custom context from the server that passed to the server function. Optionally, the first argument of a server function
/// Describes the custom context from the server that passed to the server function. Optionally, the first argument of a server function
/// can be a custom context of this type. This context can be used to access the server's state within the server function.
pub struct ServerContext {
/// The type of the context.