mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 09:02:37 -05:00
Compare commits
73 Commits
v0.1.0-bet
...
ci-disk-sp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05d2eb8ce0 | ||
|
|
e12c2d9769 | ||
|
|
825245b65f | ||
|
|
ef067f18e1 | ||
|
|
844dc21efd | ||
|
|
1a00e99a24 | ||
|
|
6c5bcf30ba | ||
|
|
be8ffe935d | ||
|
|
0b80bba4ec | ||
|
|
9cc38988d8 | ||
|
|
0d92a5dec8 | ||
|
|
0029e1d8f7 | ||
|
|
635aa5c681 | ||
|
|
f5c4c9448c | ||
|
|
bc43a9d329 | ||
|
|
1850c28d3a | ||
|
|
319a058e63 | ||
|
|
678e49268f | ||
|
|
6df4a6f120 | ||
|
|
73c6bbb225 | ||
|
|
fa57085946 | ||
|
|
aef589cd24 | ||
|
|
05ffd8c989 | ||
|
|
1125a5f7cb | ||
|
|
dfba1d9656 | ||
|
|
96418ed684 | ||
|
|
b010233bb4 | ||
|
|
7e28f56f01 | ||
|
|
dd35c31db1 | ||
|
|
c9ac4ed2b5 | ||
|
|
7631ce3b09 | ||
|
|
a5988c59ee | ||
|
|
9ba807f79b | ||
|
|
9d8627b337 | ||
|
|
64bf01c59e | ||
|
|
ed023c8970 | ||
|
|
13bdef22bd | ||
|
|
6f49a6c12a | ||
|
|
7db292779b | ||
|
|
e2496e01d0 | ||
|
|
d5bda04306 | ||
|
|
9165242744 | ||
|
|
927fe0949f | ||
|
|
459216a30e | ||
|
|
c7fa041469 | ||
|
|
cab7360bef | ||
|
|
159ec4a7bd | ||
|
|
ae40f3134a | ||
|
|
3c080e0564 | ||
|
|
e8c1bf5055 | ||
|
|
2a4a5f75c9 | ||
|
|
91b65654d6 | ||
|
|
dcca6e4e17 | ||
|
|
4550545e4f | ||
|
|
af1a4492e8 | ||
|
|
6b1b4463a0 | ||
|
|
632267c13a | ||
|
|
a349707e1f | ||
|
|
84fa6cd3a8 | ||
|
|
05468d3307 | ||
|
|
0da88f39cd | ||
|
|
5dffb0a803 | ||
|
|
e2a5c2d78f | ||
|
|
ca679ec496 | ||
|
|
10282857fe | ||
|
|
263d5b1d89 | ||
|
|
6a4cbbf266 | ||
|
|
8d14972808 | ||
|
|
441eb1697e | ||
|
|
64e6eedb4d | ||
|
|
78d965cc91 | ||
|
|
28dce925b0 | ||
|
|
1344f113c5 |
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
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use actix_web::{http::header::HeaderMap, web::Bytes, *};
|
||||
use actix_web::{http::header, web::Bytes, *};
|
||||
use futures::{StreamExt};
|
||||
|
||||
use http::StatusCode;
|
||||
@@ -12,10 +12,21 @@ use tokio::sync::RwLock;
|
||||
/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ResponseParts {
|
||||
pub headers: HeaderMap,
|
||||
pub headers: header::HeaderMap,
|
||||
pub status: Option<StatusCode>,
|
||||
}
|
||||
|
||||
impl ResponseParts{
|
||||
/// Insert a header, overwriting any previous value with the same key
|
||||
pub fn insert_header(&mut self, key: header::HeaderName, value: header::HeaderValue){
|
||||
self.headers.insert(key, value);
|
||||
}
|
||||
/// Append a header, leaving any header with the same key intact
|
||||
pub fn append_header(&mut self, key: header::HeaderName, value: header::HeaderValue){
|
||||
self.headers.append(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adding this Struct to your Scope inside of a Server Fn or Elements will allow you to override details of the Response
|
||||
/// like StatusCode and add Headers/Cookies. Because Elements and Server Fns are lower in the tree than the Response generation
|
||||
/// code, it needs to be wrapped in an `Arc<RwLock<>>` so that it can be surfaced
|
||||
@@ -28,6 +39,33 @@ impl ResponseOptions {
|
||||
let mut writable = self.0.write().await;
|
||||
*writable = parts
|
||||
}
|
||||
/// Set the status of the returned Response
|
||||
pub async fn set_status(&self, status: StatusCode){
|
||||
let mut writeable = self.0.write().await;
|
||||
let res_parts = &mut*writeable;
|
||||
res_parts.status = Some(status);
|
||||
}
|
||||
/// Insert a header, overwriting any previous value with the same key
|
||||
pub async fn insert_header(&self, key: header::HeaderName, value: header::HeaderValue){
|
||||
let mut writeable = self.0.write().await;
|
||||
let res_parts = &mut*writeable;
|
||||
res_parts.headers.insert(key, value);
|
||||
}
|
||||
/// Append a header, leaving any header with the same key intact
|
||||
pub async fn append_header(&self, key: header::HeaderName, value: header::HeaderValue){
|
||||
let mut writeable = self.0.write().await;
|
||||
let res_parts = &mut*writeable;
|
||||
res_parts.headers.append(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
|
||||
/// it sets a StatusCode of 302 and a LOCATION header with the provided value.
|
||||
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead
|
||||
pub async fn redirect(cx: leptos::Scope, path: &str){
|
||||
let response_options = use_context::<ResponseOptions>(cx).unwrap();
|
||||
response_options.set_status(StatusCode::FOUND).await;
|
||||
response_options.insert_header(header::LOCATION, header::HeaderValue::from_str(path).expect("Failed to create HeaderValue")).await;
|
||||
}
|
||||
|
||||
/// An Actix [Route](actix_web::Route) that listens for a `POST` request with
|
||||
@@ -147,7 +185,9 @@ pub fn handle_server_fns() -> Route {
|
||||
}
|
||||
} else {
|
||||
HttpResponse::BadRequest()
|
||||
.body(format!("Could not find a server function at that route."))
|
||||
.body(format!("Could not find a server function at the route {:?}. \
|
||||
\n\nIt's likely that you need to call ServerFn::register() on the \
|
||||
server function type, somewhere in your `main` function.", req.path()))
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -259,7 +299,7 @@ where IV: IntoView
|
||||
<script crossorigin="">(function () {{
|
||||
var ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
|
||||
ws.onmessage = (ev) => {{
|
||||
let msg = JSON.parse(event.data);
|
||||
let msg = JSON.parse(ev.data);
|
||||
if (msg.all) window.location.reload();
|
||||
if (msg.css) {{
|
||||
const link = document.querySelector("link#leptos");
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use axum::{
|
||||
body::{Body, Bytes, Full, StreamBody},
|
||||
extract::Path,
|
||||
http::{HeaderMap, HeaderValue, Request, StatusCode},
|
||||
http::{header::HeaderName, header::HeaderValue, HeaderMap, Request, StatusCode},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use futures::{Future, SinkExt, Stream, StreamExt};
|
||||
use http::{method::Method, uri::Uri, version::Version, Response};
|
||||
use http::{header, method::Method, uri::Uri, version::Version, Response};
|
||||
use hyper::body;
|
||||
use leptos::*;
|
||||
use leptos_meta::MetaContext;
|
||||
@@ -31,6 +31,17 @@ pub struct ResponseParts {
|
||||
pub headers: HeaderMap,
|
||||
}
|
||||
|
||||
impl ResponseParts {
|
||||
/// Insert a header, overwriting any previous value with the same key
|
||||
pub fn insert_header(&mut self, key: HeaderName, value: HeaderValue) {
|
||||
self.headers.insert(key, value);
|
||||
}
|
||||
/// Append a header, leaving any header with the same key intact
|
||||
pub fn append_header(&mut self, key: HeaderName, value: HeaderValue) {
|
||||
self.headers.append(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adding this Struct to your Scope inside of a Server Fn or Element will allow you to override details of the Response
|
||||
/// like status and add Headers/Cookies. Because Elements and Server Fns are lower in the tree than the Response generation
|
||||
/// code, it needs to be wrapped in an `Arc<RwLock<>>` so that it can be surfaced.
|
||||
@@ -38,11 +49,43 @@ pub struct ResponseParts {
|
||||
pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>);
|
||||
|
||||
impl ResponseOptions {
|
||||
/// A less boilerplatey way to overwrite the default contents of `ResponseOptions` with a new `ResponseParts`
|
||||
/// A less boilerplatey way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`
|
||||
pub async fn overwrite(&self, parts: ResponseParts) {
|
||||
let mut writable = self.0.write().await;
|
||||
*writable = parts
|
||||
}
|
||||
/// Set the status of the returned Response
|
||||
pub async fn set_status(&self, status: StatusCode) {
|
||||
let mut writeable = self.0.write().await;
|
||||
let res_parts = &mut *writeable;
|
||||
res_parts.status = Some(status);
|
||||
}
|
||||
/// Insert a header, overwriting any previous value with the same key
|
||||
pub async fn insert_header(&self, key: HeaderName, value: HeaderValue) {
|
||||
let mut writeable = self.0.write().await;
|
||||
let res_parts = &mut *writeable;
|
||||
res_parts.headers.insert(key, value);
|
||||
}
|
||||
/// Append a header, leaving any header with the same key intact
|
||||
pub async fn append_header(&self, key: HeaderName, value: HeaderValue) {
|
||||
let mut writeable = self.0.write().await;
|
||||
let res_parts = &mut *writeable;
|
||||
res_parts.headers.append(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
|
||||
/// it sets a StatusCode of 302 and a LOCATION header with the provided value.
|
||||
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead
|
||||
pub async fn redirect(cx: leptos::Scope, path: &str) {
|
||||
let response_options = use_context::<ResponseOptions>(cx).unwrap();
|
||||
response_options.set_status(StatusCode::FOUND).await;
|
||||
response_options
|
||||
.insert_header(
|
||||
header::LOCATION,
|
||||
header::HeaderValue::from_str(path).expect("Failed to create HeaderValue"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
|
||||
@@ -194,7 +237,9 @@ pub async fn handle_server_fns(
|
||||
Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body(Full::from(
|
||||
"Could not find a server function at that route.".to_string(),
|
||||
format!("Could not find a server function at the route {:?}. \
|
||||
\n\nIt's likely that you need to call ServerFn::register() on the \
|
||||
server function type, somewhere in your `main` function.", fn_name)
|
||||
))
|
||||
}
|
||||
.expect("could not build Response");
|
||||
@@ -316,7 +361,7 @@ where
|
||||
<script crossorigin="">(function () {{
|
||||
var ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
|
||||
ws.onmessage = (ev) => {{
|
||||
let msg = JSON.parse(event.data);
|
||||
let msg = JSON.parse(ev.data);
|
||||
if (msg.all) window.location.reload();
|
||||
if (msg.css) {{
|
||||
const link = document.querySelector("link#leptos");
|
||||
|
||||
@@ -24,28 +24,28 @@ leptos = { path = ".", default-features = false }
|
||||
[features]
|
||||
default = ["csr", "serde"]
|
||||
csr = [
|
||||
"leptos_dom/web",
|
||||
"leptos_macro/csr",
|
||||
"leptos_reactive/csr",
|
||||
"leptos_server/csr",
|
||||
"leptos_dom/web",
|
||||
"leptos_macro/csr",
|
||||
"leptos_reactive/csr",
|
||||
"leptos_server/csr",
|
||||
]
|
||||
hydrate = [
|
||||
"leptos_dom/web",
|
||||
"leptos_macro/hydrate",
|
||||
"leptos_reactive/hydrate",
|
||||
"leptos_server/hydrate",
|
||||
"leptos_dom/web",
|
||||
"leptos_macro/hydrate",
|
||||
"leptos_reactive/hydrate",
|
||||
"leptos_server/hydrate",
|
||||
]
|
||||
ssr = [
|
||||
"leptos_dom/ssr",
|
||||
"leptos_macro/ssr",
|
||||
"leptos_reactive/ssr",
|
||||
"leptos_server/ssr",
|
||||
"leptos_dom/ssr",
|
||||
"leptos_macro/ssr",
|
||||
"leptos_reactive/ssr",
|
||||
"leptos_server/ssr",
|
||||
]
|
||||
stable = [
|
||||
"leptos_dom/stable",
|
||||
"leptos_macro/stable",
|
||||
"leptos_reactive/stable",
|
||||
"leptos_server/stable",
|
||||
"leptos_dom/stable",
|
||||
"leptos_macro/stable",
|
||||
"leptos_reactive/stable",
|
||||
"leptos_server/stable",
|
||||
]
|
||||
serde = ["leptos_reactive/serde"]
|
||||
serde-lite = ["leptos_reactive/serde-lite"]
|
||||
@@ -53,7 +53,7 @@ miniserde = ["leptos_reactive/miniserde"]
|
||||
tracing = ["leptos_macro/tracing"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable"]
|
||||
denylist = ["stable", "tracing"]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"csr",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
//! 1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.0", features = ["stable"] }`
|
||||
//! 2. `nightly` enables the function call syntax for accessing and setting signals. If you’re using `stable`,
|
||||
//! you’ll just call `.get()`, `.set()`, or `.update()` manually. Check out the
|
||||
//! [`counters-stable` example](https://github.com/gbj/leptos/blob/main/examples/counters-stable/src/main.rs)
|
||||
//! [`counters_stable` example](https://github.com/gbj/leptos/blob/main/examples/counters_stable/src/main.rs)
|
||||
//! for examples of the correct API.
|
||||
//!
|
||||
//! # Learning by Example
|
||||
@@ -29,7 +29,7 @@
|
||||
//! counter example, showing the basics of client-side rendering and reactive DOM updates
|
||||
//! - [`counters`](https://github.com/gbj/leptos/tree/main/examples/counter) introduces parent-child
|
||||
//! communication via contexts, and the `<For/>` component for efficient keyed list updates.
|
||||
//! - [`parent-child`](https://github.com/gbj/leptos/tree/main/examples/parent-child) shows four different
|
||||
//! - [`parent_child`](https://github.com/gbj/leptos/tree/main/examples/parent_child) shows four different
|
||||
//! ways a parent component can communicate with a child, including passing a closure, context, and more
|
||||
//! - [`todomvc`](https://github.com/gbj/leptos/tree/main/examples/todomvc) implements the classic to-do
|
||||
//! app in Leptos. This is a good example of a complete, simple app. In particular, you might want to
|
||||
@@ -147,4 +147,4 @@ pub use transition::*;
|
||||
|
||||
pub use leptos_reactive::debug_warn;
|
||||
|
||||
extern crate self as leptos;
|
||||
extern crate self as leptos;
|
||||
|
||||
@@ -12,7 +12,7 @@ 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"
|
||||
@@ -140,6 +140,11 @@ features = [
|
||||
]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
web = ["leptos_reactive/csr"]
|
||||
ssr = ["leptos_reactive/ssr"]
|
||||
stable = ["leptos_reactive/stable"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable"]
|
||||
skip_feature_sets = [["web", "ssr"]]
|
||||
|
||||
@@ -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() {
|
||||
@@ -58,6 +59,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 +73,23 @@ 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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -85,24 +85,22 @@ macro_rules! generate_event_types {
|
||||
),* $(,)?} => {
|
||||
|
||||
$(
|
||||
#[doc = "The "]
|
||||
#[doc = stringify!($event)]
|
||||
#[doc = " event."]
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct $event;
|
||||
#[doc = concat!("The `", stringify!($event), "` event, which receives [", stringify!($web_sys_event), "](web_sys::", stringify!($web_sys_event), ") as its argument.")]
|
||||
#[derive(Copy, Clone)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub struct $event;
|
||||
|
||||
impl EventDescriptor for $event {
|
||||
type EventType = web_sys::$web_sys_event;
|
||||
impl EventDescriptor for $event {
|
||||
type EventType = web_sys::$web_sys_event;
|
||||
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
stringify!($event).into()
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
stringify!($event).into()
|
||||
}
|
||||
|
||||
$(
|
||||
generate_event_types!($does_not_bubble);
|
||||
)?
|
||||
}
|
||||
|
||||
$(
|
||||
generate_event_types!($does_not_bubble);
|
||||
)?
|
||||
}
|
||||
)*
|
||||
};
|
||||
|
||||
|
||||
@@ -71,8 +71,8 @@ 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();
|
||||
pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
|
||||
let cb = Closure::once_into_js(cb);
|
||||
_ = window().request_animation_frame(cb.as_ref().unchecked_ref());
|
||||
}
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ 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"))] {
|
||||
@@ -346,6 +346,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(
|
||||
|
||||
@@ -10,7 +10,8 @@ pub extern crate tracing;
|
||||
mod components;
|
||||
mod events;
|
||||
mod helpers;
|
||||
mod html;
|
||||
#[doc(hidden)]
|
||||
pub mod html;
|
||||
mod hydration;
|
||||
mod logging;
|
||||
mod macro_helpers;
|
||||
@@ -191,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"))]
|
||||
{
|
||||
@@ -326,7 +327,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 {
|
||||
|
||||
@@ -12,6 +12,7 @@ proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
cfg-if = "1"
|
||||
doc-comment = "0.3"
|
||||
itertools = "0.10"
|
||||
pad-adapter = "0.1"
|
||||
prettyplease = "0.1"
|
||||
@@ -40,4 +41,5 @@ stable = ["leptos_dom/stable", "leptos_reactive/stable"]
|
||||
tracing = []
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable"]
|
||||
denylist = ["stable", "tracing"]
|
||||
skip_feature_sets = [["csr", "hydrate"], ["hydrate", "csr"], ["hydrate", "ssr"]]
|
||||
|
||||
@@ -252,7 +252,7 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
/// When you use the component somewhere else, the names of its arguments are the names
|
||||
/// of the properties you use in the [view](mod@view) macro.
|
||||
///
|
||||
/// Every component function should have the return type `-> impl [IntoView](leptos_dom::IntoView)`.
|
||||
/// Every component function should have the return type `-> impl IntoView`.
|
||||
///
|
||||
/// You can add Rust doc comments to component function arguments and the macro will use them to
|
||||
/// generate documentation for the component.
|
||||
@@ -386,6 +386,51 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Customizing Properties
|
||||
/// You can use the `#[prop]` attribute on individual component properties (function arguments) to
|
||||
/// customize the types that component property can receive. You can use the following attributes:
|
||||
/// * `#[prop(into)]`: This will call `.into()` on any value passed into the component prop. (For example,
|
||||
/// you could apply `#[prop(into)]` to a prop that takes [Signal](leptos_reactive::Signal), which would
|
||||
/// allow users to pass a [ReadSignal](leptos_reactive::ReadSignal) or [RwSignal](leptos_reactive::RwSignal)
|
||||
/// and automatically convert it.)
|
||||
/// * `#[prop(optional)]`: If the user does not specify this property when they use the component,
|
||||
/// it will be set to its default value. If the property type is `Option<T>`, values should be passed
|
||||
/// as `name=T` and will be received as `Some(T)`.
|
||||
/// * `#[prop(optional_no_strip)]`: The same as `optional`, but requires values to be passed as `None` or
|
||||
/// `Some(T)` explicitly. This means that the optional property can be omitted (and be `None`), or explicitly
|
||||
/// specified as either `None` or `Some(T)`.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn MyComponent(
|
||||
/// cx: Scope,
|
||||
/// #[prop(into)]
|
||||
/// name: String,
|
||||
/// #[prop(optional)]
|
||||
/// optional_value: Option<i32>,
|
||||
/// #[prop(optional_no_strip)]
|
||||
/// optional_no_strip: Option<i32>
|
||||
/// ) -> impl IntoView {
|
||||
/// // whatever UI you need
|
||||
/// }
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn App(cx: Scope) -> impl IntoView {
|
||||
/// view! { cx,
|
||||
/// <MyComponent
|
||||
/// name="Greg" // automatically converted to String with `.into()`
|
||||
/// optional_value=42 // received as `Some(42)`
|
||||
/// optional_no_strip=Some(42) // received as `Some(42)`
|
||||
/// />
|
||||
/// <MyComponent
|
||||
/// name="Bob" // automatically converted to String with `.into()`
|
||||
/// // optional values can both be omitted, and received as `None`
|
||||
/// />
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_error::proc_macro_error]
|
||||
#[proc_macro_attribute]
|
||||
pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
@@ -413,6 +458,55 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Declares that a function is a [server function](leptos_server). This means that
|
||||
/// its body will only run on the server, i.e., when the `ssr` feature is enabled.
|
||||
///
|
||||
/// If you call a server function from the client (i.e., when the `csr` or `hydrate` features
|
||||
/// are enabled), it will instead make a network request to the server.
|
||||
///
|
||||
/// You can specify one, two, or three arguments to the server function:
|
||||
/// 1. **Required**: A type name that will be used to identify and register the server function
|
||||
/// (e.g., `MyServerFn`).
|
||||
/// 2. *Optional*: A URL prefix at which the function will be mounted when it’s registered
|
||||
/// (e.g., `"/api"`). Defaults to `"/"`.
|
||||
/// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for
|
||||
/// serialization) or `"Url"` (specifying that it should be use a URL-encoded form-data string).
|
||||
/// Defaults to `"Url"`. If you want to use this server function to power an
|
||||
/// [ActionForm](leptos_router::ActionForm) the encoding must be `"Url"`.
|
||||
///
|
||||
/// The server function itself can take any number of arguments, each of which should be serializable
|
||||
/// and deserializable with `serde`. Optionally, its first argument can be a Leptos [Scope](leptos::Scope),
|
||||
/// which will be injected *on the server side.* This can be used to inject the raw HTTP request or other
|
||||
/// server-side context into the server function.
|
||||
///
|
||||
/// ```ignore
|
||||
/// # use leptos::*; use serde::{Serialize, Deserialize};
|
||||
/// # #[derive(Serialize, Deserialize)]
|
||||
/// # pub struct Post { }
|
||||
/// #[server(ReadPosts, "/api")]
|
||||
/// pub async fn read_posts(how_many: u8, query: String) -> Result<Vec<Post>, ServerFnError> {
|
||||
/// // do some work on the server to access the database
|
||||
/// todo!()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Note the following:
|
||||
/// - You must **register** the server function by calling `T::register()` somewhere in your main function.
|
||||
/// - **Server functions must be `async`.** Even if the work being done inside the function body
|
||||
/// can run synchronously on the server, from the client’s perspective it involves an asynchronous
|
||||
/// function call.
|
||||
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
|
||||
/// inside the function body can’t fail, the processes of serialization/deserialization and the
|
||||
/// network call are fallible.
|
||||
/// - **Return types must be [Serializable](leptos_reactive::Serializable).**
|
||||
/// This should be fairly obvious: we have to serialize arguments to send them to the server, and we
|
||||
/// need to deserialize the result to return it to the client.
|
||||
/// - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded`
|
||||
/// form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
|
||||
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/).
|
||||
/// - **The [Scope](leptos_reactive::Scope) comes from the server.** Optionally, the first argument of a server function
|
||||
/// can be a Leptos [Scope](leptos_reactive::Scope). This scope can be used to inject dependencies like the HTTP request
|
||||
/// or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
|
||||
#[proc_macro_attribute]
|
||||
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
match server_macro_impl(args, s.into()) {
|
||||
|
||||
@@ -148,14 +148,14 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
||||
#encoding
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
fn call_fn(self, cx: ::leptos::Scope) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, ::leptos::ServerFnError>>>> {
|
||||
let #struct_name { #(#field_names),* } = self;
|
||||
#cx_assign_statement;
|
||||
Box::pin(async move { #fn_name( #cx_fn_arg #(#field_names_2),*).await })
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
#[cfg(any(not(feature = "ssr"), doc))]
|
||||
fn call_fn_client(self, cx: ::leptos::Scope) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, ::leptos::ServerFnError>>>> {
|
||||
let #struct_name { #(#field_names_3),* } = self;
|
||||
Box::pin(async move { #fn_name( #cx_fn_arg #(#field_names_4),*).await })
|
||||
|
||||
@@ -332,6 +332,10 @@ fn attribute_to_tokens_ssr(
|
||||
.as_ref()
|
||||
.expect("event listener attributes need a value")
|
||||
.as_ref();
|
||||
|
||||
#[allow(unused_variables)]
|
||||
let (name, is_force_undelegated) = parse_event(name);
|
||||
|
||||
let event_type = TYPED_EVENTS
|
||||
.iter()
|
||||
.find(|e| **e == name)
|
||||
@@ -342,7 +346,13 @@ fn attribute_to_tokens_ssr(
|
||||
.expect("couldn't parse event name");
|
||||
|
||||
exprs_for_compiler.push(quote! {
|
||||
leptos::ssr_event_listener(leptos::ev::#event_type, #handler);
|
||||
let event_type = if is_force_undelegated {
|
||||
quote! { ::leptos::ev::undelegated(::leptos::ev::#event_type) }
|
||||
} else {
|
||||
quote! { ::leptos::ev::#event_type }
|
||||
};
|
||||
|
||||
leptos::ssr_event_listener(#event_type, #handler);
|
||||
})
|
||||
} else if name.strip_prefix("prop:").is_some() || name.strip_prefix("class:").is_some() {
|
||||
// ignore props for SSR
|
||||
@@ -625,21 +635,82 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
|
||||
.as_ref()
|
||||
.expect("event listener attributes need a value")
|
||||
.as_ref();
|
||||
|
||||
let (name, is_force_undelegated) = parse_event(name);
|
||||
|
||||
let event_type = TYPED_EVENTS
|
||||
.iter()
|
||||
.find(|e| **e == name)
|
||||
.copied()
|
||||
.unwrap_or("Custom");
|
||||
let is_custom = event_type == "Custom";
|
||||
let event_type = event_type
|
||||
.parse::<TokenStream>()
|
||||
.expect("couldn't parse event name");
|
||||
|
||||
// the `on:event_name` is 1 single name. Ideally, we
|
||||
// would point `on:` to `.on`, but I don't know how to
|
||||
// get the "subspan" of a span, so this is good enough
|
||||
let event_type = if is_custom {
|
||||
quote! { Custom::new(#name) }
|
||||
} else {
|
||||
event_type
|
||||
};
|
||||
|
||||
let event_name_ident = match &node.key {
|
||||
NodeName::Punctuated(parts) => {
|
||||
if parts.len() >= 2 {
|
||||
Some(&parts[1])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let undelegated_ident = match &node.key {
|
||||
NodeName::Punctuated(parts) => parts.last().and_then(|last| {
|
||||
if last == "undelegated" {
|
||||
Some(last)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let on = match &node.key {
|
||||
NodeName::Punctuated(parts) => &parts[0],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let on = {
|
||||
let span = on.span();
|
||||
quote_spanned! {
|
||||
span => .on
|
||||
}
|
||||
};
|
||||
let event_type = if is_custom {
|
||||
event_type
|
||||
} else if let Some(ev_name) = event_name_ident {
|
||||
let span = ev_name.span();
|
||||
quote_spanned! {
|
||||
span => #ev_name
|
||||
}
|
||||
} else {
|
||||
event_type
|
||||
};
|
||||
|
||||
let event_type = if is_force_undelegated {
|
||||
let undelegated = if let Some(undelegated) = undelegated_ident {
|
||||
let span = undelegated.span();
|
||||
quote_spanned! {
|
||||
span => #undelegated
|
||||
}
|
||||
} else {
|
||||
quote! { undelegated }
|
||||
};
|
||||
quote! { ::leptos::ev::#undelegated(::leptos::ev::#event_type) }
|
||||
} else {
|
||||
quote! { ::leptos::ev::#event_type }
|
||||
};
|
||||
|
||||
quote! {
|
||||
.on(leptos::ev::#event_type, #handler)
|
||||
#on(#event_type, #handler)
|
||||
}
|
||||
} else if let Some(name) = name.strip_prefix("prop:") {
|
||||
let value = node
|
||||
@@ -647,8 +718,18 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
|
||||
.as_ref()
|
||||
.expect("prop: attributes need a value")
|
||||
.as_ref();
|
||||
let prop = match &node.key {
|
||||
NodeName::Punctuated(parts) => &parts[0],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let prop = {
|
||||
let span = prop.span();
|
||||
quote_spanned! {
|
||||
span => .prop
|
||||
}
|
||||
};
|
||||
quote! {
|
||||
.prop(#name, (#cx, #[allow(unused_braces)] #value))
|
||||
#prop(#name, (#cx, #[allow(unused_braces)] #value))
|
||||
}
|
||||
} else if let Some(name) = name.strip_prefix("class:") {
|
||||
let value = node
|
||||
@@ -656,8 +737,18 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
|
||||
.as_ref()
|
||||
.expect("class: attributes need a value")
|
||||
.as_ref();
|
||||
let class = match &node.key {
|
||||
NodeName::Punctuated(parts) => &parts[0],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let class = {
|
||||
let span = class.span();
|
||||
quote_spanned! {
|
||||
span => .class
|
||||
}
|
||||
};
|
||||
quote! {
|
||||
.class(#name, (#cx, #[allow(unused_braces)] #value))
|
||||
#class(#name, (#cx, #[allow(unused_braces)] #value))
|
||||
}
|
||||
} else {
|
||||
let name = name.replacen("attr:", "", 1);
|
||||
@@ -669,8 +760,22 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
|
||||
}
|
||||
None => quote_spanned! { span => "" },
|
||||
};
|
||||
let attr = match &node.key {
|
||||
NodeName::Punctuated(parts) => Some(&parts[0]),
|
||||
_ => None,
|
||||
};
|
||||
let attr = if let Some(attr) = attr {
|
||||
let span = attr.span();
|
||||
quote_spanned! {
|
||||
span => .attr
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
.attr
|
||||
}
|
||||
};
|
||||
quote! {
|
||||
.attr(#name, (#cx, #value))
|
||||
#attr(#name, (#cx, #value))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -931,3 +1036,11 @@ fn is_math_ml_element(tag: &str) -> bool {
|
||||
fn is_ambiguous_element(tag: &str) -> bool {
|
||||
matches!(tag, "a")
|
||||
}
|
||||
|
||||
fn parse_event(event_name: &str) -> (&str, bool) {
|
||||
if let Some(event_name) = event_name.strip_suffix(":undelegated") {
|
||||
(event_name, true)
|
||||
} else {
|
||||
(event_name, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = %format!("{:?}", 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 = %format!("{:?}", 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 = %format!("{:?}", 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 = %format!("{:?}", id),
|
||||
defined_at = %format!("{:?}", 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 = %format!("{:?}", 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 = %format!("{:?}", 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 = %format!("{:?}", self.0.id),
|
||||
defined_at = %format!("{:?}", 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 = %format!("{:?}", self.0.id),
|
||||
defined_at = %format!("{:?}", 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 = %format!("{:?}", self.0.id),
|
||||
defined_at = %format!("{:?}", 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 = %format!("{:?}", self.0.id),
|
||||
defined_at = %format!("{:?}", 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 = %format!("{:?}", 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 = %format!("{:?}", 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -155,7 +155,13 @@ impl Scope {
|
||||
// Internals
|
||||
|
||||
impl Scope {
|
||||
pub(crate) fn dispose(self) {
|
||||
/// Disposes of this reactive scope.
|
||||
///
|
||||
/// This will
|
||||
/// 1. dispose of all child `Scope`s
|
||||
/// 2. run all cleanup functions defined for this scope by [on_cleanup](crate::on_cleanup).
|
||||
/// 3. dispose of all signals, effects, and resources owned by this `Scope`.
|
||||
pub fn dispose(self) {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
// dispose of all child scopes
|
||||
let children = {
|
||||
@@ -282,9 +288,9 @@ impl Scope {
|
||||
with_runtime(self.runtime, |runtime| runtime.all_resources())
|
||||
}
|
||||
|
||||
/// Returns IDs for all [Resource](crate::Resource)s found on any scope that are
|
||||
/// pending from the server.
|
||||
pub fn pending_resources(&self) -> Vec<ResourceId> {
|
||||
/// Returns IDs for all [Resource](crate::Resource)s found on any scope that are
|
||||
/// pending from the server.
|
||||
pub fn pending_resources(&self) -> Vec<ResourceId> {
|
||||
with_runtime(self.runtime, |runtime| runtime.pending_resources())
|
||||
}
|
||||
|
||||
@@ -323,7 +329,7 @@ impl Scope {
|
||||
Box::pin(async move {
|
||||
rx.next().await;
|
||||
resolver()
|
||||
})
|
||||
}),
|
||||
),
|
||||
);
|
||||
})
|
||||
@@ -344,4 +350,4 @@ impl fmt::Debug for ScopeDisposer {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("ScopeDisposer").finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,18 @@ use thiserror::Error;
|
||||
/// # }).dispose();
|
||||
/// #
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
scope = %format!("{:?}", 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)));
|
||||
@@ -54,6 +66,16 @@ pub fn create_signal<T>(cx: Scope, value: T) -> (ReadSignal<T>, WriteSignal<T>)
|
||||
/// Creates a signal that always contains the most recent value emitted by a [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 = %format!("{:?}", cx.id),
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn create_signal_from_stream<T>(
|
||||
cx: Scope,
|
||||
mut stream: impl Stream<Item = T> + Unpin + 'static,
|
||||
@@ -120,9 +142,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 = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", self.defined_at),
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn get_untracked(&self) -> T
|
||||
where
|
||||
T: Clone,
|
||||
@@ -130,6 +167,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 = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", 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 +207,19 @@ where
|
||||
/// assert_eq!(first_char(), 'B');
|
||||
/// });
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "ReadSignal::with()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", 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 +247,19 @@ where
|
||||
/// assert_eq!(count(), 0);
|
||||
/// });
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "ReadSignal::get()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", self.defined_at),
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn get(&self) -> T
|
||||
where
|
||||
T: Clone,
|
||||
@@ -219,6 +295,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 +376,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 = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", 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 = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", 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 = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", 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 +461,19 @@ where
|
||||
/// assert_eq!(count(), 1);
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
name = "WriteSignal::update()",
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
id = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", 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 +499,19 @@ where
|
||||
/// assert_eq!(count(), 2);
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "WriteSignal::update_returning()"
|
||||
skip_all,
|
||||
fields(
|
||||
id = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", 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 +535,19 @@ where
|
||||
/// assert_eq!(count(), 1);
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "WriteSignal::set()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", 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 +559,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 +620,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 +665,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 +675,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 +684,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 = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", self.defined_at),
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn get_untracked(&self) -> T
|
||||
where
|
||||
T: Clone,
|
||||
@@ -518,21 +705,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 = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", 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 = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", 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 = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", 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 = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", 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 +800,19 @@ where
|
||||
/// # }).dispose();
|
||||
/// #
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::with()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", 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 +830,20 @@ where
|
||||
/// assert_eq!(count(), 0);
|
||||
/// # }).dispose();
|
||||
/// #
|
||||
/// ```
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::get()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", self.defined_at),
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn get(&self) -> T
|
||||
where
|
||||
T: Clone,
|
||||
@@ -603,6 +868,19 @@ where
|
||||
/// assert_eq!(count(), 1);
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::update()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", 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 +904,19 @@ where
|
||||
/// assert_eq!(count(), 2);
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::update_returning()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", 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 +935,19 @@ where
|
||||
/// assert_eq!(count(), 1);
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::set()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", 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 +968,27 @@ where
|
||||
/// assert_eq!(read_count(), 1);
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::read_only()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", 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 +1006,27 @@ where
|
||||
/// assert_eq!(count(), 1);
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::write_only()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", 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 +1043,53 @@ where
|
||||
/// assert_eq!(get_count(), 1);
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
name = "RwSignal::split()",
|
||||
skip_all,
|
||||
fields(
|
||||
id = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", 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 = %format!("{:?}", self.id),
|
||||
defined_at = %format!("{:?}", self.defined_at),
|
||||
ty = %std::any::type_name::<T>()
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn to_stream(&self) -> impl Stream<Item = T>
|
||||
where
|
||||
T: Clone,
|
||||
@@ -781,10 +1148,18 @@ impl SignalId {
|
||||
pub(crate) fn subscribe(&self, runtime: &Runtime) {
|
||||
// add subscriber
|
||||
if let Some(observer) = runtime.observer.get() {
|
||||
// add this observer to the signal's dependencies (to allow notification)
|
||||
let mut subs = runtime.signal_subscribers.borrow_mut();
|
||||
if let Some(subs) = subs.entry(*self) {
|
||||
subs.or_default().borrow_mut().insert(observer);
|
||||
}
|
||||
|
||||
// add this signal to the effect's sources (to allow cleanup)
|
||||
let mut effect_sources = runtime.effect_sources.borrow_mut();
|
||||
if let Some(effect_sources) = effect_sources.entry(observer) {
|
||||
let sources = effect_sources.or_default();
|
||||
sources.borrow_mut().insert(*self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,13 +27,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 +59,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 +67,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 +102,30 @@ where
|
||||
/// assert_eq!(above_3(&double_count), true);
|
||||
/// # });
|
||||
/// ```
|
||||
#[track_caller]
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
cx = %format!("{:?}", 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 +157,19 @@ where
|
||||
/// assert_eq!(memoized_lower(), "alice");
|
||||
/// });
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
defined_at = %format!("{:?}", 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,33 +198,68 @@ where
|
||||
/// assert_eq!(above_3(&memoized_double_count.into()), true);
|
||||
/// # });
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
defined_at = %format!("{:?}", 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()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a signal that yields the default value of `T` when
|
||||
/// you call `.get()` or `signal()`.
|
||||
pub fn default(cx: Scope) -> Self
|
||||
where
|
||||
T: Default,
|
||||
{
|
||||
Self::derive(cx, || Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,13 +28,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 {
|
||||
inner: SignalSetterTypes::Default,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,11 +85,23 @@ where
|
||||
/// assert_eq!(count(), 8);
|
||||
/// # });
|
||||
/// ```
|
||||
#[track_caller]
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
cx = %format!("{:?}", 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.
|
||||
@@ -92,23 +124,45 @@ where
|
||||
/// set_to_4(&set_double_count);
|
||||
/// assert_eq!(count(), 8);
|
||||
/// # });
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
defined_at = %format!("{:?}", 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 => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +172,7 @@ where
|
||||
{
|
||||
Write(WriteSignal<T>),
|
||||
Mapped(Scope, StoredValue<Box<dyn Fn(T)>>),
|
||||
Default,
|
||||
}
|
||||
|
||||
impl<T> Clone for SignalSetterTypes<T> {
|
||||
@@ -125,6 +180,7 @@ impl<T> Clone for SignalSetterTypes<T> {
|
||||
match self {
|
||||
Self::Write(arg0) => Self::Write(*arg0),
|
||||
Self::Mapped(cx, f) => Self::Mapped(*cx, *f),
|
||||
Self::Default => Self::Default,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,6 +195,7 @@ where
|
||||
match self {
|
||||
Self::Write(arg0) => f.debug_tuple("WriteSignal").field(arg0).finish(),
|
||||
Self::Mapped(_, _) => f.debug_tuple("Mapped").finish(),
|
||||
Self::Default => f.debug_tuple("SignalSetter<Default>").finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
//! indicate that it should only run on the server (i.e., when you have an `ssr` feature in your
|
||||
//! crate that is enabled).
|
||||
//!
|
||||
//! **Important**: All server functions must be registered by calling [ServerFn::register]
|
||||
//! somewhere within your `main` function.
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! # use leptos::*;
|
||||
//! #[server(ReadFromDB)]
|
||||
@@ -47,6 +50,11 @@
|
||||
//! log::debug!("posts = {posts{:#?}");
|
||||
//! })
|
||||
//! # });
|
||||
//!
|
||||
//! // make sure you've registered it somewhere in main
|
||||
//! fn main() {
|
||||
//! _ = ReadFromDB::register();
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! If you call this function from the client, it will serialize the function arguments and `POST`
|
||||
@@ -165,6 +173,15 @@ pub fn server_fn_by_path(path: &str) -> Option<Arc<ServerFnTraitObj>> {
|
||||
.and_then(|fns| fns.get(path).cloned())
|
||||
}
|
||||
|
||||
/// Returns the set of currently-registered server function paths, for debugging purposes.
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
pub fn server_fns_by_path() -> Vec<&'static str> {
|
||||
REGISTERED_SERVER_FUNCTIONS
|
||||
.read()
|
||||
.map(|vals| vals.keys().copied().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Holds the current options for encoding types.
|
||||
/// More could be added, but they need to be serde
|
||||
#[derive(Debug, PartialEq)]
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 }
|
||||
tracing = "0.1"
|
||||
typed-builder = "0.11"
|
||||
|
||||
[dependencies.web-sys]
|
||||
@@ -18,10 +19,11 @@ features = ["HtmlLinkElement", "HtmlMetaElement", "HtmlTitleElement"]
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = ["leptos/ssr"]
|
||||
stable = ["leptos/stable"]
|
||||
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"]]
|
||||
|
||||
110
meta/src/lib.rs
110
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,87 @@ 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 {
|
||||
println!(
|
||||
"\n\nrendering {} elements to strings\n\n",
|
||||
self.els.borrow().len()
|
||||
);
|
||||
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 +186,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 +211,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 +228,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,69 +1,7 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{Scope, component, IntoView};
|
||||
use std::{rc::Rc, cell::{RefCell, Cell}, collections::HashMap};
|
||||
use leptos::{component, IntoView, Scope};
|
||||
|
||||
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
|
||||
///
|
||||
@@ -86,97 +24,29 @@ impl MetaTagsContext {
|
||||
/// ```
|
||||
#[component(transparent)]
|
||||
pub fn Meta(
|
||||
cx: Scope,
|
||||
cx: Scope,
|
||||
/// The [`charset`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-charset) attribute.
|
||||
#[prop(optional, into)]
|
||||
charset: Option<TextProp>,
|
||||
/// The [`name`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-name) attribute.
|
||||
#[prop(optional, into)]
|
||||
name: Option<TextProp>,
|
||||
/// The [`http-equiv`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-http-equiv) attribute.
|
||||
#[prop(optional, into)]
|
||||
http_equiv: Option<TextProp>,
|
||||
/// The [`content`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-content) attribute.
|
||||
#[prop(optional, into)]
|
||||
content: Option<TextProp>
|
||||
/// The [`name`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-name) attribute.
|
||||
#[prop(optional, into)]
|
||||
name: Option<TextProp>,
|
||||
/// The [`http-equiv`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-http-equiv) attribute.
|
||||
#[prop(optional, into)]
|
||||
http_equiv: Option<TextProp>,
|
||||
/// The [`content`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-content) attribute.
|
||||
#[prop(optional, into)]
|
||||
content: Option<TextProp>,
|
||||
) -> impl IntoView {
|
||||
let meta = use_head(cx);
|
||||
let next_id = meta.tags.get_next_id();
|
||||
let id = format!("leptos-link-{}", next_id.0);
|
||||
|
||||
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 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()));
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
use leptos::{document, JsCast, UnwrapThrowExt, create_effect};
|
||||
|
||||
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
|
||||
document()
|
||||
.query_selector("head")
|
||||
.unwrap_throw()
|
||||
.unwrap_throw()
|
||||
.append_child(&el)
|
||||
.unwrap_throw();
|
||||
|
||||
// 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,31 +1,5 @@
|
||||
use crate::use_head;
|
||||
use cfg_if::cfg_if;
|
||||
use crate::{Link, LinkProps};
|
||||
use leptos::*;
|
||||
use std::{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)]
|
||||
els: Rc<RefCell<HashMap<(Option<String>, String), Option<web_sys::HtmlLinkElement>>>>,
|
||||
}
|
||||
|
||||
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(|((id, href), _)| {
|
||||
if let Some(id) = id {
|
||||
format!(r#"<link rel="stylesheet" id="{id}" href="{href}">"#)
|
||||
} else {
|
||||
format!(r#"<link rel="stylesheet" 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.
|
||||
@@ -55,50 +29,13 @@ pub fn Stylesheet(
|
||||
#[prop(optional, into)]
|
||||
id: Option<String>,
|
||||
) -> impl IntoView {
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
use leptos::document;
|
||||
|
||||
let meta = use_head(cx);
|
||||
|
||||
// TODO I guess this will create a duplicated <link> when hydrating
|
||||
let existing_el = {
|
||||
let els = meta.stylesheets.els.borrow();
|
||||
let key = (id.clone(), href.clone());
|
||||
els.get(&key).cloned()
|
||||
};
|
||||
if let Some(Some(_)) = existing_el {
|
||||
leptos::leptos_dom::debug_warn!("<Stylesheet/> already loaded stylesheet {href}");
|
||||
} else {
|
||||
let element_to_hydrate = id.as_ref()
|
||||
.and_then(|id| {
|
||||
document().get_element_by_id(&id)
|
||||
});
|
||||
|
||||
let el = element_to_hydrate.unwrap_or_else(|| {
|
||||
let el = document().create_element("link").unwrap_throw();
|
||||
el.set_attribute("rel", "stylesheet").unwrap_throw();
|
||||
if let Some(id_val) = &id{
|
||||
el.set_attribute("id", id_val).unwrap_throw();
|
||||
}
|
||||
el.set_attribute("href", &href).unwrap_throw();
|
||||
document()
|
||||
.query_selector("head")
|
||||
.unwrap_throw()
|
||||
.unwrap_throw()
|
||||
.append_child(el.unchecked_ref())
|
||||
.unwrap_throw();
|
||||
el
|
||||
});
|
||||
|
||||
meta.stylesheets
|
||||
.els
|
||||
.borrow_mut()
|
||||
.insert((id, href), Some(el.unchecked_into()));
|
||||
}
|
||||
} else {
|
||||
let meta = use_head(cx);
|
||||
meta.stylesheets.els.borrow_mut().insert((id,href), None);
|
||||
if let Some(id) = id {
|
||||
view! { cx,
|
||||
<Link id rel="stylesheet" href/>
|
||||
}
|
||||
} else {
|
||||
view! { cx,
|
||||
<Link rel="stylesheet" href/>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,24 +106,40 @@ pub fn Title(
|
||||
}
|
||||
|
||||
let el = {
|
||||
let el_ref = meta.title.el.borrow_mut();
|
||||
let mut el_ref = meta.title.el.borrow_mut();
|
||||
let el = if let Some(el) = &*el_ref {
|
||||
let prev_text = el.inner_text();
|
||||
on_cleanup(cx, {
|
||||
let el = el.clone();
|
||||
move || {
|
||||
_ = el.set_text(&prev_text);
|
||||
}
|
||||
});
|
||||
|
||||
el.clone()
|
||||
} else {
|
||||
match document().query_selector("title") {
|
||||
Ok(Some(title)) => title.unchecked_into(),
|
||||
_ => {
|
||||
let el = document().create_element("title").unwrap_throw();
|
||||
document()
|
||||
.query_selector("head")
|
||||
.unwrap_throw()
|
||||
.unwrap_throw()
|
||||
.append_child(el.unchecked_ref())
|
||||
let head = document().head().unwrap_throw();
|
||||
head.append_child(el.unchecked_ref())
|
||||
.unwrap_throw();
|
||||
|
||||
on_cleanup(cx, {
|
||||
let el = el.clone();
|
||||
move || {
|
||||
_ = head.remove_child(&el);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
el.unchecked_into()
|
||||
}
|
||||
}
|
||||
};
|
||||
*el_ref = Some(el.clone().unchecked_into());
|
||||
|
||||
el
|
||||
};
|
||||
|
||||
|
||||
@@ -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"]]
|
||||
|
||||
@@ -88,7 +88,7 @@ where
|
||||
error.set(None);
|
||||
}
|
||||
if let Some(on_response) = on_response.clone() {
|
||||
on_response(&resp.as_raw());
|
||||
on_response(resp.as_raw());
|
||||
}
|
||||
|
||||
if resp.status() == 303 {
|
||||
@@ -264,7 +264,7 @@ fn extract_form_attributes(
|
||||
.unwrap_or_else(|| "get".to_string())
|
||||
.to_lowercase(),
|
||||
form.get_attribute("action")
|
||||
.unwrap_or_else(|| "".to_string())
|
||||
.unwrap_or_default()
|
||||
.to_lowercase(),
|
||||
form.get_attribute("enctype")
|
||||
.unwrap_or_else(|| "application/x-www-form-urlencoded".to_string())
|
||||
@@ -284,7 +284,7 @@ fn extract_form_attributes(
|
||||
}),
|
||||
input.get_attribute("action").unwrap_or_else(|| {
|
||||
form.get_attribute("action")
|
||||
.unwrap_or_else(|| "".to_string())
|
||||
.unwrap_or_default()
|
||||
.to_lowercase()
|
||||
}),
|
||||
input.get_attribute("enctype").unwrap_or_else(|| {
|
||||
@@ -307,7 +307,7 @@ fn extract_form_attributes(
|
||||
}),
|
||||
button.get_attribute("action").unwrap_or_else(|| {
|
||||
form.get_attribute("action")
|
||||
.unwrap_or_else(|| "".to_string())
|
||||
.unwrap_or_default()
|
||||
.to_lowercase()
|
||||
}),
|
||||
button.get_attribute("enctype").unwrap_or_else(|| {
|
||||
@@ -344,7 +344,7 @@ fn extract_form_attributes(
|
||||
fn action_input_from_form_data<I: serde::de::DeserializeOwned>(
|
||||
form_data: &web_sys::FormData,
|
||||
) -> Result<I, serde_urlencoded::de::Error> {
|
||||
let data = web_sys::UrlSearchParams::new_with_str_sequence_sequence(&form_data).unwrap_throw();
|
||||
let data = web_sys::UrlSearchParams::new_with_str_sequence_sequence(form_data).unwrap_throw();
|
||||
let data = data.to_string().as_string().unwrap_or_default();
|
||||
serde_urlencoded::from_str::<I>(&data)
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ where
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
view! { cx,
|
||||
<a
|
||||
<html::a
|
||||
href=move || href.get().unwrap_or_default()
|
||||
prop:state={state.map(|s| s.to_js_value())}
|
||||
prop:replace={replace}
|
||||
@@ -94,17 +94,17 @@ where
|
||||
class=move || class.as_ref().map(|class| class.get())
|
||||
>
|
||||
{children(cx)}
|
||||
</a>
|
||||
</html::a>
|
||||
}
|
||||
} else {
|
||||
view! { cx,
|
||||
<a
|
||||
<html::a
|
||||
href=move || href.get().unwrap_or_default()
|
||||
aria-current=move || if is_active.get() { Some("page") } else { None }
|
||||
class=move || class.as_ref().map(|class| class.get())
|
||||
>
|
||||
{children(cx)}
|
||||
</a>
|
||||
</html::a>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
use crate::use_route;
|
||||
use leptos::*;
|
||||
@@ -8,21 +8,21 @@ use leptos::*;
|
||||
#[component]
|
||||
pub fn Outlet(cx: Scope) -> impl IntoView {
|
||||
let route = use_route(cx);
|
||||
let is_showing = Rc::new(RefCell::new(None));
|
||||
let is_showing = Rc::new(Cell::new(None::<(usize, Scope)>));
|
||||
let (outlet, set_outlet) = create_signal(cx, None);
|
||||
create_effect(cx, move |_| {
|
||||
let is_showing_val = { is_showing.borrow().clone() };
|
||||
match (route.child(), &is_showing_val) {
|
||||
create_isomorphic_effect(cx, move |_| {
|
||||
match (route.child(), &is_showing.get()) {
|
||||
(None, _) => {
|
||||
set_outlet.set(None);
|
||||
}
|
||||
(Some(child), Some(_))
|
||||
if Some(child.original_path().to_string()) == is_showing_val =>
|
||||
{
|
||||
(Some(child), Some((is_showing_val, _))) if child.id() == *is_showing_val => {
|
||||
// do nothing: we don't need to rerender the component, because it's the same
|
||||
}
|
||||
(Some(child), _) => {
|
||||
*is_showing.borrow_mut() = Some(child.original_path().to_string());
|
||||
(Some(child), prev) => {
|
||||
if let Some(prev_scope) = prev.map(|(_, scope)| scope) {
|
||||
prev_scope.dispose();
|
||||
}
|
||||
is_showing.set(Some((child.id(), child.cx())));
|
||||
provide_context(child.cx(), child.clone());
|
||||
set_outlet.set(Some(child.outlet().into_view(cx)))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{borrow::Cow, rc::Rc};
|
||||
use std::{borrow::Cow, cell::Cell, rc::Rc};
|
||||
|
||||
use leptos::*;
|
||||
|
||||
@@ -7,15 +7,19 @@ use crate::{
|
||||
ParamsMap, RouterContext,
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
static ROUTE_ID: Cell<usize> = Cell::new(0);
|
||||
}
|
||||
|
||||
/// Describes a portion of the nested layout of the app, specifying the route it should match,
|
||||
/// the element it should display, and data that should be loaded alongside the route.
|
||||
#[component(transparent)]
|
||||
pub fn Route<E, F>(
|
||||
pub fn Route<E, F, P>(
|
||||
cx: Scope,
|
||||
/// The path fragment that this route should match. This can be static (`users`),
|
||||
/// include a parameter (`:id`) or an optional parameter (`:id?`), or match a
|
||||
/// wildcard (`user/*any`).
|
||||
path: &'static str,
|
||||
path: P,
|
||||
/// The view that should be shown when this route is matched. This can be any function
|
||||
/// that takes a [Scope] and returns an [Element] (like `|cx| view! { cx, <p>"Show this"</p> })`
|
||||
/// or `|cx| view! { cx, <MyComponent/>` } or even, for a component with no props, `MyComponent`).
|
||||
@@ -27,6 +31,7 @@ pub fn Route<E, F>(
|
||||
where
|
||||
E: IntoView,
|
||||
F: Fn(Scope) -> E + 'static,
|
||||
P: std::fmt::Display,
|
||||
{
|
||||
let children = children
|
||||
.map(|children| {
|
||||
@@ -42,8 +47,14 @@ where
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let id = ROUTE_ID.with(|id| {
|
||||
let next = id.get() + 1;
|
||||
id.set(next);
|
||||
next
|
||||
});
|
||||
RouteDefinition {
|
||||
path,
|
||||
id,
|
||||
path: path.to_string(),
|
||||
children,
|
||||
view: Rc::new(move |cx| view(cx).into_view(cx)),
|
||||
}
|
||||
@@ -72,7 +83,9 @@ impl RouteContext {
|
||||
let base = base.path();
|
||||
let RouteMatch { path_match, route } = matcher()?;
|
||||
let PathMatch { path, .. } = path_match;
|
||||
let RouteDefinition { view: element, .. } = route.key;
|
||||
let RouteDefinition {
|
||||
view: element, id, ..
|
||||
} = route.key;
|
||||
let params = create_memo(cx, move |_| {
|
||||
matcher()
|
||||
.map(|matched| matched.path_match.params)
|
||||
@@ -82,6 +95,7 @@ impl RouteContext {
|
||||
Some(Self {
|
||||
inner: Rc::new(RouteContextInner {
|
||||
cx,
|
||||
id,
|
||||
base_path: base.to_string(),
|
||||
child: Box::new(child),
|
||||
path,
|
||||
@@ -97,6 +111,10 @@ impl RouteContext {
|
||||
self.inner.cx
|
||||
}
|
||||
|
||||
pub(crate) fn id(&self) -> usize {
|
||||
self.inner.id
|
||||
}
|
||||
|
||||
/// Returns the URL path of the current route,
|
||||
/// including param values in their places.
|
||||
///
|
||||
@@ -124,6 +142,7 @@ impl RouteContext {
|
||||
Self {
|
||||
inner: Rc::new(RouteContextInner {
|
||||
cx,
|
||||
id: 0,
|
||||
base_path: path.to_string(),
|
||||
child: Box::new(|| None),
|
||||
path: path.to_string(),
|
||||
@@ -153,6 +172,7 @@ impl RouteContext {
|
||||
pub(crate) struct RouteContextInner {
|
||||
cx: Scope,
|
||||
base_path: String,
|
||||
pub(crate) id: usize,
|
||||
pub(crate) child: Box<dyn Fn() -> Option<RouteContext>>,
|
||||
pub(crate) path: String,
|
||||
pub(crate) original_path: String,
|
||||
|
||||
@@ -86,7 +86,8 @@ pub fn Routes(
|
||||
|
||||
match (prev_routes, prev_match) {
|
||||
(Some(prev), Some(prev_match))
|
||||
if next_match.route.key == prev_match.route.key =>
|
||||
if next_match.route.key == prev_match.route.key
|
||||
&& next_match.route.id == prev_match.route.id =>
|
||||
{
|
||||
let prev_one = { prev.borrow()[i].clone() };
|
||||
if i >= next.borrow().len() {
|
||||
@@ -139,7 +140,8 @@ pub fn Routes(
|
||||
|
||||
if disposers.borrow().len() > i + 1 {
|
||||
let mut disposers = disposers.borrow_mut();
|
||||
let old_route_disposer = std::mem::replace(&mut disposers[i], disposer);
|
||||
let old_route_disposer =
|
||||
std::mem::replace(&mut disposers[i + 1], disposer);
|
||||
old_route_disposer.dispose();
|
||||
} else {
|
||||
disposers.borrow_mut().push(disposer);
|
||||
@@ -212,6 +214,7 @@ struct RouterState {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct RouteData {
|
||||
pub id: usize,
|
||||
pub key: RouteDefinition,
|
||||
pub pattern: String,
|
||||
pub original_path: String,
|
||||
@@ -228,6 +231,7 @@ impl RouteData {
|
||||
.split('/')
|
||||
.filter(|n| !n.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
#[allow(clippy::bool_to_int_with_if)] // on the splat.is_none()
|
||||
segments.iter().fold(
|
||||
(segments.len() as i32) - if splat.is_none() { 0 } else { 1 },
|
||||
|score, segment| score + if segment.starts_with(':') { 2 } else { 3 },
|
||||
@@ -273,7 +277,7 @@ fn create_routes(route_def: &RouteDefinition, base: &str) -> Vec<RouteData> {
|
||||
let RouteDefinition { children, .. } = route_def;
|
||||
let is_leaf = children.is_empty();
|
||||
let mut acc = Vec::new();
|
||||
for original_path in expand_optionals(route_def.path) {
|
||||
for original_path in expand_optionals(&route_def.path) {
|
||||
let path = join_paths(base, &original_path);
|
||||
let pattern = if is_leaf {
|
||||
path
|
||||
@@ -285,6 +289,7 @@ fn create_routes(route_def: &RouteDefinition, base: &str) -> Vec<RouteData> {
|
||||
};
|
||||
acc.push(RouteData {
|
||||
key: route_def.clone(),
|
||||
id: route_def.id,
|
||||
matcher: Matcher::new_with_partial(&pattern, !is_leaf),
|
||||
pattern,
|
||||
original_path: original_path.to_string(),
|
||||
|
||||
@@ -64,7 +64,7 @@ pub fn use_resolved_path(cx: Scope, path: impl Fn() -> String + 'static) -> Memo
|
||||
|
||||
create_memo(cx, move |_| {
|
||||
let path = path();
|
||||
if path.starts_with("/") {
|
||||
if path.starts_with('/') {
|
||||
Some(path)
|
||||
} else {
|
||||
route.resolve_path(&path).map(String::from)
|
||||
|
||||
@@ -135,6 +135,49 @@
|
||||
//! }
|
||||
//!
|
||||
//! ```
|
||||
//!
|
||||
//! ## Module Route Definitions
|
||||
//! Routes can also be modularized and nested by defining them in separate components, which can be
|
||||
//! located in and imported from other modules. Components that return `<Route/>` should be marked
|
||||
//! `#[component(transparent)]`, as in this example:
|
||||
//! ```rust
|
||||
//! use leptos::*;
|
||||
//! use leptos_router::*;
|
||||
//!
|
||||
//! #[component]
|
||||
//! pub fn App(cx: Scope) -> impl IntoView {
|
||||
//! view! { cx,
|
||||
//! <Router>
|
||||
//! <Routes>
|
||||
//! <Route path="/" view=move |cx| {
|
||||
//! view! { cx, "-> /" }
|
||||
//! }/>
|
||||
//! <ExternallyDefinedRoute/>
|
||||
//! </Routes>
|
||||
//! </Router>
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! // `transparent` here marks the component as returning data (a RouteDefinition), not a view
|
||||
//! #[component(transparent)]
|
||||
//! pub fn ExternallyDefinedRoute(cx: Scope) -> impl IntoView {
|
||||
//! view! { cx,
|
||||
//! <Route path="/some-area" view=move |cx| {
|
||||
//! view! { cx, <div>
|
||||
//! <h2>"Some Area"</h2>
|
||||
//! <Outlet/>
|
||||
//! </div> }
|
||||
//! }>
|
||||
//! <Route path="/path-a/:id" view=move |cx| {
|
||||
//! view! { cx, <p>"Path A"</p> }
|
||||
//! }/>
|
||||
//! <Route path="/path-b/:id" view=move |cx| {
|
||||
//! view! { cx, <p>"Path B"</p> }
|
||||
//! }/>
|
||||
//! </Route>
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
#![cfg_attr(not(feature = "stable"), feature(auto_traits))]
|
||||
#![cfg_attr(not(feature = "stable"), feature(negative_impls))]
|
||||
@@ -143,7 +186,8 @@
|
||||
mod components;
|
||||
mod history;
|
||||
mod hooks;
|
||||
mod matching;
|
||||
#[doc(hidden)]
|
||||
pub mod matching;
|
||||
|
||||
pub use components::*;
|
||||
pub use history::*;
|
||||
|
||||
@@ -3,9 +3,9 @@ mod matcher;
|
||||
mod resolve_path;
|
||||
mod route;
|
||||
|
||||
pub(crate) use expand_optionals::*;
|
||||
pub(crate) use matcher::*;
|
||||
pub(crate) use resolve_path::*;
|
||||
pub use expand_optionals::*;
|
||||
pub use matcher::*;
|
||||
pub use resolve_path::*;
|
||||
pub use route::*;
|
||||
|
||||
use crate::RouteData;
|
||||
|
||||
@@ -5,7 +5,8 @@ use leptos::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RouteDefinition {
|
||||
pub path: &'static str,
|
||||
pub id: usize,
|
||||
pub path: String,
|
||||
pub children: Vec<RouteDefinition>,
|
||||
pub view: Rc<dyn Fn(Scope) -> View>,
|
||||
}
|
||||
|
||||
35
router/tests/expand_optionals.rs
Normal file
35
router/tests/expand_optionals.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos_router::expand_optionals;
|
||||
|
||||
#[test]
|
||||
fn expand_optionals_should_expand() {
|
||||
assert_eq!(expand_optionals("/foo/:x"), vec!["/foo/:x"]);
|
||||
assert_eq!(expand_optionals("/foo/:x?"), vec!["/foo", "/foo/:x"]);
|
||||
assert_eq!(expand_optionals("/bar/:x?/"), vec!["/bar/", "/bar/:x/"]);
|
||||
assert_eq!(
|
||||
expand_optionals("/foo/:x?/:y?/:z"),
|
||||
vec!["/foo/:z", "/foo/:x/:z", "/foo/:x/:y/:z"]
|
||||
);
|
||||
assert_eq!(
|
||||
expand_optionals("/foo/:x?/:y/:z?"),
|
||||
vec!["/foo/:y", "/foo/:x/:y", "/foo/:y/:z", "/foo/:x/:y/:z"]
|
||||
);
|
||||
assert_eq!(
|
||||
expand_optionals("/foo/:x?/bar/:y?/baz/:z?"),
|
||||
vec![
|
||||
"/foo/bar/baz",
|
||||
"/foo/:x/bar/baz",
|
||||
"/foo/bar/:y/baz",
|
||||
"/foo/:x/bar/:y/baz",
|
||||
"/foo/bar/baz/:z",
|
||||
"/foo/:x/bar/baz/:z",
|
||||
"/foo/bar/:y/baz/:z",
|
||||
"/foo/:x/bar/:y/baz/:z"
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
48
router/tests/join_paths.rs
Normal file
48
router/tests/join_paths.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
// Test cases drawn from Solid Router
|
||||
// see https://github.com/solidjs/solid-router/blob/main/test/utils.spec.ts
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos_router::join_paths;
|
||||
|
||||
#[test]
|
||||
fn join_paths_should_join_with_a_single_slash() {
|
||||
assert_eq!(join_paths("/foo", "bar"), "/foo/bar");
|
||||
assert_eq!(join_paths("/foo/", "bar"), "/foo/bar");
|
||||
assert_eq!(join_paths("/foo", "/bar"), "/foo/bar");
|
||||
assert_eq!(join_paths("/foo/", "/bar"), "/foo/bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_paths_should_ensure_leading_slash() {
|
||||
assert_eq!(join_paths("/foo", ""), "/foo");
|
||||
assert_eq!(join_paths("foo", ""), "/foo");
|
||||
assert_eq!(join_paths("", "foo"), "/foo");
|
||||
assert_eq!(join_paths("", "/foo"), "/foo");
|
||||
assert_eq!(join_paths("/", "foo"), "/foo");
|
||||
assert_eq!(join_paths("/", "/foo"), "/foo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_paths_should_strip_tailing_slash_asterisk() {
|
||||
assert_eq!(join_paths("foo/*", ""), "/foo");
|
||||
assert_eq!(join_paths("foo/*", "/"), "/foo");
|
||||
assert_eq!(join_paths("/foo/*all", ""), "/foo");
|
||||
assert_eq!(join_paths("/foo/*", "bar"), "/foo/bar");
|
||||
assert_eq!(join_paths("/foo/*all", "bar"), "/foo/bar");
|
||||
assert_eq!(join_paths("/*", "foo"), "/foo");
|
||||
assert_eq!(join_paths("/*all", "foo"), "/foo");
|
||||
assert_eq!(join_paths("*", "foo"), "/foo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_paths_should_preserve_parameters() {
|
||||
assert_eq!(join_paths("/foo/:bar", ""), "/foo/:bar");
|
||||
assert_eq!(join_paths("/foo/:bar", "baz"), "/foo/:bar/baz");
|
||||
assert_eq!(join_paths("/foo", ":bar/baz"), "/foo/:bar/baz");
|
||||
assert_eq!(join_paths("", ":bar/baz"), "/:bar/baz");
|
||||
}
|
||||
}
|
||||
}
|
||||
96
router/tests/matcher.rs
Normal file
96
router/tests/matcher.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
// Test cases drawn from Solid Router
|
||||
// see https://github.com/solidjs/solid-router/blob/main/test/utils.spec.ts
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos_router::{params_map, Matcher, PathMatch};
|
||||
|
||||
#[test]
|
||||
fn create_matcher_should_return_no_params_when_location_matches_exactly() {
|
||||
let matcher = Matcher::new("/foo/bar");
|
||||
let matched = matcher.test("/foo/bar");
|
||||
assert_eq!(
|
||||
matched,
|
||||
Some(PathMatch {
|
||||
path: "/foo/bar".into(),
|
||||
params: params_map!()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_matcher_should_return_none_when_location_doesnt_match() {
|
||||
let matcher = Matcher::new("/foo/bar");
|
||||
let matched = matcher.test("/foo/baz");
|
||||
assert_eq!(matched, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_matcher_should_build_params_collection() {
|
||||
let matcher = Matcher::new("/foo/:id");
|
||||
let matched = matcher.test("/foo/abc-123");
|
||||
assert_eq!(
|
||||
matched,
|
||||
Some(PathMatch {
|
||||
path: "/foo/abc-123".into(),
|
||||
params: params_map!(
|
||||
"id".into() => "abc-123".into()
|
||||
)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_matcher_should_match_past_end_when_ending_in_asterisk() {
|
||||
let matcher = Matcher::new("/foo/bar/*");
|
||||
let matched = matcher.test("/foo/bar/baz");
|
||||
assert_eq!(
|
||||
matched,
|
||||
Some(PathMatch {
|
||||
path: "/foo/bar".into(),
|
||||
params: params_map!()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_matcher_should_not_match_past_end_when_not_ending_in_asterisk() {
|
||||
let matcher = Matcher::new("/foo/bar");
|
||||
let matched = matcher.test("/foo/bar/baz");
|
||||
assert_eq!(matched, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_matcher_should_include_remaining_unmatched_location_as_param_when_ending_in_asterisk_and_name(
|
||||
) {
|
||||
let matcher = Matcher::new("/foo/bar/*something");
|
||||
let matched = matcher.test("/foo/bar/baz/qux");
|
||||
assert_eq!(
|
||||
matched,
|
||||
Some(PathMatch {
|
||||
path: "/foo/bar".into(),
|
||||
params: params_map!(
|
||||
"something".into() => "baz/qux".into()
|
||||
)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_matcher_should_include_empty_param_when_perfect_match_ends_in_asterisk_and_name() {
|
||||
let matcher = Matcher::new("/foo/bar/*something");
|
||||
let matched = matcher.test("/foo/bar");
|
||||
assert_eq!(
|
||||
matched,
|
||||
Some(PathMatch {
|
||||
path: "/foo/bar".into(),
|
||||
params: params_map!(
|
||||
"something".into() => "".into()
|
||||
)
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
107
router/tests/resolve_path.rs
Normal file
107
router/tests/resolve_path.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
// Test cases drawn from Solid Router
|
||||
// see https://github.com/solidjs/solid-router/blob/main/test/utils.spec.ts
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos_router::{normalize, resolve_path};
|
||||
|
||||
#[test]
|
||||
fn normalize_query_string_with_opening_slash() {
|
||||
assert_eq!(normalize("/?foo=bar", false), "?foo=bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_normalize_base_arg() {
|
||||
assert_eq!(resolve_path("base", "", None), Some("/base".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_normalize_path_arg() {
|
||||
assert_eq!(resolve_path("", "path", None), Some("/path".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_normalize_from_arg() {
|
||||
assert_eq!(resolve_path("", "", Some("from")), Some("/from".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_return_default_when_all_empty() {
|
||||
assert_eq!(resolve_path("", "", None), Some("/".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_resolve_root_against_base_and_ignore_from() {
|
||||
assert_eq!(
|
||||
resolve_path("/base", "/", Some("/base/foo")),
|
||||
Some("/base".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_resolve_rooted_paths_against_base_and_ignore_from() {
|
||||
assert_eq!(
|
||||
resolve_path("/base", "/bar", Some("/base/foo")),
|
||||
Some("/base/bar".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_resolve_empty_path_against_from() {
|
||||
assert_eq!(
|
||||
resolve_path("/base", "", Some("/base/foo")),
|
||||
Some("/base/foo".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_resolve_relative_paths_against_from() {
|
||||
assert_eq!(
|
||||
resolve_path("/base", "bar", Some("/base/foo")),
|
||||
Some("/base/foo/bar".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_prepend_base_if_from_doesnt_start_with_it() {
|
||||
assert_eq!(
|
||||
resolve_path("/base", "bar", Some("/foo")),
|
||||
Some("/base/foo/bar".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_test_start_of_from_against_base_case_insensitive() {
|
||||
assert_eq!(
|
||||
resolve_path("/base", "bar", Some("BASE/foo")),
|
||||
Some("/BASE/foo/bar".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_work_with_rooted_search_and_base() {
|
||||
assert_eq!(
|
||||
resolve_path("/base", "/?foo=bar", Some("/base/page")),
|
||||
Some("/base?foo=bar".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_path_should_work_with_rooted_search() {
|
||||
assert_eq!(
|
||||
resolve_path("", "/?foo=bar", None),
|
||||
Some("/?foo=bar".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserve_spaces() {
|
||||
assert_eq!(
|
||||
resolve_path(" foo ", " bar baz ", None),
|
||||
Some("/ foo / bar baz ".into())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user