Compare commits

..

32 Commits

Author SHA1 Message Date
Greg Johnston
5d23af8581 chore: update Cargo.lock 2026-01-02 14:13:27 -05:00
Greg Johnston
1d268ab3a9 fix: merge error with missing references 2026-01-02 14:13:17 -05:00
Greg Johnston
55b3752983 change: migrate to serde_qs 1.0 (#4237) 2026-01-02 14:09:22 -05:00
zakstucke
53299e4599 change: make ScopedFuture internals private (#4274) 2026-01-02 14:04:53 -05:00
Alexis Fontaine
2d6731e508 change: remove redundant render_app_async_stream_with_context method from leptos_axum (#4309) 2026-01-02 14:01:51 -05:00
Álvaro Mondéjar Rubio
d8b371b26b change: remove _with_handle suffix from set_interval, set_timeout, request_animation_frame, and request_idle_callback (#4388) 2026-01-02 14:00:22 -05:00
Azriel Hoh
8326e514c2 feat/change: support ToggleEvent for on:toggle. (#4362) 2026-01-02 13:56:12 -05:00
Greg Johnston
6eebd71868 change: add lazy feature for lazy hydration to avoid binary-size cost (#4438) 2026-01-02 13:55:00 -05:00
Greg Johnston
dd507168fa chore: update syntax in COMMON_BUGS.md doc
Updated documentation to reflect changes in signal handling and effects in Rust.
2026-01-02 12:35:50 -05:00
Greg Johnston
8438b1633f chore: remove outdated bug from "common bugs" 2026-01-02 12:34:32 -05:00
Saber Haj Rabiee
b87c310046 fix: auto re-connect dev websocket live reload on close (#4517) 2026-01-02 12:30:59 -05:00
dependabot[bot]
692153ded2 chore(deps): bump the rust-dependencies group across 1 directory with 23 updates (#4515)
Bumps the rust-dependencies group with 20 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [serde_json](https://github.com/serde-rs/json) | `1.0.145` | `1.0.148` |
| [proc-macro2](https://github.com/dtolnay/proc-macro2) | `1.0.103` | `1.0.104` |
| [axum](https://github.com/tokio-rs/axum) | `0.8.7` | `0.8.8` |
| [tempfile](https://github.com/Stebalien/tempfile) | `3.23.0` | `3.24.0` |
| [rmp-serde](https://github.com/3Hren/msgpack-rust) | `1.3.0` | `1.3.1` |
| [reqwest](https://github.com/seanmonstar/reqwest) | `0.12.26` | `0.12.28` |
| [insta](https://github.com/mitsuhiko/insta) | `1.45.0` | `1.45.1` |
| [async-lock](https://github.com/smol-rs/async-lock) | `3.4.1` | `3.4.2` |
| [axum-core](https://github.com/tokio-rs/axum) | `0.5.5` | `0.5.6` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.50` | `1.2.51` |
| [derive_more](https://github.com/JelteF/derive_more) | `2.1.0` | `2.1.1` |
| [iri-string](https://github.com/lo48576/iri-string) | `0.7.9` | `0.7.10` |
| [itoa](https://github.com/dtolnay/itoa) | `1.0.15` | `1.0.17` |
| [miniserde](https://github.com/dtolnay/miniserde) | `0.1.43` | `0.1.45` |
| [rmp](https://github.com/3Hren/msgpack-rust) | `0.8.14` | `0.8.15` |
| [ryu](https://github.com/dtolnay/ryu) | `1.0.20` | `1.0.22` |
| [serde_spanned](https://github.com/toml-rs/toml) | `1.0.3` | `1.0.4` |
| [signal-hook-registry](https://github.com/vorner/signal-hook) | `1.4.7` | `1.4.8` |
| [toml_datetime](https://github.com/toml-rs/toml) | `0.7.3` | `0.7.5+spec-1.1.0` |
| [toml_edit](https://github.com/toml-rs/toml) | `0.23.7` | `0.23.10+spec-1.0.0` |



Updates `serde_json` from 1.0.145 to 1.0.148
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.145...v1.0.148)

Updates `proc-macro2` from 1.0.103 to 1.0.104
- [Release notes](https://github.com/dtolnay/proc-macro2/releases)
- [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.103...1.0.104)

Updates `axum` from 0.8.7 to 0.8.8
- [Release notes](https://github.com/tokio-rs/axum/releases)
- [Changelog](https://github.com/tokio-rs/axum/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/axum/compare/axum-v0.8.7...axum-v0.8.8)

Updates `tempfile` from 3.23.0 to 3.24.0
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.23.0...v3.24.0)

Updates `rmp-serde` from 1.3.0 to 1.3.1
- [Release notes](https://github.com/3Hren/msgpack-rust/releases)
- [Commits](https://github.com/3Hren/msgpack-rust/commits)

Updates `reqwest` from 0.12.26 to 0.12.28
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.26...v0.12.28)

Updates `insta` from 1.45.0 to 1.45.1
- [Release notes](https://github.com/mitsuhiko/insta/releases)
- [Changelog](https://github.com/mitsuhiko/insta/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mitsuhiko/insta/compare/1.45.0...1.45.1)

Updates `async-lock` from 3.4.1 to 3.4.2
- [Release notes](https://github.com/smol-rs/async-lock/releases)
- [Changelog](https://github.com/smol-rs/async-lock/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/async-lock/compare/v3.4.1...v3.4.2)

Updates `axum-core` from 0.5.5 to 0.5.6
- [Release notes](https://github.com/tokio-rs/axum/releases)
- [Changelog](https://github.com/tokio-rs/axum/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/axum/compare/axum-core-v0.5.5...axum-core-v0.5.6)

Updates `cc` from 1.2.50 to 1.2.51
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.50...cc-v1.2.51)

Updates `derive_more` from 2.1.0 to 2.1.1
- [Release notes](https://github.com/JelteF/derive_more/releases)
- [Changelog](https://github.com/JelteF/derive_more/blob/master/CHANGELOG.md)
- [Commits](https://github.com/JelteF/derive_more/compare/v2.1.0...v2.1.1)

Updates `derive_more-impl` from 2.1.0 to 2.1.1
- [Release notes](https://github.com/JelteF/derive_more/releases)
- [Changelog](https://github.com/JelteF/derive_more/blob/master/CHANGELOG.md)
- [Commits](https://github.com/JelteF/derive_more/compare/v2.1.0...v2.1.1)

Updates `find-msvc-tools` from 0.1.5 to 0.1.6
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/find-msvc-tools-v0.1.5...find-msvc-tools-v0.1.6)

Updates `iri-string` from 0.7.9 to 0.7.10
- [Changelog](https://github.com/lo48576/iri-string/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/lo48576/iri-string/compare/v0.7.9...v0.7.10)

Updates `itoa` from 1.0.15 to 1.0.17
- [Release notes](https://github.com/dtolnay/itoa/releases)
- [Commits](https://github.com/dtolnay/itoa/compare/1.0.15...1.0.17)

Updates `miniserde` from 0.1.43 to 0.1.45
- [Release notes](https://github.com/dtolnay/miniserde/releases)
- [Commits](https://github.com/dtolnay/miniserde/compare/0.1.43...0.1.45)

Updates `rmp` from 0.8.14 to 0.8.15
- [Release notes](https://github.com/3Hren/msgpack-rust/releases)
- [Commits](https://github.com/3Hren/msgpack-rust/commits)

Updates `rustix` from 1.1.2 to 1.1.3
- [Release notes](https://github.com/bytecodealliance/rustix/releases)
- [Changelog](https://github.com/bytecodealliance/rustix/blob/main/CHANGES.md)
- [Commits](https://github.com/bytecodealliance/rustix/compare/v1.1.2...v1.1.3)

Updates `ryu` from 1.0.20 to 1.0.22
- [Release notes](https://github.com/dtolnay/ryu/releases)
- [Commits](https://github.com/dtolnay/ryu/compare/1.0.20...1.0.22)

Updates `serde_spanned` from 1.0.3 to 1.0.4
- [Commits](https://github.com/toml-rs/toml/compare/serde_spanned-v1.0.3...serde_spanned-v1.0.4)

Updates `signal-hook-registry` from 1.4.7 to 1.4.8
- [Changelog](https://github.com/vorner/signal-hook/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vorner/signal-hook/compare/registry-v1.4.7...registry-v1.4.8)

Updates `toml_datetime` from 0.7.3 to 0.7.5+spec-1.1.0
- [Commits](https://github.com/toml-rs/toml/compare/toml_datetime-v0.7.3...toml_datetime-v0.7.5)

Updates `toml_edit` from 0.23.7 to 0.23.10+spec-1.0.0
- [Commits](https://github.com/toml-rs/toml/compare/v0.23.7...v0.23.10)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-version: 1.0.148
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: proc-macro2
  dependency-version: 1.0.104
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: axum
  dependency-version: 0.8.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tempfile
  dependency-version: 3.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: rmp-serde
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: reqwest
  dependency-version: 0.12.28
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: insta
  dependency-version: 1.45.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: async-lock
  dependency-version: 3.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: axum-core
  dependency-version: 0.5.6
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-version: 1.2.51
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: derive_more
  dependency-version: 2.1.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: derive_more-impl
  dependency-version: 2.1.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: find-msvc-tools
  dependency-version: 0.1.6
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: iri-string
  dependency-version: 0.7.10
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: itoa
  dependency-version: 1.0.17
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: miniserde
  dependency-version: 0.1.45
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: rmp
  dependency-version: 0.8.15
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: rustix
  dependency-version: 1.1.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: ryu
  dependency-version: 1.0.22
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: serde_spanned
  dependency-version: 1.0.4
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: signal-hook-registry
  dependency-version: 1.4.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: toml_datetime
  dependency-version: 0.7.5+spec-1.1.0
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: toml_edit
  dependency-version: 0.23.10+spec-1.0.0
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-02 12:29:51 -05:00
fbdb
bbb4e698ba chore: fix broken links in docs (#4505) 2026-01-02 12:29:39 -05:00
Luxalpa
0c44199bc9 feat: implement Params derive macro without nightly (#4514) 2026-01-02 12:29:10 -05:00
Ai Suzuki
4f0daada69 chore: pass lint attributes (allow, warn deny and forbid) to generated struct (closes #4498) (#4496) 2026-01-02 12:19:00 -05:00
Greg Johnston
b45eb8f39f chore: fix nonsensical example in reactive_stores (#4516) 2025-12-30 08:07:02 -05:00
Greg Johnston
46f16ec9cf fix: preserve existing attributes when rebuilding AnyViewWithAttrs (closes #4512) (#4513) 2025-12-29 13:55:11 -05:00
Greg Johnston
bfdf0bf4d1 fix: track parents when tracking keyed subfield (second half of #4475) (#4510) 2025-12-29 13:54:59 -05:00
Greg Johnston
e7e18b1995 fix: correctly read resources inside <For/> children (closes #4503) (#4504) 2025-12-26 11:26:05 -05:00
dependabot[bot]
81cff63455 chore(deps): bump actions/cache from 4 to 5 (#4489)
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-19 15:48:06 -05:00
dependabot[bot]
61186c2432 chore(deps): bump the rust-dependencies group across 1 directory with 19 updates (#4499)
Bumps the rust-dependencies group with 14 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [tracing](https://github.com/tokio-rs/tracing) | `0.1.43` | `0.1.44` |
| [bitcode](https://github.com/SoftbearStudios/bitcode) | `0.6.7` | `0.6.9` |
| [insta](https://github.com/mitsuhiko/insta) | `1.44.3` | `1.45.0` |
| [bumpalo](https://github.com/fitzgen/bumpalo) | `3.19.0` | `3.19.1` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.49` | `1.2.50` |
| [glam](https://github.com/bitshifter/glam-rs) | `0.30.8` | `0.30.9` |
| [half](https://github.com/VoidStarKat/half-rs) | `2.6.0` | `2.7.1` |
| [minicov](https://github.com/Amanieu/minicov) | `0.3.7` | `0.3.8` |
| [rustls-pki-types](https://github.com/rustls/pki-types) | `1.13.1` | `1.13.2` |
| [system-deps](https://github.com/gdesmott/system-deps) | `7.0.5` | `7.0.7` |
| [toml_parser](https://github.com/toml-rs/toml) | `1.0.4` | `1.0.6+spec-1.1.0` |
| [toml_writer](https://github.com/toml-rs/toml) | `1.0.4` | `1.0.6+spec-1.1.0` |
| [utf8-width](https://github.com/magiclen/utf8-width) | `0.1.7` | `0.1.8` |
| [version-compare](https://gitlab.com/timvisee/version-compare) | `0.2.0` | `0.2.1` |



Updates `tracing` from 0.1.43 to 0.1.44
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-0.1.43...tracing-0.1.44)

Updates `bitcode` from 0.6.7 to 0.6.9
- [Commits](https://github.com/SoftbearStudios/bitcode/commits)

Updates `insta` from 1.44.3 to 1.45.0
- [Release notes](https://github.com/mitsuhiko/insta/releases)
- [Changelog](https://github.com/mitsuhiko/insta/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mitsuhiko/insta/compare/1.44.3...1.45.0)

Updates `bitcode_derive` from 0.6.7 to 0.6.9
- [Commits](https://github.com/SoftbearStudios/bitcode/commits)

Updates `bumpalo` from 3.19.0 to 3.19.1
- [Changelog](https://github.com/fitzgen/bumpalo/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fitzgen/bumpalo/compare/v3.19.0...v3.19.1)

Updates `cc` from 1.2.49 to 1.2.50
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.49...cc-v1.2.50)

Updates `glam` from 0.30.8 to 0.30.9
- [Changelog](https://github.com/bitshifter/glam-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitshifter/glam-rs/compare/0.30.8...0.30.9)

Updates `half` from 2.6.0 to 2.7.1
- [Release notes](https://github.com/VoidStarKat/half-rs/releases)
- [Changelog](https://github.com/VoidStarKat/half-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/VoidStarKat/half-rs/compare/v2.6.0...v2.7.1)

Updates `minicov` from 0.3.7 to 0.3.8
- [Changelog](https://github.com/Amanieu/minicov/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Amanieu/minicov/compare/v0.3.7...v0.3.8)

Updates `rustls-pki-types` from 1.13.1 to 1.13.2
- [Release notes](https://github.com/rustls/pki-types/releases)
- [Commits](https://github.com/rustls/pki-types/compare/v/1.13.1...v/1.13.2)

Updates `system-deps` from 7.0.5 to 7.0.7
- [Release notes](https://github.com/gdesmott/system-deps/releases)
- [Changelog](https://github.com/gdesmott/system-deps/blob/main/CHANGELOG.md)
- [Commits](https://github.com/gdesmott/system-deps/compare/v7.0.5...v7.0.7)

Updates `toml` from 0.8.23 to 0.9.8
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.23...toml-v0.9.8)

Updates `toml_datetime` from 0.6.11 to 0.7.3
- [Commits](https://github.com/toml-rs/toml/compare/toml_datetime-v0.6.11...toml_datetime-v0.7.3)

Updates `toml_edit` from 0.22.27 to 0.23.7
- [Commits](https://github.com/toml-rs/toml/compare/v0.22.27...v0.23.7)

Updates `toml_parser` from 1.0.4 to 1.0.6+spec-1.1.0
- [Commits](https://github.com/toml-rs/toml/compare/toml_parser-v1.0.4...toml_parser-v1.0.6)

Updates `toml_writer` from 1.0.4 to 1.0.6+spec-1.1.0
- [Commits](https://github.com/toml-rs/toml/compare/toml_writer-v1.0.4...toml_writer-v1.0.6)

Updates `tracing-core` from 0.1.35 to 0.1.36
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-core-0.1.35...tracing-core-0.1.36)

Updates `utf8-width` from 0.1.7 to 0.1.8
- [Commits](https://github.com/magiclen/utf8-width/compare/v0.1.7...v0.1.8)

Updates `version-compare` from 0.2.0 to 0.2.1
- [Changelog](https://gitlab.com/timvisee/version-compare/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/timvisee/version-compare/compare/v0.2.0...v0.2.1)

---
updated-dependencies:
- dependency-name: tracing
  dependency-version: 0.1.44
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: bitcode
  dependency-version: 0.6.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: insta
  dependency-version: 1.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: bitcode_derive
  dependency-version: 0.6.9
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: bumpalo
  dependency-version: 3.19.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-version: 1.2.50
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: glam
  dependency-version: 0.30.9
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: half
  dependency-version: 2.7.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: minicov
  dependency-version: 0.3.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: rustls-pki-types
  dependency-version: 1.13.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: system-deps
  dependency-version: 7.0.7
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: toml
  dependency-version: 0.9.8
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: toml_datetime
  dependency-version: 0.7.3
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: toml_edit
  dependency-version: 0.23.7
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: toml_parser
  dependency-version: 1.0.6+spec-1.1.0
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: toml_writer
  dependency-version: 1.0.6+spec-1.1.0
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tracing-core
  dependency-version: 0.1.36
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: utf8-width
  dependency-version: 0.1.8
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: version-compare
  dependency-version: 0.2.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-19 15:47:50 -05:00
Greg Johnston
75e42ccea5 Merge pull request #4501 from leptos-rs/4488v2
Fix untracked writes on keyed store fields
2025-12-19 15:47:29 -05:00
Greg Johnston
85c7cc94ad Merge pull request #4500 from leptos-rs/fix-4481
fix: wrong path for `IntoAnyAttribute` in macro expansion from #4481
2025-12-19 15:46:56 -05:00
Greg Johnston
270536adb1 fix: prevent untracked writes on keyed subfields from notifying parent
(closes #4488)
2025-12-19 11:41:13 -05:00
Greg Johnston
cec0fb8d85 chore: add regression test to make sure untracked writes on store fields
don't notify anything
2025-12-19 11:40:44 -05:00
Greg Johnston
764b9cd57d leptos_macro-v0.8.14 2025-12-19 11:32:43 -05:00
Greg Johnston
6de2b4006a fix: wrong path for IntoAnyAttribute in macro expansion from #4481 2025-12-19 11:31:25 -05:00
Greg Johnston
65940cbefa fix: do not show Transition fallback on 2nd change if 1st change resolved synchronously (closes #4492, closes #3868) (#4495) 2025-12-19 10:42:44 -05:00
Greg Johnston
8f5c34de8a chore: publish patch versions 2025-12-19 10:22:10 -05:00
Saber Haj Rabiee
4faa340ba8 chore: upgrade dependencies (#4497) 2025-12-19 09:29:04 -05:00
Merlijn
5af5fdeeed chore: add missing import for IntoAnyAttribute when using erase_components (#4481) 2025-12-15 14:57:32 -05:00
Greg Johnston
9dd52e6c15 fix: use unkeyed paths for all store patching (closes #4486) (#4487) 2025-12-14 21:13:08 -05:00
46 changed files with 1059 additions and 998 deletions

View File

@@ -103,7 +103,7 @@ jobs:
id: pnpm-cache
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
- uses: actions/cache@v5
if: contains(inputs.directory, 'examples')
name: Setup pnpm cache
with:

1041
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -49,45 +49,45 @@ any_spawner = { path = "./any_spawner/", version = "0.3.0" }
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
either_of = { path = "./either_of/", version = "0.1.6" }
hydration_context = { path = "./hydration_context", version = "0.3.0" }
leptos = { path = "./leptos", version = "0.8.14" }
leptos = { path = "./leptos", version = "0.8.15" }
leptos_config = { path = "./leptos_config", version = "0.8.8" }
leptos_dom = { path = "./leptos_dom", version = "0.8.7" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.5" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.7" }
leptos_macro = { path = "./leptos_macro", version = "0.8.12" }
leptos_router = { path = "./router", version = "0.8.10" }
leptos_macro = { path = "./leptos_macro", version = "0.8.13" }
leptos_router = { path = "./router", version = "0.8.11" }
leptos_router_macro = { path = "./router_macro", version = "0.8.6" }
leptos_server = { path = "./leptos_server", version = "0.8.6" }
leptos_meta = { path = "./meta", version = "0.8.5" }
next_tuple = { path = "./next_tuple", version = "0.1.0" }
oco_ref = { path = "./oco", version = "0.2.1" }
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
reactive_graph = { path = "./reactive_graph", version = "0.2.11" }
reactive_stores = { path = "./reactive_stores", version = "0.3.0" }
reactive_graph = { path = "./reactive_graph", version = "0.2.12" }
reactive_stores = { path = "./reactive_stores", version = "0.3.1" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.6" }
server_fn = { path = "./server_fn", version = "0.8.8" }
server_fn = { path = "./server_fn", version = "0.8.9" }
server_fn_macro = { path = "./server_fn_macro", version = "0.8.8" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.5" }
tachys = { path = "./tachys", version = "0.2.11" }
# members deps
async-once-cell = { default-features = false, version = "0.5.3" }
async-once-cell = { default-features = false, version = "0.5.4" }
itertools = { default-features = false, version = "0.14.0" }
convert_case = { default-features = false, version = "0.8.0" }
serde_json = { default-features = false, version = "1.0.143" }
trybuild = { default-features = false, version = "1.0.110" }
typed-builder = { default-features = false, version = "0.22.0" }
typed-builder-macro = { default-features = false, version = "0.22.0" }
convert_case = { default-features = false, version = "0.10.0" }
serde_json = { default-features = false, version = "1.0.148" }
trybuild = { default-features = false, version = "1.0.114" }
typed-builder = { default-features = false, version = "0.23.2" }
typed-builder-macro = { default-features = false, version = "0.23.2" }
thiserror = { default-features = false, version = "2.0.17" }
wasm-bindgen = { default-features = false, version = "0.2.100" }
indexmap = { default-features = false, version = "2.11.0" }
wasm-bindgen = { default-features = false, version = "0.2.106" }
indexmap = { default-features = false, version = "2.12.1" }
rstml = { default-features = false, version = "0.12.1" }
rustc_version = { default-features = false, version = "0.4.1" }
guardian = { default-features = false, version = "1.3.0" }
rustc-hash = { default-features = false, version = "2.1.1" }
actix-web = { default-features = false, version = "4.11.0" }
tracing = { default-features = false, version = "0.1.41" }
slotmap = { default-features = false, version = "1.0.7" }
actix-web = { default-features = false, version = "4.12.1" }
tracing = { default-features = false, version = "0.1.44" }
slotmap = { default-features = false, version = "1.1.1" }
futures = { default-features = false, version = "0.3.31" }
dashmap = { default-features = false, version = "6.1.0" }
pin-project-lite = { default-features = false, version = "0.2.16" }
@@ -97,41 +97,41 @@ html-escape = { default-features = false, version = "0.2.13" }
proc-macro-error2 = { default-features = false, version = "2.0.1" }
const_format = { default-features = false, version = "0.2.35" }
gloo-net = { default-features = false, version = "0.6.0" }
url = { default-features = false, version = "2.5.4" }
tokio = { default-features = false, version = "1.47.1" }
url = { default-features = false, version = "2.5.7" }
tokio = { default-features = false, version = "1.48.0" }
base64 = { default-features = false, version = "0.22.1" }
cfg-if = { default-features = false, version = "1.0.3" }
wasm-bindgen-futures = { default-features = false, version = "0.4.50" }
cfg-if = { default-features = false, version = "1.0.4" }
wasm-bindgen-futures = { default-features = false, version = "0.4.56" }
tower = { default-features = false, version = "0.5.2" }
proc-macro2 = { default-features = false, version = "1.0.101" }
proc-macro2 = { default-features = false, version = "1.0.96" }
serde = { default-features = false, version = "1.0.219" }
parking_lot = { default-features = false, version = "0.12.5" }
axum = { default-features = false, version = "0.8.6" }
serde_qs = { default-features = false, version = "0.15.0" }
syn = { default-features = false, version = "2.0.106" }
parking_lot = { default-features = false, version = "0.12.4" }
axum = { default-features = false, version = "0.8.4" }
serde_qs = { default-features = false, version = "1.0.0-rc.3" }
syn = { default-features = false, version = "2.0.104" }
xxhash-rust = { default-features = false, version = "0.8.15" }
paste = { default-features = false, version = "1.0.15" }
quote = { default-features = false, version = "1.0.41" }
web-sys = { default-features = false, version = "0.3.77" }
js-sys = { default-features = false, version = "0.3.77" }
rand = { default-features = false, version = "0.9.1" }
serde-lite = { default-features = false, version = "0.5.0" }
quote = { default-features = false, version = "1.0.42" }
web-sys = { default-features = false, version = "0.3.83" }
js-sys = { default-features = false, version = "0.3.83" }
rand = { default-features = false, version = "0.9.2" }
serde-lite = { default-features = false, version = "0.5.1" }
tokio-tungstenite = { default-features = false, version = "0.28.0" }
serial_test = { default-features = false, version = "3.2.0" }
erased = { default-features = false, version = "0.1.2" }
glib = { default-features = false, version = "0.20.12" }
glib = { default-features = false, version = "0.21.5" }
async-trait = { default-features = false, version = "0.1.89" }
linear-map = { default-features = false, version = "1.2.0" }
anyhow = { default-features = false, version = "1.0.100" }
walkdir = { default-features = false, version = "2.5.0" }
actix-ws = { default-features = false, version = "0.3.0" }
tower-http = { default-features = false, version = "0.6.4" }
tower-http = { default-features = false, version = "0.6.8" }
prettyplease = { default-features = false, version = "0.2.37" }
inventory = { default-features = false, version = "0.3.21" }
config = { default-features = false, version = "0.15.14" }
camino = { default-features = false, version = "1.2.1" }
config = { default-features = false, version = "0.15.19" }
camino = { default-features = false, version = "1.2.2" }
ciborium = { default-features = false, version = "0.2.2" }
bitcode = { default-features = false, version = "0.6.6" }
bitcode = { default-features = false, version = "0.6.9" }
multer = { default-features = false, version = "3.1.0" }
leptos-spin-macro = { default-features = false, version = "0.2.0" }
sledgehammer_utils = { default-features = false, version = "0.3.1" }
@@ -139,38 +139,38 @@ sledgehammer_bindgen = { default-features = false, version = "0.6.0" }
wasm-streams = { default-features = false, version = "0.4.2" }
rkyv = { default-features = false, version = "0.8.12" }
temp-env = { default-features = false, version = "0.3.6" }
uuid = { default-features = false, version = "1.18.0" }
bytes = { default-features = false, version = "1.10.1" }
http = { default-features = false, version = "1.3.1" }
regex = { default-features = false, version = "1.11.3" }
uuid = { default-features = false, version = "1.19.0" }
bytes = { default-features = false, version = "1.11.0" }
http = { default-features = false, version = "1.4.0" }
regex = { default-features = false, version = "1.12.2" }
drain_filter_polyfill = { default-features = false, version = "0.1.3" }
tempfile = { default-features = false, version = "3.23.0" }
tempfile = { default-features = false, version = "3.24.0" }
futures-lite = { default-features = false, version = "2.6.1" }
log = { default-features = false, version = "0.4.27" }
log = { default-features = false, version = "0.4.29" }
percent-encoding = { default-features = false, version = "2.3.2" }
async-executor = { default-features = false, version = "1.13.2" }
const-str = { default-features = false, version = "0.6.4" }
async-executor = { default-features = false, version = "1.13.3" }
const-str = { default-features = false, version = "0.7.1" }
http-body-util = { default-features = false, version = "0.1.3" }
hyper = { default-features = false, version = "1.7.0" }
hyper = { default-features = false, version = "1.8.1" }
postcard = { default-features = false, version = "1.1.3" }
rmp-serde = { default-features = false, version = "1.3.0" }
reqwest = { default-features = false, version = "0.12.23" }
rmp-serde = { default-features = false, version = "1.3.1" }
reqwest = { default-features = false, version = "0.12.28" }
tower-layer = { default-features = false, version = "0.3.3" }
attribute-derive = { default-features = false, version = "0.10.5" }
insta = { default-features = false, version = "1.43.1" }
codee = { default-features = false, version = "0.3.0" }
insta = { default-features = false, version = "1.45.1" }
codee = { default-features = false, version = "0.3.5" }
actix-http = { default-features = false, version = "3.11.2" }
wasm-bindgen-test = { default-features = false, version = "0.3.50" }
wasm-bindgen-test = { default-features = false, version = "0.3.56" }
rustversion = { default-features = false, version = "1.0.22" }
getrandom = { default-features = false, version = "0.3.3" }
actix-files = { default-features = false, version = "0.6.6" }
async-lock = { default-features = false, version = "3.4.1" }
getrandom = { default-features = false, version = "0.3.4" }
actix-files = { default-features = false, version = "0.6.9" }
async-lock = { default-features = false, version = "3.4.2" }
base16 = { default-features = false, version = "0.2.1" }
digest = { default-features = false, version = "0.10.7" }
sha2 = { default-features = false, version = "0.10.8" }
subsecond = { default-features = false, version = "0.7.0-rc.0" }
dioxus-cli-config = { default-features = false, version = "0.7.0-rc.0" }
dioxus-devtools = { default-features = false, version = "0.7.0-rc.0" }
sha2 = { default-features = false, version = "0.10.9" }
subsecond = { default-features = false, version = "0.7.2" }
dioxus-cli-config = { default-features = false, version = "0.7.2" }
dioxus-devtools = { default-features = false, version = "0.7.2" }
wasm_split_helpers = { default-features = false, version = "0.2.0" }
[profile.release]

View File

@@ -74,8 +74,8 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
## What does that mean?
- **Full-stack**: Leptos can be used to build apps that run in the browser (client-side rendering), on the server (server-side rendering), or by rendering HTML on the server and then adding interactivity in the browser (server-side rendering with hydration). This includes support for HTTP streaming of both data ([`Resource`s](https://docs.rs/leptos/latest/leptos/struct.Resource.html)) and HTML (out-of-order or in-order streaming of [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) components.)
- **Isomorphic**: Leptos provides primitives to write isomorphic [server functions](https://docs.rs/leptos_server/0.2.5/leptos_server/index.html), i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser, without needing to create and maintain a separate REST or other API.
- **Full-stack**: Leptos can be used to build apps that run in the browser (client-side rendering), on the server (server-side rendering), or by rendering HTML on the server and then adding interactivity in the browser (server-side rendering with hydration). This includes support for HTTP streaming of both data ([`Resource`s](https://docs.rs/leptos/latest/leptos/prelude/struct.Resource.html)) and HTML (out-of-order or in-order streaming of [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/suspense/fn.Suspense.html) components.)
- **Isomorphic**: Leptos provides primitives to write isomorphic [server functions](https://docs.rs/server_fn/latest/server_fn/), i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser, without needing to create and maintain a separate REST or other API.
- **Web**: Leptos is built on the Web platform and Web standards. The [router](https://docs.rs/leptos_router/latest/leptos_router/) is designed to use Web fundamentals (like links and forms) and build on top of them rather than trying to replace them.
- **Framework**: Leptos provides most of what you need to build a modern web app: a reactive system, templating library, and a router that works on both the server and client side.
- **Fine-grained reactivity**: The entire framework is built from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signals value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (So, no virtual DOM overhead!)

View File

@@ -9,12 +9,12 @@ This document is intended as a running list of common issues, with example code
**Issue**: Sometimes you want to update a reactive signal in a way that depends on another signal.
```rust
let (a, set_a) = create_signal(0);
let (b, set_b) = create_signal(false);
let (a, set_a) = signal(0);
let (b, set_b) = signal(false);
create_effect(move |_| {
if a() > 5 {
set_b(true);
Effect::new(move |_| {
if a.get() > 5 {
set_b.set(true);
}
});
```
@@ -24,56 +24,10 @@ This creates an inefficient chain of updates, and can easily lead to infinite lo
**Solution**: Follow the rule, _What can be derived, should be derived._ In this case, this has the benefit of massively reducing the code size, too!
```rust
let (a, set_a) = create_signal(0);
let b = move || a () > 5;
let (a, set_a) = signal(0);
let b = move || a.get() > 5;
```
### Nested signal updates/reads triggering panic
Sometimes you have nested signals: for example, hash-map that can change over time, each of whose values can also change over time:
```rust
#[component]
pub fn App() -> impl IntoView {
let resources = create_rw_signal(HashMap::new());
let update = move |id: usize| {
resources.update(|resources| {
resources
.entry(id)
.or_insert_with(|| create_rw_signal(0))
.update(|amount| *amount += 1)
})
};
view! {
<div>
<pre>{move || format!("{:#?}", resources.get().into_iter().map(|(id, resource)| (id, resource.get())).collect::<Vec<_>>())}</pre>
<button on:click=move |_| update(1)>"+"</button>
</div>
}
}
```
Clicking the button twice will cause a panic, because of the nested signal _read_. Calling the `update` function on `resources` immediately takes out a mutable borrow on `resources`, then updates the `resource` signal—which re-runs the effect that reads from the signals, which tries to immutably access `resources` and panics. It's the nested update here which causes a problem, because the inner update triggers and effect that tries to read both signals while the outer is still updating.
You can fix this fairly easily by using the [`batch()`](https://docs.rs/leptos/latest/leptos/fn.batch.html) method:
```rust
let update = move |id: usize| {
batch(move || {
resources.update(|resources| {
resources
.entry(id)
.or_insert_with(|| create_rw_signal(0))
.update(|amount| *amount += 1)
})
});
};
```
This delays running any effects until after both updates are made, preventing the conflict entirely without requiring any other restructuring.
## Templates and the DOM
### `<input value=...>` doesn't update or stops updating
@@ -83,8 +37,8 @@ Many DOM attributes can be updated either by setting an attribute on the DOM nod
This means that in practice, attributes like `value` or `checked` on an `<input/>` element only update the _default_ value for the `<input/>`. If you want to reactively update the value, you should use `prop:value` instead to set the `value` property.
```rust
let (a, set_a) = create_signal("Starting value".to_string());
let on_input = move |ev| set_a(event_target_value(&ev));
let (a, set_a) = signal("Starting value".to_string());
let on_input = move |ev| set_a.set(event_target_value(&ev));
view! {
@@ -97,8 +51,8 @@ view! {
```
```rust
let (a, set_a) = create_signal("Starting value".to_string());
let on_input = move |ev| set_a(event_target_value(&ev));
let (a, set_a) = signal("Starting value".to_string());
let on_input = move |ev| set_a.set(event_target_value(&ev));
view! {

View File

@@ -670,7 +670,7 @@ fn CodeDemoWasm(mode: WasmDemo) -> impl IntoView {
leptos::logging::log!("wasm csr_listener listener added");
// Dispatch the event when this view is finally mounted onto the DOM.
request_animation_frame(move || {
_ = request_animation_frame(move || {
let event = web_sys::Event::new("hljs_hook")
.expect("error creating hljs_hook event");
document.dispatch_event(&event)
@@ -689,7 +689,7 @@ fn CodeDemoWasm(mode: WasmDemo) -> impl IntoView {
<Suspense fallback=move || view! { <p>"Loading code example..."</p> }>{
move || Suspend::new(async move {
Effect::new(move |_| {
request_animation_frame(move || {
_ = request_animation_frame(move || {
leptos::logging::log!("request_animation_frame invoking hljs::highlight_all");
// under SSR this is an noop, but it wouldn't be called under there anyway because
// it isn't the isomorphic version, i.e. Effect::new_isomorphic(...).
@@ -815,7 +815,7 @@ fn WasmBindgenJSHookReadyEvent() -> impl IntoView {
leptos::logging::log!("wasm csr_listener listener added");
// Dispatch the event when this view is finally mounted onto the DOM.
request_animation_frame(move || {
_ = request_animation_frame(move || {
let event = web_sys::Event::new("hljs_hook")
.expect("error creating hljs_hook event");
document.dispatch_event(&event)
@@ -866,7 +866,7 @@ fn WasmBindgenEffect() -> impl IntoView {
let example = r#"<Suspense fallback=move || view! { <p>"Loading code example..."</p> }>{
move || Suspend::new(async move {
Effect::new(move |_| {
request_animation_frame(move || {
_ = request_animation_frame(move || {
leptos::logging::log!("request_animation_frame invoking hljs::highlight_all");
// under SSR this is an noop.
crate::hljs::highlight_all();

View File

@@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"]
axum = { version = "0.8.1", optional = true }
console_error_panic_hook = "0.1.7"
console_log = "1.0"
leptos = { path = "../../leptos", features = ["tracing"] }
leptos = { path = "../../leptos", features = ["tracing", "lazy"] }
leptos_meta = { path = "../../meta" }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_router = { path = "../../router" }

View File

@@ -97,7 +97,7 @@ fn delay(
duration: Duration,
) -> impl Future<Output = Result<(), Canceled>> + Send {
let (tx, rx) = oneshot::channel();
set_timeout(
_ = set_timeout(
move || {
_ = tx.send(());
},

View File

@@ -50,7 +50,7 @@ where
};
// here, we return the handle
set_interval_with_handle(
set_interval(
f.clone(),
// this is the only reactive access, so this effect will only
// re-run when the interval changes

View File

@@ -684,7 +684,7 @@ where
additional_context.clone(),
app_fn.clone(),
);
let asyn = render_app_async_stream_with_context(
let asyn = render_app_async_with_context(
additional_context.clone(),
app_fn.clone(),
);
@@ -1019,73 +1019,6 @@ where
render_app_async_with_context(|| {}, app_fn)
}
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
/// `async` resources have loaded.
///
/// This version allows us to pass Axum State/Extension/Extractor or other info from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
/// the data to leptos in a closure. An example is below
/// ```
/// use axum::{
/// body::Body,
/// extract::Path,
/// http::Request,
/// response::{IntoResponse, Response},
/// };
/// use leptos::context::provide_context;
///
/// async fn custom_handler(
/// Path(id): Path<String>,
/// req: Request<Body>,
/// ) -> Response {
/// let handler = leptos_axum::render_app_async_with_context(
/// move || {
/// provide_context(id.clone());
/// },
/// || { /* your application here */ },
/// );
/// handler(req).await.into_response()
/// }
/// ```
/// Otherwise, this function is identical to [render_app_to_stream].
///
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [`Parts`]
/// - [`ResponseOptions`]
/// - [`ServerMetaContext`]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", fields(error), skip_all)
)]
pub fn render_app_async_stream_with_context<IV>(
additional_context: impl Fn() + 'static + Clone + Send + Sync,
app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send + 'static>>
+ Clone
+ Send
+ 'static
where
IV: IntoView + 'static,
{
handle_response(additional_context, app_fn, |app, chunks, _supports_ooo| {
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
};
let app = app.collect::<String>().await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks))
as PinnedStream<String>
})
})
}
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], asynchronously rendering an HTML page after all
/// `async` resources have loaded.
@@ -2072,19 +2005,7 @@ where
},
move || shell(options),
req,
|app, chunks, _supports_ooo| {
Box::pin(async move {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
};
let app = app.collect::<String>().await;
let chunks = chunks();
Box::pin(once(async move { app }).chain(chunks))
as PinnedStream<String>
})
},
async_stream_builder,
)
.await;

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos"
version = "0.8.14"
version = "0.8.15"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
@@ -122,6 +122,7 @@ subsecond = [
"web-sys/WebSocket",
"web-sys/Window",
]
lazy = ["tachys/lazy"]
[dev-dependencies]
tokio = { features = [

View File

@@ -83,7 +83,7 @@ pub fn AnimatedShow(
} else {
cls.set(hide_class);
let h = leptos_dom::helpers::set_timeout_with_handle(
let h = leptos_dom::helpers::set_timeout(
move || show.set(false),
hide_delay,
)

View File

@@ -287,7 +287,9 @@ where
web_sys::UrlSearchParams::new_with_str_sequence_sequence(form_data)
.unwrap_throw();
let data = data.to_string().as_string().unwrap_or_default();
serde_qs::Config::new(5, false).deserialize_str::<Self>(&data)
serde_qs::Config::new()
.use_form_encoding(true)
.deserialize_str::<Self>(&data)
}
}

View File

@@ -1,25 +1,42 @@
if (window.location.protocol === 'https:') {
protocol = 'wss://';
if (window.location.protocol === "https:") {
protocol = "wss://";
}
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();
if (msg.css) {
let found = false;
document.querySelectorAll("link").forEach((link) => {
if (link.getAttribute('href').includes(msg.css)) {
let newHref = '/' + msg.css + '?version=' + Date.now();
link.setAttribute('href', newHref);
found = true;
}
});
if (!found) console.warn(`CSS hot-reload: Could not find a <link href=/\"${msg.css}\"> element`);
function connect() {
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();
if (msg.css) {
let found = false;
document.querySelectorAll("link").forEach((link) => {
if (link.getAttribute("href").includes(msg.css)) {
let newHref = "/" + msg.css + "?version=" + Date.now();
link.setAttribute("href", newHref);
found = true;
}
});
if (!found)
console.warn(
`CSS hot-reload: Could not find a <link href=/\"${msg.css}\"> element`,
);
}
if (msg.view) {
patch(msg.view);
}
};
if(msg.view) {
patch(msg.view);
}
};
ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.');
ws.onclose = () => {
console.warn("Live-reload disconnected. Reconnecting...");
setTimeout(connect, 1000);
};
ws.onerror = () => {
ws.close();
};
}
connect();

View File

@@ -73,14 +73,14 @@ where
let to = to.into_iter().collect::<Vec<_>>();
let (list, set_list) = create_signal(from.clone());
request_animation_frame({
_ = request_animation_frame({
let to = to.clone();
let then = then.clone();
move || {
set_list(to);
if let Some(then) = then {
request_animation_frame({
_ = request_animation_frame({
move || {
set_list(then);
}

View File

@@ -116,7 +116,7 @@ pub fn event_target_checked(ev: &web_sys::Event) -> bool {
.checked()
}
/// Handle that is generated by [request_animation_frame_with_handle] and can
/// Handle that is generated by [request_animation_frame] and can
/// be used to cancel the animation frame request.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct AnimationFrameRequestHandle(i32);
@@ -129,18 +129,6 @@ impl AnimationFrameRequestHandle {
}
}
/// Runs the given function between the next repaint using
/// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(feature = "tracing", instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
_ = request_animation_frame_with_handle(cb);
}
// Closure::once_into_js only frees the callback when it's actually
// called, so this instead uses into_js_value, which can be freed by
// the host JS engine's GC if it supports weak references (which all
@@ -169,7 +157,7 @@ fn closure_once(cb: impl FnOnce() + 'static) -> JsValue {
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(feature = "tracing", instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_animation_frame_with_handle(
pub fn request_animation_frame(
cb: impl FnOnce() + 'static,
) -> Result<AnimationFrameRequestHandle, JsValue> {
#[cfg(feature = "tracing")]
@@ -190,7 +178,7 @@ pub fn request_animation_frame_with_handle(
raf(closure_once(cb))
}
/// Handle that is generated by [request_idle_callback_with_handle] and can be
/// Handle that is generated by [request_idle_callback] and can be
/// used to cancel the idle callback.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct IdleCallbackHandle(u32);
@@ -203,18 +191,6 @@ impl IdleCallbackHandle {
}
}
/// Queues the given function during an idle period using
/// [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback).
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(feature = "tracing", instrument(level = "trace", skip_all))]
#[inline(always)]
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.
@@ -224,7 +200,7 @@ pub fn request_idle_callback(cb: impl Fn() + 'static) {
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(feature = "tracing", instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_idle_callback_with_handle(
pub fn request_idle_callback(
cb: impl Fn() + 'static,
) -> Result<IdleCallbackHandle, JsValue> {
#[cfg(feature = "tracing")]
@@ -261,7 +237,7 @@ pub fn queue_microtask(task: impl FnOnce() + 'static) {
tachys::renderer::dom::queue_microtask(task);
}
/// Handle that is generated by [set_timeout_with_handle] and can be used to clear the timeout.
/// Handle that is generated by [set_timeout] and can be used to clear the timeout.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct TimeoutHandle(i32);
@@ -273,20 +249,6 @@ impl TimeoutHandle {
}
}
/// Executes the given function after the given duration of time has passed.
/// [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout).
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(
feature = "tracing",
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).
///
@@ -298,7 +260,7 @@ pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
#[inline(always)]
pub fn set_timeout_with_handle(
pub fn set_timeout(
cb: impl FnOnce() + 'static,
duration: Duration,
) -> Result<TimeoutHandle, JsValue> {
@@ -391,7 +353,7 @@ pub fn debounce<T: 'static>(
if let Some(timer) = timer.write().unwrap().take() {
timer.clear();
}
let handle = set_timeout_with_handle(
let handle = set_timeout(
{
let cb = Arc::clone(&cb);
move || {
@@ -418,20 +380,6 @@ impl IntervalHandle {
}
}
/// Repeatedly calls the given function, with a delay of the given duration between calls.
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
///
/// ### Note about Context
///
/// The callback is called outside of the reactive ownership tree. This means that it does not have access to context via [`use_context`](reactive_graph::owner::use_context). If you want to use context inside the callback, you should either call `use_context` in the body of the component, and move the value into the callback, or access the current owner inside the component body using [`Owner::current`](reactive_graph::owner::Owner::current) and reestablish it in the callback with [`Owner::with`](reactive_graph::owner::Owner::with).
#[cfg_attr(
feature = "tracing",
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
pub fn set_interval(cb: impl Fn() + 'static, duration: Duration) {
_ = set_interval_with_handle(cb, duration);
}
/// 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).
@@ -444,7 +392,7 @@ pub fn set_interval(cb: impl Fn() + 'static, duration: Duration) {
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
#[inline(always)]
pub fn set_interval_with_handle(
pub fn set_interval(
cb: impl Fn() + 'static,
duration: Duration,
) -> Result<IntervalHandle, JsValue> {

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_macro"
version = "0.8.12"
version = "0.8.14"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"

View File

@@ -199,7 +199,7 @@ mod slot;
/// ```
///
/// 9. You can use the `node_ref` or `_ref` attribute to store a reference to its DOM element in a
/// [NodeRef](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to use later.
/// [NodeRef](https://docs.rs/leptos/latest/leptos/prelude/struct.NodeRef.html) to use later.
/// ```rust
/// # use leptos::prelude::*;
///
@@ -505,9 +505,9 @@ pub fn include_view(tokens: TokenStream) -> TokenStream {
///
/// * `#[prop(into)]`: This will call `.into()` on any value passed into the component prop. (For example,
/// you could apply `#[prop(into)]` to a prop that takes
/// [Signal](https://docs.rs/leptos/latest/leptos/struct.Signal.html), which would
/// allow users to pass a [ReadSignal](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) or
/// [RwSignal](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html)
/// [Signal](https://docs.rs/leptos/latest/leptos/prelude/struct.Signal.html), which would
/// allow users to pass a [ReadSignal](https://docs.rs/leptos/latest/leptos/prelude/struct.ReadSignal.html) or
/// [RwSignal](https://docs.rs/leptos/latest/leptos/prelude/struct.RwSignal.html)
/// and automatically convert it.)
/// * `#[prop(optional)]`: If the user does not specify this property when they use the component,
/// it will be set to its default value. If the property type is `Option<T>`, values should be passed

View File

@@ -25,7 +25,7 @@ pub fn params_impl(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
let span = field.span();
quote_spanned! {
span=> #ident: <#ty as ::leptos_router::params::IntoParam>::into_param(
span=> #ident: ::leptos_router::params::macro_helpers::Wrapper::<#ty>::__into_param(
map.get_str(#field_name_string),
#field_name_string
)?
@@ -39,6 +39,8 @@ pub fn params_impl(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
let gen = quote! {
impl Params for #name {
fn from_map(map: &::leptos_router::params::ParamsMap) -> ::core::result::Result<Self, ::leptos_router::params::ParamsError> {
use ::leptos_router::params::macro_helpers::Fallback as _;
Ok(Self {
#(#fields,)*
})

View File

@@ -176,7 +176,9 @@ pub(crate) fn component_to_tokens(
let spreads = (!(spreads.is_empty())).then(|| {
if cfg!(feature = "__internal_erase_components") {
quote! {
.add_any_attr(vec![#(#spreads.into_any_attr(),)*])
.add_any_attr({
vec![#(::leptos::attr::any_attribute::IntoAnyAttribute::into_any_attr(#spreads),)*]
})
}
} else {
quote! {

View File

@@ -20,7 +20,7 @@ pub fn Demo1() -> impl IntoView {
// We need to add the 3D view onto the canvas post render.
Effect::new(move |_| {
request_animation_frame(move || {
_ = request_animation_frame(move || {
scene_sig.get_untracked().setup();
});
});

View File

@@ -95,7 +95,7 @@ pub fn App() -> impl IntoView {
// if expires_in isn't 0, then set a timeout that rerfresh a minute short of the refresh.
let expires_in = rw_expires_in.get();
if expires_in != 0 && email.get_untracked().is_some() {
let handle = set_timeout_with_handle(
let handle = set_timeout(
move || {
refresh_token.dispatch(RefreshToken {
email: email.get_untracked().unwrap(),

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_graph"
version = "0.2.11"
version = "0.2.12"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -32,7 +32,7 @@ indexmap = { workspace = true, default-features = true }
paste = { workspace = true, default-features = true }
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
web-sys = { version = "0.3.77", features = ["console"] }
web-sys = { version = "0.3.83", features = ["console"] }
[dev-dependencies]
tokio = { features = [

View File

@@ -23,10 +23,11 @@ pin_project! {
#[derive(Clone)]
#[allow(missing_docs)]
pub struct ScopedFuture<Fut> {
pub owner: Owner,
pub observer: Option<AnySubscriber>,
owner: Owner,
observer: Option<AnySubscriber>,
diagnostics: bool,
#[pin]
pub fut: Fut,
fut: Fut,
}
}
@@ -39,6 +40,7 @@ impl<Fut> ScopedFuture<Fut> {
Self {
owner,
observer,
diagnostics: true,
fut,
}
}
@@ -51,19 +53,19 @@ impl<Fut> ScopedFuture<Fut> {
Self {
owner,
observer: None,
diagnostics: false,
fut,
}
}
#[doc(hidden)]
#[track_caller]
pub fn new_untracked_with_diagnostics(
fut: Fut,
) -> ScopedFutureUntrackedWithDiagnostics<Fut> {
pub fn new_untracked_with_diagnostics(fut: Fut) -> Self {
let owner = Owner::current().unwrap_or_default();
ScopedFutureUntrackedWithDiagnostics {
Self {
owner,
observer: None,
diagnostics: true,
fut,
}
}
@@ -75,41 +77,19 @@ impl<Fut: Future> Future for ScopedFuture<Fut> {
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
this.owner.with(|| {
#[cfg(debug_assertions)]
let _maybe_guard = if this.observer.is_none() {
Some(crate::diagnostics::SpecialNonReactiveZone::enter())
} else {
None
};
this.observer.with_observer(|| this.fut.poll(cx))
this.observer.with_observer(|| {
#[cfg(debug_assertions)]
let _maybe_guard = if *this.diagnostics {
None
} else {
Some(crate::diagnostics::SpecialNonReactiveZone::enter())
};
this.fut.poll(cx)
})
})
}
}
pin_project! {
/// A [`Future`] wrapper that sets the [`Owner`] and [`Observer`] before polling the inner
/// `Future`, output of [`ScopedFuture::new_untracked_with_diagnostics`].
///
/// In leptos 0.9 this will be replaced with `ScopedFuture` itself.
#[derive(Clone)]
pub struct ScopedFutureUntrackedWithDiagnostics<Fut> {
owner: Owner,
observer: Option<AnySubscriber>,
#[pin]
fut: Fut,
}
}
impl<Fut: Future> Future for ScopedFutureUntrackedWithDiagnostics<Fut> {
type Output = Fut::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
this.owner
.with(|| this.observer.with_observer(|| this.fut.poll(cx)))
}
}
/// Utilities used to track whether asynchronous computeds are currently loading.
pub mod suspense {
use crate::{

View File

@@ -1,6 +1,6 @@
[package]
name = "reactive_stores"
version = "0.3.0"
version = "0.3.1"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"

View File

@@ -29,6 +29,7 @@ where
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
path: Arc<dyn Fn() -> StorePath + Send + Sync>,
path_unkeyed: Arc<dyn Fn() -> StorePath + Send + Sync>,
get_trigger: Arc<dyn Fn(StorePath) -> StoreFieldTrigger + Send + Sync>,
get_trigger_unkeyed:
Arc<dyn Fn(StorePath) -> StoreFieldTrigger + Send + Sync>,
@@ -113,6 +114,10 @@ impl<T> StoreField for ArcField<T> {
(self.path)()
}
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
(self.path_unkeyed)()
}
fn reader(&self) -> Option<Self::Reader> {
(self.read)().map(StoreFieldReader::new)
}
@@ -137,6 +142,9 @@ where
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
path: Arc::new(move || value.path().into_iter().collect()),
path_unkeyed: Arc::new(move || {
value.path_unkeyed().into_iter().collect()
}),
get_trigger: Arc::new(move |path| value.get_trigger(path)),
get_trigger_unkeyed: Arc::new(move |path| {
value.get_trigger_unkeyed(path)
@@ -163,6 +171,10 @@ where
let value = value.clone();
move || value.path().into_iter().collect()
}),
path_unkeyed: Arc::new({
let value = value.clone();
move || value.path_unkeyed().into_iter().collect()
}),
get_trigger: Arc::new({
let value = value.clone();
move |path| value.get_trigger(path)
@@ -211,6 +223,10 @@ where
let value = value.clone();
move || value.path().into_iter().collect()
}),
path_unkeyed: Arc::new({
let value = value.clone();
move || value.path_unkeyed().into_iter().collect()
}),
get_trigger: Arc::new({
let value = value.clone();
move |path| value.get_trigger(path)
@@ -258,6 +274,10 @@ where
let value = value.clone();
move || value.path().into_iter().collect()
}),
path_unkeyed: Arc::new({
let value = value.clone();
move || value.path_unkeyed().into_iter().collect()
}),
get_trigger: Arc::new({
let value = value.clone();
move |path| value.get_trigger(path)
@@ -306,6 +326,10 @@ where
let value = value.clone();
move || value.path().into_iter().collect()
}),
path_unkeyed: Arc::new({
let value = value.clone();
move || value.path_unkeyed().into_iter().collect()
}),
get_trigger: Arc::new({
let value = value.clone();
move |path| value.get_trigger(path)
@@ -358,6 +382,10 @@ where
let value = value.clone();
move || value.path().into_iter().collect()
}),
path_unkeyed: Arc::new({
let value = value.clone();
move || value.path_unkeyed().into_iter().collect()
}),
get_trigger: Arc::new({
let value = value.clone();
move |path| value.get_trigger(path)
@@ -396,6 +424,7 @@ impl<T> Clone for ArcField<T> {
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
path: self.path.clone(),
path_unkeyed: self.path_unkeyed.clone(),
get_trigger: Arc::clone(&self.get_trigger),
get_trigger_unkeyed: Arc::clone(&self.get_trigger_unkeyed),
read: Arc::clone(&self.read),

View File

@@ -76,6 +76,11 @@ where
fn path(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner.path()
}
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner.path_unkeyed()
}
fn reader(&self) -> Option<Self::Reader> {
let inner = self.inner.reader()?;
Some(Mapped::new_with_guard(inner, |n| n.deref()))

View File

@@ -73,6 +73,13 @@ where
.unwrap_or_default()
}
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner
.try_get_value()
.map(|inner| inner.path_unkeyed().into_iter().collect::<Vec<_>>())
.unwrap_or_default()
}
fn reader(&self) -> Option<Self::Reader> {
self.inner.try_get_value().and_then(|inner| inner.reader())
}

View File

@@ -80,6 +80,13 @@ where
.chain(iter::once(self.index.into()))
}
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner
.path_unkeyed()
.into_iter()
.chain(iter::once(self.index.into()))
}
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
self.inner.get_trigger(path)
}

View File

@@ -106,6 +106,13 @@ where
.chain(iter::once(self.path_segment))
}
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner
.path_unkeyed()
.into_iter()
.chain(iter::once(self.path_segment))
}
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
self.inner.get_trigger(path)
}
@@ -133,13 +140,20 @@ where
}
fn track_field(&self) {
let inner = self
.inner
.get_trigger(self.inner.path().into_iter().collect());
inner.this.track();
let mut full_path = self.path().into_iter().collect::<StorePath>();
let trigger = self.get_trigger(self.path().into_iter().collect());
trigger.this.track();
trigger.children.track();
// tracks `this` for all ancestors: i.e., it will track any change that is made
// directly to one of its ancestors, but not a change made to a *child* of an ancestor
// (which would end up with every subfield tracking its own siblings, because they are
// children of its parent)
while !full_path.is_empty() {
full_path.pop();
let inner = self.get_trigger(full_path.clone());
inner.this.track();
}
}
}
@@ -169,6 +183,7 @@ where
{
inner: KeyedSubfield<Inner, Prev, K, T>,
guard: Option<Guard>,
untracked: bool,
}
impl<Inner, Prev, K, T, Guard> Deref
@@ -220,6 +235,7 @@ where
K: Debug + Send + Sync + PartialEq + Eq + Hash + 'static,
{
fn untrack(&mut self) {
self.untracked = true;
if let Some(inner) = self.guard.as_mut() {
inner.untrack();
}
@@ -244,7 +260,10 @@ where
// now that the write lock is release, we can get a read lock to refresh this keyed field
// based on the new value
self.inner.update_keys();
self.inner.notify();
if !self.untracked {
self.inner.notify();
}
// reactive updates happen on the next tick
}
@@ -337,6 +356,7 @@ where
Some(KeyedSubfieldWriteGuard {
inner: self.clone(),
guard: Some(guard),
untracked: false,
})
}
@@ -348,6 +368,7 @@ where
Some(KeyedSubfieldWriteGuard {
inner: self.clone(),
guard: Some(guard),
untracked: true,
})
}
}
@@ -444,6 +465,24 @@ where
inner.into_iter().chain(this)
}
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
let inner =
self.inner.path_unkeyed().into_iter().collect::<StorePath>();
let keys = self
.inner
.keys()
.expect("using keys on a store with no keys");
let this = keys
.with_field_keys(
inner.clone(),
|keys| (keys.get(&self.key), vec![]),
|| self.inner.latest_keys(),
)
.flatten()
.map(|(_, idx)| StorePathSegment(idx));
inner.into_iter().chain(this)
}
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
self.inner.get_trigger(path)
}
@@ -721,18 +760,19 @@ mod tests {
effect::Effect,
traits::{GetUntracked, ReadUntracked, Set, Track, Write},
};
use reactive_stores::Patch;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
#[derive(Debug, Store, Default)]
#[derive(Debug, Store, Default, Patch)]
struct Todos {
#[store(key: usize = |todo| todo.id)]
todos: Vec<Todo>,
}
#[derive(Debug, Store, Default, Clone, PartialEq, Eq)]
#[derive(Debug, Store, Default, Clone, PartialEq, Eq, Patch)]
struct Todo {
id: usize,
label: String,
@@ -853,4 +893,41 @@ mod tests {
assert_eq!(b_count.load(Ordering::Relaxed), 1);
assert_eq!(c_count.load(Ordering::Relaxed), 1);
}
#[tokio::test]
async fn untracked_write_on_keyed_subfield_shouldnt_notify() {
_ = any_spawner::Executor::init_tokio();
let store = Store::new(data());
assert_eq!(store.read_untracked().todos.len(), 3);
// create an effect to read from the keyed subfield
let todos_count = Arc::new(AtomicUsize::new(0));
Effect::new_sync({
let todos_count = Arc::clone(&todos_count);
move || {
store.todos().track();
todos_count.fetch_add(1, Ordering::Relaxed);
}
});
tick().await;
assert_eq!(todos_count.load(Ordering::Relaxed), 1);
// writing to keyed subfield notifies the iterator
store.todos().write().push(Todo {
id: 13,
label: "D".into(),
});
tick().await;
assert_eq!(todos_count.load(Ordering::Relaxed), 2);
// but an untracked write doesn't
store.todos().write_untracked().push(Todo {
id: 14,
label: "E".into(),
});
tick().await;
assert_eq!(todos_count.load(Ordering::Relaxed), 2);
}
}

View File

@@ -57,11 +57,11 @@
//! # if false { // don't run effect in doctests
//! Effect::new(move |_| {
//! // you can access individual store fields with a getter
//! println!("todos: {:?}", &*store.todos().read());
//! println!("user: {:?}", &*store.user().read());
//! });
//! # }
//!
//! // won't notify the effect that listens to `todos`
//! // won't notify the effect that listens to `user`
//! store.todos().write().push(Todo {
//! label: "Test".to_string(),
//! completed: false,
@@ -833,7 +833,7 @@ mod tests {
use reactive_graph::{
effect::Effect,
owner::StoredValue,
traits::{Read, ReadUntracked, Set, Update, Write},
traits::{Read, ReadUntracked, Set, Track, Update, Write},
};
use std::sync::{
atomic::{AtomicUsize, Ordering},
@@ -1375,4 +1375,34 @@ mod tests {
assert_eq!(combined_count.load(Ordering::Relaxed), 3);
}
#[tokio::test]
async fn untracked_write_on_subfield_shouldnt_notify() {
_ = any_spawner::Executor::init_tokio();
let name_count = Arc::new(AtomicUsize::new(0));
let store = Store::new(data());
let tracked_field = store.user();
Effect::new_sync({
let name_count = Arc::clone(&name_count);
move |_| {
tracked_field.track();
name_count.fetch_add(1, Ordering::Relaxed);
}
});
tick().await;
assert_eq!(name_count.load(Ordering::Relaxed), 1);
tracked_field.write().push('!');
tick().await;
assert_eq!(name_count.load(Ordering::Relaxed), 2);
tracked_field.write_untracked().push('!');
tick().await;
assert_eq!(name_count.load(Ordering::Relaxed), 2);
}
}

View File

@@ -30,7 +30,7 @@ where
type Value = T::Value;
fn patch(&self, new: Self::Value) {
let path = self.path().into_iter().collect::<StorePath>();
let path = self.path_unkeyed().into_iter().collect::<StorePath>();
if let Some(mut writer) = self.writer() {
// don't track the writer for the whole store
writer.untrack();

View File

@@ -38,6 +38,13 @@ pub trait StoreField: Sized {
#[track_caller]
fn path(&self) -> impl IntoIterator<Item = StorePathSegment>;
/// The path of this field (see [`StorePath`]). Uses unkeyed indices for any keyed fields.
#[track_caller]
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
// TODO remove default impl next time we do a breaking release
self.path()
}
/// Reactively tracks this field.
#[track_caller]
fn track_field(&self) {
@@ -129,7 +136,9 @@ where
trigger
}
#[track_caller]
fn get_trigger_unkeyed(&self, path: StorePath) -> StoreFieldTrigger {
let caller = std::panic::Location::caller();
let orig_path = path.clone();
let mut path = StorePath::with_capacity(orig_path.len());
@@ -140,7 +149,13 @@ where
let key = self
.keys
.get_key_for_index(&(path.clone(), segment.0))
.expect("could not find key for index");
.unwrap_or_else(|| {
panic!(
"could not find key for index {:?} at {}",
&(path.clone(), segment.0),
caller
)
});
path.push(key);
} else {
path.push(*segment);
@@ -154,6 +169,11 @@ where
iter::empty()
}
#[track_caller]
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
iter::empty()
}
#[track_caller]
fn reader(&self) -> Option<Self::Reader> {
Plain::try_new(Arc::clone(&self.value))
@@ -205,6 +225,14 @@ where
.unwrap_or_default()
}
#[track_caller]
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner
.try_get_value()
.map(|n| n.path_unkeyed().into_iter().collect::<Vec<_>>())
.unwrap_or_default()
}
#[track_caller]
fn reader(&self) -> Option<Self::Reader> {
self.inner.try_get_value().and_then(|n| n.reader())

View File

@@ -84,6 +84,13 @@ where
.chain(iter::once(self.path_segment))
}
fn path_unkeyed(&self) -> impl IntoIterator<Item = StorePathSegment> {
self.inner
.path_unkeyed()
.into_iter()
.chain(iter::once(self.path_segment))
}
fn get_trigger(&self, path: StorePath) -> StoreFieldTrigger {
self.inner.get_trigger(path)
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.8.10"
version = "0.8.11"
authors = ["Greg Johnston", "Ben Wishovich"]
license = "MIT"
readme = "../README.md"

View File

@@ -658,7 +658,7 @@ pub fn RoutingProgress(
move |prev: Option<Option<IntervalHandle>>| {
if is_routing.get() && !is_showing.get() {
set_is_showing.set(true);
set_interval_with_handle(
set_interval(
move || {
set_progress.update(|n| *n += percent_per_increment);
},
@@ -670,7 +670,7 @@ pub fn RoutingProgress(
prev?
} else {
set_progress.set(100.0);
set_timeout(
_ = set_timeout(
move || {
set_progress.set(0.0);
set_is_showing.set(false);

View File

@@ -131,14 +131,14 @@ where
if !IS_NAVIGATING.load(Ordering::Relaxed) {
IS_NAVIGATING.store(true, Ordering::Relaxed);
request_animation_frame({
_ = request_animation_frame({
let navigate = navigate.clone();
let nav_options = nav_options.clone();
move || {
navigate(&new_url, nav_options.clone());
IS_NAVIGATING.store(false, Ordering::Relaxed)
}
})
});
}
});

View File

@@ -259,7 +259,7 @@ impl LocationProvider for BrowserUrl {
if url.origin() == current_origin {
let navigate = navigate.clone();
// delay by a tick here, so that the Action updates *before* the redirect
request_animation_frame(move || {
_ = request_animation_frame(move || {
navigate(&url.href(), Default::default());
});
// Use set_href() if the conditions for client-side navigation were not satisfied

View File

@@ -199,31 +199,56 @@ where
}
}
// TODO can we support Option<T> and T in a non-nightly way?
#[cfg(all(feature = "nightly", rustc_nightly))]
mod option_param {
use super::{IntoParam, ParamsError};
/// Helpers for the `Params` derive macro to allow specialization without nightly.
pub mod macro_helpers {
use crate::params::{IntoParam, ParamsError};
use std::{str::FromStr, sync::Arc};
auto trait NotOption {}
impl<T> !NotOption for Option<T> {}
/// This struct is never actually created; it just exists so that we can impl associated
/// functions on it.
pub struct Wrapper<T>(T);
impl<T> IntoParam for T
where
T: FromStr + NotOption,
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
{
fn into_param(
impl<T: IntoParam> Wrapper<T> {
/// This is the 'preferred' impl to be used for all `T` that implement `IntoParam`.
/// Because it is directly on the struct, the compiler will pick this over the impl from
/// the `Fallback` trait.
#[inline]
pub fn __into_param(
value: Option<&str>,
name: &str,
) -> Result<Self, ParamsError> {
let value = value
.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
Self::from_str(value).map_err(|e| ParamsError::Params(Arc::new(e)))
) -> Result<T, ParamsError> {
T::into_param(value, name)
}
}
}
/// If the Fallback trait is in scope, then the compiler has two possible implementations for
/// `__into_params`. It will pick the one from this trait if the inherent one doesn't exist.
/// (which it won't if `T` does not implement `IntoParam`)
pub trait Fallback<T>: Sized
where
T: FromStr,
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
{
/// Fallback function in case the inherent impl on the Wrapper struct does not exist for
/// `T`
#[inline]
fn __into_param(
value: Option<&str>,
name: &str,
) -> Result<T, ParamsError> {
let value = value
.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
T::from_str(value).map_err(|e| ParamsError::Params(Arc::new(e)))
}
}
impl<T> Fallback<T> for Wrapper<T>
where
T: FromStr,
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
{
}
}
/// Errors that can occur while parsing params using [`Params`].
#[derive(Error, Debug, Clone)]
pub enum ParamsError {

View File

@@ -5,7 +5,7 @@ license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
description = "RPC for any web framework."
readme = "../README.md"
version = "0.8.8"
version = "0.8.9"
rust-version.workspace = true
edition.workspace = true
@@ -32,7 +32,7 @@ dashmap = { workspace = true, default-features = true }
## servers
# actix
actix-web = { optional = true, workspace = true, default-features = false }
actix-web = { optional = true, workspace = true, default-features = false, features = ["ws"] }
actix-ws = { optional = true, workspace = true, default-features = true }
# axum

View File

@@ -58,7 +58,8 @@ where
{
async fn from_req(req: Request) -> Result<Self, E> {
let string_data = req.as_query().unwrap_or_default();
let args = serde_qs::Config::new(5, false)
let args = serde_qs::Config::new()
.use_form_encoding(true)
.deserialize_str::<Self>(string_data)
.map_err(|e| {
ServerFnErrorErr::Args(e.to_string()).into_app_error()
@@ -97,7 +98,8 @@ where
{
async fn from_req(req: Request) -> Result<Self, E> {
let string_data = req.try_into_string().await?;
let args = serde_qs::Config::new(5, false)
let args = serde_qs::Config::new()
.use_form_encoding(true)
.deserialize_str::<Self>(&string_data)
.map_err(|e| {
ServerFnErrorErr::Args(e.to_string()).into_app_error()
@@ -136,7 +138,8 @@ where
{
async fn from_req(req: Request) -> Result<Self, E> {
let string_data = req.as_query().unwrap_or_default();
let args = serde_qs::Config::new(5, false)
let args = serde_qs::Config::new()
.use_form_encoding(true)
.deserialize_str::<Self>(string_data)
.map_err(|e| {
ServerFnErrorErr::Args(e.to_string()).into_app_error()
@@ -175,7 +178,8 @@ where
{
async fn from_req(req: Request) -> Result<Self, E> {
let string_data = req.try_into_string().await?;
let args = serde_qs::Config::new(5, false)
let args = serde_qs::Config::new()
.use_form_encoding(true)
.deserialize_str::<Self>(&string_data)
.map_err(|e| {
ServerFnErrorErr::Args(e.to_string()).into_app_error()
@@ -214,7 +218,8 @@ where
{
async fn from_req(req: Request) -> Result<Self, E> {
let string_data = req.try_into_string().await?;
let args = serde_qs::Config::new(5, false)
let args = serde_qs::Config::new()
.use_form_encoding(true)
.deserialize_str::<Self>(&string_data)
.map_err(|e| {
ServerFnErrorErr::Args(e.to_string()).into_app_error()

View File

@@ -309,6 +309,16 @@ impl ServerFnCall {
.collect::<TokenStream2>()
}
/// Get the lint attributes for the server function.
pub fn lint_attrs(&self) -> TokenStream2 {
// pass through lint attributes from the function body
self.body
.lint_attrs
.iter()
.map(|lint_attr| quote!(#lint_attr))
.collect::<TokenStream2>()
}
fn fn_name_as_str(&self) -> String {
self.body.ident.to_string()
}
@@ -388,6 +398,8 @@ impl ServerFnCall {
PathInfo::None => quote! {},
};
let lint_attrs = self.lint_attrs();
let vis = &self.body.vis;
let struct_name = self.struct_name();
let fields = self
@@ -410,6 +422,7 @@ impl ServerFnCall {
#docs
#[derive(Debug, #derives)]
#addl_path
#lint_attrs
#vis struct #struct_name {
#(#fields),*
}
@@ -1465,6 +1478,8 @@ pub struct ServerFnBody {
pub block: TokenStream2,
/// The documentation of the server function.
pub docs: Vec<(String, Span)>,
/// The lint attributes of the server function.
pub lint_attrs: Vec<Attribute>,
/// The middleware attributes applied to the server function.
pub middlewares: Vec<Middleware>,
}
@@ -1522,6 +1537,27 @@ impl Parse for ServerFnBody {
};
!attr.path.is_ident("doc")
});
let lint_attrs = attrs
.iter()
.filter_map(|attr| {
if attr.path().is_ident("allow")
|| attr.path().is_ident("warn")
|| attr.path().is_ident("deny")
|| attr.path().is_ident("forbid")
{
return Some(attr.clone());
}
None
})
.collect();
attrs.retain(|attr| {
!attr.path().is_ident("allow")
&& !attr.path().is_ident("warn")
&& !attr.path().is_ident("deny")
&& !attr.path().is_ident("forbid")
});
// extract all #[middleware] attributes, removing them from signature of dummy
let mut middlewares: Vec<Middleware> = vec![];
attrs.retain(|attr| {
@@ -1557,6 +1593,7 @@ impl Parse for ServerFnBody {
block,
attrs,
docs,
lint_attrs,
middlewares,
})
}

View File

@@ -72,6 +72,7 @@ web-sys = { features = [
"SecurityPolicyViolationEvent",
"StorageEvent",
"SubmitEvent",
"ToggleEvent",
"TouchEvent",
"TransitionEvent",
"UiEvent",
@@ -179,6 +180,7 @@ default = ["testing"]
delegation = [] # enables event delegation
error-hook = []
hydrate = []
lazy = []
islands = ["dep:serde", "dep:serde_json"]
ssr = []
oco = ["dep:oco_ref"]

View File

@@ -608,7 +608,7 @@ generate_event_types! {
animation start: AnimationEvent,
aux click: MouseEvent,
before input: InputEvent,
before toggle: Event, // web_sys does not include `ToggleEvent`
before toggle: ToggleEvent,
#[does_not_bubble]
blur: FocusEvent,
#[does_not_bubble]
@@ -719,7 +719,7 @@ generate_event_types! {
#[does_not_bubble]
time update: Event,
#[does_not_bubble]
toggle: Event,
toggle: ToggleEvent,
touch cancel: TouchEvent,
touch end: TouchEvent,
touch move: TouchEvent,

View File

@@ -17,7 +17,7 @@ use crate::{
};
use futures::future::{join, join_all};
use std::{any::TypeId, fmt::Debug};
#[cfg(any(feature = "ssr", feature = "hydrate"))]
#[cfg(any(feature = "ssr", all(feature = "hydrate", feature = "lazy")))]
use std::{future::Future, pin::Pin};
/// A type-erased view. This can be used if control flow requires that multiple different types of
@@ -67,10 +67,10 @@ pub struct AnyView {
resolve: fn(Erased) -> Pin<Box<dyn Future<Output = AnyView> + Send>>,
#[cfg(feature = "ssr")]
dry_resolve: fn(&mut Erased),
#[cfg(feature = "hydrate")]
#[cfg(all(feature = "hydrate", not(feature = "lazy")))]
#[allow(clippy::type_complexity)]
hydrate_from_server: fn(Erased, &Cursor, &PositionState) -> AnyViewState,
#[cfg(feature = "hydrate")]
#[cfg(all(feature = "hydrate", feature = "lazy"))]
#[allow(clippy::type_complexity)]
hydrate_async: fn(
Erased,
@@ -291,7 +291,7 @@ where
}
}
#[cfg(feature = "hydrate")]
#[cfg(all(feature = "hydrate", not(feature = "lazy")))]
fn hydrate_from_server<T: RenderHtml + 'static>(
value: Erased,
cursor: &Cursor,
@@ -313,7 +313,7 @@ where
}
}
#[cfg(feature = "hydrate")]
#[cfg(all(feature = "hydrate", feature = "lazy"))]
fn hydrate_async<T: RenderHtml + 'static>(
value: Erased,
cursor: &Cursor,
@@ -367,9 +367,9 @@ where
to_html_async: to_html_async::<T::Owned>,
#[cfg(feature = "ssr")]
to_html_async_ooo: to_html_async_ooo::<T::Owned>,
#[cfg(feature = "hydrate")]
#[cfg(all(feature = "hydrate", not(feature = "lazy")))]
hydrate_from_server: hydrate_from_server::<T::Owned>,
#[cfg(feature = "hydrate")]
#[cfg(all(feature = "hydrate", feature = "lazy"))]
hydrate_async: hydrate_async::<T::Owned>,
value: Erased::new(value),
}
@@ -572,7 +572,7 @@ impl RenderHtml for AnyView {
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
#[cfg(feature = "hydrate")]
#[cfg(all(feature = "hydrate", not(feature = "lazy")))]
{
if FROM_SERVER {
(self.hydrate_from_server)(self.value, cursor, position)
@@ -583,6 +583,14 @@ impl RenderHtml for AnyView {
);
}
}
#[cfg(all(feature = "hydrate", feature = "lazy"))]
{
use futures::FutureExt;
(self.hydrate_async)(self.value, cursor, position)
.now_or_never()
.unwrap()
}
#[cfg(not(feature = "hydrate"))]
{
_ = cursor;
@@ -599,12 +607,22 @@ impl RenderHtml for AnyView {
cursor: &Cursor,
position: &PositionState,
) -> Self::State {
#[cfg(feature = "hydrate")]
#[cfg(all(feature = "hydrate", feature = "lazy"))]
{
#[cfg(all(feature = "hydrate", feature = "lazy"))]
let state =
(self.hydrate_async)(self.value, cursor, position).await;
state
}
#[cfg(all(feature = "hydrate", not(feature = "lazy")))]
{
_ = cursor;
_ = position;
panic!(
"the `lazy` feature on `tachys` must be activated to use lazy \
hydration"
);
}
#[cfg(not(feature = "hydrate"))]
{
_ = cursor;
@@ -693,13 +711,18 @@ impl Render for AnyViewWithAttrs {
fn rebuild(self, state: &mut Self::State) {
self.view.rebuild(&mut state.view);
let elements = state.elements();
// FIXME this seems wrong but I think the previous version was also broken!
if let Some(element) = elements.first() {
self.attrs.rebuild(&mut (
element.clone(),
std::mem::take(&mut state.attrs),
));
// at this point, we have rebuilt the inner view
// now we need to update attributes that were spread onto this
// this approach is not ideal, but it avoids two edge cases:
// 1) merging attributes from two unrelated views (https://github.com/leptos-rs/leptos/issues/4268)
// 2) failing to re-create attributes from the same view (https://github.com/leptos-rs/leptos/issues/4512)
for element in state.elements() {
// first, remove the previous set of attributes
self.attrs
.clone()
.rebuild(&mut (element.clone(), Vec::new()));
// then, add the new set of attributes
self.attrs.clone().build(&element);
}
}
}
@@ -824,6 +847,7 @@ impl AddAnyAttr for AnyViewWithAttrs {
/// State for any view with attributes spread onto it.
pub struct AnyViewWithAttrsState {
view: AnyViewState,
#[allow(dead_code)] // keeps attribute states alive until dropped
attrs: Vec<AnyAttributeState>,
}

View File

@@ -23,14 +23,32 @@ pub fn keyed<T, I, K, KF, VF, VFS, V>(
) -> Keyed<T, I, K, KF, VF, VFS, V>
where
I: IntoIterator<Item = T>,
K: Eq + Hash + 'static,
K: Eq + Hash + SerializableKey + 'static,
KF: Fn(&T) -> K,
V: Render,
VF: Fn(usize, T) -> (VFS, V),
VFS: Fn(usize),
{
Keyed {
items,
#[cfg(not(feature = "ssr"))]
items: Some(items),
#[cfg(feature = "ssr")]
items: None,
#[cfg(feature = "ssr")]
ssr_items: items
.into_iter()
.enumerate()
.map(|(i, t)| {
let key = if cfg!(feature = "islands") {
let key = (key_fn)(&t);
key.ser_key()
} else {
String::new()
};
let (_, view) = (view_fn)(i, t);
(key, view)
})
.collect::<Vec<_>>(),
key_fn,
view_fn,
}
@@ -45,7 +63,9 @@ where
VF: Fn(usize, T) -> (VFS, V),
VFS: Fn(usize),
{
items: I,
items: Option<I>,
#[cfg(feature = "ssr")]
ssr_items: Vec<(String, V)>,
key_fn: KF,
view_fn: VF,
}
@@ -106,14 +126,13 @@ where
VFS: Fn(usize),
{
type State = KeyedState<K, VFS, V>;
// TODO fallible state and try_build()/try_rebuild() here
fn build(self) -> Self::State {
let items = self.items.into_iter();
let items = self.items.into_iter().flatten();
let (capacity, _) = items.size_hint();
let mut hashed_items =
FxIndexSet::with_capacity_and_hasher(capacity, Default::default());
let mut rendered_items = Vec::new();
let mut rendered_items = Vec::with_capacity(capacity);
for (index, item) in items.enumerate() {
hashed_items.insert((self.key_fn)(&item));
let (set_index, view) = (self.view_fn)(index, item);
@@ -134,7 +153,7 @@ where
hashed_items,
ref mut rendered_items,
} = state;
let new_items = self.items.into_iter();
let new_items = self.items.into_iter().flatten();
let (capacity, _) = new_items.size_hint();
let mut new_hashed_items =
FxIndexSet::with_capacity_and_hasher(capacity, Default::default());
@@ -198,6 +217,8 @@ where
{
let Keyed {
items,
#[cfg(feature = "ssr")]
ssr_items,
key_fn,
view_fn,
} = self;
@@ -205,6 +226,11 @@ where
Keyed {
items,
key_fn,
#[cfg(feature = "ssr")]
ssr_items: ssr_items
.into_iter()
.map(|(k, v)| (k, v.add_any_attr(attr.clone())))
.collect(),
view_fn: Box::new(move |index, item| {
let (index, view) = view_fn(index, item);
(index, view.add_any_attr(attr.clone()))
@@ -229,21 +255,39 @@ where
const MIN_LENGTH: usize = 0;
fn dry_resolve(&mut self) {
// TODO...
#[cfg(feature = "ssr")]
for view in &mut self.ssr_items {
view.dry_resolve();
}
}
async fn resolve(self) -> Self::AsyncOutput {
futures::future::join_all(self.items.into_iter().enumerate().map(
|(index, item)| {
let (_, view) = (self.view_fn)(index, item);
view.resolve()
},
))
.await
.into_iter()
.collect::<Vec<_>>()
#[cfg(feature = "ssr")]
{
futures::future::join_all(
self.ssr_items.into_iter().map(|(_, view)| view.resolve()),
)
.await
.into_iter()
.collect::<Vec<_>>()
}
#[cfg(not(feature = "ssr"))]
{
futures::future::join_all(
self.items.into_iter().flatten().enumerate().map(
|(index, item)| {
let (_, view) = (self.view_fn)(index, item);
view.resolve()
},
),
)
.await
.into_iter()
.collect::<Vec<_>>()
}
}
#[allow(unused)]
fn to_html_with_buf(
self,
buf: &mut String,
@@ -255,8 +299,9 @@ where
if mark_branches && escape {
buf.open_branch("for");
}
for (index, item) in self.items.into_iter().enumerate() {
let (_, item) = (self.view_fn)(index, item);
#[cfg(feature = "ssr")]
for item in self.ssr_items {
if mark_branches && escape {
buf.open_branch("item");
}
@@ -278,6 +323,7 @@ where
buf.push_str("<!>");
}
#[allow(unused)]
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
self,
buf: &mut StreamBuilder,
@@ -289,13 +335,10 @@ where
if mark_branches && escape {
buf.open_branch("for");
}
for (index, item) in self.items.into_iter().enumerate() {
let branch_name = mark_branches.then(|| {
let key = (self.key_fn)(&item);
let key = key.ser_key();
format!("item-{key}")
});
let (_, item) = (self.view_fn)(index, item);
#[cfg(feature = "ssr")]
for (key, item) in self.ssr_items {
let branch_name = mark_branches.then(|| format!("item-{key}"));
if mark_branches && escape {
buf.open_branch(branch_name.as_ref().unwrap());
}
@@ -311,6 +354,7 @@ where
}
*position = Position::NextChild;
}
if mark_branches && escape {
buf.close_branch("for");
}
@@ -334,11 +378,11 @@ where
.expect("parent of keyed list should be an element");
// build list
let items = self.items.into_iter();
let items = self.items.into_iter().flatten();
let (capacity, _) = items.size_hint();
let mut hashed_items =
FxIndexSet::with_capacity_and_hasher(capacity, Default::default());
let mut rendered_items = Vec::new();
let mut rendered_items = Vec::with_capacity(capacity);
for (index, item) in items.enumerate() {
hashed_items.insert((self.key_fn)(&item));
let (set_index, view) = (self.view_fn)(index, item);
@@ -373,11 +417,11 @@ where
.expect("parent of keyed list should be an element");
// build list
let items = self.items.into_iter();
let items = self.items.into_iter().flatten();
let (capacity, _) = items.size_hint();
let mut hashed_items =
FxIndexSet::with_capacity_and_hasher(capacity, Default::default());
let mut rendered_items = Vec::new();
let mut rendered_items = Vec::with_capacity(capacity);
for (index, item) in items.enumerate() {
hashed_items.insert((self.key_fn)(&item));
let (set_index, view) = (self.view_fn)(index, item);