apply new formatting everywhere (#502)

This commit is contained in:
Greg Johnston
2023-02-11 14:30:06 -05:00
committed by GitHub
parent d4648da5c6
commit 6bab4ad966
80 changed files with 5124 additions and 4241 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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>;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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>"
);
});
}

View File

@@ -1,5 +1,4 @@
use std::{net::AddrParseError, num::ParseIntError};
use thiserror::Error;
#[derive(Debug, Error, Clone)]

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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)
}
}

View File

@@ -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))
}
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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,
];

View File

@@ -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

View File

@@ -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;
}
}
}

View File

@@ -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;
/// macros 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"),
}
}

View File

@@ -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)
/// macros 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();
}
}

View File

@@ -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)
/// macros 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();
}

View File

@@ -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. Its \
possible this is intentional, but its also possible that youre \
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. \
Its possible this is intentional, but its also \
possible that youre 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> {}

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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(),
);

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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>");

View File

@@ -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()

View File

@@ -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()

View File

@@ -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."
);
}
})
}

View File

@@ -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 {

View File

@@ -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")]

View File

@@ -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",
);
}
}
})

View File

@@ -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>>,
}

View File

@@ -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)

View File

@@ -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 signals 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);
}
}

View File

@@ -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] Youre 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] Youre 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

View File

@@ -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);

View File

@@ -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(),
}

View File

@@ -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).
///

View File

@@ -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()

View File

@@ -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);

View File

@@ -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]

View File

@@ -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;
}
});

View File

@@ -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,
{

View File

@@ -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()))
}
}

View File

@@ -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,
{

View File

@@ -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)]

View File

@@ -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)]

View File

@@ -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());

View File

@@ -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)]

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)]

View File

@@ -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)]

View File

@@ -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)
}

View File

@@ -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`.

View File

@@ -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)));
});
}

View File

@@ -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.

View File

@@ -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();

View File

@@ -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 }
},
)
}
}

View File

@@ -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()
})
}

View File

@@ -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,

View File

@@ -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));
/// # });
/// ```

View File

@@ -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) {

View File

@@ -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(),
})

View File

@@ -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)
}
}

View File

@@ -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, *};

View File

@@ -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
})
},
)
}
}
}

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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

View File

@@ -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.

View File

@@ -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