mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
feat: experimental islands (#1660)
This commit is contained in:
parent
b9a1fb7743
commit
238d61ce1e
59 changed files with 2602 additions and 510 deletions
|
@ -5,9 +5,13 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
l021 = { package = "leptos", version = "0.2.1" }
|
||||
leptos = { path = "../leptos", features = ["ssr"] }
|
||||
leptos = { path = "../leptos", features = [
|
||||
"ssr",
|
||||
"nightly",
|
||||
"experimental-islands",
|
||||
] }
|
||||
sycamore = { version = "0.8", features = ["ssr"] }
|
||||
yew = { git = "https://github.com/yewstack/yew", features = ["ssr"] }
|
||||
yew = { version = "0.20", features = ["ssr"] }
|
||||
tokio-test = "0.4"
|
||||
miniserde = "0.1"
|
||||
gloo = "0.8"
|
||||
|
|
|
@ -16,7 +16,10 @@ actix-web = { version = "4", optional = true, features = ["macros"] }
|
|||
console_log = "1"
|
||||
console_error_panic_hook = "0.1"
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../../leptos", features = ["nightly"] }
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"nightly",
|
||||
"experimental-islands",
|
||||
] }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
|
@ -41,6 +44,12 @@ ssr = [
|
|||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
[profile.wasm-release]
|
||||
inherits = "release"
|
||||
opt-level = 'z'
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["actix-files", "actix-web", "leptos_actix"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
@ -88,3 +97,5 @@ lib-features = ["hydrate"]
|
|||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
|
||||
lib-profile-release = "wasm-release"
|
||||
|
|
3
examples/hackernews_islands_axum/.cargo/config.wasm.toml
Normal file
3
examples/hackernews_islands_axum/.cargo/config.wasm.toml
Normal file
|
@ -0,0 +1,3 @@
|
|||
[unstable]
|
||||
build-std = ["std", "panic_abort", "core", "alloc"]
|
||||
build-std-features = ["panic_immediate_abort"]
|
108
examples/hackernews_islands_axum/Cargo.toml
Normal file
108
examples/hackernews_islands_axum/Cargo.toml
Normal file
|
@ -0,0 +1,108 @@
|
|||
[package]
|
||||
name = "hackernews"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
console_log = "1.0.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"nightly",
|
||||
"experimental-islands",
|
||||
] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true, features = [
|
||||
"experimental-islands",
|
||||
] }
|
||||
leptos_meta = { path = "../../meta", features = ["nightly"] }
|
||||
leptos_router = { path = "../../router", features = ["nightly"] }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
tracing = "0.1"
|
||||
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
axum = { version = "0.6.1", optional = true, features = ["http2"] }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.4", features = [
|
||||
"fs",
|
||||
"compression-br",
|
||||
], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8", optional = true }
|
||||
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
|
||||
wasm-bindgen = "0.2"
|
||||
wee_alloc = "0.4.5"
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
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:http",
|
||||
"leptos/ssr",
|
||||
"leptos_axum",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "http", "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 = "hackernews"
|
||||
# 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"
|
||||
site-addr = "0.0.0.0:8080"
|
||||
# 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
|
25
examples/hackernews_islands_axum/Dockerfile
Normal file
25
examples/hackernews_islands_axum/Dockerfile
Normal file
|
@ -0,0 +1,25 @@
|
|||
FROM rustlang/rust:nightly-bullseye as builder
|
||||
RUN wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz
|
||||
#RUN tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz
|
||||
#RUN cp cargo-binstall /usr/local/cargo/bin
|
||||
#RUN cargo binstall cargo-leptos -y
|
||||
#RUN rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu
|
||||
#RUN rustup target add wasm32-unknown-unknown
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN cargo build --release --no-default-features --features=ssr
|
||||
RUN ls -l /app/target
|
||||
|
||||
FROM rustlang/rust:nightly-bullseye as runner
|
||||
COPY --from=builder /app/target/release/hackernews /app/
|
||||
COPY --from=builder /app/pkg /app
|
||||
COPY --from=builder /app/Cargo.toml /app/
|
||||
WORKDIR /app
|
||||
ENV RUST_LOG="info"
|
||||
ENV LEPTOS_OUTPUT_NAME="hackernews"
|
||||
ENV APP_ENVIRONMENT="production"
|
||||
ENV LEPTOS_SITE_ADDR="0.0.0.0:8080"
|
||||
ENV LEPTOS_SITE_ROOT="site"
|
||||
EXPOSE 8080
|
||||
CMD ["/app/hackernews"]
|
21
examples/hackernews_islands_axum/LICENSE
Normal file
21
examples/hackernews_islands_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.
|
1
examples/hackernews_islands_axum/Makefile.toml
Normal file
1
examples/hackernews_islands_axum/Makefile.toml
Normal file
|
@ -0,0 +1 @@
|
|||
extend = [{ path = "../cargo-make/main.toml" }]
|
43
examples/hackernews_islands_axum/README.md
Normal file
43
examples/hackernews_islands_axum/README.md
Normal file
|
@ -0,0 +1,43 @@
|
|||
# Leptos Hacker News Example with Axum
|
||||
|
||||
This example creates a basic clone of the Hacker News site. It showcases Leptos' ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository. This repo differs from the main Hacker News example by using Axum as it's 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`.
|
||||
|
||||
## 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
|
||||
```
|
8
examples/hackernews_islands_axum/build-front.sh
Executable file
8
examples/hackernews_islands_axum/build-front.sh
Executable file
|
@ -0,0 +1,8 @@
|
|||
wasm-pack build --target=web --features=hydrate --release
|
||||
cd pkg
|
||||
rm *.br
|
||||
cp hackernews.js hackernews.unmin.js
|
||||
cat hackernews.unmin.js | esbuild > hackernews.js
|
||||
brotli hackernews.js
|
||||
brotli hackernews_bg.wasm
|
||||
brotli style.css
|
8
examples/hackernews_islands_axum/index.html
Normal file
8
examples/hackernews_islands_axum/index.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="css" href="/style.css"/>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
BIN
examples/hackernews_islands_axum/public/favicon.ico
Normal file
BIN
examples/hackernews_islands_axum/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
62
examples/hackernews_islands_axum/src/api.rs
Normal file
62
examples/hackernews_islands_axum/src/api.rs
Normal file
|
@ -0,0 +1,62 @@
|
|||
#![allow(unused)]
|
||||
|
||||
use leptos::Serializable;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn story(path: &str) -> String {
|
||||
format!("https://node-hnapi.herokuapp.com/{path}")
|
||||
}
|
||||
|
||||
pub fn user(path: &str) -> String {
|
||||
format!("https://hacker-news.firebaseio.com/v0/user/{path}.json")
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref CLIENT: reqwest::Client = reqwest::Client::new();
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn fetch_api<T>(path: &str) -> Option<T>
|
||||
where
|
||||
T: Serializable,
|
||||
{
|
||||
let json = CLIENT.get(path).send().await.ok()?.text().await.ok()?;
|
||||
T::de(&json).map_err(|e| log::error!("{e}")).ok()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
|
||||
pub struct Story {
|
||||
pub id: usize,
|
||||
pub title: String,
|
||||
pub points: Option<i32>,
|
||||
pub user: Option<String>,
|
||||
pub time: usize,
|
||||
pub time_ago: String,
|
||||
#[serde(alias = "type")]
|
||||
pub story_type: String,
|
||||
pub url: String,
|
||||
#[serde(default)]
|
||||
pub domain: String,
|
||||
#[serde(default)]
|
||||
pub comments: Option<Vec<Comment>>,
|
||||
pub comments_count: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
|
||||
pub struct Comment {
|
||||
pub id: usize,
|
||||
pub level: usize,
|
||||
pub user: Option<String>,
|
||||
pub time: usize,
|
||||
pub time_ago: String,
|
||||
pub content: Option<String>,
|
||||
pub comments: Vec<Comment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
|
||||
pub struct User {
|
||||
pub created: usize,
|
||||
pub id: String,
|
||||
pub karma: i32,
|
||||
pub about: Option<String>,
|
||||
}
|
28
examples/hackernews_islands_axum/src/error_template.rs
Normal file
28
examples/hackernews_islands_axum/src/error_template.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use leptos::{view, Errors, For, IntoView, RwSignal, View};
|
||||
|
||||
// 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(errors: Option<RwSignal<Errors>>) -> View {
|
||||
let Some(errors) = errors else {
|
||||
panic!("No Errors found and we expected errors!");
|
||||
};
|
||||
|
||||
view! {
|
||||
<h1>"Errors"</h1>
|
||||
<For
|
||||
// a function that returns the items we're iterating over; a signal is fine
|
||||
each=errors
|
||||
// a unique key for each item as a reference
|
||||
key=|(key, _)| key.clone()
|
||||
// renders each item to a view
|
||||
view= move | (_, error)| {
|
||||
let error_string = error.to_string();
|
||||
view! {
|
||||
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
.into_view()
|
||||
}
|
44
examples/hackernews_islands_axum/src/fallback.rs
Normal file
44
examples/hackernews_islands_axum/src/fallback.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::State,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use axum::response::Response as AxumResponse;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use leptos::{LeptosOptions};
|
||||
use crate::error_template::error_template;
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, State(options): State<LeptosOptions>, req: Request<Body>) -> AxumResponse {
|
||||
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 handler = leptos_axum::render_app_to_stream(options.to_owned(), || error_template( None));
|
||||
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),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
63
examples/hackernews_islands_axum/src/handlers.rs
Normal file
63
examples/hackernews_islands_axum/src/handlers.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let res = get_static_file(uri.clone(), "/pkg").await?;
|
||||
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
// try with `.html`
|
||||
// TODO: handle if the Uri has query parameters
|
||||
match format!("{}.html", uri).parse() {
|
||||
Ok(uri_html) => get_static_file(uri_html, "/pkg").await,
|
||||
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())),
|
||||
}
|
||||
} else {
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_static_file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let res = get_static_file(uri.clone(), "/static").await?;
|
||||
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
|
||||
} else {
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, base: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(&uri).body(Body::empty()).unwrap();
|
||||
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// When run normally, the root should be the crate root
|
||||
if base == "/static" {
|
||||
match ServeDir::new("./static").oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
))
|
||||
}
|
||||
} else if base == "/pkg" {
|
||||
match ServeDir::new("./pkg").oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
}
|
||||
} else{
|
||||
Err((StatusCode::NOT_FOUND, "Not Found".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
52
examples/hackernews_islands_axum/src/lib.rs
Normal file
52
examples/hackernews_islands_axum/src/lib.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
#![feature(lazy_cell)]
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
mod api;
|
||||
pub mod error_template;
|
||||
pub mod fallback;
|
||||
mod routes;
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
|
||||
view! {
|
||||
<Stylesheet id="leptos" href="/pkg/hackernews.css"/>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
|
||||
<Router>
|
||||
<Nav />
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="users/:id" view=User ssr=SsrMode::InOrder/>
|
||||
<Route path="stories/:id" view=Story ssr=SsrMode::InOrder/>
|
||||
<Route path=":stories?" view=Stories ssr=SsrMode::InOrder/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
extern crate wee_alloc;
|
||||
|
||||
// Use `wee_alloc` as the global allocator.
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
#[cfg(debug_assertions)]
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::leptos_dom::HydrationCtx::stop_hydrating();
|
||||
}
|
||||
}
|
||||
}
|
47
examples/hackernews_islands_axum/src/main.rs
Normal file
47
examples/hackernews_islands_axum/src/main.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
#[cfg(feature = "ssr")]
|
||||
mod ssr_imports {
|
||||
pub use axum::{routing::get, Router};
|
||||
pub use hackernews::fallback::file_and_error_handler;
|
||||
pub use leptos::*;
|
||||
pub use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
pub use tower_http::{compression::CompressionLayer, services::ServeFile};
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use hackernews::*;
|
||||
use ssr_imports::*;
|
||||
|
||||
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(|| view! { <App/> }).await;
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/favicon.ico", get(file_and_error_handler))
|
||||
.leptos_routes(&leptos_options, routes, || view! { <App/> })
|
||||
.fallback(file_and_error_handler)
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
logging::log!("listening on {}", addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// client-only stuff for Trunk
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
use hackernews::*;
|
||||
use leptos::*;
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|| {
|
||||
view! { <App/> }
|
||||
});
|
||||
}
|
4
examples/hackernews_islands_axum/src/routes.rs
Normal file
4
examples/hackernews_islands_axum/src/routes.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub mod nav;
|
||||
pub mod stories;
|
||||
pub mod story;
|
||||
pub mod users;
|
30
examples/hackernews_islands_axum/src/routes/nav.rs
Normal file
30
examples/hackernews_islands_axum/src/routes/nav.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
use leptos::{component, view, IntoView};
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn Nav() -> impl IntoView {
|
||||
view! {
|
||||
<header class="header">
|
||||
<nav class="inner">
|
||||
<A href="/home">
|
||||
<strong>"HN"</strong>
|
||||
</A>
|
||||
<A href="/new">
|
||||
<strong>"New"</strong>
|
||||
</A>
|
||||
<A href="/show">
|
||||
<strong>"Show"</strong>
|
||||
</A>
|
||||
<A href="/ask">
|
||||
<strong>"Ask"</strong>
|
||||
</A>
|
||||
<A href="/job">
|
||||
<strong>"Jobs"</strong>
|
||||
</A>
|
||||
<a class="github" href="http://github.com/gbj/leptos" target="_blank" rel="noreferrer">
|
||||
"Built with Leptos"
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
}
|
||||
}
|
164
examples/hackernews_islands_axum/src/routes/stories.rs
Normal file
164
examples/hackernews_islands_axum/src/routes/stories.rs
Normal file
|
@ -0,0 +1,164 @@
|
|||
use crate::api;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
fn category(from: &str) -> String {
|
||||
match from {
|
||||
"new" => "newest",
|
||||
"show" => "show",
|
||||
"ask" => "ask",
|
||||
"job" => "jobs",
|
||||
_ => "news",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[server(FetchStories, "/api")]
|
||||
pub async fn fetch_stories(
|
||||
story_type: String,
|
||||
page: usize,
|
||||
) -> Result<Vec<api::Story>, ServerFnError> {
|
||||
let path = format!("{}?page={}", category(&story_type), page);
|
||||
Ok(api::fetch_api::<Vec<api::Story>>(&api::story(&path))
|
||||
.await
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Stories() -> impl IntoView {
|
||||
let query = use_query_map();
|
||||
let params = use_params_map();
|
||||
let page = move || {
|
||||
query
|
||||
.with(|q| q.get("page").and_then(|page| page.parse::<usize>().ok()))
|
||||
.unwrap_or(1)
|
||||
};
|
||||
let story_type = move || {
|
||||
params
|
||||
.with(|p| p.get("stories").cloned())
|
||||
.unwrap_or_else(|| "top".to_string())
|
||||
};
|
||||
let stories = create_resource(
|
||||
move || (page(), story_type()),
|
||||
move |(page, story_type)| fetch_stories(category(&story_type), page),
|
||||
);
|
||||
let (pending, set_pending) = create_signal(false);
|
||||
|
||||
let hide_more_link = move || {
|
||||
pending()
|
||||
|| stories
|
||||
.map(|stories| {
|
||||
stories.as_ref().map(|s| s.len() < 28).unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
view! {
|
||||
|
||||
<div class="news-view">
|
||||
<div class="news-list-nav">
|
||||
<span>
|
||||
{move || if page() > 1 {
|
||||
view! {
|
||||
<a class="page-link"
|
||||
href=move || format!("/{}?page={}", story_type(), page() - 1)
|
||||
attr:aria_label="Previous Page"
|
||||
>
|
||||
"< prev"
|
||||
</a>
|
||||
}.into_any()
|
||||
} else {
|
||||
view! {
|
||||
<span class="page-link disabled" aria-hidden="true">
|
||||
"< prev"
|
||||
</span>
|
||||
}.into_any()
|
||||
}}
|
||||
</span>
|
||||
<span>"page " {page}</span>
|
||||
<span class="page-link"
|
||||
class:disabled=hide_more_link
|
||||
aria-hidden=hide_more_link
|
||||
>
|
||||
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
aria-label="Next Page"
|
||||
>
|
||||
"more >"
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<main class="news-list">
|
||||
<div>
|
||||
<Transition
|
||||
fallback=|| ()
|
||||
set_pending=set_pending.into()
|
||||
>
|
||||
{move || stories.get().map(|story| story.map(|stories| view! {
|
||||
<ul>
|
||||
<For
|
||||
each=move || stories.clone()
|
||||
key=|story| story.id
|
||||
view=move |story: api::Story| {
|
||||
view! {
|
||||
<Story story/>
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ul>
|
||||
}))}
|
||||
</Transition>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Story(story: api::Story) -> impl IntoView {
|
||||
view! {
|
||||
<li class="news-item">
|
||||
<span class="score">{story.points}</span>
|
||||
<span class="title">
|
||||
{if !story.url.starts_with("item?id=") {
|
||||
view! {
|
||||
<span>
|
||||
<a href=story.url target="_blank" rel="noreferrer">
|
||||
{story.title.clone()}
|
||||
</a>
|
||||
<span class="host">"("{story.domain}")"</span>
|
||||
</span>
|
||||
}.into_view()
|
||||
} else {
|
||||
let title = story.title.clone();
|
||||
view! { <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }.into_view()
|
||||
}}
|
||||
</span>
|
||||
<br />
|
||||
<span class="meta">
|
||||
{if story.story_type != "job" {
|
||||
view! {
|
||||
<span>
|
||||
{"by "}
|
||||
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
|
||||
{format!(" {} | ", story.time_ago)}
|
||||
<A href=format!("/stories/{}", story.id)>
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"discuss".into()
|
||||
}}
|
||||
</A>
|
||||
</span>
|
||||
}.into_view()
|
||||
} else {
|
||||
let title = story.title.clone();
|
||||
view! { <A href=format!("/item/{}", story.id)>{title.clone()}</A> }.into_view()
|
||||
}}
|
||||
</span>
|
||||
{(story.story_type != "link").then(|| view! {
|
||||
" "
|
||||
<span class="label">{story.story_type}</span>
|
||||
})}
|
||||
</li>
|
||||
}
|
||||
}
|
137
examples/hackernews_islands_axum/src/routes/story.rs
Normal file
137
examples/hackernews_islands_axum/src/routes/story.rs
Normal file
|
@ -0,0 +1,137 @@
|
|||
use crate::api;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use std::cell::RefCell;
|
||||
|
||||
#[server(FetchStory, "/api")]
|
||||
pub async fn fetch_story(
|
||||
id: String,
|
||||
) -> Result<RefCell<Option<api::Story>>, ServerFnError> {
|
||||
Ok(RefCell::new(
|
||||
api::fetch_api::<api::Story>(&api::story(&format!("item/{id}"))).await,
|
||||
))
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Story() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let story = create_resource(
|
||||
move || params().get("id").cloned().unwrap_or_default(),
|
||||
move |id| async move {
|
||||
if id.is_empty() {
|
||||
Ok(RefCell::new(None))
|
||||
} else {
|
||||
fetch_story(id).await
|
||||
}
|
||||
},
|
||||
);
|
||||
let meta_description = move || {
|
||||
story
|
||||
.map(|story| {
|
||||
story
|
||||
.as_ref()
|
||||
.map(|story| {
|
||||
story.borrow().as_ref().map(|story| story.title.clone())
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.flatten()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| "Loading story...".to_string())
|
||||
};
|
||||
|
||||
let story_view = move || {
|
||||
story.map(|story| {
|
||||
story.as_ref().ok().and_then(|story| {
|
||||
let story: Option<api::Story> = story.borrow_mut().take();
|
||||
story.map(|story| {
|
||||
view! {
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { <p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"No comments yet.".into()
|
||||
}}
|
||||
</p>
|
||||
<ul class="comment-children">
|
||||
{story.comments.unwrap_or_default().into_iter()
|
||||
.map(|comment: api::Comment| view! { <Comment comment /> })
|
||||
.collect_view()}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}})})})
|
||||
};
|
||||
|
||||
view! {
|
||||
<Meta name="description" content=meta_description/>
|
||||
<Suspense fallback=|| ()>
|
||||
{story_view}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
#[island]
|
||||
pub fn Toggle(children: Children) -> impl IntoView {
|
||||
let (open, set_open) = create_signal(true);
|
||||
view! {
|
||||
<div class="toggle" class:open=open>
|
||||
<a on:click=move |_| set_open.update(|n| *n = !*n)>
|
||||
{move || if open() {
|
||||
"[-]"
|
||||
} else {
|
||||
"[+] comments collapsed"
|
||||
}}
|
||||
</a>
|
||||
</div>
|
||||
<ul
|
||||
class="comment-children"
|
||||
style:display=move || if open() {
|
||||
"block"
|
||||
} else {
|
||||
"none"
|
||||
}
|
||||
>
|
||||
{children()}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Comment(comment: api::Comment) -> impl IntoView {
|
||||
view! {
|
||||
<li class="comment">
|
||||
<div class="by">
|
||||
<A href=format!("/users/{}", comment.user.clone().unwrap_or_default())>{comment.user.clone()}</A>
|
||||
{format!(" {}", comment.time_ago)}
|
||||
</div>
|
||||
<div class="text" inner_html=comment.content></div>
|
||||
{(!comment.comments.is_empty()).then(|| {
|
||||
view! {
|
||||
<Toggle>
|
||||
{comment.comments.into_iter()
|
||||
.map(|comment: api::Comment| view! { <Comment comment /> })
|
||||
.collect_view()}
|
||||
</Toggle>
|
||||
}
|
||||
})}
|
||||
</li>
|
||||
}
|
||||
}
|
54
examples/hackernews_islands_axum/src/routes/users.rs
Normal file
54
examples/hackernews_islands_axum/src/routes/users.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
#[allow(unused)] // User is unused in WASM build
|
||||
use crate::api::{self, User};
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[server(FetchUser, "/api")]
|
||||
pub async fn fetch_user(
|
||||
id: String,
|
||||
) -> Result<Option<api::User>, ServerFnError> {
|
||||
Ok(api::fetch_api::<User>(&api::user(&id)).await)
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn User() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let user = create_resource(
|
||||
move || params().get("id").cloned().unwrap_or_default(),
|
||||
move |id| async move {
|
||||
if id.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
fetch_user(id).await
|
||||
}
|
||||
},
|
||||
);
|
||||
view! {
|
||||
<div class="user-view">
|
||||
<Suspense fallback=|| ()>
|
||||
{move || user.get().map(|user| user.map(|user| match user {
|
||||
None => view! { <h1>"User not found."</h1> }.into_view(),
|
||||
Some(user) => view! {
|
||||
<div>
|
||||
<h1>"User: " {&user.id}</h1>
|
||||
<ul class="meta">
|
||||
<li>
|
||||
<span class="label">"Created: "</span> {user.created}
|
||||
</li>
|
||||
<li>
|
||||
<span class="label">"Karma: "</span> {user.karma}
|
||||
</li>
|
||||
<li inner_html={user.about} class="about"></li>
|
||||
</ul>
|
||||
<p class="links">
|
||||
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>
|
||||
" | "
|
||||
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
|
||||
</p>
|
||||
</div>
|
||||
}.into_view()
|
||||
}))}
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
}
|
326
examples/hackernews_islands_axum/style.css
Normal file
326
examples/hackernews_islands_axum/style.css
Normal file
|
@ -0,0 +1,326 @@
|
|||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
font-size: 15px;
|
||||
background-color: #f2f3f5;
|
||||
margin: 0;
|
||||
padding-top: 55px;
|
||||
color: #34495e;
|
||||
overflow-y: scroll
|
||||
}
|
||||
|
||||
a {
|
||||
color: #34495e;
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #335d92;
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
height: 55px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0
|
||||
}
|
||||
|
||||
.header .inner {
|
||||
max-width: 800px;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
padding: 15px 5px
|
||||
}
|
||||
|
||||
.header a {
|
||||
color: rgba(255, 255, 255, .8);
|
||||
line-height: 24px;
|
||||
transition: color .15s ease;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
font-weight: 300;
|
||||
letter-spacing: .075em;
|
||||
margin-right: 1.8em
|
||||
}
|
||||
|
||||
.header a:hover {
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.header a.active {
|
||||
color: #fff;
|
||||
font-weight: 400
|
||||
}
|
||||
|
||||
.header a:nth-child(6) {
|
||||
margin-right: 0
|
||||
}
|
||||
|
||||
.header .github {
|
||||
color: #fff;
|
||||
font-size: .9em;
|
||||
margin: 0;
|
||||
float: right
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 24px;
|
||||
margin-right: 10px;
|
||||
display: inline-block;
|
||||
vertical-align: middle
|
||||
}
|
||||
|
||||
.view {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
position: relative
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-exit-active {
|
||||
transition: all .2s ease
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-exit-active {
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
@media (max-width:860px) {
|
||||
.header .inner {
|
||||
padding: 15px 30px
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width:600px) {
|
||||
.header .inner {
|
||||
padding: 15px
|
||||
}
|
||||
|
||||
.header a {
|
||||
margin-right: 1em
|
||||
}
|
||||
|
||||
.header .github {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
|
||||
.news-view {
|
||||
padding-top: 45px
|
||||
}
|
||||
|
||||
.news-list,
|
||||
.news-list-nav {
|
||||
background-color: #fff;
|
||||
border-radius: 2px
|
||||
}
|
||||
|
||||
.news-list-nav {
|
||||
padding: 15px 30px;
|
||||
position: fixed;
|
||||
text-align: center;
|
||||
top: 55px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 998;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .1)
|
||||
}
|
||||
|
||||
.news-list-nav .page-link {
|
||||
margin: 0 1em
|
||||
}
|
||||
|
||||
.news-list-nav .disabled {
|
||||
color: #aaa
|
||||
}
|
||||
|
||||
.news-list {
|
||||
position: absolute;
|
||||
margin: 30px 0;
|
||||
width: 100%;
|
||||
transition: all .5s cubic-bezier(.55, 0, .1, 1)
|
||||
}
|
||||
|
||||
.news-list ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0
|
||||
}
|
||||
|
||||
@media (max-width:600px) {
|
||||
.news-list {
|
||||
margin: 10px 0
|
||||
}
|
||||
}
|
||||
|
||||
.news-item {
|
||||
background-color: #fff;
|
||||
padding: 20px 30px 20px 80px;
|
||||
border-bottom: 1px solid #eee;
|
||||
position: relative;
|
||||
line-height: 20px
|
||||
}
|
||||
|
||||
.news-item .score {
|
||||
color: #335d92;
|
||||
font-size: 1.1em;
|
||||
font-weight: 700;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
margin-top: -10px
|
||||
}
|
||||
|
||||
.news-item .host,
|
||||
.news-item .meta {
|
||||
font-size: .85em;
|
||||
color: #626262
|
||||
}
|
||||
|
||||
.news-item .host a,
|
||||
.news-item .meta a {
|
||||
color: #626262;
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
.news-item .host a:hover,
|
||||
.news-item .meta a:hover {
|
||||
color: #335d92
|
||||
}
|
||||
|
||||
.item-view-header {
|
||||
background-color: #fff;
|
||||
padding: 1.8em 2em 1em;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .1)
|
||||
}
|
||||
|
||||
.item-view-header h1 {
|
||||
display: inline;
|
||||
font-size: 1.5em;
|
||||
margin: 0;
|
||||
margin-right: .5em
|
||||
}
|
||||
|
||||
.item-view-header .host,
|
||||
.item-view-header .meta,
|
||||
.item-view-header .meta a {
|
||||
color: #626262
|
||||
}
|
||||
|
||||
.item-view-header .meta a {
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
.item-view-comments {
|
||||
background-color: #fff;
|
||||
margin-top: 10px;
|
||||
padding: 0 2em .5em
|
||||
}
|
||||
|
||||
.item-view-comments-header {
|
||||
margin: 0;
|
||||
font-size: 1.1em;
|
||||
padding: 1em 0;
|
||||
position: relative
|
||||
}
|
||||
|
||||
.item-view-comments-header .spinner {
|
||||
display: inline-block;
|
||||
margin: -15px 0
|
||||
}
|
||||
|
||||
.comment-children {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0
|
||||
}
|
||||
|
||||
@media (max-width:600px) {
|
||||
.item-view-header h1 {
|
||||
font-size: 1.25em
|
||||
}
|
||||
}
|
||||
|
||||
.comment-children .comment-children {
|
||||
margin-left: 1.5em
|
||||
}
|
||||
|
||||
.comment {
|
||||
border-top: 1px solid #eee;
|
||||
position: relative
|
||||
}
|
||||
|
||||
.comment .by,
|
||||
.comment .text,
|
||||
.comment .toggle {
|
||||
font-size: .9em;
|
||||
margin: 1em 0
|
||||
}
|
||||
|
||||
.comment .by {
|
||||
color: #626262
|
||||
}
|
||||
|
||||
.comment .by a {
|
||||
color: #626262;
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
.comment .text {
|
||||
overflow-wrap: break-word
|
||||
}
|
||||
|
||||
.comment .text a:hover {
|
||||
color: #335d92
|
||||
}
|
||||
|
||||
.comment .text pre {
|
||||
white-space: pre-wrap
|
||||
}
|
||||
|
||||
.comment .toggle {
|
||||
background-color: #fffbf2;
|
||||
padding: .3em .5em;
|
||||
border-radius: 4px
|
||||
}
|
||||
|
||||
.comment .toggle a {
|
||||
color: #626262;
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.comment .toggle.open {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
margin-bottom: -.5em
|
||||
}
|
||||
|
||||
.user-view {
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
padding: 2em 3em
|
||||
}
|
||||
|
||||
.user-view h1 {
|
||||
margin: 0;
|
||||
font-size: 1.5em
|
||||
}
|
||||
|
||||
.user-view .meta {
|
||||
list-style-type: none;
|
||||
padding: 0
|
||||
}
|
||||
|
||||
.user-view .label {
|
||||
display: inline-block;
|
||||
min-width: 4em
|
||||
}
|
||||
|
||||
.user-view .about {
|
||||
margin: 1em 0
|
||||
}
|
||||
|
||||
.user-view .links a {
|
||||
text-decoration: underline
|
||||
}
|
|
@ -23,3 +23,4 @@ tokio = { version = "1", features = ["rt"] }
|
|||
|
||||
[features]
|
||||
nonce = ["leptos/nonce"]
|
||||
experimental-islands = ["leptos_integration_utils/experimental-islands"]
|
||||
|
|
|
@ -25,3 +25,4 @@ once_cell = "1.17"
|
|||
|
||||
[features]
|
||||
nonce = ["leptos/nonce"]
|
||||
experimental-islands = ["leptos_integration_utils/experimental-islands"]
|
||||
|
|
|
@ -13,4 +13,7 @@ leptos = { workspace = true, features = ["ssr"] }
|
|||
leptos_hot_reload = { workspace = true }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_config = { workspace = true }
|
||||
tracing="0.1.37"
|
||||
tracing = "0.1.37"
|
||||
|
||||
[features]
|
||||
experimental-islands = []
|
||||
|
|
|
@ -79,6 +79,27 @@ pub fn html_parts_separated(
|
|||
.as_ref()
|
||||
.map(|meta| meta.dehydrate())
|
||||
.unwrap_or_default();
|
||||
let import_callback = if cfg!(feature = "experimental-islands") {
|
||||
/* r#"() => {
|
||||
for (let e of document.querySelectorAll("leptos-island")) {
|
||||
let l = e.dataset.component;
|
||||
console.log("hydrating island");
|
||||
mod["_island_" + l];
|
||||
}
|
||||
mod.hydrate();
|
||||
}"# */
|
||||
r#"() => {
|
||||
for (let e of document.querySelectorAll("leptos-island")) {
|
||||
let l = e.dataset.component;
|
||||
mod["_island_" + l](e);
|
||||
}
|
||||
mod.hydrate();
|
||||
}
|
||||
"#
|
||||
//r#"()=>{for(let e of document.querySelectorAll("leptos-island")){let l=e.dataset.component;mod["_island_"+l](e)};mod.hydrate();}"#
|
||||
} else {
|
||||
"() => mod.hydrate()"
|
||||
};
|
||||
let head = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html{html_metadata}>
|
||||
|
@ -88,7 +109,21 @@ pub fn html_parts_separated(
|
|||
{head}
|
||||
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js"{nonce}>
|
||||
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin=""{nonce}>
|
||||
<script type="module"{nonce}>import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
|
||||
<script type="module"{nonce}>
|
||||
function idle(c) {{
|
||||
if ("requestIdleCallback" in window) {{
|
||||
window.requestIdleCallback(c);
|
||||
}} else {{
|
||||
c();
|
||||
}}
|
||||
}}
|
||||
idle(() => {{
|
||||
import('/{pkg_path}/{output_name}.js')
|
||||
.then(mod => {{
|
||||
mod.default('/{pkg_path}/{wasm_output_name}.wasm').then({import_callback});
|
||||
}})
|
||||
}});
|
||||
</script>
|
||||
{leptos_autoreload}
|
||||
"#
|
||||
);
|
||||
|
|
|
@ -22,3 +22,4 @@ parking_lot = "0.12.1"
|
|||
|
||||
[features]
|
||||
nonce = ["leptos/nonce"]
|
||||
experimental-islands = ["leptos_integration_utils/experimental-islands"]
|
||||
|
|
|
@ -10,6 +10,7 @@ readme = "../README.md"
|
|||
|
||||
[dependencies]
|
||||
cfg-if = "1"
|
||||
const_format = "0.2"
|
||||
leptos_dom = { workspace = true }
|
||||
leptos_macro = { workspace = true }
|
||||
leptos_reactive = { workspace = true }
|
||||
|
@ -18,24 +19,31 @@ leptos_config = { workspace = true }
|
|||
tracing = "0.1"
|
||||
typed-builder = "0.16"
|
||||
typed-builder-macro = "0.16"
|
||||
serde = { optional = true }
|
||||
serde_json = { optional = true }
|
||||
server_fn = { workspace = true }
|
||||
web-sys = { version = "0.3.63", optional = true }
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
xxhash-rust = "0.8"
|
||||
|
||||
[features]
|
||||
default = ["serde"]
|
||||
template_macro = ["leptos_dom/web", "web-sys", "wasm-bindgen"]
|
||||
template_macro = ["leptos_dom/web", "dep:web-sys", "dep:wasm-bindgen"]
|
||||
csr = [
|
||||
"leptos_dom/csr",
|
||||
"leptos_macro/csr",
|
||||
"leptos_reactive/csr",
|
||||
"leptos_server/csr",
|
||||
"dep:wasm-bindgen",
|
||||
"dep:web-sys",
|
||||
]
|
||||
hydrate = [
|
||||
"leptos_dom/hydrate",
|
||||
"leptos_macro/hydrate",
|
||||
"leptos_reactive/hydrate",
|
||||
"leptos_server/hydrate",
|
||||
"dep:wasm-bindgen",
|
||||
"dep:web-sys",
|
||||
]
|
||||
default-tls = ["leptos_server/default-tls", "server_fn/default-tls"]
|
||||
rustls = ["leptos_server/rustls", "server_fn/rustls"]
|
||||
|
@ -58,6 +66,13 @@ miniserde = ["leptos_reactive/miniserde"]
|
|||
rkyv = ["leptos_reactive/rkyv"]
|
||||
tracing = ["leptos_macro/tracing"]
|
||||
nonce = ["leptos_dom/nonce"]
|
||||
experimental-islands = [
|
||||
"leptos_dom/experimental-islands",
|
||||
"leptos_macro/experimental-islands",
|
||||
"leptos_reactive/experimental-islands",
|
||||
"dep:serde",
|
||||
"dep:serde_json",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = [
|
||||
|
|
|
@ -155,10 +155,17 @@ pub mod ssr {
|
|||
pub use leptos_dom::{ssr::*, ssr_in_order::*};
|
||||
}
|
||||
pub use leptos_dom::{
|
||||
self, create_node_ref, document, ev, helpers::*, html, math, mount_to,
|
||||
mount_to_body, nonce, svg, window, Attribute, Class, CollectView, Errors,
|
||||
Fragment, HtmlElement, IntoAttribute, IntoClass, IntoProperty, IntoStyle,
|
||||
IntoView, NodeRef, Property, View,
|
||||
self, create_node_ref, document, ev,
|
||||
helpers::{
|
||||
event_target, event_target_checked, event_target_value,
|
||||
request_animation_frame, request_animation_frame_with_handle,
|
||||
request_idle_callback, request_idle_callback_with_handle, set_interval,
|
||||
set_interval_with_handle, set_timeout, set_timeout_with_handle,
|
||||
window_event_listener, window_event_listener_untyped,
|
||||
},
|
||||
html, math, mount_to, mount_to_body, nonce, svg, window, Attribute, Class,
|
||||
CollectView, Errors, Fragment, HtmlElement, IntoAttribute, IntoClass,
|
||||
IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
|
||||
};
|
||||
/// Utilities for simple isomorphic logging to the console or terminal.
|
||||
pub mod logging {
|
||||
|
@ -169,9 +176,11 @@ pub mod logging {
|
|||
pub mod error {
|
||||
pub use server_fn::error::{Error, Result};
|
||||
}
|
||||
#[cfg(all(target_arch = "wasm32", feature = "template_macro"))]
|
||||
pub use leptos_macro::template;
|
||||
#[cfg(not(any(target_arch = "wasm32", feature = "template_macro")))]
|
||||
pub use leptos_macro::view as template;
|
||||
pub use leptos_macro::{component, server, slot, view, Params};
|
||||
pub use leptos_macro::{component, island, server, slot, view, Params};
|
||||
pub use leptos_reactive::*;
|
||||
pub use leptos_server::{
|
||||
self, create_action, create_multi_action, create_server_action,
|
||||
|
@ -179,8 +188,6 @@ pub use leptos_server::{
|
|||
ServerFnErrorErr,
|
||||
};
|
||||
pub use server_fn::{self, ServerFn as _};
|
||||
#[cfg(all(target_arch = "wasm32", feature = "template_macro"))]
|
||||
pub use {leptos_macro::template, wasm_bindgen, web_sys};
|
||||
mod error_boundary;
|
||||
pub use error_boundary::*;
|
||||
mod animated_show;
|
||||
|
@ -188,11 +195,18 @@ mod for_loop;
|
|||
mod show;
|
||||
pub use animated_show::*;
|
||||
pub use for_loop::*;
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
pub use serde;
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
pub use serde_json;
|
||||
pub use show::*;
|
||||
pub use suspense_component::*;
|
||||
mod suspense_component;
|
||||
mod text_prop;
|
||||
mod transition;
|
||||
// used by the component macro to generate islands
|
||||
#[doc(hidden)]
|
||||
pub use const_format;
|
||||
pub use text_prop::TextProp;
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
#[doc(hidden)]
|
||||
|
@ -204,6 +218,23 @@ pub use typed_builder;
|
|||
pub use typed_builder::Optional;
|
||||
#[doc(hidden)]
|
||||
pub use typed_builder_macro;
|
||||
#[doc(hidden)]
|
||||
#[cfg(any(
|
||||
feature = "csr",
|
||||
feature = "hydrate",
|
||||
feature = "template_macro"
|
||||
))]
|
||||
pub use wasm_bindgen; // used in islands
|
||||
#[doc(hidden)]
|
||||
#[cfg(any(
|
||||
feature = "csr",
|
||||
feature = "hydrate",
|
||||
feature = "template_macro"
|
||||
))]
|
||||
pub use web_sys; // used in islands
|
||||
// used by the component macro to generate islands
|
||||
#[doc(hidden)]
|
||||
pub use xxhash_rust;
|
||||
mod children;
|
||||
pub use children::*;
|
||||
extern crate self as leptos;
|
||||
|
|
|
@ -6,7 +6,6 @@ fn simple_ssr_test() {
|
|||
let runtime = create_runtime();
|
||||
let (value, set_value) = create_signal(0);
|
||||
let rendered = view! {
|
||||
|
||||
<div>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
|
||||
<span>"Value: " {move || value.get().to_string()} "!"</span>
|
||||
|
@ -14,13 +13,22 @@ fn simple_ssr_test() {
|
|||
</div>
|
||||
};
|
||||
|
||||
assert!(rendered.into_view().render_to_string().contains(
|
||||
"<div id=\"_0-0-1\"><button id=\"_0-0-2\">-1</button><span \
|
||||
id=\"_0-0-3\">Value: \
|
||||
<!--hk=_0-0-4o|leptos-dyn-child-start-->0<!\
|
||||
--hk=_0-0-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-0-5\">+1</button></div>"
|
||||
));
|
||||
if cfg!(all(feature = "experimental-islands", feature = "ssr")) {
|
||||
assert_eq!(
|
||||
rendered.into_view().render_to_string(),
|
||||
"<div><button>-1</button><span>Value: \
|
||||
0!</span><button>+1</button></div>"
|
||||
);
|
||||
} else {
|
||||
assert!(rendered.into_view().render_to_string().contains(
|
||||
"<div data-hk=\"0-0-1\"><button \
|
||||
data-hk=\"0-0-2\">-1</button><span data-hk=\"0-0-3\">Value: \
|
||||
<!--hk=0-0-4o|leptos-dyn-child-start-->0<!\
|
||||
--hk=0-0-4c|leptos-dyn-child-end-->!</span><button \
|
||||
data-hk=\"0-0-5\">+1</button></div>"
|
||||
));
|
||||
}
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
|
@ -51,13 +59,22 @@ fn ssr_test_with_components() {
|
|||
</div>
|
||||
};
|
||||
|
||||
assert!(rendered.into_view().render_to_string().contains(
|
||||
"<div id=\"_0-0-3\"><button id=\"_0-0-4\">-1</button><span \
|
||||
id=\"_0-0-5\">Value: \
|
||||
<!--hk=_0-0-6o|leptos-dyn-child-start-->1<!\
|
||||
--hk=_0-0-6c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-0-7\">+1</button></div>"
|
||||
));
|
||||
if cfg!(all(feature = "experimental-islands", feature = "ssr")) {
|
||||
assert_eq!(
|
||||
rendered.into_view().render_to_string(),
|
||||
"<div class=\"counters\"><div><button>-1</button><span>Value: \
|
||||
1!</span><button>+1</button></div><div><button>-1</\
|
||||
button><span>Value: 2!</span><button>+1</button></div></div>"
|
||||
);
|
||||
} else {
|
||||
assert!(rendered.into_view().render_to_string().contains(
|
||||
"<div data-hk=\"0-0-3\"><button \
|
||||
data-hk=\"0-0-4\">-1</button><span data-hk=\"0-0-5\">Value: \
|
||||
<!--hk=0-0-6o|leptos-dyn-child-start-->1<!\
|
||||
--hk=0-0-6c|leptos-dyn-child-end-->!</span><button \
|
||||
data-hk=\"0-0-7\">+1</button></div>"
|
||||
));
|
||||
}
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
|
@ -88,13 +105,22 @@ fn ssr_test_with_snake_case_components() {
|
|||
</div>
|
||||
};
|
||||
|
||||
assert!(rendered.into_view().render_to_string().contains(
|
||||
"<div id=\"_0-0-3\"><button id=\"_0-0-4\">-1</button><span \
|
||||
id=\"_0-0-5\">Value: \
|
||||
<!--hk=_0-0-6o|leptos-dyn-child-start-->1<!\
|
||||
--hk=_0-0-6c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-0-7\">+1</button></div>"
|
||||
));
|
||||
if cfg!(all(feature = "experimental-islands", feature = "ssr")) {
|
||||
assert_eq!(
|
||||
rendered.into_view().render_to_string(),
|
||||
"<div class=\"counters\"><div><button>-1</button><span>Value: \
|
||||
1!</span><button>+1</button></div><div><button>-1</\
|
||||
button><span>Value: 2!</span><button>+1</button></div></div>"
|
||||
);
|
||||
} else {
|
||||
assert!(rendered.into_view().render_to_string().contains(
|
||||
"<div data-hk=\"0-0-3\"><button \
|
||||
data-hk=\"0-0-4\">-1</button><span data-hk=\"0-0-5\">Value: \
|
||||
<!--hk=0-0-6o|leptos-dyn-child-start-->1<!\
|
||||
--hk=0-0-6c|leptos-dyn-child-end-->!</span><button \
|
||||
data-hk=\"0-0-7\">+1</button></div>"
|
||||
));
|
||||
}
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
@ -111,10 +137,16 @@ fn test_classes() {
|
|||
<div class="my big" class:a={move || value.get() > 10} class:red=true class:car={move || value.get() > 1}></div>
|
||||
};
|
||||
|
||||
assert!(rendered
|
||||
.into_view()
|
||||
.render_to_string()
|
||||
.contains("<div id=\"_0-0-1\" class=\"my big red car\"></div>"));
|
||||
if cfg!(all(feature = "experimental-islands", feature = "ssr")) {
|
||||
assert_eq!(
|
||||
rendered.into_view().render_to_string(),
|
||||
"<div class=\"my big red car\"></div>"
|
||||
);
|
||||
} else {
|
||||
assert!(rendered.into_view().render_to_string().contains(
|
||||
"<div data-hk=\"0-0-1\" class=\"my big red car\"></div>"
|
||||
));
|
||||
}
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
|
@ -133,10 +165,18 @@ fn ssr_with_styles() {
|
|||
</div>
|
||||
};
|
||||
|
||||
assert!(rendered.into_view().render_to_string().contains(
|
||||
"<div id=\"_0-0-1\" class=\" myclass\"><button id=\"_0-0-2\" \
|
||||
class=\"btn myclass\">-1</button></div>"
|
||||
));
|
||||
if cfg!(all(feature = "experimental-islands", feature = "ssr")) {
|
||||
assert_eq!(
|
||||
rendered.into_view().render_to_string(),
|
||||
"<div class=\" myclass\"><button class=\"btn \
|
||||
myclass\">-1</button></div>"
|
||||
);
|
||||
} else {
|
||||
assert!(rendered.into_view().render_to_string().contains(
|
||||
"<div data-hk=\"0-0-1\" class=\" myclass\"><button \
|
||||
data-hk=\"0-0-2\" class=\"btn myclass\">-1</button></div>"
|
||||
));
|
||||
}
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
|
@ -152,10 +192,17 @@ fn ssr_option() {
|
|||
<option/>
|
||||
};
|
||||
|
||||
assert!(rendered
|
||||
.into_view()
|
||||
.render_to_string()
|
||||
.contains("<option id=\"_0-0-1\"></option>"));
|
||||
if cfg!(all(feature = "experimental-islands", feature = "ssr")) {
|
||||
assert_eq!(
|
||||
rendered.into_view().render_to_string(),
|
||||
"<option></option>"
|
||||
);
|
||||
} else {
|
||||
assert!(rendered
|
||||
.into_view()
|
||||
.render_to_string()
|
||||
.contains("<option data-hk=\"0-0-1\"></option>"));
|
||||
}
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
|
|
@ -67,6 +67,7 @@ features = [
|
|||
"CustomEvent",
|
||||
"DeviceMotionEvent",
|
||||
"DeviceOrientationEvent",
|
||||
"DomStringMap",
|
||||
"DragEvent",
|
||||
"ErrorEvent",
|
||||
"Event",
|
||||
|
@ -170,6 +171,7 @@ hydrate = ["leptos_reactive/hydrate", "web"]
|
|||
ssr = ["leptos_reactive/ssr"]
|
||||
nightly = ["leptos_reactive/nightly"]
|
||||
nonce = ["dep:base64", "dep:getrandom", "dep:rand"]
|
||||
experimental-islands = ["leptos_reactive/experimental-islands"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["nightly"]
|
||||
|
|
|
@ -66,7 +66,7 @@ pub struct ComponentRepr {
|
|||
pub children: Vec<View>,
|
||||
closing: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: HydrationKey,
|
||||
pub(crate) id: Option<HydrationKey>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) view_marker: Option<String>,
|
||||
}
|
||||
|
@ -175,12 +175,15 @@ impl ComponentRepr {
|
|||
#[inline(always)]
|
||||
pub fn new_with_id(
|
||||
name: impl Into<Oco<'static, str>>,
|
||||
id: HydrationKey,
|
||||
id: Option<HydrationKey>,
|
||||
) -> Self {
|
||||
Self::new_with_id_concrete(name.into(), id)
|
||||
}
|
||||
|
||||
fn new_with_id_concrete(name: Oco<'static, str>, id: HydrationKey) -> Self {
|
||||
fn new_with_id_concrete(
|
||||
name: Oco<'static, str>,
|
||||
id: Option<HydrationKey>,
|
||||
) -> Self {
|
||||
let markers = (
|
||||
Comment::new(format!("</{name}>"), &id, true),
|
||||
#[cfg(debug_assertions)]
|
||||
|
@ -239,7 +242,7 @@ where
|
|||
F: FnOnce() -> V,
|
||||
V: IntoView,
|
||||
{
|
||||
id: HydrationKey,
|
||||
id: Option<HydrationKey>,
|
||||
name: Oco<'static, str>,
|
||||
children_fn: F,
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ pub struct DynChildRepr {
|
|||
pub(crate) child: Rc<RefCell<Box<Option<View>>>>,
|
||||
closing: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: HydrationKey,
|
||||
pub(crate) id: Option<HydrationKey>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for DynChildRepr {
|
||||
|
@ -81,7 +81,7 @@ impl Mountable for DynChildRepr {
|
|||
}
|
||||
|
||||
impl DynChildRepr {
|
||||
fn new_with_id(id: HydrationKey) -> Self {
|
||||
fn new_with_id(id: Option<HydrationKey>) -> Self {
|
||||
let markers = (
|
||||
Comment::new("</DynChild>", &id, true),
|
||||
#[cfg(debug_assertions)]
|
||||
|
@ -126,7 +126,7 @@ where
|
|||
CF: Fn() -> N + 'static,
|
||||
N: IntoView,
|
||||
{
|
||||
id: crate::HydrationKey,
|
||||
id: Option<HydrationKey>,
|
||||
child_fn: CF,
|
||||
}
|
||||
|
||||
|
@ -146,7 +146,7 @@ where
|
|||
#[doc(hidden)]
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub const fn new_with_id(id: HydrationKey, child_fn: CF) -> Self {
|
||||
pub const fn new_with_id(id: Option<HydrationKey>, child_fn: CF) -> Self {
|
||||
Self { id, child_fn }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ pub struct EachRepr {
|
|||
pub(crate) children: Rc<RefCell<Vec<Option<EachItem>>>>,
|
||||
closing: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: HydrationKey,
|
||||
pub(crate) id: Option<HydrationKey>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for EachRepr {
|
||||
|
@ -175,7 +175,7 @@ pub(crate) struct EachItem {
|
|||
pub(crate) child: View,
|
||||
closing: Option<Comment>,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: HydrationKey,
|
||||
pub(crate) id: Option<HydrationKey>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for EachItem {
|
||||
|
|
|
@ -70,7 +70,12 @@ where
|
|||
E: Into<Error>,
|
||||
{
|
||||
fn into_view(self) -> crate::View {
|
||||
let id = ErrorKey(HydrationCtx::peek().to_string().into());
|
||||
let id = ErrorKey(
|
||||
HydrationCtx::peek()
|
||||
.map(|n| n.to_string())
|
||||
.unwrap_or_default()
|
||||
.into(),
|
||||
);
|
||||
let errors = use_context::<RwSignal<Errors>>();
|
||||
match self {
|
||||
Ok(stuff) => {
|
||||
|
|
|
@ -25,7 +25,7 @@ where
|
|||
/// Represents a group of [`views`](View).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Fragment {
|
||||
id: HydrationKey,
|
||||
id: Option<HydrationKey>,
|
||||
/// The nodes contained in the fragment.
|
||||
pub nodes: Vec<View>,
|
||||
#[cfg(debug_assertions)]
|
||||
|
@ -74,7 +74,10 @@ impl Fragment {
|
|||
|
||||
/// Creates a new [`Fragment`] with the given hydration ID from a [`Vec<Node>`].
|
||||
#[inline(always)]
|
||||
pub const fn new_with_id(id: HydrationKey, nodes: Vec<View>) -> Self {
|
||||
pub const fn new_with_id(
|
||||
id: Option<HydrationKey>,
|
||||
nodes: Vec<View>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
nodes,
|
||||
|
@ -91,7 +94,7 @@ impl Fragment {
|
|||
|
||||
/// Returns the fragment's hydration ID.
|
||||
#[inline(always)]
|
||||
pub fn id(&self) -> &HydrationKey {
|
||||
pub fn id(&self) -> &Option<HydrationKey> {
|
||||
&self.id
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ use crate::{hydration::HydrationCtx, Comment, CoreComponent, IntoView, View};
|
|||
pub struct UnitRepr {
|
||||
comment: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: HydrationKey,
|
||||
pub(crate) id: Option<HydrationKey>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for UnitRepr {
|
||||
|
|
|
@ -107,16 +107,16 @@ impl<E: FromWasmAbi> Custom<E> {
|
|||
/// # use leptos::*;
|
||||
/// # let runtime = create_runtime();
|
||||
/// # let canvas_ref: NodeRef<html::Canvas> = create_node_ref();
|
||||
/// let mut non_passive_wheel = ev::Custom::<ev::WheelEvent>::new("wheel");
|
||||
/// # if false {
|
||||
/// let mut non_passive_wheel = ev::Custom::<ev::WheelEvent>::new("wheel");
|
||||
/// let options = non_passive_wheel.options_mut();
|
||||
/// options.passive(false);
|
||||
/// # }
|
||||
/// canvas_ref.on_load(move |canvas: HtmlElement<html::Canvas>| {
|
||||
/// canvas.on(non_passive_wheel, move |_event| {
|
||||
/// // Handle _event
|
||||
/// });
|
||||
/// });
|
||||
/// # }
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
///
|
||||
|
|
|
@ -6,7 +6,7 @@ cfg_if! {
|
|||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
use crate::events::*;
|
||||
use crate::macro_helpers::*;
|
||||
use crate::{mount_child, MountKind};
|
||||
use crate::{mount_child, HydrationKey, MountKind};
|
||||
use once_cell::unsync::Lazy as LazyCell;
|
||||
use std::cell::Cell;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
@ -85,7 +85,7 @@ pub trait ElementDescriptor: ElementDescriptorBounds {
|
|||
/// A unique `id` that should be generated for each new instance of
|
||||
/// this element, and be consistent for both SSR and CSR.
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
fn hydration_id(&self) -> &HydrationKey;
|
||||
fn hydration_id(&self) -> &Option<HydrationKey>;
|
||||
}
|
||||
|
||||
/// Trait for converting any type which impl [`AsRef<web_sys::Element>`]
|
||||
|
@ -134,7 +134,7 @@ pub struct AnyElement {
|
|||
pub(crate) element: web_sys::HtmlElement,
|
||||
pub(crate) is_void: bool,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: HydrationKey,
|
||||
pub(crate) id: Option<HydrationKey>,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for AnyElement {
|
||||
|
@ -173,7 +173,7 @@ impl ElementDescriptor for AnyElement {
|
|||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
#[inline(always)]
|
||||
fn hydration_id(&self) -> &HydrationKey {
|
||||
fn hydration_id(&self) -> &Option<HydrationKey> {
|
||||
&self.id
|
||||
}
|
||||
}
|
||||
|
@ -185,7 +185,7 @@ pub struct Custom {
|
|||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
element: web_sys::HtmlElement,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id: HydrationKey,
|
||||
id: Option<HydrationKey>,
|
||||
}
|
||||
|
||||
impl Custom {
|
||||
|
@ -195,10 +195,10 @@ impl Custom {
|
|||
let id = HydrationCtx::id();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let element = if HydrationCtx::is_hydrating() {
|
||||
if let Some(el) =
|
||||
crate::document().get_element_by_id(&format!("_{id}"))
|
||||
{
|
||||
let element = if HydrationCtx::is_hydrating() && id.is_some() {
|
||||
let id = id.unwrap();
|
||||
#[cfg(feature = "hydrate")]
|
||||
if let Some(el) = crate::hydration::get_element(&id.to_string()) {
|
||||
#[cfg(debug_assertions)]
|
||||
assert_eq!(
|
||||
el.node_name().to_ascii_uppercase(),
|
||||
|
@ -208,34 +208,21 @@ impl Custom {
|
|||
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
|
||||
);
|
||||
|
||||
el.remove_attribute("id").unwrap();
|
||||
|
||||
el.unchecked_into()
|
||||
} else if let Ok(Some(el)) =
|
||||
crate::document().query_selector(&format!("[leptos-hk=_{id}]"))
|
||||
{
|
||||
#[cfg(debug_assertions)]
|
||||
assert_eq!(
|
||||
el.node_name().to_ascii_uppercase(),
|
||||
name.to_ascii_uppercase(),
|
||||
"SSR and CSR elements have the same hydration key but \
|
||||
different node kinds. Check out the docs for information \
|
||||
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
|
||||
);
|
||||
|
||||
el.remove_attribute("leptos-hk").unwrap();
|
||||
//el.remove_attribute(wasm_bindgen::intern("id")).unwrap();
|
||||
|
||||
el.unchecked_into()
|
||||
} else {
|
||||
if !is_meta_tag() {
|
||||
crate::warn!(
|
||||
"element with id {id} not found, ignoring it for \
|
||||
hydration"
|
||||
hydration",
|
||||
);
|
||||
}
|
||||
|
||||
crate::document().create_element(&name).unwrap()
|
||||
}
|
||||
#[cfg(not(feature = "hydrate"))]
|
||||
unreachable!()
|
||||
} else {
|
||||
crate::document().create_element(&name).unwrap()
|
||||
};
|
||||
|
@ -275,7 +262,7 @@ impl ElementDescriptor for Custom {
|
|||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
#[inline(always)]
|
||||
fn hydration_id(&self) -> &HydrationKey {
|
||||
fn hydration_id(&self) -> &Option<HydrationKey> {
|
||||
&self.id
|
||||
}
|
||||
}
|
||||
|
@ -1082,10 +1069,8 @@ impl<El: ElementDescriptor> IntoView for HtmlElement<El> {
|
|||
|
||||
let mut element = Element::new(element);
|
||||
|
||||
if attrs.iter_mut().any(|(name, _)| name == "id") {
|
||||
attrs.push(("leptos-hk".into(), format!("_{id}").into()));
|
||||
} else {
|
||||
attrs.push(("id".into(), format!("_{id}").into()));
|
||||
if let Some(id) = id {
|
||||
attrs.push(("data-hk".into(), id.to_string().into()));
|
||||
}
|
||||
|
||||
element.attrs = attrs;
|
||||
|
@ -1153,7 +1138,7 @@ macro_rules! generate_html_tags {
|
|||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
element: web_sys::HtmlElement,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id: HydrationKey,
|
||||
id: Option<HydrationKey>,
|
||||
}
|
||||
|
||||
impl Default for [<$tag:camel $($trailing_)?>] {
|
||||
|
@ -1218,7 +1203,7 @@ macro_rules! generate_html_tags {
|
|||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
#[inline(always)]
|
||||
fn hydration_id(&self) -> &HydrationKey {
|
||||
fn hydration_id(&self) -> &Option<HydrationKey> {
|
||||
&self.id
|
||||
}
|
||||
|
||||
|
@ -1255,7 +1240,7 @@ macro_rules! generate_html_tags {
|
|||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
fn create_leptos_element(
|
||||
tag: &str,
|
||||
id: crate::HydrationKey,
|
||||
id: Option<HydrationKey>,
|
||||
clone_element: fn() -> web_sys::HtmlElement,
|
||||
) -> web_sys::HtmlElement {
|
||||
#[cfg(not(debug_assertions))]
|
||||
|
@ -1263,9 +1248,10 @@ fn create_leptos_element(
|
|||
_ = tag;
|
||||
}
|
||||
|
||||
if HydrationCtx::is_hydrating() {
|
||||
if let Some(el) = crate::document().get_element_by_id(&format!("_{id}"))
|
||||
{
|
||||
#[cfg(feature = "hydrate")]
|
||||
if HydrationCtx::is_hydrating() && id.is_some() {
|
||||
let id = id.unwrap();
|
||||
if let Some(el) = crate::hydration::get_element(&id.to_string()) {
|
||||
#[cfg(debug_assertions)]
|
||||
assert_eq!(
|
||||
&el.node_name().to_ascii_uppercase(),
|
||||
|
@ -1275,23 +1261,6 @@ fn create_leptos_element(
|
|||
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
|
||||
);
|
||||
|
||||
el.remove_attribute("id").unwrap();
|
||||
|
||||
el.unchecked_into()
|
||||
} else if let Ok(Some(el)) =
|
||||
crate::document().query_selector(&format!("[leptos-hk=_{id}]"))
|
||||
{
|
||||
#[cfg(debug_assertions)]
|
||||
assert_eq!(
|
||||
el.node_name().to_ascii_uppercase(),
|
||||
tag,
|
||||
"SSR and CSR elements have the same hydration key but \
|
||||
different node kinds. Check out the docs for information \
|
||||
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
|
||||
);
|
||||
|
||||
el.remove_attribute("leptos-hk").unwrap();
|
||||
|
||||
el.unchecked_into()
|
||||
} else {
|
||||
if !is_meta_tag() {
|
||||
|
@ -1305,6 +1274,10 @@ fn create_leptos_element(
|
|||
} else {
|
||||
clone_element()
|
||||
}
|
||||
#[cfg(not(feature = "hydrate"))]
|
||||
{
|
||||
clone_element()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
#[cfg(all(
|
||||
feature = "experimental-islands",
|
||||
any(feature = "hydrate", feature = "ssr")
|
||||
))]
|
||||
use leptos_reactive::SharedContext;
|
||||
use std::{cell::RefCell, fmt::Display};
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "hydrate"))]
|
||||
mod hydration {
|
||||
#[cfg(feature = "hydrate")]
|
||||
mod hydrate_only {
|
||||
use once_cell::unsync::Lazy as LazyCell;
|
||||
use std::{cell::RefCell, collections::HashMap};
|
||||
use std::{cell::Cell, collections::HashMap};
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
/// See ["createTreeWalker"](https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker)
|
||||
#[allow(unused)]
|
||||
const FILTER_SHOW_COMMENT: u32 = 0b10000000;
|
||||
|
||||
// We can tell if we start in hydration mode by checking to see if the
|
||||
// id "_0-1" is present in the DOM. If it is, we know we are hydrating from
|
||||
// the server, if not, we are starting off in CSR
|
||||
thread_local! {
|
||||
pub static HYDRATION_COMMENTS: LazyCell<HashMap<String, web_sys::Comment>> = LazyCell::new(|| {
|
||||
let document = crate::document();
|
||||
|
@ -32,53 +35,50 @@ mod hydration {
|
|||
map
|
||||
});
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub static VIEW_MARKERS: LazyCell<HashMap<String, web_sys::Comment>> = LazyCell::new(|| {
|
||||
pub static HYDRATION_ELEMENTS: LazyCell<HashMap<String, web_sys::HtmlElement>> = LazyCell::new(|| {
|
||||
let document = crate::document();
|
||||
let body = document.body().unwrap();
|
||||
let walker = document
|
||||
.create_tree_walker_with_what_to_show(&body, FILTER_SHOW_COMMENT)
|
||||
.unwrap();
|
||||
let mut map = HashMap::new();
|
||||
while let Ok(Some(node)) = walker.next_node() {
|
||||
if let Some(content) = node.text_content() {
|
||||
if let Some(id) = content.strip_prefix("leptos-view|") {
|
||||
map.insert(id.into(), node.unchecked_into());
|
||||
let els = document.query_selector_all("[data-hk]");
|
||||
if let Ok(list) = els {
|
||||
let len = list.length();
|
||||
let mut map = HashMap::with_capacity(len as usize);
|
||||
for idx in 0..len {
|
||||
let el = list.item(idx).unwrap().unchecked_into::<web_sys::HtmlElement>();
|
||||
let dataset = el.dataset();
|
||||
let hk = dataset.get(wasm_bindgen::intern("hk")).unwrap();
|
||||
map.insert(hk, el);
|
||||
}
|
||||
}
|
||||
map
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
map
|
||||
});
|
||||
|
||||
pub static IS_HYDRATING: RefCell<LazyCell<bool>> = RefCell::new(LazyCell::new(|| {
|
||||
#[cfg(debug_assertions)]
|
||||
return crate::document().get_element_by_id("_0-0-1").is_some()
|
||||
|| crate::document().get_element_by_id("_0-0-1o").is_some()
|
||||
|| HYDRATION_COMMENTS.with(|comments| comments.get("_0-0-1o").is_some());
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
return crate::document().get_element_by_id("_0-0-1").is_some()
|
||||
|| HYDRATION_COMMENTS.with(|comments| comments.get("_0-0-1").is_some());
|
||||
}));
|
||||
pub static IS_HYDRATING: Cell<bool> = Cell::new(true);
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn get_marker(id: &str) -> Option<web_sys::Comment> {
|
||||
HYDRATION_COMMENTS.with(|comments| comments.get(id).cloned())
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn get_element(hk: &str) -> Option<web_sys::HtmlElement> {
|
||||
HYDRATION_ELEMENTS.with(|els| els.get(hk).cloned())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "hydrate"))]
|
||||
pub(crate) use hydration::*;
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub(crate) use hydrate_only::*;
|
||||
|
||||
/// A stable identifier within the server-rendering or hydration process.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct HydrationKey {
|
||||
/// ID of the current key.
|
||||
pub id: usize,
|
||||
/// ID of the current error boundary.
|
||||
pub error: usize,
|
||||
/// ID of the current fragment.
|
||||
pub fragment: usize,
|
||||
/// ID of the current error boundary.
|
||||
pub error: usize,
|
||||
/// ID of the current key.
|
||||
pub id: usize,
|
||||
}
|
||||
|
||||
impl Display for HydrationKey {
|
||||
|
@ -87,24 +87,94 @@ impl Display for HydrationKey {
|
|||
}
|
||||
}
|
||||
|
||||
thread_local!(static ID: RefCell<HydrationKey> = Default::default());
|
||||
impl std::str::FromStr for HydrationKey {
|
||||
type Err = (); // TODO better error
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut pieces = s.splitn(3, '-');
|
||||
let first = pieces.next().ok_or(())?;
|
||||
let second = pieces.next().ok_or(())?;
|
||||
let third = pieces.next().ok_or(())?;
|
||||
let fragment = usize::from_str(first).map_err(|_| ())?;
|
||||
let error = usize::from_str(second).map_err(|_| ())?;
|
||||
let id = usize::from_str(third).map_err(|_| ())?;
|
||||
Ok(HydrationKey {
|
||||
fragment,
|
||||
error,
|
||||
id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn parse_hydration_key() {
|
||||
use crate::HydrationKey;
|
||||
use std::str::FromStr;
|
||||
assert_eq!(
|
||||
HydrationKey::from_str("1-2-3"),
|
||||
Ok(HydrationKey {
|
||||
fragment: 1,
|
||||
error: 2,
|
||||
id: 3
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
thread_local!(static ID: RefCell<HydrationKey> = RefCell::new(HydrationKey { fragment: 0, error: 0, id: 0 }));
|
||||
|
||||
/// Control and utility methods for hydration.
|
||||
pub struct HydrationCtx;
|
||||
|
||||
impl HydrationCtx {
|
||||
/// If you're in an hydration context, get the next `id` without incrementing it.
|
||||
pub fn peek() -> Option<HydrationKey> {
|
||||
#[cfg(all(
|
||||
feature = "experimental-islands",
|
||||
any(feature = "hydrate", feature = "ssr")
|
||||
))]
|
||||
let no_hydrate = SharedContext::no_hydrate();
|
||||
#[cfg(not(all(
|
||||
feature = "experimental-islands",
|
||||
any(feature = "hydrate", feature = "ssr")
|
||||
)))]
|
||||
let no_hydrate = false;
|
||||
if no_hydrate {
|
||||
None
|
||||
} else {
|
||||
Some(ID.with(|id| *id.borrow()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the next `id` without incrementing it.
|
||||
pub fn peek() -> HydrationKey {
|
||||
pub fn peek_always() -> HydrationKey {
|
||||
ID.with(|id| *id.borrow())
|
||||
}
|
||||
|
||||
/// Increments the current hydration `id` and returns it
|
||||
pub fn id() -> HydrationKey {
|
||||
ID.with(|id| {
|
||||
let mut id = id.borrow_mut();
|
||||
id.id = id.id.wrapping_add(1);
|
||||
*id
|
||||
})
|
||||
pub fn id() -> Option<HydrationKey> {
|
||||
#[cfg(all(
|
||||
feature = "experimental-islands",
|
||||
any(feature = "hydrate", feature = "ssr")
|
||||
))]
|
||||
let no_hydrate = SharedContext::no_hydrate();
|
||||
#[cfg(not(all(
|
||||
feature = "experimental-islands",
|
||||
any(feature = "hydrate", feature = "ssr")
|
||||
)))]
|
||||
let no_hydrate = false;
|
||||
|
||||
if no_hydrate {
|
||||
None
|
||||
} else {
|
||||
Some(ID.with(|id| {
|
||||
let mut id = id.borrow_mut();
|
||||
id.id = id.id.wrapping_add(1);
|
||||
*id
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the hydration `id` for the next component, and returns it
|
||||
|
@ -130,7 +200,13 @@ impl HydrationCtx {
|
|||
#[doc(hidden)]
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub fn reset_id() {
|
||||
ID.with(|id| *id.borrow_mut() = Default::default());
|
||||
ID.with(|id| {
|
||||
*id.borrow_mut() = HydrationKey {
|
||||
fragment: 0,
|
||||
error: 0,
|
||||
id: 0,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Resumes hydration from the provided `id`. Useful for
|
||||
|
@ -139,38 +215,63 @@ impl HydrationCtx {
|
|||
ID.with(|i| *i.borrow_mut() = id);
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn stop_hydrating() {
|
||||
/// Resumes hydration after the provided `id`. Useful for
|
||||
/// islands and other fancy things.
|
||||
pub fn continue_after(id: HydrationKey) {
|
||||
ID.with(|i| {
|
||||
*i.borrow_mut() = HydrationKey {
|
||||
fragment: id.fragment,
|
||||
error: id.error,
|
||||
id: id.id + 1,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn stop_hydrating() {
|
||||
#[cfg(feature = "hydrate")]
|
||||
{
|
||||
IS_HYDRATING.with(|is_hydrating| {
|
||||
std::mem::take(&mut *is_hydrating.borrow_mut());
|
||||
is_hydrating.set(false);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub fn with_hydration_on<T>(f: impl FnOnce() -> T) -> T {
|
||||
let prev = IS_HYDRATING.with(|is_hydrating| {
|
||||
let prev = is_hydrating.get();
|
||||
is_hydrating.set(true);
|
||||
prev
|
||||
});
|
||||
let value = f();
|
||||
IS_HYDRATING.with(|is_hydrating| is_hydrating.set(prev));
|
||||
value
|
||||
}
|
||||
|
||||
/// Whether the UI is currently in the process of hydrating from the server-sent HTML.
|
||||
#[inline(always)]
|
||||
pub fn is_hydrating() -> bool {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "hydrate"))]
|
||||
#[cfg(feature = "hydrate")]
|
||||
{
|
||||
IS_HYDRATING.with(|is_hydrating| **is_hydrating.borrow())
|
||||
IS_HYDRATING.with(|is_hydrating| is_hydrating.get())
|
||||
}
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "hydrate")))]
|
||||
#[cfg(not(feature = "hydrate"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // not used in CSR
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[allow(unused)]
|
||||
pub(crate) fn to_string(id: &HydrationKey, closing: bool) -> String {
|
||||
#[cfg(debug_assertions)]
|
||||
return format!("_{id}{}", if closing { 'c' } else { 'o' });
|
||||
return format!("{id}{}", if closing { 'c' } else { 'o' });
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
let _ = closing;
|
||||
|
||||
format!("_{id}")
|
||||
id.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -268,7 +268,7 @@ cfg_if! {
|
|||
is_void: bool,
|
||||
attrs: SmallVec<[(Oco<'static, str>, Oco<'static, str>); 4]>,
|
||||
children: ElementChildren,
|
||||
id: HydrationKey,
|
||||
id: Option<HydrationKey>,
|
||||
#[cfg(debug_assertions)]
|
||||
/// Optional marker for the view macro source, in debug mode.
|
||||
pub view_marker: Option<String>
|
||||
|
@ -406,7 +406,7 @@ impl Comment {
|
|||
#[inline]
|
||||
fn new(
|
||||
content: impl Into<Oco<'static, str>>,
|
||||
id: &HydrationKey,
|
||||
id: &Option<HydrationKey>,
|
||||
closing: bool,
|
||||
) -> Self {
|
||||
Self::new_inner(content.into(), id, closing)
|
||||
|
@ -414,7 +414,7 @@ impl Comment {
|
|||
|
||||
fn new_inner(
|
||||
content: Oco<'static, str>,
|
||||
id: &HydrationKey,
|
||||
id: &Option<HydrationKey>,
|
||||
closing: bool,
|
||||
) -> Self {
|
||||
cfg_if! {
|
||||
|
@ -436,7 +436,8 @@ impl Comment {
|
|||
node.set_text_content(Some(&format!(" {content} ")));
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
if HydrationCtx::is_hydrating() {
|
||||
if HydrationCtx::is_hydrating() && id.is_some() {
|
||||
let id = id.as_ref().unwrap();
|
||||
let id = HydrationCtx::to_string(id, closing);
|
||||
|
||||
if let Some(marker) = hydration::get_marker(&id) {
|
||||
|
@ -467,7 +468,7 @@ pub struct Text {
|
|||
/// to update the node without recreating it, we need to be able
|
||||
/// to possibly reuse a previous node.
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
node: web_sys::Node,
|
||||
pub(crate) node: web_sys::Node,
|
||||
/// The current contents of the text node.
|
||||
pub content: Oco<'static, str>,
|
||||
}
|
||||
|
@ -872,18 +873,35 @@ where
|
|||
/// Runs the provided closure and mounts the result to the provided element.
|
||||
pub fn mount_to<F, N>(parent: web_sys::HtmlElement, f: F)
|
||||
where
|
||||
F: Fn() -> N + 'static,
|
||||
F: FnOnce() -> N + 'static,
|
||||
N: IntoView,
|
||||
{
|
||||
mount_to_with_stop_hydrating(parent, true, f)
|
||||
}
|
||||
|
||||
/// Runs the provided closure and mounts the result to the provided element.
|
||||
pub fn mount_to_with_stop_hydrating<F, N>(
|
||||
parent: web_sys::HtmlElement,
|
||||
stop_hydrating: bool,
|
||||
f: F,
|
||||
) where
|
||||
F: FnOnce() -> N + 'static,
|
||||
N: IntoView,
|
||||
{
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
let node = f().into_view();
|
||||
HydrationCtx::stop_hydrating();
|
||||
parent.append_child(&node.get_mountable_node()).unwrap();
|
||||
if stop_hydrating {
|
||||
HydrationCtx::stop_hydrating();
|
||||
}
|
||||
// TODO is this *ever* needed? unnecessarily remounts hydrated child
|
||||
// parent.append_child(&node.get_mountable_node()).unwrap();
|
||||
_ = parent;
|
||||
std::mem::forget(node);
|
||||
} else {
|
||||
_ = parent;
|
||||
_ = f;
|
||||
_ = stop_hydrating;
|
||||
crate::warn!("`mount_to` should not be called outside the browser.");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,18 +50,17 @@ macro_rules! generate_math_tags {
|
|||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
element: web_sys::HtmlElement,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id: HydrationKey,
|
||||
id: Option<HydrationKey>,
|
||||
}
|
||||
|
||||
impl Default for [<$tag:camel $($second:camel $($third:camel)?)?>] {
|
||||
fn default() -> Self {
|
||||
let id = HydrationCtx::id();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let element = if HydrationCtx::is_hydrating() {
|
||||
if let Some(el) = crate::document().get_element_by_id(
|
||||
&format!("_{id}")
|
||||
) {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "hydrate"))]
|
||||
let element = if HydrationCtx::is_hydrating() && id.is_some() {
|
||||
let id = id.unwrap();
|
||||
if let Some(el) = crate::hydration::get_element(&id.to_string()) {
|
||||
#[cfg(debug_assertions)]
|
||||
assert_eq!(
|
||||
el.node_name().to_ascii_uppercase(),
|
||||
|
@ -71,23 +70,6 @@ macro_rules! generate_math_tags {
|
|||
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
|
||||
);
|
||||
|
||||
el.remove_attribute("id").unwrap();
|
||||
|
||||
el.unchecked_into()
|
||||
} else if let Ok(Some(el)) = crate::document().query_selector(
|
||||
&format!("[leptos-hk=_{id}]")
|
||||
) {
|
||||
#[cfg(debug_assertions)]
|
||||
assert_eq!(
|
||||
el.node_name().to_ascii_uppercase(),
|
||||
stringify!([<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]),
|
||||
"SSR and CSR elements have the same hydration key but \
|
||||
different node kinds. Check out the docs for information \
|
||||
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
|
||||
);
|
||||
|
||||
el.remove_attribute("leptos-hk").unwrap();
|
||||
|
||||
el.unchecked_into()
|
||||
} else {
|
||||
crate::warn!(
|
||||
|
@ -110,6 +92,14 @@ macro_rules! generate_math_tags {
|
|||
)
|
||||
};
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web", not(feature = "hydrate")))]
|
||||
let element = [<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]
|
||||
.with(|el|
|
||||
el.clone_node()
|
||||
.unwrap()
|
||||
.unchecked_into()
|
||||
);
|
||||
|
||||
Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
element,
|
||||
|
@ -150,7 +140,7 @@ macro_rules! generate_math_tags {
|
|||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
fn hydration_id(&self) -> &HydrationKey {
|
||||
fn hydration_id(&self) -> &Option<HydrationKey> {
|
||||
&self.id
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
use crate::{
|
||||
html::{ElementChildren, StringOrView},
|
||||
CoreComponent, HydrationCtx, IntoView, View,
|
||||
CoreComponent, HydrationCtx, HydrationKey, IntoView, View,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use futures::{stream::FuturesUnordered, Future, Stream, StreamExt};
|
||||
|
@ -401,11 +401,11 @@ impl View {
|
|||
};
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let content = format!(r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
|
||||
HydrationCtx::to_string(&node.id, false),
|
||||
let name = to_kebab_case(&node.name);
|
||||
let content = format!(r#"{}{}{}"#,
|
||||
node.id.to_marker(false, &name),
|
||||
content(),
|
||||
HydrationCtx::to_string(&node.id, true),
|
||||
name = to_kebab_case(&node.name)
|
||||
node.id.to_marker(true, &name),
|
||||
);
|
||||
if let Some(id) = node.view_marker {
|
||||
format!("<!--leptos-view|{id}|open-->{content}<!--leptos-view|{id}|close-->").into()
|
||||
|
@ -414,9 +414,9 @@ impl View {
|
|||
}
|
||||
} else {
|
||||
format!(
|
||||
r#"{}<!--hk={}-->"#,
|
||||
r#"{}{}"#,
|
||||
content(),
|
||||
HydrationCtx::to_string(&node.id, true)
|
||||
node.id.to_marker(true)
|
||||
).into()
|
||||
}
|
||||
}
|
||||
|
@ -434,21 +434,11 @@ impl View {
|
|||
"",
|
||||
false,
|
||||
Box::new(move || {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
format!(
|
||||
"<!--hk={}|leptos-unit-->",
|
||||
HydrationCtx::to_string(&u.id, true)
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
format!(
|
||||
"<!--hk={}-->",
|
||||
HydrationCtx::to_string(&u.id, true)
|
||||
u.id.to_marker(
|
||||
true,
|
||||
#[cfg(debug_assertions)]
|
||||
"unit",
|
||||
)
|
||||
.into()
|
||||
})
|
||||
as Box<dyn FnOnce() -> Oco<'static, str>>,
|
||||
),
|
||||
|
@ -515,7 +505,10 @@ impl View {
|
|||
.flatten()
|
||||
.map(|node| {
|
||||
let id = node.id;
|
||||
let is_el = matches!(node.child, View::Element(_));
|
||||
let is_el = matches!(
|
||||
node.child,
|
||||
View::Element(_)
|
||||
);
|
||||
|
||||
let content = || {
|
||||
node.child.render_to_string_helper(
|
||||
|
@ -523,32 +516,24 @@ impl View {
|
|||
)
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if is_el {
|
||||
content()
|
||||
} else {
|
||||
format!(
|
||||
"<!--hk={}|leptos-each-item-start-->{}<!\
|
||||
--hk={}|leptos-each-item-end-->",
|
||||
HydrationCtx::to_string(&id, false),
|
||||
content(),
|
||||
HydrationCtx::to_string(&id, true),
|
||||
).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
if is_el {
|
||||
content()
|
||||
} else {
|
||||
format!(
|
||||
"{}<!--hk={}-->",
|
||||
content(),
|
||||
HydrationCtx::to_string(&id, true)
|
||||
).into()
|
||||
}
|
||||
if is_el {
|
||||
content()
|
||||
} else {
|
||||
format!(
|
||||
"{}{}{}",
|
||||
id.to_marker(
|
||||
false,
|
||||
#[cfg(debug_assertions)]
|
||||
"each-item",
|
||||
),
|
||||
content(),
|
||||
id.to_marker(
|
||||
true,
|
||||
#[cfg(debug_assertions)]
|
||||
"each-item",
|
||||
)
|
||||
)
|
||||
.into()
|
||||
}
|
||||
})
|
||||
.join("")
|
||||
|
@ -560,24 +545,21 @@ impl View {
|
|||
};
|
||||
|
||||
if wrap {
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
format!(
|
||||
r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
|
||||
HydrationCtx::to_string(&id, false),
|
||||
content(),
|
||||
HydrationCtx::to_string(&id, true),
|
||||
).into()
|
||||
} else {
|
||||
let _ = name;
|
||||
|
||||
format!(
|
||||
r#"{}<!--hk={}-->"#,
|
||||
content(),
|
||||
HydrationCtx::to_string(&id, true)
|
||||
).into()
|
||||
}
|
||||
}
|
||||
format!(
|
||||
r#"{}{}{}"#,
|
||||
id.to_marker(
|
||||
false,
|
||||
#[cfg(debug_assertions)]
|
||||
name,
|
||||
),
|
||||
content(),
|
||||
id.to_marker(
|
||||
true,
|
||||
#[cfg(debug_assertions)]
|
||||
name,
|
||||
),
|
||||
)
|
||||
.into()
|
||||
} else {
|
||||
content()
|
||||
}
|
||||
|
@ -735,3 +717,58 @@ where
|
|||
{
|
||||
html_escape::encode_double_quoted_attribute(value).into()
|
||||
}
|
||||
|
||||
pub(crate) trait ToMarker {
|
||||
fn to_marker(
|
||||
&self,
|
||||
closing: bool,
|
||||
#[cfg(debug_assertions)] component_name: &str,
|
||||
) -> Oco<'static, str>;
|
||||
}
|
||||
|
||||
impl ToMarker for HydrationKey {
|
||||
#[inline(always)]
|
||||
fn to_marker(
|
||||
&self,
|
||||
closing: bool,
|
||||
#[cfg(debug_assertions)] component_name: &str,
|
||||
) -> Oco<'static, str> {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if component_name == "unit" {
|
||||
format!("<!--hk={self}|leptos-unit-->").into()
|
||||
} else if closing {
|
||||
format!("<!--hk={self}c|leptos-{component_name}-end-->").into()
|
||||
} else {
|
||||
format!("<!--hk={self}o|leptos-{component_name}-start-->")
|
||||
.into()
|
||||
}
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
if closing {
|
||||
format!("<!--hk={self}-->").into()
|
||||
} else {
|
||||
"".into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToMarker for Option<HydrationKey> {
|
||||
#[inline(always)]
|
||||
fn to_marker(
|
||||
&self,
|
||||
closing: bool,
|
||||
#[cfg(debug_assertions)] component_name: &str,
|
||||
) -> Oco<'static, str> {
|
||||
self.map(|key| {
|
||||
key.to_marker(
|
||||
closing,
|
||||
#[cfg(debug_assertions)]
|
||||
component_name,
|
||||
)
|
||||
})
|
||||
.unwrap_or("".into())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,17 +4,16 @@
|
|||
|
||||
use crate::{
|
||||
html::{ElementChildren, StringOrView},
|
||||
ssr::render_serializers,
|
||||
ssr::{render_serializers, ToMarker},
|
||||
CoreComponent, HydrationCtx, View,
|
||||
};
|
||||
use async_recursion::async_recursion;
|
||||
use cfg_if::cfg_if;
|
||||
use futures::{channel::mpsc::UnboundedSender, Stream, StreamExt};
|
||||
use itertools::Itertools;
|
||||
use leptos_reactive::{
|
||||
create_runtime, suspense::StreamChunk, Oco, RuntimeId, SharedContext,
|
||||
};
|
||||
use std::{borrow::Cow, collections::VecDeque};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// Renders a view to HTML, waiting to return until all `async` [Resource](leptos_reactive::Resource)s
|
||||
/// loaded in `<Suspense/>` elements have finished loading.
|
||||
|
@ -255,21 +254,25 @@ impl View {
|
|||
chunks.push_back(StreamChunk::Sync(node.content))
|
||||
}
|
||||
View::Component(node) => {
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let name = crate::ssr::to_kebab_case(&node.name);
|
||||
chunks.push_back(StreamChunk::Sync(format!(r#"<!--hk={}|leptos-{name}-start-->"#, HydrationCtx::to_string(&node.id, false)).into()));
|
||||
for child in node.children {
|
||||
child.into_stream_chunks_helper(chunks, dont_escape_text);
|
||||
}
|
||||
chunks.push_back(StreamChunk::Sync(format!(r#"<!--hk={}|leptos-{name}-end-->"#, HydrationCtx::to_string(&node.id, true)).into()));
|
||||
} else {
|
||||
for child in node.children {
|
||||
child.into_stream_chunks_helper(chunks, dont_escape_text);
|
||||
}
|
||||
chunks.push_back(StreamChunk::Sync(format!(r#"<!--hk={}-->"#, HydrationCtx::to_string(&node.id, true)).into()))
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
let name = crate::ssr::to_kebab_case(&node.name);
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
chunks.push_back(StreamChunk::Sync(node.id.to_marker(
|
||||
false,
|
||||
#[cfg(debug_assertions)]
|
||||
&name,
|
||||
)));
|
||||
}
|
||||
|
||||
for child in node.children {
|
||||
child.into_stream_chunks_helper(chunks, dont_escape_text);
|
||||
}
|
||||
chunks.push_back(StreamChunk::Sync(node.id.to_marker(
|
||||
true,
|
||||
#[cfg(debug_assertions)]
|
||||
&name,
|
||||
)));
|
||||
}
|
||||
View::Element(el) => {
|
||||
let is_script_or_style =
|
||||
|
@ -303,7 +306,7 @@ impl View {
|
|||
.attrs
|
||||
.into_iter()
|
||||
.filter_map(
|
||||
|(name, value)| -> Option<Cow<'static, str>> {
|
||||
|(name, value)| -> Option<Oco<'static, str>> {
|
||||
if value.is_empty() {
|
||||
Some(format!(" {name}").into())
|
||||
} else if name == "inner_html" {
|
||||
|
@ -375,24 +378,12 @@ impl View {
|
|||
"",
|
||||
false,
|
||||
Box::new(move |chunks: &mut VecDeque<StreamChunk>| {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!(
|
||||
"<!--hk={}|leptos-unit-->",
|
||||
HydrationCtx::to_string(&u.id, true)
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!(
|
||||
"<!--hk={}-->",
|
||||
HydrationCtx::to_string(&u.id, true)
|
||||
)
|
||||
.into(),
|
||||
u.id.to_marker(
|
||||
true,
|
||||
#[cfg(debug_assertions)]
|
||||
"unit",
|
||||
),
|
||||
));
|
||||
})
|
||||
as Box<dyn FnOnce(&mut VecDeque<StreamChunk>)>,
|
||||
|
@ -480,56 +471,31 @@ impl View {
|
|||
);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if !is_el {
|
||||
chunks.push_back(
|
||||
StreamChunk::Sync(
|
||||
format!(
|
||||
"<!--hk={}|leptos-each-item-start-->",
|
||||
HydrationCtx::to_string(&id, false)
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
}
|
||||
node.child
|
||||
.into_stream_chunks_helper(
|
||||
chunks,
|
||||
dont_escape_text,
|
||||
);
|
||||
if !is_el {
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
id.to_marker(
|
||||
false,
|
||||
"each-item",
|
||||
),
|
||||
))
|
||||
};
|
||||
node.child.into_stream_chunks_helper(
|
||||
chunks,
|
||||
dont_escape_text,
|
||||
);
|
||||
|
||||
if !is_el {
|
||||
chunks.push_back(
|
||||
StreamChunk::Sync(
|
||||
format!(
|
||||
"<!--hk={}|leptos-each-item-end-->",
|
||||
HydrationCtx::to_string(&id, true)
|
||||
)
|
||||
.into(),
|
||||
if !is_el {
|
||||
chunks.push_back(
|
||||
StreamChunk::Sync(
|
||||
id.to_marker(
|
||||
true,
|
||||
#[cfg(
|
||||
debug_assertions
|
||||
)]
|
||||
"each-item",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
node.child
|
||||
.into_stream_chunks_helper(
|
||||
chunks,
|
||||
dont_escape_text,
|
||||
);
|
||||
if !is_el {
|
||||
chunks.push_back(
|
||||
StreamChunk::Sync(
|
||||
format!(
|
||||
"<!--hk={}-->",
|
||||
HydrationCtx::to_string(
|
||||
&id, true
|
||||
)
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -540,17 +506,18 @@ impl View {
|
|||
};
|
||||
|
||||
if wrap {
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
chunks.push_back(StreamChunk::Sync(format!("<!--hk={}|leptos-{name}-start-->", HydrationCtx::to_string(&id, false)).into()));
|
||||
content(chunks);
|
||||
chunks.push_back(StreamChunk::Sync(format!("<!--hk={}|leptos-{name}-end-->", HydrationCtx::to_string(&id, true)).into()));
|
||||
} else {
|
||||
let _ = name;
|
||||
content(chunks);
|
||||
chunks.push_back(StreamChunk::Sync(format!("<!--hk={}-->", HydrationCtx::to_string(&id, true)).into()))
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
id.to_marker(false, name),
|
||||
));
|
||||
}
|
||||
content(chunks);
|
||||
chunks.push_back(StreamChunk::Sync(id.to_marker(
|
||||
true,
|
||||
#[cfg(debug_assertions)]
|
||||
name,
|
||||
)));
|
||||
} else {
|
||||
content(chunks);
|
||||
}
|
||||
|
|
|
@ -47,18 +47,17 @@ macro_rules! generate_svg_tags {
|
|||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
element: web_sys::HtmlElement,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id: HydrationKey,
|
||||
id: Option<HydrationKey>,
|
||||
}
|
||||
|
||||
impl Default for [<$tag:camel $($second:camel $($third:camel)?)?>] {
|
||||
fn default() -> Self {
|
||||
let id = HydrationCtx::id();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let element = if HydrationCtx::is_hydrating() {
|
||||
if let Some(el) = crate::document().get_element_by_id(
|
||||
&format!("_{id}")
|
||||
) {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "hydrate"))]
|
||||
let element = if HydrationCtx::is_hydrating() && id.is_some() {
|
||||
let id = id.unwrap();
|
||||
if let Some(el) = crate::hydration::get_element(&id.to_string()) {
|
||||
#[cfg(debug_assertions)]
|
||||
assert_eq!(
|
||||
el.node_name().to_ascii_uppercase(),
|
||||
|
@ -67,24 +66,6 @@ macro_rules! generate_svg_tags {
|
|||
different node kinds. Check out the docs for information \
|
||||
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
|
||||
);
|
||||
|
||||
el.remove_attribute("id").unwrap();
|
||||
|
||||
el.unchecked_into()
|
||||
} else if let Ok(Some(el)) = crate::document().query_selector(
|
||||
&format!("[leptos-hk=_{id}]")
|
||||
) {
|
||||
#[cfg(debug_assertions)]
|
||||
assert_eq!(
|
||||
el.node_name().to_ascii_uppercase(),
|
||||
stringify!([<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]),
|
||||
"SSR and CSR elements have the same hydration key but \
|
||||
different node kinds. Check out the docs for information \
|
||||
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
|
||||
);
|
||||
|
||||
el.remove_attribute("leptos-hk").unwrap();
|
||||
|
||||
el.unchecked_into()
|
||||
} else {
|
||||
crate::warn!(
|
||||
|
@ -107,6 +88,14 @@ macro_rules! generate_svg_tags {
|
|||
)
|
||||
};
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web", not(feature = "hydrate")))]
|
||||
let element = [<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]
|
||||
.with(|el|
|
||||
el.clone_node()
|
||||
.unwrap()
|
||||
.unchecked_into()
|
||||
);
|
||||
|
||||
Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
element,
|
||||
|
@ -147,7 +136,7 @@ macro_rules! generate_svg_tags {
|
|||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
fn hydration_id(&self) -> &HydrationKey {
|
||||
fn hydration_id(&self) -> &Option<HydrationKey> {
|
||||
&self.id
|
||||
}
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ hydrate = []
|
|||
ssr = ["server_fn_macro/ssr"]
|
||||
nightly = ["server_fn_macro/nightly"]
|
||||
tracing = []
|
||||
experimental-islands = []
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["nightly", "tracing"]
|
||||
|
|
|
@ -15,6 +15,7 @@ use syn::{
|
|||
};
|
||||
pub struct Model {
|
||||
is_transparent: bool,
|
||||
is_island: bool,
|
||||
docs: Docs,
|
||||
vis: Visibility,
|
||||
name: Ident,
|
||||
|
@ -65,6 +66,7 @@ impl Parse for Model {
|
|||
|
||||
Ok(Self {
|
||||
is_transparent: false,
|
||||
is_island: false,
|
||||
docs,
|
||||
vis: item.vis.clone(),
|
||||
name: convert_from_snake_case(&item.sig.ident),
|
||||
|
@ -104,6 +106,7 @@ impl ToTokens for Model {
|
|||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let Self {
|
||||
is_transparent,
|
||||
is_island,
|
||||
docs,
|
||||
vis,
|
||||
name,
|
||||
|
@ -146,9 +149,28 @@ impl ToTokens for Model {
|
|||
|
||||
let props_name = format_ident!("{name}Props");
|
||||
let props_builder_name = format_ident!("{name}PropsBuilder");
|
||||
let props_serialized_name = format_ident!("{name}PropsSerialized");
|
||||
let trace_name = format!("<{name} />");
|
||||
|
||||
let prop_builder_fields = prop_builder_fields(vis, props);
|
||||
let is_island_with_children = *is_island
|
||||
&& props.iter().any(|prop| prop.name.ident == "children");
|
||||
let is_island_with_other_props = *is_island
|
||||
&& ((is_island_with_children && props.len() > 1)
|
||||
|| (!is_island_with_children && !props.is_empty()));
|
||||
|
||||
let prop_builder_fields =
|
||||
prop_builder_fields(vis, props, is_island_with_other_props);
|
||||
let props_serializer = if is_island_with_other_props {
|
||||
let fields = prop_serializer_fields(vis, props);
|
||||
quote! {
|
||||
#[derive(::leptos::serde::Deserialize)]
|
||||
#vis struct #props_serialized_name {
|
||||
#fields
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let prop_names = prop_names(props);
|
||||
|
||||
|
@ -192,25 +214,69 @@ impl ToTokens for Model {
|
|||
(quote! {}, quote! {}, quote! {}, quote! {})
|
||||
};
|
||||
|
||||
let component = if *is_transparent {
|
||||
let component_id = name.to_string();
|
||||
let hydrate_fn_name =
|
||||
Ident::new(&format!("_island_{}", component_id), name.span());
|
||||
|
||||
let island_serialize_props = if is_island_with_other_props {
|
||||
quote! {
|
||||
let _leptos_ser_props = ::leptos::serde_json::to_string(&props).expect("couldn't serialize island props");
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
let island_serialized_props = if is_island_with_other_props {
|
||||
quote! {
|
||||
.attr("data-props", _leptos_ser_props)
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let body_expr = if *is_island {
|
||||
quote! {
|
||||
::leptos::SharedContext::with_hydration(move || {
|
||||
#body_name(#prop_names)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#body_name(#prop_names)
|
||||
}
|
||||
};
|
||||
|
||||
let component = if *is_transparent {
|
||||
body_expr
|
||||
} else {
|
||||
quote! {
|
||||
::leptos::leptos_dom::Component::new(
|
||||
stringify!(#name),
|
||||
move || {
|
||||
#tracing_guard_expr
|
||||
|
||||
#tracing_props_expr
|
||||
|
||||
#body_name(#prop_names)
|
||||
#body_expr
|
||||
}
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// add island wrapper if island
|
||||
let component = if *is_island {
|
||||
quote! {
|
||||
{
|
||||
::leptos::leptos_dom::html::custom(
|
||||
::leptos::leptos_dom::html::Custom::new("leptos-island"),
|
||||
)
|
||||
.attr("data-component", #component_id)
|
||||
.attr("data-hkc", ::leptos::leptos_dom::HydrationCtx::peek_always().to_string())
|
||||
#island_serialized_props
|
||||
.child(#component)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
component
|
||||
};
|
||||
|
||||
let props_arg = if no_props {
|
||||
quote! {}
|
||||
} else {
|
||||
|
@ -222,10 +288,29 @@ impl ToTokens for Model {
|
|||
let destructure_props = if no_props {
|
||||
quote! {}
|
||||
} else {
|
||||
let wrapped_children = if is_island_with_children
|
||||
&& cfg!(feature = "ssr")
|
||||
{
|
||||
quote! {
|
||||
let children = Box::new(|| ::leptos::Fragment::lazy(|| vec![
|
||||
::leptos::SharedContext::with_hydration(move || {
|
||||
::leptos::leptos_dom::html::custom(
|
||||
::leptos::leptos_dom::html::Custom::new("leptos-children"),
|
||||
)
|
||||
.child(::leptos::SharedContext::no_hydration(children))
|
||||
.into_view()
|
||||
})
|
||||
]));
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
quote! {
|
||||
#island_serialize_props
|
||||
let #props_name {
|
||||
#prop_names
|
||||
} = props;
|
||||
#wrapped_children
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -247,18 +332,128 @@ impl ToTokens for Model {
|
|||
}
|
||||
};
|
||||
|
||||
let body = quote! {
|
||||
#body
|
||||
#destructure_props
|
||||
#tracing_span_expr
|
||||
#component
|
||||
};
|
||||
|
||||
let binding = if *is_island
|
||||
&& cfg!(any(feature = "csr", feature = "hydrate"))
|
||||
{
|
||||
let island_props = if is_island_with_children
|
||||
|| is_island_with_other_props
|
||||
{
|
||||
let (destructure, prop_builders) = if is_island_with_other_props
|
||||
{
|
||||
let prop_names = props
|
||||
.iter()
|
||||
.filter_map(|prop| {
|
||||
if prop.name.ident == "children" {
|
||||
None
|
||||
} else {
|
||||
let name = &prop.name.ident;
|
||||
Some(quote! { #name, })
|
||||
}
|
||||
})
|
||||
.collect::<TokenStream>();
|
||||
let destructure = quote! {
|
||||
let #props_serialized_name {
|
||||
#prop_names
|
||||
} = props;
|
||||
};
|
||||
let prop_builders = props
|
||||
.iter()
|
||||
.filter_map(|prop| {
|
||||
if prop.name.ident == "children" {
|
||||
None
|
||||
} else {
|
||||
let name = &prop.name.ident;
|
||||
Some(quote! {
|
||||
.#name(#name)
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<TokenStream>();
|
||||
(destructure, prop_builders)
|
||||
} else {
|
||||
(quote! {}, quote! {})
|
||||
};
|
||||
let children = if is_island_with_children {
|
||||
quote! {
|
||||
.children(Box::new(move || ::leptos::Fragment::lazy(|| vec![
|
||||
::leptos::SharedContext::with_hydration(move || {
|
||||
::leptos::leptos_dom::html::custom(
|
||||
::leptos::leptos_dom::html::Custom::new("leptos-children"),
|
||||
)
|
||||
.into_view()
|
||||
})])))
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
quote! {{
|
||||
#destructure
|
||||
#props_name::builder()
|
||||
#prop_builders
|
||||
#children
|
||||
.build()
|
||||
}}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
let deserialize_island_props = if is_island_with_other_props {
|
||||
quote! {
|
||||
let props = el.dataset().get(::leptos::wasm_bindgen::intern("props"))
|
||||
.and_then(|data| ::leptos::serde_json::from_str::<#props_serialized_name>(&data).ok())
|
||||
.expect("could not deserialize props");
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
quote! {
|
||||
#[::leptos::wasm_bindgen::prelude::wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn #hydrate_fn_name(el: ::leptos::web_sys::HtmlElement) {
|
||||
if let Some(Ok(key)) = el.dataset().get(::leptos::wasm_bindgen::intern("hkc")).map(|key| std::str::FromStr::from_str(&key)) {
|
||||
::leptos::leptos_dom::HydrationCtx::continue_from(key);
|
||||
}
|
||||
#deserialize_island_props
|
||||
::leptos::leptos_dom::mount_to_with_stop_hydrating(el, false, move || {
|
||||
#name(#island_props)
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let props_derive_serialize = if is_island_with_other_props {
|
||||
quote! { , ::leptos::serde::Serialize }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let output = quote! {
|
||||
#[doc = #builder_name_doc]
|
||||
#[doc = ""]
|
||||
#docs
|
||||
#component_fn_prop_docs
|
||||
#[derive(::leptos::typed_builder_macro::TypedBuilder)]
|
||||
#[derive(::leptos::typed_builder_macro::TypedBuilder #props_derive_serialize)]
|
||||
//#[builder(doc)]
|
||||
#[builder(crate_module_path=::leptos::typed_builder)]
|
||||
#vis struct #props_name #impl_generics #where_clause {
|
||||
#prop_builder_fields
|
||||
}
|
||||
|
||||
#props_serializer
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#binding
|
||||
|
||||
impl #impl_generics ::leptos::Props for #props_name #generics #where_clause {
|
||||
type Builder = #props_builder_name #generics;
|
||||
fn builder() -> Self::Builder {
|
||||
|
@ -277,16 +472,7 @@ impl ToTokens for Model {
|
|||
) #ret #(+ #lifetimes)*
|
||||
#where_clause
|
||||
{
|
||||
#[allow(non_snake_case)]
|
||||
// allowed for lifetimes that are needed for props struct
|
||||
#[allow(clippy::needless_lifetimes)]
|
||||
#body
|
||||
|
||||
#destructure_props
|
||||
|
||||
#tracing_span_expr
|
||||
|
||||
#component
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -301,6 +487,13 @@ impl Model {
|
|||
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
pub fn is_island(mut self) -> Self {
|
||||
self.is_island = true;
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct Prop {
|
||||
|
@ -526,6 +719,25 @@ impl TypedBuilderOpts {
|
|||
}
|
||||
}
|
||||
|
||||
impl TypedBuilderOpts {
|
||||
fn to_serde_tokens(&self) -> TokenStream {
|
||||
let default = if let Some(v) = &self.default_with_value {
|
||||
let v = v.to_token_stream().to_string();
|
||||
quote! { default=#v, }
|
||||
} else if self.default {
|
||||
quote! { default, }
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
if !default.is_empty() {
|
||||
quote! { #[serde(#default)] }
|
||||
} else {
|
||||
quote! {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for TypedBuilderOpts {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let default = if let Some(v) = &self.default_with_value {
|
||||
|
@ -565,7 +777,11 @@ impl ToTokens for TypedBuilderOpts {
|
|||
}
|
||||
}
|
||||
|
||||
fn prop_builder_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
|
||||
fn prop_builder_fields(
|
||||
vis: &Visibility,
|
||||
props: &[Prop],
|
||||
is_island_with_other_props: bool,
|
||||
) -> TokenStream {
|
||||
props
|
||||
.iter()
|
||||
.map(|prop| {
|
||||
|
@ -587,6 +803,12 @@ fn prop_builder_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
|
|||
} else {
|
||||
quote!()
|
||||
};
|
||||
let skip_children_serde =
|
||||
if is_island_with_other_props && name.ident == "children" {
|
||||
quote!(#[serde(skip)])
|
||||
} else {
|
||||
quote!()
|
||||
};
|
||||
|
||||
let PatIdent { ident, by_ref, .. } = &name;
|
||||
|
||||
|
@ -595,12 +817,43 @@ fn prop_builder_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
|
|||
#builder_docs
|
||||
#builder_attrs
|
||||
#allow_missing_docs
|
||||
#skip_children_serde
|
||||
#vis #by_ref #ident: #ty,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn prop_serializer_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
|
||||
props
|
||||
.iter()
|
||||
.filter_map(|prop| {
|
||||
if prop.name.ident == "children" {
|
||||
None
|
||||
} else {
|
||||
let Prop {
|
||||
docs,
|
||||
name,
|
||||
prop_opts,
|
||||
ty,
|
||||
} = prop;
|
||||
|
||||
let builder_attrs =
|
||||
TypedBuilderOpts::from_opts(prop_opts, is_option(ty));
|
||||
let serde_attrs = builder_attrs.to_serde_tokens();
|
||||
|
||||
let PatIdent { ident, by_ref, .. } = &name;
|
||||
|
||||
Some(quote! {
|
||||
#docs
|
||||
#serde_attrs
|
||||
#vis #by_ref #ident: #ty,
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn prop_names(props: &[Prop]) -> TokenStream {
|
||||
props
|
||||
.iter()
|
||||
|
|
|
@ -632,6 +632,33 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
|||
.into()
|
||||
}
|
||||
|
||||
/// TODO docs for islands
|
||||
#[proc_macro_error::proc_macro_error]
|
||||
#[proc_macro_attribute]
|
||||
pub fn island(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
let is_transparent = if !args.is_empty() {
|
||||
let transparent = parse_macro_input!(args as syn::Ident);
|
||||
|
||||
if transparent != "transparent" {
|
||||
abort!(
|
||||
transparent,
|
||||
"only `transparent` is supported";
|
||||
help = "try `#[island(transparent)]` or `#[island]`"
|
||||
);
|
||||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
parse_macro_input!(s as component::Model)
|
||||
.is_transparent(is_transparent)
|
||||
.is_island()
|
||||
.into_token_stream()
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Annotates a struct so that it can be used with your Component as a `slot`.
|
||||
///
|
||||
/// The `#[slot]` macro allows you to annotate plain Rust struct as component slots and use them
|
||||
|
|
|
@ -297,19 +297,10 @@ fn element_to_tokens_ssr(
|
|||
} else {
|
||||
quote! { ::leptos::leptos_dom::HydrationCtx::id() }
|
||||
};
|
||||
match node
|
||||
.attributes()
|
||||
.iter()
|
||||
.find(|node| matches!(node, NodeAttribute::Attribute(attr) if attr.key.to_string() == "id"))
|
||||
{
|
||||
Some(_) => {
|
||||
template.push_str(" leptos-hk=\"_{}\"");
|
||||
}
|
||||
None => {
|
||||
template.push_str(" id=\"_{}\"");
|
||||
}
|
||||
}
|
||||
holes.push(hydration_id);
|
||||
template.push_str("{}");
|
||||
holes.push(quote! {
|
||||
#hydration_id.map(|id| format!(" data-hk=\"{id}\"")).unwrap_or_default()
|
||||
});
|
||||
|
||||
set_class_attribute_ssr(node, template, holes, global_class);
|
||||
set_style_attribute_ssr(node, template, holes);
|
||||
|
|
|
@ -687,7 +687,7 @@ TokenStream [
|
|||
},
|
||||
Ident {
|
||||
sym: click,
|
||||
span: bytes(337..342),
|
||||
span: bytes(357..362),
|
||||
},
|
||||
Punct {
|
||||
char: ',',
|
||||
|
@ -802,7 +802,7 @@ TokenStream [
|
|||
},
|
||||
Ident {
|
||||
sym: click,
|
||||
span: bytes(343..348),
|
||||
span: bytes(383..388),
|
||||
},
|
||||
Punct {
|
||||
char: ',',
|
||||
|
@ -959,7 +959,7 @@ TokenStream [
|
|||
},
|
||||
Ident {
|
||||
sym: click,
|
||||
span: bytes(349..354),
|
||||
span: bytes(429..434),
|
||||
},
|
||||
Punct {
|
||||
char: ',',
|
||||
|
@ -1208,7 +1208,7 @@ TokenStream [
|
|||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Literal {
|
||||
lit: "<div id=\"_{}\"><button id=\"_{}\">Clear</button><button id=\"_{}\">-1</button><span id=\"_{}\">Value: ",
|
||||
lit: "<div{}><button{}>Clear</button><button{}>-1</button><span{}>Value: ",
|
||||
},
|
||||
Punct {
|
||||
char: ',',
|
||||
|
@ -1263,52 +1263,50 @@ TokenStream [
|
|||
stream: TokenStream [],
|
||||
},
|
||||
Punct {
|
||||
char: ',',
|
||||
spacing: Alone,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Joint,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
char: '.',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: leptos,
|
||||
sym: map,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Punct {
|
||||
char: '|',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: id,
|
||||
},
|
||||
Punct {
|
||||
char: '|',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: format,
|
||||
},
|
||||
Punct {
|
||||
char: '!',
|
||||
spacing: Alone,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Literal {
|
||||
lit: " data-hk=\"{id}\"",
|
||||
span: bytes(337..356),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Joint,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
char: '.',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: leptos_dom,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Joint,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: HydrationCtx,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Joint,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: id,
|
||||
sym: unwrap_or_default,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
|
@ -1366,6 +1364,56 @@ TokenStream [
|
|||
delimiter: Parenthesis,
|
||||
stream: TokenStream [],
|
||||
},
|
||||
Punct {
|
||||
char: '.',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: map,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Punct {
|
||||
char: '|',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: id,
|
||||
},
|
||||
Punct {
|
||||
char: '|',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: format,
|
||||
},
|
||||
Punct {
|
||||
char: '!',
|
||||
spacing: Alone,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Literal {
|
||||
lit: " data-hk=\"{id}\"",
|
||||
span: bytes(363..382),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
Punct {
|
||||
char: '.',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: unwrap_or_default,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [],
|
||||
},
|
||||
Punct {
|
||||
char: ',',
|
||||
spacing: Alone,
|
||||
|
@ -1418,6 +1466,158 @@ TokenStream [
|
|||
delimiter: Parenthesis,
|
||||
stream: TokenStream [],
|
||||
},
|
||||
Punct {
|
||||
char: '.',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: map,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Punct {
|
||||
char: '|',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: id,
|
||||
},
|
||||
Punct {
|
||||
char: '|',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: format,
|
||||
},
|
||||
Punct {
|
||||
char: '!',
|
||||
spacing: Alone,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Literal {
|
||||
lit: " data-hk=\"{id}\"",
|
||||
span: bytes(389..408),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
Punct {
|
||||
char: '.',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: unwrap_or_default,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [],
|
||||
},
|
||||
Punct {
|
||||
char: ',',
|
||||
spacing: Alone,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Joint,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: leptos,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Joint,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: leptos_dom,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Joint,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: HydrationCtx,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Joint,
|
||||
},
|
||||
Punct {
|
||||
char: ':',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: id,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [],
|
||||
},
|
||||
Punct {
|
||||
char: '.',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: map,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Punct {
|
||||
char: '|',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: id,
|
||||
},
|
||||
Punct {
|
||||
char: '|',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: format,
|
||||
},
|
||||
Punct {
|
||||
char: '!',
|
||||
spacing: Alone,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Literal {
|
||||
lit: " data-hk=\"{id}\"",
|
||||
span: bytes(409..428),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
Punct {
|
||||
char: '.',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: unwrap_or_default,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [],
|
||||
},
|
||||
],
|
||||
},
|
||||
Punct {
|
||||
|
@ -1685,7 +1885,7 @@ TokenStream [
|
|||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Literal {
|
||||
lit: "!</span><button id=\"_{}\">+1</button></div>",
|
||||
lit: "!</span><button{}>+1</button></div>",
|
||||
},
|
||||
Punct {
|
||||
char: ',',
|
||||
|
@ -1739,6 +1939,56 @@ TokenStream [
|
|||
delimiter: Parenthesis,
|
||||
stream: TokenStream [],
|
||||
},
|
||||
Punct {
|
||||
char: '.',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: map,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Punct {
|
||||
char: '|',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: id,
|
||||
},
|
||||
Punct {
|
||||
char: '|',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: format,
|
||||
},
|
||||
Punct {
|
||||
char: '!',
|
||||
spacing: Alone,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [
|
||||
Literal {
|
||||
lit: " data-hk=\"{id}\"",
|
||||
span: bytes(435..454),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
Punct {
|
||||
char: '.',
|
||||
spacing: Alone,
|
||||
},
|
||||
Ident {
|
||||
sym: unwrap_or_default,
|
||||
},
|
||||
Group {
|
||||
delimiter: Parenthesis,
|
||||
stream: TokenStream [],
|
||||
},
|
||||
],
|
||||
},
|
||||
Punct {
|
||||
|
|
|
@ -32,11 +32,15 @@ fn view() {
|
|||
[
|
||||
leptos::leptos_dom::html::StringOrView::String(
|
||||
format!(
|
||||
"<div id=\"_{}\"><button id=\"_{}\">Clear</button><button id=\"_{}\">-1</button><span id=\"_{}\">Value: ",
|
||||
::leptos::leptos_dom::HydrationCtx::peek(),
|
||||
::leptos::leptos_dom::HydrationCtx::id(),
|
||||
::leptos::leptos_dom::HydrationCtx::id(),
|
||||
::leptos::leptos_dom::HydrationCtx::id()
|
||||
"<div{}><button{}>Clear</button><button{}>-1</button><span{}>Value: ",
|
||||
::leptos::leptos_dom::HydrationCtx::peek().map(| id |
|
||||
format!(" data-hk=\"{id}\"")).unwrap_or_default(),
|
||||
::leptos::leptos_dom::HydrationCtx::id().map(| id |
|
||||
format!(" data-hk=\"{id}\"")).unwrap_or_default(),
|
||||
::leptos::leptos_dom::HydrationCtx::id().map(| id |
|
||||
format!(" data-hk=\"{id}\"")).unwrap_or_default(),
|
||||
::leptos::leptos_dom::HydrationCtx::id().map(| id |
|
||||
format!(" data-hk=\"{id}\"")).unwrap_or_default()
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
|
@ -49,8 +53,9 @@ fn view() {
|
|||
},
|
||||
leptos::leptos_dom::html::StringOrView::String(
|
||||
format!(
|
||||
"!</span><button id=\"_{}\">+1</button></div>",
|
||||
::leptos::leptos_dom::HydrationCtx::id()
|
||||
"!</span><button{}>+1</button></div>",
|
||||
::leptos::leptos_dom::HydrationCtx::id().map(| id |
|
||||
format!(" data-hk=\"{id}\"")).unwrap_or_default()
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
|
|
|
@ -72,6 +72,7 @@ serde = []
|
|||
serde-lite = ["dep:serde-lite"]
|
||||
miniserde = ["dep:miniserde"]
|
||||
rkyv = ["dep:rkyv", "dep:bytecheck"]
|
||||
experimental-islands = []
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["nightly"]
|
||||
|
|
|
@ -2,10 +2,11 @@ use crate::{
|
|||
runtime::PinnedFuture, suspense::StreamChunk, with_runtime, ResourceId,
|
||||
SuspenseContext,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use futures::stream::FuturesUnordered;
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
use std::cell::Cell;
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Hydration data and other context that is shared between the server
|
||||
/// and the client.
|
||||
pub struct SharedContext {
|
||||
|
@ -17,6 +18,8 @@ pub struct SharedContext {
|
|||
pub resolved_resources: HashMap<ResourceId, String>,
|
||||
/// Suspended fragments that have not yet resolved.
|
||||
pub pending_fragments: HashMap<String, FragmentData>,
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
pub no_hydrate: bool,
|
||||
}
|
||||
|
||||
impl SharedContext {
|
||||
|
@ -194,41 +197,87 @@ impl Eq for SharedContext {}
|
|||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for SharedContext {
|
||||
fn default() -> Self {
|
||||
cfg_if! {
|
||||
if #[cfg(all(feature = "hydrate", target_arch = "wasm32"))] {
|
||||
let pending_resources = js_sys::Reflect::get(
|
||||
&web_sys::window().unwrap(),
|
||||
&wasm_bindgen::JsValue::from_str("__LEPTOS_PENDING_RESOURCES"),
|
||||
);
|
||||
let pending_resources: HashSet<ResourceId> = pending_resources
|
||||
.map_err(|_| ())
|
||||
.and_then(|pr| serde_wasm_bindgen::from_value(pr).map_err(|_| ()))
|
||||
.unwrap();
|
||||
#[cfg(all(feature = "hydrate", target_arch = "wasm32"))]
|
||||
{
|
||||
let pending_resources = js_sys::Reflect::get(
|
||||
&web_sys::window().unwrap(),
|
||||
&wasm_bindgen::JsValue::from_str("__LEPTOS_PENDING_RESOURCES"),
|
||||
);
|
||||
let pending_resources: HashSet<ResourceId> = pending_resources
|
||||
.map_err(|_| ())
|
||||
.and_then(|pr| {
|
||||
serde_wasm_bindgen::from_value(pr).map_err(|_| ())
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let resolved_resources = js_sys::Reflect::get(
|
||||
&web_sys::window().unwrap(),
|
||||
&wasm_bindgen::JsValue::from_str("__LEPTOS_RESOLVED_RESOURCES"),
|
||||
)
|
||||
.unwrap(); // unwrap_or(wasm_bindgen::JsValue::NULL);
|
||||
let resolved_resources = js_sys::Reflect::get(
|
||||
&web_sys::window().unwrap(),
|
||||
&wasm_bindgen::JsValue::from_str("__LEPTOS_RESOLVED_RESOURCES"),
|
||||
)
|
||||
.unwrap(); // unwrap_or(wasm_bindgen::JsValue::NULL);
|
||||
|
||||
let resolved_resources =
|
||||
serde_wasm_bindgen::from_value(resolved_resources).unwrap();
|
||||
let resolved_resources =
|
||||
serde_wasm_bindgen::from_value(resolved_resources).unwrap();
|
||||
|
||||
|
||||
Self {
|
||||
server_resources: pending_resources.clone(),
|
||||
pending_resources,
|
||||
resolved_resources,
|
||||
pending_fragments: Default::default(),
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
server_resources: Default::default(),
|
||||
pending_resources: Default::default(),
|
||||
resolved_resources: Default::default(),
|
||||
pending_fragments: Default::default(),
|
||||
}
|
||||
Self {
|
||||
server_resources: pending_resources.clone(),
|
||||
//events: Default::default(),
|
||||
pending_resources,
|
||||
resolved_resources,
|
||||
pending_fragments: Default::default(),
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
no_hydrate: true,
|
||||
}
|
||||
}
|
||||
#[cfg(not(all(feature = "hydrate", target_arch = "wasm32")))]
|
||||
{
|
||||
Self {
|
||||
server_resources: Default::default(),
|
||||
//events: Default::default(),
|
||||
pending_resources: Default::default(),
|
||||
resolved_resources: Default::default(),
|
||||
pending_fragments: Default::default(),
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
no_hydrate: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
thread_local! {
|
||||
pub static NO_HYDRATE: Cell<bool> = Cell::new(true);
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
impl SharedContext {
|
||||
/// Whether the renderer should currently add hydration IDs.
|
||||
pub fn no_hydrate() -> bool {
|
||||
NO_HYDRATE.with(Cell::get)
|
||||
}
|
||||
|
||||
/// Sets whether the renderer should not add hydration IDs.
|
||||
pub fn set_no_hydrate(hydrate: bool) {
|
||||
NO_HYDRATE.with(|cell| cell.set(hydrate));
|
||||
}
|
||||
|
||||
/// Turns on hydration for the duration of the function call
|
||||
#[inline(always)]
|
||||
pub fn with_hydration<T>(f: impl FnOnce() -> T) -> T {
|
||||
let prev = SharedContext::no_hydrate();
|
||||
SharedContext::set_no_hydrate(false);
|
||||
let v = f();
|
||||
SharedContext::set_no_hydrate(prev);
|
||||
v
|
||||
}
|
||||
|
||||
/// Turns off hydration for the duration of the function call
|
||||
#[inline(always)]
|
||||
pub fn no_hydration<T>(f: impl FnOnce() -> T) -> T {
|
||||
let prev = SharedContext::no_hydrate();
|
||||
SharedContext::set_no_hydrate(true);
|
||||
let v = f();
|
||||
SharedContext::set_no_hydrate(prev);
|
||||
v
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#[cfg(feature = "experimental-islands")]
|
||||
use crate::SharedContext;
|
||||
#[cfg(debug_assertions)]
|
||||
use crate::SpecialNonReactiveZone;
|
||||
use crate::{
|
||||
|
@ -204,6 +206,8 @@ where
|
|||
version: Rc::new(Cell::new(0)),
|
||||
suspense_contexts: Default::default(),
|
||||
serializable,
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
should_send_to_client: Default::default(),
|
||||
});
|
||||
|
||||
let id = with_runtime(|runtime| {
|
||||
|
@ -335,6 +339,8 @@ where
|
|||
version: Rc::new(Cell::new(0)),
|
||||
suspense_contexts: Default::default(),
|
||||
serializable: ResourceSerialization::Local,
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
should_send_to_client: Default::default(),
|
||||
});
|
||||
|
||||
let id = with_runtime(|runtime| {
|
||||
|
@ -1078,6 +1084,8 @@ where
|
|||
version: Rc<Cell<usize>>,
|
||||
suspense_contexts: Rc<RefCell<HashSet<SuspenseContext>>>,
|
||||
serializable: ResourceSerialization,
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
should_send_to_client: Rc<Cell<Option<bool>>>,
|
||||
}
|
||||
|
||||
/// Whether and how the resource can be serialized.
|
||||
|
@ -1255,6 +1263,15 @@ where
|
|||
return;
|
||||
}
|
||||
|
||||
// if it's 1) in normal mode and is read, or
|
||||
// 2) is in island mode and read in an island, tell it to ship
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
if self.should_send_to_client.get().is_none()
|
||||
&& !SharedContext::no_hydrate()
|
||||
{
|
||||
self.should_send_to_client.set(Some(true));
|
||||
}
|
||||
|
||||
let version = self.version.get() + 1;
|
||||
self.version.set(version);
|
||||
self.scheduled.set(false);
|
||||
|
@ -1363,6 +1380,8 @@ pub(crate) trait SerializableResource {
|
|||
&self,
|
||||
id: ResourceId,
|
||||
) -> Pin<Box<dyn Future<Output = (ResourceId, String)>>>;
|
||||
|
||||
fn should_send_to_client(&self) -> bool;
|
||||
}
|
||||
|
||||
impl<S, T> SerializableResource for ResourceState<S, T>
|
||||
|
@ -1373,10 +1392,12 @@ where
|
|||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn to_serialization_resolver(
|
||||
&self,
|
||||
id: ResourceId,
|
||||
|
@ -1384,6 +1405,22 @@ where
|
|||
let fut = self.resource_to_serialization_resolver(id);
|
||||
Box::pin(fut)
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[inline(always)]
|
||||
fn should_send_to_client(&self) -> bool {
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
{
|
||||
self.should_send_to_client.get() == Some(true)
|
||||
}
|
||||
#[cfg(not(feature = "experimental-islands"))]
|
||||
{
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait UnserializableResource {
|
||||
|
|
|
@ -1138,8 +1138,8 @@ impl Runtime {
|
|||
.borrow()
|
||||
.iter()
|
||||
.filter_map(|(resource_id, res)| {
|
||||
if matches!(res, AnyResource::Serializable(_)) {
|
||||
Some(resource_id)
|
||||
if let AnyResource::Serializable(res) = res {
|
||||
res.should_send_to_client().then_some(resource_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -1154,7 +1154,9 @@ impl Runtime {
|
|||
let resources = { self.resources.borrow().clone() };
|
||||
for (id, resource) in resources.iter() {
|
||||
if let AnyResource::Serializable(resource) = resource {
|
||||
f.push(resource.to_serialization_resolver(id));
|
||||
if resource.should_send_to_client() {
|
||||
f.push(resource.to_serialization_resolver(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
f
|
||||
|
|
|
@ -80,3 +80,12 @@ impl<'de, T: Deserialize<'de>> Deserialize<'de> for RwSignal<T> {
|
|||
T::deserialize(deserializer).map(create_rw_signal)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: Deserialize<'de>> Deserialize<'de> for MaybeSignal<T> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
T::deserialize(deserializer).map(MaybeSignal::Static)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -275,7 +275,9 @@ impl MetaContext {
|
|||
}
|
||||
tags.push_str(&self.tags.as_string());
|
||||
|
||||
HydrationCtx::continue_from(prev_key);
|
||||
if let Some(prev_key) = prev_key {
|
||||
HydrationCtx::continue_from(prev_key);
|
||||
}
|
||||
tags
|
||||
}
|
||||
}
|
||||
|
|
|
@ -146,12 +146,12 @@ impl History for BrowserIntegration {
|
|||
/// ```
|
||||
/// # use leptos_router::*;
|
||||
/// # use leptos::*;
|
||||
/// # let runtime = create_runtime();
|
||||
/// # let rt = create_runtime();
|
||||
/// let integration = ServerIntegration {
|
||||
/// path: "http://leptos.rs/".to_string(),
|
||||
/// };
|
||||
/// provide_context(RouterIntegrationContext::new(integration));
|
||||
/// # runtime.dispose();
|
||||
/// # rt.dispose();
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct RouterIntegrationContext(pub Rc<dyn History>);
|
||||
|
@ -183,13 +183,13 @@ impl History for RouterIntegrationContext {
|
|||
/// ```
|
||||
/// # use leptos_router::*;
|
||||
/// # use leptos::*;
|
||||
/// # let runtime = create_runtime();
|
||||
/// # let rt = create_runtime();
|
||||
/// let integration = ServerIntegration {
|
||||
/// // Swap out with your URL if integrating manually.
|
||||
/// path: "http://leptos.rs/".to_string(),
|
||||
/// };
|
||||
/// provide_context(RouterIntegrationContext::new(integration));
|
||||
/// # runtime.dispose();
|
||||
/// # rt.dispose();
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ServerIntegration {
|
||||
|
|
Loading…
Reference in a new issue