mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 15:44:42 -05:00
Compare commits
59 Commits
router-tes
...
chorse
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbe3ec015c | ||
|
|
492fa6c6d3 | ||
|
|
343e8c8abe | ||
|
|
656d20cb65 | ||
|
|
a0a66b75dd | ||
|
|
085ba3506c | ||
|
|
63f3780eda | ||
|
|
6fbbd09000 | ||
|
|
f2842cf14e | ||
|
|
a6b6864bc5 | ||
|
|
063b946cd4 | ||
|
|
5a2c9ea345 | ||
|
|
808d87598b | ||
|
|
0956c48b1e | ||
|
|
8915e2615b | ||
|
|
7f47134058 | ||
|
|
af7b93fa1e | ||
|
|
ed940f577a | ||
|
|
916f30a07b | ||
|
|
e01c565de1 | ||
|
|
dffe195cdc | ||
|
|
a5e2587555 | ||
|
|
af8889fab2 | ||
|
|
267c1cfc34 | ||
|
|
3498378e60 | ||
|
|
f8c680d14d | ||
|
|
b852e459a9 | ||
|
|
681f10ec8d | ||
|
|
1d480791a1 | ||
|
|
7acc309f66 | ||
|
|
9527de15ed | ||
|
|
aeb25a715a | ||
|
|
46e91a538c | ||
|
|
1fe526c99c | ||
|
|
6b05918807 | ||
|
|
05d2eb8ce0 | ||
|
|
e12c2d9769 | ||
|
|
825245b65f | ||
|
|
844dc21efd | ||
|
|
1a00e99a24 | ||
|
|
6c5bcf30ba | ||
|
|
be8ffe935d | ||
|
|
0b80bba4ec | ||
|
|
9cc38988d8 | ||
|
|
0d92a5dec8 | ||
|
|
0029e1d8f7 | ||
|
|
635aa5c681 | ||
|
|
f5c4c9448c | ||
|
|
bc43a9d329 | ||
|
|
1850c28d3a | ||
|
|
319a058e63 | ||
|
|
678e49268f | ||
|
|
6df4a6f120 | ||
|
|
73c6bbb225 | ||
|
|
fa57085946 | ||
|
|
aef589cd24 | ||
|
|
1125a5f7cb | ||
|
|
dfba1d9656 | ||
|
|
96418ed684 |
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
|
||||
15
Cargo.toml
15
Cargo.toml
@@ -23,10 +23,23 @@ members = [
|
||||
]
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0-beta"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.1.0-beta" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.1.0-beta" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.1.0-beta" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.1.0-beta" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.1.0-beta" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.1.0-beta" }
|
||||
leptos_router = { path = "./router", version = "0.1.0-beta" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.1.0-beta" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = 'z'
|
||||
|
||||
[workspace.metadata.cargo-all-features]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
@@ -28,7 +28,6 @@ gloo-net = { git = "https://github.com/rustwasm/gloo" }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
@@ -41,31 +40,31 @@ ssr = [
|
||||
stable = ["leptos/stable", "leptos_router/stable"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["actix-files", "actix-web", "leptos_actix"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
denylist = ["actix-files", "actix-web", "leptos_actix", "stable"]
|
||||
skip_feature_sets = [["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 = "counter_isomorphic"
|
||||
output-name = "counter_isomorphic"
|
||||
# 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"
|
||||
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 = "src/styles/tailwind.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
# assets-dir = "static/assets"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-address = "127.0.0.1:3000"
|
||||
site-address = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
end2end-cmd = "npx playwright test"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
@@ -86,4 +85,4 @@ 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
|
||||
lib-default-features = false
|
||||
|
||||
@@ -15,7 +15,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
view! {
|
||||
cx,
|
||||
<>
|
||||
<Stylesheet id="leptos" href="./target/site/pkg/hackernews.css"/>
|
||||
<Stylesheet id="leptos" href="/target/site/pkg/hackernews.css"/>
|
||||
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
|
||||
<Router>
|
||||
<Nav />
|
||||
|
||||
@@ -7,11 +7,11 @@ edition = "2021"
|
||||
console_log = "0.2"
|
||||
log = "0.4"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_router = { path = "../../router", features=["csr"] }
|
||||
leptos_router = { path = "../../router", features = ["csr"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
futures = "0.3"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_meta = { path = "../../meta", features = ["csr"] }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_actix"
|
||||
version = "0.1.0-beta"
|
||||
version.workspace = true
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -10,13 +10,7 @@ description = "Actix integrations for the Leptos web framework."
|
||||
[dependencies]
|
||||
actix-web = "4"
|
||||
futures = "0.3"
|
||||
leptos = { path = "../../leptos", default-features = false, version = "0.1.0-alpha", features = [
|
||||
"ssr",
|
||||
] }
|
||||
leptos_meta = { path = "../../meta", default-features = false, version = "0.1.0-alpha", features = [
|
||||
"ssr",
|
||||
] }
|
||||
leptos_router = { path = "../../router", default-features = false, version = "0.1.0-alpha", features = [
|
||||
"ssr",
|
||||
] }
|
||||
leptos = { workspace = true, features = ["ssr"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
|
||||
@@ -290,7 +290,7 @@ where IV: IntoView
|
||||
// the site was built with cargo run and not cargo-leptos
|
||||
let bundle_path = match site_root.as_ref() {
|
||||
"pkg" => "pkg".to_string(),
|
||||
_ => format!("{}/{}", site_root, pkg_path),
|
||||
_ => format!("{site_root}/{pkg_path}"),
|
||||
};
|
||||
|
||||
let leptos_autoreload = match std::env::var("LEPTOS_WATCH").is_ok() {
|
||||
@@ -358,7 +358,7 @@ where IV: IntoView
|
||||
|
||||
let res_options = res_options.0.read().await;
|
||||
|
||||
let (status, mut headers) = (res_options.status.clone(), res_options.headers.clone());
|
||||
let (status, mut headers) = (res_options.status, res_options.headers.clone());
|
||||
let status = status.unwrap_or_default();
|
||||
|
||||
let complete_stream =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_axum"
|
||||
version = "0.1.0-beta"
|
||||
version.workspace = true
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -8,20 +8,14 @@ repository = "https://github.com/gbj/leptos"
|
||||
description = "Axum integrations for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
axum = {version="0.6", features=["macros"]}
|
||||
axum = { version = "0.6", features = ["macros"] }
|
||||
derive_builder = "0.12.0"
|
||||
futures = "0.3"
|
||||
http = "0.2.8"
|
||||
hyper = "0.14.23"
|
||||
kdl = "4.6.0"
|
||||
leptos = { path = "../../leptos", default-features = false, version = "0.1.0-beta", features = [
|
||||
"ssr",
|
||||
] }
|
||||
leptos_meta = { path = "../../meta", default-features = false, version = "0.1.0-beta", features = [
|
||||
"ssr",
|
||||
] }
|
||||
leptos_router = { path = "../../router", default-features = false, version = "0.1.0-beta", features = [
|
||||
"ssr",
|
||||
] }
|
||||
leptos_config = { path = "../../leptos_config", default-features = false, version = "0.1.0-beta" }
|
||||
leptos = { workspace = true, features = ["ssr"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_config.workspace = true
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
|
||||
@@ -139,7 +139,7 @@ pub async fn handle_server_fns(
|
||||
req: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
// Axum Path extractor doesn't remove the first slash from the path, while Actix does
|
||||
let fn_name: String = match fn_name.strip_prefix("/") {
|
||||
let fn_name: String = match fn_name.strip_prefix('/') {
|
||||
Some(path) => path.to_string(),
|
||||
None => fn_name,
|
||||
};
|
||||
@@ -180,15 +180,12 @@ pub async fn handle_server_fns(
|
||||
let res_options_outer = res_options.unwrap().0;
|
||||
let res_options_inner = res_options_outer.read().await;
|
||||
let (status, mut res_headers) = (
|
||||
res_options_inner.status.clone(),
|
||||
res_options_inner.status,
|
||||
res_options_inner.headers.clone(),
|
||||
);
|
||||
|
||||
match res.headers_mut() {
|
||||
Some(header_ref) => {
|
||||
header_ref.extend(res_headers.drain());
|
||||
}
|
||||
None => (),
|
||||
if let Some(header_ref) = res.headers_mut() {
|
||||
header_ref.extend(res_headers.drain());
|
||||
};
|
||||
|
||||
if accept_header == Some("application/json")
|
||||
@@ -237,9 +234,9 @@ pub async fn handle_server_fns(
|
||||
Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body(Full::from(
|
||||
format!("Could not find a server function at the route {:?}. \
|
||||
format!("Could not find a server function at the route {fn_name}. \
|
||||
\n\nIt's likely that you need to call ServerFn::register() on the \
|
||||
server function type, somewhere in your `main` function.", fn_name)
|
||||
server function type, somewhere in your `main` function." )
|
||||
))
|
||||
}
|
||||
.expect("could not build Response");
|
||||
@@ -339,7 +336,7 @@ where
|
||||
// the site was built with cargo run and not cargo-leptos
|
||||
let bundle_path = match site_root.as_ref() {
|
||||
"pkg" => "pkg".to_string(),
|
||||
_ => format!("{}/{}", site_root, pkg_path),
|
||||
_ => format!("{site_root}/{pkg_path}"),
|
||||
};
|
||||
|
||||
let output_name = &options.output_name;
|
||||
@@ -487,10 +484,9 @@ where
|
||||
Box::pin(complete_stream) as PinnedHtmlStream
|
||||
));
|
||||
|
||||
match res_options.status {
|
||||
Some(status) => *res.status_mut() = status,
|
||||
None => (),
|
||||
};
|
||||
if let Some(status) = res_options.status {
|
||||
*res.status_mut() = status
|
||||
}
|
||||
let mut res_headers = res_options.headers.clone();
|
||||
res.headers_mut().extend(res_headers.drain());
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos"
|
||||
version = "0.1.0-beta"
|
||||
version.workspace = true
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -10,11 +10,11 @@ readme = "../README.md"
|
||||
|
||||
[dependencies]
|
||||
cfg-if = "1"
|
||||
leptos_config = { path = "../leptos_config", default-features = false, version = "0.1.0-beta" }
|
||||
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.1.0-beta" }
|
||||
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.1.0-beta" }
|
||||
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.1.0-beta" }
|
||||
leptos_server = { path = "../leptos_server", default-features = false, version = "0.1.0-beta" }
|
||||
leptos_dom.workspace = true
|
||||
leptos_macro.workspace = true
|
||||
leptos_reactive.workspace = true
|
||||
leptos_server.workspace = true
|
||||
leptos_config.workspace = true
|
||||
tracing = "0.1"
|
||||
typed-builder = "0.11"
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos_macro::component;
|
||||
use std::rc::Rc;
|
||||
use leptos_dom::{DynChild, Fragment, IntoView, Component};
|
||||
use leptos_reactive::{provide_context, Scope, SuspenseContext};
|
||||
use leptos_dom::{Component, DynChild, Fragment, IntoView};
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
use leptos_dom::{HydrationCtx, HydrationKey};
|
||||
use leptos_macro::component;
|
||||
use leptos_reactive::{provide_context, Scope, SuspenseContext};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// If any [Resources](leptos_reactive::Resource) are read in the `children` of this
|
||||
/// component, it will show the `fallback` while they are loading. Once all are resolved,
|
||||
@@ -88,8 +88,8 @@ where
|
||||
} else {
|
||||
// run the child; we'll probably throw this away, but it will register resource reads
|
||||
let child = orig_child(cx).into_view(cx);
|
||||
|
||||
let initial = {
|
||||
|
||||
let initial = {
|
||||
// no resources were read under this, so just return the child
|
||||
if context.pending_resources.get() == 0 {
|
||||
child.clone()
|
||||
@@ -97,10 +97,10 @@ where
|
||||
// show the fallback, but also prepare to stream HTML
|
||||
else {
|
||||
let orig_child = Rc::clone(&orig_child);
|
||||
|
||||
|
||||
cx.register_suspense(
|
||||
context,
|
||||
&id_before_suspense.to_string(),
|
||||
&id_before_suspense.to_string(),
|
||||
¤t_id.to_string(),
|
||||
{
|
||||
let current_id = current_id.clone();
|
||||
@@ -117,17 +117,17 @@ where
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// return the fallback for now, wrapped in fragment identifer
|
||||
fallback().into_view(cx)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
HydrationCtx::continue_from(current_id.clone());
|
||||
|
||||
|
||||
initial
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use leptos_dom::{Fragment, IntoView, View};
|
||||
use leptos_macro::component;
|
||||
use leptos_reactive::{ Scope, SignalSetter};
|
||||
use leptos_reactive::{Scope, SignalSetter};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
/// If any [Resource](leptos_reactive::Resource)s are read in the `children` of this
|
||||
@@ -66,7 +66,7 @@ pub fn Transition<F, E>(
|
||||
#[prop(optional)]
|
||||
set_pending: Option<SignalSetter<bool>>,
|
||||
/// Will be displayed once all resources have resolved.
|
||||
children: Box<dyn Fn(Scope) -> Fragment>
|
||||
children: Box<dyn Fn(Scope) -> Fragment>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn() -> E + 'static,
|
||||
@@ -78,7 +78,6 @@ where
|
||||
crate::SuspenseProps::builder()
|
||||
.fallback({
|
||||
let prev_child = Rc::clone(&prev_children);
|
||||
let set_pending = set_pending.clone();
|
||||
move || {
|
||||
if let Some(set_pending) = &set_pending {
|
||||
set_pending.set(true);
|
||||
@@ -98,6 +97,6 @@ where
|
||||
}
|
||||
frag
|
||||
}))
|
||||
.build()
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,8 +119,7 @@ impl TryFrom<String> for Env {
|
||||
"prod" => Ok(Self::PROD),
|
||||
"production" => Ok(Self::PROD),
|
||||
other => Err(format!(
|
||||
"{} is not a supported environment. Use either `dev` or `production`.",
|
||||
other
|
||||
"{other} is not a supported environment. Use either `dev` or `production`."
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_dom"
|
||||
version = "0.1.0-beta"
|
||||
version.workspace = true
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -12,12 +12,12 @@ cfg-if = "1"
|
||||
drain_filter_polyfill = "0.1"
|
||||
educe = "0.4"
|
||||
futures = "0.3"
|
||||
gloo = "0.8"
|
||||
gloo = { version = "0.8", features = ["futures"] }
|
||||
html-escape = "0.2"
|
||||
indexmap = "1.9"
|
||||
itertools = "0.10"
|
||||
js-sys = "0.3"
|
||||
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.1.0-beta" }
|
||||
leptos_reactive.workspace = true
|
||||
once_cell = "1"
|
||||
pad-adapter = "0.1"
|
||||
paste = "1"
|
||||
@@ -140,6 +140,7 @@ features = [
|
||||
]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
web = ["leptos_reactive/csr"]
|
||||
ssr = ["leptos_reactive/ssr"]
|
||||
stable = ["leptos_reactive/stable"]
|
||||
|
||||
@@ -6,8 +6,10 @@ edition = "2021"
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1"
|
||||
gloo = { version = "0.8", features = ["futures"] }
|
||||
leptos = { path = "../../../leptos" }
|
||||
leptos = { path = "../../../leptos", features = ["tracing"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
web-sys = "0.3"
|
||||
web-sys = "0.3"
|
||||
|
||||
[workspace]
|
||||
|
||||
@@ -6,6 +6,7 @@ extern crate tracing;
|
||||
mod utils;
|
||||
|
||||
use leptos::*;
|
||||
use tracing::field::debug;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
fn main() {
|
||||
@@ -42,6 +43,7 @@ fn view_fn(cx: Scope) -> impl IntoView {
|
||||
let (is_a, set_is_a) = create_signal(cx, true);
|
||||
|
||||
let handle_toggle = move |_| {
|
||||
trace!("toggling");
|
||||
if is_a() {
|
||||
set_b(a());
|
||||
|
||||
@@ -58,6 +60,7 @@ fn view_fn(cx: Scope) -> impl IntoView {
|
||||
<div>
|
||||
<button on:click=handle_toggle>"Toggle"</button>
|
||||
</div>
|
||||
<Example/>
|
||||
<A child=Signal::from(a) />
|
||||
<A child=Signal::from(b) />
|
||||
</>
|
||||
@@ -71,7 +74,26 @@ fn A(cx: Scope, child: Signal<View>) -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
fn Example(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
trace!("rendering <Example/>");
|
||||
|
||||
let (value, set_value) = create_signal(cx, 10);
|
||||
|
||||
let memo = create_memo(cx, move |_| value() * 2);
|
||||
let derived = Signal::derive(cx, move || {
|
||||
value() * 3
|
||||
});
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
trace!("logging value of derived..., {}", derived.get());
|
||||
});
|
||||
|
||||
|
||||
set_timeout(move || { set_value.update(|v| *v += 1)}, std::time::Duration::from_millis(50));
|
||||
|
||||
view! { cx,
|
||||
<h1>"Example"</h1>
|
||||
<button on:click=move |_| set_value.update(|value| *value += 1)>
|
||||
"Click me"
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,10 +154,7 @@ where
|
||||
let component = DynChildRepr::new();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let (frag, closing) = (
|
||||
component.document_fragment.clone(),
|
||||
component.closing.node.clone(),
|
||||
);
|
||||
let closing = component.closing.node.clone();
|
||||
|
||||
let child = component.child.clone();
|
||||
|
||||
@@ -189,7 +186,9 @@ where
|
||||
// or to reuse it in the case of a text node
|
||||
|
||||
// TODO check does this still detect moves correctly?
|
||||
let was_child_moved = prev_t.is_none() && child.get_closing_node().next_sibling().as_ref() != Some(&closing);
|
||||
let was_child_moved = prev_t.is_none()
|
||||
&& child.get_closing_node().next_sibling().as_ref()
|
||||
!= Some(&closing);
|
||||
|
||||
// If the previous child was a text node, we would like to
|
||||
// make use of it again if our current child is also a text
|
||||
|
||||
@@ -41,7 +41,7 @@ cfg_if! {
|
||||
use smallvec::SmallVec;
|
||||
use std::{borrow::Cow, cell::RefCell, fmt, hash::Hash, ops::Deref, rc::Rc};
|
||||
|
||||
/// The internal representation of the [`EachKey`] core-component.
|
||||
/// The internal representation of the [`Each`] core-component.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct EachRepr {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
@@ -156,7 +156,7 @@ impl Mountable for EachRepr {
|
||||
}
|
||||
}
|
||||
|
||||
/// The internal representation of an [`EachKey`] item.
|
||||
/// The internal representation of an [`Each`] item.
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub(crate) struct EachItem {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
@@ -293,7 +293,7 @@ where
|
||||
K: Eq + Hash + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
/// Creates a new [`EachKey`] component.
|
||||
/// Creates a new [`Each`] component.
|
||||
pub fn new(items_fn: IF, key_fn: KF, each_fn: EF) -> Self {
|
||||
Self {
|
||||
items_fn,
|
||||
|
||||
@@ -16,10 +16,20 @@ thread_local! {
|
||||
pub fn add_event_listener<E>(
|
||||
target: &web_sys::Element,
|
||||
event_name: Cow<'static, str>,
|
||||
cb: impl FnMut(E) + 'static,
|
||||
mut cb: impl FnMut(E) + 'static,
|
||||
) where
|
||||
E: FromWasmAbi + 'static,
|
||||
{
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |e| {
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
|
||||
let key = event_delegation_key(&event_name);
|
||||
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
|
||||
@@ -31,10 +41,20 @@ pub fn add_event_listener<E>(
|
||||
pub fn add_event_listener_undelegated<E>(
|
||||
target: &web_sys::Element,
|
||||
event_name: &str,
|
||||
cb: impl FnMut(E) + 'static,
|
||||
mut cb: impl FnMut(E) + 'static,
|
||||
) where
|
||||
E: FromWasmAbi + 'static,
|
||||
{
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |e| {
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let event_name = intern(event_name);
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
|
||||
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
|
||||
@@ -97,7 +117,20 @@ pub(crate) fn add_delegated_event_listener(event_name: Cow<'static, str>) {
|
||||
}
|
||||
};
|
||||
|
||||
crate::window_event_listener(&event_name, handler);
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let handler = move |e| {
|
||||
let _guard = span.enter();
|
||||
handler(e);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let handler = Box::new(handler) as Box<dyn FnMut(web_sys::Event)>;
|
||||
let handler = Closure::wrap(handler).into_js_value();
|
||||
_ = crate::window()
|
||||
.add_event_listener_with_callback(&event_name, handler.unchecked_ref());
|
||||
|
||||
// register that we've created handler
|
||||
events.insert(event_name);
|
||||
|
||||
@@ -71,21 +71,57 @@ pub fn event_target_checked(ev: &web_sys::Event) -> bool {
|
||||
|
||||
/// Runs the given function between the next repaint
|
||||
/// using [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
|
||||
pub fn request_animation_frame(cb: impl FnMut() + 'static) {
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut()>).into_js_value();
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
|
||||
pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::once_into_js(cb);
|
||||
_ = window().request_animation_frame(cb.as_ref().unchecked_ref());
|
||||
}
|
||||
|
||||
/// Queues the given function during an idle period
|
||||
/// using [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback).
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
|
||||
pub fn request_idle_callback(cb: impl Fn() + 'static) {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
|
||||
_ = window().request_idle_callback(cb.as_ref().unchecked_ref());
|
||||
}
|
||||
|
||||
/// Executes the given function after the given duration of time has passed.
|
||||
/// [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout).
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", skip_all, fields(duration = ?duration))
|
||||
)]
|
||||
pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::once_into_js(Box::new(cb) as Box<dyn FnOnce()>);
|
||||
_ = window().set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
cb.as_ref().unchecked_ref(),
|
||||
@@ -107,10 +143,24 @@ impl IntervalHandle {
|
||||
|
||||
/// Repeatedly calls the given function, with a delay of the given duration between calls.
|
||||
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", skip_all, fields(duration = ?duration))
|
||||
)]
|
||||
pub fn set_interval(
|
||||
cb: impl Fn() + 'static,
|
||||
duration: Duration,
|
||||
) -> Result<IntervalHandle, JsValue> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
|
||||
let handle = window()
|
||||
.set_interval_with_callback_and_timeout_and_arguments_0(
|
||||
@@ -121,10 +171,24 @@ pub fn set_interval(
|
||||
}
|
||||
|
||||
/// Adds an event listener to the `Window`.
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", skip_all, fields(event_name = %event_name))
|
||||
)]
|
||||
pub fn window_event_listener(
|
||||
event_name: &str,
|
||||
cb: impl Fn(web_sys::Event) + 'static,
|
||||
) {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |e| {
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if !is_server() {
|
||||
let handler = Box::new(cb) as Box<dyn FnMut(web_sys::Event)>;
|
||||
|
||||
|
||||
@@ -90,7 +90,12 @@ where
|
||||
element: el,
|
||||
};
|
||||
|
||||
HtmlElement { cx, element }
|
||||
HtmlElement {
|
||||
cx,
|
||||
element,
|
||||
#[cfg(debug_assertions)]
|
||||
span: ::tracing::Span::current()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
@@ -192,6 +197,8 @@ cfg_if! {
|
||||
/// Represents an HTML element.
|
||||
#[derive(Clone)]
|
||||
pub struct HtmlElement<El: ElementDescriptor> {
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) span: ::tracing::Span,
|
||||
pub(crate) cx: Scope,
|
||||
pub(crate) element: El,
|
||||
}
|
||||
@@ -229,13 +236,15 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<El: ElementDescriptor> HtmlElement<El> {
|
||||
impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
fn new(cx: Scope, element: El) -> Self {
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
Self {
|
||||
cx,
|
||||
element,
|
||||
#[cfg(debug_assertions)]
|
||||
span: ::tracing::Span::current()
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
@@ -272,6 +281,8 @@ impl<El: ElementDescriptor> HtmlElement<El> {
|
||||
let Self {
|
||||
cx,
|
||||
element,
|
||||
#[cfg(debug_assertions)]
|
||||
span
|
||||
} = self;
|
||||
|
||||
HtmlElement {
|
||||
@@ -281,6 +292,8 @@ impl<El: ElementDescriptor> HtmlElement<El> {
|
||||
element: element.as_ref().clone(),
|
||||
is_void: element.is_void(),
|
||||
},
|
||||
#[cfg(debug_assertions)]
|
||||
span
|
||||
}
|
||||
} else {
|
||||
let Self {
|
||||
@@ -346,6 +359,71 @@ impl<El: ElementDescriptor> HtmlElement<El> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Runs the callback when this element has been mounted to the DOM.
|
||||
///
|
||||
/// ### Important Note
|
||||
/// This method will only ever run at most once. If this element
|
||||
/// is unmounted and remounted, or moved somewhere else, it will not
|
||||
/// re-run unless you call this method again.
|
||||
pub fn on_mount(self, f: impl FnOnce(Self) + 'static) -> Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
use futures::future::poll_fn;
|
||||
use once_cell::unsync::OnceCell;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
rc::Rc,
|
||||
task::{Poll, Waker},
|
||||
};
|
||||
|
||||
let this = self.clone();
|
||||
let el = self.element.as_ref().clone();
|
||||
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
while !crate::document().body().unwrap().contains(Some(&el)) {
|
||||
// We need to cook ourselves a small future that resolves
|
||||
// when the next animation frame is available
|
||||
let waker = Rc::new(RefCell::new(None::<Waker>));
|
||||
let ready = Rc::new(OnceCell::new());
|
||||
|
||||
crate::request_animation_frame({
|
||||
let waker = waker.clone();
|
||||
let ready = ready.clone();
|
||||
|
||||
move || {
|
||||
let _ = ready.set(());
|
||||
if let Some(waker) = &*waker.borrow() {
|
||||
waker.wake_by_ref();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for the animation frame to become available
|
||||
poll_fn(move |cx| {
|
||||
let mut waker_borrow = waker.borrow_mut();
|
||||
|
||||
*waker_borrow = Some(cx.waker().clone());
|
||||
|
||||
if ready.get().is_some() {
|
||||
Poll::Ready(())
|
||||
} else {
|
||||
Poll::<()>::Pending
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
f(this);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
{
|
||||
let _ = f;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an attribute to this element.
|
||||
#[track_caller]
|
||||
pub fn attr(
|
||||
@@ -506,10 +584,22 @@ impl<El: ElementDescriptor> HtmlElement<El> {
|
||||
pub fn on<E: EventDescriptor + 'static>(
|
||||
self,
|
||||
event: E,
|
||||
event_handler: impl FnMut(E::EventType) + 'static,
|
||||
#[allow(unused_mut)] // used for tracing in debug
|
||||
mut event_handler: impl FnMut(E::EventType) + 'static,
|
||||
) -> Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let onspan = ::tracing::span!(
|
||||
parent: &self.span,
|
||||
::tracing::Level::TRACE,
|
||||
"on",
|
||||
event = %event.name()
|
||||
);
|
||||
let _onguard = onspan.enter();
|
||||
}
|
||||
}
|
||||
let event_name = event.name();
|
||||
|
||||
if event.bubbles() {
|
||||
@@ -618,9 +708,9 @@ impl<El: ElementDescriptor> IntoView for HtmlElement<El> {
|
||||
let children = children;
|
||||
|
||||
if attrs.iter_mut().any(|(name, _)| name == "id") {
|
||||
attrs.push(("leptos-hk".into(), format!("_{}", id).into()));
|
||||
attrs.push(("leptos-hk".into(), format!("_{id}").into()));
|
||||
} else {
|
||||
attrs.push(("id".into(), format!("_{}", id).into()));
|
||||
attrs.push(("id".into(), format!("_{id}").into()));
|
||||
}
|
||||
|
||||
element.attrs = attrs;
|
||||
@@ -800,6 +890,17 @@ macro_rules! generate_html_tags {
|
||||
}
|
||||
|
||||
#[$meta]
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "HtmlElement",
|
||||
skip_all,
|
||||
fields(
|
||||
tag = %format!("<{}/>", stringify!($tag))
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn $tag(cx: Scope) -> HtmlElement<[<$tag:camel $($trailing_)?>]> {
|
||||
HtmlElement::new(cx, [<$tag:camel $($trailing_)?>]::default())
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ use std::{cell::RefCell, fmt::Display};
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use once_cell::unsync::Lazy as LazyCell;
|
||||
|
||||
/// We can tell if we start in hydration mode by checking to see if the
|
||||
/// id "_0" is present in the DOM. If it is, we know we are hydrating from
|
||||
/// the server, if not, we are starting off in CSR
|
||||
// We can tell if we start in hydration mode by checking to see if the
|
||||
// id "_0-0-0" is present in the DOM. If it is, we know we are hydrating from
|
||||
// the server, if not, we are starting off in CSR
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
thread_local! {
|
||||
static IS_HYDRATING: RefCell<LazyCell<bool>> = RefCell::new(LazyCell::new(|| {
|
||||
|
||||
@@ -192,7 +192,7 @@ cfg_if! {
|
||||
}
|
||||
|
||||
impl Element {
|
||||
/// Converts this leptos [`Element`] into [`HtmlElement<AnyElement`].
|
||||
/// Converts this leptos [`Element`] into [`HtmlElement<AnyElement>`].
|
||||
pub fn into_html_element(self, cx: Scope) -> HtmlElement<AnyElement> {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
@@ -206,7 +206,12 @@ impl Element {
|
||||
is_void: false,
|
||||
};
|
||||
|
||||
HtmlElement { cx, element }
|
||||
HtmlElement {
|
||||
cx,
|
||||
element,
|
||||
#[cfg(debug_assertions)]
|
||||
span: ::tracing::Span::current()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
@@ -327,7 +332,8 @@ pub struct Text {
|
||||
/// to possibly reuse a previous node.
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
node: web_sys::Node,
|
||||
content: Cow<'static, str>,
|
||||
/// The current contents of the text node.
|
||||
pub content: Cow<'static, str>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Text {
|
||||
@@ -541,8 +547,19 @@ impl View {
|
||||
pub fn on<E: ev::EventDescriptor + 'static>(
|
||||
self,
|
||||
event: E,
|
||||
event_handler: impl FnMut(E::EventType) + 'static,
|
||||
mut event_handler: impl FnMut(E::EventType) + 'static,
|
||||
) -> Self {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
trace!("calling on() {}", event.name());
|
||||
let span = ::tracing::Span::current();
|
||||
let event_handler = move |e| {
|
||||
let _guard = span.enter();
|
||||
event_handler(e);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
self.on_impl(event, Box::new(event_handler))
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ macro_rules! debug_warn {
|
||||
/// or via `println!()` (if not in the browser).
|
||||
pub fn console_log(s: &str) {
|
||||
if is_server() {
|
||||
println!("{}", s);
|
||||
println!("{s}");
|
||||
} else {
|
||||
web_sys::console::log_1(&JsValue::from_str(s));
|
||||
}
|
||||
@@ -55,7 +55,7 @@ pub fn console_log(s: &str) {
|
||||
/// or via `println!()` (if not in the browser).
|
||||
pub fn console_warn(s: &str) {
|
||||
if is_server() {
|
||||
eprintln!("{}", s);
|
||||
eprintln!("{s}");
|
||||
} else {
|
||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||
}
|
||||
@@ -65,7 +65,7 @@ pub fn console_warn(s: &str) {
|
||||
/// or via `println!()` (if not in the browser).
|
||||
pub fn console_error(s: &str) {
|
||||
if is_server() {
|
||||
eprintln!("{}", s);
|
||||
eprintln!("{s}");
|
||||
} else {
|
||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||
}
|
||||
@@ -77,7 +77,7 @@ pub fn console_debug_warn(s: &str) {
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
if is_server() {
|
||||
eprintln!("{}", s);
|
||||
eprintln!("{s}");
|
||||
} else {
|
||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_macro"
|
||||
version = "0.1.0-beta"
|
||||
version.workspace = true
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -22,15 +22,15 @@ quote = "1"
|
||||
syn = { version = "1", features = ["full"] }
|
||||
syn-rsx = "0.9"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
leptos_dom = { path = "../leptos_dom", version = "0.1.0-beta" }
|
||||
leptos_reactive = { path = "../leptos_reactive", version = "0.1.0-beta" }
|
||||
leptos_server = { path = "../leptos_server", version = "0.1.0-beta" }
|
||||
leptos_dom.workspace = true
|
||||
leptos_reactive.workspace = true
|
||||
leptos_server.workspace = true
|
||||
lazy_static = "1.4"
|
||||
|
||||
[dev-dependencies]
|
||||
log = "0.4"
|
||||
typed-builder = "0.10"
|
||||
leptos = { path = "../leptos" }
|
||||
leptos.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["ssr"]
|
||||
|
||||
@@ -176,6 +176,25 @@ mod server;
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// However, you can pass arbitrary class names using the syntax `class=("name", value)`.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// let (count, set_count) = create_signal(cx, 2);
|
||||
/// // this allows you to use CSS frameworks that include complex class names
|
||||
/// view! { cx,
|
||||
/// <div
|
||||
/// class=("is-[this_-_really]-necessary-42", move || count() < 3)
|
||||
/// >
|
||||
/// "Now you see me, now you don’t."
|
||||
/// </div>
|
||||
/// }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// 8. You can use the `_ref` attribute to store a reference to its DOM element in a
|
||||
/// [NodeRef](leptos_reactive::NodeRef) to use later.
|
||||
/// ```rust
|
||||
@@ -358,12 +377,12 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
/// ```
|
||||
///
|
||||
/// 5. You can access the children passed into the component with the `children` property, which takes
|
||||
/// an argument of the form `Box<dyn Fn(Scope) -> Fragment>`.
|
||||
/// an argument of the form `Box<dyn FnOnce(Scope) -> Fragment>`.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// #[component]
|
||||
/// fn ComponentWithChildren(cx: Scope, children: Box<dyn Fn(Scope) -> Fragment>) -> impl IntoView {
|
||||
/// fn ComponentWithChildren(cx: Scope, children: Box<dyn FnOnce(Scope) -> Fragment>) -> impl IntoView {
|
||||
/// view! {
|
||||
/// cx,
|
||||
/// <ul>
|
||||
|
||||
@@ -105,11 +105,11 @@ mod struct_info {
|
||||
builder_attr,
|
||||
builder_name: syn::Ident::new(&builder_name, proc_macro2::Span::call_site()),
|
||||
conversion_helper_trait_name: syn::Ident::new(
|
||||
&format!("{}_Optional", builder_name),
|
||||
&format!("{builder_name}_Optional"),
|
||||
proc_macro2::Span::call_site(),
|
||||
),
|
||||
core: syn::Ident::new(
|
||||
&format!("{}_core", builder_name),
|
||||
&format!("{builder_name}_core"),
|
||||
proc_macro2::Span::call_site(),
|
||||
),
|
||||
})
|
||||
@@ -282,7 +282,7 @@ mod struct_info {
|
||||
});
|
||||
let reconstructing = self.included_fields().map(|f| f.name);
|
||||
|
||||
let &FieldInfo {
|
||||
let FieldInfo {
|
||||
name: ref field_name,
|
||||
ty: ref field_type,
|
||||
..
|
||||
@@ -391,7 +391,7 @@ mod struct_info {
|
||||
),
|
||||
proc_macro2::Span::call_site(),
|
||||
);
|
||||
let repeated_fields_error_message = format!("Repeated field {}", field_name);
|
||||
let repeated_fields_error_message = format!("Repeated field {field_name}");
|
||||
|
||||
Ok(quote! {
|
||||
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||
@@ -513,7 +513,7 @@ mod struct_info {
|
||||
),
|
||||
proc_macro2::Span::call_site(),
|
||||
);
|
||||
let early_build_error_message = format!("Missing required field {}", field_name);
|
||||
let early_build_error_message = format!("Missing required field {field_name}");
|
||||
|
||||
Ok(quote! {
|
||||
#[doc(hidden)]
|
||||
@@ -622,7 +622,7 @@ mod struct_info {
|
||||
// I'd prefer “a” or “an” to “its”, but determining which is grammatically
|
||||
// correct is roughly impossible.
|
||||
let doc =
|
||||
format!("Finalise the builder and create its [`{}`] instance", name);
|
||||
format!("Finalise the builder and create its [`{name}`] instance");
|
||||
quote!(#[doc = #doc])
|
||||
}
|
||||
}
|
||||
@@ -718,7 +718,7 @@ mod struct_info {
|
||||
}
|
||||
_ => Err(Error::new_spanned(
|
||||
&assign,
|
||||
format!("Unknown parameter {:?}", name),
|
||||
format!("Unknown parameter {name:?}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -732,7 +732,7 @@ mod struct_info {
|
||||
}
|
||||
_ => Err(Error::new_spanned(
|
||||
&path,
|
||||
format!("Unknown parameter {:?}", name),
|
||||
format!("Unknown parameter {name:?}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -747,7 +747,7 @@ mod struct_info {
|
||||
let call_func = quote!(#call_func);
|
||||
Error::new_spanned(
|
||||
&call.func,
|
||||
format!("Illegal builder setting group {}", call_func),
|
||||
format!("Illegal builder setting group {call_func}"),
|
||||
)
|
||||
})?;
|
||||
match subsetting_name.as_str() {
|
||||
@@ -759,7 +759,7 @@ mod struct_info {
|
||||
}
|
||||
_ => Err(Error::new_spanned(
|
||||
&call.func,
|
||||
format!("Illegal builder setting group name {}", subsetting_name),
|
||||
format!("Illegal builder setting group name {subsetting_name}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -924,7 +924,7 @@ mod field_info {
|
||||
let tokenized_code = TokenStream::from_str(&code.value())?;
|
||||
self.default = Some(
|
||||
syn::parse(tokenized_code.into())
|
||||
.map_err(|e| Error::new_spanned(code, format!("{}", e)))?,
|
||||
.map_err(|e| Error::new_spanned(code, format!("{e}")))?,
|
||||
);
|
||||
} else {
|
||||
return Err(Error::new_spanned(assign.right, "Expected string"));
|
||||
@@ -933,7 +933,7 @@ mod field_info {
|
||||
}
|
||||
_ => Err(Error::new_spanned(
|
||||
&assign,
|
||||
format!("Unknown parameter {:?}", name),
|
||||
format!("Unknown parameter {name:?}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -950,7 +950,7 @@ mod field_info {
|
||||
}
|
||||
_ => Err(Error::new_spanned(
|
||||
&path,
|
||||
format!("Unknown parameter {:?}", name),
|
||||
format!("Unknown parameter {name:?}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -965,7 +965,7 @@ mod field_info {
|
||||
let call_func = quote!(#call_func);
|
||||
Error::new_spanned(
|
||||
&call.func,
|
||||
format!("Illegal builder setting group {}", call_func),
|
||||
format!("Illegal builder setting group {call_func}"),
|
||||
)
|
||||
})?;
|
||||
match subsetting_name.as_ref() {
|
||||
@@ -977,7 +977,7 @@ mod field_info {
|
||||
}
|
||||
_ => Err(Error::new_spanned(
|
||||
&call.func,
|
||||
format!("Illegal builder setting group name {}", subsetting_name),
|
||||
format!("Illegal builder setting group name {subsetting_name}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -1047,7 +1047,7 @@ mod field_info {
|
||||
}
|
||||
_ => Err(Error::new_spanned(
|
||||
&assign,
|
||||
format!("Unknown parameter {:?}", name),
|
||||
format!("Unknown parameter {name:?}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
||||
use proc_macro::Span;
|
||||
let span = Span::call_site();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let url = format!("{}/{}", span.source_file().path().to_string_lossy(), fn_name_as_str).replace("/", "-");
|
||||
let url = format!("{}/{}", span.source_file().path().to_string_lossy(), fn_name_as_str).replace('/', "-");
|
||||
#[cfg(target_os = "windows")]
|
||||
let url = format!("{}/{}", span.source_file().path().to_string_lossy(), fn_name_as_str).replace("\\", "-");
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{format_ident, quote, quote_spanned};
|
||||
use syn::{spanned::Spanned, ExprPath};
|
||||
use syn::{spanned::Spanned, ExprPath, Expr, ExprLit, Lit};
|
||||
use syn_rsx::{Node, NodeAttribute, NodeElement, NodeName};
|
||||
|
||||
use crate::{is_component_node, Mode};
|
||||
@@ -412,7 +412,11 @@ fn set_class_attribute_ssr(
|
||||
if a.value.as_ref().and_then(value_to_string).is_some() {
|
||||
None
|
||||
} else {
|
||||
Some((a.key.span(), &a.value))
|
||||
if fancy_class_name(&a.key.to_string(), cx, a).is_some() {
|
||||
None
|
||||
} else {
|
||||
Some((a.key.span(), &a.value))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
@@ -429,6 +433,14 @@ fn set_class_attribute_ssr(
|
||||
.filter_map(|node| {
|
||||
if let Node::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
if name == "class" {
|
||||
return if let Some((_, name, value)) = fancy_class_name(&name, &cx, &node) {
|
||||
let span = node.key.span();
|
||||
Some((span, name, value))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
if name.starts_with("class:") || name.starts_with("class-") {
|
||||
let name = if name.starts_with("class:") {
|
||||
name.replacen("class:", "", 1)
|
||||
@@ -752,6 +764,12 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
|
||||
}
|
||||
} else {
|
||||
let name = name.replacen("attr:", "", 1);
|
||||
|
||||
if let Some((fancy, _, _)) = fancy_class_name(&name, &cx, &node) {
|
||||
return fancy;
|
||||
}
|
||||
|
||||
// all other attributes
|
||||
let value = match node.value.as_ref() {
|
||||
Some(value) => {
|
||||
let value = value.as_ref();
|
||||
@@ -866,7 +884,11 @@ fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
|
||||
.last()
|
||||
.map(|segment| segment.ident.clone())
|
||||
.expect("element needs to have a name"),
|
||||
NodeName::Block(_) => panic!("blocks not allowed in tag-name position"),
|
||||
NodeName::Block(_) => {
|
||||
let span = tag_name.span();
|
||||
proc_macro_error::emit_error!(span, "blocks not allowed in tag-name position");
|
||||
Ident::new("", span)
|
||||
}
|
||||
_ => Ident::new(
|
||||
&tag_name.to_string().replace(['-', ':'], "_"),
|
||||
tag_name.span(),
|
||||
@@ -1044,3 +1066,44 @@ fn parse_event(event_name: &str) -> (&str, bool) {
|
||||
(event_name, false)
|
||||
}
|
||||
}
|
||||
|
||||
fn fancy_class_name<'a>(name: &str, cx: &Ident, node: &'a NodeAttribute) -> Option<(TokenStream, String, &'a Expr)> {
|
||||
// special case for complex class names:
|
||||
// e.g., Tailwind `class=("mt-[calc(100vh_-_3rem)]", true)`
|
||||
if name == "class" {
|
||||
if let Some(expr) = node.value.as_ref() {
|
||||
if let syn::Expr::Tuple(tuple) = expr.as_ref() {
|
||||
if tuple.elems.len() == 2 {
|
||||
let span = node.key.span();
|
||||
let class = quote_spanned! {
|
||||
span => .class
|
||||
};
|
||||
let class_name = &tuple.elems[0];
|
||||
let class_name = if let Expr::Lit(ExprLit { lit: Lit::Str(s), .. }) = class_name {
|
||||
s.value()
|
||||
} else {
|
||||
proc_macro_error::emit_error!(
|
||||
class_name.span(),
|
||||
"class name must be a string literal"
|
||||
);
|
||||
Default::default()
|
||||
};
|
||||
let value = &tuple.elems[1];
|
||||
return Some((
|
||||
quote! {
|
||||
#class(#class_name, (#cx, #value))
|
||||
},
|
||||
class_name,
|
||||
value
|
||||
))
|
||||
} else {
|
||||
proc_macro_error::emit_error!(
|
||||
tuple.span(),
|
||||
"class tuples must have two elements."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_reactive"
|
||||
version = "0.1.0-beta"
|
||||
version.workspace = true
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -20,6 +20,7 @@ serde_json = "1"
|
||||
base64 = "0.13"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["rt"], optional = true }
|
||||
tracing = "0.1"
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
web-sys = { version = "0.3", features = [
|
||||
@@ -47,4 +48,29 @@ miniserde = ["dep:miniserde"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable"]
|
||||
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"csr",
|
||||
"ssr",
|
||||
],
|
||||
[
|
||||
"csr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"ssr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"serde-lite",
|
||||
],
|
||||
[
|
||||
"serde-lite",
|
||||
"miniserde",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"miniserde",
|
||||
],
|
||||
]
|
||||
|
||||
@@ -47,13 +47,26 @@ use std::fmt::Debug;
|
||||
/// # }
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
scope = ?cx.id,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn create_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static)
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
cfg_if! {
|
||||
if #[cfg(not(feature = "ssr"))] {
|
||||
create_isomorphic_effect(cx, f);
|
||||
let e = cx.runtime.create_effect(f);
|
||||
cx.with_scope_property(|prop| prop.push(ScopeProperty::Effect(e)))
|
||||
} else {
|
||||
// clear warnings
|
||||
_ = cx;
|
||||
@@ -88,6 +101,18 @@ where
|
||||
/// });
|
||||
/// # assert_eq!(b(), 2);
|
||||
/// # }).dispose();
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
scope = ?cx.id,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn create_isomorphic_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static)
|
||||
where
|
||||
T: 'static,
|
||||
@@ -97,6 +122,17 @@ where
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
scope = ?cx.id,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn create_render_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static)
|
||||
where
|
||||
T: 'static,
|
||||
@@ -116,6 +152,8 @@ where
|
||||
{
|
||||
pub(crate) f: F,
|
||||
pub(crate) value: RefCell<Option<T>>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) defined_at: &'static std::panic::Location<'static>,
|
||||
}
|
||||
|
||||
pub(crate) trait AnyEffect {
|
||||
@@ -127,6 +165,19 @@ where
|
||||
T: 'static,
|
||||
F: Fn(Option<T>) -> T,
|
||||
{
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
name = "Effect::run()",
|
||||
level = "debug",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn run(&self, id: EffectId, runtime: RuntimeId) {
|
||||
with_runtime(runtime, |runtime| {
|
||||
// clear previous dependencies
|
||||
@@ -162,6 +213,17 @@ impl EffectId {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
name = "Effect::cleanup()",
|
||||
level = "debug",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self,
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub(crate) fn cleanup(&self, runtime: &Runtime) {
|
||||
let sources = runtime.effect_sources.borrow();
|
||||
if let Some(sources) = sources.get(*self) {
|
||||
|
||||
@@ -66,11 +66,13 @@
|
||||
//! });
|
||||
//! ```
|
||||
|
||||
#[cfg_attr(debug_assertions, macro_use)]
|
||||
pub extern crate tracing;
|
||||
|
||||
mod context;
|
||||
mod effect;
|
||||
mod hydration;
|
||||
mod memo;
|
||||
|
||||
mod resource;
|
||||
mod runtime;
|
||||
mod scope;
|
||||
|
||||
@@ -54,6 +54,16 @@ use std::fmt::Debug;
|
||||
/// });
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
cx = ?cx.id,
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn create_memo<T>(cx: Scope, f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
|
||||
where
|
||||
T: PartialEq + Debug + 'static,
|
||||
@@ -115,7 +125,10 @@ where
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Memo<T>(pub(crate) ReadSignal<Option<T>>)
|
||||
pub struct Memo<T>(
|
||||
pub(crate) ReadSignal<Option<T>>,
|
||||
#[cfg(debug_assertions)] pub(crate) &'static std::panic::Location<'static>,
|
||||
)
|
||||
where
|
||||
T: 'static;
|
||||
|
||||
@@ -124,13 +137,30 @@ where
|
||||
T: 'static,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0)
|
||||
Self(
|
||||
self.0,
|
||||
#[cfg(debug_assertions)]
|
||||
self.1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Copy for Memo<T> {}
|
||||
|
||||
impl<T> UntrackedGettableSignal<T> for Memo<T> {
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "Memo::get_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.0.id,
|
||||
defined_at = %self.1,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn get_untracked(&self) -> T
|
||||
where
|
||||
T: Clone,
|
||||
@@ -140,6 +170,19 @@ impl<T> UntrackedGettableSignal<T> for Memo<T> {
|
||||
self.0.get_untracked().unwrap()
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "Memo::with_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.0.id,
|
||||
defined_at = %self.1,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||
// Unwrapping here is fine for the same reasons as <Memo as
|
||||
// UntrackedSignal>::get_untracked
|
||||
@@ -167,6 +210,18 @@ where
|
||||
/// # }).dispose();
|
||||
/// #
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
name = "Memo::get()",
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.0.id,
|
||||
defined_at = %self.1
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn get(&self) -> T
|
||||
where
|
||||
T: Clone,
|
||||
@@ -194,6 +249,19 @@ where
|
||||
/// # }).dispose();
|
||||
/// #
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
name = "Memo::with()",
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.0.id,
|
||||
defined_at = %self.1,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn with<U>(&self, f: impl FnOnce(&T) -> U) -> U {
|
||||
// okay to unwrap here, because the value will *always* have initially
|
||||
// been set by the effect, synchronously
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
use crate::{
|
||||
create_effect, create_isomorphic_effect, create_memo, create_signal, queue_microtask,
|
||||
runtime::{with_runtime, RuntimeId},
|
||||
serialization::Serializable,
|
||||
spawn::spawn_local,
|
||||
use_context, Memo, ReadSignal, Scope, ScopeProperty, SuspenseContext, WriteSignal,
|
||||
};
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::{Cell, RefCell},
|
||||
@@ -8,13 +15,6 @@ use std::{
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
};
|
||||
use crate::{
|
||||
create_effect, create_isomorphic_effect, create_memo, create_signal, queue_microtask,
|
||||
runtime::{with_runtime, RuntimeId},
|
||||
serialization::Serializable,
|
||||
spawn::spawn_local,
|
||||
use_context, Memo, ReadSignal, Scope, ScopeProperty, SuspenseContext, WriteSignal,
|
||||
};
|
||||
|
||||
/// Creates [Resource](crate::Resource), which is a signal that reflects the
|
||||
/// current state of an asynchronous task, allowing you to integrate `async`
|
||||
@@ -83,6 +83,19 @@ where
|
||||
/// output type of the Future to be [Serializable]. If your output cannot be
|
||||
/// serialized, or you just want to make sure the [Future] runs locally, use
|
||||
/// [create_local_resource_with_initial_value()].
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
scope = ?cx.id,
|
||||
ty = %std::any::type_name::<T>(),
|
||||
signal_ty = %std::any::type_name::<S>(),
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn create_resource_with_initial_value<S, T, Fu>(
|
||||
cx: Scope,
|
||||
source: impl Fn() -> S + 'static,
|
||||
@@ -133,6 +146,8 @@ where
|
||||
id,
|
||||
source_ty: PhantomData,
|
||||
out_ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +202,19 @@ where
|
||||
/// Unlike [create_resource_with_initial_value()], this [Future] will always run
|
||||
/// on the local system and therefore its output type does not need to be
|
||||
/// [Serializable].
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
scope = ?cx.id,
|
||||
ty = %std::any::type_name::<T>(),
|
||||
signal_ty = %std::any::type_name::<S>(),
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn create_local_resource_with_initial_value<S, T, Fu>(
|
||||
cx: Scope,
|
||||
source: impl Fn() -> S + 'static,
|
||||
@@ -237,6 +265,8 @@ where
|
||||
id,
|
||||
source_ty: PhantomData,
|
||||
out_ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,6 +460,8 @@ where
|
||||
pub(crate) id: ResourceId,
|
||||
pub(crate) source_ty: PhantomData<S>,
|
||||
pub(crate) out_ty: PhantomData<T>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) defined_at: &'static std::panic::Location<'static>,
|
||||
}
|
||||
|
||||
// Resources
|
||||
@@ -449,6 +481,8 @@ where
|
||||
id: self.id,
|
||||
source_ty: PhantomData,
|
||||
out_ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: self.defined_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -635,7 +669,9 @@ where
|
||||
})
|
||||
});
|
||||
Box::pin(async move {
|
||||
rx.next().await.expect("failed while trying to resolve Resource serializer")
|
||||
rx.next()
|
||||
.await
|
||||
.expect("failed while trying to resolve Resource serializer")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,7 @@ impl RuntimeId {
|
||||
ret
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn create_signal<T>(self, value: T) -> (ReadSignal<T>, WriteSignal<T>)
|
||||
where
|
||||
T: Any + 'static,
|
||||
@@ -127,11 +128,15 @@ impl RuntimeId {
|
||||
runtime: self,
|
||||
id,
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller()
|
||||
},
|
||||
WriteSignal {
|
||||
runtime: self,
|
||||
id,
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller()
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -150,17 +155,25 @@ impl RuntimeId {
|
||||
runtime: self,
|
||||
id,
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller()
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn create_effect<T>(self, f: impl Fn(Option<T>) -> T + 'static) -> EffectId
|
||||
where
|
||||
T: Any + 'static,
|
||||
{
|
||||
#[cfg(debug_assertions)]
|
||||
let defined_at = std::panic::Location::caller();
|
||||
|
||||
with_runtime(self, |runtime| {
|
||||
let effect = Effect {
|
||||
f,
|
||||
value: RefCell::new(None),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at
|
||||
};
|
||||
let id = { runtime.effects.borrow_mut().insert(Rc::new(effect)) };
|
||||
id.run::<T>(self);
|
||||
@@ -168,10 +181,14 @@ impl RuntimeId {
|
||||
})
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn create_memo<T>(self, f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
|
||||
where
|
||||
T: PartialEq + Any + 'static,
|
||||
{
|
||||
#[cfg(debug_assertions)]
|
||||
let defined_at = std::panic::Location::caller();
|
||||
|
||||
let (read, write) = self.create_signal(None);
|
||||
|
||||
self.create_effect(move |_| {
|
||||
@@ -186,7 +203,11 @@ impl RuntimeId {
|
||||
}
|
||||
});
|
||||
|
||||
Memo(read)
|
||||
Memo(
|
||||
read,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,15 +45,38 @@ use thiserror::Error;
|
||||
/// # }).dispose();
|
||||
/// #
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
scope = ?cx.id,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn create_signal<T>(cx: Scope, value: T) -> (ReadSignal<T>, WriteSignal<T>) {
|
||||
let s = cx.runtime.create_signal(value);
|
||||
cx.with_scope_property(|prop| prop.push(ScopeProperty::Signal(s.0.id)));
|
||||
s
|
||||
}
|
||||
|
||||
/// Creates a signal that always contains the most recent value emitted by a [Stream].
|
||||
/// Creates a signal that always contains the most recent value emitted by a
|
||||
/// [Stream](futures::stream::Stream).
|
||||
/// If the stream has not yet emitted a value since the signal was created, the signal's
|
||||
/// value will be `None`.
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
scope = ?cx.id,
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn create_signal_from_stream<T>(
|
||||
cx: Scope,
|
||||
mut stream: impl Stream<Item = T> + Unpin + 'static,
|
||||
@@ -120,9 +143,24 @@ where
|
||||
pub(crate) runtime: RuntimeId,
|
||||
pub(crate) id: SignalId,
|
||||
pub(crate) ty: PhantomData<T>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) defined_at: &'static std::panic::Location<'static>,
|
||||
}
|
||||
|
||||
impl<T> UntrackedGettableSignal<T> for ReadSignal<T> {
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "ReadSignal::get_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn get_untracked(&self) -> T
|
||||
where
|
||||
T: Clone,
|
||||
@@ -130,6 +168,19 @@ impl<T> UntrackedGettableSignal<T> for ReadSignal<T> {
|
||||
self.with_no_subscription(|v| v.clone())
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "ReadSignal::with_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||
self.with_no_subscription(f)
|
||||
}
|
||||
@@ -157,6 +208,19 @@ where
|
||||
/// assert_eq!(first_char(), 'B');
|
||||
/// });
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "ReadSignal::with()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn with<U>(&self, f: impl FnOnce(&T) -> U) -> U {
|
||||
self.id.with(self.runtime, f)
|
||||
}
|
||||
@@ -184,6 +248,19 @@ where
|
||||
/// assert_eq!(count(), 0);
|
||||
/// });
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "ReadSignal::get()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn get(&self) -> T
|
||||
where
|
||||
T: Clone,
|
||||
@@ -219,6 +296,8 @@ impl<T> Clone for ReadSignal<T> {
|
||||
runtime: self.runtime,
|
||||
id: self.id,
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: self.defined_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -298,21 +377,62 @@ where
|
||||
pub(crate) runtime: RuntimeId,
|
||||
pub(crate) id: SignalId,
|
||||
pub(crate) ty: PhantomData<T>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) defined_at: &'static std::panic::Location<'static>,
|
||||
}
|
||||
|
||||
impl<T> UntrackedSettableSignal<T> for WriteSignal<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "WriteSignal::set_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn set_untracked(&self, new_value: T) {
|
||||
self.id
|
||||
.update_with_no_effect(self.runtime, |v| *v = new_value);
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "WriteSignal::updated_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn update_untracked(&self, f: impl FnOnce(&mut T)) {
|
||||
self.id.update_with_no_effect(self.runtime, f);
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "WriteSignal::update_returning_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn update_returning_untracked<U>(&self, f: impl FnOnce(&mut T) -> U) -> Option<U> {
|
||||
self.id.update_with_no_effect(self.runtime, f)
|
||||
}
|
||||
@@ -342,6 +462,19 @@ where
|
||||
/// assert_eq!(count(), 1);
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
name = "WriteSignal::update()",
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn update(&self, f: impl FnOnce(&mut T)) {
|
||||
self.id.update(self.runtime, f);
|
||||
}
|
||||
@@ -367,6 +500,19 @@ where
|
||||
/// assert_eq!(count(), 2);
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "WriteSignal::update_returning()"
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn update_returning<U>(&self, f: impl FnOnce(&mut T) -> U) -> Option<U> {
|
||||
self.id.update(self.runtime, f)
|
||||
}
|
||||
@@ -390,6 +536,19 @@ where
|
||||
/// assert_eq!(count(), 1);
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "WriteSignal::set()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn set(&self, new_value: T) {
|
||||
self.id.update(self.runtime, |n| *n = new_value);
|
||||
}
|
||||
@@ -401,6 +560,8 @@ impl<T> Clone for WriteSignal<T> {
|
||||
runtime: self.runtime,
|
||||
id: self.id,
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: self.defined_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,6 +621,16 @@ where
|
||||
/// # }).dispose();
|
||||
/// #
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn create_rw_signal<T>(cx: Scope, value: T) -> RwSignal<T> {
|
||||
let s = cx.runtime.create_rw_signal(value);
|
||||
cx.with_scope_property(|prop| prop.push(ScopeProperty::Signal(s.id)));
|
||||
@@ -495,6 +666,8 @@ where
|
||||
pub(crate) runtime: RuntimeId,
|
||||
pub(crate) id: SignalId,
|
||||
pub(crate) ty: PhantomData<T>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) defined_at: &'static std::panic::Location<'static>,
|
||||
}
|
||||
|
||||
impl<T> Clone for RwSignal<T> {
|
||||
@@ -503,6 +676,8 @@ impl<T> Clone for RwSignal<T> {
|
||||
runtime: self.runtime,
|
||||
id: self.id,
|
||||
ty: self.ty,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: self.defined_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -510,6 +685,19 @@ impl<T> Clone for RwSignal<T> {
|
||||
impl<T> Copy for RwSignal<T> {}
|
||||
|
||||
impl<T> UntrackedGettableSignal<T> for RwSignal<T> {
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::get_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn get_untracked(&self) -> T
|
||||
where
|
||||
T: Clone,
|
||||
@@ -518,21 +706,73 @@ impl<T> UntrackedGettableSignal<T> for RwSignal<T> {
|
||||
.with_no_subscription(self.runtime, |v: &T| v.clone())
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::with_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||
self.id.with_no_subscription(self.runtime, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> UntrackedSettableSignal<T> for RwSignal<T> {
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::set_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn set_untracked(&self, new_value: T) {
|
||||
self.id
|
||||
.update_with_no_effect(self.runtime, |v| *v = new_value);
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::update_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn update_untracked(&self, f: impl FnOnce(&mut T)) {
|
||||
self.id.update_with_no_effect(self.runtime, f);
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::update_returning_untracked()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn update_returning_untracked<U>(&self, f: impl FnOnce(&mut T) -> U) -> Option<U> {
|
||||
self.id.update_with_no_effect(self.runtime, f)
|
||||
}
|
||||
@@ -561,6 +801,19 @@ where
|
||||
/// # }).dispose();
|
||||
/// #
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::with()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn with<U>(&self, f: impl FnOnce(&T) -> U) -> U {
|
||||
self.id.with(self.runtime, f)
|
||||
}
|
||||
@@ -578,7 +831,20 @@ where
|
||||
/// assert_eq!(count(), 0);
|
||||
/// # }).dispose();
|
||||
/// #
|
||||
/// ```
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::get()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn get(&self) -> T
|
||||
where
|
||||
T: Clone,
|
||||
@@ -603,6 +869,19 @@ where
|
||||
/// assert_eq!(count(), 1);
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::update()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn update(&self, f: impl FnOnce(&mut T)) {
|
||||
self.id.update(self.runtime, f);
|
||||
}
|
||||
@@ -626,6 +905,19 @@ where
|
||||
/// assert_eq!(count(), 2);
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::update_returning()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn update_returning<U>(&self, f: impl FnOnce(&mut T) -> U) -> Option<U> {
|
||||
self.id.update(self.runtime, f)
|
||||
}
|
||||
@@ -644,6 +936,19 @@ where
|
||||
/// assert_eq!(count(), 1);
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::set()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn set(&self, value: T) {
|
||||
self.id.update(self.runtime, |n| *n = value);
|
||||
}
|
||||
@@ -664,11 +969,27 @@ where
|
||||
/// assert_eq!(read_count(), 1);
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::read_only()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn read_only(&self) -> ReadSignal<T> {
|
||||
ReadSignal {
|
||||
runtime: self.runtime,
|
||||
id: self.id,
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -686,11 +1007,27 @@ where
|
||||
/// assert_eq!(count(), 1);
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::write_only()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn write_only(&self) -> WriteSignal<T> {
|
||||
WriteSignal {
|
||||
runtime: self.runtime,
|
||||
id: self.id,
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -707,22 +1044,53 @@ where
|
||||
/// assert_eq!(get_count(), 1);
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::split()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn split(&self) -> (ReadSignal<T>, WriteSignal<T>) {
|
||||
(
|
||||
ReadSignal {
|
||||
runtime: self.runtime,
|
||||
id: self.id,
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
},
|
||||
WriteSignal {
|
||||
runtime: self.runtime,
|
||||
id: self.id,
|
||||
ty: PhantomData,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Generates a [Stream] that emits the new value of the signal whenever it changes.
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::to_stream()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = ?self.id,
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn to_stream(&self) -> impl Stream<Item = T>
|
||||
where
|
||||
T: Clone,
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
use crate::{store_value, Memo, ReadSignal, RwSignal, Scope, StoredValue, UntrackedGettableSignal};
|
||||
|
||||
/// Helper trait for converting `Fn() -> T` closures into
|
||||
/// [`Signal<T>`].
|
||||
pub trait IntoSignal<T>: Sized {
|
||||
/// Consumes `self`, returning a [`Signal<T>`].
|
||||
fn derive_signal(self, cx: Scope) -> Signal<T>;
|
||||
}
|
||||
|
||||
impl<F, T> IntoSignal<T> for F
|
||||
where
|
||||
F: Fn() -> T + 'static,
|
||||
{
|
||||
fn derive_signal(self, cx: Scope) -> Signal<T> {
|
||||
Signal::derive(cx, self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper for any kind of readable reactive signal: a [ReadSignal](crate::ReadSignal),
|
||||
/// [Memo](crate::Memo), [RwSignal](crate::RwSignal), or derived signal closure.
|
||||
///
|
||||
@@ -27,13 +43,22 @@ use crate::{store_value, Memo, ReadSignal, RwSignal, Scope, StoredValue, Untrack
|
||||
/// # });
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Signal<T>(SignalTypes<T>)
|
||||
pub struct Signal<T>
|
||||
where
|
||||
T: 'static;
|
||||
T: 'static,
|
||||
{
|
||||
inner: SignalTypes<T>,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static std::panic::Location<'static>,
|
||||
}
|
||||
|
||||
impl<T> Clone for Signal<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0)
|
||||
Self {
|
||||
inner: self.inner,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: self.defined_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +75,7 @@ where
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
match &self.0 {
|
||||
match &self.inner {
|
||||
SignalTypes::ReadSignal(s) => s.get_untracked(),
|
||||
SignalTypes::Memo(m) => m.get_untracked(),
|
||||
SignalTypes::DerivedSignal(cx, f) => cx.untrack(|| f.with(|f| f())),
|
||||
@@ -58,7 +83,7 @@ where
|
||||
}
|
||||
|
||||
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||
match &self.0 {
|
||||
match &self.inner {
|
||||
SignalTypes::ReadSignal(s) => s.with_untracked(f),
|
||||
SignalTypes::Memo(s) => s.with_untracked(f),
|
||||
SignalTypes::DerivedSignal(cx, v_f) => {
|
||||
@@ -93,11 +118,30 @@ where
|
||||
/// assert_eq!(above_3(&double_count), true);
|
||||
/// # });
|
||||
/// ```
|
||||
#[track_caller]
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
cx = ?cx.id
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn derive(cx: Scope, derived_signal: impl Fn() -> T + 'static) -> Self {
|
||||
Self(SignalTypes::DerivedSignal(
|
||||
cx,
|
||||
store_value(cx, Box::new(derived_signal)),
|
||||
))
|
||||
let span = ::tracing::Span::current();
|
||||
|
||||
let derived_signal = move || {
|
||||
let _guard = span.enter();
|
||||
derived_signal()
|
||||
};
|
||||
|
||||
Self {
|
||||
inner: SignalTypes::DerivedSignal(cx, store_value(cx, Box::new(derived_signal))),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies a function to the current value of the signal, and subscribes
|
||||
@@ -129,8 +173,19 @@ where
|
||||
/// assert_eq!(memoized_lower(), "alice");
|
||||
/// });
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn with<U>(&self, f: impl FnOnce(&T) -> U) -> U {
|
||||
match &self.0 {
|
||||
match &self.inner {
|
||||
SignalTypes::ReadSignal(s) => s.with(f),
|
||||
SignalTypes::Memo(s) => s.with(f),
|
||||
SignalTypes::DerivedSignal(_, s) => f(&s.with(|s| s())),
|
||||
@@ -159,11 +214,22 @@ where
|
||||
/// assert_eq!(above_3(&memoized_double_count.into()), true);
|
||||
/// # });
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn get(&self) -> T
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
match &self.0 {
|
||||
match &self.inner {
|
||||
SignalTypes::ReadSignal(s) => s.get(),
|
||||
SignalTypes::Memo(s) => s.get(),
|
||||
SignalTypes::DerivedSignal(_, s) => s.with(|s| s()),
|
||||
@@ -181,20 +247,35 @@ where
|
||||
}
|
||||
|
||||
impl<T> From<ReadSignal<T>> for Signal<T> {
|
||||
#[track_caller]
|
||||
fn from(value: ReadSignal<T>) -> Self {
|
||||
Self(SignalTypes::ReadSignal(value))
|
||||
Self {
|
||||
inner: SignalTypes::ReadSignal(value),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RwSignal<T>> for Signal<T> {
|
||||
#[track_caller]
|
||||
fn from(value: RwSignal<T>) -> Self {
|
||||
Self(SignalTypes::ReadSignal(value.read_only()))
|
||||
Self {
|
||||
inner: SignalTypes::ReadSignal(value.read_only()),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Memo<T>> for Signal<T> {
|
||||
#[track_caller]
|
||||
fn from(value: Memo<T>) -> Self {
|
||||
Self(SignalTypes::Memo(value))
|
||||
Self {
|
||||
inner: SignalTypes::Memo(value),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,6 +441,18 @@ where
|
||||
/// assert_eq!(above_3(&double_count), true);
|
||||
/// # });
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeSignal::derive()",
|
||||
skip_all,
|
||||
fields(
|
||||
cx = ?cx.id,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn derive(cx: Scope, derived_signal: impl Fn() -> T + 'static) -> Self {
|
||||
Self::Dynamic(Signal::derive(cx, derived_signal))
|
||||
}
|
||||
@@ -396,6 +489,15 @@ where
|
||||
/// assert_eq!(static_value(), "Bob");
|
||||
/// });
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeSignal::derive()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
pub fn with<U>(&self, f: impl FnOnce(&T) -> U) -> U {
|
||||
match &self {
|
||||
Self::Static(value) => f(value),
|
||||
@@ -427,6 +529,15 @@ where
|
||||
/// assert_eq!(above_3(&static_value.into()), true);
|
||||
/// # });
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "MaybeSignal::derive()",
|
||||
skip_all,
|
||||
fields(ty = %std::any::type_name::<T>())
|
||||
)
|
||||
)]
|
||||
pub fn get(&self) -> T
|
||||
where
|
||||
T: Clone,
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
use crate::{store_value, RwSignal, Scope, StoredValue, WriteSignal};
|
||||
|
||||
/// Helper trait for converting `Fn(T)` into [`SignalSetter<T>`].
|
||||
pub trait IntoSignalSetter<T>: Sized {
|
||||
/// Consumes `self`, returning [`SignalSetter<T>`].
|
||||
fn mapped_signal_setter(self, cx: Scope) -> SignalSetter<T>;
|
||||
}
|
||||
|
||||
impl<F, T> IntoSignalSetter<T> for F
|
||||
where
|
||||
F: Fn(T) + 'static,
|
||||
{
|
||||
fn mapped_signal_setter(self, cx: Scope) -> SignalSetter<T> {
|
||||
SignalSetter::map(cx, self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper for any kind of settable reactive signal: a [WriteSignal](crate::WriteSignal),
|
||||
/// [RwSignal](crate::RwSignal), or closure that receives a value and sets a signal depending
|
||||
/// on it.
|
||||
@@ -28,19 +43,33 @@ use crate::{store_value, RwSignal, Scope, StoredValue, WriteSignal};
|
||||
/// # });
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct SignalSetter<T>(SignalSetterTypes<T>)
|
||||
pub struct SignalSetter<T>
|
||||
where
|
||||
T: 'static;
|
||||
T: 'static,
|
||||
{
|
||||
inner: SignalSetterTypes<T>,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static std::panic::Location<'static>,
|
||||
}
|
||||
|
||||
impl<T> Clone for SignalSetter<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0)
|
||||
Self {
|
||||
inner: self.inner,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: self.defined_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default + 'static> Default for SignalSetter<T> {
|
||||
#[track_caller]
|
||||
fn default() -> Self {
|
||||
Self(SignalSetterTypes::Default)
|
||||
Self {
|
||||
inner: SignalSetterTypes::Default,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,11 +100,23 @@ where
|
||||
/// assert_eq!(count(), 8);
|
||||
/// # });
|
||||
/// ```
|
||||
#[track_caller]
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
cx = ?cx.id,
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn map(cx: Scope, mapped_setter: impl Fn(T) + 'static) -> Self {
|
||||
Self(SignalSetterTypes::Mapped(
|
||||
cx,
|
||||
store_value(cx, Box::new(mapped_setter)),
|
||||
))
|
||||
Self {
|
||||
inner: SignalSetterTypes::Mapped(cx, store_value(cx, Box::new(mapped_setter))),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls the setter function with the given value.
|
||||
@@ -98,8 +139,19 @@ where
|
||||
/// set_to_4(&set_double_count);
|
||||
/// assert_eq!(count(), 8);
|
||||
/// # });
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
defined_at = %self.defined_at,
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn set(&self, value: T) {
|
||||
match &self.0 {
|
||||
match &self.inner {
|
||||
SignalSetterTypes::Write(s) => s.set(value),
|
||||
SignalSetterTypes::Mapped(_, s) => s.with(|s| s(value)),
|
||||
SignalSetterTypes::Default => {}
|
||||
@@ -108,14 +160,24 @@ where
|
||||
}
|
||||
|
||||
impl<T> From<WriteSignal<T>> for SignalSetter<T> {
|
||||
#[track_caller]
|
||||
fn from(value: WriteSignal<T>) -> Self {
|
||||
Self(SignalSetterTypes::Write(value))
|
||||
Self {
|
||||
inner: SignalSetterTypes::Write(value),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RwSignal<T>> for SignalSetter<T> {
|
||||
#[track_caller]
|
||||
fn from(value: RwSignal<T>) -> Self {
|
||||
Self(SignalSetterTypes::Write(value.write_only()))
|
||||
Self {
|
||||
inner: SignalSetterTypes::Write(value.write_only()),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_server"
|
||||
version = "0.1.0-beta"
|
||||
version.workspace = true
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -8,8 +8,8 @@ repository = "https://github.com/gbj/leptos"
|
||||
description = "RPC for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.1.0-beta" }
|
||||
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.1.0-beta" }
|
||||
leptos_dom.workspace = true
|
||||
leptos_reactive.workspace = true
|
||||
form_urlencoded = "1"
|
||||
gloo-net = "0.2"
|
||||
lazy_static = "1"
|
||||
@@ -26,7 +26,7 @@ proc-macro2 = "1.0.47"
|
||||
ciborium = "0.2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = "../leptos", default-features = false }
|
||||
leptos.workspace = true
|
||||
|
||||
[features]
|
||||
csr = [
|
||||
|
||||
@@ -9,7 +9,8 @@ description = "Tools to set HTML metadata in the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../leptos", version = "0.1.0-beta", default-features = false }
|
||||
leptos.workspace = true
|
||||
tracing = "0.1"
|
||||
typed-builder = "0.11"
|
||||
|
||||
[dependencies.web-sys]
|
||||
@@ -17,11 +18,12 @@ version = "0.3"
|
||||
features = ["HtmlLinkElement", "HtmlMetaElement", "HtmlTitleElement"]
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = ["leptos/ssr"]
|
||||
stable = ["leptos/stable"]
|
||||
default = []
|
||||
csr = ["leptos/csr", "leptos/tracing"]
|
||||
hydrate = ["leptos/hydrate", "leptos/tracing"]
|
||||
ssr = ["leptos/ssr", "leptos/tracing"]
|
||||
stable = ["leptos/stable", "leptos/tracing"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
106
meta/src/lib.rs
106
meta/src/lib.rs
@@ -36,14 +36,26 @@
|
||||
//!
|
||||
//! ```
|
||||
|
||||
use std::{fmt::Debug, rc::Rc};
|
||||
use cfg_if::cfg_if;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashMap,
|
||||
fmt::Debug,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use leptos::{leptos_dom::debug_warn, *};
|
||||
|
||||
mod link;
|
||||
mod meta_tags;
|
||||
mod script;
|
||||
mod style;
|
||||
mod stylesheet;
|
||||
mod title;
|
||||
pub use link::*;
|
||||
pub use meta_tags::*;
|
||||
pub use script::*;
|
||||
pub use style::*;
|
||||
pub use stylesheet::*;
|
||||
pub use title::*;
|
||||
|
||||
@@ -51,11 +63,83 @@ pub use title::*;
|
||||
///
|
||||
/// This should generally by provided somewhere in the root of your application using
|
||||
/// [provide_meta_context].
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct MetaContext {
|
||||
pub(crate) title: TitleContext,
|
||||
pub(crate) stylesheets: StylesheetContext,
|
||||
pub(crate) meta_tags: MetaTagsContext,
|
||||
pub(crate) tags: MetaTagsContext,
|
||||
}
|
||||
|
||||
/// Manages all of the element created by components.
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct MetaTagsContext {
|
||||
next_id: Rc<Cell<MetaTagId>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
els: Rc<RefCell<HashMap<String, (HtmlElement<AnyElement>, Scope, Option<web_sys::Element>)>>>,
|
||||
}
|
||||
|
||||
impl MetaTagsContext {
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn as_string(&self) -> String {
|
||||
self.els
|
||||
.borrow()
|
||||
.iter()
|
||||
.map(|(_, (builder_el, cx, _))| builder_el.clone().into_view(*cx).render_to_string(*cx))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn register(&self, cx: Scope, id: String, builder_el: HtmlElement<AnyElement>) {
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
use leptos::document;
|
||||
|
||||
let element_to_hydrate = document()
|
||||
.get_element_by_id(&id);
|
||||
|
||||
let el = element_to_hydrate.unwrap_or_else({
|
||||
let builder_el = builder_el.clone();
|
||||
move || {
|
||||
let head = document().head().unwrap_throw();
|
||||
head
|
||||
.append_child(&builder_el)
|
||||
.unwrap_throw();
|
||||
|
||||
(*builder_el).clone().unchecked_into()
|
||||
}
|
||||
});
|
||||
|
||||
on_cleanup(cx, {
|
||||
let el = el.clone();
|
||||
let els = self.els.clone();
|
||||
let id = id.clone();
|
||||
move || {
|
||||
let head = document().head().unwrap_throw();
|
||||
_ = head.remove_child(&el);
|
||||
els.borrow_mut().remove(&id);
|
||||
}
|
||||
});
|
||||
|
||||
self
|
||||
.els
|
||||
.borrow_mut()
|
||||
.insert(id, (builder_el.into_any(), cx, Some(el)));
|
||||
|
||||
} else {
|
||||
self.els.borrow_mut().insert(id, (builder_el, cx, None));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||
struct MetaTagId(usize);
|
||||
|
||||
impl MetaTagsContext {
|
||||
fn get_next_id(&self) -> MetaTagId {
|
||||
let current_id = self.next_id.get();
|
||||
let next_id = MetaTagId(current_id.0 + 1);
|
||||
self.next_id.set(next_id);
|
||||
next_id
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides a [MetaContext], if there is not already one provided. This ensures that you can provide it
|
||||
@@ -98,7 +182,7 @@ impl MetaContext {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
#[cfg(feature = "ssr")]
|
||||
/// Converts the existing metadata tags into HTML that can be injected into the document head.
|
||||
///
|
||||
/// This should be called *after* the app’s component tree has been rendered into HTML, so that
|
||||
@@ -123,14 +207,15 @@ impl MetaContext {
|
||||
/// // `app` contains only the body content w/ hydration stuff, not the meta tags
|
||||
/// assert_eq!(
|
||||
/// app.into_view(cx).render_to_string(cx),
|
||||
/// "<main id=\"_0-1\"><leptos-unit leptos id=_0-2c></leptos-unit><leptos-unit leptos id=_0-3c></leptos-unit><p id=\"_0-4\">Some text</p></main>"
|
||||
/// "<main id=\"_0-1\"><leptos-unit leptos id=_0-2c></leptos-unit><leptos-unit leptos id=_0-4c></leptos-unit><p id=\"_0-5\">Some text</p></main>"
|
||||
/// );
|
||||
/// // `MetaContext::dehydrate()` gives you HTML that should be in the `<head>`
|
||||
/// assert_eq!(use_head(cx).dehydrate(), r#"<title>my title</title><link rel="stylesheet" href="/style.css">"#)
|
||||
/// assert_eq!(use_head(cx).dehydrate(), r#"<title>my title</title><link id="leptos-link-1" href="/style.css" rel="stylesheet" leptos-hk="_0-3"/>"#)
|
||||
/// });
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn dehydrate(&self) -> String {
|
||||
let prev_key = HydrationCtx::peek();
|
||||
let mut tags = String::new();
|
||||
|
||||
// Title
|
||||
@@ -139,12 +224,9 @@ impl MetaContext {
|
||||
tags.push_str(&title);
|
||||
tags.push_str("</title>");
|
||||
}
|
||||
// Stylesheets
|
||||
tags.push_str(&self.stylesheets.as_string());
|
||||
|
||||
// Meta tags
|
||||
tags.push_str(&self.meta_tags.as_string());
|
||||
tags.push_str(&self.tags.as_string());
|
||||
|
||||
HydrationCtx::continue_from(prev_key);
|
||||
tags
|
||||
}
|
||||
}
|
||||
|
||||
109
meta/src/link.rs
Normal file
109
meta/src/link.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use crate::use_head;
|
||||
use leptos::*;
|
||||
|
||||
/// Injects an [HTMLLinkElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document
|
||||
/// head, accepting any of the valid attributes for that tag.
|
||||
/// ```
|
||||
/// use leptos::*;
|
||||
/// use leptos_meta::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> impl IntoView {
|
||||
/// provide_meta_context(cx);
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Link rel="preload"
|
||||
/// href="myFont.woff2"
|
||||
/// as_="font"
|
||||
/// type_="font/woff2"
|
||||
/// crossorigin="anonymous"
|
||||
/// />
|
||||
/// </main>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[component(transparent)]
|
||||
pub fn Link(
|
||||
cx: Scope,
|
||||
/// The [`id`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-id) attribute.
|
||||
#[prop(optional, into)]
|
||||
id: Option<String>,
|
||||
/// The [`as`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-as) attribute.
|
||||
#[prop(optional, into)]
|
||||
as_: Option<String>,
|
||||
/// The [`crossorigin`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-crossorigin) attribute.
|
||||
#[prop(optional, into)]
|
||||
crossorigin: Option<String>,
|
||||
/// The [`disabled`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-disabled) attribute.
|
||||
#[prop(optional, into)]
|
||||
disabled: Option<bool>,
|
||||
/// The [`fetchpriority`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-fetchpriority) attribute.
|
||||
#[prop(optional, into)]
|
||||
fetchpriority: Option<String>,
|
||||
/// The [`href`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-href) attribute.
|
||||
#[prop(optional, into)]
|
||||
href: Option<String>,
|
||||
/// The [`hreflang`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-hreflang) attribute.
|
||||
#[prop(optional, into)]
|
||||
hreflang: Option<String>,
|
||||
/// The [`imagesizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-imagesizes) attribute.
|
||||
#[prop(optional, into)]
|
||||
imagesizes: Option<String>,
|
||||
/// The [`imagesrcset`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-imagesrcset) attribute.
|
||||
#[prop(optional, into)]
|
||||
imagesrcset: Option<String>,
|
||||
/// The [`integrity`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-integrity) attribute.
|
||||
#[prop(optional, into)]
|
||||
integrity: Option<String>,
|
||||
/// The [`media`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-media) attribute.
|
||||
#[prop(optional, into)]
|
||||
media: Option<String>,
|
||||
/// The [`prefetch`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-prefetch) attribute.
|
||||
#[prop(optional, into)]
|
||||
prefetch: Option<String>,
|
||||
/// The [`referrerpolicy`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-referrerpolicy) attribute.
|
||||
#[prop(optional, into)]
|
||||
referrerpolicy: Option<String>,
|
||||
/// The [`rel`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-rel) attribute.
|
||||
#[prop(optional, into)]
|
||||
rel: Option<String>,
|
||||
/// The [`sizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-sizes) attribute.
|
||||
#[prop(optional, into)]
|
||||
sizes: Option<String>,
|
||||
/// The [`title`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-title) attribute.
|
||||
#[prop(optional, into)]
|
||||
title: Option<String>,
|
||||
/// The [`type`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-type) attribute.
|
||||
#[prop(optional, into)]
|
||||
type_: Option<String>,
|
||||
/// The [`blocking`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-blocking) attribute.
|
||||
#[prop(optional, into)]
|
||||
blocking: Option<String>,
|
||||
) -> impl IntoView {
|
||||
let meta = use_head(cx);
|
||||
let next_id = meta.tags.get_next_id();
|
||||
let id = id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0));
|
||||
|
||||
let builder_el = leptos::link(cx)
|
||||
.attr("id", &id)
|
||||
.attr("as_", as_)
|
||||
.attr("crossorigin", crossorigin)
|
||||
.attr("disabled", disabled.unwrap_or(false))
|
||||
.attr("fetchpriority", fetchpriority)
|
||||
.attr("href", href)
|
||||
.attr("hreflang", hreflang)
|
||||
.attr("imagesizes", imagesizes)
|
||||
.attr("imagesrcset", imagesrcset)
|
||||
.attr("integrity", integrity)
|
||||
.attr("media", media)
|
||||
.attr("prefetch", prefetch)
|
||||
.attr("referrerpolicy", referrerpolicy)
|
||||
.attr("rel", rel)
|
||||
.attr("sizes", sizes)
|
||||
.attr("title", title)
|
||||
.attr("type", type_)
|
||||
.attr("blocking", blocking);
|
||||
|
||||
meta.tags.register(cx, id, builder_el.into_any());
|
||||
}
|
||||
@@ -1,73 +1,7 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{component, IntoView, Scope};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashMap,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use crate::{use_head, TextProp};
|
||||
|
||||
/// Manages all of the `<meta>` elements set by [Meta] components.
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct MetaTagsContext {
|
||||
next_id: Cell<MetaTagId>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
els: Rc<RefCell<HashMap<MetaTagId, (Option<MetaTag>, Option<web_sys::HtmlMetaElement>)>>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||
struct MetaTagId(usize);
|
||||
|
||||
impl MetaTagsContext {
|
||||
fn get_next_id(&self) -> MetaTagId {
|
||||
let current_id = self.next_id.get();
|
||||
let next_id = MetaTagId(current_id.0 + 1);
|
||||
self.next_id.set(next_id);
|
||||
next_id
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum MetaTag {
|
||||
Charset(TextProp),
|
||||
HttpEquiv {
|
||||
http_equiv: TextProp,
|
||||
content: Option<TextProp>,
|
||||
},
|
||||
Name {
|
||||
name: TextProp,
|
||||
content: TextProp,
|
||||
},
|
||||
}
|
||||
|
||||
impl MetaTagsContext {
|
||||
/// Converts the set of `<meta>` elements into an HTML string that can be injected into the `<head>`.
|
||||
pub fn as_string(&self) -> String {
|
||||
self.els
|
||||
.borrow()
|
||||
.iter()
|
||||
.filter_map(|(id, (tag, _))| {
|
||||
tag.as_ref().map(|tag| {
|
||||
let id = id.0;
|
||||
|
||||
match tag {
|
||||
MetaTag::Charset(charset) => format!(r#"<meta charset="{}" data-leptos-meta="{id}">"#, charset.get()),
|
||||
MetaTag::HttpEquiv { http_equiv, content } => {
|
||||
if let Some(content) = &content {
|
||||
format!(r#"<meta http-equiv="{}" content="{}" data-leptos-meta="{id}">"#, http_equiv.get(), content.get())
|
||||
} else {
|
||||
format!(r#"<meta http-equiv="{}" data-leptos-meta="{id}">"#, http_equiv.get())
|
||||
}
|
||||
},
|
||||
MetaTag::Name { name, content } => format!(r#"<meta name="{}" content="{}" data-leptos-meta="{id}">"#, name.get(), content.get()),
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Injects an [HTMLMetaElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMetaElement) into the document
|
||||
/// head to set metadata
|
||||
///
|
||||
@@ -104,89 +38,15 @@ pub fn Meta(
|
||||
#[prop(optional, into)]
|
||||
content: Option<TextProp>,
|
||||
) -> impl IntoView {
|
||||
let tag = match (charset, name, http_equiv, content) {
|
||||
(Some(charset), _, _, _) => MetaTag::Charset(charset),
|
||||
(_, _, Some(http_equiv), content) => MetaTag::HttpEquiv { http_equiv, content },
|
||||
(_, Some(name), _, Some(content)) => MetaTag::Name { name, content },
|
||||
_ => panic!("<Meta/> tag expects either `charset`, `http_equiv`, or `name` and `content` to be set.")
|
||||
};
|
||||
let meta = use_head(cx);
|
||||
let next_id = meta.tags.get_next_id();
|
||||
let id = format!("leptos-link-{}", next_id.0);
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
use leptos::{document, JsCast, UnwrapThrowExt, create_effect};
|
||||
let builder_el = leptos::meta(cx)
|
||||
.attr("charset", move || charset.as_ref().map(|v| v.get()))
|
||||
.attr("name", move || name.as_ref().map(|v| v.get()))
|
||||
.attr("http-equiv", move || http_equiv.as_ref().map(|v| v.get()))
|
||||
.attr("content", move || content.as_ref().map(|v| v.get()));
|
||||
|
||||
let meta = use_head(cx);
|
||||
let meta_tags = meta.meta_tags;
|
||||
let id = meta_tags.get_next_id();
|
||||
|
||||
let el = if let Ok(Some(el)) = document().query_selector(&format!("[data-leptos-meta='{}']", id.0)) {
|
||||
el
|
||||
} else {
|
||||
document().create_element("meta").unwrap_throw()
|
||||
};
|
||||
|
||||
match tag {
|
||||
MetaTag::Charset(charset) => {
|
||||
create_effect(cx, {
|
||||
let el = el.clone();
|
||||
move |_| {
|
||||
_ = el.set_attribute("charset", &charset.get());
|
||||
}
|
||||
})
|
||||
},
|
||||
MetaTag::HttpEquiv { http_equiv, content } => {
|
||||
create_effect(cx, {
|
||||
let el = el.clone();
|
||||
move |_| {
|
||||
_ = el.set_attribute("http-equiv", &http_equiv.get());
|
||||
}
|
||||
});
|
||||
if let Some(content) = content {
|
||||
create_effect(cx, {
|
||||
let el = el.clone();
|
||||
move |_| {
|
||||
_ = el.set_attribute("content", &content.get());
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
MetaTag::Name { name, content } => {
|
||||
create_effect(cx, {
|
||||
let el = el.clone();
|
||||
move |_| {
|
||||
_ = el.set_attribute("name", &name.get());
|
||||
}
|
||||
});
|
||||
create_effect(cx, {
|
||||
let el = el.clone();
|
||||
move |_| {
|
||||
_ = el.set_attribute("content", &content.get());
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
// add to head
|
||||
let head = document()
|
||||
.query_selector("head")
|
||||
.unwrap_throw()
|
||||
.unwrap_throw();
|
||||
head.append_child(&el)
|
||||
.unwrap_throw();
|
||||
|
||||
leptos::on_cleanup(cx, {
|
||||
let el = el.clone();
|
||||
move || {
|
||||
head.remove_child(&el);
|
||||
}
|
||||
});
|
||||
|
||||
// add to meta tags
|
||||
meta_tags.els.borrow_mut().insert(id, (None, Some(el.unchecked_into())));
|
||||
} else {
|
||||
let meta = use_head(cx);
|
||||
let meta_tags = meta.meta_tags;
|
||||
meta_tags.els.borrow_mut().insert(meta_tags.get_next_id(), (Some(tag), None));
|
||||
}
|
||||
}
|
||||
meta.tags.register(cx, id, builder_el.into_any());
|
||||
}
|
||||
|
||||
98
meta/src/script.rs
Normal file
98
meta/src/script.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
use crate::use_head;
|
||||
use leptos::*;
|
||||
|
||||
/// Injects an [HTMLScriptElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement) into the document
|
||||
/// head, accepting any of the valid attributes for that tag.
|
||||
/// ```
|
||||
/// use leptos::*;
|
||||
/// use leptos_meta::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> impl IntoView {
|
||||
/// provide_meta_context(cx);
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Script>
|
||||
/// "console.log('Hello, world!');"
|
||||
/// </Script>
|
||||
/// </main>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[component(transparent)]
|
||||
pub fn Script(
|
||||
cx: Scope,
|
||||
/// An ID for the `<script>` tag.
|
||||
#[prop(optional, into)]
|
||||
id: Option<String>,
|
||||
/// The [`async`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-async) attribute.
|
||||
#[prop(optional, into)]
|
||||
async_: Option<String>,
|
||||
/// The [`crossorigin`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-crossorigin) attribute.
|
||||
#[prop(optional, into)]
|
||||
crossorigin: Option<String>,
|
||||
/// The [`defer`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-defer) attribute.
|
||||
#[prop(optional, into)]
|
||||
defer: Option<String>,
|
||||
/// The [`fetchpriority `](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-fetchpriority ) attribute.
|
||||
#[prop(optional, into)]
|
||||
fetchpriority: Option<String>,
|
||||
/// The [`integrity`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-integrity) attribute.
|
||||
#[prop(optional, into)]
|
||||
integrity: Option<String>,
|
||||
/// The [`nomodule`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-nomodule) attribute.
|
||||
#[prop(optional, into)]
|
||||
nomodule: Option<String>,
|
||||
/// The [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-nonce) attribute.
|
||||
#[prop(optional, into)]
|
||||
nonce: Option<String>,
|
||||
/// The [`referrerpolicy`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-referrerpolicy) attribute.
|
||||
#[prop(optional, into)]
|
||||
referrerpolicy: Option<String>,
|
||||
/// The [`src`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-src) attribute.
|
||||
#[prop(optional, into)]
|
||||
src: Option<String>,
|
||||
/// The [`type`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type) attribute.
|
||||
#[prop(optional, into)]
|
||||
type_: Option<String>,
|
||||
/// The [`blocking`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-blocking) attribute.
|
||||
#[prop(optional, into)]
|
||||
blocking: Option<String>,
|
||||
/// The content of the `<script>` tag.
|
||||
#[prop(optional)]
|
||||
children: Option<Box<dyn FnOnce(Scope) -> Fragment>>,
|
||||
) -> impl IntoView {
|
||||
let meta = use_head(cx);
|
||||
let next_id = meta.tags.get_next_id();
|
||||
let id = id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0));
|
||||
|
||||
let builder_el = leptos::script(cx)
|
||||
.attr("id", &id)
|
||||
.attr("async", async_)
|
||||
.attr("crossorigin", crossorigin)
|
||||
.attr("defer", defer)
|
||||
.attr("fetchpriority ", fetchpriority)
|
||||
.attr("integrity", integrity)
|
||||
.attr("nomodule", nomodule)
|
||||
.attr("nonce", nonce)
|
||||
.attr("referrerpolicy", referrerpolicy)
|
||||
.attr("src", src)
|
||||
.attr("type", type_)
|
||||
.attr("blocking", blocking);
|
||||
let builder_el = if let Some(children) = children {
|
||||
let frag = children(cx);
|
||||
let mut script = String::new();
|
||||
for node in frag.nodes {
|
||||
match node {
|
||||
View::Text(text) => script.push_str(&text.content),
|
||||
_ => leptos::warn!("Only text nodes are supported as children of <Script/>."),
|
||||
}
|
||||
}
|
||||
builder_el.child(script)
|
||||
} else {
|
||||
builder_el
|
||||
};
|
||||
|
||||
meta.tags.register(cx, id, builder_el.into_any());
|
||||
}
|
||||
70
meta/src/style.rs
Normal file
70
meta/src/style.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use crate::use_head;
|
||||
use leptos::*;
|
||||
|
||||
/// Injects an [HTMLStyleElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLStyleElement) into the document
|
||||
/// head, accepting any of the valid attributes for that tag.
|
||||
/// ```
|
||||
/// use leptos::*;
|
||||
/// use leptos_meta::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> impl IntoView {
|
||||
/// provide_meta_context(cx);
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Style>
|
||||
/// "body { font-weight: bold; }"
|
||||
/// </Style>
|
||||
/// </main>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[component(transparent)]
|
||||
pub fn Style(
|
||||
cx: Scope,
|
||||
/// An ID for the `<script>` tag.
|
||||
#[prop(optional, into)]
|
||||
id: Option<String>,
|
||||
/// The [`media`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style#attr-media) attribute.
|
||||
#[prop(optional, into)]
|
||||
media: Option<String>,
|
||||
/// The [`nonce`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style#attr-nonce) attribute.
|
||||
#[prop(optional, into)]
|
||||
nonce: Option<String>,
|
||||
/// The [`title`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style#attr-title) attribute.
|
||||
#[prop(optional, into)]
|
||||
title: Option<String>,
|
||||
/// The [`blocking`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style#attr-blocking) attribute.
|
||||
#[prop(optional, into)]
|
||||
blocking: Option<String>,
|
||||
/// The content of the `<style>` tag.
|
||||
#[prop(optional)]
|
||||
children: Option<Box<dyn FnOnce(Scope) -> Fragment>>,
|
||||
) -> impl IntoView {
|
||||
let meta = use_head(cx);
|
||||
let next_id = meta.tags.get_next_id();
|
||||
let id = id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0));
|
||||
|
||||
let builder_el = leptos::style(cx)
|
||||
.attr("id", &id)
|
||||
.attr("media", media)
|
||||
.attr("nonce", nonce)
|
||||
.attr("title", title)
|
||||
.attr("blocking", blocking);
|
||||
let builder_el = if let Some(children) = children {
|
||||
let frag = children(cx);
|
||||
let mut style = String::new();
|
||||
for node in frag.nodes {
|
||||
match node {
|
||||
View::Text(text) => style.push_str(&text.content),
|
||||
_ => leptos::warn!("Only text nodes are supported as children of <Style/>."),
|
||||
}
|
||||
}
|
||||
builder_el.child(style)
|
||||
} else {
|
||||
builder_el
|
||||
};
|
||||
|
||||
meta.tags.register(cx, id, builder_el.into_any());
|
||||
}
|
||||
@@ -1,51 +1,5 @@
|
||||
use crate::use_head;
|
||||
use cfg_if::cfg_if;
|
||||
use crate::{Link, LinkProps};
|
||||
use leptos::*;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashMap,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
/// Manages all of the stylesheets set by [Stylesheet] components.
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct StylesheetContext {
|
||||
#[allow(clippy::type_complexity)]
|
||||
// key is (id, href)
|
||||
els: Rc<RefCell<HashMap<StyleSheetData, Option<web_sys::HtmlLinkElement>>>>,
|
||||
next_id: Rc<Cell<StylesheetId>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||
struct StylesheetId(usize);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
struct StyleSheetData {
|
||||
id: String,
|
||||
href: String,
|
||||
}
|
||||
|
||||
impl StylesheetContext {
|
||||
fn get_next_id(&self) -> StylesheetId {
|
||||
let current_id = self.next_id.get();
|
||||
let next_id = StylesheetId(current_id.0 + 1);
|
||||
self.next_id.set(next_id);
|
||||
next_id
|
||||
}
|
||||
}
|
||||
|
||||
impl StylesheetContext {
|
||||
/// Converts the set of stylesheets into an HTML string that can be injected into the `<head>`.
|
||||
pub fn as_string(&self) -> String {
|
||||
self.els
|
||||
.borrow()
|
||||
.iter()
|
||||
.map(|(StyleSheetData { id, href }, _)| {
|
||||
format!(r#"<link rel="stylesheet" id="{id}" href="{href}">"#)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Injects an [HTMLLinkElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document
|
||||
/// head that loads a stylesheet from the URL given by the `href` property.
|
||||
@@ -75,50 +29,13 @@ pub fn Stylesheet(
|
||||
#[prop(optional, into)]
|
||||
id: Option<String>,
|
||||
) -> impl IntoView {
|
||||
let meta = use_head(cx);
|
||||
let stylesheets = &meta.stylesheets;
|
||||
let next_id = stylesheets.get_next_id();
|
||||
let id = id.unwrap_or_else(|| format!("leptos-style-{}", next_id.0));
|
||||
let key = StyleSheetData { id, href };
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
use leptos::document;
|
||||
|
||||
let element_to_hydrate = document().get_element_by_id(&key.id);
|
||||
|
||||
let el = element_to_hydrate.unwrap_or_else(|| {
|
||||
let el = document().create_element("link").unwrap_throw();
|
||||
el.set_attribute("rel", "stylesheet").unwrap_throw();
|
||||
el.set_attribute("id", &key.id).unwrap_throw();
|
||||
el.set_attribute("href", &key.href).unwrap_throw();
|
||||
let head = document().head().unwrap_throw();
|
||||
head
|
||||
.append_child(el.unchecked_ref())
|
||||
.unwrap_throw();
|
||||
|
||||
el
|
||||
});
|
||||
|
||||
on_cleanup(cx, {
|
||||
let el = el.clone();
|
||||
let els = meta.stylesheets.els.clone();
|
||||
let key = key.clone();
|
||||
move || {
|
||||
let head = document().head().unwrap_throw();
|
||||
_ = head.remove_child(&el);
|
||||
els.borrow_mut().remove(&key);
|
||||
}
|
||||
});
|
||||
|
||||
meta.stylesheets
|
||||
.els
|
||||
.borrow_mut()
|
||||
.insert(key, Some(el.unchecked_into()));
|
||||
|
||||
} else {
|
||||
let meta = use_head(cx);
|
||||
meta.stylesheets.els.borrow_mut().insert(key, None);
|
||||
if let Some(id) = id {
|
||||
view! { cx,
|
||||
<Link id rel="stylesheet" href/>
|
||||
}
|
||||
} else {
|
||||
view! { cx,
|
||||
<Link rel="stylesheet" href/>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ repository = "https://github.com/gbj/leptos"
|
||||
description = "Router for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../leptos", version = "0.1.0-beta", default-features = false }
|
||||
leptos.workspace = true
|
||||
cfg-if = "1"
|
||||
common_macros = "0.1"
|
||||
gloo-net = "0.2"
|
||||
@@ -53,7 +53,7 @@ features = [
|
||||
]
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
default = []
|
||||
csr = ["leptos/csr"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = ["leptos/ssr", "dep:url", "dep:regex"]
|
||||
@@ -62,3 +62,4 @@ stable = ["leptos/stable"]
|
||||
[package.metadata.cargo-all-features]
|
||||
# No need to test optional dependencies as they are enabled by the ssr feature
|
||||
denylist = ["url", "regex", "stable"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
@@ -36,7 +36,7 @@ pub fn Form<A>(
|
||||
#[prop(optional)]
|
||||
on_response: Option<Rc<dyn Fn(&web_sys::Response)>>,
|
||||
/// Component children; should include the HTML of the form elements.
|
||||
children: Box<dyn Fn(Scope) -> Fragment>,
|
||||
children: Box<dyn FnOnce(Scope) -> Fragment>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
A: ToHref + 'static,
|
||||
@@ -134,7 +134,7 @@ pub fn ActionForm<I, O>(
|
||||
/// manually using [leptos_server::Action::using_server_fn].
|
||||
action: Action<I, Result<O, ServerFnError>>,
|
||||
/// Component children; should include the HTML of the form elements.
|
||||
children: Box<dyn Fn(Scope) -> Fragment>,
|
||||
children: Box<dyn FnOnce(Scope) -> Fragment>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
I: Clone + ServerFn + 'static,
|
||||
@@ -208,7 +208,7 @@ pub fn MultiActionForm<I, O>(
|
||||
/// manually using [leptos_server::Action::using_server_fn].
|
||||
action: MultiAction<I, Result<O, ServerFnError>>,
|
||||
/// Component children; should include the HTML of the form elements.
|
||||
children: Box<dyn Fn(Scope) -> Fragment>,
|
||||
children: Box<dyn FnOnce(Scope) -> Fragment>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
I: Clone + ServerFn + 'static,
|
||||
|
||||
@@ -58,7 +58,7 @@ pub fn A<H>(
|
||||
#[prop(optional, into)]
|
||||
class: Option<MaybeSignal<String>>,
|
||||
/// The nodes or elements to be shown inside the link.
|
||||
children: Box<dyn Fn(Scope) -> Fragment>,
|
||||
children: Box<dyn FnOnce(Scope) -> Fragment>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
H: ToHref + 'static,
|
||||
|
||||
@@ -26,7 +26,7 @@ pub fn Route<E, F, P>(
|
||||
view: F,
|
||||
/// `children` may be empty or include nested routes.
|
||||
#[prop(optional)]
|
||||
children: Option<Box<dyn Fn(Scope) -> Fragment>>,
|
||||
children: Option<Box<dyn FnOnce(Scope) -> Fragment>>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
E: IntoView,
|
||||
|
||||
@@ -32,7 +32,7 @@ pub fn Router(
|
||||
/// The `<Router/>` should usually wrap your whole page. It can contain
|
||||
/// any elements, and should include a [Routes](crate::Routes) component somewhere
|
||||
/// to define and display [Route](crate::Route)s.
|
||||
children: Box<dyn Fn(Scope) -> Fragment>,
|
||||
children: Box<dyn FnOnce(Scope) -> Fragment>,
|
||||
) -> impl IntoView {
|
||||
// create a new RouterContext and provide it to every component beneath the router
|
||||
let router = RouterContext::new(cx, base, fallback);
|
||||
@@ -104,10 +104,10 @@ impl RouterContext {
|
||||
value: base_path.to_string(),
|
||||
replace: true,
|
||||
scroll: false,
|
||||
state: State(None)
|
||||
state: State(None),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the current URL
|
||||
let (reference, set_reference) = create_signal(cx, source.with(|s| s.value.clone()));
|
||||
|
||||
@@ -22,7 +22,7 @@ use crate::{
|
||||
pub fn Routes(
|
||||
cx: Scope,
|
||||
#[prop(optional)] base: Option<String>,
|
||||
children: Box<dyn Fn(Scope) -> Fragment>,
|
||||
children: Box<dyn FnOnce(Scope) -> Fragment>,
|
||||
) -> impl IntoView {
|
||||
let router = use_context::<RouterContext>(cx).unwrap_or_else(|| {
|
||||
log::warn!("<Routes/> component should be nested within a <Router/>.");
|
||||
|
||||
@@ -118,7 +118,7 @@ where
|
||||
Some(value) => match T::from_str(value) {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
eprintln!("{e}");
|
||||
Err(ParamsError::Params(Rc::new(e)))
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user