Compare commits

...

74 Commits

Author SHA1 Message Date
Greg Johnston
55194c6c2e fix: fix node_ref in SSR 2023-02-04 13:57:35 -05:00
Roland Fredenhagen
5d612d9740 error on non meta input for prop attribute (#469) 2023-02-04 13:17:04 -05:00
John Funk
eacff684ef Add simple icon logo (#468) 2023-02-04 10:19:33 -05:00
Greg Johnston
4034aa9c11 feature: add isomorphic <Redirect/> component (closes #412) (#466) 2023-02-04 10:02:17 -05:00
Roland Fredenhagen
45275ff8d4 impl Default for MaybeSignal (#464) 2023-02-04 10:01:55 -05:00
Greg Johnston
3ff5089bf4 docs: note about optional fallback (closes #406) (#463) 2023-02-04 08:34:38 -05:00
Jan
c28297fe93 Do it on an other branch (#460) 2023-02-04 07:12:53 -05:00
Greg Johnston
6d0d70cd17 perf: further reduce WASM binary size by ~5-7% (#459)
* Update `leptos_router` docs
* Further reducing WASM bundle sizes
2023-02-03 17:38:44 -05:00
g-re-g
c4e693e01e Derive debug in server macro (#458) 2023-02-03 17:38:29 -05:00
Greg Johnston
2be4e8d959 docs: add new Children types to macro docs (#454) 2023-02-03 12:51:37 -05:00
Odiseo
fec4ff4381 fix: typo in leptos_config description (#455) 2023-02-03 12:51:26 -05:00
Greg Johnston
25c313aeb5 fix: stack overflow in with nested outlet (closes #452) (#453) 2023-02-03 11:03:02 -05:00
martin frances
0dbcc323ba Clippy: "{input} is not a supported environment. (#451) 2023-02-03 10:08:23 -05:00
Greg Johnston
6b683f9ab6 fix: leptos_router hydration issues (#450) 2023-02-03 06:50:36 -05:00
Tobias Goulden Schultz
aae4d4445e fix: update leptos dependencies to point to the same workspace as other examples (#449) 2023-02-02 23:24:22 -05:00
Greg Johnston
bb9df8937d feature: allow on: event listeners on <Component/> nodes (#448) 2023-02-02 23:24:03 -05:00
Greg Johnston
05277f03b6 fix: successfully pass context to nested routes via <Outlet/> (#447) 2023-02-02 21:00:32 -05:00
Gentle
f698f8badd use latest tokio in leptos_axum (#443) 2023-02-02 17:00:49 -05:00
martin frances
98f51fec8a router: Machete - Removed unused deps. (#442) 2023-02-02 17:00:12 -05:00
martin frances
65465cad78 leptos_macro: Machete - Removed unused deps. (#441) 2023-02-02 16:59:49 -05:00
martin frances
ddee545e7e leptos-server: Removed dependecy on log, linear-map, rmp-serde. (#439) 2023-02-02 16:59:07 -05:00
g-re-g
cbfb724af2 Dedup from_str implementations for Env (#426) 2023-02-02 07:18:20 -05:00
Greg Johnston
0953007f47 fix: correct behavior of <Show/> so it renders correctly when toggling between conditions multiple times, without rerendering on every change (#436) 2023-02-01 20:37:00 -05:00
Greg Johnston
53f7677258 Fix top-level SVG elements in SSR (#435) 2023-02-01 20:36:50 -05:00
Greg Johnston
6373fd42fb Switch examples to check instead of build (for CI resources) and add missing examples (#437) 2023-02-01 20:36:37 -05:00
Greg Johnston
e1bcf77b03 docs: Document inner_html attribute (#429) 2023-02-01 19:21:08 -05:00
Greg Johnston
b0762bbfb5 Make RouteDefinition public (#430) 2023-02-01 19:20:50 -05:00
IcosaHedron
63a7a4dec1 Several Minor Updates on Examples (#427) 2023-02-01 19:20:34 -05:00
jquesada2016
1f6a326268 fixes cx not found on components marked with #[component(transparent)] (#423) 2023-02-01 11:17:20 -05:00
Greg Johnston
0efc39db8b fix: Make all fragment rendering lazy (closes #299 and #421) (#425)
Make all fragment rendering lazy (closes #299 and #421)
2023-02-01 06:47:12 -05:00
Greg Johnston
cbf2f73e95 fix: HTML entity issues in axum_errors example (#424) 2023-01-31 23:39:31 -05:00
Ben Wishovich
160f336303 Update ErrorBoundary to use miette::Diagnostic instead of Error, and various other tweaks (#401)
* Switch RwLock to parking_lot so they are no longer async
* cleanup todo_app_sqlite_axum
* add errors_axum example

---------

Co-authored-by: Indrazar <110272232+Indrazar@users.noreply.github.com>
2023-01-31 21:56:42 -05:00
starmaker
e2b1365e46 Implemented update_returning for StoredValue (#419) 2023-01-31 17:40:39 -05:00
Greg Johnston
45eee12b18 Fix issues with attribute names in SSR (#418) 2023-01-31 11:57:05 -05:00
Bruno De Simone
e2cdbc746f Add leptos_routes functions for integrations (#415)
* added leptos_routes_with_context

* added leptos_routes_with_handler for axum integration
2023-01-31 09:09:58 -05:00
Ben Wishovich
48cf8d9382 Switch RwLock to parking_lot so they are no longer async (#414) 2023-01-30 20:11:56 -05:00
Greg Johnston
42e50327a6 Fix <option> and <use> top-level types in SSR (#416) 2023-01-30 20:10:07 -05:00
martin frances
ea0e2ce363 Escape <HTML> and <BODY> tokens in documentation markup. (#410) 2023-01-30 19:17:41 -05:00
martin frances
465cbc36be Minor: Bump typed-builder from 0.11 to 0.12. (#409) 2023-01-30 19:17:09 -05:00
Greg Johnston
62061f90ea Add <Html/> and <Body/> components in leptos_meta (#407)
Closes #376.
2023-01-29 19:07:48 -05:00
Greg Johnston
9a231ddef0 Merge pull request #408 from leptos-rs/fix-boolean-attributes-ssr
Fix boolean attributes in `view` macro fast-path SSR
2023-01-29 18:43:21 -05:00
Greg Johnston
ce6a093f9f oops 2023-01-29 17:11:02 -05:00
Greg Johnston
f07fa0e0be escape attributes 2023-01-29 16:55:28 -05:00
Greg Johnston
43ad91512a Fixes boolean attributes in SSR (closes #405) 2023-01-29 16:29:06 -05:00
Greg Johnston
116d23f2c3 Revert "fix: Fixes boolean attributes in HTML fast-path (closes issue #405)"
This reverts commit 2ecb345a79.
2023-01-29 16:27:28 -05:00
Greg Johnston
2ecb345a79 fix: Fixes boolean attributes in HTML fast-path (closes issue #405) 2023-01-29 16:02:47 -05:00
Greg Johnston
f55f833426 Merge pull request #403 from leptos-rs/children-type-alias
Add `Children` type alias
2023-01-29 07:07:09 -05:00
Greg Johnston
7101a2f55e Add Children type alias 2023-01-28 22:32:00 -05:00
Greg Johnston
f8b76387ec Fix labels in parent_child README 2023-01-28 21:52:16 -05:00
Greg Johnston
11fc51577b 0.1.3 2023-01-28 12:12:09 -05:00
Greg Johnston
ae1ca969ef Merge pull request #397 from leptos-rs/v0.1.2
`v0.1.2`
2023-01-28 11:17:30 -05:00
Greg Johnston
895f9d8487 Missing web-sys types 2023-01-28 08:19:13 -05:00
Greg Johnston
1e45b182a0 Fix <ErrorBoundary/> removal behavior 2023-01-28 08:14:32 -05:00
Greg Johnston
4c26dc597d Docs for <Show/> component 2023-01-28 08:07:23 -05:00
Greg Johnston
2863d49a1c Docs for <ErrorBoundary/> 2023-01-28 07:54:13 -05:00
Greg Johnston
087eb18c8b Merge pull request #396 from leptos-rs/hydration-fix-small-wasm
Fix hydration issue related to WASM size reduction
2023-01-28 07:16:23 -05:00
Greg Johnston
c7c672717c Fix hydration issue related to WASM size reduction 2023-01-28 07:16:08 -05:00
Greg Johnston
c69cc02f30 Merge pull request #393 from leptos-rs/small-wasm
Reduce WASM binary sizes by 3-5%
2023-01-28 07:08:52 -05:00
Greg Johnston
9eb81f00f9 Merge pull request #395 from thomasqueirozb/main
Fix gtk example
2023-01-28 07:08:43 -05:00
Thomas Queiroz
72fe3d45f0 Fix gtk example 2023-01-28 01:14:02 -03:00
Greg Johnston
7802d941bd cargo fmt 2023-01-27 17:04:34 -05:00
Greg Johnston
f10784f686 Merge pull request #391 from leptos-rs/remove-gloo-dependency
Remove `gloo` dependency in `leptos_dom`
2023-01-27 16:58:54 -05:00
Greg Johnston
35197691c0 Merge pull request #392 from martinfrances107/cargo_outdated
doc/book updated leptos version.
2023-01-27 16:58:41 -05:00
Greg Johnston
4afbef87f6 clippy stuff 2023-01-27 16:56:22 -05:00
Greg Johnston
218485e3be Make helpers into concrete functions for WASM binary size purposes 2023-01-27 16:24:05 -05:00
Greg Johnston
8d60a191eb Missing Storage dependency (now that gloo is gone) 2023-01-27 15:36:20 -05:00
Greg Johnston
1ba01a46af Use a concrete helper function to generate elements 2023-01-27 15:33:28 -05:00
Martin
fdece25051 BugFix, ch03 properly construct the "input_element". 2023-01-27 20:04:29 +00:00
Greg Johnston
590056e047 Remove gloo dependency in leptos_dom 2023-01-27 14:01:07 -05:00
Martin
817bb1628e doc/book updated leptos version. 2023-01-27 19:00:31 +00:00
Greg Johnston
f911cdd56f Merge pull request #390 from leptos-rs/action-form-clear-input
fix: Align `<ActionForm/>` behavior with `Action`
2023-01-27 13:53:31 -05:00
Greg Johnston
76a9c719a3 Fix missing docs error (#389) 2023-01-27 12:29:22 -05:00
Greg Johnston
395336a8c0 Correctly set pending state with ActionForm 2023-01-27 12:19:20 -05:00
Greg Johnston
b84906e6dc ActionForm should clear input as Action::dispatch() does 2023-01-27 12:15:50 -05:00
100 changed files with 2120 additions and 865 deletions

View File

@@ -24,17 +24,17 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.1.1"
version = "0.1.3"
[workspace.dependencies]
leptos = { path = "./leptos", default-features = false, version = "0.1.1" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.1.1" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.1.1" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.1.1" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.1.1" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.1.1" }
leptos_router = { path = "./router", version = "0.1.1" }
leptos_meta = { path = "./meta", default-feature = false, version = "0.1.1" }
leptos = { path = "./leptos", default-features = false, version = "0.1.3" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.1.3" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.1.3" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.1.3" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.1.3" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.1.3" }
leptos_router = { path = "./router", version = "0.1.3" }
leptos_meta = { path = "./meta", default-feature = false, version = "0.1.3" }
[profile.release]
codegen-units = 1

View File

@@ -8,7 +8,7 @@
default_to_workspace = false
[tasks.ci]
dependencies = ["build", "build-examples", "test"]
dependencies = ["build", "check-examples", "test"]
[tasks.build]
clear = true
@@ -19,22 +19,24 @@ command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.build-examples]
[tasks.check-examples]
clear = true
dependencies = [
{ name = "build", path = "examples/counter" },
{ name = "build", path = "examples/counter_isomorphic" },
{ name = "build", path = "examples/counters" },
{ name = "build", path = "examples/counters_stable" },
{ name = "build", path = "examples/fetch" },
{ name = "build", path = "examples/hackernews" },
{ name = "build", path = "examples/hackernews_axum" },
{ name = "build", path = "examples/parent_child" },
{ name = "build", path = "examples/router" },
{ name = "build", path = "examples/tailwind" },
{ name = "build", path = "examples/todo_app_sqlite" },
{ name = "build", path = "examples/todo_app_sqlite_axum" },
{ name = "build", path = "examples/todomvc" },
{ name = "check", path = "examples/counter" },
{ name = "check", path = "examples/counter_isomorphic" },
{ name = "check", path = "examples/counter_without_macros" },
{ name = "check", path = "examples/counters" },
{ name = "check", path = "examples/counters_stable" },
{ name = "check", path = "examples/errors_axum" },
{ name = "check", path = "examples/fetch" },
{ name = "check", path = "examples/hackernews" },
{ name = "check", path = "examples/hackernews_axum" },
{ name = "check", path = "examples/parent_child" },
{ name = "check", path = "examples/router" },
{ name = "check", path = "examples/tailwind" },
{ name = "check", path = "examples/todo_app_sqlite" },
{ name = "check", path = "examples/todo_app_sqlite_axum" },
{ name = "check", path = "examples/todomvc" },
]
[tasks.test]

View File

@@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.0.18"
leptos = "0.1"

View File

@@ -1,5 +1,5 @@
use leptos::*;
fn main() {
mount_to_body(|_cx| view! { cx, <p>"Hello, world!"</p> })
mount_to_body(|cx| view! { cx, <p>"Hello, world!"</p> })
}

View File

@@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.0.18"
leptos = "0.1"

View File

@@ -4,7 +4,9 @@ fn main() {
mount_to_body(|cx| {
let name = "gbj";
let userid = 0;
let _input_element: Element;
// This will be filled by _ref=input below.
let input_element = NodeRef::<HtmlElement<Input>>::new(cx);
view! {
cx,
@@ -17,7 +19,7 @@ fn main() {
prop:value="todo" // `prop:` lets you set a property on a DOM node
value="initial" // side note: the DOM `value` attribute only sets *initial* value
// this is very important when working with forms!
_ref=_input_element // `_ref` stores tis element in a variable
_ref=input_element // `_ref` stores tis element in a variable
/>
<ul data-user=userid> // attributes can take expressions as values
<li class="todo my-todo" // here we set the `class` attribute

View File

@@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.0.18"
leptos = "0.1"

View File

@@ -2,3 +2,8 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -2,3 +2,8 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -0,0 +1,9 @@
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -2,3 +2,8 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -0,0 +1,94 @@
[package]
name = "errors_axum"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1.0.66"
console_log = "0.2.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
cfg-if = "1.0.0"
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
"serde",
] }
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
leptos_meta = { path = "../../../leptos/meta", default-features = false }
leptos_router = { path = "../../../leptos/router", default-features = false }
leptos_reactive = { path = "../../../leptos/leptos_reactive", default-features = false }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
serde_json = "1.0.89"
gloo-net = { version = "0.2.5", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.3.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8" }
thiserror = "1.0.38"
tracing = "0.1.37"
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = ["dep:axum", "dep:tower", "dep:tower-http", "dep:tokio", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "dep:leptos_axum"]
[package.metadata.cargo-all-features]
denylist = [
"axum",
"tower",
"tower-http",
"tokio",
"leptos_axum",
]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "errors_axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Greg Johnston
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,9 @@
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -0,0 +1,41 @@
# Leptos Errors Demonstration with Axum
This example demonstrates how Leptos Errors can work with an Axum backend on a server.
## Client Side Rendering
This example cannot be built as a trunk standalone CSR-only app as it requires the server to send status codes.
## Server Side Rendering with cargo-leptos
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
1. Install cargo-leptos
```bash
cargo install --locked cargo-leptos
```
2. Build the site in watch mode, recompiling on file changes
```bash
cargo leptos watch
```
Open browser on [http://localhost:3000/](http://localhost:3000/)
3. When ready to deploy, run
```bash
cargo leptos build --release
```
## Server Side Rendering without cargo-leptos
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
1. Install wasm-pack
```bash
cargo install wasm-pack
```
2. Build the Webassembly used to hydrate the HTML from the server
```bash
wasm-pack build --target=web --debug --no-default-features --features=hydrate
```
3. Run the server to serve the Webassembly, JS, and HTML
```bash
cargo run --no-default-features --features=ssr
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,62 @@
use crate::errors::AppError;
use cfg_if::cfg_if;
use leptos::Errors;
use leptos::*;
#[cfg(feature = "ssr")]
use leptos_axum::ResponseOptions;
// A basic function to display errors served by the error boundaries.
// Feel free to do more complicated things here than just displaying them.
#[component]
pub fn ErrorTemplate(
cx: Scope,
#[prop(optional)] outside_errors: Option<Errors>,
#[prop(optional)] errors: Option<RwSignal<Errors>>,
) -> impl IntoView {
let errors = match outside_errors {
Some(e) => create_rw_signal(cx, e),
None => match errors {
Some(e) => e,
None => panic!("No Errors found and we expected errors!"),
},
};
// Get Errors from Signal
let errors = errors.get().0;
// Downcast lets us take a type that implements `std::error::Error`
let errors: Vec<AppError> = errors
.into_iter()
.filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned())
.collect();
println!("Errors: {errors:#?}");
// Only the response code for the first error is actually sent from the server
// this may be customized by the specific application
cfg_if! { if #[cfg(feature="ssr")] {
let response = use_context::<ResponseOptions>(cx);
if let Some(response) = response {
response.set_status(errors[0].status_code());
}
}}
view! { cx,
<h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each= move || {errors.clone().into_iter().enumerate()}
// a unique key for each item as a reference
key=|(index, _error)| *index
// renders each item to a view
view= move |error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! { cx,
<h2>{error_code.to_string()}</h2>
<p>"Error: " {error_string}</p>
}
}
/>
}
}

View File

@@ -0,0 +1,19 @@
use http::status::StatusCode;
use thiserror::Error;
#[derive(Debug, Clone, Error)]
pub enum AppError {
#[error("Not Found")]
NotFound,
#[error("Internal Server Error")]
InternalServerError,
}
impl AppError {
pub fn status_code(&self) -> StatusCode {
match self {
AppError::NotFound => StatusCode::NOT_FOUND,
AppError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}

View File

@@ -0,0 +1,45 @@
use cfg_if::cfg_if;
cfg_if! { if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::Extension,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions, Errors, view};
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
use crate::errors::AppError;
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else{
let mut errors = Errors::default();
errors.insert_with_default_key(AppError::NotFound);
let handler = leptos_axum::render_app_to_stream(options.to_owned(), move |cx| view!{cx, <ErrorTemplate outside_errors=errors.clone()/>});
handler(req).await.into_response()
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)),
}
}
}}

View File

@@ -0,0 +1,79 @@
use crate::{
error_template::{ErrorTemplate, ErrorTemplateProps},
errors::AppError,
};
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[cfg(feature = "ssr")]
pub fn register_server_functions() {
_ = CauseInternalServerError::register();
}
#[server(CauseInternalServerError, "/api")]
pub async fn cause_internal_server_error() -> Result<(), ServerFnError> {
// fake API delay
std::thread::sleep(std::time::Duration::from_millis(1250));
Err(ServerFnError::ServerError(
"Generic Server Error".to_string(),
))
}
#[component]
pub fn App(cx: Scope) -> impl IntoView {
//let id = use_context::<String>(cx);
provide_meta_context(cx);
view! {
cx,
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/errors_axum.css"/>
<Router>
<header>
<h1>"Error Examples:"</h1>
</header>
<main>
<Routes>
<Route path="" view=|cx| view! {
cx,
<ExampleErrors/>
}/>
</Routes>
</main>
</Router>
}
}
#[component]
pub fn ExampleErrors(cx: Scope) -> impl IntoView {
let generate_internal_error = create_server_action::<CauseInternalServerError>(cx);
view! { cx,
<p>
"These links will load 404 pages since they do not exist. Verify with browser development tools: " <br/>
<a href="/404">"This links to a page that does not exist"</a><br/>
<a href="/404" target="_blank">"Same link, but in a new tab"</a>
</p>
<p>
"After pressing this button check browser network tools. Can be used even when WASM is blocked:"
<ActionForm action=generate_internal_error>
<input name="error1" type="submit" value="Generate Internal Server Error"/>
</ActionForm>
</p>
<p>"The following <div> will always contain an error and cause this page to produce status 500. Check browser dev tools. "</p>
<div>
// note that the error boundries could be placed above in the Router or lower down
// in a particular route. The generated errors on the entire page contribue to the
// final status code sent by the server when producing ssr pages.
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
<ReturnsError/>
</ErrorBoundary>
</div>
}
}
#[component]
pub fn ReturnsError(_cx: Scope) -> impl IntoView {
Err::<String, AppError>(AppError::InternalServerError)
}

View File

@@ -0,0 +1,25 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod error_template;
pub mod errors;
pub mod fallback;
pub mod landing;
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
use crate::landing::*;
#[wasm_bindgen]
pub fn hydrate() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(|cx| {
view! { cx, <App/> }
});
}
}
}

View File

@@ -0,0 +1,72 @@
use cfg_if::cfg_if;
cfg_if! { if #[cfg(feature = "ssr")] {
use crate::fallback::file_and_error_handler;
use crate::landing::*;
use axum::body::Body as AxumBody;
use axum::{
extract::{Extension, Path},
http::Request,
response::{IntoResponse, Response},
routing::{get, post},
Router,
};
use errors_axum::*;
use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use std::sync::Arc;
}}
//Define a handler to test extractor with state
#[cfg(feature = "ssr")]
async fn custom_handler(
Path(id): Path<String>,
Extension(options): Extension<Arc<LeptosOptions>>,
req: Request<AxumBody>,
) -> Response {
let handler = leptos_axum::render_app_to_stream_with_context(
(*options).clone(),
move |cx| {
provide_context(cx, id.clone());
},
|cx| view! { cx, <App/> },
);
handler(req).await.into_response()
}
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
crate::landing::register_server_functions();
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_address;
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
// build our application with a route
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.route("/special/:id", get(custom_handler))
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> })
.fallback(file_and_error_handler)
.layer(Extension(Arc::new(leptos_options)));
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on http://{}", &addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
// this is if we were using client-only rending with Trunk
#[cfg(not(feature = "ssr"))]
pub fn main() {
// This example cannot be built as a trunk standalone CSR-only app.
// The server is needed to demonstrate the error statuses.
}

View File

@@ -0,0 +1,3 @@
.pending {
color: purple;
}

View File

@@ -2,3 +2,8 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -6,7 +6,7 @@ const APP_ID: &str = "dev.leptos.Counter";
// Basic GTK app setup from https://gtk-rs.org/gtk4-rs/stable/latest/book/hello_world.html
fn main() {
_ = create_scope(|cx| {
_ = create_scope(create_runtime(), |cx| {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();

View File

@@ -2,3 +2,8 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -1,4 +1,4 @@
use leptos::{component, Scope, IntoView, view};
use leptos::{component, view, IntoView, Scope};
use leptos_router::*;
#[component]
@@ -6,7 +6,7 @@ pub fn Nav(cx: Scope) -> impl IntoView {
view! { cx,
<header class="header">
<nav class="inner">
<A href="/">
<A href="/home">
<strong>"HN"</strong>
</A>
<A href="/new">

View File

@@ -2,3 +2,8 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -2,3 +2,8 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -2,12 +2,12 @@
This example highlights four different ways that child components can communicate with their parent:
1. <ButtonA/>: passing a WriteSignal as one of the child component props,
1. `<ButtonA/>`: passing a WriteSignal as one of the child component props,
for the child component to write into and the parent to read
2. <ButtonB/>: passing a closure as one of the child component props, for
2. `<ButtonB/>`: passing a closure as one of the child component props, for
the child component to call
3. <ButtonC/>: adding a simple event listener on the child component itself
4. <ButtonD/>: providing a context that is used in the component (rather than prop drilling)
3. `<ButtonC/>`: adding a simple event listener on the child component itself
4. `<ButtonD/>`: providing a context that is used in the component (rather than prop drilling)
## Client Side Rendering

View File

@@ -2,3 +2,8 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -20,6 +20,7 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
<A exact=true href="/">"Contacts"</A>
<A href="about">"About"</A>
<A href="settings">"Settings"</A>
<A href="redirect-home">"Redirect to Home"</A>
</nav>
<main>
<Routes>
@@ -44,6 +45,10 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
path="settings"
view=move |cx| view! { cx, <Settings/> }
/>
<Route
path="redirect-home"
view=move |cx| view! { cx, <Redirect path="/"/> }
/>
</Routes>
</main>
</Router>

View File

@@ -2,3 +2,8 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -2,3 +2,8 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -12,13 +12,13 @@ console_log = "0.2.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
cfg-if = "1.0.0"
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
] }
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
leptos_meta = { path = "../../../leptos/meta", default-features = false }
leptos_router = { path = "../../../leptos/router", default-features = false }
leptos_reactive = { path = "../../../leptos/leptos_reactive", default-features = false }
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
leptos_reactive = { path = "../../leptos_reactive", default-features = false }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
@@ -34,12 +34,14 @@ sqlx = { version = "0.6.2", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }
thiserror = "1.0.38"
tracing = "0.1.37"
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = ["dep:axum", "dep:tower", "dep:tower-http", "dep:tokio", "dep:sqlx", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "leptos_axum"]
ssr = ["dep:axum", "dep:tower", "dep:tower-http", "dep:tokio", "dep:sqlx", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "dep:leptos_axum"]
[package.metadata.cargo-all-features]
denylist = [
@@ -53,27 +55,27 @@ denylist = [
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "todo_app_sqlite_axum"
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "todo_app_sqlite_axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target

View File

@@ -2,3 +2,8 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -3,8 +3,7 @@
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle. Make sure you have trunk installed with `cargo install trunk`.
This example cannot be built as a trunk standalone CSR-only app. Only the server may directly connect to the database.
## Server Side Rendering with cargo-leptos
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)

View File

@@ -1,28 +1,65 @@
use crate::errors::TodoAppError;
use cfg_if::cfg_if;
use leptos::Errors;
use leptos::{view, For, ForProps, IntoView, RwSignal, Scope, View};
use leptos::*;
#[cfg(feature = "ssr")]
use leptos_axum::ResponseOptions;
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them
pub fn error_template(cx: Scope, errors: Option<RwSignal<Errors>>) -> View {
let Some(errors) = errors else {
panic!("No Errors found and we expected errors!");
#[component]
pub fn ErrorTemplate(
cx: Scope,
#[prop(optional)] outside_errors: Option<Errors>,
#[prop(optional)] errors: Option<RwSignal<Errors>>,
) -> impl IntoView {
let errors = match outside_errors {
Some(e) => create_rw_signal(cx, e),
None => match errors {
Some(e) => e,
None => panic!("No Errors found and we expected errors!"),
},
};
// Get Errors from Signal
let errors = errors.get().0;
// Downcast lets us take a type that implements `std::error::Error`
let errors: Vec<TodoAppError> = errors
.into_iter()
.filter_map(|(_k, v)| v.downcast_ref::<TodoAppError>().cloned())
.collect();
println!("Errors: {errors:#?}");
// Only the response code for the first error is actually sent from the server
// this may be customized by the specific application
cfg_if! {
if #[cfg(feature="ssr")]{
let response = use_context::<ResponseOptions>(cx);
if let Some(response) = response{
response.set_status(errors[0].status_code());
}
}
}
view! {cx,
<h1>"Errors"</h1>
<For
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each= move || {errors.get().0.into_iter()}
each= move || {errors.clone().into_iter().enumerate()}
// a unique key for each item as a reference
key=|error| error.0.clone()
key=|(index, _error)| *index
// renders each item to a view
view= move |error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! {
cx,
<h2>{error_code.to_string()}</h2>
<p>"Error: " {error_string}</p>
}
}
/>
}
.into_view(cx)
}

View File

@@ -0,0 +1,19 @@
use http::status::StatusCode;
use thiserror::Error;
#[derive(Debug, Clone, Error)]
pub enum TodoAppError {
#[error("Not Found")]
NotFound,
#[error("Internal Server Error")]
InternalServerError,
}
impl TodoAppError {
pub fn status_code(&self) -> StatusCode {
match self {
TodoAppError::NotFound => StatusCode::NOT_FOUND,
TodoAppError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}

View File

@@ -12,8 +12,9 @@ if #[cfg(feature = "ssr")] {
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions};
use crate::error_template::error_template;
use leptos::{LeptosOptions, Errors, view};
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
use crate::errors::TodoAppError;
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
@@ -23,21 +24,22 @@ if #[cfg(feature = "ssr")] {
if res.status() == StatusCode::OK {
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(options.to_owned(), |cx| error_template(cx, None));
let mut errors = Errors::default();
errors.insert_with_default_key(TodoAppError::NotFound);
let handler = leptos_axum::render_app_to_stream(options.to_owned(), move |cx| view!{cx, <ErrorTemplate outside_errors=errors.clone()/>});
handler(req).await.into_response()
}
}
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
let root_path = format!("{root}");
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(&root_path).oneshot(req).await {
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
format!("Something went wrong: {err}"),
)),
}
}

View File

@@ -1,6 +1,7 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod error_template;
pub mod errors;
pub mod fallback;
pub mod todo;

View File

@@ -7,7 +7,6 @@ if #[cfg(feature = "ssr")] {
routing::{post, get},
extract::{Extension, Path},
http::Request,
body::StreamBody,
response::{IntoResponse, Response},
Router,
};
@@ -17,7 +16,6 @@ if #[cfg(feature = "ssr")] {
use crate::fallback::file_and_error_handler;
use leptos_axum::{generate_route_list, LeptosRoutes};
use std::sync::Arc;
use leptos_reactive::run_scope;
//Define a handler to test extractor with state
async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
@@ -45,7 +43,7 @@ if #[cfg(feature = "ssr")] {
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_address.clone();
let addr = leptos_options.site_address;
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> }).await;
// build our application with a route
@@ -58,7 +56,7 @@ if #[cfg(feature = "ssr")] {
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on {}", &addr);
log!("listening on http://{}", &addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
@@ -68,15 +66,9 @@ if #[cfg(feature = "ssr")] {
// client-only stuff for Trunk
else {
use todo_app_sqlite_axum::todo::*;
pub fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! { cx, <TodoApp/> }
});
// This example cannot be built as a trunk standalone CSR-only app.
// Only the server may directly connect to the database.
}
}
}

View File

@@ -1,3 +1,4 @@
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
@@ -10,7 +11,7 @@ cfg_if! {
// use http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode};
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
Ok(SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))
}
pub fn register_server_functions() {
@@ -40,9 +41,9 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
// this is just an example of how to access server context injected in the handlers
// http::Request doesn't implement Clone, so more work will be needed to do use_context() on this
let req_parts = use_context::<leptos_axum::RequestParts>(cx);
if let Some(req_parts) = req_parts{
println!("Uri = {:?}", req_parts.uri);
if let Some(req_parts) = req_parts {
println!("Uri = {:?}", req_parts.uri);
}
use futures::TryStreamExt;
@@ -107,7 +108,7 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
#[component]
pub fn TodoApp(cx: Scope) -> impl IntoView {
let id = use_context::<String>(cx);
//let id = use_context::<String>(cx);
provide_meta_context(cx);
view! {
cx,
@@ -121,8 +122,10 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
<Routes>
<Route path="" view=|cx| view! {
cx,
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
<Todos/>
}/>
</ErrorBoundary>
}/> //Route
</Routes>
</main>
</Router>

View File

@@ -11,6 +11,7 @@ console_error_panic_hook = "0.1.7"
uuid = { version = "1", features = ["v4", "js", "serde"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
web-sys = { version = "0.3", features = ["Storage"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.0"

View File

@@ -2,3 +2,8 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -13,5 +13,5 @@ futures = "0.3"
leptos = { workspace = true, features = ["ssr"] }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_router = { workspace = true, features = ["ssr"] }
parking_lot = "0.12.1"
regex = "1.7.0"
tokio = "1.24.1"

View File

@@ -18,9 +18,9 @@ use http::StatusCode;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use parking_lot::RwLock;
use regex::Regex;
use std::sync::Arc;
use tokio::sync::RwLock;
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.
@@ -49,25 +49,25 @@ pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>);
impl ResponseOptions {
/// A less boilerplatey way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`
pub async fn overwrite(&self, parts: ResponseParts) {
let mut writable = self.0.write().await;
pub fn overwrite(&self, parts: ResponseParts) {
let mut writable = self.0.write();
*writable = parts
}
/// Set the status of the returned Response
pub async fn set_status(&self, status: StatusCode) {
let mut writeable = self.0.write().await;
pub fn set_status(&self, status: StatusCode) {
let mut writeable = self.0.write();
let res_parts = &mut *writeable;
res_parts.status = Some(status);
}
/// Insert a header, overwriting any previous value with the same key
pub async fn insert_header(&self, key: header::HeaderName, value: header::HeaderValue) {
let mut writeable = self.0.write().await;
pub fn insert_header(&self, key: header::HeaderName, value: header::HeaderValue) {
let mut writeable = self.0.write();
let res_parts = &mut *writeable;
res_parts.headers.insert(key, value);
}
/// Append a header, leaving any header with the same key intact
pub async fn append_header(&self, key: header::HeaderName, value: header::HeaderValue) {
let mut writeable = self.0.write().await;
pub fn append_header(&self, key: header::HeaderName, value: header::HeaderValue) {
let mut writeable = self.0.write();
let res_parts = &mut *writeable;
res_parts.headers.append(key, value);
}
@@ -76,15 +76,13 @@ impl ResponseOptions {
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
/// it sets a [StatusCode] of 302 and a [LOCATION](header::LOCATION) header with the provided value.
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead.
pub async fn redirect(cx: leptos::Scope, path: &str) {
pub fn redirect(cx: leptos::Scope, path: &str) {
let response_options = use_context::<ResponseOptions>(cx).unwrap();
response_options.set_status(StatusCode::FOUND).await;
response_options
.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path).expect("Failed to create HeaderValue"),
)
.await;
response_options.set_status(StatusCode::FOUND);
response_options.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path).expect("Failed to create HeaderValue"),
);
}
/// An Actix [Route](actix_web::Route) that listens for a `POST` request with
@@ -182,7 +180,7 @@ pub fn handle_server_fns_with_context(
runtime.dispose();
let mut res: HttpResponseBuilder;
let mut res_parts = res_options.0.write().await;
let mut res_parts = res_options.0.write();
if accept_header == Some("application/json")
|| accept_header == Some("application/x-www-form-urlencoded")
@@ -339,9 +337,7 @@ where
}
};
let (head, tail) = html_parts(&options);
stream_app(app, head, tail, res_options, additional_context).await
stream_app(&options, app, res_options, additional_context).await
}
})
}
@@ -429,9 +425,7 @@ where
}
};
let (head, tail) = html_parts(&options);
stream_app(app, head, tail, res_options, |_cx| {}).await
stream_app(&options, app, res_options, |_cx| {}).await
}
})
}
@@ -444,6 +438,7 @@ fn provide_contexts(cx: leptos::Scope, req: &HttpRequest, res_options: ResponseO
provide_context(cx, MetaContext::new());
provide_context(cx, res_options);
provide_context(cx, req.clone());
provide_server_redirect(cx, move |path| redirect(cx, path));
}
fn leptos_corrected_path(req: &HttpRequest) -> String {
@@ -457,23 +452,31 @@ fn leptos_corrected_path(req: &HttpRequest) -> String {
}
async fn stream_app(
options: &LeptosOptions,
app: impl FnOnce(leptos::Scope) -> View + 'static,
head: String,
tail: String,
res_options: ResponseOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
) -> HttpResponse<BoxBody> {
let (stream, runtime, _) = render_to_stream_with_prefix_undisposed_with_context(
let (stream, runtime, scope) = render_to_stream_with_prefix_undisposed_with_context(
app,
move |cx| {
let head = use_context::<MetaContext>(cx)
let meta = use_context::<MetaContext>(cx);
let head = meta
.as_ref()
.map(|meta| meta.dehydrate())
.unwrap_or_default();
format!("{head}</head><body>").into()
let body_meta = meta
.as_ref()
.and_then(|meta| meta.body.as_string())
.unwrap_or_default();
format!("{head}</head><body{body_meta}>").into()
},
additional_context,
);
let cx = leptos::Scope { runtime, id: scope };
let (head, tail) = html_parts(options, use_context::<MetaContext>(cx).as_ref());
let mut stream = Box::pin(
futures::stream::once(async move { head.clone() })
.chain(stream)
@@ -489,7 +492,7 @@ async fn stream_app(
let second_chunk = stream.next().await;
let third_chunk = stream.next().await;
let res_options = res_options.0.read().await;
let res_options = res_options.0.read();
let (status, mut headers) = (res_options.status, res_options.headers.clone());
let status = status.unwrap_or_default();
@@ -516,7 +519,7 @@ async fn stream_app(
res
}
fn html_parts(options: &LeptosOptions) -> (String, String) {
fn html_parts(options: &LeptosOptions, meta_context: Option<&MetaContext>) -> (String, String) {
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
@@ -557,9 +560,12 @@ fn html_parts(options: &LeptosOptions) -> (String, String) {
false => "".to_string(),
};
let html_metadata = meta_context
.and_then(|mc| mc.html.as_string())
.unwrap_or_default();
let head = format!(
r#"<!DOCTYPE html>
<html lang="en">
<html{html_metadata}>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
@@ -641,6 +647,16 @@ pub trait LeptosRoutes {
Data: 'static,
Fut: Future<Output = Result<DataResponse<Data>, actix_web::Error>>,
IV: IntoView + 'static;
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static;
}
/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
@@ -687,4 +703,28 @@ where
}
router
}
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
let mut router = self;
for path in paths.iter() {
router = router.route(
path,
render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
),
);
}
router
}
}

View File

@@ -16,4 +16,5 @@ leptos = { workspace = true, features = ["ssr"] }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_config = { workspace = true }
tokio = { version = "1.0", features = ["full"] }
tokio = { version = "1", features = ["full"] }
parking_lot = "0.12.1"

View File

@@ -19,8 +19,9 @@ use hyper::body;
use leptos::*;
use leptos_meta::MetaContext;
use leptos_router::*;
use parking_lot::RwLock;
use std::{io, pin::Pin, sync::Arc};
use tokio::{sync::RwLock, task::spawn_blocking, task::LocalSet};
use tokio::{task::spawn_blocking, task::LocalSet};
/// A struct to hold the parts of the incoming Request. Since `http::Request` isn't cloneable, we're forced
/// to construct this for Leptos to use in Axum
@@ -59,25 +60,25 @@ pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>);
impl ResponseOptions {
/// A less boilerplatey way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`
pub async fn overwrite(&self, parts: ResponseParts) {
let mut writable = self.0.write().await;
pub fn overwrite(&self, parts: ResponseParts) {
let mut writable = self.0.write();
*writable = parts
}
/// Set the status of the returned Response
pub async fn set_status(&self, status: StatusCode) {
let mut writeable = self.0.write().await;
pub fn set_status(&self, status: StatusCode) {
let mut writeable = self.0.write();
let res_parts = &mut *writeable;
res_parts.status = Some(status);
}
/// Insert a header, overwriting any previous value with the same key
pub async fn insert_header(&self, key: HeaderName, value: HeaderValue) {
let mut writeable = self.0.write().await;
pub fn insert_header(&self, key: HeaderName, value: HeaderValue) {
let mut writeable = self.0.write();
let res_parts = &mut *writeable;
res_parts.headers.insert(key, value);
}
/// Append a header, leaving any header with the same key intact
pub async fn append_header(&self, key: HeaderName, value: HeaderValue) {
let mut writeable = self.0.write().await;
pub fn append_header(&self, key: HeaderName, value: HeaderValue) {
let mut writeable = self.0.write();
let res_parts = &mut *writeable;
res_parts.headers.append(key, value);
}
@@ -86,15 +87,13 @@ impl ResponseOptions {
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
/// it sets a StatusCode of 302 and a LOCATION header with the provided value.
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead
pub async fn redirect(cx: leptos::Scope, path: &str) {
pub fn redirect(cx: leptos::Scope, path: &str) {
let response_options = use_context::<ResponseOptions>(cx).unwrap();
response_options.set_status(StatusCode::FOUND).await;
response_options
.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path).expect("Failed to create HeaderValue"),
)
.await;
response_options.set_status(StatusCode::FOUND);
response_options.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path).expect("Failed to create HeaderValue"),
);
}
/// Decomposes an HTTP request into its parts, allowing you to read its headers
@@ -226,7 +225,7 @@ async fn handle_server_fns_inner(
// Add headers from ResponseParts if they exist. These should be added as long
// as the server function returns an OK response
let res_options_outer = res_options.unwrap().0;
let res_options_inner = res_options_outer.read().await;
let res_options_inner = res_options_outer.read();
let (status, mut res_headers) = (
res_options_inner.status,
res_options_inner.headers.clone(),
@@ -420,61 +419,6 @@ where
let full_path = format!("http://leptos.dev{path}");
let pkg_path = &options.site_pkg_dir;
let output_name = &options.output_name;
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
let mut wasm_output_name = output_name.clone();
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
wasm_output_name.push_str("_bg");
}
let site_ip = &options.site_address.ip().to_string();
let reload_port = options.reload_port;
let leptos_autoreload = match std::env::var("LEPTOS_WATCH").is_ok() {
true => format!(
r#"
<script crossorigin="">(function () {{
var ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
ws.onmessage = (ev) => {{
let msg = JSON.parse(ev.data);
if (msg.all) window.location.reload();
if (msg.css) {{
const link = document.querySelector("link#leptos");
if (link) {{
let href = link.getAttribute('href').split('?')[0];
let newHref = href + '?version=' + new Date().getMilliseconds();
link.setAttribute('href', newHref);
}} else {{
console.warn("Could not find link#leptos");
}}
}};
}};
ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.');
}})()
</script>
"#
),
false => "".to_string(),
};
let head = format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js">
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
{leptos_autoreload}
"#
);
let tail = "</body></html>";
let (mut tx, rx) = futures::channel::mpsc::channel(8);
spawn_blocking({
@@ -503,6 +447,7 @@ where
provide_context(cx, MetaContext::new());
provide_context(cx, req_parts);
provide_context(cx, default_res_options);
provide_server_redirect(cx, move |path| redirect(cx, path));
app_fn(cx).into_view(cx)
}
};
@@ -518,19 +463,24 @@ where
},
add_context,
);
let cx = Scope { runtime, id: scope };
let (head, tail) = html_parts(&options, use_context::<MetaContext>(cx).as_ref());
_ = tx.send(head).await;
let mut shell = Box::pin(bundle);
while let Some(fragment) = shell.next().await {
_ = tx.send(fragment).await;
}
_ = tx.send(tail.to_string()).await;
// Extract the value of ResponseOptions from here
let cx = Scope { runtime, id: scope };
let res_options =
use_context::<ResponseOptions>(cx).unwrap();
let new_res_parts = res_options.0.read().await.clone();
let new_res_parts = res_options.0.read().clone();
let mut writable = res_options2.0.write().await;
let mut writable = res_options2.0.write();
*writable = new_res_parts;
runtime.dispose();
@@ -543,12 +493,7 @@ where
}
});
let mut stream = Box::pin(
futures::stream::once(async move { head.clone() })
.chain(rx)
.chain(futures::stream::once(async { tail.to_string() }))
.map(|html| Ok(Bytes::from(html))),
);
let mut stream = Box::pin(rx.map(|html| Ok(Bytes::from(html))));
// Get the first, second, and third chunks in the stream, which renders the app shell, and thus allows Resources to run
let first_chunk = stream.next().await;
@@ -556,7 +501,7 @@ where
let third_chunk = stream.next().await;
// Extract the resources now that they've been rendered
let res_options = res_options3.0.read().await;
let res_options = res_options3.0.read();
let complete_stream = futures::stream::iter([
first_chunk.unwrap(),
@@ -581,6 +526,65 @@ where
}
}
fn html_parts(options: &LeptosOptions, meta: Option<&MetaContext>) -> (String, &'static str) {
let pkg_path = &options.site_pkg_dir;
let output_name = &options.output_name;
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
let mut wasm_output_name = output_name.clone();
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
wasm_output_name.push_str("_bg");
}
let site_ip = &options.site_address.ip().to_string();
let reload_port = options.reload_port;
let leptos_autoreload = match std::env::var("LEPTOS_WATCH").is_ok() {
true => format!(
r#"
<script crossorigin="">(function () {{
var ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
ws.onmessage = (ev) => {{
let msg = JSON.parse(ev.data);
if (msg.all) window.location.reload();
if (msg.css) {{
const link = document.querySelector("link#leptos");
if (link) {{
let href = link.getAttribute('href').split('?')[0];
let newHref = href + '?version=' + new Date().getMilliseconds();
link.setAttribute('href', newHref);
}} else {{
console.warn("Could not find link#leptos");
}}
}};
}};
ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.');
}})()
</script>
"#
),
false => "".to_string(),
};
let html_metadata = meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
let head = format!(
r#"<!DOCTYPE html>
<html{html_metadata}>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js">
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
{leptos_autoreload}
"#
);
let tail = "</body></html>";
(head, tail)
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
@@ -601,7 +605,7 @@ where
.run_until(async move {
tokio::task::spawn_local(async move {
let routes = leptos_router::generate_route_list_inner(app_fn);
let mut writable = routes_inner.0.write().await;
let mut writable = routes_inner.0.write();
*writable = routes;
})
.await
@@ -609,7 +613,7 @@ where
})
.await;
let routes = routes.0.read().await.to_owned();
let routes = routes.0.read().to_owned();
// Axum's Router defines Root routes as "/" not ""
let routes: Vec<String> = routes
.into_iter()
@@ -634,6 +638,21 @@ pub trait LeptosRoutes {
) -> Self
where
IV: IntoView + 'static;
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static;
fn leptos_routes_with_handler<H, T>(self, paths: Vec<String>, handler: H) -> Self
where
H: axum::handler::Handler<T, (), axum::body::Body>,
T: 'static;
}
/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
/// to those paths to Leptos's renderer.
@@ -656,4 +675,40 @@ impl LeptosRoutes for axum::Router {
}
router
}
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
let mut router = self;
for path in paths.iter() {
router = router.route(
path,
get(render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
)),
);
}
router
}
fn leptos_routes_with_handler<H, T>(self, paths: Vec<String>, handler: H) -> Self
where
H: axum::handler::Handler<T, (), axum::body::Body>,
T: 'static,
{
let mut router = self;
for path in paths.iter() {
router = router.route(path, get(handler.clone()));
}
router
}
}

View File

@@ -16,18 +16,37 @@ leptos_reactive = { workspace = true }
leptos_server = { workspace = true }
leptos_config = { workspace = true }
tracing = "0.1"
typed-builder = "0.11"
once_cell = "1.17.0"
typed-builder = "0.12"
[dev-dependencies]
leptos = { path = ".", default-features = false }
[features]
default = ["csr", "serde"]
csr = ["leptos_dom/web", "leptos_macro/csr", "leptos_reactive/csr", "leptos_server/csr"]
hydrate = ["leptos_dom/web", "leptos_macro/hydrate", "leptos_reactive/hydrate", "leptos_server/hydrate"]
ssr = ["leptos_dom/ssr", "leptos_macro/ssr", "leptos_reactive/ssr", "leptos_server/ssr"]
stable = ["leptos_dom/stable", "leptos_macro/stable", "leptos_reactive/stable", "leptos_server/stable"]
csr = [
"leptos_dom/web",
"leptos_macro/csr",
"leptos_reactive/csr",
"leptos_server/csr",
]
hydrate = [
"leptos_dom/web",
"leptos_macro/hydrate",
"leptos_reactive/hydrate",
"leptos_server/hydrate",
]
ssr = [
"leptos_dom/ssr",
"leptos_macro/ssr",
"leptos_reactive/ssr",
"leptos_server/ssr",
]
stable = [
"leptos_dom/stable",
"leptos_macro/stable",
"leptos_reactive/stable",
"leptos_server/stable",
]
serde = ["leptos_reactive/serde"]
serde-lite = ["leptos_reactive/serde-lite"]
miniserde = ["leptos_reactive/miniserde"]

View File

@@ -1,17 +1,41 @@
use leptos_dom::{Errors, Fragment, IntoView};
use crate::Children;
use leptos_dom::{Errors, IntoView};
use leptos_macro::component;
use leptos_reactive::{create_rw_signal, provide_context, RwSignal, Scope};
/// When you render a `Result<_, _>` in your view, in the `Err` case it will
/// render nothing, and search up through the view tree for an `<ErrorBoundary/>`.
/// This component lets you define a fallback that should be rendered in that
/// error case, allowing you to handle errors within a section of the interface.
///
/// ```
/// # use leptos_reactive::*;
/// # use leptos_macro::*;
/// # use leptos_dom::*; use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// let (value, set_value) = create_signal(cx, Ok(0));
/// let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
///
/// view! { cx,
/// <input type="text" on:input=on_input/>
/// <ErrorBoundary
/// fallback=move |_, _| view! { cx, <p class="error">"Enter a valid number."</p>}
/// >
/// <p>"Value is: " {value}</p>
/// </ErrorBoundary>
/// }
/// # });
/// ```
#[component(transparent)]
pub fn ErrorBoundary<F, IV>(
cx: Scope,
/// The components inside the tag which will get rendered
children: Box<dyn FnOnce(Scope) -> Fragment>,
children: Children,
/// A fallback that will be shown if an error occurs.
fallback: F,
) -> impl IntoView
where
F: Fn(Scope, Option<RwSignal<Errors>>) -> IV + 'static,
F: Fn(Scope, RwSignal<Errors>) -> IV + 'static,
IV: IntoView,
{
let errors: RwSignal<Errors> = create_rw_signal(cx, Errors::default());
@@ -23,6 +47,6 @@ where
move || match errors.get().0.is_empty() {
true => children.clone().into_view(cx),
false => fallback(cx, Some(errors)).into_view(cx),
false => fallback(cx, errors).into_view(cx),
}
}

View File

@@ -164,3 +164,15 @@ pub use transition::*;
pub use leptos_reactive::debug_warn;
extern crate self as leptos;
/// The most common type for the `children` property on components,
/// which can only be called once.
pub type Children = Box<dyn FnOnce(Scope) -> Fragment>;
/// A type for the `children` property on components that can be called
/// more than once.
pub type ChildrenFn = Box<dyn Fn(Scope) -> Fragment>;
/// A type for the `children` property on components that can be called
/// more than once, but may mutate the children.
pub type ChildrenFnMut = Box<dyn FnMut(Scope) -> Fragment>;

View File

@@ -1,16 +1,39 @@
use leptos::component;
use leptos_dom::{Fragment, IntoView};
use leptos_reactive::Scope;
use once_cell::sync::Lazy;
use leptos_reactive::{create_memo, Scope};
/// A component that will show it's children when the passed in closure is True, and show the fallback
/// when the closure is false
/// A component that will show its children when the `when` condition is `true`,
/// and show the fallback when it is `false`, without rerendering every time
/// the condition changes.
///
/// *Note*: Because of the nature of generic arguments, its not really possible
/// to make the `fallback` optional. If you want an empty fallback state—in other
/// words, if you want to show the children if `when` is true and noting otherwise—use
/// `fallback=|_| ()` (i.e., a fallback function that returns the unit type `()`).
///
/// ```rust
/// # use leptos_reactive::*;
/// # use leptos_macro::*;
/// # use leptos_dom::*; use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// let (value, set_value) = create_signal(cx, 0);
///
/// view! { cx,
/// <Show
/// when=move || value() < 5
/// fallback=|cx| view! { cx, "Big number!" }
/// >
/// "Small number!"
/// </Show>
/// }
/// # });
/// ```
#[component]
pub fn Show<F, W, IV>(
/// The scope the component is running in
cx: Scope,
/// The components Show wraps
children: Box<dyn FnOnce(Scope) -> Fragment>,
children: Box<dyn Fn(Scope) -> Fragment>,
/// A closure that returns a bool that determines whether this thing runs
when: W,
/// A closure that returns what gets rendered if the when statement is false
@@ -21,12 +44,10 @@ where
F: Fn(Scope) -> IV + 'static,
IV: IntoView,
{
// now you don't render until `when` is actually true
let children = Lazy::new(move || children(cx).into_view(cx));
let fallback = Lazy::new(move || fallback(cx).into_view(cx));
let memoized_when = create_memo(cx, move |_| when());
move || match when() {
true => children.clone(),
false => fallback.clone(),
move || match memoized_when.get() {
true => children(cx).into_view(cx),
false => fallback(cx).into_view(cx),
}
}

View File

@@ -129,3 +129,22 @@ fn ssr_with_styles() {
);
});
}
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
#[test]
fn ssr_option() {
use leptos::*;
_ = create_scope(create_runtime(), |cx| {
let (value, set_value) = create_signal(cx, 0);
let rendered = view! {
cx,
<option/>
};
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<option id=\"_0-1\"></option>"
);
});
}

View File

@@ -5,7 +5,7 @@ edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/leptos-rs/leptos"
description = "Configuraiton for the Leptos web framework."
description = "Configuration for the Leptos web framework."
readme = "../README.md"
[dependencies]
@@ -14,4 +14,4 @@ fs = "0.0.5"
regex = "1.7.0"
serde = { version = "1.0.151", features = ["derive"] }
thiserror = "1.0.38"
typed-builder = "0.11"
typed-builder = "0.12"

View File

@@ -87,50 +87,34 @@ impl Default for Env {
}
}
fn from_str(input: &str) -> Result<Env, String> {
let sanitized = input.to_lowercase();
match sanitized.as_ref() {
"dev" | "development" => Ok(Env::DEV),
"prod" | "production" => Ok(Env::PROD),
_ => Err(format!(
"{input} is not a supported environment. Use either `dev` or `production`.",
)),
}
}
impl FromStr for Env {
type Err = ();
fn from_str(input: &str) -> Result<Self, Self::Err> {
let sanitized = input.to_lowercase();
match sanitized.as_ref() {
"dev" => Ok(Self::DEV),
"development" => Ok(Self::DEV),
"prod" => Ok(Self::PROD),
"production" => Ok(Self::PROD),
_ => Ok(Self::DEV),
}
from_str(input).or_else(|_| Ok(Self::default()))
}
}
impl From<&str> for Env {
fn from(str: &str) -> Self {
let sanitized = str.to_lowercase();
match sanitized.as_str() {
"dev" => Self::DEV,
"development" => Self::DEV,
"prod" => Self::PROD,
"production" => Self::PROD,
_ => {
panic!("Env var is not recognized. Maybe try `dev` or `prod`")
}
}
from_str(str).unwrap_or_else(|err| panic!("{}", err))
}
}
impl From<&Result<String, VarError>> for Env {
fn from(input: &Result<String, VarError>) -> Self {
match input {
Ok(str) => {
let sanitized = str.to_lowercase();
match sanitized.as_ref() {
"dev" => Self::DEV,
"development" => Self::DEV,
"prod" => Self::PROD,
"production" => Self::PROD,
_ => {
panic!("Env var is not recognized. Maybe try `dev` or `prod`")
}
}
}
Err(_) => Self::DEV,
Ok(str) => from_str(str).unwrap_or_else(|err| panic!("{}", err)),
Err(_) => Self::default(),
}
}
}
@@ -139,15 +123,7 @@ impl TryFrom<String> for Env {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
match s.to_lowercase().as_str() {
"dev" => Ok(Self::DEV),
"development" => Ok(Self::DEV),
"prod" => Ok(Self::PROD),
"production" => Ok(Self::PROD),
other => Err(format!(
"{other} is not a supported environment. Use either `dev` or `production`."
)),
}
from_str(s.as_str())
}
}

View File

@@ -12,7 +12,6 @@ cfg-if = "1"
drain_filter_polyfill = "0.1"
educe = "0.4"
futures = "0.3"
gloo = { version = "0.8", features = ["futures"] }
html-escape = "0.2"
indexmap = "1.9"
itertools = "0.10"
@@ -34,8 +33,11 @@ leptos = { path = "../leptos" }
[dependencies.web-sys]
version = "0.3"
features = [
"console",
"Comment",
"Document",
"DomTokenList",
"Location",
"Range",
"Text",
"HtmlCollection",
@@ -49,6 +51,7 @@ features = [
"DeviceMotionEvent",
"DeviceOrientationEvent",
"DragEvent",
"ErrorEvent",
"FocusEvent",
"GamepadEvent",
"HashChangeEvent",

View File

@@ -1,4 +1,7 @@
use crate::{hydration::HydrationCtx, Comment, IntoView, View};
use crate::{
hydration::{HydrationCtx, HydrationKey},
Comment, IntoView, View,
};
use cfg_if::cfg_if;
use leptos_reactive::Scope;
use std::{borrow::Cow, cell::RefCell, fmt, ops::Deref, rc::Rc};
@@ -7,8 +10,6 @@ cfg_if! {
use crate::{mount_child, prepare_to_move, unmount_child, MountKind, Mountable};
use leptos_reactive::{create_effect, ScopeDisposer};
use wasm_bindgen::JsCast;
} else {
use crate::hydration::HydrationKey;
}
}
@@ -77,9 +78,7 @@ impl Mountable for DynChildRepr {
}
impl DynChildRepr {
fn new() -> Self {
let id = HydrationCtx::id();
fn new_with_id(id: HydrationKey) -> Self {
let markers = (
Comment::new(Cow::Borrowed("</DynChild>"), &id, true),
#[cfg(debug_assertions)]
@@ -124,6 +123,7 @@ where
CF: Fn() -> N + 'static,
N: IntoView,
{
id: crate::HydrationKey,
child_fn: CF,
}
@@ -135,7 +135,12 @@ where
/// Creates a new dynamic child which will re-render whenever it's
/// signal dependencies change.
pub fn new(child_fn: CF) -> Self {
Self { child_fn }
Self::new_with_id(HydrationCtx::id(), child_fn)
}
#[doc(hidden)]
pub fn new_with_id(id: HydrationKey, child_fn: CF) -> Self {
Self { id, child_fn }
}
}
@@ -149,167 +154,182 @@ where
instrument(level = "trace", name = "<DynChild />", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
let Self { child_fn } = self;
// concrete inner function
fn create_dyn_view(
cx: Scope,
component: DynChildRepr,
child_fn: Box<dyn Fn() -> View>,
) -> DynChildRepr {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let closing = component.closing.node.clone();
let component = DynChildRepr::new();
let child = component.child.clone();
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let closing = component.closing.node.clone();
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
let span = tracing::Span::current();
let child = component.child.clone();
#[cfg(all(target_arch = "wasm32", feature = "web"))]
create_effect(
cx,
move |prev_run: Option<(Option<web_sys::Node>, ScopeDisposer)>| {
#[cfg(debug_assertions)]
let _guard = span.enter();
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
let span = tracing::Span::current();
let (new_child, disposer) =
cx.run_child_scope(|cx| child_fn().into_view(cx));
#[cfg(all(target_arch = "wasm32", feature = "web"))]
create_effect(
cx,
move |prev_run: Option<(Option<web_sys::Node>, ScopeDisposer)>| {
#[cfg(debug_assertions)]
let _guard = span.enter();
let mut child_borrow = child.borrow_mut();
let (new_child, disposer) =
cx.run_child_scope(|cx| child_fn().into_view(cx));
// Is this at least the second time we are loading a child?
if let Some((prev_t, prev_disposer)) = prev_run {
let child = child_borrow.take().unwrap();
let mut child_borrow = child.borrow_mut();
// Dispose of the scope
prev_disposer.dispose();
// Is this at least the second time we are loading a child?
if let Some((prev_t, prev_disposer)) = prev_run {
let child = child_borrow.take().unwrap();
// We need to know if our child wasn't moved elsewhere.
// If it was, `DynChild` no longer "owns" that child, and
// is therefore no longer sound to unmount it from the DOM
// or to reuse it in the case of a text node
// Dispose of the scope
prev_disposer.dispose();
// TODO check does this still detect moves correctly?
let was_child_moved = prev_t.is_none()
&& child.get_closing_node().next_sibling().as_ref()
!= Some(&closing);
// We need to know if our child wasn't moved elsewhere.
// If it was, `DynChild` no longer "owns" that child, and
// is therefore no longer sound to unmount it from the DOM
// or to reuse it in the case of a text node
// If the previous child was a text node, we would like to
// make use of it again if our current child is also a text
// node
let ret = if let Some(prev_t) = prev_t {
// Here, our child is also a text node
if let Some(new_t) = new_child.get_text() {
if !was_child_moved && child != new_child {
prev_t
.unchecked_ref::<web_sys::Text>()
.set_data(&new_t.content);
// TODO check does this still detect moves correctly?
let was_child_moved = prev_t.is_none()
&& child.get_closing_node().next_sibling().as_ref()
!= Some(&closing);
**child_borrow = Some(new_child);
// If the previous child was a text node, we would like to
// make use of it again if our current child is also a text
// node
let ret = if let Some(prev_t) = prev_t {
// Here, our child is also a text node
if let Some(new_t) = new_child.get_text() {
if !was_child_moved && child != new_child {
prev_t
.unchecked_ref::<web_sys::Text>()
.set_data(&new_t.content);
(Some(prev_t), disposer)
} else {
mount_child(MountKind::Before(&closing), &new_child);
**child_borrow = Some(new_child.clone());
(Some(new_t.node.clone()), disposer)
}
}
// Child is not a text node, so we can remove the previous
// text node
else {
if !was_child_moved && child != new_child {
// Remove the text
closing
.previous_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>()
.remove();
}
// Mount the new child, and we're done
mount_child(MountKind::Before(&closing), &new_child);
**child_borrow = Some(new_child);
(Some(prev_t), disposer)
} else {
mount_child(MountKind::Before(&closing), &new_child);
**child_borrow = Some(new_child.clone());
(Some(new_t.node.clone()), disposer)
(None, disposer)
}
}
// Child is not a text node, so we can remove the previous
// text node
// Otherwise, the new child can still be a text node,
// but we know the previous child was not, so no special
// treatment here
else {
if !was_child_moved && child != new_child {
// Remove the text
closing
.previous_sibling()
// Technically, I think this check shouldn't be necessary, but
// I can imagine some edge case that the child changes while
// hydration is ongoing
if !HydrationCtx::is_hydrating() {
if !was_child_moved && child != new_child {
// Remove the child
let start = child.get_opening_node();
let end = &closing;
unmount_child(&start, end);
}
// Mount the new child
mount_child(MountKind::Before(&closing), &new_child);
}
// We want to reuse text nodes, so hold onto it if
// our child is one
let t = new_child.get_text().map(|t| t.node.clone());
**child_borrow = Some(new_child);
(t, disposer)
};
ret
}
// Otherwise, we know for sure this is our first time
else {
// We need to remove the text created from SSR
if HydrationCtx::is_hydrating() && new_child.get_text().is_some() {
let t = closing
.previous_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>();
// See note on ssr.rs when matching on `DynChild`
// for more details on why we need to do this for
// release
if !cfg!(debug_assertions) {
t.previous_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>()
.remove();
}
// Mount the new child, and we're done
t.remove();
mount_child(MountKind::Before(&closing), &new_child);
**child_borrow = Some(new_child);
(None, disposer)
}
}
// Otherwise, the new child can still be a text node,
// but we know the previous child was not, so no special
// treatment here
else {
// Technically, I think this check shouldn't be necessary, but
// I can imagine some edge case that the child changes while
// hydration is ongoing
// If we are not hydrating, we simply mount the child
if !HydrationCtx::is_hydrating() {
if !was_child_moved && child != new_child {
// Remove the child
let start = child.get_opening_node();
let end = &closing;
unmount_child(&start, end);
}
// Mount the new child
mount_child(MountKind::Before(&closing), &new_child);
}
// We want to reuse text nodes, so hold onto it if
// our child is one
// We want to update text nodes, rather than replace them, so
// make sure to hold onto the text node
let t = new_child.get_text().map(|t| t.node.clone());
**child_borrow = Some(new_child);
(t, disposer)
};
ret
}
// Otherwise, we know for sure this is our first time
else {
// We need to remove the text created from SSR
if HydrationCtx::is_hydrating() && new_child.get_text().is_some() {
let t = closing
.previous_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>();
// See note on ssr.rs when matching on `DynChild`
// for more details on why we need to do this for
// release
if !cfg!(debug_assertions) {
t.previous_sibling()
.unwrap()
.unchecked_into::<web_sys::Element>()
.remove();
}
t.remove();
mount_child(MountKind::Before(&closing), &new_child);
}
},
);
// If we are not hydrating, we simply mount the child
if !HydrationCtx::is_hydrating() {
mount_child(MountKind::Before(&closing), &new_child);
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
let new_child = child_fn().into_view(cx);
// We want to update text nodes, rather than replace them, so
// make sure to hold onto the text node
let t = new_child.get_text().map(|t| t.node.clone());
**child.borrow_mut() = Some(new_child);
}
**child_borrow = Some(new_child);
(t, disposer)
}
},
);
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
let new_child = child_fn().into_view(cx);
**child.borrow_mut() = Some(new_child);
component
}
// monomorphized outer function
let Self { id, child_fn } = self;
let component = DynChildRepr::new_with_id(id);
let component = create_dyn_view(
cx,
component,
Box::new(move || child_fn().into_view(cx)),
);
View::CoreComponent(crate::CoreComponent::DynChild(component))
}
}

View File

@@ -1 +0,0 @@

View File

@@ -5,12 +5,12 @@ use std::{collections::HashMap, error::Error, sync::Arc};
/// A struct to hold all the possible errors that could be provided by child Views
#[derive(Debug, Clone, Default)]
pub struct Errors(pub HashMap<HydrationKey, Arc<dyn Error>>);
pub struct Errors(pub HashMap<HydrationKey, Arc<dyn Error + Send + Sync>>);
impl<T, E> IntoView for Result<T, E>
where
T: IntoView + 'static,
E: std::error::Error + Send + Sync + 'static,
E: Error + Send + Sync + 'static,
{
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
match self {
@@ -19,23 +19,27 @@ where
match use_context::<RwSignal<Errors>>(cx) {
Some(errors) => {
let id = HydrationCtx::id();
errors.update(move |errors: &mut Errors| errors.insert(id, error));
errors.update({
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let id = id.clone();
move |errors: &mut Errors| errors.insert(id, error)
});
// remove the error from the list if this drops,
// i.e., if it's in a DynChild that switches from Err to Ok
// Only can run on the client, will panic on the server
cfg_if! {
if #[cfg(any(feature = "hydrate", feature="csr"))] {
use leptos_reactive::{on_cleanup, queue_microtask};
on_cleanup(cx, move || {
queue_microtask(move || {
errors.update(|errors: &mut Errors| {
errors.remove::<E>(&id);
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use leptos_reactive::{on_cleanup, queue_microtask};
on_cleanup(cx, move || {
queue_microtask(move || {
errors.update(|errors: &mut Errors| {
errors.remove::<E>(&id);
});
});
});
});
});
}
}
}
}
None => {
#[cfg(debug_assertions)]
@@ -54,14 +58,21 @@ impl Errors {
/// Add an error to Errors that will be processed by `<ErrorBoundary/>`
pub fn insert<E>(&mut self, key: HydrationKey, error: E)
where
E: Error + 'static,
E: Error + Send + Sync + 'static,
{
self.0.insert(key, Arc::new(error));
}
/// Add an error with the default key for errors outside the reactive system
pub fn insert_with_default_key<E>(&mut self, error: E)
where
E: Error + Send + Sync + 'static,
{
self.0.insert(HydrationKey::default(), Arc::new(error));
}
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
pub fn remove<E>(&mut self, key: &HydrationKey)
where
E: Error + 'static,
E: Error + Send + Sync + 'static,
{
self.0.remove(key);
}

View File

@@ -16,7 +16,8 @@ thread_local! {
pub fn add_event_listener<E>(
target: &web_sys::Element,
event_name: Cow<'static, str>,
mut cb: impl FnMut(E) + 'static,
#[cfg(debug_assertions)] mut cb: impl FnMut(E) + 'static,
#[cfg(not(debug_assertions))] cb: impl FnMut(E) + 'static,
) where
E: FromWasmAbi + 'static,
{

View File

@@ -150,6 +150,7 @@ generate_event_types! {
canplaythrough: Event,
change: Event,
click: MouseEvent,
#[does_not_bubble]
close: Event,
compositionend: CompositionEvent,
compositionstart: CompositionEvent,

View File

@@ -6,12 +6,8 @@ use cfg_if::cfg_if;
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use crate::events::*;
use crate::macro_helpers::Property;
use crate::macro_helpers::{
attribute_expression, class_expression, property_expression,
};
use crate::macro_helpers::*;
use crate::{mount_child, MountKind};
use leptos_reactive::create_render_effect;
use once_cell::unsync::Lazy as LazyCell;
use wasm_bindgen::JsCast;
@@ -46,7 +42,7 @@ cfg_if! {
use crate::{
ev::EventDescriptor,
hydration::HydrationCtx,
macro_helpers::{Attribute, Class, IntoAttribute, IntoClass, IntoProperty},
macro_helpers::{IntoAttribute, IntoClass, IntoProperty},
Element, Fragment, IntoView, NodeRef, Text, View,
};
use leptos_reactive::Scope;
@@ -203,10 +199,8 @@ impl Custom {
el.unchecked_into()
} else {
gloo::console::warn!(
"element with id",
format!("_{id}"),
"not found, ignoring it for hydration"
crate::warn!(
"element with id {id} not found, ignoring it for hydration"
);
crate::document().create_element(&name).unwrap()
@@ -495,26 +489,18 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
let el = self.element.as_ref();
let value = attr.into_attribute(self.cx);
match value {
Attribute::Fn(cx, f) => {
let el = el.clone();
create_render_effect(cx, move |old| {
let new = f();
if old.as_ref() != Some(&new) {
attribute_expression(&el, &name, new.clone());
}
new
});
}
_ => attribute_expression(el, &name, value),
};
attribute_helper(
self.element.as_ref(),
name,
attr.into_attribute(self.cx),
);
self
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
use crate::macro_helpers::Attribute;
let mut this = self;
let mut attr = attr.into_attribute(this.cx);
@@ -554,26 +540,16 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
let el = self.element.as_ref();
let class_list = el.class_list();
let value = class.into_class(self.cx);
match value {
Class::Fn(cx, f) => {
create_render_effect(cx, move |old| {
let new = f();
if old.as_ref() != Some(&new) && (old.is_some() || new) {
class_expression(&class_list, &name, new)
}
new
});
}
Class::Value(value) => class_expression(&class_list, &name, value),
};
class_helper(el, name, value);
self
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
use crate::macro_helpers::Class;
let mut this = self;
let class = class.into_class(this.cx);
@@ -609,25 +585,7 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
let name = name.into();
let value = value.into_property(self.cx);
let el = self.element.as_ref();
match value {
Property::Fn(cx, f) => {
let el = el.clone();
create_render_effect(cx, move |old| {
let new = f();
let prop_name = wasm_bindgen::intern(&name);
if old.as_ref() != Some(&new)
&& !(old.is_none() && new == wasm_bindgen::JsValue::UNDEFINED)
{
property_expression(&el, prop_name, new.clone())
}
new
});
}
Property::Value(value) => {
let prop_name = wasm_bindgen::intern(&name);
property_expression(el, prop_name, value)
}
};
property_helper(el, name, value);
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
@@ -848,63 +806,18 @@ macro_rules! generate_html_tags {
let id = HydrationCtx::id();
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let element = if HydrationCtx::is_hydrating() {
if let Some(el) = crate::document().get_element_by_id(
&format!("_{id}")
) {
#[cfg(debug_assertions)]
assert_eq!(
el.node_name().to_ascii_uppercase(),
stringify!([<$tag:upper>]),
"SSR and CSR elements have the same `TopoId` \
but different node kinds. This is either a \
discrepancy between SSR and CSR rendering
logic, which is considered a bug, or it \
can also be a leptos hydration issue."
);
el.remove_attribute("id").unwrap();
el.unchecked_into()
} else if let Ok(Some(el)) = crate::document().query_selector(
&format!("[leptos-hk=_{id}]")
) {
#[cfg(debug_assertions)]
assert_eq!(
el.node_name().to_ascii_uppercase(),
stringify!([<$tag:upper>]),
"SSR and CSR elements have the same `TopoId` \
but different node kinds. This is either a \
discrepancy between SSR and CSR rendering
logic, which is considered a bug, or it \
can also be a leptos hydration issue."
);
el.remove_attribute("leptos-hk").unwrap();
el.unchecked_into()
} else {
gloo::console::warn!(
"element with id",
format!("_{id}"),
"not found, ignoring it for hydration"
);
let element = create_leptos_element(
&stringify!([<$tag:upper>]),
id,
|| {
[<$tag:upper>]
.with(|el|
el.clone_node()
.unwrap()
.unchecked_into()
)
}
} else {
[<$tag:upper>]
.with(|el|
el.clone_node()
.unwrap()
.unchecked_into()
)
};
}
);
Self {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@@ -979,24 +892,70 @@ macro_rules! generate_html_tags {
}
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
fn create_leptos_element(
tag: &str,
id: crate::HydrationKey,
clone_element: fn() -> web_sys::HtmlElement,
) -> web_sys::HtmlElement {
if HydrationCtx::is_hydrating() {
if let Some(el) = crate::document().get_element_by_id(&format!("_{id}")) {
#[cfg(debug_assertions)]
assert_eq!(
&el.node_name().to_ascii_uppercase(),
tag,
"SSR and CSR elements have the same `TopoId` but different node \
kinds. This is either a discrepancy between SSR and CSR rendering
logic, which is considered a bug, or it can also be a leptos \
hydration issue."
);
el.remove_attribute("id").unwrap();
el.unchecked_into()
} else if let Ok(Some(el)) =
crate::document().query_selector(&format!("[leptos-hk=_{id}]"))
{
#[cfg(debug_assertions)]
assert_eq!(
el.node_name().to_ascii_uppercase(),
tag,
"SSR and CSR elements have the same `TopoId` but different node \
kinds. This is either a discrepancy between SSR and CSR rendering
logic, which is considered a bug, or it can also be a leptos \
hydration issue."
);
el.remove_attribute("leptos-hk").unwrap();
el.unchecked_into()
} else {
crate::warn!("element with id {id} not found, ignoring it for hydration");
clone_element()
}
} else {
clone_element()
}
}
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
fn warn_on_ambiguous_a(parent: &web_sys::Element, child: &View) {
if let View::Element(el) = &child {
if el.name == "a"
if (el.name == "a"
|| el.name == "script"
|| el.name == "style"
|| el.name == "title"
|| el.name == "title")
&& parent.namespace_uri() != el.element.namespace_uri()
{
if parent.namespace_uri() != el.element.namespace_uri() {
crate::warn!(
"Warning: you are appending an SVG element to an HTML element, or \
an HTML element to an SVG. Typically, this occurs when you create \
an <a/> or <script/> with the `view` macro and append it to an \
SVG, but the framework assumed it was HTML when you created it. To \
specify that it is an SVG element, use <svg::{{tag name}}/> in the \
view macro."
)
}
crate::warn!(
"Warning: you are appending an SVG element to an HTML element, or an \
HTML element to an SVG. Typically, this occurs when you create an \
<a/> or <script/> with the `view` macro and append it to an SVG, but \
the framework assumed it was HTML when you created it. To specify \
that it is an SVG element, use <svg::{{tag name}}/> in the view \
macro."
)
}
}
}

View File

@@ -95,10 +95,8 @@ macro_rules! generate_math_tags {
el.unchecked_into()
} else {
gloo::console::warn!(
"element with id",
format!("_{id}"),
"not found, ignoring it for hydration"
crate::warn!(
"element with id {id} not found, ignoring it for hydration"
);
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]

View File

@@ -92,10 +92,8 @@ macro_rules! generate_svg_tags {
el.unchecked_into()
} else {
gloo::console::warn!(
"element with id",
format!("_{id}"),
"not found, ignoring it for hydration"
crate::warn!(
"element with id {id} not found, ignoring it for hydration"
);
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]

View File

@@ -21,7 +21,7 @@ cfg_if! {
while let Ok(Some(node)) = walker.next_node() {
if let Some(content) = node.text_content() {
if let Some(hk) = content.strip_prefix("hk=") {
if let Some(hk) = hk.split("|").next() {
if let Some(hk) = hk.split('|').next() {
map.insert(hk.into(), node.unchecked_into());
}
}

View File

@@ -308,10 +308,8 @@ impl Comment {
marker.remove();
} else {
gloo::console::warn!(
"component with id",
id,
"not found, ignoring it for hydration"
crate::warn!(
"component with id {id} not found, ignoring it for hydration"
);
}
}

View File

@@ -49,9 +49,9 @@ impl Attribute {
/// Converts the attribute to its HTML value at that moment, not including
/// the attribute name, so it can be rendered on the server.
pub fn as_nameless_value_string(&self) -> String {
pub fn as_nameless_value_string(&self) -> Option<String> {
match self {
Attribute::String(value) => value.to_string(),
Attribute::String(value) => Some(value.to_string()),
Attribute::Fn(_, f) => {
let mut value = f();
while let Attribute::Fn(_, f) = value {
@@ -59,11 +59,16 @@ impl Attribute {
}
value.as_nameless_value_string()
}
Attribute::Option(_, value) => value
.as_ref()
.map(|value| value.to_string())
.unwrap_or_default(),
Attribute::Bool(_) => String::new(),
Attribute::Option(_, value) => {
value.as_ref().map(|value| value.to_string())
}
Attribute::Bool(include) => {
if *include {
Some("".to_string())
} else {
None
}
}
}
}
}
@@ -169,7 +174,31 @@ attr_type!(f64);
attr_type!(char);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub fn attribute_expression(
use std::borrow::Cow;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn attribute_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
value: Attribute,
) {
use leptos_reactive::create_render_effect;
match value {
Attribute::Fn(cx, f) => {
let el = el.clone();
create_render_effect(cx, move |old| {
let new = f();
if old.as_ref() != Some(&new) {
attribute_expression(&el, &name, new.clone());
}
new
});
}
_ => attribute_expression(el, &name, value),
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn attribute_expression(
el: &web_sys::Element,
attr_name: &str,
value: Attribute,

View File

@@ -67,7 +67,33 @@ impl<T: IntoClass> IntoClass for (Scope, T) {
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub fn class_expression(
use std::borrow::Cow;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn class_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
value: Class,
) {
use leptos_reactive::create_render_effect;
let class_list = el.class_list();
match value {
Class::Fn(cx, f) => {
create_render_effect(cx, move |old| {
let new = f();
if old.as_ref() != Some(&new) && (old.is_some() || new) {
class_expression(&class_list, &name, new)
}
new
});
}
Class::Value(value) => class_expression(&class_list, &name, value),
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn class_expression(
class_list: &web_sys::DomTokenList,
class_name: &str,
value: bool,

View File

@@ -77,7 +77,39 @@ prop_type!(f64);
prop_type!(bool);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub fn property_expression(
use std::borrow::Cow;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn property_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
value: Property,
) {
use leptos_reactive::create_render_effect;
match value {
Property::Fn(cx, f) => {
let el = el.clone();
create_render_effect(cx, move |old| {
let new = f();
let prop_name = wasm_bindgen::intern(&name);
if old.as_ref() != Some(&new)
&& !(old.is_none() && new == wasm_bindgen::JsValue::UNDEFINED)
{
property_expression(&el, prop_name, new.clone())
}
new
});
}
Property::Value(value) => {
let prop_name = wasm_bindgen::intern(&name);
property_expression(el, prop_name, value)
}
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn property_expression(
el: &web_sys::Element,
prop_name: &str,
value: JsValue,

View File

@@ -13,25 +13,22 @@ proc-macro = true
[dependencies]
cfg-if = "1"
doc-comment = "0.3"
html-escape = "0.2"
itertools = "0.10"
pad-adapter = "0.1"
prettyplease = "0.1"
proc-macro-error = "1"
proc-macro2 = "1"
quote = "1"
syn = { version = "1", features = ["full"] }
syn-rsx = "0.9"
uuid = { version = "1", features = ["v4"] }
leptos_dom = { workspace = true }
leptos_reactive = { workspace = true }
leptos_server = { workspace = true }
lazy_static = "1.4"
convert_case = "0.6.0"
[dev-dependencies]
log = "0.4"
typed-builder = "0.11"
typed-builder = "0.12"
leptos = { path = "../leptos" }
[features]

View File

@@ -4,6 +4,7 @@ use convert_case::{
};
use itertools::Itertools;
use proc_macro2::{Ident, TokenStream};
use proc_macro_error::ResultExt;
use quote::{format_ident, ToTokens, TokenStreamExt};
use std::collections::HashSet;
use syn::{
@@ -167,7 +168,7 @@ impl ToTokens for Model {
let component = if *is_transparent {
quote! {
#body_name(cx, #prop_names)
#body_name(#scope_name, #prop_names)
}
} else {
quote! {
@@ -287,8 +288,8 @@ impl Prop {
} else {
abort!(
typed.pat,
"only `prop: bool` style types are allowed within the \
`#[component]` macro"
"only `prop: bool` style types are allowed within the `#[component]` \
macro"
);
};
@@ -402,17 +403,15 @@ enum PropOpt {
impl PropOpt {
fn from_attribute(attr: &Attribute) -> Option<HashSet<Self>> {
const ABORT_OPT_MESSAGE: &str = "only `optional`, \
`optional_no_strip`, \
`strip_option`, \
`default` and `into` are \
allowed as arguments to `#[prop()]`";
const ABORT_OPT_MESSAGE: &str = "only `optional`, `optional_no_strip`, \
`strip_option`, `default` and `into` are \
allowed as arguments to `#[prop()]`";
if attr.path != parse_quote!(prop) {
return None;
}
if let Meta::List(MetaList { nested, .. }) = attr.parse_meta().ok()? {
if let Meta::List(MetaList { nested, .. }) = attr.parse_meta().unwrap_or_abort() {
Some(
nested
.iter()
@@ -613,9 +612,9 @@ fn is_option(ty: &Type) -> bool {
}
fn unwrap_option(ty: &Type) -> Option<Type> {
const STD_OPTION_MSG: &str = "make sure you're not shadowing the \
`std::option::Option` type that is automatically imported from the \
standard prelude";
const STD_OPTION_MSG: &str =
"make sure you're not shadowing the `std::option::Option` type that is \
automatically imported from the standard prelude";
if let Type::Path(TypePath {
path: Path { segments, .. },

View File

@@ -230,6 +230,22 @@ mod server;
/// # });
/// ```
///
/// 10. You can set any HTML elements `innerHTML` with the `inner_html` attribute on an
/// element. Be careful: this HTML will not be escaped, so you should ensure that it
/// only contains trusted input.
/// ```rust
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let html = "<p>This HTML will be injected.</p>";
/// view! { cx,
/// <div inner_html=html/>
/// }
/// # ;
/// # }
/// # });
/// ```
///
/// Heres a simple example that shows off several of these features, put together
/// ```rust
/// # use leptos::*;
@@ -441,12 +457,14 @@ pub fn view(tokens: TokenStream) -> TokenStream {
/// ```
///
/// 5. You can access the children passed into the component with the `children` property, which takes
/// an argument of the form `Box<dyn FnOnce(Scope) -> Fragment>`.
/// an argument of the type `Children`. This is an alias for `Box<dyn FnOnce(Scope) -> Fragment>`.
/// If you need `children` to be a `Fn` or `FnMut`, you can use the `ChildrenFn` or `ChildrenFnMut`
/// type aliases.
///
/// ```
/// # use leptos::*;
/// #[component]
/// fn ComponentWithChildren(cx: Scope, children: Box<dyn FnOnce(Scope) -> Fragment>) -> impl IntoView {
/// fn ComponentWithChildren(cx: Scope, children: Children) -> impl IntoView {
/// view! {
/// cx,
/// <ul>

View File

@@ -137,7 +137,7 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
};
Ok(quote::quote! {
#[derive(Clone, ::serde::Serialize, ::serde::Deserialize)]
#[derive(Clone, Debug, ::serde::Serialize, ::serde::Deserialize)]
pub struct #struct_name {
#(#fields),*
}

View File

@@ -171,7 +171,7 @@ pub(crate) fn render_view(
cx,
Span::call_site(),
nodes,
false,
true,
TagType::Unknown,
global_class,
)
@@ -219,7 +219,7 @@ fn fragment_to_tokens_ssr(
});
quote! {
{
leptos::Fragment::new(vec![
leptos::Fragment::lazy(|| vec![
#(#nodes),*
])
}
@@ -262,11 +262,22 @@ fn root_element_to_tokens_ssr(
};
let tag_name = node.name.to_string();
let typed_element_name = Ident::new(&camel_case_tag_name(&tag_name), node.name.span());
let typed_element_name = {
let camel_cased =
camel_case_tag_name(&tag_name.replace("svg::", "").replace("math::", ""));
Ident::new(&camel_cased, node.name.span())
};
let typed_element_name = if is_svg_element(&tag_name) {
quote! { svg::#typed_element_name }
} else if is_math_ml_element(&tag_name) {
quote! { math::#typed_element_name }
} else {
quote! { #typed_element_name }
};
quote! {
{
#(#exprs_for_compiler)*
::leptos::HtmlElement::from_html(cx, leptos::#typed_element_name::default(), #template)
::leptos::HtmlElement::from_html(cx, leptos::leptos_dom::#typed_element_name::default(), #template)
}
}
}
@@ -288,8 +299,13 @@ fn element_to_tokens_ssr(
{#component}.into_view(cx).render_to_string(cx),
})
} else {
let tag_name = node
.name
.to_string()
.replace("svg::", "")
.replace("math::", "");
template.push('<');
template.push_str(&node.name.to_string());
template.push_str(&tag_name);
for attr in &node.attributes {
if let Node::Attribute(attr) = attr {
@@ -336,7 +352,7 @@ fn element_to_tokens_ssr(
),
Node::Text(text) => {
if let Some(value) = value_to_string(&text.value) {
template.push_str(&value);
template.push_str(&html_escape::encode_safe(&value));
} else {
template.push_str("{}");
let value = text.value.as_ref();
@@ -390,32 +406,10 @@ fn attribute_to_tokens_ssr(
exprs_for_compiler: &mut Vec<TokenStream>,
) {
let name = node.key.to_string();
if name == "ref" || name == "_ref" {
if name == "ref" || name == "_ref" || name == "node_ref" {
// ignore refs on SSR
} else if let Some(name) = name.strip_prefix("on:") {
let handler = node
.value
.as_ref()
.expect("event listener attributes need a value")
.as_ref();
#[allow(unused_variables)]
let (name, is_force_undelegated) = parse_event(name);
let event_type = TYPED_EVENTS
.iter()
.find(|e| **e == name)
.copied()
.unwrap_or("Custom");
let event_type = event_type
.parse::<TokenStream>()
.expect("couldn't parse event name");
let event_type = if is_force_undelegated {
quote! { ::leptos::ev::undelegated(::leptos::ev::#event_type) }
} else {
quote! { ::leptos::ev::#event_type }
};
} else if name.strip_prefix("on:").is_some() {
let (event_type, handler) = event_from_attribute_node(node);
exprs_for_compiler.push(quote! {
leptos::ssr_event_listener(#event_type, #handler);
})
@@ -427,18 +421,21 @@ fn attribute_to_tokens_ssr(
if name != "class" {
template.push(' ');
template.push_str(&name);
if let Some(value) = node.value.as_ref() {
if let Some(value) = value_to_string(value) {
template.push_str(&name);
template.push_str("=\"");
template.push_str(&value);
template.push('"');
} else {
template.push_str("=\"{}\"");
template.push_str("{}");
let value = value.as_ref();
holes.push(quote! {
leptos::escape_attr(&{#value}.into_attribute(#cx).as_nameless_value_string()),
&{#value}.into_attribute(#cx)
.as_nameless_value_string()
.map(|a| format!("{}=\"{}\"", #name, leptos::escape_attr(&a)))
.unwrap_or_default(),
})
}
}
@@ -555,7 +552,9 @@ fn set_class_attribute_ssr(
template.push_str(" {}");
let value = value.as_ref();
holes.push(quote! {
leptos::escape_attr(&(cx, #value).into_attribute(#cx).as_nameless_value_string()),
&(cx, #value).into_attribute(#cx).as_nameless_value_string()
.map(|a| leptos::escape_attr(&a).to_string())
.unwrap_or_default(),
});
}
}
@@ -621,7 +620,7 @@ fn node_to_tokens(
cx,
Span::call_site(),
&fragment.children,
false,
true,
parent_type,
global_class,
),
@@ -703,7 +702,7 @@ fn element_to_tokens(
cx,
Span::call_site(),
&fragment.children,
false,
true,
parent_type,
global_class,
),
@@ -926,7 +925,9 @@ fn component_to_tokens(
let props = attrs
.clone()
.filter(|attr| !attr.key.to_string().starts_with("clone:"))
.filter(|attr| {
!attr.key.to_string().starts_with("clone:") && !attr.key.to_string().starts_with("on:")
})
.map(|attr| {
let name = &attr.key;
@@ -945,6 +946,7 @@ fn component_to_tokens(
});
let items_to_clone = attrs
.clone()
.filter(|attr| attr.key.to_string().starts_with("clone:"))
.map(|attr| {
let ident = attr
@@ -958,6 +960,17 @@ fn component_to_tokens(
})
.collect::<Vec<_>>();
let events = attrs
.filter(|attr| attr.key.to_string().starts_with("on:"))
.map(|attr| {
let (event_type, handler) = event_from_attribute_node(attr);
quote! {
.on(#event_type, #handler)
}
})
.collect::<Vec<_>>();
let children = if node.children.is_empty() {
quote! {}
} else {
@@ -983,17 +996,55 @@ fn component_to_tokens(
}
};
quote! {
let component = quote! {
#name(
#cx,
#component_props_name::builder()
#(#props)*
#children
.build(),
.build()
)
};
if events.is_empty() {
component
} else {
quote! {
#component.into_view(#cx)
#(#events)*
}
}
}
fn event_from_attribute_node(attr: &NodeAttribute) -> (TokenStream, &Expr) {
let event_name = attr.key.to_string().strip_prefix("on:").unwrap().to_owned();
let handler = attr
.value
.as_ref()
.expect("event listener attributes need a value")
.as_ref();
#[allow(unused_variables)]
let (name, is_force_undelegated) = parse_event(&event_name);
let event_type = TYPED_EVENTS
.iter()
.find(|e| **e == name)
.copied()
.unwrap_or("Custom");
let event_type = event_type
.parse::<TokenStream>()
.expect("couldn't parse event name");
let event_type = if is_force_undelegated {
quote! { ::leptos::ev::undelegated(::leptos::ev::#event_type) }
} else {
quote! { ::leptos::ev::#event_type }
};
(event_type, handler)
}
fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
match tag_name {
NodeName::Path(path) => path
@@ -1058,11 +1109,17 @@ fn is_self_closing(node: &NodeElement) -> bool {
fn camel_case_tag_name(tag_name: &str) -> String {
let mut chars = tag_name.chars();
let first = chars.next();
let underscore = if tag_name == "option" || tag_name == "use" {
"_"
} else {
""
};
first
.map(|f| f.to_ascii_uppercase())
.into_iter()
.chain(chars)
.collect()
.collect::<String>()
+ underscore
}
fn is_svg_element(tag: &str) -> bool {

View File

@@ -200,7 +200,7 @@ where
}
impl EffectId {
pub(crate) fn run<T>(&self, runtime_id: RuntimeId) {
pub(crate) fn run(&self, runtime_id: RuntimeId) {
_ = with_runtime(runtime_id, |runtime| {
let effect = {
let effects = runtime.effects.borrow();

View File

@@ -130,7 +130,8 @@ where
});
let id = with_runtime(cx.runtime, |runtime| {
runtime.create_serializable_resource(Rc::clone(&r))
let r = Rc::clone(&r) as Rc<dyn SerializableResource>;
runtime.create_serializable_resource(r)
})
.expect("tried to create a Resource in a Runtime that has been disposed.");
@@ -250,7 +251,8 @@ where
});
let id = with_runtime(cx.runtime, |runtime| {
runtime.create_unserializable_resource(Rc::clone(&r))
let r = Rc::clone(&r) as Rc<dyn UnserializableResource>;
runtime.create_unserializable_resource(r)
})
.expect("tried to create a Resource in a runtime that has been disposed.");

View File

@@ -1,8 +1,8 @@
#![forbid(unsafe_code)]
use crate::{
hydration::SharedContext, serialization::Serializable, AnyEffect, AnyResource, Effect,
EffectId, Memo, ReadSignal, ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId,
ScopeProperty, SignalId, WriteSignal,
hydration::SharedContext, AnyEffect, AnyResource, Effect, EffectId, Memo, ReadSignal,
ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId, ScopeProperty,
SerializableResource, SignalId, UnserializableResource, WriteSignal,
};
use cfg_if::cfg_if;
use futures::stream::FuturesUnordered;
@@ -115,18 +115,19 @@ impl RuntimeId {
ret
}
#[track_caller]
pub(crate) fn create_concrete_signal(self, value: Rc<RefCell<dyn Any>>) -> SignalId {
with_runtime(self, |runtime| runtime.signals.borrow_mut().insert(value))
.expect("tried to create a signal in a runtime that has been disposed")
}
#[track_caller]
pub(crate) fn create_signal<T>(self, value: T) -> (ReadSignal<T>, WriteSignal<T>)
where
T: Any + 'static,
{
let id = with_runtime(self, |runtime| {
runtime
.signals
.borrow_mut()
.insert(Rc::new(RefCell::new(value)))
})
.expect("tried to create a signal in a runtime that has been disposed");
let id = self.create_concrete_signal(Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>);
(
ReadSignal {
runtime: self,
@@ -149,13 +150,7 @@ impl RuntimeId {
where
T: Any + 'static,
{
let id = with_runtime(self, |runtime| {
runtime
.signals
.borrow_mut()
.insert(Rc::new(RefCell::new(value)))
})
.expect("tried to create a signal in a runtime that has been disposed");
let id = self.create_concrete_signal(Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>);
RwSignal {
runtime: self,
id,
@@ -165,6 +160,12 @@ impl RuntimeId {
}
}
#[track_caller]
pub(crate) fn create_concrete_effect(self, effect: Rc<dyn AnyEffect>) -> EffectId {
with_runtime(self, |runtime| runtime.effects.borrow_mut().insert(effect))
.expect("tried to create an effect in a runtime that has been disposed")
}
#[track_caller]
pub(crate) fn create_effect<T>(self, f: impl Fn(Option<T>) -> T + 'static) -> EffectId
where
@@ -173,18 +174,16 @@ impl RuntimeId {
#[cfg(debug_assertions)]
let defined_at = std::panic::Location::caller();
with_runtime(self, |runtime| {
let effect = Effect {
f,
value: RefCell::new(None),
#[cfg(debug_assertions)]
defined_at,
};
let id = { runtime.effects.borrow_mut().insert(Rc::new(effect)) };
id.run::<T>(self);
id
})
.expect("tried to create an effect in a runtime that has been disposed")
let effect = Effect {
f,
value: RefCell::new(None),
#[cfg(debug_assertions)]
defined_at,
};
let id = self.create_concrete_effect(Rc::new(effect));
id.run(self);
id
}
#[track_caller]
@@ -256,27 +255,19 @@ impl Runtime {
Self::default()
}
pub(crate) fn create_unserializable_resource<S, T>(
pub(crate) fn create_unserializable_resource(
&self,
state: Rc<ResourceState<S, T>>,
) -> ResourceId
where
S: Clone + 'static,
T: 'static,
{
state: Rc<dyn UnserializableResource>,
) -> ResourceId {
self.resources
.borrow_mut()
.insert(AnyResource::Unserializable(state))
}
pub(crate) fn create_serializable_resource<S, T>(
pub(crate) fn create_serializable_resource(
&self,
state: Rc<ResourceState<S, T>>,
) -> ResourceId
where
S: Clone + 'static,
T: Serializable + 'static,
{
state: Rc<dyn SerializableResource>,
) -> ResourceId {
self.resources
.borrow_mut()
.insert(AnyResource::Serializable(state))

View File

@@ -83,6 +83,17 @@ impl Scope {
self.id
}
/// Returns the chain of scope IDs beginning with this one, going to its parent, grandparents, etc.
pub fn ancestry(&self) -> Vec<ScopeId> {
let mut ids = vec![self.id];
let mut cx = *self;
while let Some(parent) = cx.parent() {
ids.push(parent.id());
cx = parent;
}
ids
}
/// Creates a child scope and runs the given function within it, returning a handle to dispose of it.
///
/// The child scope has its own lifetime and disposer, but will be disposed when the parent is

View File

@@ -399,6 +399,12 @@ where
Dynamic(Signal<T>),
}
impl<T: Default> Default for MaybeSignal<T> {
fn default() -> Self {
Self::Static(Default::default())
}
}
impl<T> UntrackedGettableSignal<T> for MaybeSignal<T>
where
T: 'static,

View File

@@ -89,6 +89,30 @@ where
self.0.update_untracked(f);
}
/// Applies a function to the current value to mutate it in place.
/// Forwards the return value of the closure if the closure was called.
/// ```
/// use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
///
/// pub struct MyUncloneableData {
/// pub value: String
/// }
///
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
/// let updated = data.update_returning(|data| {
/// data.value = "b".into();
/// data.value.clone()
/// });
///
/// assert_eq!(data.with(|data| data.value.clone()), "b");
/// assert_eq!(updated, Some(String::from("b")));
/// });
/// ```
pub fn update_returning<U>(&self, f: impl FnOnce(&mut T) -> U) -> Option<U> {
self.0.update_returning_untracked(f)
}
/// Sets the stored value.
/// ```
/// # use leptos_reactive::*;

View File

@@ -14,12 +14,9 @@ leptos_reactive = { workspace = true }
form_urlencoded = "1"
gloo-net = "0.2"
lazy_static = "1"
linear-map = "1"
log = "0.4"
serde = { version = "1", features = ["derive"] }
serde_urlencoded = "0.7"
thiserror = "1"
rmp-serde = "1.1.1"
serde_json = "1.0.89"
quote = "1"
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
@@ -31,23 +28,23 @@ leptos = { path = "../leptos" }
[features]
csr = [
#"leptos/csr",
"leptos_dom/web",
"leptos_reactive/csr",
#"leptos/csr",
"leptos_dom/web",
"leptos_reactive/csr",
]
hydrate = [
#"leptos/hydrate",
"leptos_dom/web",
"leptos_reactive/hydrate",
#"leptos/hydrate",
"leptos_dom/web",
"leptos_reactive/hydrate",
]
ssr = [
#"leptos/ssr",
"leptos_reactive/ssr",
#"leptos/ssr",
"leptos_reactive/ssr",
]
stable = [
#"leptos/stable",
"leptos_dom/stable",
"leptos_reactive/stable",
#"leptos/stable",
"leptos_dom/stable",
"leptos_reactive/stable",
]
[package.metadata.cargo-all-features]

View File

@@ -96,6 +96,11 @@ where
self.0.with(|a| a.pending.read_only())
}
/// Updates whether the action is currently pending.
pub fn set_pending(&self, pending: bool) {
self.0.with(|a| a.pending.set(pending))
}
/// The URL associated with the action (typically as part of a server function.)
/// This enables integration with the `ActionForm` component in `leptos_router`.
pub fn url(&self) -> Option<String> {

14
logos/Simple_Icon.svg Normal file
View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
<g>
<path d="M10.1,17.8C7.6,17,5.9,14.6,5.9,12c0-3.4,2.7-6.1,6.1-6.1c0.3,0,0.5,0,0.8,0
c0.4-0.9,0.9-1.7,1.5-2.5c-0.5-0.6-0.8-1.4-0.8-2.3c0-0.3,0-0.7,0.1-1c-0.5-0.1-1-0.1-1.5-0.1C5.4,0.1,0.1,5.4,0.1,12
c0,4.3,2.3,8.1,5.8,10.2L6,22.1C7.9,21.2,9.3,19.7,10.1,17.8z"/>
<path d="M16.9,3c0.7,0,1.3-0.4,1.6-0.9l0,0c-1-0.6-2.1-1.1-3.2-1.5l0,0c-0.1,0.2-0.1,0.4-0.1,0.6
C15.1,2.2,15.9,3,16.9,3z"/>
<path d="M19.9,3.1c-0.7,1-1.8,1.6-2.9,1.6c-0.3,0-0.5,0-0.8-0.1c-0.2,0-0.3-0.1-0.5-0.1
c-0.5,0.6-0.9,1.2-1.2,1.9c2.2,1,3.7,3.1,3.7,5.6c0,3.4-2.7,6.1-6.1,6.1c0,0-0.1,0-0.1,0c-0.8,2.1-2.2,3.8-4.1,5
c1.3,0.5,2.7,0.8,4.2,0.8c6.6,0,11.9-5.3,11.9-11.9C23.9,8.4,22.3,5.3,19.9,3.1z"/>
<circle cx="12" cy="12" r="4.4"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 907 B

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.1.1"
version = "0.1.3"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -11,7 +11,7 @@ description = "Tools to set HTML metadata in the Leptos web framework."
cfg-if = "1"
leptos = { workspace = true }
tracing = "0.1"
typed-builder = "0.11"
typed-builder = "0.12"
[dependencies.web-sys]
version = "0.3"

74
meta/src/body.rs Normal file
View File

@@ -0,0 +1,74 @@
use crate::TextProp;
use cfg_if::cfg_if;
use leptos::*;
use std::{cell::RefCell, rc::Rc};
/// Contains the current metadata for the document's `<body>`.
#[derive(Clone, Default)]
pub struct BodyContext {
class: Rc<RefCell<Option<TextProp>>>,
}
impl BodyContext {
/// Converts the `<body>` metadata into an HTML string.
pub fn as_string(&self) -> Option<String> {
self.class
.borrow()
.as_ref()
.map(|class| format!(" class=\"{}\"", class.get()))
}
}
impl std::fmt::Debug for BodyContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("TitleContext").finish()
}
}
/// A component to set metadata on the documents `<body>` element from
/// within the application.
///
/// ```
/// use leptos::*;
/// use leptos_meta::*;
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
/// let (prefers_dark, set_prefers_dark) = create_signal(cx, false);
/// let body_class = move || if prefers_dark() {
/// "dark".to_string()
/// } else {
/// "light".to_string()
/// };
///
/// view! { cx,
/// <main>
/// <Body class=body_class/>
/// </main>
/// }
/// }
/// ```
#[component(transparent)]
pub fn Body(
cx: Scope,
/// The `class` attribute on the `<body>`.
#[prop(optional, into)]
class: Option<TextProp>,
) -> impl IntoView {
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
let el = document().body().expect("there to be a <body> element");
if let Some(class) = class {
create_render_effect(cx, move |_| {
let value = class.get();
_ = el.set_attribute("class", &value);
});
}
} else {
let meta = crate::use_head(cx);
*meta.body.class.borrow_mut() = class;
}
}
}

85
meta/src/html.rs Normal file
View File

@@ -0,0 +1,85 @@
use crate::TextProp;
use cfg_if::cfg_if;
use leptos::*;
use std::{cell::RefCell, rc::Rc};
/// Contains the current metadata for the document's `<html>`.
#[derive(Clone, Default)]
pub struct HtmlContext {
lang: Rc<RefCell<Option<TextProp>>>,
dir: Rc<RefCell<Option<TextProp>>>,
}
impl HtmlContext {
/// Converts the `<html>` metadata into an HTML string.
pub fn as_string(&self) -> Option<String> {
match (self.lang.borrow().as_ref(), self.dir.borrow().as_ref()) {
(None, None) => None,
(Some(lang), None) => Some(format!(" lang=\"{}\"", lang.get())),
(None, Some(dir)) => Some(format!(" dir=\"{}\"", dir.get())),
(Some(lang), Some(dir)) => {
Some(format!(" lang=\"{}\" dir=\"{}\"", lang.get(), dir.get()))
}
}
}
}
impl std::fmt::Debug for HtmlContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("TitleContext").finish()
}
}
/// A component to set metadata on the documents `<html>` element from
/// within the application.
///
/// ```
/// use leptos::*;
/// use leptos_meta::*;
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
///
/// view! { cx,
/// <main>
/// <Html lang="he" dir="rtl"/>
/// </main>
/// }
/// }
/// ```
#[component(transparent)]
pub fn Html(
cx: Scope,
/// The `lang` attribute on the `<html>`.
#[prop(optional, into)]
lang: Option<TextProp>,
/// The `dir` attribute on the `<html>`.
#[prop(optional, into)]
dir: Option<TextProp>,
) -> impl IntoView {
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
let el = document().document_element().expect("there to be a <html> element");
if let Some(lang) = lang {
let el = el.clone();
create_render_effect(cx, move |_| {
let value = lang.get();
_ = el.set_attribute("lang", &value);
});
}
if let Some(dir) = dir {
create_render_effect(cx, move |_| {
let value = dir.get();
_ = el.set_attribute("dir", &value);
});
}
} else {
let meta = crate::use_head(cx);
*meta.html.lang.borrow_mut() = lang;
*meta.html.dir.borrow_mut() = dir;
}
}
}

View File

@@ -55,12 +55,16 @@ use std::{
use leptos::{leptos_dom::debug_warn, *};
mod body;
mod html;
mod link;
mod meta_tags;
mod script;
mod style;
mod stylesheet;
mod title;
pub use body::*;
pub use html::*;
pub use link::*;
pub use meta_tags::*;
pub use script::*;
@@ -74,13 +78,19 @@ pub use title::*;
/// [provide_meta_context].
#[derive(Clone, Default, Debug)]
pub struct MetaContext {
pub(crate) title: TitleContext,
pub(crate) tags: MetaTagsContext,
/// Metadata associated with the `<html>` element
pub html: HtmlContext,
/// Metadata associated with the `<title>` element.
pub title: TitleContext,
/// Metadata associated with the `<body>` element
pub body: BodyContext,
/// Other metadata tags.
pub tags: MetaTagsContext,
}
/// Manages all of the element created by components.
#[derive(Clone, Default)]
pub(crate) struct MetaTagsContext {
pub struct MetaTagsContext {
next_id: Rc<Cell<MetaTagId>>,
#[allow(clippy::type_complexity)]
els: Rc<RefCell<HashMap<String, (HtmlElement<AnyElement>, Scope, Option<web_sys::Element>)>>>,
@@ -93,7 +103,8 @@ impl std::fmt::Debug for MetaTagsContext {
}
impl MetaTagsContext {
#[cfg(feature = "ssr")]
/// Converts metadata tags into an HTML string.
#[cfg(any(feature = "ssr", docs))]
pub fn as_string(&self) -> String {
self.els
.borrow()
@@ -102,6 +113,7 @@ impl MetaTagsContext {
.collect()
}
#[doc(hidden)]
pub fn register(&self, cx: Scope, id: String, builder_el: HtmlElement<AnyElement>) {
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
@@ -209,7 +221,7 @@ impl MetaContext {
///
/// # #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
/// run_scope(create_runtime(), |cx| {
/// provide_context(cx, MetaContext::new());
/// provide_meta_context(cx);
///
/// let app = view! { cx,
/// <main>

View File

@@ -55,7 +55,7 @@ where
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// provide_context(cx, MetaContext::new());
/// provide_meta_context(cx);
/// let formatter = |text| format!("{text} — Leptos Online");
///
/// view! { cx,

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.1.1"
version = "0.1.3"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -13,17 +13,16 @@ leptos = { workspace = true }
cfg-if = "1"
common_macros = "0.1"
gloo-net = "0.2"
itertools = "0.10"
lazy_static = "1"
linear-map = "1"
log = "0.4"
regex = { version = "1", optional = true }
bincode = "1"
url = { version = "2", optional = true }
percent-encoding = "2"
thiserror = "1"
serde_urlencoded = "0.7"
serde = "1"
tracing = "0.1"
js-sys = { version = "0.3" }
wasm-bindgen = { version = "0.2" }
wasm-bindgen-futures = { version = "0.4" }
@@ -31,26 +30,26 @@ wasm-bindgen-futures = { version = "0.4" }
[dependencies.web-sys]
version = "0.3"
features = [
# History/Routing
"History",
"HtmlAnchorElement",
"MouseEvent",
"Url",
# Form
"FormData",
"HtmlButtonElement",
"HtmlFormElement",
"HtmlInputElement",
"SubmitEvent",
"Url",
"UrlSearchParams",
# Fetching in Hydrate Mode
"Headers",
"Request",
"RequestInit",
"RequestMode",
"Response",
"Window",
# History/Routing
"History",
"HtmlAnchorElement",
"MouseEvent",
"Url",
# Form
"FormData",
"HtmlButtonElement",
"HtmlFormElement",
"HtmlInputElement",
"SubmitEvent",
"Url",
"UrlSearchParams",
# Fetching in Hydrate Mode
"Headers",
"Request",
"RequestInit",
"RequestMode",
"Response",
"Window",
]
[features]

View File

@@ -38,91 +38,114 @@ pub fn Form<A>(
#[allow(clippy::type_complexity)]
on_response: Option<Rc<dyn Fn(&web_sys::Response)>>,
/// Component children; should include the HTML of the form elements.
children: Box<dyn FnOnce(Scope) -> Fragment>,
children: Children,
) -> impl IntoView
where
A: ToHref + 'static,
{
let action_version = version;
let action = use_resolved_path(cx, move || action.to_href()());
fn inner(
cx: Scope,
method: Option<&'static str>,
action: Memo<Option<String>>,
enctype: Option<String>,
version: Option<RwSignal<usize>>,
error: Option<RwSignal<Option<Box<dyn Error>>>>,
#[allow(clippy::type_complexity)] on_form_data: Option<Rc<dyn Fn(&web_sys::FormData)>>,
#[allow(clippy::type_complexity)] on_response: Option<Rc<dyn Fn(&web_sys::Response)>>,
children: Children,
) -> HtmlElement<Form> {
let action_version = version;
let on_submit = move |ev: web_sys::SubmitEvent| {
if ev.default_prevented() {
return;
}
let navigate = use_navigate(cx);
let on_submit = move |ev: web_sys::SubmitEvent| {
if ev.default_prevented() {
return;
}
let navigate = use_navigate(cx);
let (form, method, action, enctype) = extract_form_attributes(&ev);
let (form, method, action, enctype) = extract_form_attributes(&ev);
let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw();
if let Some(on_form_data) = on_form_data.clone() {
on_form_data(&form_data);
}
let params =
web_sys::UrlSearchParams::new_with_str_sequence_sequence(&form_data).unwrap_throw();
let action = use_resolved_path(cx, move || action.clone())
.get()
.unwrap_or_default();
// POST
if method == "post" {
ev.prevent_default();
let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw();
if let Some(on_form_data) = on_form_data.clone() {
on_form_data(&form_data);
}
let params =
web_sys::UrlSearchParams::new_with_str_sequence_sequence(&form_data).unwrap_throw();
let action = use_resolved_path(cx, move || action.clone())
.get()
.unwrap_or_default();
// POST
if method == "post" {
ev.prevent_default();
let on_response = on_response.clone();
spawn_local(async move {
let res = gloo_net::http::Request::post(&action)
.header("Accept", "application/json")
.header("Content-Type", &enctype)
.body(params)
.send()
.await;
match res {
Err(e) => {
log::error!("<Form/> error while POSTing: {e:#?}");
if let Some(error) = error {
error.set(Some(Box::new(e)));
}
}
Ok(resp) => {
if let Some(version) = action_version {
version.update(|n| *n += 1);
}
if let Some(error) = error {
error.set(None);
}
if let Some(on_response) = on_response.clone() {
on_response(resp.as_raw());
let on_response = on_response.clone();
spawn_local(async move {
let res = gloo_net::http::Request::post(&action)
.header("Accept", "application/json")
.header("Content-Type", &enctype)
.body(params)
.send()
.await;
match res {
Err(e) => {
log::error!("<Form/> error while POSTing: {e:#?}");
if let Some(error) = error {
error.set(Some(Box::new(e)));
}
}
Ok(resp) => {
if let Some(version) = action_version {
version.update(|n| *n += 1);
}
if let Some(error) = error {
error.set(None);
}
if let Some(on_response) = on_response.clone() {
on_response(resp.as_raw());
}
if resp.status() == 303 {
if let Some(redirect_url) = resp.headers().get("Location") {
_ = navigate(&redirect_url, Default::default());
if resp.status() == 303 {
if let Some(redirect_url) = resp.headers().get("Location") {
_ = navigate(&redirect_url, Default::default());
}
}
}
}
}
});
}
// otherwise, GET
else {
let params = params.to_string().as_string().unwrap_or_default();
if navigate(&format!("{action}?{params}"), Default::default()).is_ok() {
ev.prevent_default();
});
}
// otherwise, GET
else {
let params = params.to_string().as_string().unwrap_or_default();
if navigate(&format!("{action}?{params}"), Default::default()).is_ok() {
ev.prevent_default();
}
}
};
let method = method.unwrap_or("get");
view! { cx,
<form
method=method
action=move || action.get()
enctype=enctype
on:submit=on_submit
>
{children(cx)}
</form>
}
};
let method = method.unwrap_or("get");
view! { cx,
<form
method=method
action=move || action.get()
enctype=enctype
on:submit=on_submit
>
{children(cx)}
</form>
}
let action = use_resolved_path(cx, move || action.to_href()());
inner(
cx,
method,
action,
enctype,
version,
error,
on_form_data,
on_response,
children,
)
}
/// Automatically turns a server [Action](leptos_server::Action) into an HTML
@@ -136,7 +159,7 @@ pub fn ActionForm<I, O>(
/// manually using [leptos_server::Action::using_server_fn].
action: Action<I, Result<O, ServerFnError>>,
/// Component children; should include the HTML of the form elements.
children: Box<dyn FnOnce(Scope) -> Fragment>,
children: Children,
) -> impl IntoView
where
I: Clone + ServerFn + 'static,
@@ -155,7 +178,10 @@ where
let on_form_data = Rc::new(move |form_data: &web_sys::FormData| {
let data = action_input_from_form_data(form_data);
match data {
Ok(data) => input.set(Some(data)),
Ok(data) => {
input.set(Some(data));
action.set_pending(true);
}
Err(e) => log::error!("{e}"),
}
});
@@ -167,11 +193,6 @@ where
JsFuture::from(resp.text().expect("couldn't get .text() from Response")).await;
match body {
Ok(json) => {
log::debug!(
"body is {:?}\nO is {:?}",
json.as_string().unwrap(),
std::any::type_name::<O>()
);
match O::from_json(
&json.as_string().expect("couldn't get String from JsString"),
) {
@@ -182,7 +203,9 @@ where
}
}
Err(e) => log::error!("{e:?}"),
}
};
input.set(None);
action.set_pending(false);
});
});
@@ -210,7 +233,7 @@ pub fn MultiActionForm<I, O>(
/// manually using [leptos_server::Action::using_server_fn].
action: MultiAction<I, Result<O, ServerFnError>>,
/// Component children; should include the HTML of the form elements.
children: Box<dyn FnOnce(Scope) -> Fragment>,
children: Children,
) -> impl IntoView
where
I: Clone + ServerFn + 'static,

View File

@@ -65,40 +65,52 @@ pub fn A<H>(
#[prop(optional, into)]
class: Option<MaybeSignal<String>>,
/// The nodes or elements to be shown inside the link.
children: Box<dyn FnOnce(Scope) -> Fragment>,
children: Children,
) -> impl IntoView
where
H: ToHref + 'static,
{
let location = use_location(cx);
let href = use_resolved_path(cx, move || href.to_href()());
let is_active = create_memo(cx, move |_| match href.get() {
None => false,
fn inner(
cx: Scope,
href: Memo<Option<String>>,
exact: bool,
state: Option<State>,
replace: bool,
class: Option<MaybeSignal<String>>,
children: Children,
) -> HtmlElement<A> {
let location = use_location(cx);
let is_active = create_memo(cx, move |_| match href.get() {
None => false,
Some(to) => {
let path = to
.split(['?', '#'])
.next()
.unwrap_or_default()
.to_lowercase();
let loc = location.pathname.get().to_lowercase();
if exact {
loc == path
} else {
loc.starts_with(&path)
Some(to) => {
let path = to
.split(['?', '#'])
.next()
.unwrap_or_default()
.to_lowercase();
let loc = location.pathname.get().to_lowercase();
if exact {
loc == path
} else {
loc.starts_with(&path)
}
}
}
});
});
view! { cx,
<a
href=move || href.get().unwrap_or_default()
prop:state={state.map(|s| s.to_js_value())}
prop:replace={replace}
aria-current=move || if is_active.get() { Some("page") } else { None }
class=move || class.as_ref().map(|class| class.get())
>
{children(cx)}
</a>
view! { cx,
<a
href=move || href.get().unwrap_or_default()
prop:state={state.map(|s| s.to_js_value())}
prop:replace={replace}
aria-current=move || if is_active.get() { Some("page") } else { None }
class=move || class.as_ref().map(|class| class.get())
>
{children(cx)}
</a>
}
}
let href = use_resolved_path(cx, move || href.to_href()());
inner(cx, href, exact, state, replace, class, children)
}

View File

@@ -1,6 +1,7 @@
mod form;
mod link;
mod outlet;
mod redirect;
mod route;
mod router;
mod routes;
@@ -8,6 +9,7 @@ mod routes;
pub use form::*;
pub use link::*;
pub use outlet::*;
pub use redirect::*;
pub use route::*;
pub use router::*;
pub use routes::*;

View File

@@ -7,6 +7,7 @@ use leptos::*;
/// that child route is displayed. Renders nothing if there is no nested child.
#[component]
pub fn Outlet(cx: Scope) -> impl IntoView {
let id = HydrationCtx::id();
let route = use_route(cx);
let is_showing = Rc::new(Cell::new(None::<(usize, Scope)>));
let (outlet, set_outlet) = create_signal(cx, None::<View>);
@@ -26,11 +27,11 @@ pub fn Outlet(cx: Scope) -> impl IntoView {
prev_scope.dispose();
}
is_showing.set(Some((child.id(), child.cx())));
provide_context(child.cx(), child.clone());
set_outlet.set(Some(child.outlet().into_view(cx)))
provide_context(cx, child.clone());
set_outlet.set(Some(child.outlet(cx).into_view(cx)))
}
}
});
move || outlet.get()
leptos::DynChild::new_with_id(id, move || outlet.get())
}

View File

@@ -0,0 +1,65 @@
use crate::{use_navigate, use_resolved_path, NavigateOptions};
use leptos::{component, provide_context, use_context, IntoView, Scope};
use std::rc::Rc;
/// Redirects the user to a new URL, whether on the client side or on the server
/// side. If rendered on the server, this sets a `302` status code and sets a `Location`
/// header. If rendered in the browser, it uses client-side navigation to redirect.
/// In either case, it resolves the route relative to the current route. (To use
/// an absolute path, prefix it with `/`).
///
/// **Note**: Support for server-side redirects is provided by the server framework
/// integrations (`leptos_actix` and `leptos_axum`). If youre not using one of those
/// integrations, you should manually provide a way of redirecting on the server
/// using [provide_server_redirect].
#[component]
pub fn Redirect<P>(
cx: Scope,
/// The relative path to which the user should be redirected.
path: P,
/// Navigation options to be used on the client side.
#[prop(optional)]
options: Option<NavigateOptions>,
) -> impl IntoView
where
P: std::fmt::Display + 'static,
{
// resolve relative path
let path = use_resolved_path(cx, move || path.to_string());
let path = path.get().unwrap_or_else(|| "/".to_string());
// redirect on the server
if let Some(redirect_fn) = use_context::<ServerRedirectFunction>(cx) {
(redirect_fn.f)(&path);
}
// redirect on the client
let navigate = use_navigate(cx);
navigate(&path, options.unwrap_or_default())
}
/// Wrapping type for a function provided as context to allow for
/// server-side redirects. See [provide_server_redirect]
/// and [Redirect].
#[derive(Clone)]
pub struct ServerRedirectFunction {
f: Rc<dyn Fn(&str)>,
}
impl std::fmt::Debug for ServerRedirectFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServerRedirectFunction").finish()
}
}
/// Provides a function that can be used to redirect the user to another
/// absolute path, on the server. This should set a `302` status code and an
/// appropriate `Location` header.
pub fn provide_server_redirect(cx: Scope, handler: impl Fn(&str) + 'static) {
provide_context(
cx,
ServerRedirectFunction {
f: Rc::new(handler),
},
)
}

View File

@@ -29,38 +29,54 @@ pub fn Route<E, F, P>(
view: F,
/// `children` may be empty or include nested routes.
#[prop(optional)]
children: Option<Box<dyn FnOnce(Scope) -> Fragment>>,
children: Option<Children>,
) -> impl IntoView
where
E: IntoView,
F: Fn(Scope) -> E + 'static,
P: std::fmt::Display,
{
let children = children
.map(|children| {
children(cx)
.as_children()
.iter()
.filter_map(|child| {
child
.as_transparent()
.and_then(|t| t.downcast_ref::<RouteDefinition>())
})
.cloned()
.collect::<Vec<_>>()
})
.unwrap_or_default();
let id = ROUTE_ID.with(|id| {
let next = id.get() + 1;
id.set(next);
next
});
RouteDefinition {
id,
path: path.to_string(),
children,
view: Rc::new(move |cx| view(cx).into_view(cx)),
fn inner(
cx: Scope,
children: Option<Children>,
path: String,
view: Rc<dyn Fn(Scope) -> View>,
) -> RouteDefinition {
let children = children
.map(|children| {
children(cx)
.as_children()
.iter()
.filter_map(|child| {
child
.as_transparent()
.and_then(|t| t.downcast_ref::<RouteDefinition>())
})
.cloned()
.collect::<Vec<_>>()
})
.unwrap_or_default();
let id = ROUTE_ID.with(|id| {
let next = id.get() + 1;
id.set(next);
next
});
RouteDefinition {
id,
path,
children,
view,
}
}
inner(
cx,
children,
path.to_string(),
Rc::new(move |cx| view(cx).into_view(cx)),
)
}
impl IntoView for RouteDefinition {
@@ -104,7 +120,7 @@ impl RouteContext {
path: RefCell::new(path),
original_path: route.original_path.to_string(),
params,
outlet: Box::new(move || Some(element(cx))),
outlet: Box::new(move |cx| Some(element(cx))),
}),
})
}
@@ -155,7 +171,7 @@ impl RouteContext {
path: RefCell::new(path.to_string()),
original_path: path.to_string(),
params: create_memo(cx, |_| ParamsMap::new()),
outlet: Box::new(move || fallback.as_ref().map(move |f| f(cx))),
outlet: Box::new(move |cx| fallback.as_ref().map(move |f| f(cx))),
}),
}
}
@@ -171,8 +187,8 @@ impl RouteContext {
}
/// The view associated with the current route.
pub fn outlet(&self) -> impl IntoView {
(self.inner.outlet)()
pub fn outlet(&self, cx: Scope) -> impl IntoView {
(self.inner.outlet)(cx)
}
}
@@ -184,7 +200,7 @@ pub(crate) struct RouteContextInner {
pub(crate) path: RefCell<String>,
pub(crate) original_path: String,
pub(crate) params: Memo<ParamsMap>,
pub(crate) outlet: Box<dyn Fn() -> Option<View>>,
pub(crate) outlet: Box<dyn Fn(Scope) -> Option<View>>,
}
impl PartialEq for RouteContextInner {

View File

@@ -32,7 +32,7 @@ pub fn Router(
/// The `<Router/>` should usually wrap your whole page. It can contain
/// any elements, and should include a [Routes](crate::Routes) component somewhere
/// to define and display [Route](crate::Route)s.
children: Box<dyn FnOnce(Scope) -> Fragment>,
children: Children,
) -> impl IntoView {
// create a new RouterContext and provide it to every component beneath the router
let router = RouterContext::new(cx, base, fallback);

View File

@@ -22,14 +22,13 @@ use crate::{
pub fn Routes(
cx: Scope,
#[prop(optional)] base: Option<String>,
children: Box<dyn FnOnce(Scope) -> Fragment>,
children: Children,
) -> impl IntoView {
let router = use_context::<RouterContext>(cx)
.expect("<Routes/> component should be nested within a <Router/>.");
let base_route = router.base();
let mut branches = Vec::new();
let id_before = HydrationCtx::peek();
let frag = children(cx);
let children = frag
.as_children()
@@ -195,11 +194,12 @@ pub fn Routes(
});
// show the root route
let id = HydrationCtx::id();
let root = create_memo(cx, move |prev| {
provide_context(cx, route_states);
route_states.with(|state| {
if state.routes.borrow().is_empty() {
Some(base_route.outlet().into_view(cx))
Some(base_route.outlet(cx).into_view(cx))
} else {
let root = state.routes.borrow();
let root = root.get(0);
@@ -208,7 +208,7 @@ pub fn Routes(
}
if prev.is_none() || !root_equal.get() {
root.as_ref().map(|route| route.outlet().into_view(cx))
root.as_ref().map(|route| route.outlet(cx).into_view(cx))
} else {
prev.cloned().unwrap()
}
@@ -216,8 +216,8 @@ pub fn Routes(
})
});
HydrationCtx::continue_from(id_before);
(move || root.get()).into_view(cx)
//HydrationCtx::continue_from(id_before);
leptos::DynChild::new_with_id(id, move || root.get())
}
#[derive(Clone, Debug, PartialEq)]

View File

@@ -8,10 +8,6 @@
//! apps (SPAs), server-side rendering/multi-page apps (MPAs), or to synchronize
//! state between the two.
//!
//! **Note:** This is a work in progress. The feature to pass client-side route [State] in
//! [History.state](https://developer.mozilla.org/en-US/docs/Web/API/History/state), in particular,
//! is incomplete.
//!
//! ## Philosophy
//!
//! Leptos Router is built on a few simple principles:
@@ -23,12 +19,7 @@
//! and are rendered by different components. This means you can navigate between siblings
//! in this tree without re-rendering or triggering any change in the parent routes.
//!
//! 3. **Route-based data loading.** Each route should know exactly which data it needs
//! to render itself when the route is defined. This allows each routes data to be
//! reloaded independently, and allows data from nested routes to be loaded in parallel,
//! avoiding waterfalls.
//!
//! 4. **Progressive enhancement.** The [A] and [Form] components resolve any relative
//! 3. **Progressive enhancement.** The [A] and [Form] components resolve any relative
//! nested routes, render actual `<a>` and `<form>` elements, and (when possible)
//! upgrading them to handle those navigations with client-side routing. If youre using
//! them with server-side rendering (with or without hydration), they just work,
@@ -202,6 +193,7 @@ mod history;
mod hooks;
#[doc(hidden)]
pub mod matching;
pub use matching::RouteDefinition;
pub use components::*;
#[cfg(any(feature = "ssr", doc))]

View File

@@ -3,11 +3,18 @@ use std::rc::Rc;
use leptos::leptos_dom::View;
use leptos::*;
/// Defines a single route in a nested route tree. This is the return
/// type of the [`<Route/>`](crate::Route) component, but can also be
/// used to build your own configuration-based or filesystem-based routing.
#[derive(Clone)]
pub struct RouteDefinition {
/// A unique ID for each route.
pub id: usize,
/// The path. This can include params like `:id` or wildcards like `*all`.
pub path: String,
/// Other route definitions nested within this one.
pub children: Vec<RouteDefinition>,
/// The view that should be displayed when this route is matched.
pub view: Rc<dyn Fn(Scope) -> View>,
}