mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 09:54:41 -05:00
apply new formatting everywhere (#502)
This commit is contained in:
@@ -32,11 +32,19 @@ pub struct ResponseParts {
|
||||
|
||||
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) {
|
||||
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) {
|
||||
pub fn append_header(
|
||||
&mut self,
|
||||
key: header::HeaderName,
|
||||
value: header::HeaderValue,
|
||||
) {
|
||||
self.headers.append(key, value);
|
||||
}
|
||||
}
|
||||
@@ -60,13 +68,21 @@ impl ResponseOptions {
|
||||
res_parts.status = Some(status);
|
||||
}
|
||||
/// Insert a header, overwriting any previous value with the same key
|
||||
pub fn insert_header(&self, key: header::HeaderName, value: header::HeaderValue) {
|
||||
pub fn insert_header(
|
||||
&self,
|
||||
key: header::HeaderName,
|
||||
value: header::HeaderValue,
|
||||
) {
|
||||
let mut writeable = self.0.write();
|
||||
let res_parts = &mut *writeable;
|
||||
res_parts.headers.insert(key, value);
|
||||
}
|
||||
/// Append a header, leaving any header with the same key intact
|
||||
pub fn append_header(&self, key: header::HeaderName, value: header::HeaderValue) {
|
||||
pub fn append_header(
|
||||
&self,
|
||||
key: header::HeaderName,
|
||||
value: header::HeaderValue,
|
||||
) {
|
||||
let mut writeable = self.0.write();
|
||||
let res_parts = &mut *writeable;
|
||||
res_parts.headers.append(key, value);
|
||||
@@ -81,7 +97,8 @@ pub fn redirect(cx: leptos::Scope, path: &str) {
|
||||
response_options.set_status(StatusCode::FOUND);
|
||||
response_options.insert_header(
|
||||
header::LOCATION,
|
||||
header::HeaderValue::from_str(path).expect("Failed to create HeaderValue"),
|
||||
header::HeaderValue::from_str(path)
|
||||
.expect("Failed to create HeaderValue"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -173,7 +190,8 @@ pub fn handle_server_fns_with_context(
|
||||
|
||||
match server_fn(cx, body).await {
|
||||
Ok(serialized) => {
|
||||
let res_options = use_context::<ResponseOptions>(cx).unwrap();
|
||||
let res_options =
|
||||
use_context::<ResponseOptions>(cx).unwrap();
|
||||
|
||||
// clean up the scope, which we only needed to run the server fn
|
||||
disposer.dispose();
|
||||
@@ -183,7 +201,8 @@ pub fn handle_server_fns_with_context(
|
||||
let mut res_parts = res_options.0.write();
|
||||
|
||||
if accept_header == Some("application/json")
|
||||
|| accept_header == Some("application/x-www-form-urlencoded")
|
||||
|| accept_header
|
||||
== Some("application/x-www-form-urlencoded")
|
||||
|| accept_header == Some("application/cbor")
|
||||
{
|
||||
res = HttpResponse::Ok();
|
||||
@@ -221,7 +240,9 @@ pub fn handle_server_fns_with_context(
|
||||
res.body(Bytes::from(data))
|
||||
}
|
||||
Payload::Url(data) => {
|
||||
res.content_type("application/x-www-form-urlencoded");
|
||||
res.content_type(
|
||||
"application/x-www-form-urlencoded",
|
||||
);
|
||||
res.body(data)
|
||||
}
|
||||
Payload::Json(data) => {
|
||||
@@ -230,13 +251,15 @@ pub fn handle_server_fns_with_context(
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
Err(e) => HttpResponse::InternalServerError()
|
||||
.body(e.to_string()),
|
||||
}
|
||||
} else {
|
||||
HttpResponse::BadRequest().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.",
|
||||
\n\nIt's likely that you need to call \
|
||||
ServerFn::register() on the server function type, \
|
||||
somewhere in your `main` function.",
|
||||
req.path()
|
||||
))
|
||||
}
|
||||
@@ -256,13 +279,13 @@ pub fn handle_server_fns_with_context(
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// use actix_web::{HttpServer, App};
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::*;
|
||||
/// use std::{env,net::SocketAddr};
|
||||
/// use std::{env, net::SocketAddr};
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> impl IntoView {
|
||||
/// view! { cx, <main>"Hello, world!"</main> }
|
||||
/// view! { cx, <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
@@ -272,11 +295,17 @@ pub fn handle_server_fns_with_context(
|
||||
/// let addr = conf.leptos_options.site_addr.clone();
|
||||
/// HttpServer::new(move || {
|
||||
/// let leptos_options = &conf.leptos_options;
|
||||
///
|
||||
///
|
||||
/// App::new()
|
||||
/// // {tail:.*} passes the remainder of the URL as the route
|
||||
/// // the actual routing will be handled by `leptos_router`
|
||||
/// .route("/{tail:.*}", leptos_actix::render_app_to_stream(leptos_options.to_owned(), |cx| view! { cx, <MyApp/> }))
|
||||
/// .route(
|
||||
/// "/{tail:.*}",
|
||||
/// leptos_actix::render_app_to_stream(
|
||||
/// leptos_options.to_owned(),
|
||||
/// |cx| view! { cx, <MyApp/> },
|
||||
/// ),
|
||||
/// )
|
||||
/// })
|
||||
/// .bind(&addr)?
|
||||
/// .run()
|
||||
@@ -353,14 +382,14 @@ where
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// use actix_web::{HttpServer, App};
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::*;
|
||||
/// use std::{env,net::SocketAddr};
|
||||
/// use leptos_actix::DataResponse;
|
||||
/// use std::{env, net::SocketAddr};
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope, data: &'static str) -> impl IntoView {
|
||||
/// view! { cx, <main>"Hello, world!"</main> }
|
||||
/// view! { cx, <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
@@ -370,14 +399,21 @@ where
|
||||
/// let addr = conf.leptos_options.site_addr.clone();
|
||||
/// HttpServer::new(move || {
|
||||
/// let leptos_options = &conf.leptos_options;
|
||||
///
|
||||
///
|
||||
/// App::new()
|
||||
/// // {tail:.*} passes the remainder of the URL as the route
|
||||
/// // the actual routing will be handled by `leptos_router`
|
||||
/// .route("/{tail:.*}", leptos_actix::render_preloaded_data_app(
|
||||
/// leptos_options.to_owned(),
|
||||
/// |req| async move { Ok(DataResponse::Data("async func that can preload data")) },
|
||||
/// |cx, data| view! { cx, <MyApp data/> })
|
||||
/// .route(
|
||||
/// "/{tail:.*}",
|
||||
/// leptos_actix::render_preloaded_data_app(
|
||||
/// leptos_options.to_owned(),
|
||||
/// |req| async move {
|
||||
/// Ok(DataResponse::Data(
|
||||
/// "async func that can preload data",
|
||||
/// ))
|
||||
/// },
|
||||
/// |cx, data| view! { cx, <MyApp data/> },
|
||||
/// ),
|
||||
/// )
|
||||
/// })
|
||||
/// .bind(&addr)?
|
||||
@@ -430,7 +466,11 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
fn provide_contexts(cx: leptos::Scope, req: &HttpRequest, res_options: ResponseOptions) {
|
||||
fn provide_contexts(
|
||||
cx: leptos::Scope,
|
||||
req: &HttpRequest,
|
||||
res_options: ResponseOptions,
|
||||
) {
|
||||
let path = leptos_corrected_path(req);
|
||||
|
||||
let integration = ServerIntegration { path };
|
||||
@@ -457,25 +497,27 @@ async fn stream_app(
|
||||
res_options: ResponseOptions,
|
||||
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
|
||||
) -> HttpResponse<BoxBody> {
|
||||
let (stream, runtime, scope) = render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
move |cx| {
|
||||
let meta = use_context::<MetaContext>(cx);
|
||||
let head = meta
|
||||
.as_ref()
|
||||
.map(|meta| meta.dehydrate())
|
||||
.unwrap_or_default();
|
||||
let body_meta = meta
|
||||
.as_ref()
|
||||
.and_then(|meta| meta.body.as_string())
|
||||
.unwrap_or_default();
|
||||
format!("{head}</head><body{body_meta}>").into()
|
||||
},
|
||||
additional_context,
|
||||
);
|
||||
let (stream, runtime, scope) =
|
||||
render_to_stream_with_prefix_undisposed_with_context(
|
||||
app,
|
||||
move |cx| {
|
||||
let meta = use_context::<MetaContext>(cx);
|
||||
let head = meta
|
||||
.as_ref()
|
||||
.map(|meta| meta.dehydrate())
|
||||
.unwrap_or_default();
|
||||
let body_meta = meta
|
||||
.as_ref()
|
||||
.and_then(|meta| meta.body.as_string())
|
||||
.unwrap_or_default();
|
||||
format!("{head}</head><body{body_meta}>").into()
|
||||
},
|
||||
additional_context,
|
||||
);
|
||||
|
||||
let cx = leptos::Scope { runtime, id: scope };
|
||||
let (head, tail) = html_parts(options, use_context::<MetaContext>(cx).as_ref());
|
||||
let (head, tail) =
|
||||
html_parts(options, use_context::<MetaContext>(cx).as_ref());
|
||||
|
||||
let mut stream = Box::pin(
|
||||
futures::stream::once(async move { head.clone() })
|
||||
@@ -493,11 +535,13 @@ async fn stream_app(
|
||||
|
||||
let res_options = res_options.0.read();
|
||||
|
||||
let (status, mut headers) = (res_options.status, res_options.headers.clone());
|
||||
let (status, mut headers) =
|
||||
(res_options.status, res_options.headers.clone());
|
||||
let status = status.unwrap_or_default();
|
||||
|
||||
let complete_stream =
|
||||
futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()]).chain(stream);
|
||||
futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()])
|
||||
.chain(stream);
|
||||
let mut res = HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.streaming(complete_stream);
|
||||
@@ -514,7 +558,10 @@ async fn stream_app(
|
||||
res
|
||||
}
|
||||
|
||||
fn html_parts(options: &LeptosOptions, meta_context: Option<&MetaContext>) -> (String, String) {
|
||||
fn html_parts(
|
||||
options: &LeptosOptions,
|
||||
meta_context: Option<&MetaContext>,
|
||||
) -> (String, String) {
|
||||
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
|
||||
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
|
||||
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
|
||||
@@ -578,7 +625,9 @@ fn html_parts(options: &LeptosOptions, meta_context: Option<&MetaContext>) -> (S
|
||||
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
|
||||
/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
|
||||
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths.
|
||||
pub fn generate_route_list<IV>(app_fn: impl FnOnce(leptos::Scope) -> IV + 'static) -> Vec<String>
|
||||
pub fn generate_route_list<IV>(
|
||||
app_fn: impl FnOnce(leptos::Scope) -> IV + 'static,
|
||||
) -> Vec<String>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
@@ -658,7 +707,12 @@ pub trait LeptosRoutes {
|
||||
/// to those paths to Leptos's renderer.
|
||||
impl<T> LeptosRoutes for actix_web::App<T>
|
||||
where
|
||||
T: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
|
||||
T: ServiceFactory<
|
||||
ServiceRequest,
|
||||
Config = (),
|
||||
Error = Error,
|
||||
InitError = (),
|
||||
>,
|
||||
{
|
||||
fn leptos_routes<IV>(
|
||||
self,
|
||||
@@ -671,7 +725,10 @@ where
|
||||
{
|
||||
let mut router = self;
|
||||
for path in paths.iter() {
|
||||
router = router.route(path, render_app_to_stream(options.clone(), app_fn.clone()));
|
||||
router = router.route(
|
||||
path,
|
||||
render_app_to_stream(options.clone(), app_fn.clone()),
|
||||
);
|
||||
}
|
||||
router
|
||||
}
|
||||
@@ -693,7 +750,11 @@ where
|
||||
for path in paths.iter() {
|
||||
router = router.route(
|
||||
path,
|
||||
render_preloaded_data_app(options.clone(), data_fn.clone(), app_fn.clone()),
|
||||
render_preloaded_data_app(
|
||||
options.clone(),
|
||||
data_fn.clone(),
|
||||
app_fn.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
router
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
use axum::{
|
||||
body::{Body, Bytes, Full, StreamBody},
|
||||
extract::Path,
|
||||
http::{header::HeaderName, header::HeaderValue, HeaderMap, Request, StatusCode},
|
||||
http::{
|
||||
header::{HeaderName, HeaderValue},
|
||||
HeaderMap, Request, StatusCode,
|
||||
},
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
};
|
||||
@@ -21,7 +24,7 @@ use leptos_meta::MetaContext;
|
||||
use leptos_router::*;
|
||||
use parking_lot::RwLock;
|
||||
use std::{io, pin::Pin, sync::Arc};
|
||||
use tokio::{task::spawn_blocking, task::LocalSet};
|
||||
use tokio::task::{spawn_blocking, LocalSet};
|
||||
|
||||
/// A struct to hold the parts of the incoming Request. Since `http::Request` isn't cloneable, we're forced
|
||||
/// to construct this for Leptos to use in Axum
|
||||
@@ -92,7 +95,8 @@ pub fn redirect(cx: leptos::Scope, path: &str) {
|
||||
response_options.set_status(StatusCode::FOUND);
|
||||
response_options.insert_header(
|
||||
header::LOCATION,
|
||||
header::HeaderValue::from_str(path).expect("Failed to create HeaderValue"),
|
||||
header::HeaderValue::from_str(path)
|
||||
.expect("Failed to create HeaderValue"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -118,8 +122,8 @@ pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
|
||||
///
|
||||
/// ```
|
||||
/// use axum::{handler::Handler, routing::post, Router};
|
||||
/// use std::net::SocketAddr;
|
||||
/// use leptos::*;
|
||||
/// use std::net::SocketAddr;
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[tokio::main]
|
||||
@@ -128,7 +132,7 @@ pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
|
||||
///
|
||||
/// // build our application with a route
|
||||
/// let app = Router::new()
|
||||
/// .route("/api/*fn_name", post(leptos_axum::handle_server_fns));
|
||||
/// .route("/api/*fn_name", post(leptos_axum::handle_server_fns));
|
||||
///
|
||||
/// // run our app with hyper
|
||||
/// // `axum::Server` is a re-export of `hyper::Server`
|
||||
@@ -196,9 +200,12 @@ async fn handle_server_fns_inner(
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on({
|
||||
async move {
|
||||
let res = if let Some(server_fn) = server_fn_by_path(fn_name.as_str()) {
|
||||
let res = if let Some(server_fn) =
|
||||
server_fn_by_path(fn_name.as_str())
|
||||
{
|
||||
let runtime = create_runtime();
|
||||
let (cx, disposer) = raw_scope_and_disposer(runtime);
|
||||
let (cx, disposer) =
|
||||
raw_scope_and_disposer(runtime);
|
||||
|
||||
additional_context(cx);
|
||||
|
||||
@@ -211,34 +218,43 @@ async fn handle_server_fns_inner(
|
||||
match server_fn(cx, &req_parts.body).await {
|
||||
Ok(serialized) => {
|
||||
// If ResponseOptions are set, add the headers and status to the request
|
||||
let res_options = use_context::<ResponseOptions>(cx);
|
||||
let res_options =
|
||||
use_context::<ResponseOptions>(cx);
|
||||
|
||||
// clean up the scope, which we only needed to run the server fn
|
||||
disposer.dispose();
|
||||
runtime.dispose();
|
||||
|
||||
// if this is Accept: application/json then send a serialized JSON response
|
||||
let accept_header =
|
||||
headers.get("Accept").and_then(|value| value.to_str().ok());
|
||||
let accept_header = headers
|
||||
.get("Accept")
|
||||
.and_then(|value| value.to_str().ok());
|
||||
let mut res = Response::builder();
|
||||
|
||||
// Add headers from ResponseParts if they exist. These should be added as long
|
||||
// as the server function returns an OK response
|
||||
let res_options_outer = res_options.unwrap().0;
|
||||
let res_options_inner = res_options_outer.read();
|
||||
let res_options_outer =
|
||||
res_options.unwrap().0;
|
||||
let res_options_inner =
|
||||
res_options_outer.read();
|
||||
let (status, mut res_headers) = (
|
||||
res_options_inner.status,
|
||||
res_options_inner.headers.clone(),
|
||||
);
|
||||
|
||||
if let Some(header_ref) = res.headers_mut() {
|
||||
header_ref.extend(res_headers.drain());
|
||||
if let Some(header_ref) = res.headers_mut()
|
||||
{
|
||||
header_ref.extend(res_headers.drain());
|
||||
};
|
||||
|
||||
if accept_header == Some("application/json")
|
||||
|| accept_header
|
||||
== Some("application/x-www-form-urlencoded")
|
||||
|| accept_header == Some("application/cbor")
|
||||
== Some(
|
||||
"application/\
|
||||
x-www-form-urlencoded",
|
||||
)
|
||||
|| accept_header
|
||||
== Some("application/cbor")
|
||||
{
|
||||
res = res.status(StatusCode::OK);
|
||||
}
|
||||
@@ -246,7 +262,9 @@ async fn handle_server_fns_inner(
|
||||
else {
|
||||
let referer = headers
|
||||
.get("Referer")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| {
|
||||
value.to_str().ok()
|
||||
})
|
||||
.unwrap_or("/");
|
||||
|
||||
res = res
|
||||
@@ -260,16 +278,23 @@ async fn handle_server_fns_inner(
|
||||
};
|
||||
match serialized {
|
||||
Payload::Binary(data) => res
|
||||
.header("Content-Type", "application/cbor")
|
||||
.header(
|
||||
"Content-Type",
|
||||
"application/cbor",
|
||||
)
|
||||
.body(Full::from(data)),
|
||||
Payload::Url(data) => res
|
||||
.header(
|
||||
"Content-Type",
|
||||
"application/x-www-form-urlencoded",
|
||||
"application/\
|
||||
x-www-form-urlencoded",
|
||||
)
|
||||
.body(Full::from(data)),
|
||||
Payload::Json(data) => res
|
||||
.header("Content-Type", "application/json")
|
||||
.header(
|
||||
"Content-Type",
|
||||
"application/json",
|
||||
)
|
||||
.body(Full::from(data)),
|
||||
}
|
||||
}
|
||||
@@ -280,11 +305,13 @@ async fn handle_server_fns_inner(
|
||||
} else {
|
||||
Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body(Full::from(
|
||||
format!("Could not find a server function at the route {fn_name}. \
|
||||
\n\nIt's likely that you need to call ServerFn::register() on the \
|
||||
server function type, somewhere in your `main` function." )
|
||||
))
|
||||
.body(Full::from(format!(
|
||||
"Could not find a server function at the \
|
||||
route {fn_name}. \n\nIt's likely that \
|
||||
you need to call ServerFn::register() on \
|
||||
the server function type, somewhere in \
|
||||
your `main` function."
|
||||
)))
|
||||
}
|
||||
.expect("could not build Response");
|
||||
|
||||
@@ -297,7 +324,8 @@ async fn handle_server_fns_inner(
|
||||
rx.await.unwrap()
|
||||
}
|
||||
|
||||
pub type PinnedHtmlStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>>;
|
||||
pub type PinnedHtmlStream =
|
||||
Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>>;
|
||||
|
||||
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an HTML stream of your application.
|
||||
@@ -310,28 +338,28 @@ pub type PinnedHtmlStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// use axum::handler::Handler;
|
||||
/// use axum::Router;
|
||||
/// use std::{net::SocketAddr, env};
|
||||
/// use axum::{handler::Handler, Router};
|
||||
/// use leptos::*;
|
||||
/// use leptos_config::get_configuration;
|
||||
/// use std::{env, net::SocketAddr};
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> impl IntoView {
|
||||
/// view! { cx, <main>"Hello, world!"</main> }
|
||||
/// view! { cx, <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
///
|
||||
/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
|
||||
/// let leptos_options = conf.leptos_options;
|
||||
/// let addr = leptos_options.site_addr.clone();
|
||||
///
|
||||
///
|
||||
/// // build our application with a route
|
||||
/// let app = Router::new()
|
||||
/// .fallback(leptos_axum::render_app_to_stream(leptos_options, |cx| view! { cx, <MyApp/> }));
|
||||
/// let app = Router::new().fallback(leptos_axum::render_app_to_stream(
|
||||
/// leptos_options,
|
||||
/// |cx| view! { cx, <MyApp/> },
|
||||
/// ));
|
||||
///
|
||||
/// // run our app with hyper
|
||||
/// // `axum::Server` is a re-export of `hyper::Server`
|
||||
@@ -354,8 +382,13 @@ pub fn render_app_to_stream<IV>(
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<StreamBody<PinnedHtmlStream>>> + Send + 'static>>
|
||||
+ Clone
|
||||
) -> Pin<
|
||||
Box<
|
||||
dyn Future<Output = Response<StreamBody<PinnedHtmlStream>>>
|
||||
+ Send
|
||||
+ 'static,
|
||||
>,
|
||||
> + Clone
|
||||
+ Send
|
||||
+ 'static
|
||||
where
|
||||
@@ -395,8 +428,13 @@ pub fn render_app_to_stream_with_context<IV>(
|
||||
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = Response<StreamBody<PinnedHtmlStream>>> + Send + 'static>>
|
||||
+ Clone
|
||||
) -> Pin<
|
||||
Box<
|
||||
dyn Future<Output = Response<StreamBody<PinnedHtmlStream>>>
|
||||
+ Send
|
||||
+ 'static,
|
||||
>,
|
||||
> + Clone
|
||||
+ Send
|
||||
+ 'static
|
||||
where
|
||||
@@ -502,13 +540,16 @@ where
|
||||
// Extract the resources now that they've been rendered
|
||||
let res_options = res_options3.0.read();
|
||||
|
||||
let complete_stream =
|
||||
futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()])
|
||||
.chain(stream);
|
||||
let complete_stream = futures::stream::iter([
|
||||
first_chunk.unwrap(),
|
||||
second_chunk.unwrap(),
|
||||
])
|
||||
.chain(stream);
|
||||
|
||||
let mut res = Response::new(StreamBody::new(
|
||||
Box::pin(complete_stream) as PinnedHtmlStream
|
||||
));
|
||||
let mut res = Response::new(StreamBody::new(Box::pin(
|
||||
complete_stream,
|
||||
)
|
||||
as PinnedHtmlStream));
|
||||
|
||||
if let Some(status) = res_options.status {
|
||||
*res.status_mut() = status
|
||||
@@ -522,7 +563,10 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn html_parts(options: &LeptosOptions, meta: Option<&MetaContext>) -> (String, &'static str) {
|
||||
fn html_parts(
|
||||
options: &LeptosOptions,
|
||||
meta: Option<&MetaContext>,
|
||||
) -> (String, &'static str) {
|
||||
let pkg_path = &options.site_pkg_dir;
|
||||
let output_name = &options.output_name;
|
||||
|
||||
@@ -564,7 +608,8 @@ fn html_parts(options: &LeptosOptions, meta: Option<&MetaContext>) -> (String, &
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
let html_metadata = meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
|
||||
let html_metadata =
|
||||
meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
|
||||
let head = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html{html_metadata}>
|
||||
@@ -584,7 +629,9 @@ fn html_parts(options: &LeptosOptions, meta: Option<&MetaContext>) -> (String, &
|
||||
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
|
||||
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
|
||||
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
|
||||
pub async fn generate_route_list<IV>(app_fn: impl FnOnce(Scope) -> IV + 'static) -> Vec<String>
|
||||
pub async fn generate_route_list<IV>(
|
||||
app_fn: impl FnOnce(Scope) -> IV + 'static,
|
||||
) -> Vec<String>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
@@ -645,7 +692,11 @@ pub trait LeptosRoutes {
|
||||
where
|
||||
IV: IntoView + 'static;
|
||||
|
||||
fn leptos_routes_with_handler<H, T>(self, paths: Vec<String>, handler: H) -> Self
|
||||
fn leptos_routes_with_handler<H, T>(
|
||||
self,
|
||||
paths: Vec<String>,
|
||||
handler: H,
|
||||
) -> Self
|
||||
where
|
||||
H: axum::handler::Handler<T, (), axum::body::Body>,
|
||||
T: 'static;
|
||||
@@ -696,7 +747,11 @@ impl LeptosRoutes for axum::Router {
|
||||
router
|
||||
}
|
||||
|
||||
fn leptos_routes_with_handler<H, T>(self, paths: Vec<String>, handler: H) -> Self
|
||||
fn leptos_routes_with_handler<H, T>(
|
||||
self,
|
||||
paths: Vec<String>,
|
||||
handler: H,
|
||||
) -> Self
|
||||
where
|
||||
H: axum::handler::Handler<T, (), axum::body::Body>,
|
||||
T: 'static,
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
//!
|
||||
//! #[component]
|
||||
//! fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
|
||||
//! todo!()
|
||||
//! todo!()
|
||||
//! }
|
||||
//!
|
||||
//! pub fn main() {
|
||||
@@ -142,14 +142,14 @@
|
||||
//! ```
|
||||
|
||||
pub use leptos_config::*;
|
||||
pub use leptos_dom;
|
||||
pub use leptos_dom::wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
pub use leptos_dom::*;
|
||||
pub use leptos_dom::{
|
||||
self,
|
||||
wasm_bindgen::{JsCast, UnwrapThrowExt},
|
||||
*,
|
||||
};
|
||||
pub use leptos_macro::*;
|
||||
pub use leptos_reactive::*;
|
||||
pub use leptos_server;
|
||||
pub use leptos_server::*;
|
||||
|
||||
pub use leptos_server::{self, *};
|
||||
pub use tracing;
|
||||
pub use typed_builder;
|
||||
mod error_boundary;
|
||||
@@ -161,9 +161,8 @@ pub use show::*;
|
||||
mod suspense;
|
||||
pub use suspense::*;
|
||||
mod transition;
|
||||
pub use transition::*;
|
||||
|
||||
pub use leptos_dom::debug_warn;
|
||||
pub use transition::*;
|
||||
|
||||
extern crate self as leptos;
|
||||
|
||||
@@ -188,15 +187,14 @@ pub type ChildrenFnMut = Box<dyn FnMut(Scope) -> Fragment>;
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn MyHeading(
|
||||
/// cx: Scope,
|
||||
/// text: String,
|
||||
/// #[prop(optional, into)]
|
||||
/// class: Option<AttributeValue>
|
||||
/// cx: Scope,
|
||||
/// text: String,
|
||||
/// #[prop(optional, into)] class: Option<AttributeValue>,
|
||||
/// ) -> impl IntoView {
|
||||
/// view!{
|
||||
/// cx,
|
||||
/// <h1 class=class>{text}</h1>
|
||||
/// }
|
||||
/// view! {
|
||||
/// cx,
|
||||
/// <h1 class=class>{text}</h1>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub type AttributeValue = Box<dyn IntoAttribute>;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos_dom::HydrationCtx;
|
||||
use leptos_dom::{DynChild, Fragment, IntoView};
|
||||
use leptos_dom::{DynChild, Fragment, HydrationCtx, IntoView};
|
||||
use leptos_macro::component;
|
||||
use leptos_reactive::{provide_context, Scope, SuspenseContext};
|
||||
use std::rc::Rc;
|
||||
|
||||
@@ -19,7 +19,9 @@ use std::{cell::RefCell, rc::Rc};
|
||||
/// # use leptos::*;
|
||||
/// # if false {
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// async fn fetch_cats(how_many: u32) -> Option<Vec<String>> { Some(vec![]) }
|
||||
/// async fn fetch_cats(how_many: u32) -> Option<Vec<String>> {
|
||||
/// Some(vec![])
|
||||
/// }
|
||||
///
|
||||
/// let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
|
||||
/// let (pending, set_pending) = create_signal(cx, false);
|
||||
|
||||
@@ -16,7 +16,11 @@ fn simple_ssr_test() {
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<div id=\"_0-1\"><button id=\"_0-2\">-1</button><span id=\"_0-3\">Value: <!--hk=_0-4o|leptos-dyn-child-start-->0<!--hk=_0-4c|leptos-dyn-child-end-->!</span><button id=\"_0-5\">+1</button></div>"
|
||||
"<div id=\"_0-1\"><button id=\"_0-2\">-1</button><span \
|
||||
id=\"_0-3\">Value: \
|
||||
<!--hk=_0-4o|leptos-dyn-child-start-->0<!\
|
||||
--hk=_0-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-5\">+1</button></div>"
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -50,7 +54,21 @@ fn ssr_test_with_components() {
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<div id=\"_0-1\" class=\"counters\"><!--hk=_0-1-0o|leptos-counter-start--><div id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span id=\"_0-1-3\">Value: <!--hk=_0-1-4o|leptos-dyn-child-start-->1<!--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button id=\"_0-1-5\">+1</button></div><!--hk=_0-1-0c|leptos-counter-end--><!--hk=_0-1-5-0o|leptos-counter-start--><div id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span id=\"_0-1-5-3\">Value: <!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button id=\"_0-1-5-5\">+1</button></div><!--hk=_0-1-5-0c|leptos-counter-end--></div>"
|
||||
"<div id=\"_0-1\" \
|
||||
class=\"counters\"><!--hk=_0-1-0o|leptos-counter-start--><div \
|
||||
id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
|
||||
id=\"_0-1-3\">Value: \
|
||||
<!--hk=_0-1-4o|leptos-dyn-child-start-->1<!\
|
||||
--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-1-5\">+1</button></div><!\
|
||||
--hk=_0-1-0c|leptos-counter-end--><!\
|
||||
--hk=_0-1-5-0o|leptos-counter-start--><div \
|
||||
id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span \
|
||||
id=\"_0-1-5-3\">Value: \
|
||||
<!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!\
|
||||
--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-1-5-5\">+1</button></div><!\
|
||||
--hk=_0-1-5-0c|leptos-counter-end--></div>"
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -84,7 +102,22 @@ fn ssr_test_with_snake_case_components() {
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<div id=\"_0-1\" class=\"counters\"><!--hk=_0-1-0o|leptos-snake-case-counter-start--><div id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span id=\"_0-1-3\">Value: <!--hk=_0-1-4o|leptos-dyn-child-start-->1<!--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button id=\"_0-1-5\">+1</button></div><!--hk=_0-1-0c|leptos-snake-case-counter-end--><!--hk=_0-1-5-0o|leptos-snake-case-counter-start--><div id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span id=\"_0-1-5-3\">Value: <!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button id=\"_0-1-5-5\">+1</button></div><!--hk=_0-1-5-0c|leptos-snake-case-counter-end--></div>"
|
||||
"<div id=\"_0-1\" \
|
||||
class=\"counters\"><!\
|
||||
--hk=_0-1-0o|leptos-snake-case-counter-start--><div \
|
||||
id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
|
||||
id=\"_0-1-3\">Value: \
|
||||
<!--hk=_0-1-4o|leptos-dyn-child-start-->1<!\
|
||||
--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-1-5\">+1</button></div><!\
|
||||
--hk=_0-1-0c|leptos-snake-case-counter-end--><!\
|
||||
--hk=_0-1-5-0o|leptos-snake-case-counter-start--><div \
|
||||
id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span \
|
||||
id=\"_0-1-5-3\">Value: \
|
||||
<!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!\
|
||||
--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button \
|
||||
id=\"_0-1-5-5\">+1</button></div><!\
|
||||
--hk=_0-1-5-0c|leptos-snake-case-counter-end--></div>"
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -125,7 +158,8 @@ fn ssr_with_styles() {
|
||||
|
||||
assert_eq!(
|
||||
rendered.into_view(cx).render_to_string(cx),
|
||||
"<div id=\"_0-1\" class=\" myclass\"><button id=\"_0-2\" class=\"btn myclass\">-1</button></div>"
|
||||
"<div id=\"_0-1\" class=\" myclass\"><button id=\"_0-2\" \
|
||||
class=\"btn myclass\">-1</button></div>"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::{net::AddrParseError, num::ParseIntError};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error, Clone)]
|
||||
|
||||
@@ -5,9 +5,7 @@ pub mod errors;
|
||||
use crate::errors::LeptosConfigError;
|
||||
use config::{Config, File, FileFormat};
|
||||
use regex::Regex;
|
||||
use std::convert::TryFrom;
|
||||
use std::fs;
|
||||
use std::{env::VarError, net::SocketAddr, str::FromStr};
|
||||
use std::{convert::TryFrom, env::VarError, fs, net::SocketAddr, str::FromStr};
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
/// A Struct to allow us to parse LeptosOptions from the file. Not really needed, most interactions should
|
||||
@@ -53,18 +51,26 @@ pub struct LeptosOptions {
|
||||
impl LeptosOptions {
|
||||
fn try_from_env() -> Result<Self, LeptosConfigError> {
|
||||
Ok(LeptosOptions {
|
||||
output_name: std::env::var("LEPTOS_OUTPUT_NAME")
|
||||
.map_err(|e| LeptosConfigError::EnvVarError(format!("LEPTOS_OUTPUT_NAME: {e}")))?,
|
||||
output_name: std::env::var("LEPTOS_OUTPUT_NAME").map_err(|e| {
|
||||
LeptosConfigError::EnvVarError(format!(
|
||||
"LEPTOS_OUTPUT_NAME: {e}"
|
||||
))
|
||||
})?,
|
||||
site_root: env_w_default("LEPTOS_SITE_ROOT", "target/site")?,
|
||||
site_pkg_dir: env_w_default("LEPTOS_SITE_PKG_DIR", "pkg")?,
|
||||
env: Env::default(),
|
||||
site_addr: env_w_default("LEPTOS_SITE_ADDR", "127.0.0.1:3000")?.parse()?,
|
||||
reload_port: env_w_default("LEPTOS_RELOAD_PORT", "3001")?.parse()?,
|
||||
site_addr: env_w_default("LEPTOS_SITE_ADDR", "127.0.0.1:3000")?
|
||||
.parse()?,
|
||||
reload_port: env_w_default("LEPTOS_RELOAD_PORT", "3001")?
|
||||
.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn env_w_default(key: &str, default: &str) -> Result<String, LeptosConfigError> {
|
||||
fn env_w_default(
|
||||
key: &str,
|
||||
default: &str,
|
||||
) -> Result<String, LeptosConfigError> {
|
||||
match std::env::var(key) {
|
||||
Ok(val) => Ok(val),
|
||||
Err(VarError::NotPresent) => Ok(default.to_string()),
|
||||
@@ -93,7 +99,8 @@ fn from_str(input: &str) -> Result<Env, String> {
|
||||
"dev" | "development" => Ok(Env::DEV),
|
||||
"prod" | "production" => Ok(Env::PROD),
|
||||
_ => Err(format!(
|
||||
"{input} is not a supported environment. Use either `dev` or `production`.",
|
||||
"{input} is not a supported environment. Use either `dev` or \
|
||||
`production`.",
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -132,11 +139,15 @@ impl TryFrom<String> for Env {
|
||||
/// you'll need to set the options as environment variables or rely on the defaults. This is the preferred
|
||||
/// approach for cargo-leptos. If Some("./Cargo.toml") is provided, Leptos will read in the settings itself. This
|
||||
/// option currently does not allow dashes in file or foldernames, as all dashes become underscores
|
||||
pub async fn get_configuration(path: Option<&str>) -> Result<ConfFile, LeptosConfigError> {
|
||||
pub async fn get_configuration(
|
||||
path: Option<&str>,
|
||||
) -> Result<ConfFile, LeptosConfigError> {
|
||||
if let Some(path) = path {
|
||||
let text = fs::read_to_string(path).map_err(|_| LeptosConfigError::ConfigNotFound)?;
|
||||
let text = fs::read_to_string(path)
|
||||
.map_err(|_| LeptosConfigError::ConfigNotFound)?;
|
||||
|
||||
let re: Regex = Regex::new(r#"(?m)^\[package.metadata.leptos\]"#).unwrap();
|
||||
let re: Regex =
|
||||
Regex::new(r#"(?m)^\[package.metadata.leptos\]"#).unwrap();
|
||||
let start = match re.find(&text) {
|
||||
Some(found) => found.start(),
|
||||
None => return Err(LeptosConfigError::ConfigSectionNotFound),
|
||||
@@ -154,7 +165,9 @@ pub async fn get_configuration(path: Option<&str>) -> Result<ConfFile, LeptosCon
|
||||
// Layer on the environment-specific values.
|
||||
// Add in settings from environment variables (with a prefix of LEPTOS and '_' as separator)
|
||||
// E.g. `LEPTOS_RELOAD_PORT=5001 would set `LeptosOptions.reload_port`
|
||||
.add_source(config::Environment::with_prefix("LEPTOS").separator("_"))
|
||||
.add_source(
|
||||
config::Environment::with_prefix("LEPTOS").separator("_"),
|
||||
)
|
||||
.build()?;
|
||||
|
||||
settings
|
||||
|
||||
@@ -5,8 +5,8 @@ mod fragment;
|
||||
mod unit;
|
||||
|
||||
use crate::{
|
||||
hydration::{HydrationCtx, HydrationKey},
|
||||
Comment, IntoView, View,
|
||||
hydration::{HydrationCtx, HydrationKey},
|
||||
Comment, IntoView, View,
|
||||
};
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use crate::{mount_child, prepare_to_move, MountKind, Mountable};
|
||||
@@ -28,231 +28,234 @@ use wasm_bindgen::JsCast;
|
||||
#[derive(educe::Educe)]
|
||||
#[educe(Default, Clone, PartialEq, Eq)]
|
||||
pub enum CoreComponent {
|
||||
/// The [Unit] component.
|
||||
#[educe(Default)]
|
||||
Unit(UnitRepr),
|
||||
/// The [DynChild] component.
|
||||
DynChild(DynChildRepr),
|
||||
/// The [Each] component.
|
||||
Each(EachRepr),
|
||||
/// The [Unit] component.
|
||||
#[educe(Default)]
|
||||
Unit(UnitRepr),
|
||||
/// The [DynChild] component.
|
||||
DynChild(DynChildRepr),
|
||||
/// The [Each] component.
|
||||
Each(EachRepr),
|
||||
}
|
||||
|
||||
impl fmt::Debug for CoreComponent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Unit(u) => u.fmt(f),
|
||||
Self::DynChild(dc) => dc.fmt(f),
|
||||
Self::Each(e) => e.fmt(f),
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Unit(u) => u.fmt(f),
|
||||
Self::DynChild(dc) => dc.fmt(f),
|
||||
Self::Each(e) => e.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom leptos component.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct ComponentRepr {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) document_fragment: web_sys::DocumentFragment,
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
mounted: Rc<OnceCell<()>>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) name: Cow<'static, str>,
|
||||
#[cfg(debug_assertions)]
|
||||
_opening: Comment,
|
||||
/// The children of the component.
|
||||
pub children: Vec<View>,
|
||||
closing: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: HydrationKey,
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) document_fragment: web_sys::DocumentFragment,
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
mounted: Rc<OnceCell<()>>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) name: Cow<'static, str>,
|
||||
#[cfg(debug_assertions)]
|
||||
_opening: Comment,
|
||||
/// The children of the component.
|
||||
pub children: Vec<View>,
|
||||
closing: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: HydrationKey,
|
||||
}
|
||||
|
||||
impl fmt::Debug for ComponentRepr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use fmt::Write;
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use fmt::Write;
|
||||
|
||||
if self.children.is_empty() {
|
||||
#[cfg(debug_assertions)]
|
||||
return write!(f, "<{} />", self.name);
|
||||
if self.children.is_empty() {
|
||||
#[cfg(debug_assertions)]
|
||||
return write!(f, "<{} />", self.name);
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
return f.write_str("<Component />");
|
||||
} else {
|
||||
#[cfg(debug_assertions)]
|
||||
writeln!(f, "<{}>", self.name)?;
|
||||
#[cfg(not(debug_assertions))]
|
||||
f.write_str("<Component>")?;
|
||||
#[cfg(not(debug_assertions))]
|
||||
return f.write_str("<Component />");
|
||||
} else {
|
||||
#[cfg(debug_assertions)]
|
||||
writeln!(f, "<{}>", self.name)?;
|
||||
#[cfg(not(debug_assertions))]
|
||||
f.write_str("<Component>")?;
|
||||
|
||||
let mut pad_adapter = pad_adapter::PadAdapter::new(f);
|
||||
let mut pad_adapter = pad_adapter::PadAdapter::new(f);
|
||||
|
||||
for child in &self.children {
|
||||
writeln!(pad_adapter, "{child:#?}")?;
|
||||
}
|
||||
for child in &self.children {
|
||||
writeln!(pad_adapter, "{child:#?}")?;
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
write!(f, "</{}>", self.name)?;
|
||||
#[cfg(not(debug_assertions))]
|
||||
f.write_str("</Component>")?;
|
||||
#[cfg(debug_assertions)]
|
||||
write!(f, "</{}>", self.name)?;
|
||||
#[cfg(not(debug_assertions))]
|
||||
f.write_str("</Component>")?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
impl Mountable for ComponentRepr {
|
||||
fn get_mountable_node(&self) -> web_sys::Node {
|
||||
if self.mounted.get().is_none() {
|
||||
self.mounted.set(()).unwrap();
|
||||
fn get_mountable_node(&self) -> web_sys::Node {
|
||||
if self.mounted.get().is_none() {
|
||||
self.mounted.set(()).unwrap();
|
||||
|
||||
self
|
||||
.document_fragment
|
||||
.unchecked_ref::<web_sys::Node>()
|
||||
.to_owned()
|
||||
self.document_fragment
|
||||
.unchecked_ref::<web_sys::Node>()
|
||||
.to_owned()
|
||||
}
|
||||
// We need to prepare all children to move
|
||||
else {
|
||||
let opening = self.get_opening_node();
|
||||
|
||||
prepare_to_move(
|
||||
&self.document_fragment,
|
||||
&opening,
|
||||
&self.closing.node,
|
||||
);
|
||||
|
||||
self.document_fragment.clone().unchecked_into()
|
||||
}
|
||||
}
|
||||
// We need to prepare all children to move
|
||||
else {
|
||||
let opening = self.get_opening_node();
|
||||
|
||||
prepare_to_move(&self.document_fragment, &opening, &self.closing.node);
|
||||
fn get_opening_node(&self) -> web_sys::Node {
|
||||
#[cfg(debug_assertions)]
|
||||
return self._opening.node.clone();
|
||||
|
||||
self.document_fragment.clone().unchecked_into()
|
||||
#[cfg(not(debug_assertions))]
|
||||
return if let Some(child) = self.children.get(0) {
|
||||
child.get_opening_node()
|
||||
} else {
|
||||
self.closing.node.clone()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn get_opening_node(&self) -> web_sys::Node {
|
||||
#[cfg(debug_assertions)]
|
||||
return self._opening.node.clone();
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
return if let Some(child) = self.children.get(0) {
|
||||
child.get_opening_node()
|
||||
} else {
|
||||
self.closing.node.clone()
|
||||
};
|
||||
}
|
||||
|
||||
fn get_closing_node(&self) -> web_sys::Node {
|
||||
self.closing.node.clone()
|
||||
}
|
||||
fn get_closing_node(&self) -> web_sys::Node {
|
||||
self.closing.node.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoView for ComponentRepr {
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "<Component />", skip_all, fields(name = %self.name)))]
|
||||
fn into_view(self, _: Scope) -> View {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
for child in &self.children {
|
||||
mount_child(MountKind::Before(&self.closing.node), child);
|
||||
}
|
||||
}
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "<Component />", skip_all, fields(name = %self.name)))]
|
||||
fn into_view(self, _: Scope) -> View {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
for child in &self.children {
|
||||
mount_child(MountKind::Before(&self.closing.node), child);
|
||||
}
|
||||
}
|
||||
|
||||
View::Component(self)
|
||||
}
|
||||
View::Component(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentRepr {
|
||||
/// Creates a new [`Component`].
|
||||
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self::new_with_id(name, HydrationCtx::id())
|
||||
}
|
||||
|
||||
/// Creates a new [`Component`] with the given hydration ID.
|
||||
pub fn new_with_id(
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
id: HydrationKey,
|
||||
) -> Self {
|
||||
let name = name.into();
|
||||
|
||||
let markers = (
|
||||
Comment::new(Cow::Owned(format!("</{name}>")), &id, true),
|
||||
#[cfg(debug_assertions)]
|
||||
Comment::new(Cow::Owned(format!("<{name}>")), &id, false),
|
||||
);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let document_fragment = {
|
||||
let fragment = crate::document().create_document_fragment();
|
||||
|
||||
// Insert the comments into the document fragment
|
||||
// so they can serve as our references when inserting
|
||||
// future nodes
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
#[cfg(debug_assertions)]
|
||||
fragment
|
||||
.append_with_node_2(&markers.1.node, &markers.0.node)
|
||||
.expect("append to not err");
|
||||
#[cfg(not(debug_assertions))]
|
||||
fragment
|
||||
.append_with_node_1(&markers.0.node)
|
||||
.expect("append to not err");
|
||||
}
|
||||
|
||||
fragment
|
||||
};
|
||||
|
||||
Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
document_fragment,
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
mounted: Default::default(),
|
||||
#[cfg(debug_assertions)]
|
||||
_opening: markers.1,
|
||||
closing: markers.0,
|
||||
#[cfg(debug_assertions)]
|
||||
name,
|
||||
children: Vec::with_capacity(1),
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id,
|
||||
/// Creates a new [`Component`].
|
||||
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self::new_with_id(name, HydrationCtx::id())
|
||||
}
|
||||
|
||||
/// Creates a new [`Component`] with the given hydration ID.
|
||||
pub fn new_with_id(
|
||||
name: impl Into<Cow<'static, str>>,
|
||||
id: HydrationKey,
|
||||
) -> Self {
|
||||
let name = name.into();
|
||||
|
||||
let markers = (
|
||||
Comment::new(Cow::Owned(format!("</{name}>")), &id, true),
|
||||
#[cfg(debug_assertions)]
|
||||
Comment::new(Cow::Owned(format!("<{name}>")), &id, false),
|
||||
);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let document_fragment = {
|
||||
let fragment = crate::document().create_document_fragment();
|
||||
|
||||
// Insert the comments into the document fragment
|
||||
// so they can serve as our references when inserting
|
||||
// future nodes
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
#[cfg(debug_assertions)]
|
||||
fragment
|
||||
.append_with_node_2(&markers.1.node, &markers.0.node)
|
||||
.expect("append to not err");
|
||||
#[cfg(not(debug_assertions))]
|
||||
fragment
|
||||
.append_with_node_1(&markers.0.node)
|
||||
.expect("append to not err");
|
||||
}
|
||||
|
||||
fragment
|
||||
};
|
||||
|
||||
Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
document_fragment,
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
mounted: Default::default(),
|
||||
#[cfg(debug_assertions)]
|
||||
_opening: markers.1,
|
||||
closing: markers.0,
|
||||
#[cfg(debug_assertions)]
|
||||
name,
|
||||
children: Vec::with_capacity(1),
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A user-defined `leptos` component.
|
||||
pub struct Component<F, V>
|
||||
where
|
||||
F: FnOnce(Scope) -> V,
|
||||
V: IntoView,
|
||||
F: FnOnce(Scope) -> V,
|
||||
V: IntoView,
|
||||
{
|
||||
id: HydrationKey,
|
||||
name: Cow<'static, str>,
|
||||
children_fn: F,
|
||||
id: HydrationKey,
|
||||
name: Cow<'static, str>,
|
||||
children_fn: F,
|
||||
}
|
||||
|
||||
impl<F, V> Component<F, V>
|
||||
where
|
||||
F: FnOnce(Scope) -> V,
|
||||
V: IntoView,
|
||||
F: FnOnce(Scope) -> V,
|
||||
V: IntoView,
|
||||
{
|
||||
/// Creates a new component.
|
||||
pub fn new(name: impl Into<Cow<'static, str>>, f: F) -> Self {
|
||||
Self {
|
||||
id: HydrationCtx::next_component(),
|
||||
name: name.into(),
|
||||
children_fn: f,
|
||||
/// Creates a new component.
|
||||
pub fn new(name: impl Into<Cow<'static, str>>, f: F) -> Self {
|
||||
Self {
|
||||
id: HydrationCtx::next_component(),
|
||||
name: name.into(),
|
||||
children_fn: f,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, V> IntoView for Component<F, V>
|
||||
where
|
||||
F: FnOnce(Scope) -> V,
|
||||
V: IntoView,
|
||||
F: FnOnce(Scope) -> V,
|
||||
V: IntoView,
|
||||
{
|
||||
#[track_caller]
|
||||
fn into_view(self, cx: Scope) -> View {
|
||||
let Self {
|
||||
id,
|
||||
name,
|
||||
children_fn,
|
||||
} = self;
|
||||
#[track_caller]
|
||||
fn into_view(self, cx: Scope) -> View {
|
||||
let Self {
|
||||
id,
|
||||
name,
|
||||
children_fn,
|
||||
} = self;
|
||||
|
||||
let mut repr = ComponentRepr::new_with_id(name, id);
|
||||
let mut repr = ComponentRepr::new_with_id(name, id);
|
||||
|
||||
// disposed automatically when the parent scope is disposed
|
||||
let (child, _) =
|
||||
cx.run_child_scope(|cx| cx.untrack(|| children_fn(cx).into_view(cx)));
|
||||
// disposed automatically when the parent scope is disposed
|
||||
let (child, _) = cx
|
||||
.run_child_scope(|cx| cx.untrack(|| children_fn(cx).into_view(cx)));
|
||||
|
||||
repr.children.push(child);
|
||||
repr.children.push(child);
|
||||
|
||||
repr.into_view(cx)
|
||||
}
|
||||
repr.into_view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
hydration::{HydrationCtx, HydrationKey},
|
||||
Comment, IntoView, View,
|
||||
hydration::{HydrationCtx, HydrationKey},
|
||||
Comment, IntoView, View,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos_reactive::Scope;
|
||||
@@ -16,320 +16,349 @@ cfg_if! {
|
||||
/// The internal representation of the [`DynChild`] core-component.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct DynChildRepr {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
document_fragment: web_sys::DocumentFragment,
|
||||
#[cfg(debug_assertions)]
|
||||
opening: Comment,
|
||||
pub(crate) child: Rc<RefCell<Box<Option<View>>>>,
|
||||
closing: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: HydrationKey,
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
document_fragment: web_sys::DocumentFragment,
|
||||
#[cfg(debug_assertions)]
|
||||
opening: Comment,
|
||||
pub(crate) child: Rc<RefCell<Box<Option<View>>>>,
|
||||
closing: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: HydrationKey,
|
||||
}
|
||||
|
||||
impl fmt::Debug for DynChildRepr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use fmt::Write;
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use fmt::Write;
|
||||
|
||||
f.write_str("<DynChild>\n")?;
|
||||
f.write_str("<DynChild>\n")?;
|
||||
|
||||
let mut pad_adapter = pad_adapter::PadAdapter::new(f);
|
||||
let mut pad_adapter = pad_adapter::PadAdapter::new(f);
|
||||
|
||||
writeln!(
|
||||
pad_adapter,
|
||||
"{:#?}",
|
||||
self.child.borrow().deref().deref().as_ref().unwrap()
|
||||
)?;
|
||||
writeln!(
|
||||
pad_adapter,
|
||||
"{:#?}",
|
||||
self.child.borrow().deref().deref().as_ref().unwrap()
|
||||
)?;
|
||||
|
||||
f.write_str("</DynChild>")
|
||||
}
|
||||
f.write_str("</DynChild>")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
impl Mountable for DynChildRepr {
|
||||
fn get_mountable_node(&self) -> web_sys::Node {
|
||||
if self.document_fragment.child_nodes().length() != 0 {
|
||||
self.document_fragment.clone().unchecked_into()
|
||||
} else {
|
||||
let opening = self.get_opening_node();
|
||||
fn get_mountable_node(&self) -> web_sys::Node {
|
||||
if self.document_fragment.child_nodes().length() != 0 {
|
||||
self.document_fragment.clone().unchecked_into()
|
||||
} else {
|
||||
let opening = self.get_opening_node();
|
||||
|
||||
prepare_to_move(&self.document_fragment, &opening, &self.closing.node);
|
||||
prepare_to_move(
|
||||
&self.document_fragment,
|
||||
&opening,
|
||||
&self.closing.node,
|
||||
);
|
||||
|
||||
self.document_fragment.clone().unchecked_into()
|
||||
self.document_fragment.clone().unchecked_into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_opening_node(&self) -> web_sys::Node {
|
||||
#[cfg(debug_assertions)]
|
||||
return self.opening.node.clone();
|
||||
fn get_opening_node(&self) -> web_sys::Node {
|
||||
#[cfg(debug_assertions)]
|
||||
return self.opening.node.clone();
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
return self
|
||||
.child
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_opening_node();
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
return self
|
||||
.child
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_opening_node();
|
||||
}
|
||||
|
||||
fn get_closing_node(&self) -> web_sys::Node {
|
||||
self.closing.node.clone()
|
||||
}
|
||||
fn get_closing_node(&self) -> web_sys::Node {
|
||||
self.closing.node.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl DynChildRepr {
|
||||
fn new_with_id(id: HydrationKey) -> Self {
|
||||
let markers = (
|
||||
Comment::new(Cow::Borrowed("</DynChild>"), &id, true),
|
||||
#[cfg(debug_assertions)]
|
||||
Comment::new(Cow::Borrowed("<DynChild>"), &id, false),
|
||||
);
|
||||
fn new_with_id(id: HydrationKey) -> Self {
|
||||
let markers = (
|
||||
Comment::new(Cow::Borrowed("</DynChild>"), &id, true),
|
||||
#[cfg(debug_assertions)]
|
||||
Comment::new(Cow::Borrowed("<DynChild>"), &id, false),
|
||||
);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let document_fragment = {
|
||||
let fragment = crate::document().create_document_fragment();
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let document_fragment = {
|
||||
let fragment = crate::document().create_document_fragment();
|
||||
|
||||
// Insert the comments into the document fragment
|
||||
// so they can serve as our references when inserting
|
||||
// future nodes
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
#[cfg(debug_assertions)]
|
||||
fragment
|
||||
.append_with_node_2(&markers.1.node, &markers.0.node)
|
||||
.unwrap();
|
||||
#[cfg(not(debug_assertions))]
|
||||
fragment.append_with_node_1(&markers.0.node).unwrap();
|
||||
}
|
||||
// Insert the comments into the document fragment
|
||||
// so they can serve as our references when inserting
|
||||
// future nodes
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
#[cfg(debug_assertions)]
|
||||
fragment
|
||||
.append_with_node_2(&markers.1.node, &markers.0.node)
|
||||
.unwrap();
|
||||
#[cfg(not(debug_assertions))]
|
||||
fragment.append_with_node_1(&markers.0.node).unwrap();
|
||||
}
|
||||
|
||||
fragment
|
||||
};
|
||||
fragment
|
||||
};
|
||||
|
||||
Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
document_fragment,
|
||||
#[cfg(debug_assertions)]
|
||||
opening: markers.1,
|
||||
child: Default::default(),
|
||||
closing: markers.0,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id,
|
||||
Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
document_fragment,
|
||||
#[cfg(debug_assertions)]
|
||||
opening: markers.1,
|
||||
child: Default::default(),
|
||||
closing: markers.0,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents any [`View`] that can change over time.
|
||||
pub struct DynChild<CF, N>
|
||||
where
|
||||
CF: Fn() -> N + 'static,
|
||||
N: IntoView,
|
||||
CF: Fn() -> N + 'static,
|
||||
N: IntoView,
|
||||
{
|
||||
id: crate::HydrationKey,
|
||||
child_fn: CF,
|
||||
id: crate::HydrationKey,
|
||||
child_fn: CF,
|
||||
}
|
||||
|
||||
impl<CF, N> DynChild<CF, N>
|
||||
where
|
||||
CF: Fn() -> N + 'static,
|
||||
N: IntoView,
|
||||
CF: Fn() -> N + 'static,
|
||||
N: IntoView,
|
||||
{
|
||||
/// Creates a new dynamic child which will re-render whenever it's
|
||||
/// signal dependencies change.
|
||||
pub fn new(child_fn: CF) -> Self {
|
||||
Self::new_with_id(HydrationCtx::id(), child_fn)
|
||||
}
|
||||
/// Creates a new dynamic child which will re-render whenever it's
|
||||
/// signal dependencies change.
|
||||
pub fn new(child_fn: CF) -> Self {
|
||||
Self::new_with_id(HydrationCtx::id(), child_fn)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn new_with_id(id: HydrationKey, child_fn: CF) -> Self {
|
||||
Self { id, child_fn }
|
||||
}
|
||||
#[doc(hidden)]
|
||||
pub fn new_with_id(id: HydrationKey, child_fn: CF) -> Self {
|
||||
Self { id, child_fn }
|
||||
}
|
||||
}
|
||||
|
||||
impl<CF, N> IntoView for DynChild<CF, N>
|
||||
where
|
||||
CF: Fn() -> N + 'static,
|
||||
N: IntoView,
|
||||
CF: Fn() -> N + 'static,
|
||||
N: IntoView,
|
||||
{
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", name = "<DynChild />", skip_all)
|
||||
)]
|
||||
fn into_view(self, cx: Scope) -> View {
|
||||
// concrete inner function
|
||||
fn create_dyn_view(
|
||||
cx: Scope,
|
||||
component: DynChildRepr,
|
||||
child_fn: Box<dyn Fn() -> View>,
|
||||
) -> DynChildRepr {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let closing = component.closing.node.clone();
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", name = "<DynChild />", skip_all)
|
||||
)]
|
||||
fn into_view(self, cx: Scope) -> View {
|
||||
// concrete inner function
|
||||
fn create_dyn_view(
|
||||
cx: Scope,
|
||||
component: DynChildRepr,
|
||||
child_fn: Box<dyn Fn() -> View>,
|
||||
) -> DynChildRepr {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let closing = component.closing.node.clone();
|
||||
|
||||
let child = component.child.clone();
|
||||
let child = component.child.clone();
|
||||
|
||||
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
|
||||
let span = tracing::Span::current();
|
||||
#[cfg(all(
|
||||
debug_assertions,
|
||||
target_arch = "wasm32",
|
||||
feature = "web"
|
||||
))]
|
||||
let span = tracing::Span::current();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
create_effect(
|
||||
cx,
|
||||
move |prev_run: Option<(Option<web_sys::Node>, ScopeDisposer)>| {
|
||||
#[cfg(debug_assertions)]
|
||||
let _guard = span.enter();
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
create_effect(
|
||||
cx,
|
||||
move |prev_run: Option<(
|
||||
Option<web_sys::Node>,
|
||||
ScopeDisposer,
|
||||
)>| {
|
||||
#[cfg(debug_assertions)]
|
||||
let _guard = span.enter();
|
||||
|
||||
let (new_child, disposer) =
|
||||
cx.run_child_scope(|cx| child_fn().into_view(cx));
|
||||
let (new_child, disposer) =
|
||||
cx.run_child_scope(|cx| child_fn().into_view(cx));
|
||||
|
||||
let mut child_borrow = child.borrow_mut();
|
||||
let mut child_borrow = child.borrow_mut();
|
||||
|
||||
// Is this at least the second time we are loading a child?
|
||||
if let Some((prev_t, prev_disposer)) = prev_run {
|
||||
let child = child_borrow.take().unwrap();
|
||||
// Is this at least the second time we are loading a child?
|
||||
if let Some((prev_t, prev_disposer)) = prev_run {
|
||||
let child = child_borrow.take().unwrap();
|
||||
|
||||
// Dispose of the scope
|
||||
prev_disposer.dispose();
|
||||
// Dispose of the scope
|
||||
prev_disposer.dispose();
|
||||
|
||||
// We need to know if our child wasn't moved elsewhere.
|
||||
// If it was, `DynChild` no longer "owns" that child, and
|
||||
// is therefore no longer sound to unmount it from the DOM
|
||||
// or to reuse it in the case of a text node
|
||||
// We need to know if our child wasn't moved elsewhere.
|
||||
// If it was, `DynChild` no longer "owns" that child, and
|
||||
// is therefore no longer sound to unmount it from the DOM
|
||||
// or to reuse it in the case of a text node
|
||||
|
||||
// TODO check does this still detect moves correctly?
|
||||
let was_child_moved = prev_t.is_none()
|
||||
&& child.get_closing_node().next_sibling().as_ref()
|
||||
!= Some(&closing);
|
||||
// TODO check does this still detect moves correctly?
|
||||
let was_child_moved = prev_t.is_none()
|
||||
&& child.get_closing_node().next_sibling().as_ref()
|
||||
!= Some(&closing);
|
||||
|
||||
// If the previous child was a text node, we would like to
|
||||
// make use of it again if our current child is also a text
|
||||
// node
|
||||
let ret = if let Some(prev_t) = prev_t {
|
||||
// Here, our child is also a text node
|
||||
if let Some(new_t) = new_child.get_text() {
|
||||
if !was_child_moved && child != new_child {
|
||||
prev_t
|
||||
.unchecked_ref::<web_sys::Text>()
|
||||
.set_data(&new_t.content);
|
||||
// If the previous child was a text node, we would like to
|
||||
// make use of it again if our current child is also a text
|
||||
// node
|
||||
let ret = if let Some(prev_t) = prev_t {
|
||||
// Here, our child is also a text node
|
||||
if let Some(new_t) = new_child.get_text() {
|
||||
if !was_child_moved && child != new_child {
|
||||
prev_t
|
||||
.unchecked_ref::<web_sys::Text>()
|
||||
.set_data(&new_t.content);
|
||||
|
||||
**child_borrow = Some(new_child);
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
(Some(prev_t), disposer)
|
||||
} else {
|
||||
mount_child(MountKind::Before(&closing), &new_child);
|
||||
(Some(prev_t), disposer)
|
||||
} else {
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
);
|
||||
|
||||
**child_borrow = Some(new_child.clone());
|
||||
**child_borrow = Some(new_child.clone());
|
||||
|
||||
(Some(new_t.node.clone()), disposer)
|
||||
}
|
||||
}
|
||||
// Child is not a text node, so we can remove the previous
|
||||
// text node
|
||||
else {
|
||||
if !was_child_moved && child != new_child {
|
||||
// Remove the text
|
||||
closing
|
||||
.previous_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::Element>()
|
||||
.remove();
|
||||
}
|
||||
(Some(new_t.node.clone()), disposer)
|
||||
}
|
||||
}
|
||||
// Child is not a text node, so we can remove the previous
|
||||
// text node
|
||||
else {
|
||||
if !was_child_moved && child != new_child {
|
||||
// Remove the text
|
||||
closing
|
||||
.previous_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::Element>()
|
||||
.remove();
|
||||
}
|
||||
|
||||
// Mount the new child, and we're done
|
||||
mount_child(MountKind::Before(&closing), &new_child);
|
||||
// Mount the new child, and we're done
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
);
|
||||
|
||||
**child_borrow = Some(new_child);
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
(None, disposer)
|
||||
}
|
||||
}
|
||||
// Otherwise, the new child can still be a text node,
|
||||
// but we know the previous child was not, so no special
|
||||
// treatment here
|
||||
else {
|
||||
// Technically, I think this check shouldn't be necessary, but
|
||||
// I can imagine some edge case that the child changes while
|
||||
// hydration is ongoing
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
if !was_child_moved && child != new_child {
|
||||
// Remove the child
|
||||
let start = child.get_opening_node();
|
||||
let end = &closing;
|
||||
(None, disposer)
|
||||
}
|
||||
}
|
||||
// Otherwise, the new child can still be a text node,
|
||||
// but we know the previous child was not, so no special
|
||||
// treatment here
|
||||
else {
|
||||
// Technically, I think this check shouldn't be necessary, but
|
||||
// I can imagine some edge case that the child changes while
|
||||
// hydration is ongoing
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
if !was_child_moved && child != new_child {
|
||||
// Remove the child
|
||||
let start = child.get_opening_node();
|
||||
let end = &closing;
|
||||
|
||||
unmount_child(&start, end);
|
||||
}
|
||||
unmount_child(&start, end);
|
||||
}
|
||||
|
||||
// Mount the new child
|
||||
mount_child(MountKind::Before(&closing), &new_child);
|
||||
}
|
||||
// Mount the new child
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
);
|
||||
}
|
||||
|
||||
// We want to reuse text nodes, so hold onto it if
|
||||
// our child is one
|
||||
let t = new_child.get_text().map(|t| t.node.clone());
|
||||
// We want to reuse text nodes, so hold onto it if
|
||||
// our child is one
|
||||
let t =
|
||||
new_child.get_text().map(|t| t.node.clone());
|
||||
|
||||
**child_borrow = Some(new_child);
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
(t, disposer)
|
||||
};
|
||||
(t, disposer)
|
||||
};
|
||||
|
||||
ret
|
||||
}
|
||||
// Otherwise, we know for sure this is our first time
|
||||
else {
|
||||
// We need to remove the text created from SSR
|
||||
if HydrationCtx::is_hydrating() && new_child.get_text().is_some() {
|
||||
let t = closing
|
||||
.previous_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::Element>();
|
||||
ret
|
||||
}
|
||||
// Otherwise, we know for sure this is our first time
|
||||
else {
|
||||
// We need to remove the text created from SSR
|
||||
if HydrationCtx::is_hydrating()
|
||||
&& new_child.get_text().is_some()
|
||||
{
|
||||
let t = closing
|
||||
.previous_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::Element>();
|
||||
|
||||
// See note on ssr.rs when matching on `DynChild`
|
||||
// for more details on why we need to do this for
|
||||
// release
|
||||
if !cfg!(debug_assertions) {
|
||||
t.previous_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::Element>()
|
||||
.remove();
|
||||
}
|
||||
// See note on ssr.rs when matching on `DynChild`
|
||||
// for more details on why we need to do this for
|
||||
// release
|
||||
if !cfg!(debug_assertions) {
|
||||
t.previous_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::Element>()
|
||||
.remove();
|
||||
}
|
||||
|
||||
t.remove();
|
||||
t.remove();
|
||||
|
||||
mount_child(MountKind::Before(&closing), &new_child);
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
);
|
||||
}
|
||||
|
||||
// If we are not hydrating, we simply mount the child
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
);
|
||||
}
|
||||
|
||||
// We want to update text nodes, rather than replace them, so
|
||||
// make sure to hold onto the text node
|
||||
let t = new_child.get_text().map(|t| t.node.clone());
|
||||
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
(t, disposer)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
{
|
||||
let new_child = child_fn().into_view(cx);
|
||||
|
||||
**child.borrow_mut() = Some(new_child);
|
||||
}
|
||||
|
||||
// If we are not hydrating, we simply mount the child
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
mount_child(MountKind::Before(&closing), &new_child);
|
||||
}
|
||||
component
|
||||
}
|
||||
|
||||
// We want to update text nodes, rather than replace them, so
|
||||
// make sure to hold onto the text node
|
||||
let t = new_child.get_text().map(|t| t.node.clone());
|
||||
// monomorphized outer function
|
||||
let Self { id, child_fn } = self;
|
||||
|
||||
**child_borrow = Some(new_child);
|
||||
let component = DynChildRepr::new_with_id(id);
|
||||
let component = create_dyn_view(
|
||||
cx,
|
||||
component,
|
||||
Box::new(move || child_fn().into_view(cx)),
|
||||
);
|
||||
|
||||
(t, disposer)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
{
|
||||
let new_child = child_fn().into_view(cx);
|
||||
|
||||
**child.borrow_mut() = Some(new_child);
|
||||
}
|
||||
|
||||
component
|
||||
View::CoreComponent(crate::CoreComponent::DynChild(component))
|
||||
}
|
||||
|
||||
// monomorphized outer function
|
||||
let Self { id, child_fn } = self;
|
||||
|
||||
let component = DynChildRepr::new_with_id(id);
|
||||
let component = create_dyn_view(
|
||||
cx,
|
||||
component,
|
||||
Box::new(move || child_fn().into_view(cx)),
|
||||
);
|
||||
|
||||
View::CoreComponent(crate::CoreComponent::DynChild(component))
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,79 +9,83 @@ pub struct Errors(pub HashMap<String, Arc<dyn Error + Send + Sync>>);
|
||||
|
||||
impl<T, E> IntoView for Result<T, E>
|
||||
where
|
||||
T: IntoView + 'static,
|
||||
E: Error + Send + Sync + 'static,
|
||||
T: IntoView + 'static,
|
||||
E: Error + Send + Sync + 'static,
|
||||
{
|
||||
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
|
||||
let id = HydrationCtx::peek().previous;
|
||||
let errors = use_context::<RwSignal<Errors>>(cx);
|
||||
match self {
|
||||
Ok(stuff) => {
|
||||
if let Some(errors) = errors {
|
||||
errors.update(|errors| {
|
||||
errors.0.remove(&id);
|
||||
});
|
||||
}
|
||||
stuff.into_view(cx)
|
||||
}
|
||||
Err(error) => {
|
||||
match errors {
|
||||
Some(errors) => {
|
||||
errors.update({
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let id = id.clone();
|
||||
move |errors: &mut Errors| errors.insert(id, error)
|
||||
});
|
||||
|
||||
// remove the error from the list if this drops,
|
||||
// i.e., if it's in a DynChild that switches from Err to Ok
|
||||
// Only can run on the client, will panic on the server
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
use leptos_reactive::{on_cleanup, queue_microtask};
|
||||
on_cleanup(cx, move || {
|
||||
queue_microtask(move || {
|
||||
errors.update(|errors: &mut Errors| {
|
||||
errors.remove::<E>(&id);
|
||||
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
|
||||
let id = HydrationCtx::peek().previous;
|
||||
let errors = use_context::<RwSignal<Errors>>(cx);
|
||||
match self {
|
||||
Ok(stuff) => {
|
||||
if let Some(errors) = errors {
|
||||
errors.update(|errors| {
|
||||
errors.0.remove(&id);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
stuff.into_view(cx)
|
||||
}
|
||||
Err(error) => {
|
||||
match errors {
|
||||
Some(errors) => {
|
||||
errors.update({
|
||||
#[cfg(all(
|
||||
target_arch = "wasm32",
|
||||
feature = "web"
|
||||
))]
|
||||
let id = id.clone();
|
||||
move |errors: &mut Errors| errors.insert(id, error)
|
||||
});
|
||||
|
||||
// remove the error from the list if this drops,
|
||||
// i.e., if it's in a DynChild that switches from Err to Ok
|
||||
// Only can run on the client, will panic on the server
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
use leptos_reactive::{on_cleanup, queue_microtask};
|
||||
on_cleanup(cx, move || {
|
||||
queue_microtask(move || {
|
||||
errors.update(|errors: &mut Errors| {
|
||||
errors.remove::<E>(&id);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
#[cfg(debug_assertions)]
|
||||
warn!(
|
||||
"No ErrorBoundary components found! Returning \
|
||||
errors will not be handled and will silently \
|
||||
disappear"
|
||||
);
|
||||
}
|
||||
}
|
||||
().into_view(cx)
|
||||
}
|
||||
}
|
||||
None => {
|
||||
#[cfg(debug_assertions)]
|
||||
warn!(
|
||||
"No ErrorBoundary components found! Returning errors will not \
|
||||
be handled and will silently disappear"
|
||||
);
|
||||
}
|
||||
}
|
||||
().into_view(cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Errors {
|
||||
/// Add an error to Errors that will be processed by `<ErrorBoundary/>`
|
||||
pub fn insert<E>(&mut self, key: String, error: E)
|
||||
where
|
||||
E: Error + Send + Sync + 'static,
|
||||
{
|
||||
self.0.insert(key, Arc::new(error));
|
||||
}
|
||||
/// Add an error with the default key for errors outside the reactive system
|
||||
pub fn insert_with_default_key<E>(&mut self, error: E)
|
||||
where
|
||||
E: Error + Send + Sync + 'static,
|
||||
{
|
||||
self.0.insert(String::new(), Arc::new(error));
|
||||
}
|
||||
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
|
||||
pub fn remove<E>(&mut self, key: &str)
|
||||
where
|
||||
E: Error + Send + Sync + 'static,
|
||||
{
|
||||
self.0.remove(key);
|
||||
}
|
||||
/// Add an error to Errors that will be processed by `<ErrorBoundary/>`
|
||||
pub fn insert<E>(&mut self, key: String, error: E)
|
||||
where
|
||||
E: Error + Send + Sync + 'static,
|
||||
{
|
||||
self.0.insert(key, Arc::new(error));
|
||||
}
|
||||
/// Add an error with the default key for errors outside the reactive system
|
||||
pub fn insert_with_default_key<E>(&mut self, error: E)
|
||||
where
|
||||
E: Error + Send + Sync + 'static,
|
||||
{
|
||||
self.0.insert(String::new(), Arc::new(error));
|
||||
}
|
||||
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
|
||||
pub fn remove<E>(&mut self, key: &str)
|
||||
where
|
||||
E: Error + Send + Sync + 'static,
|
||||
{
|
||||
self.0.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,79 +1,78 @@
|
||||
use leptos_reactive::Scope;
|
||||
|
||||
use crate::{
|
||||
hydration::HydrationKey, ComponentRepr, HydrationCtx, IntoView, View,
|
||||
hydration::HydrationKey, ComponentRepr, HydrationCtx, IntoView, View,
|
||||
};
|
||||
use leptos_reactive::Scope;
|
||||
|
||||
/// Trait for converting any iterable into a [`Fragment`].
|
||||
pub trait IntoFragment {
|
||||
/// Consumes this type, returning [`Fragment`].
|
||||
fn into_fragment(self, cx: Scope) -> Fragment;
|
||||
/// Consumes this type, returning [`Fragment`].
|
||||
fn into_fragment(self, cx: Scope) -> Fragment;
|
||||
}
|
||||
|
||||
impl<I, V> IntoFragment for I
|
||||
where
|
||||
I: IntoIterator<Item = V>,
|
||||
V: IntoView,
|
||||
I: IntoIterator<Item = V>,
|
||||
V: IntoView,
|
||||
{
|
||||
fn into_fragment(self, cx: Scope) -> Fragment {
|
||||
self.into_iter().map(|v| v.into_view(cx)).collect()
|
||||
}
|
||||
fn into_fragment(self, cx: Scope) -> Fragment {
|
||||
self.into_iter().map(|v| v.into_view(cx)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a group of [`views`](View).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Fragment {
|
||||
id: HydrationKey,
|
||||
/// The nodes contained in the fragment.
|
||||
pub nodes: Vec<View>,
|
||||
id: HydrationKey,
|
||||
/// The nodes contained in the fragment.
|
||||
pub nodes: Vec<View>,
|
||||
}
|
||||
|
||||
impl FromIterator<View> for Fragment {
|
||||
fn from_iter<T: IntoIterator<Item = View>>(iter: T) -> Self {
|
||||
Fragment::new(iter.into_iter().collect())
|
||||
}
|
||||
fn from_iter<T: IntoIterator<Item = View>>(iter: T) -> Self {
|
||||
Fragment::new(iter.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<View> for Fragment {
|
||||
fn from(view: View) -> Self {
|
||||
Fragment::new(vec![view])
|
||||
}
|
||||
fn from(view: View) -> Self {
|
||||
Fragment::new(vec![view])
|
||||
}
|
||||
}
|
||||
|
||||
impl Fragment {
|
||||
/// Creates a new [`Fragment`] from a [`Vec<Node>`].
|
||||
pub fn new(nodes: Vec<View>) -> Self {
|
||||
Self::new_with_id(HydrationCtx::id(), nodes)
|
||||
}
|
||||
/// Creates a new [`Fragment`] from a [`Vec<Node>`].
|
||||
pub fn new(nodes: Vec<View>) -> Self {
|
||||
Self::new_with_id(HydrationCtx::id(), nodes)
|
||||
}
|
||||
|
||||
/// Creates a new [`Fragment`] from a function that returns [`Vec<Node>`].
|
||||
pub fn lazy(nodes: impl FnOnce() -> Vec<View>) -> Self {
|
||||
Self::new_with_id(HydrationCtx::id(), nodes())
|
||||
}
|
||||
/// Creates a new [`Fragment`] from a function that returns [`Vec<Node>`].
|
||||
pub fn lazy(nodes: impl FnOnce() -> Vec<View>) -> Self {
|
||||
Self::new_with_id(HydrationCtx::id(), nodes())
|
||||
}
|
||||
|
||||
/// Creates a new [`Fragment`] with the given hydration ID from a [`Vec<Node>`].
|
||||
pub fn new_with_id(id: HydrationKey, nodes: Vec<View>) -> Self {
|
||||
Self { id, nodes }
|
||||
}
|
||||
/// Creates a new [`Fragment`] with the given hydration ID from a [`Vec<Node>`].
|
||||
pub fn new_with_id(id: HydrationKey, nodes: Vec<View>) -> Self {
|
||||
Self { id, nodes }
|
||||
}
|
||||
|
||||
/// Gives access to the [View] children contained within the fragment.
|
||||
pub fn as_children(&self) -> &[View] {
|
||||
&self.nodes
|
||||
}
|
||||
/// Gives access to the [View] children contained within the fragment.
|
||||
pub fn as_children(&self) -> &[View] {
|
||||
&self.nodes
|
||||
}
|
||||
|
||||
/// Returns the fragment's hydration ID.
|
||||
pub fn id(&self) -> &HydrationKey {
|
||||
&self.id
|
||||
}
|
||||
/// Returns the fragment's hydration ID.
|
||||
pub fn id(&self) -> &HydrationKey {
|
||||
&self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoView for Fragment {
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "</>", skip_all, fields(children = self.nodes.len())))]
|
||||
fn into_view(self, cx: leptos_reactive::Scope) -> View {
|
||||
let mut frag = ComponentRepr::new_with_id("", self.id.clone());
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "</>", skip_all, fields(children = self.nodes.len())))]
|
||||
fn into_view(self, cx: leptos_reactive::Scope) -> View {
|
||||
let mut frag = ComponentRepr::new_with_id("", self.id.clone());
|
||||
|
||||
frag.children = self.nodes;
|
||||
frag.children = self.nodes;
|
||||
|
||||
frag.into_view(cx)
|
||||
}
|
||||
frag.into_view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,42 +15,42 @@ use crate::{hydration::HydrationCtx, Comment, CoreComponent, IntoView, View};
|
||||
/// The internal representation of the [`Unit`] core-component.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct UnitRepr {
|
||||
comment: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: HydrationKey,
|
||||
comment: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: HydrationKey,
|
||||
}
|
||||
|
||||
impl fmt::Debug for UnitRepr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("<() />")
|
||||
}
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("<() />")
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UnitRepr {
|
||||
fn default() -> Self {
|
||||
let id = HydrationCtx::id();
|
||||
fn default() -> Self {
|
||||
let id = HydrationCtx::id();
|
||||
|
||||
Self {
|
||||
comment: Comment::new("<() />", &id, true),
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id,
|
||||
Self {
|
||||
comment: Comment::new("<() />", &id, true),
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
impl Mountable for UnitRepr {
|
||||
fn get_mountable_node(&self) -> web_sys::Node {
|
||||
self.comment.node.clone().unchecked_into()
|
||||
}
|
||||
fn get_mountable_node(&self) -> web_sys::Node {
|
||||
self.comment.node.clone().unchecked_into()
|
||||
}
|
||||
|
||||
fn get_opening_node(&self) -> web_sys::Node {
|
||||
self.comment.node.clone().unchecked_into()
|
||||
}
|
||||
fn get_opening_node(&self) -> web_sys::Node {
|
||||
self.comment.node.clone().unchecked_into()
|
||||
}
|
||||
|
||||
fn get_closing_node(&self) -> web_sys::Node {
|
||||
self.comment.node.clone().unchecked_into()
|
||||
}
|
||||
fn get_closing_node(&self) -> web_sys::Node {
|
||||
self.comment.node.clone().unchecked_into()
|
||||
}
|
||||
}
|
||||
|
||||
/// The unit `()` leptos counterpart.
|
||||
@@ -58,13 +58,13 @@ impl Mountable for UnitRepr {
|
||||
pub struct Unit;
|
||||
|
||||
impl IntoView for Unit {
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", name = "<() />", skip_all)
|
||||
)]
|
||||
fn into_view(self, _: leptos_reactive::Scope) -> crate::View {
|
||||
let component = UnitRepr::default();
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
instrument(level = "trace", name = "<() />", skip_all)
|
||||
)]
|
||||
fn into_view(self, _: leptos_reactive::Scope) -> crate::View {
|
||||
let component = UnitRepr::default();
|
||||
|
||||
View::CoreComponent(CoreComponent::Unit(component))
|
||||
}
|
||||
View::CoreComponent(CoreComponent::Unit(component))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ pub mod typed;
|
||||
use std::{borrow::Cow, cell::RefCell, collections::HashSet};
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use wasm_bindgen::{
|
||||
convert::FromWasmAbi, intern, prelude::Closure, JsCast, JsValue,
|
||||
UnwrapThrowExt,
|
||||
convert::FromWasmAbi, intern, prelude::Closure, JsCast, JsValue,
|
||||
UnwrapThrowExt,
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
@@ -14,135 +14,141 @@ thread_local! {
|
||||
/// Adds an event listener to the target DOM element using implicit event delegation.
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub fn add_event_listener<E>(
|
||||
target: &web_sys::Element,
|
||||
event_name: Cow<'static, str>,
|
||||
#[cfg(debug_assertions)] mut cb: impl FnMut(E) + 'static,
|
||||
#[cfg(not(debug_assertions))] cb: impl FnMut(E) + 'static,
|
||||
target: &web_sys::Element,
|
||||
event_name: Cow<'static, str>,
|
||||
#[cfg(debug_assertions)] mut cb: impl FnMut(E) + 'static,
|
||||
#[cfg(not(debug_assertions))] cb: impl FnMut(E) + 'static,
|
||||
) where
|
||||
E: FromWasmAbi + 'static,
|
||||
E: FromWasmAbi + 'static,
|
||||
{
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |e| {
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
};
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |e| {
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
|
||||
let key = event_delegation_key(&event_name);
|
||||
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
|
||||
add_delegated_event_listener(event_name);
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
|
||||
let key = event_delegation_key(&event_name);
|
||||
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
|
||||
add_delegated_event_listener(event_name);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub fn add_event_listener_undelegated<E>(
|
||||
target: &web_sys::Element,
|
||||
event_name: &str,
|
||||
mut cb: impl FnMut(E) + 'static,
|
||||
target: &web_sys::Element,
|
||||
event_name: &str,
|
||||
mut cb: impl FnMut(E) + 'static,
|
||||
) where
|
||||
E: FromWasmAbi + 'static,
|
||||
E: FromWasmAbi + 'static,
|
||||
{
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |e| {
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
};
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |e| {
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let event_name = intern(event_name);
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
|
||||
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
|
||||
let event_name = intern(event_name);
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn FnMut(E)>).into_js_value();
|
||||
_ = target.add_event_listener_with_callback(event_name, cb.unchecked_ref());
|
||||
}
|
||||
|
||||
// cf eventHandler in ryansolid/dom-expressions
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn add_delegated_event_listener(event_name: Cow<'static, str>) {
|
||||
GLOBAL_EVENTS.with(|global_events| {
|
||||
let mut events = global_events.borrow_mut();
|
||||
if !events.contains(&event_name) {
|
||||
// create global handler
|
||||
let key = JsValue::from_str(&event_delegation_key(&event_name));
|
||||
let handler = move |ev: web_sys::Event| {
|
||||
let target = ev.target();
|
||||
let node = ev.composed_path().get(0);
|
||||
let mut node = if node.is_undefined() || node.is_null() {
|
||||
JsValue::from(target)
|
||||
} else {
|
||||
node
|
||||
};
|
||||
GLOBAL_EVENTS.with(|global_events| {
|
||||
let mut events = global_events.borrow_mut();
|
||||
if !events.contains(&event_name) {
|
||||
// create global handler
|
||||
let key = JsValue::from_str(&event_delegation_key(&event_name));
|
||||
let handler = move |ev: web_sys::Event| {
|
||||
let target = ev.target();
|
||||
let node = ev.composed_path().get(0);
|
||||
let mut node = if node.is_undefined() || node.is_null() {
|
||||
JsValue::from(target)
|
||||
} else {
|
||||
node
|
||||
};
|
||||
|
||||
// TODO reverse Shadow DOM retargetting
|
||||
// TODO reverse Shadow DOM retargetting
|
||||
|
||||
// TODO simulate currentTarget
|
||||
// TODO simulate currentTarget
|
||||
|
||||
while !node.is_null() {
|
||||
let node_is_disabled =
|
||||
js_sys::Reflect::get(&node, &JsValue::from_str("disabled"))
|
||||
.unwrap_throw()
|
||||
.is_truthy();
|
||||
if !node_is_disabled {
|
||||
let maybe_handler =
|
||||
js_sys::Reflect::get(&node, &key).unwrap_throw();
|
||||
if !maybe_handler.is_undefined() {
|
||||
let f = maybe_handler.unchecked_ref::<js_sys::Function>();
|
||||
let _ = f.call1(&node, &ev);
|
||||
while !node.is_null() {
|
||||
let node_is_disabled = js_sys::Reflect::get(
|
||||
&node,
|
||||
&JsValue::from_str("disabled"),
|
||||
)
|
||||
.unwrap_throw()
|
||||
.is_truthy();
|
||||
if !node_is_disabled {
|
||||
let maybe_handler =
|
||||
js_sys::Reflect::get(&node, &key).unwrap_throw();
|
||||
if !maybe_handler.is_undefined() {
|
||||
let f = maybe_handler
|
||||
.unchecked_ref::<js_sys::Function>();
|
||||
let _ = f.call1(&node, &ev);
|
||||
|
||||
if ev.cancel_bubble() {
|
||||
return;
|
||||
if ev.cancel_bubble() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// navigate up tree
|
||||
let host =
|
||||
js_sys::Reflect::get(&node, &JsValue::from_str("host"))
|
||||
.unwrap_throw();
|
||||
if host.is_truthy()
|
||||
&& host != node
|
||||
&& host.dyn_ref::<web_sys::Node>().is_some()
|
||||
{
|
||||
node = host;
|
||||
} else if let Some(parent) =
|
||||
node.unchecked_into::<web_sys::Node>().parent_node()
|
||||
{
|
||||
node = parent.into()
|
||||
} else {
|
||||
node = JsValue::null()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let handler = move |e| {
|
||||
let _guard = span.enter();
|
||||
handler(e);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// navigate up tree
|
||||
let host = js_sys::Reflect::get(&node, &JsValue::from_str("host"))
|
||||
.unwrap_throw();
|
||||
if host.is_truthy()
|
||||
&& host != node
|
||||
&& host.dyn_ref::<web_sys::Node>().is_some()
|
||||
{
|
||||
node = host;
|
||||
} else if let Some(parent) =
|
||||
node.unchecked_into::<web_sys::Node>().parent_node()
|
||||
{
|
||||
node = parent.into()
|
||||
} else {
|
||||
node = JsValue::null()
|
||||
}
|
||||
let handler = Box::new(handler) as Box<dyn FnMut(web_sys::Event)>;
|
||||
let handler = Closure::wrap(handler).into_js_value();
|
||||
_ = crate::window().add_event_listener_with_callback(
|
||||
&event_name,
|
||||
handler.unchecked_ref(),
|
||||
);
|
||||
|
||||
// register that we've created handler
|
||||
events.insert(event_name);
|
||||
}
|
||||
};
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let handler = move |e| {
|
||||
let _guard = span.enter();
|
||||
handler(e);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let handler = Box::new(handler) as Box<dyn FnMut(web_sys::Event)>;
|
||||
let handler = Closure::wrap(handler).into_js_value();
|
||||
_ = crate::window()
|
||||
.add_event_listener_with_callback(&event_name, handler.unchecked_ref());
|
||||
|
||||
// register that we've created handler
|
||||
events.insert(event_name);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn event_delegation_key(event_name: &str) -> String {
|
||||
let event_name = intern(event_name);
|
||||
let mut n = String::from("$$$");
|
||||
n.push_str(event_name);
|
||||
n
|
||||
let event_name = intern(event_name);
|
||||
let mut n = String::from("$$$");
|
||||
n.push_str(event_name);
|
||||
n
|
||||
}
|
||||
|
||||
@@ -5,20 +5,20 @@ use wasm_bindgen::convert::FromWasmAbi;
|
||||
|
||||
/// A trait for converting types into [web_sys events](web_sys).
|
||||
pub trait EventDescriptor: Clone {
|
||||
/// The [`web_sys`] event type, such as [`web_sys::MouseEvent`].
|
||||
type EventType: FromWasmAbi;
|
||||
/// The [`web_sys`] event type, such as [`web_sys::MouseEvent`].
|
||||
type EventType: FromWasmAbi;
|
||||
|
||||
/// The name of the event, such as `click` or `mouseover`.
|
||||
fn name(&self) -> Cow<'static, str>;
|
||||
/// The name of the event, such as `click` or `mouseover`.
|
||||
fn name(&self) -> Cow<'static, str>;
|
||||
|
||||
/// Indicates if this event bubbles. For example, `click` bubbles,
|
||||
/// but `focus` does not.
|
||||
///
|
||||
/// If this method returns true, then the event will be delegated globally,
|
||||
/// otherwise, event listeners will be directly attached to the element.
|
||||
fn bubbles(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// Indicates if this event bubbles. For example, `click` bubbles,
|
||||
/// but `focus` does not.
|
||||
///
|
||||
/// If this method returns true, then the event will be delegated globally,
|
||||
/// otherwise, event listeners will be directly attached to the element.
|
||||
fn bubbles(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Overrides the [`EventDescriptor::bubbles`] method to always return
|
||||
@@ -28,54 +28,54 @@ pub trait EventDescriptor: Clone {
|
||||
pub struct undelegated<Ev: EventDescriptor>(pub Ev);
|
||||
|
||||
impl<Ev: EventDescriptor> EventDescriptor for undelegated<Ev> {
|
||||
type EventType = Ev::EventType;
|
||||
type EventType = Ev::EventType;
|
||||
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
self.0.name()
|
||||
}
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
self.0.name()
|
||||
}
|
||||
|
||||
fn bubbles(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn bubbles(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// A custom event.
|
||||
pub struct Custom<E: FromWasmAbi = web_sys::Event> {
|
||||
name: Cow<'static, str>,
|
||||
_event_type: PhantomData<E>,
|
||||
name: Cow<'static, str>,
|
||||
_event_type: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<E: FromWasmAbi> Clone for Custom<E> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
name: self.name.clone(),
|
||||
_event_type: PhantomData,
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
name: self.name.clone(),
|
||||
_event_type: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: FromWasmAbi> EventDescriptor for Custom<E> {
|
||||
type EventType = E;
|
||||
type EventType = E;
|
||||
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
self.name.clone()
|
||||
}
|
||||
fn name(&self) -> Cow<'static, str> {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn bubbles(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn bubbles(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: FromWasmAbi> Custom<E> {
|
||||
/// Creates a custom event type that can be used within
|
||||
/// [`HtmlElement::on`](crate::HtmlElement::on), for events
|
||||
/// which are not covered in the [`ev`](crate::ev) module.
|
||||
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
_event_type: PhantomData,
|
||||
/// Creates a custom event type that can be used within
|
||||
/// [`HtmlElement::on`](crate::HtmlElement::on), for events
|
||||
/// which are not covered in the [`ev`](crate::ev) module.
|
||||
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
_event_type: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! generate_event_types {
|
||||
|
||||
@@ -4,53 +4,53 @@ use wasm_bindgen::{prelude::Closure, JsCast, JsValue, UnwrapThrowExt};
|
||||
|
||||
/// Sets a property on a DOM element.
|
||||
pub fn set_property(
|
||||
el: &web_sys::Element,
|
||||
prop_name: &str,
|
||||
value: &Option<JsValue>,
|
||||
el: &web_sys::Element,
|
||||
prop_name: &str,
|
||||
value: &Option<JsValue>,
|
||||
) {
|
||||
let key = JsValue::from_str(prop_name);
|
||||
match value {
|
||||
Some(value) => _ = js_sys::Reflect::set(el, &key, value),
|
||||
None => _ = js_sys::Reflect::delete_property(el, &key),
|
||||
};
|
||||
let key = JsValue::from_str(prop_name);
|
||||
match value {
|
||||
Some(value) => _ = js_sys::Reflect::set(el, &key, value),
|
||||
None => _ = js_sys::Reflect::delete_property(el, &key),
|
||||
};
|
||||
}
|
||||
|
||||
/// Gets the value of a property set on a DOM element.
|
||||
pub fn get_property(
|
||||
el: &web_sys::Element,
|
||||
prop_name: &str,
|
||||
el: &web_sys::Element,
|
||||
prop_name: &str,
|
||||
) -> Result<JsValue, JsValue> {
|
||||
let key = JsValue::from_str(prop_name);
|
||||
js_sys::Reflect::get(el, &key)
|
||||
let key = JsValue::from_str(prop_name);
|
||||
js_sys::Reflect::get(el, &key)
|
||||
}
|
||||
|
||||
/// Returns the current [`window.location`](https://developer.mozilla.org/en-US/docs/Web/API/Window/location).
|
||||
pub fn location() -> web_sys::Location {
|
||||
window().location()
|
||||
window().location()
|
||||
}
|
||||
|
||||
/// Current [`window.location.hash`](https://developer.mozilla.org/en-US/docs/Web/API/Window/location)
|
||||
/// without the beginning #.
|
||||
pub fn location_hash() -> Option<String> {
|
||||
if is_server() {
|
||||
None
|
||||
} else {
|
||||
location().hash().ok().map(|hash| hash.replace('#', ""))
|
||||
}
|
||||
if is_server() {
|
||||
None
|
||||
} else {
|
||||
location().hash().ok().map(|hash| hash.replace('#', ""))
|
||||
}
|
||||
}
|
||||
|
||||
/// Current [`window.location.pathname`](https://developer.mozilla.org/en-US/docs/Web/API/Window/location).
|
||||
pub fn location_pathname() -> Option<String> {
|
||||
location().pathname().ok()
|
||||
location().pathname().ok()
|
||||
}
|
||||
|
||||
/// Helper function to extract [`Event.target`](https://developer.mozilla.org/en-US/docs/Web/API/Event/target)
|
||||
/// from any event.
|
||||
pub fn event_target<T>(event: &web_sys::Event) -> T
|
||||
where
|
||||
T: JsCast,
|
||||
T: JsCast,
|
||||
{
|
||||
event.target().unwrap_throw().unchecked_into::<T>()
|
||||
event.target().unwrap_throw().unchecked_into::<T>()
|
||||
}
|
||||
|
||||
/// Helper function to extract `event.target.value` from an event.
|
||||
@@ -58,60 +58,60 @@ where
|
||||
/// This is useful in the `on:input` or `on:change` listeners for an `<input>` element.
|
||||
pub fn event_target_value<T>(event: &T) -> String
|
||||
where
|
||||
T: JsCast,
|
||||
T: JsCast,
|
||||
{
|
||||
event
|
||||
.unchecked_ref::<web_sys::Event>()
|
||||
.target()
|
||||
.unwrap_throw()
|
||||
.unchecked_into::<web_sys::HtmlInputElement>()
|
||||
.value()
|
||||
event
|
||||
.unchecked_ref::<web_sys::Event>()
|
||||
.target()
|
||||
.unwrap_throw()
|
||||
.unchecked_into::<web_sys::HtmlInputElement>()
|
||||
.value()
|
||||
}
|
||||
|
||||
/// Helper function to extract `event.target.checked` from an event.
|
||||
///
|
||||
/// This is useful in the `on:change` listeners for an `<input type="checkbox">` element.
|
||||
pub fn event_target_checked(ev: &web_sys::Event) -> bool {
|
||||
ev.target()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::HtmlInputElement>()
|
||||
.checked()
|
||||
ev.target()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::HtmlInputElement>()
|
||||
.checked()
|
||||
}
|
||||
|
||||
/// Runs the given function between the next repaint
|
||||
/// using [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
|
||||
pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::once_into_js(cb);
|
||||
_ = window().request_animation_frame(cb.as_ref().unchecked_ref());
|
||||
let cb = Closure::once_into_js(cb);
|
||||
_ = window().request_animation_frame(cb.as_ref().unchecked_ref());
|
||||
}
|
||||
|
||||
/// Queues the given function during an idle period
|
||||
/// using [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback).
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
|
||||
pub fn request_idle_callback(cb: impl Fn() + 'static) {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
|
||||
_ = window().request_idle_callback(cb.as_ref().unchecked_ref());
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
|
||||
_ = window().request_idle_callback(cb.as_ref().unchecked_ref());
|
||||
}
|
||||
|
||||
/// Executes the given function after the given duration of time has passed.
|
||||
@@ -121,21 +121,21 @@ pub fn request_idle_callback(cb: impl Fn() + 'static) {
|
||||
instrument(level = "trace", skip_all, fields(duration = ?duration))
|
||||
)]
|
||||
pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::once_into_js(Box::new(cb) as Box<dyn FnOnce()>);
|
||||
_ = window().set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
cb.as_ref().unchecked_ref(),
|
||||
duration.as_millis().try_into().unwrap_throw(),
|
||||
);
|
||||
let cb = Closure::once_into_js(Box::new(cb) as Box<dyn FnOnce()>);
|
||||
_ = window().set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
cb.as_ref().unchecked_ref(),
|
||||
duration.as_millis().try_into().unwrap_throw(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Handle that is generated by [set_interval] and can be used to clear the interval.
|
||||
@@ -143,11 +143,11 @@ pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) {
|
||||
pub struct IntervalHandle(i32);
|
||||
|
||||
impl IntervalHandle {
|
||||
/// Cancels the repeating event to which this refers.
|
||||
/// See [`clearInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval)
|
||||
pub fn clear(&self) {
|
||||
window().clear_interval_with_handle(self.0);
|
||||
}
|
||||
/// Cancels the repeating event to which this refers.
|
||||
/// See [`clearInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval)
|
||||
pub fn clear(&self) {
|
||||
window().clear_interval_with_handle(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Repeatedly calls the given function, with a delay of the given duration between calls.
|
||||
@@ -157,26 +157,26 @@ impl IntervalHandle {
|
||||
instrument(level = "trace", skip_all, fields(duration = ?duration))
|
||||
)]
|
||||
pub fn set_interval(
|
||||
cb: impl Fn() + 'static,
|
||||
duration: Duration,
|
||||
cb: impl Fn() + 'static,
|
||||
duration: Duration,
|
||||
) -> Result<IntervalHandle, JsValue> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move || {
|
||||
let _guard = span.enter();
|
||||
cb();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
|
||||
let handle = window()
|
||||
.set_interval_with_callback_and_timeout_and_arguments_0(
|
||||
cb.as_ref().unchecked_ref(),
|
||||
duration.as_millis().try_into().unwrap_throw(),
|
||||
)?;
|
||||
Ok(IntervalHandle(handle))
|
||||
let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
|
||||
let handle = window()
|
||||
.set_interval_with_callback_and_timeout_and_arguments_0(
|
||||
cb.as_ref().unchecked_ref(),
|
||||
duration.as_millis().try_into().unwrap_throw(),
|
||||
)?;
|
||||
Ok(IntervalHandle(handle))
|
||||
}
|
||||
|
||||
/// Adds an event listener to the `Window`.
|
||||
@@ -185,34 +185,34 @@ pub fn set_interval(
|
||||
instrument(level = "trace", skip_all, fields(event_name = %event_name))
|
||||
)]
|
||||
pub fn window_event_listener(
|
||||
event_name: &str,
|
||||
cb: impl Fn(web_sys::Event) + 'static,
|
||||
event_name: &str,
|
||||
cb: impl Fn(web_sys::Event) + 'static,
|
||||
) {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |e| {
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
};
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = move |e| {
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !is_server() {
|
||||
let handler = Box::new(cb) as Box<dyn FnMut(web_sys::Event)>;
|
||||
if !is_server() {
|
||||
let handler = Box::new(cb) as Box<dyn FnMut(web_sys::Event)>;
|
||||
|
||||
let cb = Closure::wrap(handler).into_js_value();
|
||||
_ =
|
||||
window().add_event_listener_with_callback(event_name, cb.unchecked_ref());
|
||||
}
|
||||
let cb = Closure::wrap(handler).into_js_value();
|
||||
_ = window()
|
||||
.add_event_listener_with_callback(event_name, cb.unchecked_ref());
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// This exists only to enable type inference on event listeners when in SSR mode.
|
||||
pub fn ssr_event_listener<E: crate::ev::EventDescriptor + 'static>(
|
||||
event: E,
|
||||
event_handler: impl FnMut(E::EventType) + 'static,
|
||||
event: E,
|
||||
event_handler: impl FnMut(E::EventType) + 'static,
|
||||
) {
|
||||
_ = event;
|
||||
_ = event_handler;
|
||||
_ = event;
|
||||
_ = event_handler;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -178,69 +178,69 @@ macro_rules! generate_math_tags {
|
||||
}
|
||||
|
||||
generate_math_tags![
|
||||
/// MathML element.
|
||||
math,
|
||||
/// MathML element.
|
||||
mi,
|
||||
/// MathML element.
|
||||
mn,
|
||||
/// MathML element.
|
||||
mo,
|
||||
/// MathML element.
|
||||
ms,
|
||||
/// MathML element.
|
||||
mspace,
|
||||
/// MathML element.
|
||||
mtext,
|
||||
/// MathML element.
|
||||
menclose,
|
||||
/// MathML element.
|
||||
merror,
|
||||
/// MathML element.
|
||||
mfenced,
|
||||
/// MathML element.
|
||||
mfrac,
|
||||
/// MathML element.
|
||||
mpadded,
|
||||
/// MathML element.
|
||||
mphantom,
|
||||
/// MathML element.
|
||||
mroot,
|
||||
/// MathML element.
|
||||
mrow,
|
||||
/// MathML element.
|
||||
msqrt,
|
||||
/// MathML element.
|
||||
mstyle,
|
||||
/// MathML element.
|
||||
mmultiscripts,
|
||||
/// MathML element.
|
||||
mover,
|
||||
/// MathML element.
|
||||
mprescripts,
|
||||
/// MathML element.
|
||||
msub,
|
||||
/// MathML element.
|
||||
msubsup,
|
||||
/// MathML element.
|
||||
msup,
|
||||
/// MathML element.
|
||||
munder,
|
||||
/// MathML element.
|
||||
munderover,
|
||||
/// MathML element.
|
||||
mtable,
|
||||
/// MathML element.
|
||||
mtd,
|
||||
/// MathML element.
|
||||
mtr,
|
||||
/// MathML element.
|
||||
maction,
|
||||
/// MathML element.
|
||||
annotation,
|
||||
/// MathML element.
|
||||
annotation
|
||||
- xml,
|
||||
/// MathML element.
|
||||
semantics,
|
||||
/// MathML element.
|
||||
math,
|
||||
/// MathML element.
|
||||
mi,
|
||||
/// MathML element.
|
||||
mn,
|
||||
/// MathML element.
|
||||
mo,
|
||||
/// MathML element.
|
||||
ms,
|
||||
/// MathML element.
|
||||
mspace,
|
||||
/// MathML element.
|
||||
mtext,
|
||||
/// MathML element.
|
||||
menclose,
|
||||
/// MathML element.
|
||||
merror,
|
||||
/// MathML element.
|
||||
mfenced,
|
||||
/// MathML element.
|
||||
mfrac,
|
||||
/// MathML element.
|
||||
mpadded,
|
||||
/// MathML element.
|
||||
mphantom,
|
||||
/// MathML element.
|
||||
mroot,
|
||||
/// MathML element.
|
||||
mrow,
|
||||
/// MathML element.
|
||||
msqrt,
|
||||
/// MathML element.
|
||||
mstyle,
|
||||
/// MathML element.
|
||||
mmultiscripts,
|
||||
/// MathML element.
|
||||
mover,
|
||||
/// MathML element.
|
||||
mprescripts,
|
||||
/// MathML element.
|
||||
msub,
|
||||
/// MathML element.
|
||||
msubsup,
|
||||
/// MathML element.
|
||||
msup,
|
||||
/// MathML element.
|
||||
munder,
|
||||
/// MathML element.
|
||||
munderover,
|
||||
/// MathML element.
|
||||
mtable,
|
||||
/// MathML element.
|
||||
mtd,
|
||||
/// MathML element.
|
||||
mtr,
|
||||
/// MathML element.
|
||||
maction,
|
||||
/// MathML element.
|
||||
annotation,
|
||||
/// MathML element.
|
||||
annotation
|
||||
- xml,
|
||||
/// MathML element.
|
||||
semantics,
|
||||
];
|
||||
|
||||
@@ -51,25 +51,25 @@ cfg_if! {
|
||||
/// A stable identifer within the server-rendering or hydration process.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct HydrationKey {
|
||||
/// The key of the previous component.
|
||||
pub previous: String,
|
||||
/// The element offset within the current component.
|
||||
pub offset: usize,
|
||||
/// The key of the previous component.
|
||||
pub previous: String,
|
||||
/// The element offset within the current component.
|
||||
pub offset: usize,
|
||||
}
|
||||
|
||||
impl Display for HydrationKey {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}{}", self.previous, self.offset)
|
||||
}
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}{}", self.previous, self.offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HydrationKey {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
previous: "0-".to_string(),
|
||||
offset: 0,
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
previous: "0-".to_string(),
|
||||
offset: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thread_local!(static ID: RefCell<HydrationKey> = Default::default());
|
||||
@@ -78,65 +78,65 @@ thread_local!(static ID: RefCell<HydrationKey> = Default::default());
|
||||
pub struct HydrationCtx;
|
||||
|
||||
impl HydrationCtx {
|
||||
/// Get the next `id` without incrementing it.
|
||||
pub fn peek() -> HydrationKey {
|
||||
ID.with(|id| id.borrow().clone())
|
||||
}
|
||||
|
||||
/// Increments the current hydration `id` and returns it
|
||||
pub fn id() -> HydrationKey {
|
||||
ID.with(|id| {
|
||||
let mut id = id.borrow_mut();
|
||||
id.offset = id.offset.wrapping_add(1);
|
||||
id.clone()
|
||||
})
|
||||
}
|
||||
|
||||
/// Resets the hydration `id` for the next component, and returns it
|
||||
pub fn next_component() -> HydrationKey {
|
||||
ID.with(|id| {
|
||||
let mut id = id.borrow_mut();
|
||||
let offset = id.offset;
|
||||
id.previous.push_str(&offset.to_string());
|
||||
id.previous.push('-');
|
||||
id.offset = 0;
|
||||
id.clone()
|
||||
})
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub fn reset_id() {
|
||||
ID.with(|id| *id.borrow_mut() = Default::default());
|
||||
}
|
||||
|
||||
/// Resums hydration from the provided `id`. Usefull for
|
||||
/// `Suspense` and other fancy things.
|
||||
pub fn continue_from(id: HydrationKey) {
|
||||
ID.with(|i| *i.borrow_mut() = id);
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn stop_hydrating() {
|
||||
IS_HYDRATING.with(|is_hydrating| {
|
||||
std::mem::take(&mut *is_hydrating.borrow_mut());
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn is_hydrating() -> bool {
|
||||
IS_HYDRATING.with(|is_hydrating| **is_hydrating.borrow())
|
||||
}
|
||||
|
||||
pub(crate) fn to_string(id: &HydrationKey, closing: bool) -> String {
|
||||
#[cfg(debug_assertions)]
|
||||
return format!("_{id}{}", if closing { 'c' } else { 'o' });
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
let _ = closing;
|
||||
|
||||
format!("_{id}")
|
||||
/// Get the next `id` without incrementing it.
|
||||
pub fn peek() -> HydrationKey {
|
||||
ID.with(|id| id.borrow().clone())
|
||||
}
|
||||
|
||||
/// Increments the current hydration `id` and returns it
|
||||
pub fn id() -> HydrationKey {
|
||||
ID.with(|id| {
|
||||
let mut id = id.borrow_mut();
|
||||
id.offset = id.offset.wrapping_add(1);
|
||||
id.clone()
|
||||
})
|
||||
}
|
||||
|
||||
/// Resets the hydration `id` for the next component, and returns it
|
||||
pub fn next_component() -> HydrationKey {
|
||||
ID.with(|id| {
|
||||
let mut id = id.borrow_mut();
|
||||
let offset = id.offset;
|
||||
id.previous.push_str(&offset.to_string());
|
||||
id.previous.push('-');
|
||||
id.offset = 0;
|
||||
id.clone()
|
||||
})
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub fn reset_id() {
|
||||
ID.with(|id| *id.borrow_mut() = Default::default());
|
||||
}
|
||||
|
||||
/// Resums hydration from the provided `id`. Usefull for
|
||||
/// `Suspense` and other fancy things.
|
||||
pub fn continue_from(id: HydrationKey) {
|
||||
ID.with(|i| *i.borrow_mut() = id);
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn stop_hydrating() {
|
||||
IS_HYDRATING.with(|is_hydrating| {
|
||||
std::mem::take(&mut *is_hydrating.borrow_mut());
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn is_hydrating() -> bool {
|
||||
IS_HYDRATING.with(|is_hydrating| **is_hydrating.borrow())
|
||||
}
|
||||
|
||||
pub(crate) fn to_string(id: &HydrationKey, closing: bool) -> String {
|
||||
#[cfg(debug_assertions)]
|
||||
return format!("_{id}{}", if closing { 'c' } else { 'o' });
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
let _ = closing;
|
||||
|
||||
format!("_{id}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -44,45 +44,45 @@ macro_rules! debug_warn {
|
||||
/// Log a string to the console (in the browser)
|
||||
/// or via `println!()` (if not in the browser).
|
||||
pub fn console_log(s: &str) {
|
||||
if is_server() {
|
||||
println!("{s}");
|
||||
} else {
|
||||
web_sys::console::log_1(&JsValue::from_str(s));
|
||||
}
|
||||
if is_server() {
|
||||
println!("{s}");
|
||||
} else {
|
||||
web_sys::console::log_1(&JsValue::from_str(s));
|
||||
}
|
||||
}
|
||||
|
||||
/// Log a warning to the console (in the browser)
|
||||
/// or via `println!()` (if not in the browser).
|
||||
pub fn console_warn(s: &str) {
|
||||
if is_server() {
|
||||
eprintln!("{s}");
|
||||
} else {
|
||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||
}
|
||||
if is_server() {
|
||||
eprintln!("{s}");
|
||||
} else {
|
||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||
}
|
||||
}
|
||||
|
||||
/// Log an error to the console (in the browser)
|
||||
/// or via `println!()` (if not in the browser).
|
||||
pub fn console_error(s: &str) {
|
||||
if is_server() {
|
||||
eprintln!("{s}");
|
||||
} else {
|
||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||
}
|
||||
if is_server() {
|
||||
eprintln!("{s}");
|
||||
} else {
|
||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||
}
|
||||
}
|
||||
|
||||
/// Log an error to the console (in the browser)
|
||||
/// or via `println!()` (if not in the browser), but only in a debug build.
|
||||
pub fn console_debug_warn(s: &str) {
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
if is_server() {
|
||||
eprintln!("{s}");
|
||||
} else {
|
||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||
}
|
||||
} else {
|
||||
let _ = s;
|
||||
}
|
||||
}
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
if is_server() {
|
||||
eprintln!("{s}");
|
||||
} else {
|
||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||
}
|
||||
} else {
|
||||
let _ = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use leptos_reactive::Scope;
|
||||
use std::rc::Rc;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
|
||||
@@ -10,225 +9,227 @@ use wasm_bindgen::UnwrapThrowExt;
|
||||
/// macro’s use. You usually won't need to interact with it directly.
|
||||
#[derive(Clone)]
|
||||
pub enum Attribute {
|
||||
/// A plain string value.
|
||||
String(String),
|
||||
/// A (presumably reactive) function, which will be run inside an effect to do targeted updates to the attribute.
|
||||
Fn(Scope, Rc<dyn Fn() -> Attribute>),
|
||||
/// An optional string value, which sets the attribute to the value if `Some` and removes the attribute if `None`.
|
||||
Option(Scope, Option<String>),
|
||||
/// A boolean attribute, which sets the attribute if `true` and removes the attribute if `false`.
|
||||
Bool(bool),
|
||||
/// A plain string value.
|
||||
String(String),
|
||||
/// A (presumably reactive) function, which will be run inside an effect to do targeted updates to the attribute.
|
||||
Fn(Scope, Rc<dyn Fn() -> Attribute>),
|
||||
/// An optional string value, which sets the attribute to the value if `Some` and removes the attribute if `None`.
|
||||
Option(Scope, Option<String>),
|
||||
/// A boolean attribute, which sets the attribute if `true` and removes the attribute if `false`.
|
||||
Bool(bool),
|
||||
}
|
||||
|
||||
impl Attribute {
|
||||
/// Converts the attribute to its HTML value at that moment, including the attribute name,
|
||||
/// so it can be rendered on the server.
|
||||
pub fn as_value_string(&self, attr_name: &'static str) -> String {
|
||||
match self {
|
||||
Attribute::String(value) => format!("{attr_name}=\"{value}\""),
|
||||
Attribute::Fn(_, f) => {
|
||||
let mut value = f();
|
||||
while let Attribute::Fn(_, f) = value {
|
||||
value = f();
|
||||
/// Converts the attribute to its HTML value at that moment, including the attribute name,
|
||||
/// so it can be rendered on the server.
|
||||
pub fn as_value_string(&self, attr_name: &'static str) -> String {
|
||||
match self {
|
||||
Attribute::String(value) => format!("{attr_name}=\"{value}\""),
|
||||
Attribute::Fn(_, f) => {
|
||||
let mut value = f();
|
||||
while let Attribute::Fn(_, f) = value {
|
||||
value = f();
|
||||
}
|
||||
value.as_value_string(attr_name)
|
||||
}
|
||||
Attribute::Option(_, value) => value
|
||||
.as_ref()
|
||||
.map(|value| format!("{attr_name}=\"{value}\""))
|
||||
.unwrap_or_default(),
|
||||
Attribute::Bool(include) => {
|
||||
if *include {
|
||||
attr_name.to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
value.as_value_string(attr_name)
|
||||
}
|
||||
Attribute::Option(_, value) => value
|
||||
.as_ref()
|
||||
.map(|value| format!("{attr_name}=\"{value}\""))
|
||||
.unwrap_or_default(),
|
||||
Attribute::Bool(include) => {
|
||||
if *include {
|
||||
attr_name.to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the attribute to its HTML value at that moment, not including
|
||||
/// the attribute name, so it can be rendered on the server.
|
||||
pub fn as_nameless_value_string(&self) -> Option<String> {
|
||||
match self {
|
||||
Attribute::String(value) => Some(value.to_string()),
|
||||
Attribute::Fn(_, f) => {
|
||||
let mut value = f();
|
||||
while let Attribute::Fn(_, f) = value {
|
||||
value = f();
|
||||
/// Converts the attribute to its HTML value at that moment, not including
|
||||
/// the attribute name, so it can be rendered on the server.
|
||||
pub fn as_nameless_value_string(&self) -> Option<String> {
|
||||
match self {
|
||||
Attribute::String(value) => Some(value.to_string()),
|
||||
Attribute::Fn(_, f) => {
|
||||
let mut value = f();
|
||||
while let Attribute::Fn(_, f) = value {
|
||||
value = f();
|
||||
}
|
||||
value.as_nameless_value_string()
|
||||
}
|
||||
Attribute::Option(_, value) => {
|
||||
value.as_ref().map(|value| value.to_string())
|
||||
}
|
||||
Attribute::Bool(include) => {
|
||||
if *include {
|
||||
Some("".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
value.as_nameless_value_string()
|
||||
}
|
||||
Attribute::Option(_, value) => {
|
||||
value.as_ref().map(|value| value.to_string())
|
||||
}
|
||||
Attribute::Bool(include) => {
|
||||
if *include {
|
||||
Some("".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Attribute {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::String(l0), Self::String(r0)) => l0 == r0,
|
||||
(Self::Fn(_, _), Self::Fn(_, _)) => false,
|
||||
(Self::Option(_, l0), Self::Option(_, r0)) => l0 == r0,
|
||||
(Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
|
||||
_ => false,
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::String(l0), Self::String(r0)) => l0 == r0,
|
||||
(Self::Fn(_, _), Self::Fn(_, _)) => false,
|
||||
(Self::Option(_, l0), Self::Option(_, r0)) => l0 == r0,
|
||||
(Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Attribute {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::String(arg0) => f.debug_tuple("String").field(arg0).finish(),
|
||||
Self::Fn(_, _) => f.debug_tuple("Fn").finish(),
|
||||
Self::Option(_, arg0) => f.debug_tuple("Option").field(arg0).finish(),
|
||||
Self::Bool(arg0) => f.debug_tuple("Bool").field(arg0).finish(),
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::String(arg0) => f.debug_tuple("String").field(arg0).finish(),
|
||||
Self::Fn(_, _) => f.debug_tuple("Fn").finish(),
|
||||
Self::Option(_, arg0) => {
|
||||
f.debug_tuple("Option").field(arg0).finish()
|
||||
}
|
||||
Self::Bool(arg0) => f.debug_tuple("Bool").field(arg0).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts some type into an [Attribute].
|
||||
///
|
||||
/// This is implemented by default for Rust primitive and string types.
|
||||
pub trait IntoAttribute {
|
||||
/// Converts the object into an [Attribute].
|
||||
fn into_attribute(self, cx: Scope) -> Attribute;
|
||||
/// Helper function for dealing with [Box<dyn IntoAttribute>]
|
||||
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute;
|
||||
/// Converts the object into an [Attribute].
|
||||
fn into_attribute(self, cx: Scope) -> Attribute;
|
||||
/// Helper function for dealing with [Box<dyn IntoAttribute>]
|
||||
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute;
|
||||
}
|
||||
|
||||
impl<T: IntoAttribute + 'static> From<T> for Box<dyn IntoAttribute> {
|
||||
fn from(value: T) -> Self {
|
||||
Box::new(value)
|
||||
}
|
||||
fn from(value: T) -> Self {
|
||||
Box::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Attribute {
|
||||
#[inline]
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
self
|
||||
}
|
||||
#[inline]
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn into_attribute_boxed(self: Box<Self>, _: Scope) -> Attribute {
|
||||
*self
|
||||
}
|
||||
#[inline]
|
||||
fn into_attribute_boxed(self: Box<Self>, _: Scope) -> Attribute {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_into_attr_boxed {
|
||||
() => {
|
||||
#[inline]
|
||||
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
|
||||
self.into_attribute(cx)
|
||||
}
|
||||
};
|
||||
() => {
|
||||
#[inline]
|
||||
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
|
||||
self.into_attribute(cx)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<Attribute> {
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
self.unwrap_or(Attribute::Option(cx, None))
|
||||
}
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
self.unwrap_or(Attribute::Option(cx, None))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for String {
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
Attribute::String(self)
|
||||
}
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
Attribute::String(self)
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for bool {
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
Attribute::Bool(self)
|
||||
}
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
Attribute::Bool(self)
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<String> {
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
Attribute::Option(cx, self)
|
||||
}
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
Attribute::Option(cx, self)
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl<T, U> IntoAttribute for T
|
||||
where
|
||||
T: Fn() -> U + 'static,
|
||||
U: IntoAttribute,
|
||||
T: Fn() -> U + 'static,
|
||||
U: IntoAttribute,
|
||||
{
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
let modified_fn = Rc::new(move || (self)().into_attribute(cx));
|
||||
Attribute::Fn(cx, modified_fn)
|
||||
}
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
let modified_fn = Rc::new(move || (self)().into_attribute(cx));
|
||||
Attribute::Fn(cx, modified_fn)
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl<T: IntoAttribute> IntoAttribute for (Scope, T) {
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
self.1.into_attribute(self.0)
|
||||
}
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
self.1.into_attribute(self.0)
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for (Scope, Option<Box<dyn IntoAttribute>>) {
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
match self.1 {
|
||||
Some(bx) => bx.into_attribute_boxed(self.0),
|
||||
None => Attribute::Option(self.0, None),
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
match self.1 {
|
||||
Some(bx) => bx.into_attribute_boxed(self.0),
|
||||
None => Attribute::Option(self.0, None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for (Scope, Box<dyn IntoAttribute>) {
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
self.1.into_attribute_boxed(self.0)
|
||||
}
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
self.1.into_attribute_boxed(self.0)
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
macro_rules! attr_type {
|
||||
($attr_type:ty) => {
|
||||
impl IntoAttribute for $attr_type {
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
Attribute::String(self.to_string())
|
||||
}
|
||||
($attr_type:ty) => {
|
||||
impl IntoAttribute for $attr_type {
|
||||
fn into_attribute(self, _: Scope) -> Attribute {
|
||||
Attribute::String(self.to_string())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
|
||||
self.into_attribute(cx)
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
|
||||
self.into_attribute(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<$attr_type> {
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
Attribute::Option(cx, self.map(|n| n.to_string()))
|
||||
}
|
||||
impl IntoAttribute for Option<$attr_type> {
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
Attribute::Option(cx, self.map(|n| n.to_string()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
|
||||
self.into_attribute(cx)
|
||||
}
|
||||
}
|
||||
};
|
||||
#[inline]
|
||||
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
|
||||
self.into_attribute(cx)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
attr_type!(&String);
|
||||
@@ -253,64 +254,64 @@ attr_type!(char);
|
||||
use std::borrow::Cow;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn attribute_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
value: Attribute,
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
value: Attribute,
|
||||
) {
|
||||
use leptos_reactive::create_render_effect;
|
||||
match value {
|
||||
Attribute::Fn(cx, f) => {
|
||||
let el = el.clone();
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
if old.as_ref() != Some(&new) {
|
||||
attribute_expression(&el, &name, new.clone());
|
||||
use leptos_reactive::create_render_effect;
|
||||
match value {
|
||||
Attribute::Fn(cx, f) => {
|
||||
let el = el.clone();
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
if old.as_ref() != Some(&new) {
|
||||
attribute_expression(&el, &name, new.clone());
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
_ => attribute_expression(el, &name, value),
|
||||
};
|
||||
_ => attribute_expression(el, &name, value),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn attribute_expression(
|
||||
el: &web_sys::Element,
|
||||
attr_name: &str,
|
||||
value: Attribute,
|
||||
el: &web_sys::Element,
|
||||
attr_name: &str,
|
||||
value: Attribute,
|
||||
) {
|
||||
match value {
|
||||
Attribute::String(value) => {
|
||||
let value = wasm_bindgen::intern(&value);
|
||||
if attr_name == "inner_html" {
|
||||
el.set_inner_html(value);
|
||||
} else {
|
||||
let attr_name = wasm_bindgen::intern(attr_name);
|
||||
el.set_attribute(attr_name, value).unwrap_throw();
|
||||
}
|
||||
}
|
||||
Attribute::Option(_, value) => {
|
||||
if attr_name == "inner_html" {
|
||||
el.set_inner_html(&value.unwrap_or_default());
|
||||
} else {
|
||||
let attr_name = wasm_bindgen::intern(attr_name);
|
||||
match value {
|
||||
Some(value) => {
|
||||
match value {
|
||||
Attribute::String(value) => {
|
||||
let value = wasm_bindgen::intern(&value);
|
||||
el.set_attribute(attr_name, value).unwrap_throw();
|
||||
}
|
||||
None => el.remove_attribute(attr_name).unwrap_throw(),
|
||||
if attr_name == "inner_html" {
|
||||
el.set_inner_html(value);
|
||||
} else {
|
||||
let attr_name = wasm_bindgen::intern(attr_name);
|
||||
el.set_attribute(attr_name, value).unwrap_throw();
|
||||
}
|
||||
}
|
||||
}
|
||||
Attribute::Option(_, value) => {
|
||||
if attr_name == "inner_html" {
|
||||
el.set_inner_html(&value.unwrap_or_default());
|
||||
} else {
|
||||
let attr_name = wasm_bindgen::intern(attr_name);
|
||||
match value {
|
||||
Some(value) => {
|
||||
let value = wasm_bindgen::intern(&value);
|
||||
el.set_attribute(attr_name, value).unwrap_throw();
|
||||
}
|
||||
None => el.remove_attribute(attr_name).unwrap_throw(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Attribute::Bool(value) => {
|
||||
let attr_name = wasm_bindgen::intern(attr_name);
|
||||
if value {
|
||||
el.set_attribute(attr_name, attr_name).unwrap_throw();
|
||||
} else {
|
||||
el.remove_attribute(attr_name).unwrap_throw();
|
||||
}
|
||||
}
|
||||
_ => panic!("Remove nested Fn in Attribute"),
|
||||
}
|
||||
Attribute::Bool(value) => {
|
||||
let attr_name = wasm_bindgen::intern(attr_name);
|
||||
if value {
|
||||
el.set_attribute(attr_name, attr_name).unwrap_throw();
|
||||
} else {
|
||||
el.remove_attribute(attr_name).unwrap_throw();
|
||||
}
|
||||
}
|
||||
_ => panic!("Remove nested Fn in Attribute"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,61 +9,61 @@ use wasm_bindgen::UnwrapThrowExt;
|
||||
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
|
||||
/// macro’s use. You usually won't need to interact with it directly.
|
||||
pub enum Class {
|
||||
/// Whether the class is present.
|
||||
Value(bool),
|
||||
/// A (presumably reactive) function, which will be run inside an effect to toggle the class.
|
||||
Fn(Scope, Box<dyn Fn() -> bool>),
|
||||
/// Whether the class is present.
|
||||
Value(bool),
|
||||
/// A (presumably reactive) function, which will be run inside an effect to toggle the class.
|
||||
Fn(Scope, Box<dyn Fn() -> bool>),
|
||||
}
|
||||
|
||||
/// Converts some type into a [Class].
|
||||
pub trait IntoClass {
|
||||
/// Converts the object into a [Class].
|
||||
fn into_class(self, cx: Scope) -> Class;
|
||||
/// Converts the object into a [Class].
|
||||
fn into_class(self, cx: Scope) -> Class;
|
||||
}
|
||||
|
||||
impl IntoClass for bool {
|
||||
fn into_class(self, _cx: Scope) -> Class {
|
||||
Class::Value(self)
|
||||
}
|
||||
fn into_class(self, _cx: Scope) -> Class {
|
||||
Class::Value(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoClass for T
|
||||
where
|
||||
T: Fn() -> bool + 'static,
|
||||
T: Fn() -> bool + 'static,
|
||||
{
|
||||
fn into_class(self, cx: Scope) -> Class {
|
||||
let modified_fn = Box::new(self);
|
||||
Class::Fn(cx, modified_fn)
|
||||
}
|
||||
fn into_class(self, cx: Scope) -> Class {
|
||||
let modified_fn = Box::new(self);
|
||||
Class::Fn(cx, modified_fn)
|
||||
}
|
||||
}
|
||||
|
||||
impl Class {
|
||||
/// Converts the class to its HTML value at that moment so it can be rendered on the server.
|
||||
pub fn as_value_string(&self, class_name: &'static str) -> &'static str {
|
||||
match self {
|
||||
Class::Value(value) => {
|
||||
if *value {
|
||||
class_name
|
||||
} else {
|
||||
""
|
||||
/// Converts the class to its HTML value at that moment so it can be rendered on the server.
|
||||
pub fn as_value_string(&self, class_name: &'static str) -> &'static str {
|
||||
match self {
|
||||
Class::Value(value) => {
|
||||
if *value {
|
||||
class_name
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
Class::Fn(_, f) => {
|
||||
let value = f();
|
||||
if value {
|
||||
class_name
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Class::Fn(_, f) => {
|
||||
let value = f();
|
||||
if value {
|
||||
class_name
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoClass> IntoClass for (Scope, T) {
|
||||
fn into_class(self, _: Scope) -> Class {
|
||||
self.1.into_class(self.0)
|
||||
}
|
||||
fn into_class(self, _: Scope) -> Class {
|
||||
self.1.into_class(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
@@ -71,37 +71,37 @@ use std::borrow::Cow;
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn class_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
value: Class,
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
value: Class,
|
||||
) {
|
||||
use leptos_reactive::create_render_effect;
|
||||
use leptos_reactive::create_render_effect;
|
||||
|
||||
let class_list = el.class_list();
|
||||
match value {
|
||||
Class::Fn(cx, f) => {
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
if old.as_ref() != Some(&new) && (old.is_some() || new) {
|
||||
class_expression(&class_list, &name, new)
|
||||
let class_list = el.class_list();
|
||||
match value {
|
||||
Class::Fn(cx, f) => {
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
if old.as_ref() != Some(&new) && (old.is_some() || new) {
|
||||
class_expression(&class_list, &name, new)
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
Class::Value(value) => class_expression(&class_list, &name, value),
|
||||
};
|
||||
Class::Value(value) => class_expression(&class_list, &name, value),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn class_expression(
|
||||
class_list: &web_sys::DomTokenList,
|
||||
class_name: &str,
|
||||
value: bool,
|
||||
class_list: &web_sys::DomTokenList,
|
||||
class_name: &str,
|
||||
value: bool,
|
||||
) {
|
||||
let class_name = wasm_bindgen::intern(class_name);
|
||||
if value {
|
||||
class_list.add_1(class_name).unwrap_throw();
|
||||
} else {
|
||||
class_list.remove_1(class_name).unwrap_throw();
|
||||
}
|
||||
let class_name = wasm_bindgen::intern(class_name);
|
||||
if value {
|
||||
class_list.add_1(class_name).unwrap_throw();
|
||||
} else {
|
||||
class_list.remove_1(class_name).unwrap_throw();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,51 +9,51 @@ use wasm_bindgen::UnwrapThrowExt;
|
||||
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
|
||||
/// macro’s use. You usually won't need to interact with it directly.
|
||||
pub enum Property {
|
||||
/// A static JavaScript value.
|
||||
Value(JsValue),
|
||||
/// A (presumably reactive) function, which will be run inside an effect to toggle the class.
|
||||
Fn(Scope, Box<dyn Fn() -> JsValue>),
|
||||
/// A static JavaScript value.
|
||||
Value(JsValue),
|
||||
/// A (presumably reactive) function, which will be run inside an effect to toggle the class.
|
||||
Fn(Scope, Box<dyn Fn() -> JsValue>),
|
||||
}
|
||||
|
||||
/// Converts some type into a [Property].
|
||||
///
|
||||
/// This is implemented by default for Rust primitive types, [String] and friends, and [JsValue].
|
||||
pub trait IntoProperty {
|
||||
/// Converts the object into a [Property].
|
||||
fn into_property(self, cx: Scope) -> Property;
|
||||
/// Converts the object into a [Property].
|
||||
fn into_property(self, cx: Scope) -> Property;
|
||||
}
|
||||
|
||||
impl<T, U> IntoProperty for T
|
||||
where
|
||||
T: Fn() -> U + 'static,
|
||||
U: Into<JsValue>,
|
||||
T: Fn() -> U + 'static,
|
||||
U: Into<JsValue>,
|
||||
{
|
||||
fn into_property(self, cx: Scope) -> Property {
|
||||
let modified_fn = Box::new(move || self().into());
|
||||
Property::Fn(cx, modified_fn)
|
||||
}
|
||||
fn into_property(self, cx: Scope) -> Property {
|
||||
let modified_fn = Box::new(move || self().into());
|
||||
Property::Fn(cx, modified_fn)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoProperty> IntoProperty for (Scope, T) {
|
||||
fn into_property(self, _: Scope) -> Property {
|
||||
self.1.into_property(self.0)
|
||||
}
|
||||
fn into_property(self, _: Scope) -> Property {
|
||||
self.1.into_property(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! prop_type {
|
||||
($prop_type:ty) => {
|
||||
impl IntoProperty for $prop_type {
|
||||
fn into_property(self, _cx: Scope) -> Property {
|
||||
Property::Value(self.into())
|
||||
}
|
||||
}
|
||||
($prop_type:ty) => {
|
||||
impl IntoProperty for $prop_type {
|
||||
fn into_property(self, _cx: Scope) -> Property {
|
||||
Property::Value(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoProperty for Option<$prop_type> {
|
||||
fn into_property(self, _cx: Scope) -> Property {
|
||||
Property::Value(self.into())
|
||||
}
|
||||
}
|
||||
};
|
||||
impl IntoProperty for Option<$prop_type> {
|
||||
fn into_property(self, _cx: Scope) -> Property {
|
||||
Property::Value(self.into())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
prop_type!(JsValue);
|
||||
@@ -81,39 +81,40 @@ use std::borrow::Cow;
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn property_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
value: Property,
|
||||
el: &web_sys::Element,
|
||||
name: Cow<'static, str>,
|
||||
value: Property,
|
||||
) {
|
||||
use leptos_reactive::create_render_effect;
|
||||
use leptos_reactive::create_render_effect;
|
||||
|
||||
match value {
|
||||
Property::Fn(cx, f) => {
|
||||
let el = el.clone();
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
let prop_name = wasm_bindgen::intern(&name);
|
||||
if old.as_ref() != Some(&new)
|
||||
&& !(old.is_none() && new == wasm_bindgen::JsValue::UNDEFINED)
|
||||
{
|
||||
property_expression(&el, prop_name, new.clone())
|
||||
match value {
|
||||
Property::Fn(cx, f) => {
|
||||
let el = el.clone();
|
||||
create_render_effect(cx, move |old| {
|
||||
let new = f();
|
||||
let prop_name = wasm_bindgen::intern(&name);
|
||||
if old.as_ref() != Some(&new)
|
||||
&& !(old.is_none()
|
||||
&& new == wasm_bindgen::JsValue::UNDEFINED)
|
||||
{
|
||||
property_expression(&el, prop_name, new.clone())
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
Property::Value(value) => {
|
||||
let prop_name = wasm_bindgen::intern(&name);
|
||||
property_expression(el, prop_name, value)
|
||||
}
|
||||
};
|
||||
Property::Value(value) => {
|
||||
let prop_name = wasm_bindgen::intern(&name);
|
||||
property_expression(el, prop_name, value)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn property_expression(
|
||||
el: &web_sys::Element,
|
||||
prop_name: &str,
|
||||
value: JsValue,
|
||||
el: &web_sys::Element,
|
||||
prop_name: &str,
|
||||
value: JsValue,
|
||||
) {
|
||||
js_sys::Reflect::set(el, &JsValue::from_str(prop_name), &value)
|
||||
.unwrap_throw();
|
||||
js_sys::Reflect::set(el, &JsValue::from_str(prop_name), &value)
|
||||
.unwrap_throw();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use std::cell::Cell;
|
||||
|
||||
use leptos_reactive::{create_effect, create_rw_signal, RwSignal, Scope};
|
||||
|
||||
use crate::{ElementDescriptor, HtmlElement};
|
||||
use leptos_reactive::{create_effect, create_rw_signal, RwSignal, Scope};
|
||||
use std::cell::Cell;
|
||||
|
||||
/// Contains a shared reference to a DOM node creating while using the `view`
|
||||
/// macro to create your UI.
|
||||
@@ -11,93 +9,93 @@ use crate::{ElementDescriptor, HtmlElement};
|
||||
/// # use leptos::*;
|
||||
/// #[component]
|
||||
/// pub fn MyComponent(cx: Scope) -> impl IntoView {
|
||||
/// let input_ref = NodeRef::<Input>::new(cx);
|
||||
/// let input_ref = NodeRef::<Input>::new(cx);
|
||||
///
|
||||
/// let on_click = move |_| {
|
||||
/// let node = input_ref
|
||||
/// .get()
|
||||
/// .expect("input_ref should be loaded by now");
|
||||
/// // `node` is strongly typed
|
||||
/// // it is dereferenced to an `HtmlInputElement` automatically
|
||||
/// log!("value is {:?}", node.value())
|
||||
/// };
|
||||
/// let on_click = move |_| {
|
||||
/// let node =
|
||||
/// input_ref.get().expect("input_ref should be loaded by now");
|
||||
/// // `node` is strongly typed
|
||||
/// // it is dereferenced to an `HtmlInputElement` automatically
|
||||
/// log!("value is {:?}", node.value())
|
||||
/// };
|
||||
///
|
||||
/// view! {
|
||||
/// cx,
|
||||
/// <div>
|
||||
/// // `node_ref` loads the input
|
||||
/// <input _ref=input_ref type="text"/>
|
||||
/// // the button consumes it
|
||||
/// <button on:click=on_click>"Click me"</button>
|
||||
/// </div>
|
||||
/// }
|
||||
/// view! {
|
||||
/// cx,
|
||||
/// <div>
|
||||
/// // `node_ref` loads the input
|
||||
/// <input _ref=input_ref type="text"/>
|
||||
/// // the button consumes it
|
||||
/// <button on:click=on_click>"Click me"</button>
|
||||
/// </div>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub struct NodeRef<T: ElementDescriptor + 'static>(
|
||||
RwSignal<Option<HtmlElement<T>>>,
|
||||
RwSignal<Option<HtmlElement<T>>>,
|
||||
);
|
||||
|
||||
impl<T: ElementDescriptor + 'static> NodeRef<T> {
|
||||
/// Creates an empty reference.
|
||||
pub fn new(cx: Scope) -> Self {
|
||||
Self(create_rw_signal(cx, None))
|
||||
}
|
||||
/// Creates an empty reference.
|
||||
pub fn new(cx: Scope) -> Self {
|
||||
Self(create_rw_signal(cx, None))
|
||||
}
|
||||
|
||||
/// Gets the element that is currently stored in the reference.
|
||||
///
|
||||
/// This tracks reactively, so that node references can be used in effects.
|
||||
/// Initially, the value will be `None`, but once it is loaded the effect
|
||||
/// will rerun and its value will be `Some(Element)`.
|
||||
#[track_caller]
|
||||
pub fn get(&self) -> Option<HtmlElement<T>>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.0.get()
|
||||
}
|
||||
/// Gets the element that is currently stored in the reference.
|
||||
///
|
||||
/// This tracks reactively, so that node references can be used in effects.
|
||||
/// Initially, the value will be `None`, but once it is loaded the effect
|
||||
/// will rerun and its value will be `Some(Element)`.
|
||||
#[track_caller]
|
||||
pub fn get(&self) -> Option<HtmlElement<T>>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.0.get()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Loads an element into the reference. This tracks reactively,
|
||||
/// so that effects that use the node reference will rerun once it is loaded,
|
||||
/// i.e., effects can be forward-declared.
|
||||
#[track_caller]
|
||||
pub fn load(&self, node: &HtmlElement<T>)
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.0.update(|current| {
|
||||
if current.is_some() {
|
||||
crate::debug_warn!(
|
||||
"You are setting a NodeRef that has already been filled. It’s \
|
||||
possible this is intentional, but it’s also possible that you’re \
|
||||
accidentally using the same NodeRef for multiple _ref attributes."
|
||||
);
|
||||
}
|
||||
*current = Some(node.clone());
|
||||
});
|
||||
}
|
||||
#[doc(hidden)]
|
||||
/// Loads an element into the reference. This tracks reactively,
|
||||
/// so that effects that use the node reference will rerun once it is loaded,
|
||||
/// i.e., effects can be forward-declared.
|
||||
#[track_caller]
|
||||
pub fn load(&self, node: &HtmlElement<T>)
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.0.update(|current| {
|
||||
if current.is_some() {
|
||||
crate::debug_warn!(
|
||||
"You are setting a NodeRef that has already been filled. \
|
||||
It’s possible this is intentional, but it’s also \
|
||||
possible that you’re accidentally using the same NodeRef \
|
||||
for multiple _ref attributes."
|
||||
);
|
||||
}
|
||||
*current = Some(node.clone());
|
||||
});
|
||||
}
|
||||
|
||||
/// Runs the provided closure when the `NodeRef` has been connected
|
||||
/// with it's [`HtmlElement`].
|
||||
pub fn on_load<F>(self, cx: Scope, f: F)
|
||||
where
|
||||
T: Clone,
|
||||
F: FnOnce(HtmlElement<T>) + 'static,
|
||||
{
|
||||
let f = Cell::new(Some(f));
|
||||
/// Runs the provided closure when the `NodeRef` has been connected
|
||||
/// with it's [`HtmlElement`].
|
||||
pub fn on_load<F>(self, cx: Scope, f: F)
|
||||
where
|
||||
T: Clone,
|
||||
F: FnOnce(HtmlElement<T>) + 'static,
|
||||
{
|
||||
let f = Cell::new(Some(f));
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
if let Some(node_ref) = self.get() {
|
||||
f.take().unwrap()(node_ref);
|
||||
}
|
||||
});
|
||||
}
|
||||
create_effect(cx, move |_| {
|
||||
if let Some(node_ref) = self.get() {
|
||||
f.take().unwrap()(node_ref);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ElementDescriptor> Clone for NodeRef<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0)
|
||||
}
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ElementDescriptor + 'static> Copy for NodeRef<T> {}
|
||||
|
||||
@@ -21,19 +21,19 @@ use std::borrow::Cow;
|
||||
/// ```
|
||||
pub fn render_to_string<F, N>(f: F) -> String
|
||||
where
|
||||
F: FnOnce(Scope) -> N + 'static,
|
||||
N: IntoView,
|
||||
F: FnOnce(Scope) -> N + 'static,
|
||||
N: IntoView,
|
||||
{
|
||||
let runtime = leptos_reactive::create_runtime();
|
||||
HydrationCtx::reset_id();
|
||||
let runtime = leptos_reactive::create_runtime();
|
||||
HydrationCtx::reset_id();
|
||||
|
||||
let html = leptos_reactive::run_scope(runtime, |cx| {
|
||||
f(cx).into_view(cx).render_to_string(cx)
|
||||
});
|
||||
let html = leptos_reactive::run_scope(runtime, |cx| {
|
||||
f(cx).into_view(cx).render_to_string(cx)
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
runtime.dispose();
|
||||
|
||||
html.into()
|
||||
html.into()
|
||||
}
|
||||
|
||||
/// Renders a function to a stream of HTML strings.
|
||||
@@ -49,9 +49,9 @@ where
|
||||
/// 3) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
|
||||
/// read under that `<Suspense/>` resolve.
|
||||
pub fn render_to_stream(
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
) -> impl Stream<Item = String> {
|
||||
render_to_stream_with_prefix(view, |_| "".into())
|
||||
render_to_stream_with_prefix(view, |_| "".into())
|
||||
}
|
||||
|
||||
/// Renders a function to a stream of HTML strings. After the `view` runs, the `prefix` will run with
|
||||
@@ -69,13 +69,13 @@ pub fn render_to_stream(
|
||||
/// 4) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
|
||||
/// read under that `<Suspense/>` resolve.
|
||||
pub fn render_to_stream_with_prefix(
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
|
||||
) -> impl Stream<Item = String> {
|
||||
let (stream, runtime, _) =
|
||||
render_to_stream_with_prefix_undisposed(view, prefix);
|
||||
runtime.dispose();
|
||||
stream
|
||||
let (stream, runtime, _) =
|
||||
render_to_stream_with_prefix_undisposed(view, prefix);
|
||||
runtime.dispose();
|
||||
stream
|
||||
}
|
||||
|
||||
/// Renders a function to a stream of HTML strings and returns the [Scope] and [RuntimeId] that were created, so
|
||||
@@ -94,10 +94,10 @@ pub fn render_to_stream_with_prefix(
|
||||
/// 4) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
|
||||
/// read under that `<Suspense/>` resolve.
|
||||
pub fn render_to_stream_with_prefix_undisposed(
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
|
||||
) -> (impl Stream<Item = String>, RuntimeId, ScopeId) {
|
||||
render_to_stream_with_prefix_undisposed_with_context(view, prefix, |_cx| {})
|
||||
render_to_stream_with_prefix_undisposed_with_context(view, prefix, |_cx| {})
|
||||
}
|
||||
|
||||
/// Renders a function to a stream of HTML strings and returns the [Scope] and [RuntimeId] that were created, so
|
||||
@@ -116,50 +116,50 @@ pub fn render_to_stream_with_prefix_undisposed(
|
||||
/// 4) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
|
||||
/// read under that `<Suspense/>` resolve.
|
||||
pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
|
||||
additional_context: impl FnOnce(Scope) + 'static,
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
|
||||
additional_context: impl FnOnce(Scope) + 'static,
|
||||
) -> (impl Stream<Item = String>, RuntimeId, ScopeId) {
|
||||
HydrationCtx::reset_id();
|
||||
HydrationCtx::reset_id();
|
||||
|
||||
// create the runtime
|
||||
let runtime = create_runtime();
|
||||
// create the runtime
|
||||
let runtime = create_runtime();
|
||||
|
||||
let (
|
||||
(shell, prefix, pending_resources, pending_fragments, serializers),
|
||||
scope,
|
||||
_,
|
||||
) = run_scope_undisposed(runtime, {
|
||||
move |cx| {
|
||||
// Add additional context items
|
||||
additional_context(cx);
|
||||
// the actual app body/template code
|
||||
// this does NOT contain any of the data being loaded asynchronously in resources
|
||||
let shell = view(cx).render_to_string(cx);
|
||||
let (
|
||||
(shell, prefix, pending_resources, pending_fragments, serializers),
|
||||
scope,
|
||||
_,
|
||||
) = run_scope_undisposed(runtime, {
|
||||
move |cx| {
|
||||
// Add additional context items
|
||||
additional_context(cx);
|
||||
// the actual app body/template code
|
||||
// this does NOT contain any of the data being loaded asynchronously in resources
|
||||
let shell = view(cx).render_to_string(cx);
|
||||
|
||||
let resources = cx.pending_resources();
|
||||
let pending_resources = serde_json::to_string(&resources).unwrap();
|
||||
let prefix = prefix(cx);
|
||||
let resources = cx.pending_resources();
|
||||
let pending_resources = serde_json::to_string(&resources).unwrap();
|
||||
let prefix = prefix(cx);
|
||||
|
||||
(
|
||||
shell,
|
||||
prefix,
|
||||
pending_resources,
|
||||
cx.pending_fragments(),
|
||||
cx.serialization_resolvers(),
|
||||
)
|
||||
(
|
||||
shell,
|
||||
prefix,
|
||||
pending_resources,
|
||||
cx.pending_fragments(),
|
||||
cx.serialization_resolvers(),
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
let fragments = FuturesUnordered::new();
|
||||
for (fragment_id, (key_before, fut)) in pending_fragments {
|
||||
fragments.push(async move { (fragment_id, key_before, fut.await) })
|
||||
}
|
||||
});
|
||||
|
||||
let fragments = FuturesUnordered::new();
|
||||
for (fragment_id, (key_before, fut)) in pending_fragments {
|
||||
fragments.push(async move { (fragment_id, key_before, fut.await) })
|
||||
}
|
||||
|
||||
// resources and fragments
|
||||
// stream HTML for each <Suspense/> as it resolves
|
||||
// TODO can remove id_before_suspense entirely now
|
||||
let fragments = fragments.map(|(fragment_id, _, html)| {
|
||||
// resources and fragments
|
||||
// stream HTML for each <Suspense/> as it resolves
|
||||
// TODO can remove id_before_suspense entirely now
|
||||
let fragments = fragments.map(|(fragment_id, _, html)| {
|
||||
format!(
|
||||
r#"
|
||||
<template id="{fragment_id}f">{html}</template>
|
||||
@@ -185,24 +185,24 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
"#
|
||||
)
|
||||
});
|
||||
// stream data for each Resource as it resolves
|
||||
let resources = serializers.map(|(id, json)| {
|
||||
let id = serde_json::to_string(&id).unwrap();
|
||||
format!(
|
||||
r#"<script>
|
||||
// stream data for each Resource as it resolves
|
||||
let resources = serializers.map(|(id, json)| {
|
||||
let id = serde_json::to_string(&id).unwrap();
|
||||
format!(
|
||||
r#"<script>
|
||||
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
|
||||
__LEPTOS_RESOURCE_RESOLVERS.get({id})({json:?})
|
||||
}} else {{
|
||||
__LEPTOS_RESOLVED_RESOURCES.set({id}, {json:?});
|
||||
}}
|
||||
</script>"#,
|
||||
)
|
||||
});
|
||||
)
|
||||
});
|
||||
|
||||
// HTML for the view function and script to store resources
|
||||
let stream = futures::stream::once(async move {
|
||||
format!(
|
||||
r#"
|
||||
// HTML for the view function and script to store resources
|
||||
let stream = futures::stream::once(async move {
|
||||
format!(
|
||||
r#"
|
||||
{prefix}
|
||||
{shell}
|
||||
<script>
|
||||
@@ -211,258 +211,269 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
__LEPTOS_RESOURCE_RESOLVERS = new Map();
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
})
|
||||
// TODO these should be combined again in a way that chains them appropriately
|
||||
// such that individual resources can resolve before all fragments are done
|
||||
.chain(fragments)
|
||||
.chain(resources);
|
||||
)
|
||||
})
|
||||
// TODO these should be combined again in a way that chains them appropriately
|
||||
// such that individual resources can resolve before all fragments are done
|
||||
.chain(fragments)
|
||||
.chain(resources);
|
||||
|
||||
(stream, runtime, scope)
|
||||
(stream, runtime, scope)
|
||||
}
|
||||
|
||||
impl View {
|
||||
/// Consumes the node and renders it into an HTML string.
|
||||
pub fn render_to_string(self, _cx: Scope) -> Cow<'static, str> {
|
||||
self.render_to_string_helper()
|
||||
}
|
||||
/// Consumes the node and renders it into an HTML string.
|
||||
pub fn render_to_string(self, _cx: Scope) -> Cow<'static, str> {
|
||||
self.render_to_string_helper()
|
||||
}
|
||||
|
||||
pub(crate) fn render_to_string_helper(self) -> Cow<'static, str> {
|
||||
match self {
|
||||
View::Text(node) => node.content,
|
||||
View::Component(node) => {
|
||||
let content = || {
|
||||
node
|
||||
.children
|
||||
.into_iter()
|
||||
.map(|node| node.render_to_string_helper())
|
||||
.join("")
|
||||
};
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
format!(r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
|
||||
HydrationCtx::to_string(&node.id, false),
|
||||
content(),
|
||||
HydrationCtx::to_string(&node.id, true),
|
||||
name = to_kebab_case(&node.name)
|
||||
).into()
|
||||
} else {
|
||||
format!(
|
||||
r#"{}<!--hk={}-->"#,
|
||||
content(),
|
||||
HydrationCtx::to_string(&node.id, true)
|
||||
).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
View::Suspense(id, node) => format!(
|
||||
"<!--suspense-open-{id}-->{}<!--suspense-close-{id}-->",
|
||||
View::CoreComponent(node).render_to_string_helper()
|
||||
)
|
||||
.into(),
|
||||
View::CoreComponent(node) => {
|
||||
let (id, name, wrap, content) = match node {
|
||||
CoreComponent::Unit(u) => (
|
||||
u.id.clone(),
|
||||
"",
|
||||
false,
|
||||
Box::new(move || {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
format!(
|
||||
"<!--hk={}|leptos-unit-->",
|
||||
HydrationCtx::to_string(&u.id, true)
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
format!("<!--hk={}-->", HydrationCtx::to_string(&u.id, true))
|
||||
.into()
|
||||
}) as Box<dyn FnOnce() -> Cow<'static, str>>,
|
||||
),
|
||||
CoreComponent::DynChild(node) => {
|
||||
let child = node.child.take();
|
||||
(
|
||||
node.id,
|
||||
"dyn-child",
|
||||
true,
|
||||
Box::new(move || {
|
||||
if let Some(child) = *child {
|
||||
// On debug builds, `DynChild` has two marker nodes,
|
||||
// so there is no way for the text to be merged with
|
||||
// surrounding text when the browser parses the HTML,
|
||||
// but in release, `DynChild` only has a trailing marker,
|
||||
// and the browser automatically merges the dynamic text
|
||||
// into one single node, so we need to artificially make the
|
||||
// browser create the dynamic text as it's own text node
|
||||
if let View::Text(t) = child {
|
||||
if !cfg!(debug_assertions) {
|
||||
format!("<!>{}", t.content).into()
|
||||
} else {
|
||||
t.content
|
||||
}
|
||||
pub(crate) fn render_to_string_helper(self) -> Cow<'static, str> {
|
||||
match self {
|
||||
View::Text(node) => node.content,
|
||||
View::Component(node) => {
|
||||
let content = || {
|
||||
node.children
|
||||
.into_iter()
|
||||
.map(|node| node.render_to_string_helper())
|
||||
.join("")
|
||||
};
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
format!(r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
|
||||
HydrationCtx::to_string(&node.id, false),
|
||||
content(),
|
||||
HydrationCtx::to_string(&node.id, true),
|
||||
name = to_kebab_case(&node.name)
|
||||
).into()
|
||||
} else {
|
||||
child.render_to_string_helper()
|
||||
format!(
|
||||
r#"{}<!--hk={}-->"#,
|
||||
content(),
|
||||
HydrationCtx::to_string(&node.id, true)
|
||||
).into()
|
||||
}
|
||||
} else {
|
||||
"".into()
|
||||
}
|
||||
}) as Box<dyn FnOnce() -> Cow<'static, str>>,
|
||||
}
|
||||
View::Suspense(id, node) => format!(
|
||||
"<!--suspense-open-{id}-->{}<!--suspense-close-{id}-->",
|
||||
View::CoreComponent(node).render_to_string_helper()
|
||||
)
|
||||
}
|
||||
CoreComponent::Each(node) => {
|
||||
let children = node.children.take();
|
||||
(
|
||||
node.id,
|
||||
"each",
|
||||
true,
|
||||
Box::new(move || {
|
||||
children
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|node| {
|
||||
let id = node.id;
|
||||
.into(),
|
||||
View::CoreComponent(node) => {
|
||||
let (id, name, wrap, content) = match node {
|
||||
CoreComponent::Unit(u) => (
|
||||
u.id.clone(),
|
||||
"",
|
||||
false,
|
||||
Box::new(move || {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
format!(
|
||||
"<!--hk={}|leptos-unit-->",
|
||||
HydrationCtx::to_string(&u.id, true)
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
let content = || node.child.render_to_string_helper();
|
||||
#[cfg(not(debug_assertions))]
|
||||
format!(
|
||||
"<!--hk={}-->",
|
||||
HydrationCtx::to_string(&u.id, true)
|
||||
)
|
||||
.into()
|
||||
})
|
||||
as Box<dyn FnOnce() -> Cow<'static, str>>,
|
||||
),
|
||||
CoreComponent::DynChild(node) => {
|
||||
let child = node.child.take();
|
||||
(
|
||||
node.id,
|
||||
"dyn-child",
|
||||
true,
|
||||
Box::new(move || {
|
||||
if let Some(child) = *child {
|
||||
// On debug builds, `DynChild` has two marker nodes,
|
||||
// so there is no way for the text to be merged with
|
||||
// surrounding text when the browser parses the HTML,
|
||||
// but in release, `DynChild` only has a trailing marker,
|
||||
// and the browser automatically merges the dynamic text
|
||||
// into one single node, so we need to artificially make the
|
||||
// browser create the dynamic text as it's own text node
|
||||
if let View::Text(t) = child {
|
||||
if !cfg!(debug_assertions) {
|
||||
format!("<!>{}", t.content).into()
|
||||
} else {
|
||||
t.content
|
||||
}
|
||||
} else {
|
||||
child.render_to_string_helper()
|
||||
}
|
||||
} else {
|
||||
"".into()
|
||||
}
|
||||
})
|
||||
as Box<dyn FnOnce() -> Cow<'static, str>>,
|
||||
)
|
||||
}
|
||||
CoreComponent::Each(node) => {
|
||||
let children = node.children.take();
|
||||
(
|
||||
node.id,
|
||||
"each",
|
||||
true,
|
||||
Box::new(move || {
|
||||
children
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|node| {
|
||||
let id = node.id;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
format!(
|
||||
let content = || {
|
||||
node.child.render_to_string_helper()
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
format!(
|
||||
"<!--hk={}|leptos-each-item-start-->{}<!\
|
||||
--hk={}|leptos-each-item-end-->",
|
||||
HydrationCtx::to_string(&id, false),
|
||||
content(),
|
||||
HydrationCtx::to_string(&id, true),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
format!(
|
||||
"{}<!--hk={}-->",
|
||||
content(),
|
||||
HydrationCtx::to_string(&id, true)
|
||||
)
|
||||
})
|
||||
.join("")
|
||||
.into()
|
||||
})
|
||||
as Box<dyn FnOnce() -> Cow<'static, str>>,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
format!(
|
||||
"{}<!--hk={}-->",
|
||||
content(),
|
||||
HydrationCtx::to_string(&id, true)
|
||||
)
|
||||
})
|
||||
.join("")
|
||||
.into()
|
||||
}) as Box<dyn FnOnce() -> Cow<'static, str>>,
|
||||
)
|
||||
}
|
||||
};
|
||||
if wrap {
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
format!(
|
||||
r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
|
||||
HydrationCtx::to_string(&id, false),
|
||||
content(),
|
||||
HydrationCtx::to_string(&id, true),
|
||||
).into()
|
||||
} else {
|
||||
let _ = name;
|
||||
|
||||
if wrap {
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
format!(
|
||||
r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
|
||||
HydrationCtx::to_string(&id, false),
|
||||
content(),
|
||||
HydrationCtx::to_string(&id, true),
|
||||
).into()
|
||||
} else {
|
||||
let _ = name;
|
||||
|
||||
format!(
|
||||
r#"{}<!--hk={}-->"#,
|
||||
content(),
|
||||
HydrationCtx::to_string(&id, true)
|
||||
).into()
|
||||
format!(
|
||||
r#"{}<!--hk={}-->"#,
|
||||
content(),
|
||||
HydrationCtx::to_string(&id, true)
|
||||
).into()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content()
|
||||
}
|
||||
}
|
||||
View::Element(el) => {
|
||||
if let Some(prerendered) = el.prerendered {
|
||||
prerendered
|
||||
} else {
|
||||
let tag_name = el.name;
|
||||
View::Element(el) => {
|
||||
if let Some(prerendered) = el.prerendered {
|
||||
prerendered
|
||||
} else {
|
||||
let tag_name = el.name;
|
||||
|
||||
let mut inner_html = None;
|
||||
let mut inner_html = None;
|
||||
|
||||
let attrs = el
|
||||
.attrs
|
||||
.into_iter()
|
||||
.filter_map(|(name, value)| -> Option<Cow<'static, str>> {
|
||||
if value.is_empty() {
|
||||
Some(format!(" {name}").into())
|
||||
} else if name == "inner_html" {
|
||||
inner_html = Some(value);
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
format!(
|
||||
let attrs = el
|
||||
.attrs
|
||||
.into_iter()
|
||||
.filter_map(
|
||||
|(name, value)| -> Option<Cow<'static, str>> {
|
||||
if value.is_empty() {
|
||||
Some(format!(" {name}").into())
|
||||
} else if name == "inner_html" {
|
||||
inner_html = Some(value);
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
format!(
|
||||
" {name}=\"{}\"",
|
||||
html_escape::encode_double_quoted_attribute(&value)
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
})
|
||||
.join("");
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
.join("");
|
||||
|
||||
if el.is_void {
|
||||
format!("<{tag_name}{attrs}/>").into()
|
||||
} else if let Some(inner_html) = inner_html {
|
||||
format!("<{tag_name}{attrs}>{inner_html}</{tag_name}>").into()
|
||||
} else {
|
||||
let children = el
|
||||
.children
|
||||
.into_iter()
|
||||
.map(|node| node.render_to_string_helper())
|
||||
.join("");
|
||||
if el.is_void {
|
||||
format!("<{tag_name}{attrs}/>").into()
|
||||
} else if let Some(inner_html) = inner_html {
|
||||
format!("<{tag_name}{attrs}>{inner_html}</{tag_name}>")
|
||||
.into()
|
||||
} else {
|
||||
let children = el
|
||||
.children
|
||||
.into_iter()
|
||||
.map(|node| node.render_to_string_helper())
|
||||
.join("");
|
||||
|
||||
format!("<{tag_name}{attrs}>{children}</{tag_name}>").into()
|
||||
}
|
||||
format!("<{tag_name}{attrs}>{children}</{tag_name}>")
|
||||
.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
View::Transparent(_) => Default::default(),
|
||||
}
|
||||
}
|
||||
View::Transparent(_) => Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn to_kebab_case(name: &str) -> String {
|
||||
if name.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let mut new_name = String::with_capacity(name.len() + 8);
|
||||
|
||||
let mut chars = name.chars();
|
||||
|
||||
new_name.push(
|
||||
chars
|
||||
.next()
|
||||
.map(|mut c| {
|
||||
if c.is_ascii() {
|
||||
c.make_ascii_lowercase();
|
||||
}
|
||||
|
||||
c
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
for mut char in chars {
|
||||
if char.is_ascii_uppercase() {
|
||||
char.make_ascii_lowercase();
|
||||
|
||||
new_name.push('-');
|
||||
if name.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
new_name.push(char);
|
||||
}
|
||||
let mut new_name = String::with_capacity(name.len() + 8);
|
||||
|
||||
new_name
|
||||
let mut chars = name.chars();
|
||||
|
||||
new_name.push(
|
||||
chars
|
||||
.next()
|
||||
.map(|mut c| {
|
||||
if c.is_ascii() {
|
||||
c.make_ascii_lowercase();
|
||||
}
|
||||
|
||||
c
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
for mut char in chars {
|
||||
if char.is_ascii_uppercase() {
|
||||
char.make_ascii_lowercase();
|
||||
|
||||
new_name.push('-');
|
||||
}
|
||||
|
||||
new_name.push(char);
|
||||
}
|
||||
|
||||
new_name
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn escape_attr<T>(value: &T) -> Cow<'_, str>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
T: AsRef<str>,
|
||||
{
|
||||
html_escape::encode_double_quoted_attribute(value)
|
||||
html_escape::encode_double_quoted_attribute(value)
|
||||
}
|
||||
|
||||
@@ -7,39 +7,39 @@ use std::{any::Any, fmt, rc::Rc};
|
||||
pub struct Transparent(Rc<dyn Any>);
|
||||
|
||||
impl Transparent {
|
||||
/// Creates a new wrapper for this data.
|
||||
pub fn new<T>(value: T) -> Self
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
Self(Rc::new(value))
|
||||
}
|
||||
/// Creates a new wrapper for this data.
|
||||
pub fn new<T>(value: T) -> Self
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
Self(Rc::new(value))
|
||||
}
|
||||
|
||||
/// Returns some reference to the inner value if it is of type `T`, or `None` if it isn't.
|
||||
pub fn downcast_ref<T>(&self) -> Option<&T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.0.downcast_ref()
|
||||
}
|
||||
/// Returns some reference to the inner value if it is of type `T`, or `None` if it isn't.
|
||||
pub fn downcast_ref<T>(&self) -> Option<&T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.0.downcast_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Transparent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("Transparent").finish()
|
||||
}
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("Transparent").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Transparent {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
std::ptr::eq(&self.0, &other.0)
|
||||
}
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
std::ptr::eq(&self.0, &other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Transparent {}
|
||||
|
||||
impl IntoView for Transparent {
|
||||
fn into_view(self, _: Scope) -> View {
|
||||
View::Transparent(self)
|
||||
}
|
||||
fn into_view(self, _: Scope) -> View {
|
||||
View::Transparent(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@ use proc_macro_error::ResultExt;
|
||||
use quote::{format_ident, ToTokens, TokenStreamExt};
|
||||
use std::collections::HashSet;
|
||||
use syn::{
|
||||
parse::Parse, parse_quote, AngleBracketedGenericArguments, Attribute, FnArg, GenericArgument,
|
||||
ItemFn, LitStr, Meta, MetaList, MetaNameValue, NestedMeta, Pat, PatIdent, Path, PathArguments,
|
||||
ReturnType, Type, TypePath, Visibility,
|
||||
parse::Parse, parse_quote, AngleBracketedGenericArguments, Attribute,
|
||||
FnArg, GenericArgument, ItemFn, LitStr, Meta, MetaList, MetaNameValue,
|
||||
NestedMeta, Pat, PatIdent, Path, PathArguments, ReturnType, Type, TypePath,
|
||||
Visibility,
|
||||
};
|
||||
|
||||
pub struct Model {
|
||||
@@ -62,7 +63,8 @@ impl Parse for Model {
|
||||
item.sig.inputs.iter_mut().for_each(|arg| {
|
||||
if let FnArg::Typed(ty) = arg {
|
||||
drain_filter(&mut ty.attrs, |attr| {
|
||||
attr.path == parse_quote!(doc) || attr.path == parse_quote!(prop)
|
||||
attr.path == parse_quote!(doc)
|
||||
|| attr.path == parse_quote!(prop)
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -91,7 +93,10 @@ impl Parse for Model {
|
||||
|
||||
// implemented manually because Vec::drain_filter is nightly only
|
||||
// follows std recommended parallel
|
||||
fn drain_filter<T>(vec: &mut Vec<T>, mut some_predicate: impl FnMut(&mut T) -> bool) {
|
||||
fn drain_filter<T>(
|
||||
vec: &mut Vec<T>,
|
||||
mut some_predicate: impl FnMut(&mut T) -> bool,
|
||||
) {
|
||||
let mut i = 0;
|
||||
while i < vec.len() {
|
||||
if some_predicate(&mut vec[i]) {
|
||||
@@ -139,32 +144,33 @@ impl ToTokens for Model {
|
||||
|
||||
let prop_names = prop_names(props);
|
||||
|
||||
let builder_name_doc =
|
||||
LitStr::new(&format!("Props for the [`{name}`] component."), name.span());
|
||||
let builder_name_doc = LitStr::new(
|
||||
&format!("Props for the [`{name}`] component."),
|
||||
name.span(),
|
||||
);
|
||||
|
||||
let component_fn_prop_docs = generate_component_fn_prop_docs(props);
|
||||
|
||||
let (tracing_instrument_attr, tracing_span_expr, tracing_guard_expr) = if cfg!(
|
||||
feature = "tracing"
|
||||
) {
|
||||
(
|
||||
quote! {
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
::leptos::leptos_dom::tracing::instrument(level = "trace", name = #trace_name, skip_all)
|
||||
)]
|
||||
},
|
||||
quote! {
|
||||
let span = ::leptos::leptos_dom::tracing::Span::current();
|
||||
},
|
||||
quote! {
|
||||
#[cfg(debug_assertions)]
|
||||
let _guard = span.entered();
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(quote! {}, quote! {}, quote! {})
|
||||
};
|
||||
let (tracing_instrument_attr, tracing_span_expr, tracing_guard_expr) =
|
||||
if cfg!(feature = "tracing") {
|
||||
(
|
||||
quote! {
|
||||
#[cfg_attr(
|
||||
debug_assertions,
|
||||
::leptos::leptos_dom::tracing::instrument(level = "trace", name = #trace_name, skip_all)
|
||||
)]
|
||||
},
|
||||
quote! {
|
||||
let span = ::leptos::leptos_dom::tracing::Span::current();
|
||||
},
|
||||
quote! {
|
||||
#[cfg(debug_assertions)]
|
||||
let _guard = span.entered();
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(quote! {}, quote! {}, quote! {})
|
||||
};
|
||||
|
||||
let component = if *is_transparent {
|
||||
quote! {
|
||||
@@ -249,11 +255,16 @@ impl Prop {
|
||||
.attrs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, attr)| PropOpt::from_attribute(attr).map(|opt| (i, opt)))
|
||||
.filter_map(|(i, attr)| {
|
||||
PropOpt::from_attribute(attr).map(|opt| (i, opt))
|
||||
})
|
||||
.fold(HashSet::new(), |mut acc, cur| {
|
||||
// Make sure opts aren't repeated
|
||||
if acc.intersection(&cur.1).next().is_some() {
|
||||
abort!(typed.attrs[cur.0], "`#[prop]` options are repeated");
|
||||
abort!(
|
||||
typed.attrs[cur.0],
|
||||
"`#[prop]` options are repeated"
|
||||
);
|
||||
}
|
||||
|
||||
acc.extend(cur.1);
|
||||
@@ -262,10 +273,13 @@ impl Prop {
|
||||
});
|
||||
|
||||
// Make sure conflicting options are not present
|
||||
if prop_opts.contains(&PropOpt::Optional) && prop_opts.contains(&PropOpt::OptionalNoStrip) {
|
||||
if prop_opts.contains(&PropOpt::Optional)
|
||||
&& prop_opts.contains(&PropOpt::OptionalNoStrip)
|
||||
{
|
||||
abort!(
|
||||
typed,
|
||||
"`optional` and `optional_no_strip` options are mutually exclusive"
|
||||
"`optional` and `optional_no_strip` options are mutually \
|
||||
exclusive"
|
||||
);
|
||||
} else if prop_opts.contains(&PropOpt::Optional)
|
||||
&& prop_opts.contains(&PropOpt::StripOption)
|
||||
@@ -279,7 +293,8 @@ impl Prop {
|
||||
{
|
||||
abort!(
|
||||
typed,
|
||||
"`optional_no_strip` and `strip_option` options are mutually exclusive"
|
||||
"`optional_no_strip` and `strip_option` options are mutually \
|
||||
exclusive"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -288,8 +303,8 @@ impl Prop {
|
||||
} else {
|
||||
abort!(
|
||||
typed.pat,
|
||||
"only `prop: bool` style types are allowed within the `#[component]` \
|
||||
macro"
|
||||
"only `prop: bool` style types are allowed within the \
|
||||
`#[component]` macro"
|
||||
);
|
||||
};
|
||||
|
||||
@@ -333,7 +348,8 @@ impl Docs {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, attr)| {
|
||||
if let Meta::NameValue(MetaNameValue { lit: doc, .. }) = attr.parse_meta().unwrap()
|
||||
if let Meta::NameValue(MetaNameValue { lit: doc, .. }) =
|
||||
attr.parse_meta().unwrap()
|
||||
{
|
||||
let doc_str = quote!(#doc);
|
||||
|
||||
@@ -368,7 +384,8 @@ impl Docs {
|
||||
.0
|
||||
.iter()
|
||||
.map(|attr| {
|
||||
if let Meta::NameValue(MetaNameValue { lit: doc, .. }) = attr.parse_meta().unwrap()
|
||||
if let Meta::NameValue(MetaNameValue { lit: doc, .. }) =
|
||||
attr.parse_meta().unwrap()
|
||||
{
|
||||
let mut doc_str = quote!(#doc).to_string();
|
||||
|
||||
@@ -403,15 +420,17 @@ enum PropOpt {
|
||||
|
||||
impl PropOpt {
|
||||
fn from_attribute(attr: &Attribute) -> Option<HashSet<Self>> {
|
||||
const ABORT_OPT_MESSAGE: &str = "only `optional`, `optional_no_strip`, \
|
||||
`strip_option`, `default` and `into` are \
|
||||
allowed as arguments to `#[prop()]`";
|
||||
const ABORT_OPT_MESSAGE: &str =
|
||||
"only `optional`, `optional_no_strip`, `strip_option`, `default` \
|
||||
and `into` are allowed as arguments to `#[prop()]`";
|
||||
|
||||
if attr.path != parse_quote!(prop) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Meta::List(MetaList { nested, .. }) = attr.parse_meta().unwrap_or_abort() {
|
||||
if let Meta::List(MetaList { nested, .. }) =
|
||||
attr.parse_meta().unwrap_or_abort()
|
||||
{
|
||||
Some(
|
||||
nested
|
||||
.iter()
|
||||
@@ -473,7 +492,8 @@ struct TypedBuilderOpts {
|
||||
impl TypedBuilderOpts {
|
||||
fn from_opts(opts: &HashSet<PropOpt>, is_ty_option: bool) -> Self {
|
||||
Self {
|
||||
default: opts.contains(&PropOpt::Optional) || opts.contains(&PropOpt::OptionalNoStrip),
|
||||
default: opts.contains(&PropOpt::Optional)
|
||||
|| opts.contains(&PropOpt::OptionalNoStrip),
|
||||
default_with_value: opts.iter().find_map(|p| match p {
|
||||
PropOpt::OptionalWithDefault(v) => Some(v.to_owned()),
|
||||
_ => None,
|
||||
@@ -531,7 +551,8 @@ fn prop_builder_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
|
||||
ty,
|
||||
} = prop;
|
||||
|
||||
let builder_attrs = TypedBuilderOpts::from_opts(prop_opts, is_option(ty));
|
||||
let builder_attrs =
|
||||
TypedBuilderOpts::from_opts(prop_opts, is_option(ty));
|
||||
|
||||
let builder_docs = prop_to_doc(prop, PropDocStyle::Inline);
|
||||
|
||||
@@ -566,7 +587,8 @@ fn generate_component_fn_prop_docs(props: &[Prop]) -> TokenStream {
|
||||
let optional_prop_docs = props
|
||||
.iter()
|
||||
.filter(|Prop { prop_opts, .. }| {
|
||||
prop_opts.contains(&PropOpt::Optional) || prop_opts.contains(&PropOpt::OptionalNoStrip)
|
||||
prop_opts.contains(&PropOpt::Optional)
|
||||
|| prop_opts.contains(&PropOpt::OptionalNoStrip)
|
||||
})
|
||||
.map(|p| prop_to_doc(p, PropDocStyle::List))
|
||||
.collect::<TokenStream>();
|
||||
@@ -613,8 +635,8 @@ fn is_option(ty: &Type) -> bool {
|
||||
|
||||
fn unwrap_option(ty: &Type) -> Option<Type> {
|
||||
const STD_OPTION_MSG: &str =
|
||||
"make sure you're not shadowing the `std::option::Option` type that is \
|
||||
automatically imported from the standard prelude";
|
||||
"make sure you're not shadowing the `std::option::Option` type that \
|
||||
is automatically imported from the standard prelude";
|
||||
|
||||
if let Type::Path(TypePath {
|
||||
path: Path { segments, .. },
|
||||
@@ -623,9 +645,9 @@ fn unwrap_option(ty: &Type) -> Option<Type> {
|
||||
{
|
||||
if let [first] = &segments.iter().collect::<Vec<_>>()[..] {
|
||||
if first.ident == "Option" {
|
||||
if let PathArguments::AngleBracketed(AngleBracketedGenericArguments {
|
||||
args, ..
|
||||
}) = &first.arguments
|
||||
if let PathArguments::AngleBracketed(
|
||||
AngleBracketedGenericArguments { args, .. },
|
||||
) = &first.arguments
|
||||
{
|
||||
if let [first] = &args.iter().collect::<Vec<_>>()[..] {
|
||||
if let GenericArgument::Type(ty) = first {
|
||||
@@ -706,7 +728,11 @@ fn prop_to_doc(
|
||||
&if !prop_opts.contains(&PropOpt::Into) {
|
||||
format!("- **{}**: [`{}`]", quote!(#name), pretty_ty)
|
||||
} else {
|
||||
format!("- **{}**: `impl`[`Into<{}>`]", quote!(#name), pretty_ty)
|
||||
format!(
|
||||
"- **{}**: `impl`[`Into<{}>`]",
|
||||
quote!(#name),
|
||||
pretty_ty
|
||||
)
|
||||
},
|
||||
name.ident.span(),
|
||||
);
|
||||
|
||||
@@ -19,7 +19,10 @@ pub(crate) enum Mode {
|
||||
|
||||
impl Default for Mode {
|
||||
fn default() -> Self {
|
||||
if cfg!(feature = "hydrate") || cfg!(feature = "csr") || cfg!(feature = "web") {
|
||||
if cfg!(feature = "hydrate")
|
||||
|| cfg!(feature = "csr")
|
||||
|| cfg!(feature = "web")
|
||||
{
|
||||
Mode::Client
|
||||
} else {
|
||||
Mode::Ssr
|
||||
@@ -282,7 +285,9 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
let mut tokens = tokens.into_iter();
|
||||
let (cx, comma) = (tokens.next(), tokens.next());
|
||||
match (cx, comma) {
|
||||
(Some(TokenTree::Ident(cx)), Some(TokenTree::Punct(punct))) if punct.as_char() == ',' => {
|
||||
(Some(TokenTree::Ident(cx)), Some(TokenTree::Punct(punct)))
|
||||
if punct.as_char() == ',' =>
|
||||
{
|
||||
let first = tokens.next();
|
||||
let second = tokens.next();
|
||||
let third = tokens.next();
|
||||
@@ -292,11 +297,17 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
if *first == "class" && eq.as_char() == '=' =>
|
||||
{
|
||||
match &fourth {
|
||||
Some(TokenTree::Punct(comma)) if comma.as_char() == ',' => third.clone(),
|
||||
Some(TokenTree::Punct(comma))
|
||||
if comma.as_char() == ',' =>
|
||||
{
|
||||
third.clone()
|
||||
}
|
||||
_ => {
|
||||
let error_msg = concat!(
|
||||
"To create a scope class with the view! macro you must put a comma `,` after the value.\n",
|
||||
"e.g., view!{cx, class=\"my-class\", <div>...</div>}"
|
||||
"To create a scope class with the view! macro \
|
||||
you must put a comma `,` after the value.\n",
|
||||
"e.g., view!{cx, class=\"my-class\", \
|
||||
<div>...</div>}"
|
||||
);
|
||||
panic!("{error_msg}")
|
||||
}
|
||||
@@ -326,7 +337,10 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
.into()
|
||||
}
|
||||
_ => {
|
||||
panic!("view! macro needs a context and RSX: e.g., view! {{ cx, <div>...</div> }}")
|
||||
panic!(
|
||||
"view! macro needs a context and RSX: e.g., view! {{ cx, \
|
||||
<div>...</div> }}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -351,33 +365,34 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
///
|
||||
/// #[component]
|
||||
/// fn HelloComponent(
|
||||
/// cx: Scope,
|
||||
/// /// The user's name.
|
||||
/// name: String,
|
||||
/// /// The user's age.
|
||||
/// age: u8
|
||||
/// cx: Scope,
|
||||
/// /// The user's name.
|
||||
/// name: String,
|
||||
/// /// The user's age.
|
||||
/// age: u8,
|
||||
/// ) -> impl IntoView {
|
||||
/// // create the signals (reactive values) that will update the UI
|
||||
/// let (age, set_age) = create_signal(cx, age);
|
||||
/// // increase `age` by 1 every second
|
||||
/// set_interval(move || {
|
||||
/// set_age.update(|age| *age += 1)
|
||||
/// }, Duration::from_secs(1));
|
||||
///
|
||||
/// // return the user interface, which will be automatically updated
|
||||
/// // when signal values change
|
||||
/// view! { cx,
|
||||
/// <p>"Your name is " {name} " and you are " {age} " years old."</p>
|
||||
/// }
|
||||
/// // create the signals (reactive values) that will update the UI
|
||||
/// let (age, set_age) = create_signal(cx, age);
|
||||
/// // increase `age` by 1 every second
|
||||
/// set_interval(
|
||||
/// move || set_age.update(|age| *age += 1),
|
||||
/// Duration::from_secs(1),
|
||||
/// );
|
||||
///
|
||||
/// // return the user interface, which will be automatically updated
|
||||
/// // when signal values change
|
||||
/// view! { cx,
|
||||
/// <p>"Your name is " {name} " and you are " {age} " years old."</p>
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[component]
|
||||
/// fn App(cx: Scope) -> impl IntoView {
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <HelloComponent name="Greg".to_string() age=32/>
|
||||
/// </main>
|
||||
/// }
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <HelloComponent name="Greg".to_string() age=32/>
|
||||
/// </main>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
@@ -402,11 +417,15 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
///
|
||||
/// // PascalCase: Generated component will be called MyComponent
|
||||
/// #[component]
|
||||
/// fn MyComponent(cx: Scope) -> impl IntoView { todo!() }
|
||||
/// fn MyComponent(cx: Scope) -> impl IntoView {
|
||||
/// todo!()
|
||||
/// }
|
||||
///
|
||||
/// // snake_case: Generated component will be called MySnakeCaseComponent
|
||||
/// #[component]
|
||||
/// fn my_snake_case_component(cx: Scope) -> impl IntoView { todo!() }
|
||||
/// fn my_snake_case_component(cx: Scope) -> impl IntoView {
|
||||
/// todo!()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// 3. The macro generates a type `ComponentProps` for every `Component` (so, `HomePage` generates `HomePageProps`,
|
||||
@@ -419,22 +438,28 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
/// use component::{MyComponent, MyComponentProps};
|
||||
///
|
||||
/// mod component {
|
||||
/// use leptos::*;
|
||||
/// use leptos::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn MyComponent(cx: Scope) -> impl IntoView { todo!() }
|
||||
/// #[component]
|
||||
/// pub fn MyComponent(cx: Scope) -> impl IntoView {
|
||||
/// todo!()
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
///
|
||||
/// use snake_case_component::{MySnakeCaseComponent, MySnakeCaseComponentProps};
|
||||
/// use snake_case_component::{
|
||||
/// MySnakeCaseComponent, MySnakeCaseComponentProps,
|
||||
/// };
|
||||
///
|
||||
/// mod snake_case_component {
|
||||
/// use leptos::*;
|
||||
/// use leptos::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn my_snake_case_component(cx: Scope) -> impl IntoView { todo!() }
|
||||
/// #[component]
|
||||
/// pub fn my_snake_case_component(cx: Scope) -> impl IntoView {
|
||||
/// todo!()
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
@@ -454,8 +479,10 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
/// # use leptos::*;
|
||||
/// #[component]
|
||||
/// fn MyComponent<T>(cx: Scope, render_prop: T) -> impl IntoView
|
||||
/// where T: Fn() -> HtmlElement<Div> {
|
||||
/// todo!()
|
||||
/// where
|
||||
/// T: Fn() -> HtmlElement<Div>,
|
||||
/// {
|
||||
/// todo!()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
@@ -468,26 +495,26 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
/// # use leptos::*;
|
||||
/// #[component]
|
||||
/// fn ComponentWithChildren(cx: Scope, children: Children) -> impl IntoView {
|
||||
/// view! {
|
||||
/// cx,
|
||||
/// <ul>
|
||||
/// {children(cx)
|
||||
/// .nodes
|
||||
/// .into_iter()
|
||||
/// .map(|child| view! { cx, <li>{child}</li> })
|
||||
/// .collect::<Vec<_>>()}
|
||||
/// </ul>
|
||||
/// }
|
||||
/// view! {
|
||||
/// cx,
|
||||
/// <ul>
|
||||
/// {children(cx)
|
||||
/// .nodes
|
||||
/// .into_iter()
|
||||
/// .map(|child| view! { cx, <li>{child}</li> })
|
||||
/// .collect::<Vec<_>>()}
|
||||
/// </ul>
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[component]
|
||||
/// fn WrapSomeChildren(cx: Scope) -> impl IntoView {
|
||||
/// view! { cx,
|
||||
/// <ComponentWithChildren>
|
||||
/// "Ooh, look at us!"
|
||||
/// <span>"We're being projected!"</span>
|
||||
/// </ComponentWithChildren>
|
||||
/// }
|
||||
/// view! { cx,
|
||||
/// <ComponentWithChildren>
|
||||
/// "Ooh, look at us!"
|
||||
/// <span>"We're being projected!"</span>
|
||||
/// </ComponentWithChildren>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
@@ -509,30 +536,27 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
///
|
||||
/// #[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>
|
||||
/// 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
|
||||
/// // whatever UI you need
|
||||
/// }
|
||||
///
|
||||
/// #[component]
|
||||
/// #[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`
|
||||
/// />
|
||||
/// }
|
||||
/// 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]
|
||||
@@ -630,7 +654,9 @@ pub fn derive_prop(input: TokenStream) -> TokenStream {
|
||||
|
||||
// Derive Params trait for routing
|
||||
#[proc_macro_derive(Params, attributes(params))]
|
||||
pub fn params_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
pub fn params_derive(
|
||||
input: proc_macro::TokenStream,
|
||||
) -> proc_macro::TokenStream {
|
||||
let ast = syn::parse(input).unwrap();
|
||||
params::impl_params(&ast)
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{DeriveInput, Error, Result};
|
||||
use syn::{spanned::Spanned, DeriveInput, Error, Result};
|
||||
|
||||
pub fn impl_derive_prop(ast: &DeriveInput) -> Result<TokenStream> {
|
||||
let data = match &ast.data {
|
||||
syn::Data::Struct(data) => match &data.fields {
|
||||
syn::Fields::Named(fields) => {
|
||||
let struct_info = struct_info::StructInfo::new(ast, fields.named.iter())?;
|
||||
let struct_info =
|
||||
struct_info::StructInfo::new(ast, fields.named.iter())?;
|
||||
let builder_creation = struct_info.builder_creation_impl()?;
|
||||
let conversion_helper = struct_info.conversion_helper_impl()?;
|
||||
let fields = struct_info
|
||||
@@ -48,26 +48,34 @@ pub fn impl_derive_prop(ast: &DeriveInput) -> Result<TokenStream> {
|
||||
}
|
||||
},
|
||||
syn::Data::Enum(_) => {
|
||||
return Err(Error::new(ast.span(), "Prop is not supported for enums"))
|
||||
return Err(Error::new(
|
||||
ast.span(),
|
||||
"Prop is not supported for enums",
|
||||
))
|
||||
}
|
||||
syn::Data::Union(_) => {
|
||||
return Err(Error::new(ast.span(), "Prop is not supported for unions"))
|
||||
return Err(Error::new(
|
||||
ast.span(),
|
||||
"Prop is not supported for unions",
|
||||
))
|
||||
}
|
||||
};
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
mod struct_info {
|
||||
use super::{
|
||||
field_info::{FieldBuilderAttr, FieldInfo},
|
||||
util::{
|
||||
empty_type, empty_type_tuple, expr_to_single_string,
|
||||
make_punctuated_single, modify_types_generics_hack,
|
||||
path_to_single_string, strip_raw_ident_prefix, type_tuple,
|
||||
},
|
||||
};
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::parse::Error;
|
||||
|
||||
use super::field_info::{FieldBuilderAttr, FieldInfo};
|
||||
use super::util::{
|
||||
empty_type, empty_type_tuple, expr_to_single_string, make_punctuated_single,
|
||||
modify_types_generics_hack, path_to_single_string, strip_raw_ident_prefix, type_tuple,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StructInfo<'a> {
|
||||
pub vis: &'a syn::Visibility,
|
||||
@@ -93,17 +101,27 @@ mod struct_info {
|
||||
fields: impl Iterator<Item = &'a syn::Field>,
|
||||
) -> Result<StructInfo<'a>, Error> {
|
||||
let builder_attr = TypeBuilderAttr::new(&ast.attrs)?;
|
||||
let builder_name = strip_raw_ident_prefix(format!("{}Builder", ast.ident));
|
||||
let builder_name =
|
||||
strip_raw_ident_prefix(format!("{}Builder", ast.ident));
|
||||
Ok(StructInfo {
|
||||
vis: &ast.vis,
|
||||
name: &ast.ident,
|
||||
generics: &ast.generics,
|
||||
fields: fields
|
||||
.enumerate()
|
||||
.map(|(i, f)| FieldInfo::new(i, f, builder_attr.field_defaults.clone()))
|
||||
.map(|(i, f)| {
|
||||
FieldInfo::new(
|
||||
i,
|
||||
f,
|
||||
builder_attr.field_defaults.clone(),
|
||||
)
|
||||
})
|
||||
.collect::<Result<_, _>>()?,
|
||||
builder_attr,
|
||||
builder_name: syn::Ident::new(&builder_name, proc_macro2::Span::call_site()),
|
||||
builder_name: syn::Ident::new(
|
||||
&builder_name,
|
||||
proc_macro2::Span::call_site(),
|
||||
),
|
||||
conversion_helper_trait_name: syn::Ident::new(
|
||||
&format!("{builder_name}_Optional"),
|
||||
proc_macro2::Span::call_site(),
|
||||
@@ -115,7 +133,10 @@ mod struct_info {
|
||||
})
|
||||
}
|
||||
|
||||
fn modify_generics<F: FnMut(&mut syn::Generics)>(&self, mut mutator: F) -> syn::Generics {
|
||||
fn modify_generics<F: FnMut(&mut syn::Generics)>(
|
||||
&self,
|
||||
mut mutator: F,
|
||||
) -> syn::Generics {
|
||||
let mut generics = self.generics.clone();
|
||||
mutator(&mut generics);
|
||||
generics
|
||||
@@ -128,38 +149,51 @@ mod struct_info {
|
||||
ref builder_name,
|
||||
..
|
||||
} = *self;
|
||||
let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
|
||||
let (impl_generics, ty_generics, where_clause) =
|
||||
self.generics.split_for_impl();
|
||||
let all_fields_param = syn::GenericParam::Type(
|
||||
syn::Ident::new("PropFields", proc_macro2::Span::call_site()).into(),
|
||||
syn::Ident::new("PropFields", proc_macro2::Span::call_site())
|
||||
.into(),
|
||||
);
|
||||
let b_generics = self.modify_generics(|g| {
|
||||
g.params.insert(0, all_fields_param.clone());
|
||||
});
|
||||
let empties_tuple = type_tuple(self.included_fields().map(|_| empty_type()));
|
||||
let generics_with_empty = modify_types_generics_hack(&ty_generics, |args| {
|
||||
args.insert(0, syn::GenericArgument::Type(empties_tuple.clone().into()));
|
||||
});
|
||||
let phantom_generics = self.generics.params.iter().map(|param| match param {
|
||||
syn::GenericParam::Lifetime(lifetime) => {
|
||||
let lifetime = &lifetime.lifetime;
|
||||
quote!(::core::marker::PhantomData<&#lifetime ()>)
|
||||
}
|
||||
syn::GenericParam::Type(ty) => {
|
||||
let ty = &ty.ident;
|
||||
quote!(::core::marker::PhantomData<#ty>)
|
||||
}
|
||||
syn::GenericParam::Const(_cnst) => {
|
||||
quote!()
|
||||
}
|
||||
});
|
||||
let builder_method_doc = match self.builder_attr.builder_method_doc {
|
||||
let empties_tuple =
|
||||
type_tuple(self.included_fields().map(|_| empty_type()));
|
||||
let generics_with_empty =
|
||||
modify_types_generics_hack(&ty_generics, |args| {
|
||||
args.insert(
|
||||
0,
|
||||
syn::GenericArgument::Type(
|
||||
empties_tuple.clone().into(),
|
||||
),
|
||||
);
|
||||
});
|
||||
let phantom_generics =
|
||||
self.generics.params.iter().map(|param| match param {
|
||||
syn::GenericParam::Lifetime(lifetime) => {
|
||||
let lifetime = &lifetime.lifetime;
|
||||
quote!(::core::marker::PhantomData<&#lifetime ()>)
|
||||
}
|
||||
syn::GenericParam::Type(ty) => {
|
||||
let ty = &ty.ident;
|
||||
quote!(::core::marker::PhantomData<#ty>)
|
||||
}
|
||||
syn::GenericParam::Const(_cnst) => {
|
||||
quote!()
|
||||
}
|
||||
});
|
||||
let builder_method_doc = match self.builder_attr.builder_method_doc
|
||||
{
|
||||
Some(ref doc) => quote!(#doc),
|
||||
None => {
|
||||
let doc = format!(
|
||||
"
|
||||
Create a builder for building `{name}`.
|
||||
On the builder, call {setters} to set the values of the fields.
|
||||
Finally, call `.build()` to create the instance of `{name}`.
|
||||
On the builder, call {setters} to set the values of the \
|
||||
fields.
|
||||
Finally, call `.build()` to create the instance of \
|
||||
`{name}`.
|
||||
",
|
||||
name = self.name,
|
||||
setters = {
|
||||
@@ -172,7 +206,8 @@ mod struct_info {
|
||||
} else {
|
||||
write!(&mut result, ", ").unwrap();
|
||||
}
|
||||
write!(&mut result, "`.{}(...)`", field.name).unwrap();
|
||||
write!(&mut result, "`.{}(...)`", field.name)
|
||||
.unwrap();
|
||||
if field.builder_attr.default.is_some() {
|
||||
write!(&mut result, "(optional)").unwrap();
|
||||
}
|
||||
@@ -188,8 +223,9 @@ mod struct_info {
|
||||
Some(ref doc) => quote!(#[doc = #doc]),
|
||||
None => {
|
||||
let doc = format!(
|
||||
"Builder for [`{name}`] instances.\n\nSee [`{name}::builder()`] for more info."
|
||||
);
|
||||
"Builder for [`{name}`] instances.\n\nSee \
|
||||
[`{name}::builder()`] for more info."
|
||||
);
|
||||
quote!(#[doc = #doc])
|
||||
}
|
||||
}
|
||||
@@ -197,8 +233,11 @@ mod struct_info {
|
||||
quote!(#[doc(hidden)])
|
||||
};
|
||||
|
||||
let (b_generics_impl, b_generics_ty, b_generics_where_extras_predicates) =
|
||||
b_generics.split_for_impl();
|
||||
let (
|
||||
b_generics_impl,
|
||||
b_generics_ty,
|
||||
b_generics_where_extras_predicates,
|
||||
) = b_generics.split_for_impl();
|
||||
let mut b_generics_where: syn::WhereClause = syn::parse2(quote! {
|
||||
where PropFields: Clone
|
||||
})?;
|
||||
@@ -266,7 +305,10 @@ mod struct_info {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn field_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
|
||||
pub fn field_impl(
|
||||
&self,
|
||||
field: &FieldInfo,
|
||||
) -> Result<TokenStream, Error> {
|
||||
let StructInfo {
|
||||
ref builder_name, ..
|
||||
} = *self;
|
||||
@@ -296,7 +338,9 @@ mod struct_info {
|
||||
syn::parse(quote!(#ident).into()).unwrap()
|
||||
}
|
||||
syn::GenericParam::Lifetime(lifetime_def) => {
|
||||
syn::GenericArgument::Lifetime(lifetime_def.lifetime.clone())
|
||||
syn::GenericArgument::Lifetime(
|
||||
lifetime_def.lifetime.clone(),
|
||||
)
|
||||
}
|
||||
syn::GenericParam::Const(const_param) => {
|
||||
let ident = const_param.ident.clone();
|
||||
@@ -319,11 +363,17 @@ mod struct_info {
|
||||
.elems
|
||||
.push_value(f.tuplized_type_ty_param());
|
||||
} else {
|
||||
g.params
|
||||
.insert(index_after_lifetime_in_generics, f.generic_ty_param());
|
||||
g.params.insert(
|
||||
index_after_lifetime_in_generics,
|
||||
f.generic_ty_param(),
|
||||
);
|
||||
let generic_argument: syn::Type = f.type_ident();
|
||||
ty_generics_tuple.elems.push_value(generic_argument.clone());
|
||||
target_generics_tuple.elems.push_value(generic_argument);
|
||||
ty_generics_tuple
|
||||
.elems
|
||||
.push_value(generic_argument.clone());
|
||||
target_generics_tuple
|
||||
.elems
|
||||
.push_value(generic_argument);
|
||||
}
|
||||
ty_generics_tuple.elems.push_punct(Default::default());
|
||||
target_generics_tuple.elems.push_punct(Default::default());
|
||||
@@ -362,26 +412,29 @@ mod struct_info {
|
||||
} else {
|
||||
field_type
|
||||
};
|
||||
let (arg_type, arg_expr) = if field.builder_attr.setter.auto_into.is_some() {
|
||||
(
|
||||
quote!(impl ::core::convert::Into<#arg_type>),
|
||||
quote!(#field_name.into()),
|
||||
)
|
||||
} else {
|
||||
(quote!(#arg_type), quote!(#field_name))
|
||||
};
|
||||
|
||||
let (param_list, arg_expr) =
|
||||
if let Some(transform) = &field.builder_attr.setter.transform {
|
||||
let params = transform.params.iter().map(|(pat, ty)| quote!(#pat: #ty));
|
||||
let body = &transform.body;
|
||||
(quote!(#(#params),*), quote!({ #body }))
|
||||
} else if field.builder_attr.setter.strip_option.is_some() {
|
||||
(quote!(#field_name: #arg_type), quote!(Some(#arg_expr)))
|
||||
let (arg_type, arg_expr) =
|
||||
if field.builder_attr.setter.auto_into.is_some() {
|
||||
(
|
||||
quote!(impl ::core::convert::Into<#arg_type>),
|
||||
quote!(#field_name.into()),
|
||||
)
|
||||
} else {
|
||||
(quote!(#field_name: #arg_type), arg_expr)
|
||||
(quote!(#arg_type), quote!(#field_name))
|
||||
};
|
||||
|
||||
let (param_list, arg_expr) = if let Some(transform) =
|
||||
&field.builder_attr.setter.transform
|
||||
{
|
||||
let params =
|
||||
transform.params.iter().map(|(pat, ty)| quote!(#pat: #ty));
|
||||
let body = &transform.body;
|
||||
(quote!(#(#params),*), quote!({ #body }))
|
||||
} else if field.builder_attr.setter.strip_option.is_some() {
|
||||
(quote!(#field_name: #arg_type), quote!(Some(#arg_expr)))
|
||||
} else {
|
||||
(quote!(#field_name: #arg_type), arg_expr)
|
||||
};
|
||||
|
||||
let repeated_fields_error_type_name = syn::Ident::new(
|
||||
&format!(
|
||||
"{}_Error_Repeated_field_{}",
|
||||
@@ -390,7 +443,8 @@ mod struct_info {
|
||||
),
|
||||
proc_macro2::Span::call_site(),
|
||||
);
|
||||
let repeated_fields_error_message = format!("Repeated field {field_name}");
|
||||
let repeated_fields_error_message =
|
||||
format!("Repeated field {field_name}");
|
||||
|
||||
Ok(quote! {
|
||||
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||
@@ -421,7 +475,10 @@ mod struct_info {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn required_field_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
|
||||
pub fn required_field_impl(
|
||||
&self,
|
||||
field: &FieldInfo,
|
||||
) -> Result<TokenStream, Error> {
|
||||
let StructInfo {
|
||||
ref name,
|
||||
ref builder_name,
|
||||
@@ -442,7 +499,9 @@ mod struct_info {
|
||||
syn::parse(quote!(#ident).into()).unwrap()
|
||||
}
|
||||
syn::GenericParam::Lifetime(lifetime_def) => {
|
||||
syn::GenericArgument::Lifetime(lifetime_def.lifetime.clone())
|
||||
syn::GenericArgument::Lifetime(
|
||||
lifetime_def.lifetime.clone(),
|
||||
)
|
||||
}
|
||||
syn::GenericParam::Const(const_param) => {
|
||||
let ident = &const_param.ident;
|
||||
@@ -464,11 +523,14 @@ mod struct_info {
|
||||
// whether or not `f` is set.
|
||||
assert!(
|
||||
f.ordinal != field.ordinal,
|
||||
"`required_field_impl` called for optional field {}",
|
||||
"`required_field_impl` called for optional field \
|
||||
{}",
|
||||
field.name
|
||||
);
|
||||
g.params
|
||||
.insert(index_after_lifetime_in_generics, f.generic_ty_param());
|
||||
g.params.insert(
|
||||
index_after_lifetime_in_generics,
|
||||
f.generic_ty_param(),
|
||||
);
|
||||
builder_generics_tuple.elems.push_value(f.type_ident());
|
||||
} else if f.ordinal < field.ordinal {
|
||||
// Only add a `build` method that warns about missing `field` if `f` is set.
|
||||
@@ -484,8 +546,10 @@ mod struct_info {
|
||||
// missing we will show a warning for `field` and
|
||||
// not for `f` - which means this warning should appear whether
|
||||
// or not `f` is set.
|
||||
g.params
|
||||
.insert(index_after_lifetime_in_generics, f.generic_ty_param());
|
||||
g.params.insert(
|
||||
index_after_lifetime_in_generics,
|
||||
f.generic_ty_param(),
|
||||
);
|
||||
builder_generics_tuple.elems.push_value(f.type_ident());
|
||||
}
|
||||
|
||||
@@ -512,7 +576,8 @@ mod struct_info {
|
||||
),
|
||||
proc_macro2::Span::call_site(),
|
||||
);
|
||||
let early_build_error_message = format!("Missing required field {field_name}");
|
||||
let early_build_error_message =
|
||||
format!("Missing required field {field_name}");
|
||||
|
||||
Ok(quote! {
|
||||
#[doc(hidden)]
|
||||
@@ -551,24 +616,31 @@ mod struct_info {
|
||||
lifetimes: None,
|
||||
modifier: syn::TraitBoundModifier::None,
|
||||
path: syn::PathSegment {
|
||||
ident: self.conversion_helper_trait_name.clone(),
|
||||
ident: self
|
||||
.conversion_helper_trait_name
|
||||
.clone(),
|
||||
arguments: syn::PathArguments::AngleBracketed(
|
||||
syn::AngleBracketedGenericArguments {
|
||||
colon2_token: None,
|
||||
lt_token: Default::default(),
|
||||
args: make_punctuated_single(syn::GenericArgument::Type(
|
||||
field.ty.clone(),
|
||||
)),
|
||||
args: make_punctuated_single(
|
||||
syn::GenericArgument::Type(
|
||||
field.ty.clone(),
|
||||
),
|
||||
),
|
||||
gt_token: Default::default(),
|
||||
},
|
||||
),
|
||||
}
|
||||
.into(),
|
||||
};
|
||||
let mut generic_param: syn::TypeParam = field.generic_ident.clone().into();
|
||||
let mut generic_param: syn::TypeParam =
|
||||
field.generic_ident.clone().into();
|
||||
generic_param.bounds.push(trait_ref.into());
|
||||
g.params
|
||||
.insert(index_after_lifetime_in_generics, generic_param.into());
|
||||
g.params.insert(
|
||||
index_after_lifetime_in_generics,
|
||||
generic_param.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -576,21 +648,22 @@ mod struct_info {
|
||||
|
||||
let (_, ty_generics, where_clause) = self.generics.split_for_impl();
|
||||
|
||||
let modified_ty_generics = modify_types_generics_hack(&ty_generics, |args| {
|
||||
args.insert(
|
||||
0,
|
||||
syn::GenericArgument::Type(
|
||||
type_tuple(self.included_fields().map(|field| {
|
||||
if field.builder_attr.default.is_some() {
|
||||
field.type_ident()
|
||||
} else {
|
||||
field.tuplized_type_ty_param()
|
||||
}
|
||||
}))
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
});
|
||||
let modified_ty_generics =
|
||||
modify_types_generics_hack(&ty_generics, |args| {
|
||||
args.insert(
|
||||
0,
|
||||
syn::GenericArgument::Type(
|
||||
type_tuple(self.included_fields().map(|field| {
|
||||
if field.builder_attr.default.is_some() {
|
||||
field.type_ident()
|
||||
} else {
|
||||
field.tuplized_type_ty_param()
|
||||
}
|
||||
}))
|
||||
.into(),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
let descructuring = self.included_fields().map(|f| f.name);
|
||||
|
||||
@@ -620,8 +693,10 @@ mod struct_info {
|
||||
None => {
|
||||
// I'd prefer “a” or “an” to “its”, but determining which is grammatically
|
||||
// correct is roughly impossible.
|
||||
let doc =
|
||||
format!("Finalise the builder and create its [`{name}`] instance");
|
||||
let doc = format!(
|
||||
"Finalise the builder and create its [`{name}`] \
|
||||
instance"
|
||||
);
|
||||
quote!(#[doc = #doc])
|
||||
}
|
||||
}
|
||||
@@ -668,7 +743,9 @@ mod struct_info {
|
||||
pub fn new(attrs: &[syn::Attribute]) -> Result<TypeBuilderAttr, Error> {
|
||||
let mut result = TypeBuilderAttr::default();
|
||||
for attr in attrs {
|
||||
if path_to_single_string(&attr.path).as_deref() != Some("builder") {
|
||||
if path_to_single_string(&attr.path).as_deref()
|
||||
!= Some("builder")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -687,7 +764,10 @@ mod struct_info {
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::new_spanned(attr.tokens.clone(), "Expected (<...>)"));
|
||||
return Err(Error::new_spanned(
|
||||
attr.tokens.clone(),
|
||||
"Expected (<...>)",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -698,8 +778,14 @@ mod struct_info {
|
||||
fn apply_meta(&mut self, expr: syn::Expr) -> Result<(), Error> {
|
||||
match expr {
|
||||
syn::Expr::Assign(assign) => {
|
||||
let name = expr_to_single_string(&assign.left)
|
||||
.ok_or_else(|| Error::new_spanned(&assign.left, "Expected identifier"))?;
|
||||
let name = expr_to_single_string(&assign.left).ok_or_else(
|
||||
|| {
|
||||
Error::new_spanned(
|
||||
&assign.left,
|
||||
"Expected identifier",
|
||||
)
|
||||
},
|
||||
)?;
|
||||
match name.as_str() {
|
||||
"builder_method_doc" => {
|
||||
self.builder_method_doc = Some(*assign.right);
|
||||
@@ -722,8 +808,10 @@ mod struct_info {
|
||||
}
|
||||
}
|
||||
syn::Expr::Path(path) => {
|
||||
let name = path_to_single_string(&path.path)
|
||||
.ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
|
||||
let name =
|
||||
path_to_single_string(&path.path).ok_or_else(|| {
|
||||
Error::new_spanned(&path, "Expected identifier")
|
||||
})?;
|
||||
match name.as_str() {
|
||||
"doc" => {
|
||||
self.doc = true;
|
||||
@@ -736,19 +824,22 @@ mod struct_info {
|
||||
}
|
||||
}
|
||||
syn::Expr::Call(call) => {
|
||||
let subsetting_name = if let syn::Expr::Path(path) = &*call.func {
|
||||
path_to_single_string(&path.path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.ok_or_else(|| {
|
||||
let call_func = &call.func;
|
||||
let call_func = quote!(#call_func);
|
||||
Error::new_spanned(
|
||||
&call.func,
|
||||
format!("Illegal builder setting group {call_func}"),
|
||||
)
|
||||
})?;
|
||||
let subsetting_name =
|
||||
if let syn::Expr::Path(path) = &*call.func {
|
||||
path_to_single_string(&path.path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.ok_or_else(|| {
|
||||
let call_func = &call.func;
|
||||
let call_func = quote!(#call_func);
|
||||
Error::new_spanned(
|
||||
&call.func,
|
||||
format!(
|
||||
"Illegal builder setting group {call_func}"
|
||||
),
|
||||
)
|
||||
})?;
|
||||
match subsetting_name.as_str() {
|
||||
"field_defaults" => {
|
||||
for arg in call.args {
|
||||
@@ -758,7 +849,10 @@ mod struct_info {
|
||||
}
|
||||
_ => Err(Error::new_spanned(
|
||||
&call.func,
|
||||
format!("Illegal builder setting group name {subsetting_name}"),
|
||||
format!(
|
||||
"Illegal builder setting group name \
|
||||
{subsetting_name}"
|
||||
),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -769,14 +863,13 @@ mod struct_info {
|
||||
}
|
||||
|
||||
mod field_info {
|
||||
use super::util::{
|
||||
expr_to_single_string, ident_to_type, path_to_single_string,
|
||||
strip_raw_ident_prefix,
|
||||
};
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::quote;
|
||||
use syn::parse::Error;
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
use super::util::{
|
||||
expr_to_single_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix,
|
||||
};
|
||||
use syn::{parse::Error, spanned::Spanned};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FieldInfo<'a> {
|
||||
@@ -798,7 +891,10 @@ mod field_info {
|
||||
ordinal,
|
||||
name,
|
||||
generic_ident: syn::Ident::new(
|
||||
&format!("__{}", strip_raw_ident_prefix(name.to_string())),
|
||||
&format!(
|
||||
"__{}",
|
||||
strip_raw_ident_prefix(name.to_string())
|
||||
),
|
||||
Span::call_site(),
|
||||
),
|
||||
ty: &field.ty,
|
||||
@@ -843,12 +939,16 @@ mod field_info {
|
||||
return None;
|
||||
}
|
||||
let generic_params =
|
||||
if let syn::PathArguments::AngleBracketed(generic_params) = &segment.arguments {
|
||||
if let syn::PathArguments::AngleBracketed(generic_params) =
|
||||
&segment.arguments
|
||||
{
|
||||
generic_params
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
if let syn::GenericArgument::Type(ty) = generic_params.args.first()? {
|
||||
if let syn::GenericArgument::Type(ty) =
|
||||
generic_params.args.first()?
|
||||
{
|
||||
Some(ty)
|
||||
} else {
|
||||
None
|
||||
@@ -874,7 +974,9 @@ mod field_info {
|
||||
impl FieldBuilderAttr {
|
||||
pub fn with(mut self, attrs: &[syn::Attribute]) -> Result<Self, Error> {
|
||||
for attr in attrs {
|
||||
if path_to_single_string(&attr.path).as_deref() != Some("builder") {
|
||||
if path_to_single_string(&attr.path).as_deref()
|
||||
!= Some("builder")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -893,7 +995,10 @@ mod field_info {
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::new_spanned(attr.tokens.clone(), "Expected (<...>)"));
|
||||
return Err(Error::new_spanned(
|
||||
attr.tokens.clone(),
|
||||
"Expected (<...>)",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -906,8 +1011,14 @@ mod field_info {
|
||||
pub fn apply_meta(&mut self, expr: syn::Expr) -> Result<(), Error> {
|
||||
match expr {
|
||||
syn::Expr::Assign(assign) => {
|
||||
let name = expr_to_single_string(&assign.left)
|
||||
.ok_or_else(|| Error::new_spanned(&assign.left, "Expected identifier"))?;
|
||||
let name = expr_to_single_string(&assign.left).ok_or_else(
|
||||
|| {
|
||||
Error::new_spanned(
|
||||
&assign.left,
|
||||
"Expected identifier",
|
||||
)
|
||||
},
|
||||
)?;
|
||||
match name.as_str() {
|
||||
"default" => {
|
||||
self.default = Some(*assign.right);
|
||||
@@ -920,13 +1031,23 @@ mod field_info {
|
||||
}) = *assign.right
|
||||
{
|
||||
use std::str::FromStr;
|
||||
let tokenized_code = TokenStream::from_str(&code.value())?;
|
||||
let tokenized_code =
|
||||
TokenStream::from_str(&code.value())?;
|
||||
self.default = Some(
|
||||
syn::parse(tokenized_code.into())
|
||||
.map_err(|e| Error::new_spanned(code, format!("{e}")))?,
|
||||
syn::parse(tokenized_code.into()).map_err(
|
||||
|e| {
|
||||
Error::new_spanned(
|
||||
code,
|
||||
format!("{e}"),
|
||||
)
|
||||
},
|
||||
)?,
|
||||
);
|
||||
} else {
|
||||
return Err(Error::new_spanned(assign.right, "Expected string"));
|
||||
return Err(Error::new_spanned(
|
||||
assign.right,
|
||||
"Expected string",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -937,13 +1058,18 @@ mod field_info {
|
||||
}
|
||||
}
|
||||
syn::Expr::Path(path) => {
|
||||
let name = path_to_single_string(&path.path)
|
||||
.ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
|
||||
let name =
|
||||
path_to_single_string(&path.path).ok_or_else(|| {
|
||||
Error::new_spanned(&path, "Expected identifier")
|
||||
})?;
|
||||
match name.as_str() {
|
||||
"default" => {
|
||||
self.default = Some(
|
||||
syn::parse(quote!(::core::default::Default::default()).into())
|
||||
.unwrap(),
|
||||
syn::parse(
|
||||
quote!(::core::default::Default::default())
|
||||
.into(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -954,19 +1080,22 @@ mod field_info {
|
||||
}
|
||||
}
|
||||
syn::Expr::Call(call) => {
|
||||
let subsetting_name = if let syn::Expr::Path(path) = &*call.func {
|
||||
path_to_single_string(&path.path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.ok_or_else(|| {
|
||||
let call_func = &call.func;
|
||||
let call_func = quote!(#call_func);
|
||||
Error::new_spanned(
|
||||
&call.func,
|
||||
format!("Illegal builder setting group {call_func}"),
|
||||
)
|
||||
})?;
|
||||
let subsetting_name =
|
||||
if let syn::Expr::Path(path) = &*call.func {
|
||||
path_to_single_string(&path.path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.ok_or_else(|| {
|
||||
let call_func = &call.func;
|
||||
let call_func = quote!(#call_func);
|
||||
Error::new_spanned(
|
||||
&call.func,
|
||||
format!(
|
||||
"Illegal builder setting group {call_func}"
|
||||
),
|
||||
)
|
||||
})?;
|
||||
match subsetting_name.as_ref() {
|
||||
"setter" => {
|
||||
for arg in call.args {
|
||||
@@ -976,7 +1105,10 @@ mod field_info {
|
||||
}
|
||||
_ => Err(Error::new_spanned(
|
||||
&call.func,
|
||||
format!("Illegal builder setting group name {subsetting_name}"),
|
||||
format!(
|
||||
"Illegal builder setting group name \
|
||||
{subsetting_name}"
|
||||
),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -987,13 +1119,18 @@ mod field_info {
|
||||
}) => {
|
||||
if let syn::Expr::Path(path) = *expr {
|
||||
let name = path_to_single_string(&path.path)
|
||||
.ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
|
||||
.ok_or_else(|| {
|
||||
Error::new_spanned(&path, "Expected identifier")
|
||||
})?;
|
||||
match name.as_str() {
|
||||
"default" => {
|
||||
self.default = None;
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(Error::new_spanned(path, "Unknown setting".to_owned())),
|
||||
_ => Err(Error::new_spanned(
|
||||
path,
|
||||
"Unknown setting".to_owned(),
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
Err(Error::new_spanned(
|
||||
@@ -1010,15 +1147,22 @@ mod field_info {
|
||||
if let (Some(skip), None) = (&self.setter.skip, &self.default) {
|
||||
return Err(Error::new(
|
||||
*skip,
|
||||
"#[builder(skip)] must be accompanied by default or default_code",
|
||||
"#[builder(skip)] must be accompanied by default or \
|
||||
default_code",
|
||||
));
|
||||
}
|
||||
|
||||
if let (Some(strip_option), Some(transform)) =
|
||||
(&self.setter.strip_option, &self.setter.transform)
|
||||
{
|
||||
let mut error = Error::new(transform.span, "transform conflicts with strip_option");
|
||||
error.combine(Error::new(*strip_option, "strip_option set here"));
|
||||
let mut error = Error::new(
|
||||
transform.span,
|
||||
"transform conflicts with strip_option",
|
||||
);
|
||||
error.combine(Error::new(
|
||||
*strip_option,
|
||||
"strip_option set here",
|
||||
));
|
||||
return Err(error);
|
||||
}
|
||||
Ok(())
|
||||
@@ -1029,8 +1173,14 @@ mod field_info {
|
||||
fn apply_meta(&mut self, expr: syn::Expr) -> Result<(), Error> {
|
||||
match expr {
|
||||
syn::Expr::Assign(assign) => {
|
||||
let name = expr_to_single_string(&assign.left)
|
||||
.ok_or_else(|| Error::new_spanned(&assign.left, "Expected identifier"))?;
|
||||
let name = expr_to_single_string(&assign.left).ok_or_else(
|
||||
|| {
|
||||
Error::new_spanned(
|
||||
&assign.left,
|
||||
"Expected identifier",
|
||||
)
|
||||
},
|
||||
)?;
|
||||
match name.as_str() {
|
||||
"doc" => {
|
||||
self.doc = Some(*assign.right);
|
||||
@@ -1040,8 +1190,10 @@ mod field_info {
|
||||
// if self.strip_option.is_some() {
|
||||
// return Err(Error::new(assign.span(), "Illegal setting - transform
|
||||
// conflicts with strip_option")); }
|
||||
self.transform =
|
||||
Some(parse_transform_closure(assign.left.span(), &assign.right)?);
|
||||
self.transform = Some(parse_transform_closure(
|
||||
assign.left.span(),
|
||||
&assign.right,
|
||||
)?);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(Error::new_spanned(
|
||||
@@ -1051,8 +1203,10 @@ mod field_info {
|
||||
}
|
||||
}
|
||||
syn::Expr::Path(path) => {
|
||||
let name = path_to_single_string(&path.path)
|
||||
.ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
|
||||
let name =
|
||||
path_to_single_string(&path.path).ok_or_else(|| {
|
||||
Error::new_spanned(&path, "Expected identifier")
|
||||
})?;
|
||||
macro_rules! handle_fields {
|
||||
( $( $flag:expr, $field:ident, $already:expr, $checks:expr; )* ) => {
|
||||
match name.as_str() {
|
||||
@@ -1093,7 +1247,9 @@ mod field_info {
|
||||
}) => {
|
||||
if let syn::Expr::Path(path) = *expr {
|
||||
let name = path_to_single_string(&path.path)
|
||||
.ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
|
||||
.ok_or_else(|| {
|
||||
Error::new_spanned(&path, "Expected identifier")
|
||||
})?;
|
||||
match name.as_str() {
|
||||
"doc" => {
|
||||
self.doc = None;
|
||||
@@ -1111,7 +1267,10 @@ mod field_info {
|
||||
self.strip_option = None;
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(Error::new_spanned(path, "Unknown setting".to_owned())),
|
||||
_ => Err(Error::new_spanned(
|
||||
path,
|
||||
"Unknown setting".to_owned(),
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
Err(Error::new_spanned(
|
||||
@@ -1132,16 +1291,25 @@ mod field_info {
|
||||
span: Span,
|
||||
}
|
||||
|
||||
fn parse_transform_closure(span: Span, expr: &syn::Expr) -> Result<Transform, Error> {
|
||||
fn parse_transform_closure(
|
||||
span: Span,
|
||||
expr: &syn::Expr,
|
||||
) -> Result<Transform, Error> {
|
||||
let closure = match expr {
|
||||
syn::Expr::Closure(closure) => closure,
|
||||
_ => return Err(Error::new_spanned(expr, "Expected closure")),
|
||||
};
|
||||
if let Some(kw) = &closure.asyncness {
|
||||
return Err(Error::new(kw.span, "Transform closure cannot be async"));
|
||||
return Err(Error::new(
|
||||
kw.span,
|
||||
"Transform closure cannot be async",
|
||||
));
|
||||
}
|
||||
if let Some(kw) = &closure.capture {
|
||||
return Err(Error::new(kw.span, "Transform closure cannot be move"));
|
||||
return Err(Error::new(
|
||||
kw.span,
|
||||
"Transform closure cannot be move",
|
||||
));
|
||||
}
|
||||
|
||||
let params = closure
|
||||
@@ -1216,7 +1384,9 @@ mod util {
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn type_tuple(elems: impl Iterator<Item = syn::Type>) -> syn::TypeTuple {
|
||||
pub fn type_tuple(
|
||||
elems: impl Iterator<Item = syn::Type>,
|
||||
) -> syn::TypeTuple {
|
||||
let mut result = syn::TypeTuple {
|
||||
paren_token: Default::default(),
|
||||
elems: elems.collect(),
|
||||
@@ -1234,7 +1404,9 @@ mod util {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_punctuated_single<T, P: Default>(value: T) -> syn::punctuated::Punctuated<T, P> {
|
||||
pub fn make_punctuated_single<T, P: Default>(
|
||||
value: T,
|
||||
) -> syn::punctuated::Punctuated<T, P> {
|
||||
let mut punctuated = syn::punctuated::Punctuated::new();
|
||||
punctuated.push(value);
|
||||
punctuated
|
||||
@@ -1245,17 +1417,21 @@ mod util {
|
||||
mut mutator: F,
|
||||
) -> syn::AngleBracketedGenericArguments
|
||||
where
|
||||
F: FnMut(&mut syn::punctuated::Punctuated<syn::GenericArgument, syn::token::Comma>),
|
||||
F: FnMut(
|
||||
&mut syn::punctuated::Punctuated<
|
||||
syn::GenericArgument,
|
||||
syn::token::Comma,
|
||||
>,
|
||||
),
|
||||
{
|
||||
let mut abga: syn::AngleBracketedGenericArguments =
|
||||
syn::parse(ty_generics.clone().into_token_stream().into()).unwrap_or_else(|_| {
|
||||
syn::AngleBracketedGenericArguments {
|
||||
syn::parse(ty_generics.clone().into_token_stream().into())
|
||||
.unwrap_or_else(|_| syn::AngleBracketedGenericArguments {
|
||||
colon2_token: None,
|
||||
lt_token: Default::default(),
|
||||
args: Default::default(),
|
||||
gt_token: Default::default(),
|
||||
}
|
||||
});
|
||||
});
|
||||
mutator(&mut abga.args);
|
||||
abga
|
||||
}
|
||||
|
||||
@@ -23,7 +23,10 @@ fn fn_arg_is_cx(f: &syn::FnArg) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Result<TokenStream2> {
|
||||
pub fn server_macro_impl(
|
||||
args: proc_macro::TokenStream,
|
||||
s: TokenStream2,
|
||||
) -> Result<TokenStream2> {
|
||||
let ServerFnName {
|
||||
struct_name,
|
||||
prefix,
|
||||
@@ -57,17 +60,21 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
||||
|
||||
let fields = body.inputs.iter().filter(|f| !fn_arg_is_cx(f)).map(|f| {
|
||||
let typed_arg = match f {
|
||||
FnArg::Receiver(_) => panic!("cannot use receiver types in server function macro"),
|
||||
FnArg::Receiver(_) => {
|
||||
panic!("cannot use receiver types in server function macro")
|
||||
}
|
||||
FnArg::Typed(t) => t,
|
||||
};
|
||||
quote! { pub #typed_arg }
|
||||
});
|
||||
|
||||
let cx_arg = body
|
||||
.inputs
|
||||
.iter()
|
||||
.next()
|
||||
.and_then(|f| if fn_arg_is_cx(f) { Some(f) } else { None });
|
||||
let cx_arg = body.inputs.iter().next().and_then(|f| {
|
||||
if fn_arg_is_cx(f) {
|
||||
Some(f)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let cx_assign_statement = if let Some(FnArg::Typed(arg)) = cx_arg {
|
||||
if let Pat::Ident(id) = &*arg.pat {
|
||||
quote! {
|
||||
@@ -88,7 +95,9 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
||||
|
||||
let fn_args = body.inputs.iter().map(|f| {
|
||||
let typed_arg = match f {
|
||||
FnArg::Receiver(_) => panic!("cannot use receiver types in server function macro"),
|
||||
FnArg::Receiver(_) => {
|
||||
panic!("cannot use receiver types in server function macro")
|
||||
}
|
||||
FnArg::Typed(t) => t,
|
||||
};
|
||||
let is_cx = fn_arg_is_cx(f);
|
||||
@@ -124,10 +133,14 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
||||
|
||||
let output_ty = if let syn::Type::Path(pat) = &return_ty {
|
||||
if pat.path.segments[0].ident == "Result" {
|
||||
if let PathArguments::AngleBracketed(args) = &pat.path.segments[0].arguments {
|
||||
if let PathArguments::AngleBracketed(args) =
|
||||
&pat.path.segments[0].arguments
|
||||
{
|
||||
&args.args[0]
|
||||
} else {
|
||||
panic!("server functions should return Result<T, ServerFnError>");
|
||||
panic!(
|
||||
"server functions should return Result<T, ServerFnError>"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
panic!("server functions should return Result<T, ServerFnError>");
|
||||
|
||||
@@ -156,7 +156,12 @@ pub(crate) fn render_view(
|
||||
}
|
||||
}
|
||||
1 => root_node_to_tokens_ssr(cx, &nodes[0], global_class),
|
||||
_ => fragment_to_tokens_ssr(cx, Span::call_site(), nodes, global_class),
|
||||
_ => fragment_to_tokens_ssr(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
nodes,
|
||||
global_class,
|
||||
),
|
||||
}
|
||||
} else {
|
||||
match nodes.len() {
|
||||
@@ -185,9 +190,12 @@ fn root_node_to_tokens_ssr(
|
||||
global_class: Option<&TokenTree>,
|
||||
) -> TokenStream {
|
||||
match node {
|
||||
Node::Fragment(fragment) => {
|
||||
fragment_to_tokens_ssr(cx, Span::call_site(), &fragment.children, global_class)
|
||||
}
|
||||
Node::Fragment(fragment) => fragment_to_tokens_ssr(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
&fragment.children,
|
||||
global_class,
|
||||
),
|
||||
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => quote! {},
|
||||
Node::Text(node) => {
|
||||
let value = node.value.as_ref();
|
||||
@@ -202,7 +210,9 @@ fn root_node_to_tokens_ssr(
|
||||
#value
|
||||
}
|
||||
}
|
||||
Node::Element(node) => root_element_to_tokens_ssr(cx, node, global_class),
|
||||
Node::Element(node) => {
|
||||
root_element_to_tokens_ssr(cx, node, global_class)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,8 +277,9 @@ fn root_element_to_tokens_ssr(
|
||||
let typed_element_name = if is_custom_element {
|
||||
Ident::new("Custom", node.name.span())
|
||||
} else {
|
||||
let camel_cased =
|
||||
camel_case_tag_name(&tag_name.replace("svg::", "").replace("math::", ""));
|
||||
let camel_cased = camel_case_tag_name(
|
||||
&tag_name.replace("svg::", "").replace("math::", ""),
|
||||
);
|
||||
Ident::new(&camel_cased, node.name.span())
|
||||
};
|
||||
let typed_element_name = if is_svg_element(&tag_name) {
|
||||
@@ -324,7 +335,13 @@ fn element_to_tokens_ssr(
|
||||
|
||||
for attr in &node.attributes {
|
||||
if let Node::Attribute(attr) = attr {
|
||||
inner_html = attribute_to_tokens_ssr(cx, attr, template, holes, exprs_for_compiler);
|
||||
inner_html = attribute_to_tokens_ssr(
|
||||
cx,
|
||||
attr,
|
||||
template,
|
||||
holes,
|
||||
exprs_for_compiler,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,7 +393,9 @@ fn element_to_tokens_ssr(
|
||||
),
|
||||
Node::Text(text) => {
|
||||
if let Some(value) = value_to_string(&text.value) {
|
||||
template.push_str(&html_escape::encode_safe(&value));
|
||||
template.push_str(&html_escape::encode_safe(
|
||||
&value,
|
||||
));
|
||||
} else {
|
||||
template.push_str("{}");
|
||||
let value = text.value.as_ref();
|
||||
@@ -439,7 +458,9 @@ fn attribute_to_tokens_ssr<'a>(
|
||||
exprs_for_compiler.push(quote! {
|
||||
leptos::ssr_event_listener(#event_type, #handler);
|
||||
})
|
||||
} else if name.strip_prefix("prop:").is_some() || name.strip_prefix("class:").is_some() {
|
||||
} else if name.strip_prefix("prop:").is_some()
|
||||
|| name.strip_prefix("class:").is_some()
|
||||
{
|
||||
// ignore props for SSR
|
||||
// ignore classes: we'll handle these separately
|
||||
} else if name == "inner_html" {
|
||||
@@ -538,7 +559,9 @@ fn set_class_attribute_ssr(
|
||||
if let Node::Attribute(node) = node {
|
||||
let name = node.key.to_string();
|
||||
if name == "class" {
|
||||
return if let Some((_, name, value)) = fancy_class_name(&name, cx, node) {
|
||||
return if let Some((_, name, value)) =
|
||||
fancy_class_name(&name, cx, node)
|
||||
{
|
||||
let span = node.key.span();
|
||||
Some((span, name, value))
|
||||
} else {
|
||||
@@ -667,7 +690,9 @@ fn node_to_tokens(
|
||||
quote! { #value }
|
||||
}
|
||||
Node::Attribute(node) => attribute_to_tokens(cx, node),
|
||||
Node::Element(node) => element_to_tokens(cx, node, parent_type, global_class),
|
||||
Node::Element(node) => {
|
||||
element_to_tokens(cx, node, parent_type, global_class)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -705,7 +730,9 @@ fn element_to_tokens(
|
||||
}
|
||||
TagType::Html => quote! { leptos::leptos_dom::#name(#cx) },
|
||||
TagType::Svg => quote! { leptos::leptos_dom::svg::#name(#cx) },
|
||||
TagType::Math => quote! { leptos::leptos_dom::math::#name(#cx) },
|
||||
TagType::Math => {
|
||||
quote! { leptos::leptos_dom::math::#name(#cx) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let name = &node.name;
|
||||
@@ -749,8 +776,12 @@ fn element_to_tokens(
|
||||
#[allow(unused_braces)] #value
|
||||
}
|
||||
}
|
||||
Node::Element(node) => element_to_tokens(cx, node, parent_type, global_class),
|
||||
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => quote! {},
|
||||
Node::Element(node) => {
|
||||
element_to_tokens(cx, node, parent_type, global_class)
|
||||
}
|
||||
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => {
|
||||
quote! {}
|
||||
}
|
||||
};
|
||||
quote! {
|
||||
.child((#cx, #child))
|
||||
@@ -957,7 +988,8 @@ fn component_to_tokens(
|
||||
let props = attrs
|
||||
.clone()
|
||||
.filter(|attr| {
|
||||
!attr.key.to_string().starts_with("clone:") && !attr.key.to_string().starts_with("on:")
|
||||
!attr.key.to_string().starts_with("clone:")
|
||||
&& !attr.key.to_string().starts_with("on:")
|
||||
})
|
||||
.map(|attr| {
|
||||
let name = &attr.key;
|
||||
@@ -1051,7 +1083,8 @@ fn event_from_attribute_node(
|
||||
attr: &NodeAttribute,
|
||||
force_undelegated: bool,
|
||||
) -> (TokenStream, &Expr) {
|
||||
let event_name = attr.key.to_string().strip_prefix("on:").unwrap().to_owned();
|
||||
let event_name =
|
||||
attr.key.to_string().strip_prefix("on:").unwrap().to_owned();
|
||||
|
||||
let handler = attr
|
||||
.value
|
||||
@@ -1090,7 +1123,10 @@ fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
|
||||
.expect("element needs to have a name"),
|
||||
NodeName::Block(_) => {
|
||||
let span = tag_name.span();
|
||||
proc_macro_error::emit_error!(span, "blocks not allowed in tag-name position");
|
||||
proc_macro_error::emit_error!(
|
||||
span,
|
||||
"blocks not allowed in tag-name position"
|
||||
);
|
||||
Ident::new("", span)
|
||||
}
|
||||
_ => Ident::new(
|
||||
@@ -1291,7 +1327,8 @@ fn fancy_class_name<'a>(
|
||||
};
|
||||
let class_name = &tuple.elems[0];
|
||||
let class_name = if let Expr::Lit(ExprLit {
|
||||
lit: Lit::Str(s), ..
|
||||
lit: Lit::Str(s),
|
||||
..
|
||||
}) = class_name
|
||||
{
|
||||
s.value()
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{runtime::with_runtime, Scope};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
collections::HashMap,
|
||||
};
|
||||
|
||||
use crate::{runtime::with_runtime, Scope};
|
||||
|
||||
/// Provides a context value of type `T` to the current reactive [Scope](crate::Scope)
|
||||
/// and all of its descendants. This can be consumed using [use_context](crate::use_context).
|
||||
///
|
||||
@@ -58,7 +57,8 @@ where
|
||||
|
||||
_ = with_runtime(cx.runtime, |runtime| {
|
||||
let mut contexts = runtime.scope_contexts.borrow_mut();
|
||||
let context = contexts.entry(cx.id).unwrap().or_insert_with(HashMap::new);
|
||||
let context =
|
||||
contexts.entry(cx.id).unwrap().or_insert_with(HashMap::new);
|
||||
context.insert(id, Box::new(value) as Box<dyn Any>);
|
||||
});
|
||||
}
|
||||
@@ -119,21 +119,25 @@ where
|
||||
let contexts = runtime.scope_contexts.borrow();
|
||||
let context = contexts.get(cx.id);
|
||||
context
|
||||
.and_then(|context| context.get(&id).and_then(|val| val.downcast_ref::<T>()))
|
||||
.and_then(|context| {
|
||||
context.get(&id).and_then(|val| val.downcast_ref::<T>())
|
||||
})
|
||||
.cloned()
|
||||
};
|
||||
match local_value {
|
||||
Some(val) => Some(val),
|
||||
None => runtime
|
||||
.scope_parents
|
||||
.borrow()
|
||||
.get(cx.id)
|
||||
.and_then(|parent| {
|
||||
use_context::<T>(Scope {
|
||||
runtime: cx.runtime,
|
||||
id: *parent,
|
||||
None => {
|
||||
runtime
|
||||
.scope_parents
|
||||
.borrow()
|
||||
.get(cx.id)
|
||||
.and_then(|parent| {
|
||||
use_context::<T>(Scope {
|
||||
runtime: cx.runtime,
|
||||
id: *parent,
|
||||
})
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::macros::debug_warn;
|
||||
use crate::runtime::{with_runtime, RuntimeId};
|
||||
use crate::{Runtime, Scope, ScopeProperty};
|
||||
use crate::{
|
||||
macros::debug_warn,
|
||||
runtime::{with_runtime, RuntimeId},
|
||||
Runtime, Scope, ScopeProperty,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use std::cell::RefCell;
|
||||
use std::fmt::Debug;
|
||||
use std::{cell::RefCell, fmt::Debug};
|
||||
|
||||
/// Effects run a certain chunk of code whenever the signals they depend on change.
|
||||
/// `create_effect` immediately runs the given function once, tracks its dependence
|
||||
@@ -115,8 +116,10 @@ where
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn create_isomorphic_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static)
|
||||
where
|
||||
pub fn create_isomorphic_effect<T>(
|
||||
cx: Scope,
|
||||
f: impl Fn(Option<T>) -> T + 'static,
|
||||
) where
|
||||
T: 'static,
|
||||
{
|
||||
let e = cx.runtime.create_effect(f);
|
||||
@@ -210,7 +213,13 @@ impl EffectId {
|
||||
if let Some(effect) = effect {
|
||||
effect.run(*self, runtime_id);
|
||||
} else {
|
||||
debug_warn!("[Effect] Trying to run an Effect that has been disposed. This is probably either a logic error in a component that creates and disposes of scopes, or a Resource resolving after its scope has been dropped without having been cleaned up.");
|
||||
debug_warn!(
|
||||
"[Effect] Trying to run an Effect that has been disposed. \
|
||||
This is probably either a logic error in a component \
|
||||
that creates and disposes of scopes, or a Resource \
|
||||
resolving after its scope has been dropped without \
|
||||
having been cleaned up."
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -39,30 +39,30 @@
|
||||
//! // this is omitted from most of the examples in the docs
|
||||
//! // you usually won't need to call it yourself
|
||||
//! create_scope(create_runtime(), |cx| {
|
||||
//! // a signal: returns a (getter, setter) pair
|
||||
//! let (count, set_count) = create_signal(cx, 0);
|
||||
//! // a signal: returns a (getter, setter) pair
|
||||
//! let (count, set_count) = create_signal(cx, 0);
|
||||
//!
|
||||
//! // calling the getter gets the value
|
||||
//! assert_eq!(count(), 0);
|
||||
//! // calling the setter sets the value
|
||||
//! set_count(1);
|
||||
//! // or we can mutate it in place with update()
|
||||
//! set_count.update(|n| *n += 1);
|
||||
//! // calling the getter gets the value
|
||||
//! assert_eq!(count(), 0);
|
||||
//! // calling the setter sets the value
|
||||
//! set_count(1);
|
||||
//! // or we can mutate it in place with update()
|
||||
//! set_count.update(|n| *n += 1);
|
||||
//!
|
||||
//! // a derived signal: a plain closure that relies on the signal
|
||||
//! // the closure will run whenever we *access* double_count()
|
||||
//! let double_count = move || count() * 2;
|
||||
//! assert_eq!(double_count(), 4);
|
||||
//!
|
||||
//! // a memo: subscribes to the signal
|
||||
//! // the closure will run only when count changes
|
||||
//! let memoized_triple_count = create_memo(cx, move |_| count() * 3);
|
||||
//! assert_eq!(memoized_triple_count(), 6);
|
||||
//! // a derived signal: a plain closure that relies on the signal
|
||||
//! // the closure will run whenever we *access* double_count()
|
||||
//! let double_count = move || count() * 2;
|
||||
//! assert_eq!(double_count(), 4);
|
||||
//!
|
||||
//! // this effect will run whenever count() changes
|
||||
//! create_effect(cx, move |_| {
|
||||
//! println!("Count = {}", count());
|
||||
//! });
|
||||
//! // a memo: subscribes to the signal
|
||||
//! // the closure will run only when count changes
|
||||
//! let memoized_triple_count = create_memo(cx, move |_| count() * 3);
|
||||
//! assert_eq!(memoized_triple_count(), 6);
|
||||
//!
|
||||
//! // this effect will run whenever count() changes
|
||||
//! create_effect(cx, move |_| {
|
||||
//! println!("Count = {}", count());
|
||||
//! });
|
||||
//! });
|
||||
//! ```
|
||||
|
||||
@@ -136,7 +136,10 @@ pub trait UntrackedSettableSignal<T> {
|
||||
/// Runs the provided closure with a mutable reference to the current
|
||||
/// value without notifying dependents and returns
|
||||
/// the value the closure returned.
|
||||
fn update_returning_untracked<U>(&self, f: impl FnOnce(&mut T) -> U) -> Option<U>;
|
||||
fn update_returning_untracked<U>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut T) -> U,
|
||||
) -> Option<U>;
|
||||
}
|
||||
|
||||
mod macros {
|
||||
|
||||
@@ -65,7 +65,10 @@ use std::fmt::Debug;
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn create_memo<T>(cx: Scope, f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
|
||||
pub fn create_memo<T>(
|
||||
cx: Scope,
|
||||
f: impl Fn(Option<&T>) -> T + 'static,
|
||||
) -> Memo<T>
|
||||
where
|
||||
T: PartialEq + 'static,
|
||||
{
|
||||
@@ -270,9 +273,13 @@ where
|
||||
.with(|n| f(n.as_ref().expect("Memo is missing its initial value")))
|
||||
}
|
||||
|
||||
pub(crate) fn try_with<U>(&self, f: impl Fn(&T) -> U) -> Result<U, SignalError> {
|
||||
self.0
|
||||
.try_with(|n| f(n.as_ref().expect("Memo is missing its initial value")))
|
||||
pub(crate) fn try_with<U>(
|
||||
&self,
|
||||
f: impl Fn(&T) -> U,
|
||||
) -> Result<U, SignalError> {
|
||||
self.0.try_with(|n| {
|
||||
f(n.as_ref().expect("Memo is missing its initial value"))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
create_effect, create_isomorphic_effect, create_memo, create_signal, queue_microtask,
|
||||
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_context, Memo, ReadSignal, Scope, ScopeProperty, SuspenseContext,
|
||||
WriteSignal,
|
||||
};
|
||||
use std::{
|
||||
any::Any,
|
||||
@@ -113,7 +115,9 @@ where
|
||||
|
||||
let (loading, set_loading) = create_signal(cx, false);
|
||||
|
||||
let fetcher = Rc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
|
||||
let fetcher = Rc::new(move |s| {
|
||||
Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>
|
||||
});
|
||||
let source = create_memo(cx, move |_| source());
|
||||
|
||||
let r = Rc::new(ResourceState {
|
||||
@@ -170,17 +174,18 @@ where
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
/// #[derive(Debug, Clone)] // doesn't implement Serialize, Deserialize
|
||||
/// struct ComplicatedUnserializableStruct {
|
||||
/// // something here that can't be serialized
|
||||
/// // something here that can't be serialized
|
||||
/// }
|
||||
/// // any old async function; maybe this is calling a REST API or something
|
||||
/// async fn setup_complicated_struct() -> ComplicatedUnserializableStruct {
|
||||
/// // do some work
|
||||
/// ComplicatedUnserializableStruct { }
|
||||
/// // do some work
|
||||
/// ComplicatedUnserializableStruct {}
|
||||
/// }
|
||||
///
|
||||
/// // create the resource; it will run but not be serialized
|
||||
/// # if cfg!(not(any(feature = "csr", feature = "hydrate"))) {
|
||||
/// let result = create_local_resource(cx, move || (), |_| setup_complicated_struct());
|
||||
/// let result =
|
||||
/// create_local_resource(cx, move || (), |_| setup_complicated_struct());
|
||||
/// # }
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
@@ -234,7 +239,9 @@ where
|
||||
|
||||
let (loading, set_loading) = create_signal(cx, false);
|
||||
|
||||
let fetcher = Rc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
|
||||
let fetcher = Rc::new(move |s| {
|
||||
Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>
|
||||
});
|
||||
let source = create_memo(cx, move |_| source());
|
||||
|
||||
let r = Rc::new(ResourceState {
|
||||
@@ -300,7 +307,8 @@ where
|
||||
context.pending_resources.remove(&id); // no longer pending
|
||||
r.resolved.set(true);
|
||||
|
||||
let res = T::from_json(&data).expect_throw("could not deserialize Resource JSON");
|
||||
let res = T::from_json(&data)
|
||||
.expect_throw("could not deserialize Resource JSON");
|
||||
|
||||
r.set_value.update(|n| *n = Some(res));
|
||||
r.set_loading.update(|n| *n = false);
|
||||
@@ -318,21 +326,25 @@ where
|
||||
let set_value = r.set_value;
|
||||
let set_loading = r.set_loading;
|
||||
move |res: String| {
|
||||
let res =
|
||||
T::from_json(&res).expect_throw("could not deserialize Resource JSON");
|
||||
let res = T::from_json(&res)
|
||||
.expect_throw("could not deserialize Resource JSON");
|
||||
resolved.set(true);
|
||||
set_value.update(|n| *n = Some(res));
|
||||
set_loading.update(|n| *n = false);
|
||||
}
|
||||
};
|
||||
let resolve =
|
||||
wasm_bindgen::closure::Closure::wrap(Box::new(resolve) as Box<dyn Fn(String)>);
|
||||
let resolve = wasm_bindgen::closure::Closure::wrap(
|
||||
Box::new(resolve) as Box<dyn Fn(String)>,
|
||||
);
|
||||
let resource_resolvers = js_sys::Reflect::get(
|
||||
&web_sys::window().unwrap(),
|
||||
&wasm_bindgen::JsValue::from_str("__LEPTOS_RESOURCE_RESOLVERS"),
|
||||
)
|
||||
.expect_throw("no __LEPTOS_RESOURCE_RESOLVERS found in the JS global scope");
|
||||
let id = serde_json::to_string(&id).expect_throw("could not serialize Resource ID");
|
||||
.expect_throw(
|
||||
"no __LEPTOS_RESOURCE_RESOLVERS found in the JS global scope",
|
||||
);
|
||||
let id = serde_json::to_string(&id)
|
||||
.expect_throw("could not serialize Resource ID");
|
||||
_ = js_sys::Reflect::set(
|
||||
&resource_resolvers,
|
||||
&wasm_bindgen::JsValue::from_str(&id),
|
||||
@@ -365,7 +377,9 @@ where
|
||||
T: Clone,
|
||||
{
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| resource.read())
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
|
||||
resource.read()
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
@@ -380,7 +394,9 @@ where
|
||||
/// [Resource::read].
|
||||
pub fn with<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U> {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| resource.with(f))
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
|
||||
resource.with(f)
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
@@ -389,15 +405,22 @@ where
|
||||
/// Returns a signal that indicates whether the resource is currently loading.
|
||||
pub fn loading(&self) -> ReadSignal<bool> {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| resource.loading)
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
|
||||
resource.loading
|
||||
})
|
||||
})
|
||||
.expect("tried to call Resource::loading() in a runtime that has already been disposed.")
|
||||
.expect(
|
||||
"tried to call Resource::loading() in a runtime that has already \
|
||||
been disposed.",
|
||||
)
|
||||
}
|
||||
|
||||
/// Re-runs the async function with the current source data.
|
||||
pub fn refetch(&self) {
|
||||
_ = with_runtime(self.runtime, |runtime| {
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| resource.refetch())
|
||||
runtime.resource(self.id, |resource: &ResourceState<S, T>| {
|
||||
resource.refetch()
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -413,7 +436,10 @@ where
|
||||
resource.to_serialization_resolver(self.id)
|
||||
})
|
||||
})
|
||||
.expect("tried to serialize a Resource in a runtime that has already been disposed")
|
||||
.expect(
|
||||
"tried to serialize a Resource in a runtime that has already been \
|
||||
disposed",
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -673,8 +699,16 @@ where
|
||||
let mut tx = tx.clone();
|
||||
move |value| {
|
||||
if let Some(value) = value.as_ref() {
|
||||
tx.try_send((id, value.to_json().expect("could not serialize Resource")))
|
||||
.expect("failed while trying to write to Resource serializer");
|
||||
tx.try_send((
|
||||
id,
|
||||
value
|
||||
.to_json()
|
||||
.expect("could not serialize Resource"),
|
||||
))
|
||||
.expect(
|
||||
"failed while trying to write to Resource \
|
||||
serializer",
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
hydration::SharedContext, AnyEffect, AnyResource, Effect, EffectId, Memo, ReadSignal,
|
||||
ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId, ScopeProperty,
|
||||
SerializableResource, SignalId, UnserializableResource, WriteSignal,
|
||||
hydration::SharedContext, AnyEffect, AnyResource, Effect, EffectId, Memo,
|
||||
ReadSignal, ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer,
|
||||
ScopeId, ScopeProperty, SerializableResource, SignalId,
|
||||
UnserializableResource, WriteSignal,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use futures::stream::FuturesUnordered;
|
||||
@@ -34,7 +35,10 @@ cfg_if! {
|
||||
|
||||
/// Get the selected runtime from the thread-local set of runtimes. On the server,
|
||||
/// this will return the correct runtime. In the browser, there should only be one runtime.
|
||||
pub(crate) fn with_runtime<T>(id: RuntimeId, f: impl FnOnce(&Runtime) -> T) -> Result<T, ()> {
|
||||
pub(crate) fn with_runtime<T>(
|
||||
id: RuntimeId,
|
||||
f: impl FnOnce(&Runtime) -> T,
|
||||
) -> Result<T, ()> {
|
||||
// in the browser, everything should exist under one runtime
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
@@ -88,7 +92,10 @@ impl RuntimeId {
|
||||
let disposer = ScopeDisposer(Box::new(move || scope.dispose()));
|
||||
(scope, disposer)
|
||||
})
|
||||
.expect("tried to create raw scope in a runtime that has already been disposed")
|
||||
.expect(
|
||||
"tried to create raw scope in a runtime that has already been \
|
||||
disposed",
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn run_scope_undisposed<T>(
|
||||
@@ -109,24 +116,38 @@ impl RuntimeId {
|
||||
.expect("tried to run scope in a runtime that has been disposed")
|
||||
}
|
||||
|
||||
pub(crate) fn run_scope<T>(self, f: impl FnOnce(Scope) -> T, parent: Option<Scope>) -> T {
|
||||
pub(crate) fn run_scope<T>(
|
||||
self,
|
||||
f: impl FnOnce(Scope) -> T,
|
||||
parent: Option<Scope>,
|
||||
) -> T {
|
||||
let (ret, _, disposer) = self.run_scope_undisposed(f, parent);
|
||||
disposer.dispose();
|
||||
ret
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn create_concrete_signal(self, value: Rc<RefCell<dyn Any>>) -> SignalId {
|
||||
pub(crate) fn create_concrete_signal(
|
||||
self,
|
||||
value: Rc<RefCell<dyn Any>>,
|
||||
) -> SignalId {
|
||||
with_runtime(self, |runtime| runtime.signals.borrow_mut().insert(value))
|
||||
.expect("tried to create a signal in a runtime that has been disposed")
|
||||
.expect(
|
||||
"tried to create a signal in a runtime that has been disposed",
|
||||
)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn create_signal<T>(self, value: T) -> (ReadSignal<T>, WriteSignal<T>)
|
||||
pub(crate) fn create_signal<T>(
|
||||
self,
|
||||
value: T,
|
||||
) -> (ReadSignal<T>, WriteSignal<T>)
|
||||
where
|
||||
T: Any + 'static,
|
||||
{
|
||||
let id = self.create_concrete_signal(Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>);
|
||||
let id = self.create_concrete_signal(
|
||||
Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>
|
||||
);
|
||||
|
||||
(
|
||||
ReadSignal {
|
||||
@@ -150,7 +171,9 @@ impl RuntimeId {
|
||||
where
|
||||
T: Any + 'static,
|
||||
{
|
||||
let id = self.create_concrete_signal(Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>);
|
||||
let id = self.create_concrete_signal(
|
||||
Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>
|
||||
);
|
||||
RwSignal {
|
||||
runtime: self,
|
||||
id,
|
||||
@@ -161,13 +184,21 @@ impl RuntimeId {
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn create_concrete_effect(self, effect: Rc<dyn AnyEffect>) -> EffectId {
|
||||
with_runtime(self, |runtime| runtime.effects.borrow_mut().insert(effect))
|
||||
.expect("tried to create an effect in a runtime that has been disposed")
|
||||
pub(crate) fn create_concrete_effect(
|
||||
self,
|
||||
effect: Rc<dyn AnyEffect>,
|
||||
) -> EffectId {
|
||||
with_runtime(self, |runtime| {
|
||||
runtime.effects.borrow_mut().insert(effect)
|
||||
})
|
||||
.expect("tried to create an effect in a runtime that has been disposed")
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn create_effect<T>(self, f: impl Fn(Option<T>) -> T + 'static) -> EffectId
|
||||
pub(crate) fn create_effect<T>(
|
||||
self,
|
||||
f: impl Fn(Option<T>) -> T + 'static,
|
||||
) -> EffectId
|
||||
where
|
||||
T: Any + 'static,
|
||||
{
|
||||
@@ -187,7 +218,10 @@ impl RuntimeId {
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn create_memo<T>(self, f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
|
||||
pub(crate) fn create_memo<T>(
|
||||
self,
|
||||
f: impl Fn(Option<&T>) -> T + 'static,
|
||||
) -> Memo<T>
|
||||
where
|
||||
T: PartialEq + Any + 'static,
|
||||
{
|
||||
@@ -224,13 +258,17 @@ pub(crate) struct Runtime {
|
||||
pub scope_parents: RefCell<SparseSecondaryMap<ScopeId, ScopeId>>,
|
||||
pub scope_children: RefCell<SparseSecondaryMap<ScopeId, Vec<ScopeId>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub scope_contexts: RefCell<SparseSecondaryMap<ScopeId, HashMap<TypeId, Box<dyn Any>>>>,
|
||||
pub scope_contexts:
|
||||
RefCell<SparseSecondaryMap<ScopeId, HashMap<TypeId, Box<dyn Any>>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub scope_cleanups: RefCell<SparseSecondaryMap<ScopeId, Vec<Box<dyn FnOnce()>>>>,
|
||||
pub scope_cleanups:
|
||||
RefCell<SparseSecondaryMap<ScopeId, Vec<Box<dyn FnOnce()>>>>,
|
||||
pub signals: RefCell<SlotMap<SignalId, Rc<RefCell<dyn Any>>>>,
|
||||
pub signal_subscribers: RefCell<SecondaryMap<SignalId, RefCell<HashSet<EffectId>>>>,
|
||||
pub signal_subscribers:
|
||||
RefCell<SecondaryMap<SignalId, RefCell<HashSet<EffectId>>>>,
|
||||
pub effects: RefCell<SlotMap<EffectId, Rc<dyn AnyEffect>>>,
|
||||
pub effect_sources: RefCell<SecondaryMap<EffectId, RefCell<HashSet<SignalId>>>>,
|
||||
pub effect_sources:
|
||||
RefCell<SecondaryMap<EffectId, RefCell<HashSet<SignalId>>>>,
|
||||
pub resources: RefCell<SlotMap<ResourceId, AnyResource>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,10 @@ use std::{collections::HashMap, fmt};
|
||||
/// values will not have access to values created under another `create_scope`.
|
||||
///
|
||||
/// You usually don't need to call this manually.
|
||||
pub fn create_scope(runtime: RuntimeId, f: impl FnOnce(Scope) + 'static) -> ScopeDisposer {
|
||||
pub fn create_scope(
|
||||
runtime: RuntimeId,
|
||||
f: impl FnOnce(Scope) + 'static,
|
||||
) -> ScopeDisposer {
|
||||
runtime.run_scope_undisposed(f, None).2
|
||||
}
|
||||
|
||||
@@ -37,7 +40,10 @@ pub fn raw_scope_and_disposer(runtime: RuntimeId) -> (Scope, ScopeDisposer) {
|
||||
/// of the synchronous operation.
|
||||
///
|
||||
/// You usually don't need to call this manually.
|
||||
pub fn run_scope<T>(runtime: RuntimeId, f: impl FnOnce(Scope) -> T + 'static) -> T {
|
||||
pub fn run_scope<T>(
|
||||
runtime: RuntimeId,
|
||||
f: impl FnOnce(Scope) -> T + 'static,
|
||||
) -> T {
|
||||
runtime.run_scope(f, None)
|
||||
}
|
||||
|
||||
@@ -116,13 +122,20 @@ impl Scope {
|
||||
/// This is useful for applications like a list or a router, which may want to create child scopes and
|
||||
/// dispose of them when they are no longer needed (e.g., a list item has been destroyed or the user
|
||||
/// has navigated away from the route.)
|
||||
pub fn run_child_scope<T>(self, f: impl FnOnce(Scope) -> T) -> (T, ScopeDisposer) {
|
||||
let (res, child_id, disposer) = self.runtime.run_scope_undisposed(f, Some(self));
|
||||
pub fn run_child_scope<T>(
|
||||
self,
|
||||
f: impl FnOnce(Scope) -> T,
|
||||
) -> (T, ScopeDisposer) {
|
||||
let (res, child_id, disposer) =
|
||||
self.runtime.run_scope_undisposed(f, Some(self));
|
||||
_ = with_runtime(self.runtime, |runtime| {
|
||||
let mut children = runtime.scope_children.borrow_mut();
|
||||
children
|
||||
.entry(self.id)
|
||||
.expect("trying to add a child to a Scope that has already been disposed")
|
||||
.expect(
|
||||
"trying to add a child to a Scope that has already been \
|
||||
disposed",
|
||||
)
|
||||
.or_default()
|
||||
.push(child_id);
|
||||
});
|
||||
@@ -161,7 +174,10 @@ impl Scope {
|
||||
runtime.observer.set(prev_observer);
|
||||
untracked_result
|
||||
})
|
||||
.expect("tried to run untracked function in a runtime that has been disposed")
|
||||
.expect(
|
||||
"tried to run untracked function in a runtime that has been \
|
||||
disposed",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +208,9 @@ impl Scope {
|
||||
}
|
||||
}
|
||||
// run cleanups
|
||||
if let Some(cleanups) = runtime.scope_cleanups.borrow_mut().remove(self.id) {
|
||||
if let Some(cleanups) =
|
||||
runtime.scope_cleanups.borrow_mut().remove(self.id)
|
||||
{
|
||||
for cleanup in cleanups {
|
||||
cleanup();
|
||||
}
|
||||
@@ -209,14 +227,20 @@ impl Scope {
|
||||
ScopeProperty::Signal(id) => {
|
||||
// remove the signal
|
||||
runtime.signals.borrow_mut().remove(id);
|
||||
let subs = runtime.signal_subscribers.borrow_mut().remove(id);
|
||||
let subs = runtime
|
||||
.signal_subscribers
|
||||
.borrow_mut()
|
||||
.remove(id);
|
||||
|
||||
// each of the subs needs to remove the signal from its dependencies
|
||||
// so that it doesn't try to read the (now disposed) signal
|
||||
if let Some(subs) = subs {
|
||||
let source_map = runtime.effect_sources.borrow();
|
||||
let source_map =
|
||||
runtime.effect_sources.borrow();
|
||||
for effect in subs.borrow().iter() {
|
||||
if let Some(effect_sources) = source_map.get(*effect) {
|
||||
if let Some(effect_sources) =
|
||||
source_map.get(*effect)
|
||||
{
|
||||
effect_sources.borrow_mut().remove(&id);
|
||||
}
|
||||
}
|
||||
@@ -235,12 +259,15 @@ impl Scope {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn with_scope_property(&self, f: impl FnOnce(&mut Vec<ScopeProperty>)) {
|
||||
pub(crate) fn with_scope_property(
|
||||
&self,
|
||||
f: impl FnOnce(&mut Vec<ScopeProperty>),
|
||||
) {
|
||||
_ = with_runtime(self.runtime, |runtime| {
|
||||
let scopes = runtime.scopes.borrow();
|
||||
let scope = scopes
|
||||
.get(self.id)
|
||||
.expect("tried to add property to a scope that has been disposed");
|
||||
let scope = scopes.get(self.id).expect(
|
||||
"tried to add property to a scope that has been disposed",
|
||||
);
|
||||
f(&mut scope.borrow_mut());
|
||||
})
|
||||
}
|
||||
@@ -310,18 +337,23 @@ impl ScopeDisposer {
|
||||
impl Scope {
|
||||
/// Returns IDs for all [Resource](crate::Resource)s found on any scope.
|
||||
pub fn all_resources(&self) -> Vec<ResourceId> {
|
||||
with_runtime(self.runtime, |runtime| runtime.all_resources()).unwrap_or_default()
|
||||
with_runtime(self.runtime, |runtime| runtime.all_resources())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 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()).unwrap_or_default()
|
||||
with_runtime(self.runtime, |runtime| runtime.pending_resources())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns IDs for all [Resource](crate::Resource)s found on any scope.
|
||||
pub fn serialization_resolvers(&self) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
|
||||
with_runtime(self.runtime, |runtime| runtime.serialization_resolvers()).unwrap_or_default()
|
||||
pub fn serialization_resolvers(
|
||||
&self,
|
||||
) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
|
||||
with_runtime(self.runtime, |runtime| runtime.serialization_resolvers())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Registers the given [SuspenseContext](crate::SuspenseContext) with the current scope,
|
||||
@@ -341,7 +373,8 @@ impl Scope {
|
||||
let (tx, mut rx) = futures::channel::mpsc::unbounded();
|
||||
|
||||
create_isomorphic_effect(*self, move |_| {
|
||||
let pending = context.pending_resources.try_with(|n| *n).unwrap_or(0);
|
||||
let pending =
|
||||
context.pending_resources.try_with(|n| *n).unwrap_or(0);
|
||||
if pending == 0 {
|
||||
_ = tx.unbounded_send(());
|
||||
}
|
||||
@@ -363,7 +396,9 @@ impl Scope {
|
||||
/// The set of all HTML fragments currently pending.
|
||||
/// Returns a tuple of the hydration ID of the previous element, and a pinned `Future` that will yield the
|
||||
/// `<Suspense/>` HTML when all resources are resolved.
|
||||
pub fn pending_fragments(&self) -> HashMap<String, (String, PinnedFuture<String>)> {
|
||||
pub fn pending_fragments(
|
||||
&self,
|
||||
) -> HashMap<String, (String, PinnedFuture<String>)> {
|
||||
with_runtime(self.runtime, |runtime| {
|
||||
let mut shared_context = runtime.shared_context.borrow_mut();
|
||||
std::mem::take(&mut shared_context.pending_fragments)
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use std::{cell::RefCell, collections::HashMap, fmt::Debug, hash::Hash, rc::Rc};
|
||||
|
||||
use crate::{create_isomorphic_effect, create_signal, ReadSignal, Scope, WriteSignal};
|
||||
use crate::{
|
||||
create_isomorphic_effect, create_signal, ReadSignal, Scope, WriteSignal,
|
||||
};
|
||||
use std::{
|
||||
cell::RefCell, collections::HashMap, fmt::Debug, hash::Hash, rc::Rc,
|
||||
};
|
||||
|
||||
/// Creates a conditional signal that only notifies subscribers when a change
|
||||
/// in the source signal’s value changes whether it is equal to the key value
|
||||
@@ -16,26 +19,29 @@ use crate::{create_isomorphic_effect, create_signal, ReadSignal, Scope, WriteSig
|
||||
/// # use std::rc::Rc;
|
||||
/// # use std::cell::RefCell;
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
/// let (a, set_a) = create_signal(cx, 0);
|
||||
/// let is_selected = create_selector(cx, a);
|
||||
/// let total_notifications = Rc::new(RefCell::new(0));
|
||||
/// let not = Rc::clone(&total_notifications);
|
||||
/// create_isomorphic_effect(cx, {let is_selected = is_selected.clone(); move |_| {
|
||||
/// if is_selected(5) {
|
||||
/// *not.borrow_mut() += 1;
|
||||
/// }
|
||||
/// }});
|
||||
/// let (a, set_a) = create_signal(cx, 0);
|
||||
/// let is_selected = create_selector(cx, a);
|
||||
/// let total_notifications = Rc::new(RefCell::new(0));
|
||||
/// let not = Rc::clone(&total_notifications);
|
||||
/// create_isomorphic_effect(cx, {
|
||||
/// let is_selected = is_selected.clone();
|
||||
/// move |_| {
|
||||
/// if is_selected(5) {
|
||||
/// *not.borrow_mut() += 1;
|
||||
/// }
|
||||
/// }
|
||||
/// });
|
||||
///
|
||||
/// assert_eq!(is_selected(5), false);
|
||||
/// assert_eq!(*total_notifications.borrow(), 0);
|
||||
/// set_a(5);
|
||||
/// assert_eq!(is_selected(5), true);
|
||||
/// assert_eq!(*total_notifications.borrow(), 1);
|
||||
/// set_a(5);
|
||||
/// assert_eq!(is_selected(5), true);
|
||||
/// assert_eq!(*total_notifications.borrow(), 1);
|
||||
/// set_a(4);
|
||||
/// assert_eq!(is_selected(5), false);
|
||||
/// assert_eq!(is_selected(5), false);
|
||||
/// assert_eq!(*total_notifications.borrow(), 0);
|
||||
/// set_a(5);
|
||||
/// assert_eq!(is_selected(5), true);
|
||||
/// assert_eq!(*total_notifications.borrow(), 1);
|
||||
/// set_a(5);
|
||||
/// assert_eq!(is_selected(5), true);
|
||||
/// assert_eq!(*total_notifications.borrow(), 1);
|
||||
/// set_a(4);
|
||||
/// assert_eq!(is_selected(5), false);
|
||||
/// # })
|
||||
/// # .dispose()
|
||||
/// ```
|
||||
@@ -64,8 +70,9 @@ where
|
||||
T: PartialEq + Eq + Debug + Clone + Hash + 'static,
|
||||
{
|
||||
#[allow(clippy::type_complexity)]
|
||||
let subs: Rc<RefCell<HashMap<T, (ReadSignal<bool>, WriteSignal<bool>)>>> =
|
||||
Rc::new(RefCell::new(HashMap::new()));
|
||||
let subs: Rc<
|
||||
RefCell<HashMap<T, (ReadSignal<bool>, WriteSignal<bool>)>>,
|
||||
> = Rc::new(RefCell::new(HashMap::new()));
|
||||
let v = Rc::new(RefCell::new(None));
|
||||
|
||||
create_isomorphic_effect(cx, {
|
||||
@@ -78,7 +85,9 @@ where
|
||||
if prev.as_ref() != Some(&next_value) {
|
||||
let subs = { subs.borrow().clone() };
|
||||
for (key, signal) in subs.into_iter() {
|
||||
if f(&key, &next_value) || (prev.is_some() && f(&key, prev.as_ref().unwrap())) {
|
||||
if f(&key, &next_value)
|
||||
|| (prev.is_some() && f(&key, prev.as_ref().unwrap()))
|
||||
{
|
||||
signal.1.update(|n| *n = true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
use crate::{
|
||||
macros::debug_warn,
|
||||
runtime::{with_runtime, RuntimeId},
|
||||
Runtime, Scope, ScopeProperty, UntrackedGettableSignal, UntrackedSettableSignal,
|
||||
Runtime, Scope, ScopeProperty, UntrackedGettableSignal,
|
||||
UntrackedSettableSignal,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use futures::Stream;
|
||||
@@ -59,7 +60,10 @@ use thiserror::Error;
|
||||
)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn create_signal<T>(cx: Scope, value: T) -> (ReadSignal<T>, WriteSignal<T>) {
|
||||
pub fn create_signal<T>(
|
||||
cx: Scope,
|
||||
value: T,
|
||||
) -> (ReadSignal<T>, WriteSignal<T>) {
|
||||
let s = cx.runtime.create_signal(value);
|
||||
cx.with_scope_property(|prop| prop.push(ScopeProperty::Signal(s.0.id)));
|
||||
s
|
||||
@@ -285,8 +289,12 @@ where
|
||||
|
||||
/// Applies the function to the current Signal, if it exists, and subscribes
|
||||
/// the running effect.
|
||||
pub(crate) fn try_with<U>(&self, f: impl FnOnce(&T) -> U) -> Result<U, SignalError> {
|
||||
match with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f)) {
|
||||
pub(crate) fn try_with<U>(
|
||||
&self,
|
||||
f: impl FnOnce(&T) -> U,
|
||||
) -> Result<U, SignalError> {
|
||||
match with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f))
|
||||
{
|
||||
Ok(Ok(v)) => Ok(v),
|
||||
Ok(Err(e)) => Err(e),
|
||||
Err(_) => Err(SignalError::RuntimeDisposed),
|
||||
@@ -453,7 +461,10 @@ where
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn update_returning_untracked<U>(&self, f: impl FnOnce(&mut T) -> U) -> Option<U> {
|
||||
fn update_returning_untracked<U>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut T) -> U,
|
||||
) -> Option<U> {
|
||||
self.id.update_with_no_effect(self.runtime, f)
|
||||
}
|
||||
}
|
||||
@@ -511,11 +522,17 @@ where
|
||||
/// let (count, set_count) = create_signal(cx, 0);
|
||||
///
|
||||
/// // notifies subscribers
|
||||
/// let value = set_count.update_returning(|n| { *n = 1; *n * 10 });
|
||||
/// let value = set_count.update_returning(|n| {
|
||||
/// *n = 1;
|
||||
/// *n * 10
|
||||
/// });
|
||||
/// assert_eq!(value, Some(10));
|
||||
/// assert_eq!(count(), 1);
|
||||
///
|
||||
/// let value = set_count.update_returning(|n| { *n += 1; *n * 10 });
|
||||
/// let value = set_count.update_returning(|n| {
|
||||
/// *n += 1;
|
||||
/// *n * 10
|
||||
/// });
|
||||
/// assert_eq!(value, Some(20));
|
||||
/// assert_eq!(count(), 2);
|
||||
/// # }).dispose();
|
||||
@@ -533,7 +550,10 @@ where
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn update_returning<U>(&self, f: impl FnOnce(&mut T) -> U) -> Option<U> {
|
||||
pub fn update_returning<U>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut T) -> U,
|
||||
) -> Option<U> {
|
||||
self.id.update(self.runtime, f)
|
||||
}
|
||||
|
||||
@@ -793,7 +813,10 @@ impl<T> UntrackedSettableSignal<T> for RwSignal<T> {
|
||||
)
|
||||
)
|
||||
)]
|
||||
fn update_returning_untracked<U>(&self, f: impl FnOnce(&mut T) -> U) -> Option<U> {
|
||||
fn update_returning_untracked<U>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut T) -> U,
|
||||
) -> Option<U> {
|
||||
self.id.update_with_no_effect(self.runtime, f)
|
||||
}
|
||||
}
|
||||
@@ -885,7 +908,11 @@ where
|
||||
///
|
||||
/// // you can include arbitrary logic in this update function
|
||||
/// // also notifies subscribers, even though the value hasn't changed
|
||||
/// count.update(|n| if *n > 3 { *n += 1 });
|
||||
/// count.update(|n| {
|
||||
/// if *n > 3 {
|
||||
/// *n += 1
|
||||
/// }
|
||||
/// });
|
||||
/// assert_eq!(count(), 1);
|
||||
/// # }).dispose();
|
||||
/// ```
|
||||
@@ -916,11 +943,17 @@ where
|
||||
/// let count = create_rw_signal(cx, 0);
|
||||
///
|
||||
/// // notifies subscribers
|
||||
/// let value = count.update_returning(|n| { *n = 1; *n * 10 });
|
||||
/// let value = count.update_returning(|n| {
|
||||
/// *n = 1;
|
||||
/// *n * 10
|
||||
/// });
|
||||
/// assert_eq!(value, Some(10));
|
||||
/// assert_eq!(count(), 1);
|
||||
///
|
||||
/// let value = count.update_returning(|n| { *n += 1; *n * 10 });
|
||||
/// let value = count.update_returning(|n| {
|
||||
/// *n += 1;
|
||||
/// *n * 10
|
||||
/// });
|
||||
/// assert_eq!(value, Some(20));
|
||||
/// assert_eq!(count(), 2);
|
||||
/// # }).dispose();
|
||||
@@ -938,7 +971,10 @@ where
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub fn update_returning<U>(&self, f: impl FnOnce(&mut T) -> U) -> Option<U> {
|
||||
pub fn update_returning<U>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut T) -> U,
|
||||
) -> Option<U> {
|
||||
self.id.update(self.runtime, f)
|
||||
}
|
||||
|
||||
@@ -1208,7 +1244,9 @@ impl SignalId {
|
||||
}?;
|
||||
let value = value.try_borrow().unwrap_or_else(|e| {
|
||||
debug_warn!(
|
||||
"Signal::try_with_no_subscription failed on Signal<{}>. It seems you're trying to read the value of a signal within an effect caused by updating the signal.",
|
||||
"Signal::try_with_no_subscription failed on Signal<{}>. It \
|
||||
seems you're trying to read the value of a signal within an \
|
||||
effect caused by updating the signal.",
|
||||
std::any::type_name::<T>()
|
||||
);
|
||||
panic!("{e}");
|
||||
@@ -1246,15 +1284,25 @@ impl SignalId {
|
||||
.expect("tried to access a signal in a runtime that has been disposed")
|
||||
}
|
||||
|
||||
pub(crate) fn with<T, U>(&self, runtime: RuntimeId, f: impl FnOnce(&T) -> U) -> U
|
||||
pub(crate) fn with<T, U>(
|
||||
&self,
|
||||
runtime: RuntimeId,
|
||||
f: impl FnOnce(&T) -> U,
|
||||
) -> U
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
with_runtime(runtime, |runtime| self.try_with(runtime, f).unwrap())
|
||||
.expect("tried to access a signal in a runtime that has been disposed")
|
||||
.expect(
|
||||
"tried to access a signal in a runtime that has been disposed",
|
||||
)
|
||||
}
|
||||
|
||||
fn update_value<T, U>(&self, runtime: RuntimeId, f: impl FnOnce(&mut T) -> U) -> Option<U>
|
||||
fn update_value<T, U>(
|
||||
&self,
|
||||
runtime: RuntimeId,
|
||||
f: impl FnOnce(&mut T) -> U,
|
||||
) -> Option<U>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
@@ -1269,14 +1317,19 @@ impl SignalId {
|
||||
Some(f(value))
|
||||
} else {
|
||||
debug_warn!(
|
||||
"[Signal::update] failed when downcasting to Signal<{}>",
|
||||
"[Signal::update] failed when downcasting to \
|
||||
Signal<{}>",
|
||||
std::any::type_name::<T>()
|
||||
);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
debug_warn!(
|
||||
"[Signal::update] You’re trying to update a Signal<{}> that has already been disposed of. This is probably either a logic error in a component that creates and disposes of scopes, or a Resource resolving after its scope has been dropped without having been cleaned up.",
|
||||
"[Signal::update] You’re trying to update a Signal<{}> \
|
||||
that has already been disposed of. This is probably \
|
||||
either a logic error in a component that creates and \
|
||||
disposes of scopes, or a Resource resolving after its \
|
||||
scope has been dropped without having been cleaned up.",
|
||||
std::any::type_name::<T>()
|
||||
);
|
||||
None
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{store_value, Memo, ReadSignal, RwSignal, Scope, StoredValue, UntrackedGettableSignal};
|
||||
use crate::{
|
||||
store_value, Memo, ReadSignal, RwSignal, Scope, StoredValue,
|
||||
UntrackedGettableSignal,
|
||||
};
|
||||
|
||||
/// Helper trait for converting `Fn() -> T` closures into
|
||||
/// [`Signal<T>`].
|
||||
@@ -33,9 +36,9 @@ where
|
||||
///
|
||||
/// // this function takes any kind of wrapped signal
|
||||
/// fn above_3(arg: &Signal<i32>) -> bool {
|
||||
/// // ✅ calling the signal clones and returns the value
|
||||
/// // it is a shorthand for arg.get()
|
||||
/// arg() > 3
|
||||
/// // ✅ calling the signal clones and returns the value
|
||||
/// // it is a shorthand for arg.get()
|
||||
/// arg() > 3
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(above_3(&count.into()), false);
|
||||
@@ -112,7 +115,7 @@ where
|
||||
///
|
||||
/// // this function takes any kind of wrapped signal
|
||||
/// fn above_3(arg: &Signal<i32>) -> bool {
|
||||
/// arg() > 3
|
||||
/// arg() > 3
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(above_3(&count.into()), false);
|
||||
@@ -139,7 +142,10 @@ where
|
||||
};
|
||||
|
||||
Self {
|
||||
inner: SignalTypes::DerivedSignal(cx, store_value(cx, Box::new(derived_signal))),
|
||||
inner: SignalTypes::DerivedSignal(
|
||||
cx,
|
||||
store_value(cx, Box::new(derived_signal)),
|
||||
),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
@@ -207,7 +213,7 @@ where
|
||||
///
|
||||
/// // this function takes any kind of wrapped signal
|
||||
/// fn above_3(arg: &Signal<i32>) -> bool {
|
||||
/// arg.get() > 3
|
||||
/// arg.get() > 3
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(above_3(&count.into()), false);
|
||||
@@ -294,7 +300,9 @@ impl<T> Clone for SignalTypes<T> {
|
||||
match self {
|
||||
Self::ReadSignal(arg0) => Self::ReadSignal(*arg0),
|
||||
Self::Memo(arg0) => Self::Memo(*arg0),
|
||||
Self::DerivedSignal(arg0, arg1) => Self::DerivedSignal(*arg0, *arg1),
|
||||
Self::DerivedSignal(arg0, arg1) => {
|
||||
Self::DerivedSignal(*arg0, *arg1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -307,9 +315,13 @@ where
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::ReadSignal(arg0) => f.debug_tuple("ReadSignal").field(arg0).finish(),
|
||||
Self::ReadSignal(arg0) => {
|
||||
f.debug_tuple("ReadSignal").field(arg0).finish()
|
||||
}
|
||||
Self::Memo(arg0) => f.debug_tuple("Memo").field(arg0).finish(),
|
||||
Self::DerivedSignal(_, _) => f.debug_tuple("DerivedSignal").finish(),
|
||||
Self::DerivedSignal(_, _) => {
|
||||
f.debug_tuple("DerivedSignal").finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -322,7 +334,9 @@ where
|
||||
match (self, other) {
|
||||
(Self::ReadSignal(l0), Self::ReadSignal(r0)) => l0 == r0,
|
||||
(Self::Memo(l0), Self::Memo(r0)) => l0 == r0,
|
||||
(Self::DerivedSignal(_, l0), Self::DerivedSignal(_, r0)) => std::ptr::eq(l0, r0),
|
||||
(Self::DerivedSignal(_, l0), Self::DerivedSignal(_, r0)) => {
|
||||
std::ptr::eq(l0, r0)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -377,9 +391,9 @@ where
|
||||
///
|
||||
/// // this function takes either a reactive or non-reactive value
|
||||
/// fn above_3(arg: &MaybeSignal<i32>) -> bool {
|
||||
/// // ✅ calling the signal clones and returns the value
|
||||
/// // it is a shorthand for arg.get()
|
||||
/// arg() > 3
|
||||
/// // ✅ calling the signal clones and returns the value
|
||||
/// // it is a shorthand for arg.get()
|
||||
/// arg() > 3
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(above_3(&static_value.into()), true);
|
||||
@@ -441,7 +455,7 @@ where
|
||||
///
|
||||
/// // this function takes any kind of wrapped signal
|
||||
/// fn above_3(arg: &Signal<i32>) -> bool {
|
||||
/// arg() > 3
|
||||
/// arg() > 3
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(above_3(&count.into()), false);
|
||||
@@ -527,7 +541,7 @@ where
|
||||
///
|
||||
/// // this function takes any kind of wrapped signal
|
||||
/// fn above_3(arg: &MaybeSignal<i32>) -> bool {
|
||||
/// arg.get() > 3
|
||||
/// arg.get() > 3
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(above_3(&count.into()), false);
|
||||
|
||||
@@ -32,9 +32,9 @@ where
|
||||
///
|
||||
/// // this function takes any kind of signal setter
|
||||
/// fn set_to_4(setter: &SignalSetter<i32>) {
|
||||
/// // ✅ calling the signal sets the value
|
||||
/// // it is a shorthand for arg.set()
|
||||
/// setter(4);
|
||||
/// // ✅ calling the signal sets the value
|
||||
/// // it is a shorthand for arg.set()
|
||||
/// setter(4);
|
||||
/// }
|
||||
///
|
||||
/// set_to_4(&set_count.into());
|
||||
@@ -90,9 +90,9 @@ where
|
||||
///
|
||||
/// // this function takes any kind of signal setter
|
||||
/// fn set_to_4(setter: &SignalSetter<i32>) {
|
||||
/// // ✅ calling the signal sets the value
|
||||
/// // it is a shorthand for arg.set()
|
||||
/// setter(4)
|
||||
/// // ✅ calling the signal sets the value
|
||||
/// // it is a shorthand for arg.set()
|
||||
/// setter(4)
|
||||
/// }
|
||||
///
|
||||
/// set_to_4(&set_count.into());
|
||||
@@ -114,7 +114,10 @@ where
|
||||
)]
|
||||
pub fn map(cx: Scope, mapped_setter: impl Fn(T) + 'static) -> Self {
|
||||
Self {
|
||||
inner: SignalSetterTypes::Mapped(cx, store_value(cx, Box::new(mapped_setter))),
|
||||
inner: SignalSetterTypes::Mapped(
|
||||
cx,
|
||||
store_value(cx, Box::new(mapped_setter)),
|
||||
),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
}
|
||||
@@ -209,7 +212,9 @@ where
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Write(arg0) => f.debug_tuple("WriteSignal").field(arg0).finish(),
|
||||
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(),
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::{create_memo, IntoSignalSetter, RwSignal, Scope, Signal, SignalSetter};
|
||||
use crate::{
|
||||
create_memo, IntoSignalSetter, RwSignal, Scope, Signal, SignalSetter,
|
||||
};
|
||||
|
||||
/// Derives a reactive slice of an [RwSignal](crate::RwSignal).
|
||||
///
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{create_rw_signal, RwSignal, Scope, UntrackedGettableSignal, UntrackedSettableSignal};
|
||||
use crate::{
|
||||
create_rw_signal, RwSignal, Scope, UntrackedGettableSignal,
|
||||
UntrackedSettableSignal,
|
||||
};
|
||||
|
||||
/// A **non-reactive** wrapper for any value, which can be created with [store_value].
|
||||
///
|
||||
@@ -109,7 +112,10 @@ where
|
||||
/// assert_eq!(updated, Some(String::from("b")));
|
||||
/// });
|
||||
/// ```
|
||||
pub fn update_returning<U>(&self, f: impl FnOnce(&mut T) -> U) -> Option<U> {
|
||||
pub fn update_returning<U>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut T) -> U,
|
||||
) -> Option<U> {
|
||||
self.0.update_returning_untracked(f)
|
||||
}
|
||||
|
||||
@@ -157,7 +163,7 @@ where
|
||||
/// # create_scope(create_runtime(), |cx| {
|
||||
/// // this structure is neither `Copy` nor `Clone`
|
||||
/// pub struct MyUncloneableData {
|
||||
/// pub value: String
|
||||
/// pub value: String,
|
||||
/// }
|
||||
///
|
||||
/// // ✅ you can move the `StoredValue` and access it with .with()
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#[cfg(not(feature = "stable"))]
|
||||
use leptos_reactive::{
|
||||
create_isomorphic_effect, create_memo, create_runtime, create_scope, create_signal,
|
||||
create_isomorphic_effect, create_memo, create_runtime, create_scope,
|
||||
create_signal,
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
#[test]
|
||||
fn effect_runs() {
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
create_scope(create_runtime(), |cx| {
|
||||
let (a, set_a) = create_signal(cx, -1);
|
||||
@@ -35,8 +35,7 @@ fn effect_runs() {
|
||||
#[cfg(not(feature = "stable"))]
|
||||
#[test]
|
||||
fn effect_tracks_memo() {
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
create_scope(create_runtime(), |cx| {
|
||||
let (a, set_a) = create_signal(cx, -1);
|
||||
@@ -66,8 +65,7 @@ fn effect_tracks_memo() {
|
||||
#[cfg(not(feature = "stable"))]
|
||||
#[test]
|
||||
fn untrack_mutes_effect() {
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
create_scope(create_runtime(), |cx| {
|
||||
let (a, set_a) = create_signal(cx, -1);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#[cfg(not(feature = "stable"))]
|
||||
use leptos_reactive::{create_memo, create_runtime, create_scope, create_signal};
|
||||
use leptos_reactive::{
|
||||
create_memo, create_runtime, create_scope, create_signal,
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
#[test]
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
//#[cfg(not(feature = "stable"))]
|
||||
use leptos_reactive::{
|
||||
create_isomorphic_effect, create_runtime, create_scope, create_signal, UntrackedGettableSignal,
|
||||
UntrackedSettableSignal,
|
||||
create_isomorphic_effect, create_runtime, create_scope, create_signal,
|
||||
UntrackedGettableSignal, UntrackedSettableSignal,
|
||||
};
|
||||
|
||||
//#[cfg(not(feature = "stable"))]
|
||||
#[test]
|
||||
fn untracked_set_doesnt_trigger_effect() {
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
create_scope(create_runtime(), |cx| {
|
||||
let (a, set_a) = create_signal(cx, -1);
|
||||
@@ -39,8 +38,7 @@ fn untracked_set_doesnt_trigger_effect() {
|
||||
|
||||
#[test]
|
||||
fn untracked_get_doesnt_trigger_effect() {
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
create_scope(create_runtime(), |cx| {
|
||||
let (a, set_a) = create_signal(cx, -1);
|
||||
@@ -52,7 +50,8 @@ fn untracked_get_doesnt_trigger_effect() {
|
||||
create_isomorphic_effect(cx, {
|
||||
let b = b.clone();
|
||||
move |_| {
|
||||
let formatted = format!("Values are {} and {}", a(), a2.get_untracked());
|
||||
let formatted =
|
||||
format!("Values are {} and {}", a(), a2.get_untracked());
|
||||
*b.borrow_mut() = formatted;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{ServerFn, ServerFnError};
|
||||
use leptos_reactive::{
|
||||
create_rw_signal, spawn_local, store_value, ReadSignal, RwSignal, Scope, StoredValue,
|
||||
create_rw_signal, spawn_local, store_value, ReadSignal, RwSignal, Scope,
|
||||
StoredValue,
|
||||
};
|
||||
use std::{future::Future, pin::Pin, rc::Rc};
|
||||
|
||||
@@ -65,15 +66,16 @@ use std::{future::Future, pin::Pin, rc::Rc};
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// // if there's a single argument, just use that
|
||||
/// let action1 = create_action(cx, |input: &String| {
|
||||
/// let input = input.clone();
|
||||
/// async move { todo!() }
|
||||
/// let input = input.clone();
|
||||
/// async move { todo!() }
|
||||
/// });
|
||||
///
|
||||
/// // if there are no arguments, use the unit type `()`
|
||||
/// let action2 = create_action(cx, |input: &()| async { todo!() });
|
||||
///
|
||||
/// // if there are multiple arguments, use a tuple
|
||||
/// let action3 = create_action(cx, |input: &(usize, String)| async { todo!() });
|
||||
/// let action3 =
|
||||
/// create_action(cx, |input: &(usize, String)| async { todo!() });
|
||||
/// # });
|
||||
/// ```
|
||||
pub struct Action<I, O>(StoredValue<ActionState<I, O>>)
|
||||
@@ -259,15 +261,16 @@ where
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// // if there's a single argument, just use that
|
||||
/// let action1 = create_action(cx, |input: &String| {
|
||||
/// let input = input.clone();
|
||||
/// async move { todo!() }
|
||||
/// let input = input.clone();
|
||||
/// async move { todo!() }
|
||||
/// });
|
||||
///
|
||||
/// // if there are no arguments, use the unit type `()`
|
||||
/// let action2 = create_action(cx, |input: &()| async { todo!() });
|
||||
///
|
||||
/// // if there are multiple arguments, use a tuple
|
||||
/// let action3 = create_action(cx, |input: &(usize, String)| async { todo!() });
|
||||
/// let action3 =
|
||||
/// create_action(cx, |input: &(usize, String)| async { todo!() });
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn create_action<I, O, F, Fu>(cx: Scope, action_fn: F) -> Action<I, O>
|
||||
@@ -306,14 +309,16 @@ where
|
||||
///
|
||||
/// #[server(MyServerFn)]
|
||||
/// async fn my_server_fn() -> Result<(), ServerFnError> {
|
||||
/// todo!()
|
||||
/// todo!()
|
||||
/// }
|
||||
///
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// let my_server_action = create_server_action::<MyServerFn>(cx);
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn create_server_action<S>(cx: Scope) -> Action<S, Result<S::Output, ServerFnError>>
|
||||
pub fn create_server_action<S>(
|
||||
cx: Scope,
|
||||
) -> Action<S, Result<S::Output, ServerFnError>>
|
||||
where
|
||||
S: Clone + ServerFn,
|
||||
{
|
||||
|
||||
@@ -80,7 +80,6 @@
|
||||
|
||||
pub use form_urlencoded;
|
||||
use leptos_reactive::*;
|
||||
|
||||
use proc_macro2::{Literal, TokenStream};
|
||||
use quote::TokenStreamExt;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
@@ -95,7 +94,6 @@ mod action;
|
||||
mod multi_action;
|
||||
pub use action::*;
|
||||
pub use multi_action::*;
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
@@ -103,7 +101,10 @@ use std::{
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
type ServerFnTraitObj = dyn Fn(Scope, &[u8]) -> Pin<Box<dyn Future<Output = Result<Payload, ServerFnError>>>>
|
||||
type ServerFnTraitObj = dyn Fn(
|
||||
Scope,
|
||||
&[u8],
|
||||
) -> Pin<Box<dyn Future<Output = Result<Payload, ServerFnError>>>>
|
||||
+ Send
|
||||
+ Sync;
|
||||
|
||||
@@ -302,16 +303,18 @@ where
|
||||
// serialize the output
|
||||
let result = match Self::encoding() {
|
||||
Encoding::Url => match serde_json::to_string(&result)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))
|
||||
{
|
||||
.map_err(|e| {
|
||||
ServerFnError::Serialization(e.to_string())
|
||||
}) {
|
||||
Ok(r) => Payload::Url(r),
|
||||
Err(e) => return Err(e),
|
||||
},
|
||||
Encoding::Cbor => {
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
match ciborium::ser::into_writer(&result, &mut buffer)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))
|
||||
{
|
||||
.map_err(|e| {
|
||||
ServerFnError::Serialization(e.to_string())
|
||||
}) {
|
||||
Ok(_) => Payload::Binary(buffer),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
@@ -319,7 +322,8 @@ where
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}) as Pin<Box<dyn Future<Output = Result<Payload, ServerFnError>>>>
|
||||
})
|
||||
as Pin<Box<dyn Future<Output = Result<Payload, ServerFnError>>>>
|
||||
});
|
||||
|
||||
// store it in the hashmap
|
||||
@@ -332,8 +336,9 @@ where
|
||||
// return Err
|
||||
match prev {
|
||||
Some(_) => Err(ServerFnError::Registration(format!(
|
||||
"There was already a server function registered at {:?}. \
|
||||
This can happen if you use the same server function name in two different modules
|
||||
"There was already a server function registered at {:?}. This \
|
||||
can happen if you use the same server function name in two \
|
||||
different modules
|
||||
on `stable` or in `release` mode.",
|
||||
Self::url()
|
||||
))),
|
||||
@@ -452,6 +457,7 @@ where
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))?;
|
||||
|
||||
let mut deserializer = JSONDeserializer::from_str(&text);
|
||||
T::deserialize(&mut deserializer).map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
T::deserialize(&mut deserializer)
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{ServerFn, ServerFnError};
|
||||
use leptos_reactive::{
|
||||
create_rw_signal, spawn_local, store_value, ReadSignal, RwSignal, Scope, StoredValue,
|
||||
create_rw_signal, spawn_local, store_value, ReadSignal, RwSignal, Scope,
|
||||
StoredValue,
|
||||
};
|
||||
use std::{future::Future, pin::Pin, rc::Rc};
|
||||
|
||||
@@ -46,15 +47,16 @@ use std::{future::Future, pin::Pin, rc::Rc};
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// // if there's a single argument, just use that
|
||||
/// let action1 = create_multi_action(cx, |input: &String| {
|
||||
/// let input = input.clone();
|
||||
/// async move { todo!() }
|
||||
/// let input = input.clone();
|
||||
/// async move { todo!() }
|
||||
/// });
|
||||
///
|
||||
/// // if there are no arguments, use the unit type `()`
|
||||
/// let action2 = create_multi_action(cx, |input: &()| async { todo!() });
|
||||
///
|
||||
/// // if there are multiple arguments, use a tuple
|
||||
/// let action3 = create_multi_action(cx, |input: &(usize, String)| async { todo!() });
|
||||
/// let action3 =
|
||||
/// create_multi_action(cx, |input: &(usize, String)| async { todo!() });
|
||||
/// # });
|
||||
/// ```
|
||||
pub struct MultiAction<I, O>(StoredValue<MultiActionState<I, O>>)
|
||||
@@ -269,18 +271,22 @@ where
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// // if there's a single argument, just use that
|
||||
/// let action1 = create_multi_action(cx, |input: &String| {
|
||||
/// let input = input.clone();
|
||||
/// async move { todo!() }
|
||||
/// let input = input.clone();
|
||||
/// async move { todo!() }
|
||||
/// });
|
||||
///
|
||||
/// // if there are no arguments, use the unit type `()`
|
||||
/// let action2 = create_multi_action(cx, |input: &()| async { todo!() });
|
||||
///
|
||||
/// // if there are multiple arguments, use a tuple
|
||||
/// let action3 = create_multi_action(cx, |input: &(usize, String)| async { todo!() });
|
||||
/// let action3 =
|
||||
/// create_multi_action(cx, |input: &(usize, String)| async { todo!() });
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn create_multi_action<I, O, F, Fu>(cx: Scope, action_fn: F) -> MultiAction<I, O>
|
||||
pub fn create_multi_action<I, O, F, Fu>(
|
||||
cx: Scope,
|
||||
action_fn: F,
|
||||
) -> MultiAction<I, O>
|
||||
where
|
||||
I: 'static,
|
||||
O: 'static,
|
||||
@@ -313,14 +319,16 @@ where
|
||||
///
|
||||
/// #[server(MyServerFn)]
|
||||
/// async fn my_server_fn() -> Result<(), ServerFnError> {
|
||||
/// todo!()
|
||||
/// todo!()
|
||||
/// }
|
||||
///
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// let my_server_multi_action = create_server_multi_action::<MyServerFn>(cx);
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn create_server_multi_action<S>(cx: Scope) -> MultiAction<S, Result<S::Output, ServerFnError>>
|
||||
pub fn create_server_multi_action<S>(
|
||||
cx: Scope,
|
||||
) -> MultiAction<S, Result<S::Output, ServerFnError>>
|
||||
where
|
||||
S: Clone + ServerFn,
|
||||
{
|
||||
|
||||
@@ -34,19 +34,21 @@ impl std::fmt::Debug for BodyContext {
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> impl IntoView {
|
||||
/// provide_meta_context(cx);
|
||||
/// let (prefers_dark, set_prefers_dark) = create_signal(cx, false);
|
||||
/// let body_class = move || if prefers_dark() {
|
||||
/// "dark".to_string()
|
||||
/// } else {
|
||||
/// "light".to_string()
|
||||
/// };
|
||||
/// provide_meta_context(cx);
|
||||
/// let (prefers_dark, set_prefers_dark) = create_signal(cx, false);
|
||||
/// let body_class = move || {
|
||||
/// if prefers_dark() {
|
||||
/// "dark".to_string()
|
||||
/// } else {
|
||||
/// "light".to_string()
|
||||
/// }
|
||||
/// };
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Body class=body_class/>
|
||||
/// </main>
|
||||
/// }
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Body class=body_class/>
|
||||
/// </main>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[component(transparent)]
|
||||
|
||||
@@ -39,13 +39,13 @@ impl std::fmt::Debug for HtmlContext {
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> impl IntoView {
|
||||
/// provide_meta_context(cx);
|
||||
/// provide_meta_context(cx);
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Html lang="he" dir="rtl"/>
|
||||
/// </main>
|
||||
/// }
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Html lang="he" dir="rtl"/>
|
||||
/// </main>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[component(transparent)]
|
||||
|
||||
@@ -16,23 +16,22 @@
|
||||
//!
|
||||
//! #[component]
|
||||
//! fn MyApp(cx: Scope) -> impl IntoView {
|
||||
//! let (name, set_name) = create_signal(cx, "Alice".to_string());
|
||||
//! let (name, set_name) = create_signal(cx, "Alice".to_string());
|
||||
//!
|
||||
//! view! { cx,
|
||||
//! <Title
|
||||
//! // reactively sets document.title when `name` changes
|
||||
//! text=name
|
||||
//! // applies the `formatter` function to the `text` value
|
||||
//! formatter=|text| format!("“{text}” is your name")
|
||||
//! />
|
||||
//! <main>
|
||||
//! <input
|
||||
//! prop:value=name
|
||||
//! on:input=move |ev| set_name(event_target_value(&ev))
|
||||
//! view! { cx,
|
||||
//! <Title
|
||||
//! // reactively sets document.title when `name` changes
|
||||
//! text=name
|
||||
//! // applies the `formatter` function to the `text` value
|
||||
//! formatter=|text| format!("“{text}” is your name")
|
||||
//! />
|
||||
//! </main>
|
||||
//! }
|
||||
//!
|
||||
//! <main>
|
||||
//! <input
|
||||
//! prop:value=name
|
||||
//! on:input=move |ev| set_name(event_target_value(&ev))
|
||||
//! />
|
||||
//! </main>
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//! # Feature Flags
|
||||
@@ -46,6 +45,7 @@
|
||||
//! which mode your app is operating in.
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{leptos_dom::debug_warn, *};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashMap,
|
||||
@@ -53,8 +53,6 @@ use std::{
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use leptos::{leptos_dom::debug_warn, *};
|
||||
|
||||
mod body;
|
||||
mod html;
|
||||
mod link;
|
||||
@@ -93,7 +91,14 @@ pub struct MetaContext {
|
||||
pub struct MetaTagsContext {
|
||||
next_id: Rc<Cell<MetaTagId>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
els: Rc<RefCell<HashMap<String, (HtmlElement<AnyElement>, Scope, Option<web_sys::Element>)>>>,
|
||||
els: Rc<
|
||||
RefCell<
|
||||
HashMap<
|
||||
String,
|
||||
(HtmlElement<AnyElement>, Scope, Option<web_sys::Element>),
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for MetaTagsContext {
|
||||
@@ -109,12 +114,19 @@ impl MetaTagsContext {
|
||||
self.els
|
||||
.borrow()
|
||||
.iter()
|
||||
.map(|(_, (builder_el, cx, _))| builder_el.clone().into_view(*cx).render_to_string(*cx))
|
||||
.map(|(_, (builder_el, cx, _))| {
|
||||
builder_el.clone().into_view(*cx).render_to_string(*cx)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn register(&self, cx: Scope, id: String, builder_el: HtmlElement<AnyElement>) {
|
||||
pub fn register(
|
||||
&self,
|
||||
cx: Scope,
|
||||
id: String,
|
||||
builder_el: HtmlElement<AnyElement>,
|
||||
) {
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
|
||||
use leptos::document;
|
||||
@@ -190,10 +202,11 @@ pub fn use_head(cx: Scope) -> MetaContext {
|
||||
match use_context::<MetaContext>(cx) {
|
||||
None => {
|
||||
debug_warn!(
|
||||
"use_head() is being called without a MetaContext being provided. \
|
||||
We'll automatically create and provide one, but if this is being called in a child \
|
||||
route it may cause bugs. To be safe, you should provide_meta_context(cx) \
|
||||
somewhere in the root of the app."
|
||||
"use_head() is being called without a MetaContext being \
|
||||
provided. We'll automatically create and provide one, but if \
|
||||
this is being called in a child route it may cause bugs. To \
|
||||
be safe, you should provide_meta_context(cx) somewhere in \
|
||||
the root of the app."
|
||||
);
|
||||
let meta = MetaContext::new();
|
||||
provide_context(cx, meta.clone());
|
||||
|
||||
@@ -9,18 +9,18 @@ use leptos::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> impl IntoView {
|
||||
/// provide_meta_context(cx);
|
||||
/// provide_meta_context(cx);
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Link rel="preload"
|
||||
/// href="myFont.woff2"
|
||||
/// as_="font"
|
||||
/// type_="font/woff2"
|
||||
/// crossorigin="anonymous"
|
||||
/// />
|
||||
/// </main>
|
||||
/// }
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Link rel="preload"
|
||||
/// href="myFont.woff2"
|
||||
/// as_="font"
|
||||
/// type_="font/woff2"
|
||||
/// crossorigin="anonymous"
|
||||
/// />
|
||||
/// </main>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[component(transparent)]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use leptos::{component, IntoView, Scope};
|
||||
|
||||
use crate::{use_head, TextProp};
|
||||
use leptos::{component, IntoView, Scope};
|
||||
|
||||
/// Injects an [HTMLMetaElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMetaElement) into the document
|
||||
/// head to set metadata
|
||||
|
||||
@@ -9,15 +9,15 @@ use leptos::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> impl IntoView {
|
||||
/// provide_meta_context(cx);
|
||||
/// provide_meta_context(cx);
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Script>
|
||||
/// "console.log('Hello, world!');"
|
||||
/// </Script>
|
||||
/// </main>
|
||||
/// }
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Script>
|
||||
/// "console.log('Hello, world!');"
|
||||
/// </Script>
|
||||
/// </main>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[component(transparent)]
|
||||
@@ -86,7 +86,9 @@ pub fn Script(
|
||||
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/>."),
|
||||
_ => leptos::warn!(
|
||||
"Only text nodes are supported as children of <Script/>."
|
||||
),
|
||||
}
|
||||
}
|
||||
builder_el.child(script)
|
||||
|
||||
@@ -9,15 +9,15 @@ use leptos::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> impl IntoView {
|
||||
/// provide_meta_context(cx);
|
||||
/// provide_meta_context(cx);
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Style>
|
||||
/// "body { font-weight: bold; }"
|
||||
/// </Style>
|
||||
/// </main>
|
||||
/// }
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Style>
|
||||
/// "body { font-weight: bold; }"
|
||||
/// </Style>
|
||||
/// </main>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[component(transparent)]
|
||||
@@ -58,7 +58,9 @@ pub fn Style(
|
||||
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/>."),
|
||||
_ => leptos::warn!(
|
||||
"Only text nodes are supported as children of <Style/>."
|
||||
),
|
||||
}
|
||||
}
|
||||
builder_el.child(style)
|
||||
|
||||
@@ -10,13 +10,13 @@ use leptos::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> impl IntoView {
|
||||
/// provide_meta_context(cx);
|
||||
/// provide_meta_context(cx);
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Stylesheet href="/style.css"/>
|
||||
/// </main>
|
||||
/// }
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Stylesheet href="/style.css"/>
|
||||
/// </main>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[component(transparent)]
|
||||
|
||||
@@ -55,33 +55,33 @@ where
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> impl IntoView {
|
||||
/// provide_meta_context(cx);
|
||||
/// let formatter = |text| format!("{text} — Leptos Online");
|
||||
/// provide_meta_context(cx);
|
||||
/// let formatter = |text| format!("{text} — Leptos Online");
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Title formatter/>
|
||||
/// // ... routing logic here
|
||||
/// </main>
|
||||
/// }
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Title formatter/>
|
||||
/// // ... routing logic here
|
||||
/// </main>
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[component]
|
||||
/// fn PageA(cx: Scope) -> impl IntoView {
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Title text="Page A"/> // sets title to "Page A — Leptos Online"
|
||||
/// </main>
|
||||
/// }
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Title text="Page A"/> // sets title to "Page A — Leptos Online"
|
||||
/// </main>
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[component]
|
||||
/// fn PageB(cx: Scope) -> impl IntoView {
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Title text="Page B"/> // sets title to "Page B — Leptos Online"
|
||||
/// </main>
|
||||
/// }
|
||||
/// view! { cx,
|
||||
/// <main>
|
||||
/// <Title text="Page B"/> // sets title to "Page B — Leptos Online"
|
||||
/// </main>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[component(transparent)]
|
||||
|
||||
@@ -68,12 +68,16 @@ where
|
||||
|
||||
let (form, method, action, enctype) = extract_form_attributes(&ev);
|
||||
|
||||
let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw();
|
||||
let form_data =
|
||||
web_sys::FormData::new_with_form(&form).unwrap_throw();
|
||||
if let Some(on_form_data) = on_form_data.clone() {
|
||||
on_form_data(&form_data);
|
||||
}
|
||||
let params =
|
||||
web_sys::UrlSearchParams::new_with_str_sequence_sequence(&form_data).unwrap_throw();
|
||||
web_sys::UrlSearchParams::new_with_str_sequence_sequence(
|
||||
&form_data,
|
||||
)
|
||||
.unwrap_throw();
|
||||
let action = use_resolved_path(cx, move || action.clone())
|
||||
.get()
|
||||
.unwrap_or_default();
|
||||
@@ -108,8 +112,13 @@ where
|
||||
}
|
||||
|
||||
if resp.status() == 303 {
|
||||
if let Some(redirect_url) = resp.headers().get("Location") {
|
||||
_ = navigate(&redirect_url, Default::default());
|
||||
if let Some(redirect_url) =
|
||||
resp.headers().get("Location")
|
||||
{
|
||||
_ = navigate(
|
||||
&redirect_url,
|
||||
Default::default(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,7 +128,9 @@ where
|
||||
// otherwise, GET
|
||||
else {
|
||||
let params = params.to_string().as_string().unwrap_or_default();
|
||||
if navigate(&format!("{action}?{params}"), Default::default()).is_ok() {
|
||||
if navigate(&format!("{action}?{params}"), Default::default())
|
||||
.is_ok()
|
||||
{
|
||||
ev.prevent_default();
|
||||
}
|
||||
}
|
||||
@@ -179,7 +190,10 @@ where
|
||||
let action_url = if let Some(url) = action.url() {
|
||||
url
|
||||
} else {
|
||||
debug_warn!("<ActionForm/> action needs a URL. Either use create_server_action() or Action::using_server_fn().");
|
||||
debug_warn!(
|
||||
"<ActionForm/> action needs a URL. Either use \
|
||||
create_server_action() or Action::using_server_fn()."
|
||||
);
|
||||
String::new()
|
||||
};
|
||||
let version = action.version();
|
||||
@@ -200,17 +214,21 @@ where
|
||||
let on_response = Rc::new(move |resp: &web_sys::Response| {
|
||||
let resp = resp.clone().expect("couldn't get Response");
|
||||
spawn_local(async move {
|
||||
let body =
|
||||
JsFuture::from(resp.text().expect("couldn't get .text() from Response")).await;
|
||||
let body = JsFuture::from(
|
||||
resp.text().expect("couldn't get .text() from Response"),
|
||||
)
|
||||
.await;
|
||||
match body {
|
||||
Ok(json) => {
|
||||
match O::from_json(
|
||||
&json.as_string().expect("couldn't get String from JsString"),
|
||||
&json
|
||||
.as_string()
|
||||
.expect("couldn't get String from JsString"),
|
||||
) {
|
||||
Ok(res) => value.set(Some(Ok(res))),
|
||||
Err(e) => {
|
||||
value.set(Some(Err(ServerFnError::Deserialization(e.to_string()))))
|
||||
}
|
||||
Err(e) => value.set(Some(Err(
|
||||
ServerFnError::Deserialization(e.to_string()),
|
||||
))),
|
||||
}
|
||||
}
|
||||
Err(e) => log::error!("{e:?}"),
|
||||
@@ -258,7 +276,10 @@ where
|
||||
let action = if let Some(url) = multi_action.url() {
|
||||
url
|
||||
} else {
|
||||
debug_warn!("<MultiActionForm/> action needs a URL. Either use create_server_action() or Action::using_server_fn().");
|
||||
debug_warn!(
|
||||
"<MultiActionForm/> action needs a URL. Either use \
|
||||
create_server_action() or Action::using_server_fn()."
|
||||
);
|
||||
String::new()
|
||||
};
|
||||
|
||||
@@ -309,10 +330,14 @@ fn extract_form_attributes(
|
||||
.unwrap_or_default()
|
||||
.to_lowercase(),
|
||||
form.get_attribute("enctype")
|
||||
.unwrap_or_else(|| "application/x-www-form-urlencoded".to_string())
|
||||
.unwrap_or_else(|| {
|
||||
"application/x-www-form-urlencoded".to_string()
|
||||
})
|
||||
.to_lowercase(),
|
||||
)
|
||||
} else if let Some(input) = el.dyn_ref::<web_sys::HtmlInputElement>() {
|
||||
} else if let Some(input) =
|
||||
el.dyn_ref::<web_sys::HtmlInputElement>()
|
||||
{
|
||||
let form = ev
|
||||
.target()
|
||||
.unwrap()
|
||||
@@ -331,11 +356,15 @@ fn extract_form_attributes(
|
||||
}),
|
||||
input.get_attribute("enctype").unwrap_or_else(|| {
|
||||
form.get_attribute("enctype")
|
||||
.unwrap_or_else(|| "application/x-www-form-urlencoded".to_string())
|
||||
.unwrap_or_else(|| {
|
||||
"application/x-www-form-urlencoded".to_string()
|
||||
})
|
||||
.to_lowercase()
|
||||
}),
|
||||
)
|
||||
} else if let Some(button) = el.dyn_ref::<web_sys::HtmlButtonElement>() {
|
||||
} else if let Some(button) =
|
||||
el.dyn_ref::<web_sys::HtmlButtonElement>()
|
||||
{
|
||||
let form = ev
|
||||
.target()
|
||||
.unwrap()
|
||||
@@ -354,18 +383,25 @@ fn extract_form_attributes(
|
||||
}),
|
||||
button.get_attribute("enctype").unwrap_or_else(|| {
|
||||
form.get_attribute("enctype")
|
||||
.unwrap_or_else(|| "application/x-www-form-urlencoded".to_string())
|
||||
.unwrap_or_else(|| {
|
||||
"application/x-www-form-urlencoded".to_string()
|
||||
})
|
||||
.to_lowercase()
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
leptos_dom::debug_warn!("<Form/> cannot be submitted from a tag other than <form>, <input>, or <button>");
|
||||
leptos_dom::debug_warn!(
|
||||
"<Form/> cannot be submitted from a tag other than \
|
||||
<form>, <input>, or <button>"
|
||||
);
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
None => match ev.target() {
|
||||
None => {
|
||||
leptos_dom::debug_warn!("<Form/> SubmitEvent fired without a target.");
|
||||
leptos_dom::debug_warn!(
|
||||
"<Form/> SubmitEvent fired without a target."
|
||||
);
|
||||
panic!()
|
||||
}
|
||||
Some(form) => {
|
||||
@@ -375,8 +411,9 @@ fn extract_form_attributes(
|
||||
form.get_attribute("method")
|
||||
.unwrap_or_else(|| "get".to_string()),
|
||||
form.get_attribute("action").unwrap_or_default(),
|
||||
form.get_attribute("enctype")
|
||||
.unwrap_or_else(|| "application/x-www-form-urlencoded".to_string()),
|
||||
form.get_attribute("enctype").unwrap_or_else(|| {
|
||||
"application/x-www-form-urlencoded".to_string()
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -386,7 +423,9 @@ 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)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use leptos::leptos_dom::IntoView;
|
||||
use leptos::*;
|
||||
|
||||
use crate::{use_location, use_resolved_path, State};
|
||||
use leptos::{leptos_dom::IntoView, *};
|
||||
|
||||
/// Describes a value that is either a static or a reactive URL, i.e.,
|
||||
/// a [String], a [&str], or a reactive `Fn() -> String`.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
use crate::use_route;
|
||||
use leptos::*;
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
/// Displays the child route nested in a parent route, allowing you to control exactly where
|
||||
/// that child route is displayed. Renders nothing if there is no nested child.
|
||||
@@ -19,7 +18,9 @@ pub fn Outlet(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
set_outlet.set(None);
|
||||
}
|
||||
(Some(child), Some((is_showing_val, _))) if child.id() == *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), prev) => {
|
||||
@@ -28,7 +29,8 @@ pub fn Outlet(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
_ = cx.child_scope(|child_cx| {
|
||||
provide_context(child_cx, child.clone());
|
||||
set_outlet.set(Some(child.outlet(child_cx).into_view(child_cx)));
|
||||
set_outlet
|
||||
.set(Some(child.outlet(child_cx).into_view(child_cx)));
|
||||
is_showing.set(Some((child.id(), child_cx)));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use leptos::*;
|
||||
|
||||
use crate::{
|
||||
matching::{resolve_path, PathMatch, RouteDefinition, RouteMatch},
|
||||
ParamsMap, RouterContext,
|
||||
};
|
||||
use leptos::*;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
static ROUTE_ID: Cell<usize> = Cell::new(0);
|
||||
@@ -161,7 +159,11 @@ impl RouteContext {
|
||||
self.inner.params
|
||||
}
|
||||
|
||||
pub(crate) fn base(cx: Scope, path: &str, fallback: Option<fn(Scope) -> View>) -> Self {
|
||||
pub(crate) fn base(
|
||||
cx: Scope,
|
||||
path: &str,
|
||||
fallback: Option<fn(Scope) -> View>,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: Rc::new(RouteContextInner {
|
||||
cx,
|
||||
@@ -171,14 +173,17 @@ impl RouteContext {
|
||||
path: RefCell::new(path.to_string()),
|
||||
original_path: path.to_string(),
|
||||
params: create_memo(cx, |_| ParamsMap::new()),
|
||||
outlet: Box::new(move |cx| fallback.as_ref().map(move |f| f(cx))),
|
||||
outlet: Box::new(move |cx| {
|
||||
fallback.as_ref().map(move |f| f(cx))
|
||||
}),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves a relative route, relative to the current route's path.
|
||||
pub fn resolve_path(&self, to: &str) -> Option<String> {
|
||||
resolve_path(&self.inner.base_path, to, Some(&self.inner.path.borrow())).map(String::from)
|
||||
resolve_path(&self.inner.base_path, to, Some(&self.inner.path.borrow()))
|
||||
.map(String::from)
|
||||
}
|
||||
|
||||
/// The nested child route, if any.
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
use cfg_if::cfg_if;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use leptos::*;
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
#[cfg(feature = "transition")]
|
||||
use leptos_reactive::use_transition;
|
||||
|
||||
use crate::{
|
||||
create_location, matching::resolve_path, Branch, History, Location, LocationChange,
|
||||
RouteContext, RouterIntegrationContext, State,
|
||||
create_location, matching::resolve_path, Branch, History, Location,
|
||||
LocationChange, RouteContext, RouterIntegrationContext, State,
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
use crate::{unescape, Url};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
#[cfg(feature = "transition")]
|
||||
use leptos_reactive::use_transition;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use thiserror::Error;
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
/// Provides for client-side and server-side routing. This should usually be somewhere near
|
||||
/// the root of the application.
|
||||
@@ -120,10 +115,12 @@ impl RouterContext {
|
||||
}
|
||||
|
||||
// the current URL
|
||||
let (reference, set_reference) = create_signal(cx, source.with(|s| s.value.clone()));
|
||||
let (reference, set_reference) =
|
||||
create_signal(cx, source.with(|s| s.value.clone()));
|
||||
|
||||
// the current History.state
|
||||
let (state, set_state) = create_signal(cx, source.with(|s| s.state.clone()));
|
||||
let (state, set_state) =
|
||||
create_signal(cx, source.with(|s| s.state.clone()));
|
||||
|
||||
// we'll use this transition to wait for async resources to load when navigating to a new route
|
||||
#[cfg(feature = "transition")]
|
||||
@@ -131,7 +128,8 @@ impl RouterContext {
|
||||
|
||||
// Each field of `location` reactively represents a different part of the current location
|
||||
let location = create_location(cx, reference, state);
|
||||
let referrers: Rc<RefCell<Vec<LocationChange>>> = Rc::new(RefCell::new(Vec::new()));
|
||||
let referrers: Rc<RefCell<Vec<LocationChange>>> =
|
||||
Rc::new(RefCell::new(Vec::new()));
|
||||
|
||||
// Create base route with fallback element
|
||||
let base_path = base_path.unwrap_or_default();
|
||||
@@ -220,7 +218,9 @@ impl RouterContextInner {
|
||||
return Err(NavigationError::MaxRedirects);
|
||||
}
|
||||
|
||||
if resolved_to != this.reference.get() || options.state != (this.state).get() {
|
||||
if resolved_to != this.reference.get()
|
||||
|| options.state != (this.state).get()
|
||||
{
|
||||
if cfg!(feature = "server") {
|
||||
self.history.navigate(&LocationChange {
|
||||
value: resolved_to,
|
||||
@@ -230,12 +230,14 @@ impl RouterContextInner {
|
||||
});
|
||||
} else {
|
||||
{
|
||||
self.referrers.borrow_mut().push(LocationChange {
|
||||
value: self.reference.get(),
|
||||
replace: options.replace,
|
||||
scroll: options.scroll,
|
||||
state: self.state.get(),
|
||||
});
|
||||
self.referrers.borrow_mut().push(
|
||||
LocationChange {
|
||||
value: self.reference.get(),
|
||||
replace: options.replace,
|
||||
scroll: options.scroll,
|
||||
state: self.state.get(),
|
||||
},
|
||||
);
|
||||
}
|
||||
let len = self.referrers.borrow().len();
|
||||
|
||||
@@ -319,7 +321,9 @@ impl RouterContextInner {
|
||||
// let browser handle this event if link has target,
|
||||
// or if it doesn't have href or state
|
||||
// TODO "state" is set as a prop, not an attribute
|
||||
if !target.is_empty() || (href.is_empty() && !a.has_attribute("state")) {
|
||||
if !target.is_empty()
|
||||
|| (href.is_empty() && !a.has_attribute("state"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -347,15 +351,15 @@ impl RouterContextInner {
|
||||
}
|
||||
|
||||
let to = path_name + &unescape(&url.search) + &unescape(&url.hash);
|
||||
let state = get_property(a.unchecked_ref(), "state")
|
||||
.ok()
|
||||
.and_then(|value| {
|
||||
let state = get_property(a.unchecked_ref(), "state").ok().and_then(
|
||||
|value| {
|
||||
if value == wasm_bindgen::JsValue::UNDEFINED {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
ev.prevent_default();
|
||||
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
use crate::{
|
||||
matching::{
|
||||
expand_optionals, get_route_matches, join_paths, Branch, Matcher,
|
||||
RouteDefinition, RouteMatch,
|
||||
},
|
||||
RouteContext, RouterContext,
|
||||
};
|
||||
use leptos::*;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
cmp::Reverse,
|
||||
@@ -5,16 +13,6 @@ use std::{
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use leptos::*;
|
||||
|
||||
use crate::{
|
||||
matching::{
|
||||
expand_optionals, get_route_matches, join_paths, Branch, Matcher, RouteDefinition,
|
||||
RouteMatch,
|
||||
},
|
||||
RouteContext, RouterContext,
|
||||
};
|
||||
|
||||
/// Contains route definitions and manages the actual routing process.
|
||||
///
|
||||
/// You should locate the `<Routes/>` component wherever on the page you want the routes to appear.
|
||||
@@ -92,7 +90,8 @@ pub fn Routes(
|
||||
{
|
||||
let mut prev_one = { prev.borrow()[i].clone() };
|
||||
if next_match.path_match.path != prev_one.path() {
|
||||
prev_one.set_path(next_match.path_match.path.clone());
|
||||
prev_one
|
||||
.set_path(next_match.path_match.path.clone());
|
||||
}
|
||||
if i >= next.borrow().len() {
|
||||
next.borrow_mut().push(prev_one);
|
||||
@@ -116,10 +115,12 @@ pub fn Routes(
|
||||
{
|
||||
let next = next.clone();
|
||||
move |cx| {
|
||||
if let Some(route_states) = use_context::<Memo<RouterState>>(cx)
|
||||
if let Some(route_states) =
|
||||
use_context::<Memo<RouterState>>(cx)
|
||||
{
|
||||
route_states.with(|route_states| {
|
||||
let routes = route_states.routes.borrow();
|
||||
let routes =
|
||||
route_states.routes.borrow();
|
||||
routes.get(i + 1).cloned()
|
||||
})
|
||||
} else {
|
||||
@@ -184,11 +185,15 @@ pub fn Routes(
|
||||
|
||||
if prev.is_none() || !root_equal.get() {
|
||||
let (root_view, _) = cx.run_child_scope(|cx| {
|
||||
let prev_cx = std::mem::replace(&mut *root_cx.borrow_mut(), Some(cx));
|
||||
let prev_cx = std::mem::replace(
|
||||
&mut *root_cx.borrow_mut(),
|
||||
Some(cx),
|
||||
);
|
||||
if let Some(prev_cx) = prev_cx {
|
||||
prev_cx.dispose();
|
||||
}
|
||||
root.as_ref().map(|route| route.outlet(cx).into_view(cx))
|
||||
root.as_ref()
|
||||
.map(|route| route.outlet(cx).into_view(cx))
|
||||
});
|
||||
root_view
|
||||
} else {
|
||||
@@ -231,7 +236,9 @@ impl RouteData {
|
||||
#[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 },
|
||||
|score, segment| {
|
||||
score + if segment.starts_with(':') { 2 } else { 3 }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use crate::{Branch, RouterIntegrationContext, ServerIntegration};
|
||||
use leptos::*;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use crate::{Branch, RouterIntegrationContext, ServerIntegration};
|
||||
|
||||
/// Context to contain all possible routes.
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct PossibleBranchContext(pub(crate) Rc<RefCell<Vec<Branch>>>);
|
||||
@@ -10,7 +9,9 @@ pub struct PossibleBranchContext(pub(crate) Rc<RefCell<Vec<Branch>>>);
|
||||
/// Generates a list of all routes this application could possibly serve. This returns the raw routes in the leptos_router
|
||||
/// format. Odds are you want `generate_route_list()` from either the actix or axum integrations if you want
|
||||
/// to work with their router
|
||||
pub fn generate_route_list_inner<IV>(app_fn: impl FnOnce(Scope) -> IV + 'static) -> Vec<String>
|
||||
pub fn generate_route_list_inner<IV>(
|
||||
app_fn: impl FnOnce(Scope) -> IV + 'static,
|
||||
) -> Vec<String>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
@@ -29,7 +30,9 @@ where
|
||||
let branches = branches.0.borrow();
|
||||
branches
|
||||
.iter()
|
||||
.flat_map(|branch| branch.routes.last().map(|route| route.pattern.clone()))
|
||||
.flat_map(|branch| {
|
||||
branch.routes.last().map(|route| route.pattern.clone())
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use super::params::ParamsMap;
|
||||
use crate::{State, Url};
|
||||
use leptos::*;
|
||||
|
||||
use crate::{State, Url};
|
||||
|
||||
use super::params::ParamsMap;
|
||||
|
||||
/// Creates a reactive location from the given path and state.
|
||||
pub fn create_location(cx: Scope, path: ReadSignal<String>, state: ReadSignal<State>) -> Location {
|
||||
pub fn create_location(
|
||||
cx: Scope,
|
||||
path: ReadSignal<String>,
|
||||
state: ReadSignal<State>,
|
||||
) -> Location {
|
||||
let url = create_memo(cx, move |prev: Option<&Url>| {
|
||||
path.with(|path| match Url::try_from(path.as_str()) {
|
||||
Ok(url) => url,
|
||||
@@ -16,10 +18,12 @@ pub fn create_location(cx: Scope, path: ReadSignal<String>, state: ReadSignal<St
|
||||
})
|
||||
});
|
||||
|
||||
let pathname = create_memo(cx, move |_| url.with(|url| url.pathname.clone()));
|
||||
let pathname =
|
||||
create_memo(cx, move |_| url.with(|url| url.pathname.clone()));
|
||||
let search = create_memo(cx, move |_| url.with(|url| url.search.clone()));
|
||||
let hash = create_memo(cx, move |_| url.with(|url| url.hash.clone()));
|
||||
let query = create_memo(cx, move |_| url.with(|url| url.search_params.clone()));
|
||||
let query =
|
||||
create_memo(cx, move |_| url.with(|url| url.search_params.clone()));
|
||||
|
||||
Location {
|
||||
pathname,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use leptos::*;
|
||||
use std::rc::Rc;
|
||||
|
||||
mod location;
|
||||
mod params;
|
||||
@@ -83,11 +82,19 @@ impl History for BrowserIntegration {
|
||||
|
||||
if loc.replace {
|
||||
history
|
||||
.replace_state_with_url(&loc.state.to_js_value(), "", Some(&loc.value))
|
||||
.replace_state_with_url(
|
||||
&loc.state.to_js_value(),
|
||||
"",
|
||||
Some(&loc.value),
|
||||
)
|
||||
.unwrap_throw();
|
||||
} else {
|
||||
history
|
||||
.push_state_with_url(&loc.state.to_js_value(), "", Some(&loc.value))
|
||||
.push_state_with_url(
|
||||
&loc.state.to_js_value(),
|
||||
"",
|
||||
Some(&loc.value),
|
||||
)
|
||||
.unwrap_throw();
|
||||
}
|
||||
// scroll to el
|
||||
@@ -116,7 +123,9 @@ impl History for BrowserIntegration {
|
||||
/// # use leptos_router::*;
|
||||
/// # use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// let integration = ServerIntegration { path: "insert/current/path/here".to_string() };
|
||||
/// let integration = ServerIntegration {
|
||||
/// path: "insert/current/path/here".to_string(),
|
||||
/// };
|
||||
/// provide_context(cx, RouterIntegrationContext::new(integration));
|
||||
/// # });
|
||||
/// ```
|
||||
|
||||
@@ -104,7 +104,8 @@ pub trait IntoParam
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError>;
|
||||
fn into_param(value: Option<&str>, name: &str)
|
||||
-> Result<Self, ParamsError>;
|
||||
}
|
||||
|
||||
impl<T> IntoParam for Option<T>
|
||||
@@ -112,7 +113,10 @@ where
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: std::error::Error + 'static,
|
||||
{
|
||||
fn into_param(value: Option<&str>, _name: &str) -> Result<Self, ParamsError> {
|
||||
fn into_param(
|
||||
value: Option<&str>,
|
||||
_name: &str,
|
||||
) -> Result<Self, ParamsError> {
|
||||
match value {
|
||||
None => Ok(None),
|
||||
Some(value) => match T::from_str(value) {
|
||||
|
||||
@@ -22,7 +22,8 @@ pub fn unescape(s: &str) -> String {
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn escape(s: &str) -> String {
|
||||
percent_encoding::utf8_percent_encode(s, percent_encoding::NON_ALPHANUMERIC).to_string()
|
||||
percent_encoding::utf8_percent_encode(s, percent_encoding::NON_ALPHANUMERIC)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
@@ -36,7 +37,8 @@ impl TryFrom<&str> for Url {
|
||||
|
||||
fn try_from(url: &str) -> Result<Self, Self::Error> {
|
||||
let fake_host = String::from("http://leptos");
|
||||
let url = web_sys::Url::new_with_base(url, &fake_host).map_js_error()?;
|
||||
let url =
|
||||
web_sys::Url::new_with_base(url, &fake_host).map_js_error()?;
|
||||
Ok(Self {
|
||||
origin: url.origin(),
|
||||
pathname: url.pathname(),
|
||||
@@ -44,15 +46,30 @@ impl TryFrom<&str> for Url {
|
||||
search_params: ParamsMap(
|
||||
try_iter(&url.search_params())
|
||||
.map_js_error()?
|
||||
.ok_or("Failed to use URLSearchParams as an iterator".to_string())?
|
||||
.ok_or(
|
||||
"Failed to use URLSearchParams as an iterator"
|
||||
.to_string(),
|
||||
)?
|
||||
.map(|value| {
|
||||
let array: Array = value.map_js_error()?.dyn_into().map_js_error()?;
|
||||
let array: Array =
|
||||
value.map_js_error()?.dyn_into().map_js_error()?;
|
||||
Ok((
|
||||
array.get(0).dyn_into::<JsString>().map_js_error()?.into(),
|
||||
array.get(1).dyn_into::<JsString>().map_js_error()?.into(),
|
||||
array
|
||||
.get(0)
|
||||
.dyn_into::<JsString>()
|
||||
.map_js_error()?
|
||||
.into(),
|
||||
array
|
||||
.get(1)
|
||||
.dyn_into::<JsString>()
|
||||
.map_js_error()?
|
||||
.into(),
|
||||
))
|
||||
})
|
||||
.collect::<Result<linear_map::LinearMap<String, String>, Self::Error>>()?,
|
||||
.collect::<Result<
|
||||
linear_map::LinearMap<String, String>,
|
||||
Self::Error,
|
||||
>>()?,
|
||||
),
|
||||
hash: url.hash(),
|
||||
})
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use leptos::{create_memo, use_context, Memo, Scope};
|
||||
|
||||
use crate::{
|
||||
Location, NavigateOptions, NavigationError, Params, ParamsError, ParamsMap, RouteContext,
|
||||
RouterContext,
|
||||
Location, NavigateOptions, NavigationError, Params, ParamsError, ParamsMap,
|
||||
RouteContext, RouterContext,
|
||||
};
|
||||
use leptos::{create_memo, use_context, Memo, Scope};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Returns the current [RouterContext], containing information about the router's state.
|
||||
pub fn use_router(cx: Scope) -> RouterContext {
|
||||
if let Some(router) = use_context::<RouterContext>(cx) {
|
||||
router
|
||||
} else {
|
||||
leptos::leptos_dom::debug_warn!("You must call use_router() within a <Router/> component");
|
||||
leptos::leptos_dom::debug_warn!(
|
||||
"You must call use_router() within a <Router/> component"
|
||||
);
|
||||
panic!("You must call use_router() within a <Router/> component");
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,10 @@ where
|
||||
}
|
||||
|
||||
/// Resolves the given path relative to the current route.
|
||||
pub fn use_resolved_path(cx: Scope, path: impl Fn() -> String + 'static) -> Memo<Option<String>> {
|
||||
pub fn use_resolved_path(
|
||||
cx: Scope,
|
||||
path: impl Fn() -> String + 'static,
|
||||
) -> Memo<Option<String>> {
|
||||
let route = use_route(cx);
|
||||
|
||||
create_memo(cx, move |_| {
|
||||
@@ -73,7 +76,11 @@ pub fn use_resolved_path(cx: Scope, path: impl Fn() -> String + 'static) -> Memo
|
||||
}
|
||||
|
||||
/// Returns a function that can be used to navigate to a new route.
|
||||
pub fn use_navigate(cx: Scope) -> impl Fn(&str, NavigateOptions) -> Result<(), NavigationError> {
|
||||
pub fn use_navigate(
|
||||
cx: Scope,
|
||||
) -> impl Fn(&str, NavigateOptions) -> Result<(), NavigationError> {
|
||||
let router = use_router(cx);
|
||||
move |to, options| Rc::clone(&router.inner).navigate_from_route(to, &options)
|
||||
move |to, options| {
|
||||
Rc::clone(&router.inner).navigate_from_route(to, &options)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```rust
|
||||
//!
|
||||
//!
|
||||
//! use leptos::*;
|
||||
//! use leptos_router::*;
|
||||
//!
|
||||
@@ -128,7 +128,6 @@
|
||||
//! fn About(cx: Scope) -> impl IntoView {
|
||||
//! todo!()
|
||||
//! }
|
||||
//!
|
||||
//! ```
|
||||
//!
|
||||
//! ## Module Route Definitions
|
||||
@@ -195,11 +194,9 @@ mod history;
|
||||
mod hooks;
|
||||
#[doc(hidden)]
|
||||
pub mod matching;
|
||||
pub use matching::RouteDefinition;
|
||||
|
||||
pub use components::*;
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
pub use extract_routes::*;
|
||||
pub use history::*;
|
||||
pub use hooks::*;
|
||||
pub use matching::*;
|
||||
pub use matching::{RouteDefinition, *};
|
||||
|
||||
@@ -14,31 +14,38 @@ pub fn expand_optionals(pattern: &str) -> Vec<Cow<str>> {
|
||||
match captures {
|
||||
None => vec![pattern.into()],
|
||||
Some(matched) => {
|
||||
let start: usize = js_sys::Reflect::get(&matched, &JsValue::from_str("index"))
|
||||
.unwrap()
|
||||
.as_f64()
|
||||
.unwrap() as usize;
|
||||
let start: usize =
|
||||
js_sys::Reflect::get(&matched, &JsValue::from_str("index"))
|
||||
.unwrap()
|
||||
.as_f64()
|
||||
.unwrap() as usize;
|
||||
let mut prefix = pattern[0..start].to_string();
|
||||
let mut suffix = &pattern[start + matched.get(1).as_string().unwrap().len()..];
|
||||
let mut suffix =
|
||||
&pattern[start + matched.get(1).as_string().unwrap().len()..];
|
||||
let mut prefixes = vec![prefix.clone()];
|
||||
|
||||
prefix += &matched.get(1).as_string().unwrap();
|
||||
prefixes.push(prefix.clone());
|
||||
|
||||
while let Some(matched) = OPTIONAL_RE_2.exec(suffix.trim_start_matches('?')) {
|
||||
while let Some(matched) =
|
||||
OPTIONAL_RE_2.exec(suffix.trim_start_matches('?'))
|
||||
{
|
||||
prefix += &matched.get(1).as_string().unwrap();
|
||||
prefixes.push(prefix.clone());
|
||||
suffix = &suffix[matched.get(0).as_string().unwrap().len()..];
|
||||
}
|
||||
|
||||
expand_optionals(suffix)
|
||||
.iter()
|
||||
.fold(Vec::new(), |mut results, expansion| {
|
||||
expand_optionals(suffix).iter().fold(
|
||||
Vec::new(),
|
||||
|mut results, expansion| {
|
||||
results.extend(prefixes.iter().map(|prefix| {
|
||||
Cow::Owned(prefix.clone() + expansion.trim_start_matches('?'))
|
||||
Cow::Owned(
|
||||
prefix.clone() + expansion.trim_start_matches('?'),
|
||||
)
|
||||
}));
|
||||
results
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,20 +72,25 @@ pub fn expand_optionals(pattern: &str) -> Vec<Cow<str>> {
|
||||
prefix += &captures[1];
|
||||
prefixes.push(prefix.clone());
|
||||
|
||||
while let Some(captures) = OPTIONAL_RE_2.captures(suffix.trim_start_matches('?')) {
|
||||
while let Some(captures) =
|
||||
OPTIONAL_RE_2.captures(suffix.trim_start_matches('?'))
|
||||
{
|
||||
prefix += &captures[1];
|
||||
prefixes.push(prefix.clone());
|
||||
suffix = &suffix[captures[0].len()..];
|
||||
}
|
||||
|
||||
expand_optionals(suffix)
|
||||
.iter()
|
||||
.fold(Vec::new(), |mut results, expansion| {
|
||||
expand_optionals(suffix).iter().fold(
|
||||
Vec::new(),
|
||||
|mut results, expansion| {
|
||||
results.extend(prefixes.iter().map(|prefix| {
|
||||
Cow::Owned(prefix.clone() + expansion.trim_start_matches('?'))
|
||||
Cow::Owned(
|
||||
prefix.clone() + expansion.trim_start_matches('?'),
|
||||
)
|
||||
}));
|
||||
results
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,9 @@ impl Matcher {
|
||||
// quick path: not a match if
|
||||
// 1) matcher has add'l segments not found in location
|
||||
// 2) location has add'l segments, there's no splat, and partial matches not allowed
|
||||
if loc_len < self.len || (len_diff > 0 && self.splat.is_none() && !self.partial) {
|
||||
if loc_len < self.len
|
||||
|| (len_diff > 0 && self.splat.is_none() && !self.partial)
|
||||
{
|
||||
None
|
||||
}
|
||||
// otherwise, start building a match
|
||||
@@ -68,7 +70,9 @@ impl Matcher {
|
||||
let mut path = String::new();
|
||||
let mut params = ParamsMap::new();
|
||||
|
||||
for (segment, loc_segment) in self.segments.iter().zip(loc_segments.iter()) {
|
||||
for (segment, loc_segment) in
|
||||
self.segments.iter().zip(loc_segments.iter())
|
||||
{
|
||||
if let Some(param_name) = segment.strip_prefix(':') {
|
||||
params.insert(param_name.into(), (*loc_segment).into());
|
||||
} else if segment != loc_segment {
|
||||
|
||||
@@ -3,20 +3,22 @@ mod matcher;
|
||||
mod resolve_path;
|
||||
mod route;
|
||||
|
||||
use crate::RouteData;
|
||||
pub use expand_optionals::*;
|
||||
pub use matcher::*;
|
||||
pub use resolve_path::*;
|
||||
pub use route::*;
|
||||
|
||||
use crate::RouteData;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct RouteMatch {
|
||||
pub path_match: PathMatch,
|
||||
pub route: RouteData,
|
||||
}
|
||||
|
||||
pub(crate) fn get_route_matches(branches: Vec<Branch>, location: String) -> Vec<RouteMatch> {
|
||||
pub(crate) fn get_route_matches(
|
||||
branches: Vec<Branch>,
|
||||
location: String,
|
||||
) -> Vec<RouteMatch> {
|
||||
for branch in branches {
|
||||
if let Some(matches) = branch.matcher(&location) {
|
||||
return matches;
|
||||
|
||||
@@ -17,7 +17,9 @@ pub fn resolve_path<'a>(
|
||||
let result = if let Some(from_path) = from_path {
|
||||
if path.starts_with('/') {
|
||||
base_path
|
||||
} else if from_path.to_lowercase().find(&base_path.to_lowercase()) != Some(0) {
|
||||
} else if from_path.to_lowercase().find(&base_path.to_lowercase())
|
||||
!= Some(0)
|
||||
{
|
||||
base_path + from_path
|
||||
} else {
|
||||
from_path
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use leptos::{leptos_dom::View, *};
|
||||
use std::rc::Rc;
|
||||
|
||||
use leptos::leptos_dom::View;
|
||||
use leptos::*;
|
||||
|
||||
/// Defines a single route in a nested route tree. This is the return
|
||||
/// type of the [`<Route/>`](crate::Route) component, but can also be
|
||||
/// used to build your own configuration-based or filesystem-based routing.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
imports_granularity = "Crate"
|
||||
max_width = 80
|
||||
format_strings = true
|
||||
imports_layout = "vertical"
|
||||
group_imports = "One"
|
||||
format_code_in_doc_comments = true
|
||||
format_code_in_doc_comments = true
|
||||
|
||||
Reference in New Issue
Block a user