mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 13:43:01 -05:00
Compare commits
23 Commits
cloudflare
...
update-par
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e236b7d07 | ||
|
|
6b683f9ab6 | ||
|
|
aae4d4445e | ||
|
|
bb9df8937d | ||
|
|
05277f03b6 | ||
|
|
f698f8badd | ||
|
|
98f51fec8a | ||
|
|
65465cad78 | ||
|
|
ddee545e7e | ||
|
|
cbfb724af2 | ||
|
|
0953007f47 | ||
|
|
53f7677258 | ||
|
|
6373fd42fb | ||
|
|
e1bcf77b03 | ||
|
|
b0762bbfb5 | ||
|
|
63a7a4dec1 | ||
|
|
1f6a326268 | ||
|
|
0efc39db8b | ||
|
|
cbf2f73e95 | ||
|
|
160f336303 | ||
|
|
e2b1365e46 | ||
|
|
45eee12b18 | ||
|
|
e2cdbc746f |
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
9
examples/counter_without_macros/Makefile.toml
Normal file
9
examples/counter_without_macros/Makefile.toml
Normal 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"
|
||||
@@ -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"
|
||||
|
||||
94
examples/errors_axum/Cargo.toml
Normal file
94
examples/errors_axum/Cargo.toml
Normal 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
|
||||
21
examples/errors_axum/LICENSE
Normal file
21
examples/errors_axum/LICENSE
Normal 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.
|
||||
9
examples/errors_axum/Makefile.toml
Normal file
9
examples/errors_axum/Makefile.toml
Normal 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"
|
||||
41
examples/errors_axum/README.md
Normal file
41
examples/errors_axum/README.md
Normal 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
|
||||
```
|
||||
BIN
examples/errors_axum/public/favicon.ico
Normal file
BIN
examples/errors_axum/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
62
examples/errors_axum/src/error_template.rs
Normal file
62
examples/errors_axum/src/error_template.rs
Normal 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>
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
}
|
||||
19
examples/errors_axum/src/errors.rs
Normal file
19
examples/errors_axum/src/errors.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
45
examples/errors_axum/src/fallback.rs
Normal file
45
examples/errors_axum/src/fallback.rs
Normal 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}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}}
|
||||
79
examples/errors_axum/src/landing.rs
Normal file
79
examples/errors_axum/src/landing.rs
Normal 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)
|
||||
}
|
||||
25
examples/errors_axum/src/lib.rs
Normal file
25
examples/errors_axum/src/lib.rs
Normal 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/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
72
examples/errors_axum/src/main.rs
Normal file
72
examples/errors_axum/src/main.rs
Normal 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.
|
||||
}
|
||||
3
examples/errors_axum/style.css
Normal file
3
examples/errors_axum/style.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.pending {
|
||||
color: purple;
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -7,7 +7,8 @@ use web_sys::MouseEvent;
|
||||
// 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
|
||||
// the child component to call
|
||||
// 4) <ButtonC/>: providing a context that is used in the component (rather than prop drilling)
|
||||
// 3) <ButtonC/>: adding an `on:` event listener to a component
|
||||
// 4) <ButtonD/>: providing a context that is used in the component (rather than prop drilling)
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct SmallcapsContext(WriteSignal<bool>);
|
||||
@@ -17,6 +18,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
// just some signals to toggle three classes on our <p>
|
||||
let (red, set_red) = create_signal(cx, false);
|
||||
let (right, set_right) = create_signal(cx, false);
|
||||
let (italics, set_italics) = create_signal(cx, false);
|
||||
let (smallcaps, set_smallcaps) = create_signal(cx, false);
|
||||
|
||||
// the newtype pattern isn't *necessary* here but is a good practice
|
||||
@@ -31,6 +33,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
// class: attributes take F: Fn() => bool, and these signals all implement Fn()
|
||||
class:red=red
|
||||
class:right=right
|
||||
class:italics=italics
|
||||
class:smallcaps=smallcaps
|
||||
>
|
||||
"Lorem ipsum sit dolor amet."
|
||||
@@ -42,8 +45,13 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
// Button B: pass a closure
|
||||
<ButtonB on_click=move |_| set_right.update(|value| *value = !*value)/>
|
||||
|
||||
// Button B: use a regular event listener
|
||||
// setting an event listener on a component like this applies it
|
||||
// to each of the top-level elements the component returns
|
||||
<ButtonC on:click=move |_| set_italics.update(|value| *value = !*value)/>
|
||||
|
||||
// Button D gets its setter from context rather than props
|
||||
<ButtonC/>
|
||||
<ButtonD/>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
@@ -53,7 +61,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
pub fn ButtonA(
|
||||
cx: Scope,
|
||||
/// Signal that will be toggled when the button is clicked.
|
||||
setter: WriteSignal<bool>
|
||||
setter: WriteSignal<bool>,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
cx,
|
||||
@@ -70,7 +78,7 @@ pub fn ButtonA(
|
||||
pub fn ButtonB<F>(
|
||||
cx: Scope,
|
||||
/// Callback that will be invoked when the button is clicked.
|
||||
on_click: F
|
||||
on_click: F,
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn(MouseEvent) + 'static,
|
||||
@@ -97,10 +105,22 @@ where
|
||||
// if Rust ever had named function arguments we could drop this requirement
|
||||
}
|
||||
|
||||
/// Button C is a dummy: it renders a button but doesn't handle
|
||||
/// its click. Instead, the parent component adds an event listener.
|
||||
#[component]
|
||||
pub fn ButtonC(cx: Scope) -> impl IntoView {
|
||||
view! {
|
||||
cx,
|
||||
<button>
|
||||
"Toggle Italics"
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
/// Button D is very similar to Button A, but instead of passing the setter as a prop
|
||||
/// we get it from the context
|
||||
#[component]
|
||||
pub fn ButtonC(cx: Scope) -> impl IntoView {
|
||||
pub fn ButtonD(cx: Scope) -> impl IntoView {
|
||||
let setter = use_context::<SmallcapsContext>(cx).unwrap().0;
|
||||
|
||||
view! {
|
||||
@@ -112,3 +132,7 @@ pub fn ButtonC(cx: Scope) -> impl IntoView {
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
leptos::mount_to_body(|cx| view! { cx, <App/> })
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
19
examples/todo_app_sqlite_axum/src/errors.rs
Normal file
19
examples/todo_app_sqlite_axum/src/errors.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
pub mod error_template;
|
||||
pub mod errors;
|
||||
pub mod fallback;
|
||||
pub mod todo;
|
||||
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -646,6 +646,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
|
||||
@@ -692,4 +702,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,5 +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"
|
||||
|
||||
@@ -637,6 +637,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.
|
||||
@@ -659,4 +674,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
|
||||
}
|
||||
}
|
||||
|
||||
9
integrations/cloudflare/.gitignore
vendored
9
integrations/cloudflare/.gitignore
vendored
@@ -1,9 +0,0 @@
|
||||
.DS_Store
|
||||
/node_modules
|
||||
|
||||
**/*.rs.bk
|
||||
wasm-pack.log
|
||||
|
||||
build/
|
||||
/target
|
||||
/dist
|
||||
@@ -1,30 +0,0 @@
|
||||
[package]
|
||||
name = "leptos-worker"
|
||||
version = "0.0.0"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = ["console_error_panic_hook"]
|
||||
|
||||
[dependencies]
|
||||
cfg-if = "0.1.2"
|
||||
worker = "0.0.11"
|
||||
serde_json = "1.0.67"
|
||||
leptos = { path = "/Users/gjohnston/Documents/Projects/leptos-main/leptos/leptos", default-features = false, features = ["ssr"] }
|
||||
leptos_router = { path = "/Users/gjohnston/Documents/Projects/leptos-main/leptos/router", default-features = false, features = ["ssr"] }
|
||||
leptos_meta = { path = "/Users/gjohnston/Documents/Projects/leptos-main/leptos/meta", default-features = false, features = ["ssr"]}
|
||||
tracing = "0.1"
|
||||
# The `console_error_panic_hook` crate provides better debugging of panics by
|
||||
# logging them with `console.error`. This is great for development, but requires
|
||||
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
|
||||
# code size when deploying.
|
||||
console_error_panic_hook = { version = "0.1.1", optional = true }
|
||||
parking_lot = "0.12.1"
|
||||
futures = "0.3.26"
|
||||
|
||||
[profile.release]
|
||||
# Tell `rustc` to optimize for small code size.
|
||||
opt-level = "s"
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"deploy": "wrangler publish",
|
||||
"dev": "wrangler dev --local"
|
||||
},
|
||||
"devDependencies": {
|
||||
"wrangler": "^2.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,463 +0,0 @@
|
||||
// Reader's guide...
|
||||
// Look at these functions:
|
||||
// - main
|
||||
// - leptos_routes
|
||||
// - render_app_to_string
|
||||
// - <App/>
|
||||
//
|
||||
// TODO
|
||||
// - What's the best way to serve static client-side JS/Wasm/CSS?
|
||||
// - Does `leptos_routes` run on every single request? Simple log to check.
|
||||
// The issue here is that it renders the app to extract the routes.
|
||||
// This is O(n + 1) for N requests to the server, but this is expensive
|
||||
// if this routing process is re-created every time.
|
||||
|
||||
use std::{sync::Arc, future::Future};
|
||||
|
||||
use futures::StreamExt;
|
||||
use serde_json::json;
|
||||
use worker::*;
|
||||
|
||||
mod utils;
|
||||
|
||||
fn log_request(req: &Request) {
|
||||
console_log!(
|
||||
"{} - [{}], located at: {:?}, within: {}",
|
||||
Date::now().to_string(),
|
||||
req.path(),
|
||||
req.cf().coordinates().unwrap_or_default(),
|
||||
req.cf().region().unwrap_or_else(|| "unknown region".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[event(fetch)]
|
||||
pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result<Response> {
|
||||
log_request(&req);
|
||||
utils::set_panic_hook();
|
||||
|
||||
// Use Cloudflare Worker router to serve all requests
|
||||
let router = Router::new();
|
||||
router
|
||||
// leptos_routes mounts all possible routes from your Leptos app
|
||||
// as Cloudflare router routes
|
||||
.leptos_routes(
|
||||
generate_route_list(|cx| view! { cx, <App/> }),
|
||||
// this function takes the HTTP Request and router context (unused)
|
||||
// `render_app_to_string` provides `Request` via context, so we can access
|
||||
// the HTTP request during server rendering
|
||||
|req, _| render_app_to_string("/pkg", "leptos_worker", req, |cx| view! { cx, <App/> })
|
||||
)
|
||||
.run(req, env)
|
||||
.await
|
||||
}
|
||||
|
||||
// integration
|
||||
use parking_lot::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.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ResponseParts {
|
||||
pub headers: worker::Headers,
|
||||
pub status: Option<u16>,
|
||||
}
|
||||
|
||||
impl ResponseParts {
|
||||
/// Insert a header, overwriting any previous value with the same key
|
||||
pub fn set_header(&mut self, key: &str, value: &str) -> Result<()> {
|
||||
self.headers.set(key, value)
|
||||
}
|
||||
/// Append a header, leaving any header with the same key intact
|
||||
pub fn append_header(&mut self, key: &str, value: &str) -> Result<()> {
|
||||
self.headers.append(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Adding this Struct to your Scope inside of a Server Fn or Elements will allow you to override details of the Response
|
||||
/// like StatusCode and add Headers/Cookies. Because Elements and Server Fns are lower in the tree than the Response generation
|
||||
/// code, it needs to be wrapped in an `Arc<RwLock<>>` so that it can be surfaced
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>);
|
||||
|
||||
impl ResponseOptions {
|
||||
/// A less boilerplatey way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`
|
||||
pub fn overwrite(&self, parts: ResponseParts) {
|
||||
let mut writable = self.0.write();
|
||||
*writable = parts
|
||||
}
|
||||
/// Set the status of the returned Response
|
||||
pub fn set_status(&self, status: u16) {
|
||||
let mut writeable = self.0.write();
|
||||
let res_parts = &mut *writeable;
|
||||
res_parts.status = Some(status);
|
||||
}
|
||||
/// Set a header, overwriting any previous value with the same key
|
||||
pub fn set_header(&self, key: &str, value: &str) {
|
||||
let mut writeable = self.0.write();
|
||||
let res_parts = &mut *writeable;
|
||||
res_parts.headers.set(key, value);
|
||||
}
|
||||
/// Append a header, leaving any header with the same key intact
|
||||
pub fn append_header(&self, key: &str, value: &str) {
|
||||
let mut writeable = self.0.write();
|
||||
let res_parts = &mut *writeable;
|
||||
res_parts.headers.append(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_route_list<IV>(app_fn: impl FnOnce(leptos::Scope) -> IV + 'static) -> Vec<String>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let mut routes = leptos_router::generate_route_list_inner(app_fn);
|
||||
|
||||
// replace empty paths with "/"
|
||||
// otherwise, CF router works very similar to Leptos
|
||||
// with :params and *blobs
|
||||
routes = routes
|
||||
.iter()
|
||||
.map(|s| {
|
||||
if s.is_empty() {
|
||||
return "/".to_string();
|
||||
}
|
||||
s.to_string()
|
||||
})
|
||||
.collect();
|
||||
|
||||
if routes.is_empty() {
|
||||
vec!["/".to_string()]
|
||||
} else {
|
||||
routes
|
||||
}
|
||||
}
|
||||
|
||||
// This is having one of those annoying type/trait issues, which I'll figure out later
|
||||
/*
|
||||
async fn stream_app(
|
||||
pkg_url: &str,
|
||||
crate_name: &str,
|
||||
app: impl FnOnce(leptos::Scope) -> leptos::View + 'static,
|
||||
res_options: ResponseOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
) -> Response {
|
||||
let (stream, runtime, scope) = render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
move |cx| {
|
||||
let meta = use_context::<MetaContext>(cx);
|
||||
let head = meta
|
||||
.as_ref()
|
||||
.map(|meta| meta.dehydrate())
|
||||
.unwrap_or_default();
|
||||
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 meta = use_context::<MetaContext>(cx);
|
||||
|
||||
// see comment above re `leptos_meta`
|
||||
let html_meta = meta.as_ref().and_then(|mc| mc.html.as_string()).unwrap_or_default();
|
||||
let head = format!(r#"
|
||||
<!DOCTYPE html>
|
||||
<html{html_meta}>
|
||||
<head>
|
||||
<link rel="modulepreload" href="/{pkg_url}/{crate_name}.js">
|
||||
<link rel="preload" href="/{pkg_url}/{crate_name}_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<script type="module">import init, {{ hydrate }} from '/{pkg_url}/{crate_name}.js'; init('/{pkg_url}/{crate_name}_bg.wasm').then(hydrate);</script>"#);
|
||||
let tail = "</body></html>";
|
||||
|
||||
let mut stream = Box::pin(
|
||||
futures::stream::once(async move { head.clone() })
|
||||
.chain(stream)
|
||||
.chain(futures::stream::once(async move {
|
||||
runtime.dispose();
|
||||
tail.to_string()
|
||||
}))
|
||||
);
|
||||
|
||||
// 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;
|
||||
let second_chunk = stream.next().await;
|
||||
let third_chunk = stream.next().await;
|
||||
|
||||
let res_options = res_options.0.read();
|
||||
|
||||
let (status, mut headers) = (res_options.status, res_options.headers.clone());
|
||||
let status = status.unwrap_or(200);
|
||||
|
||||
let complete_stream = futures::stream::iter([
|
||||
first_chunk.unwrap(),
|
||||
second_chunk.unwrap(),
|
||||
third_chunk.unwrap(),
|
||||
])
|
||||
.chain(stream)
|
||||
.map(|html| Ok(html));
|
||||
Response::from_stream(stream)
|
||||
.expect("to create Response from Stream")
|
||||
.with_status(status)
|
||||
.with_headers(headers)
|
||||
}
|
||||
|
||||
pub fn render_app_to_stream_with_additional_context<IV>(
|
||||
pkg_url: &str,
|
||||
crate_name: &str,
|
||||
req: Request,
|
||||
app_fn: impl FnOnce(Scope) -> IV + Clone + 'static,
|
||||
additional_context: impl FnOnce(Scope) + Clone + Send + 'static
|
||||
)-> Result<Response>
|
||||
where IV: IntoView
|
||||
{
|
||||
let pkg_url = pkg_url.to_owned();
|
||||
let crate_name = crate_name.to_owned();
|
||||
let runtime = create_runtime();
|
||||
let (html, _, _) = run_scope_undisposed(runtime, move |cx| {
|
||||
let integration = RouterIntegrationContext::new(ServerIntegration { path: format!("https://leptos.dev{}", req.path()) });
|
||||
provide_context(cx, integration);
|
||||
provide_context(cx, MetaContext::new());
|
||||
provide_context(cx, Arc::new(req.clone()));
|
||||
let html = app_fn(cx).into_view(cx).render_to_string(cx);
|
||||
|
||||
let meta = use_context::<MetaContext>(cx);
|
||||
let html_meta = meta.as_ref().and_then(|mc| mc.html.as_string()).unwrap_or_default();
|
||||
let body_meta = meta.as_ref().and_then(|mc| mc.body.as_string()).unwrap_or_default();
|
||||
let meta_tags = meta.map(|mc| mc.dehydrate()).unwrap_or_default();
|
||||
format!(r#"
|
||||
<!DOCTYPE html>
|
||||
<html{html_meta}>
|
||||
<head>
|
||||
<link rel="modulepreload" href="/{pkg_url}/{crate_name}.js">
|
||||
<link rel="preload" href="/{pkg_url}/{crate_name}_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<script type="module">import init, {{ hydrate }} from '/{pkg_url}/{crate_name}.js'; init('/{pkg_url}/{crate_name}_bg.wasm').then(hydrate);</script>
|
||||
{meta_tags}
|
||||
</head>
|
||||
<body{body_meta}>
|
||||
{html}
|
||||
</body>
|
||||
</html>"#)
|
||||
});
|
||||
runtime.dispose();
|
||||
Response::from_html(html)
|
||||
}*/
|
||||
|
||||
pub fn render_app_to_string<IV>(
|
||||
pkg_url: &str,
|
||||
crate_name: &str,
|
||||
req: Request,
|
||||
app_fn: impl FnOnce(Scope) -> IV + Clone + 'static
|
||||
)-> Result<Response>
|
||||
where IV: IntoView
|
||||
{
|
||||
// this stuff is so we know where to load client-side JS/Wasm from
|
||||
let pkg_url = pkg_url.to_owned();
|
||||
let crate_name = crate_name.to_owned();
|
||||
let runtime = create_runtime();
|
||||
|
||||
// build the app shell
|
||||
let (html, _, _) = run_scope_undisposed(runtime, move |cx| {
|
||||
let integration = RouterIntegrationContext::new(ServerIntegration {
|
||||
// add https://leptos.dev so URL crate doesn't fuss
|
||||
path: format!("https://leptos.dev{}", req.path())
|
||||
});
|
||||
// this is how the router knows where we are when server rendering
|
||||
provide_context(cx, integration);
|
||||
|
||||
// leptos_meta lets you
|
||||
// 1. add attributes to the root <html> tag
|
||||
// 2. add attributes to <body>
|
||||
// 3. inject <link>, <style>, <meta>, and <script> tags
|
||||
// into the <head>
|
||||
// (all from within components that would otherwise be in the <body>)
|
||||
//
|
||||
// So we provide `MetaContext`, the app can use it, then
|
||||
// (below) we extract some data from it to inject into HTML before serving
|
||||
provide_context(cx, MetaContext::new());
|
||||
|
||||
// provide an Arc<Request> to be able to access headers, etc. during SSR
|
||||
provide_context(cx, Arc::new(req.clone()));
|
||||
|
||||
// render the shell
|
||||
let html = app_fn(cx).into_view(cx).render_to_string(cx);
|
||||
|
||||
// now inject `leptos_meta` stuff
|
||||
let meta = use_context::<MetaContext>(cx);
|
||||
let html_meta = meta.as_ref().and_then(|mc| mc.html.as_string()).unwrap_or_default();
|
||||
let body_meta = meta.as_ref().and_then(|mc| mc.body.as_string()).unwrap_or_default();
|
||||
let meta_tags = meta.map(|mc| mc.dehydrate()).unwrap_or_default();
|
||||
format!(r#"
|
||||
<!DOCTYPE html>
|
||||
<html{html_meta}>
|
||||
<head>
|
||||
<link rel="modulepreload" href="/{pkg_url}/{crate_name}.js">
|
||||
<link rel="preload" href="/{pkg_url}/{crate_name}_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<script type="module">import init, {{ hydrate }} from '/{pkg_url}/{crate_name}.js'; init('/{pkg_url}/{crate_name}_bg.wasm').then(hydrate);</script>
|
||||
{meta_tags}
|
||||
</head>
|
||||
<body{body_meta}>
|
||||
{html}
|
||||
</body>
|
||||
</html>"#)
|
||||
});
|
||||
runtime.dispose();
|
||||
Response::from_html(html)
|
||||
}
|
||||
|
||||
pub async fn render_preloaded_data_app_to_string<Data, Fut, IV>(
|
||||
req: Request,
|
||||
data_fn: impl Fn(&Request) -> Fut + Clone + 'static,
|
||||
app_fn: impl FnOnce(Scope, Data) -> IV + Clone + 'static
|
||||
) -> Result<Response>
|
||||
where
|
||||
Data: 'static,
|
||||
Fut: Future<Output = Result<DataResponse<Data>>>,
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let data = data_fn(&req).await;
|
||||
let data = match data {
|
||||
Err(e) => return Response::error(e.to_string(), 500),
|
||||
Ok(DataResponse::Response(r)) => return Ok(r),
|
||||
Ok(DataResponse::Data(d)) => d
|
||||
};
|
||||
|
||||
let runtime = create_runtime();
|
||||
let (html, _, _) = run_scope_undisposed(runtime, move |cx| {
|
||||
let integration = RouterIntegrationContext::new(ServerIntegration { path: format!("https://leptos.dev{}", req.path()) });
|
||||
provide_context(cx, integration);
|
||||
provide_context(cx, MetaContext::new());
|
||||
provide_context(cx, Arc::new(req.clone()));
|
||||
let html = app_fn(cx, data).into_view(cx).render_to_string(cx);
|
||||
|
||||
let meta = use_context::<MetaContext>(cx);
|
||||
let html_meta = meta.as_ref().and_then(|mc| mc.html.as_string()).unwrap_or_default();
|
||||
let body_meta = meta.as_ref().and_then(|mc| mc.body.as_string()).unwrap_or_default();
|
||||
let meta_tags = meta.map(|mc| mc.dehydrate()).unwrap_or_default();
|
||||
format!(r#"
|
||||
<!DOCTYPE html>
|
||||
<html{html_meta}>
|
||||
<head>
|
||||
{meta_tags}
|
||||
</head>
|
||||
<body{body_meta}>
|
||||
{html}
|
||||
</body>
|
||||
</html>"#)
|
||||
});
|
||||
runtime.dispose();
|
||||
Response::from_html(html)
|
||||
}
|
||||
|
||||
pub enum DataResponse<T> {
|
||||
Data(T),
|
||||
Response(worker::Response),
|
||||
}
|
||||
|
||||
/// This trait allows one to pass a list of routes and a render function to Cloudflare's router, letting us avoid
|
||||
/// having to use wildcards or manually define all routes in multiple places.
|
||||
pub trait LeptosRoutes<T> {
|
||||
fn leptos_routes(
|
||||
self,
|
||||
paths: Vec<String>,
|
||||
app_fn: fn(Request, worker::RouteContext<T>) -> Result<Response>
|
||||
) -> Self;
|
||||
|
||||
fn leptos_preloaded_data_routes<Data, Fut, IV>(
|
||||
self,
|
||||
paths: Vec<String>,
|
||||
data_fn: impl Fn(Request) -> Fut + Clone + 'static,
|
||||
app_fn: impl Fn(leptos::Scope, Data) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
where
|
||||
Data: 'static,
|
||||
Fut: Future<Output = Result<DataResponse<Data>>>,
|
||||
IV: IntoView + 'static;
|
||||
}
|
||||
|
||||
/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
|
||||
/// to those paths to Leptos's renderer.
|
||||
impl<T> LeptosRoutes<T> for worker::Router<'_, T>
|
||||
where T: 'static
|
||||
{
|
||||
fn leptos_routes(
|
||||
self,
|
||||
paths: Vec<String>,
|
||||
app_fn: fn(Request, worker::RouteContext<T>) -> Result<Response>
|
||||
) -> Self
|
||||
{
|
||||
let mut router = self;
|
||||
for path in paths.iter() {
|
||||
router = router.get(path, app_fn);
|
||||
}
|
||||
router
|
||||
}
|
||||
|
||||
fn leptos_preloaded_data_routes<Data, Fut, IV>(
|
||||
self,
|
||||
paths: Vec<String>,
|
||||
data_fn: impl Fn(Request) -> Fut + Clone + 'static,
|
||||
app_fn: impl Fn(leptos::Scope, Data) -> IV + Clone + Send + 'static,
|
||||
) -> Self
|
||||
where
|
||||
Data: 'static,
|
||||
Fut: Future<Output = Result<DataResponse<Data>>>,
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let mut router = self;
|
||||
|
||||
for path in paths.iter() {
|
||||
router = router.get(
|
||||
path,
|
||||
|req, _| todo!() //render_preloaded_data_app_to_string(req, data_fn.clone(), app_fn.clone()),
|
||||
);
|
||||
}
|
||||
router
|
||||
}
|
||||
}
|
||||
|
||||
// This app
|
||||
|
||||
use leptos::{component, Scope, IntoView, create_signal, view, render_to_string, provide_context, LeptosOptions, use_context, create_runtime, run_scope, run_scope_undisposed, get_configuration, render_to_stream_with_prefix_undisposed_with_context};
|
||||
use leptos_router::*;
|
||||
use leptos_meta::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
|
||||
view! { cx,
|
||||
<Router>
|
||||
<Meta name="color-scheme" content="dark"/>
|
||||
<Title text="Hello from Leptos Cloudflare"/>
|
||||
<nav>
|
||||
<a href="/">"Home"</a>
|
||||
<a href="/about">"About"</a>
|
||||
</nav>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="/" view=|cx| view! { cx, <HomePage/> }/>
|
||||
<Route path="/about" view=|cx| view! { cx, <About/> }/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn HomePage(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
view! { cx,
|
||||
<h1>"Hello, Leptos Cloudflare!"</h1>
|
||||
<button on:click=move |_| set_count.update(|n| *n += 1)>
|
||||
"Click me: " {count}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn About(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<h1>"About"</h1>
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
// https://github.com/rustwasm/console_error_panic_hook#readme
|
||||
if #[cfg(feature = "console_error_panic_hook")] {
|
||||
extern crate console_error_panic_hook;
|
||||
pub use self::console_error_panic_hook::set_once as set_panic_hook;
|
||||
} else {
|
||||
#[inline]
|
||||
pub fn set_panic_hook() {}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
name = "" # todo
|
||||
main = "build/worker/shim.mjs"
|
||||
compatibility_date = "2022-01-20"
|
||||
|
||||
[vars]
|
||||
WORKERS_RS_VERSION = "0.0.11"
|
||||
|
||||
[build]
|
||||
command = "cargo install -q worker-build --version 0.0.7 && worker-build --release"
|
||||
@@ -17,7 +17,6 @@ leptos_server = { workspace = true }
|
||||
leptos_config = { workspace = true }
|
||||
tracing = "0.1"
|
||||
typed-builder = "0.12"
|
||||
once_cell = "1.17.0"
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = ".", default-features = false }
|
||||
|
||||
@@ -35,7 +35,7 @@ pub fn ErrorBoundary<F, IV>(
|
||||
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());
|
||||
@@ -47,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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::Children;
|
||||
use leptos::component;
|
||||
use leptos_dom::IntoView;
|
||||
use leptos_reactive::Scope;
|
||||
use once_cell::sync::Lazy;
|
||||
use leptos_dom::{Fragment, IntoView};
|
||||
use leptos_reactive::{create_memo, Scope};
|
||||
|
||||
/// 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
|
||||
@@ -30,7 +28,7 @@ pub fn Show<F, W, IV>(
|
||||
/// The scope the component is running in
|
||||
cx: Scope,
|
||||
/// The components Show wraps
|
||||
children: Children,
|
||||
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
|
||||
@@ -41,12 +39,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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,50 +87,35 @@ 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!(
|
||||
"{} is not a supported environment. Use either `dev` or `production`.",
|
||||
input
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
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 +124,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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,9 +154,9 @@ where
|
||||
instrument(level = "trace", name = "<DynChild />", skip_all)
|
||||
)]
|
||||
fn into_view(self, cx: Scope) -> View {
|
||||
let Self { child_fn } = self;
|
||||
let Self { id, child_fn } = self;
|
||||
|
||||
let component = DynChildRepr::new();
|
||||
let component = DynChildRepr::new_with_id(id);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let closing = component.closing.node.clone();
|
||||
|
||||
@@ -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 {
|
||||
@@ -34,7 +34,6 @@ where
|
||||
on_cleanup(cx, move || {
|
||||
queue_microtask(move || {
|
||||
errors.update(|errors: &mut Errors| {
|
||||
crate::log!("removing error at {id}");
|
||||
errors.remove::<E>(&id);
|
||||
});
|
||||
});
|
||||
@@ -59,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);
|
||||
}
|
||||
|
||||
@@ -13,20 +13,17 @@ 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]
|
||||
|
||||
@@ -167,7 +167,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 +287,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,11 +402,9 @@ 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;
|
||||
@@ -613,9 +611,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, .. },
|
||||
|
||||
@@ -230,6 +230,22 @@ mod server;
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// 10. You can set any HTML element’s `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/>
|
||||
/// }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// Here’s a simple example that shows off several of these features, put together
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
|
||||
@@ -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();
|
||||
@@ -392,30 +408,8 @@ fn attribute_to_tokens_ssr(
|
||||
let name = node.key.to_string();
|
||||
if name == "ref" || name == "_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,10 +421,10 @@ 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('"');
|
||||
@@ -626,7 +620,7 @@ fn node_to_tokens(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
&fragment.children,
|
||||
false,
|
||||
true,
|
||||
parent_type,
|
||||
global_class,
|
||||
),
|
||||
@@ -708,7 +702,7 @@ fn element_to_tokens(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
&fragment.children,
|
||||
false,
|
||||
true,
|
||||
parent_type,
|
||||
global_class,
|
||||
),
|
||||
@@ -931,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;
|
||||
|
||||
@@ -950,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
|
||||
@@ -963,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 {
|
||||
@@ -988,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -13,12 +13,10 @@ 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"
|
||||
@@ -31,26 +29,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]
|
||||
|
||||
@@ -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>);
|
||||
@@ -27,10 +28,10 @@ pub fn Outlet(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
is_showing.set(Some((child.id(), child.cx())));
|
||||
provide_context(child.cx(), child.clone());
|
||||
set_outlet.set(Some(child.outlet().into_view(cx)))
|
||||
set_outlet.set(Some(child.outlet(cx).into_view(cx)))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
move || outlet.get()
|
||||
leptos::DynChild::new_with_id(id, move || outlet.get())
|
||||
}
|
||||
|
||||
@@ -104,7 +104,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 +155,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 +171,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 +184,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 {
|
||||
|
||||
@@ -29,7 +29,6 @@ pub fn Routes(
|
||||
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)]
|
||||
|
||||
@@ -202,6 +202,7 @@ mod history;
|
||||
mod hooks;
|
||||
#[doc(hidden)]
|
||||
pub mod matching;
|
||||
pub use matching::RouteDefinition;
|
||||
|
||||
pub use components::*;
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user