Compare commits

..

17 Commits

Author SHA1 Message Date
Greg Johnston
2dd8b78336 v0.3.1 2023-06-14 09:56:41 -04:00
Greg Johnston
fd901d3e53 tests: fix broken SSR doctests (#1056) 2023-06-14 09:14:13 -04:00
Nova
d517f01e58 fix: replace ouroboros with self_cell (#1171) 2023-06-14 08:15:06 -04:00
Greg Johnston
904c2e8a67 v0.3.0 2023-05-13 19:44:06 -04:00
Greg Johnston
a5c3be586a docs: tweak new slice docs 2023-05-13 19:43:17 -04:00
Markus Kohlhase
9f5139d929 examples: fix trunk config to run tailwind at the right time (#1040) 2023-05-13 19:39:36 -04:00
sjud
bae305340e change: update create_slice to allow different types on getter and setter (#1036) 2023-05-13 19:39:17 -04:00
Greg Johnston
40c1556f29 change: remove APIs that had been marked deprecated (#1037) 2023-05-12 19:45:48 -04:00
Greg Johnston
0db4f5821f fix: avoid extra { escaping (closes #1035) (#1038) 2023-05-12 16:29:33 -04:00
Greg Johnston
12ebc95800 fix: flickering <Transition/> in release mode (closes #960) (#1030) 2023-05-11 14:51:33 -04:00
Greg Johnston
d7b919032e feat: SsrMode::PartiallyBlocked (#1026) 2023-05-10 13:30:01 -04:00
Greg Johnston
be8bf8b0d6 fix: corrects error-deserialization behavior of ActionForm (closes #1024) (#1025) 2023-05-09 06:40:22 -04:00
Greg Johnston
f84f1422f4 fix: maintain insertion order of meta tags (#1021) 2023-05-08 08:36:54 -04:00
Snêu
b01976e3bb examples: fix indentations (#1017) 2023-05-08 08:36:45 -04:00
agilarity
50b48fb272 chore: build CSS with trunk (#1016)
This configures a hook to run the tailwindcss CLI when a build is triggered or retriggered via Trunk watch. It eliminates the need to run the tailwindcss manually.
2023-05-08 08:36:07 -04:00
agilarity
1617e31d69 CI: clean up examples after verification (#1019)
* build: improve task names

* build: add clean-examples task

Make it easy to clean all the cargo and trunk files in the examples.

* build: clean after verify
2023-05-08 08:35:27 -04:00
Chris
51cd082d4c docs: add examples for manual server integration for router (#1015) 2023-05-08 08:34:43 -04:00
33 changed files with 487 additions and 636 deletions

View File

@@ -25,22 +25,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.3.0-alpha"
version = "0.3.1"
[workspace.dependencies]
leptos = { path = "./leptos", default-features = false, version = "0.3.0-alpha" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.3.0-alpha" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.3.0-alpha" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.3.0-alpha" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.3.0-alpha" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.3.0-alpha" }
server_fn = { path = "./server_fn", default-features = false, version = "0.3.0-alpha" }
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.3.0-alpha" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.3.0-alpha" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.3.0-alpha" }
leptos_router = { path = "./router", version = "0.3.0-alpha" }
leptos_meta = { path = "./meta", default-features = false, version = "0.3.0-alpha" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.3.0-alpha" }
leptos = { path = "./leptos", default-features = false, version = "0.3.1" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.3.1" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.3.1" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.3.1" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.3.1" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.3.1" }
server_fn = { path = "./server_fn", default-features = false, version = "0.3.1" }
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.3.1" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.3.1" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.3.1" }
leptos_router = { path = "./router", version = "0.3.1" }
leptos_meta = { path = "./meta", default-features = false, version = "0.3.1" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.3.1" }
[profile.release]
codegen-units = 1

View File

@@ -69,7 +69,11 @@ dependencies = [
[tasks.test]
clear = true
dependencies = ["test-all", "test-leptos_macro-example", "doc-leptos_macro-example"]
dependencies = [
"test-all",
"test-leptos_macro-example",
"doc-leptos_macro-example",
]
[tasks.test-all]
command = "cargo"
@@ -102,9 +106,15 @@ cwd = "examples"
command = "cargo"
args = ["make", "verify-flow"]
[tasks.clean-examples]
description = "Clean all example projects"
cwd = "examples"
command = "cargo"
args = ["make", "clean-all"]
[env]
RUSTFLAGS = ""
LEPTOS_OUTPUT_NAME="ci" # allows examples to check/build without cargo-leptos
LEPTOS_OUTPUT_NAME = "ci" # allows examples to check/build without cargo-leptos
[env.github-actions]
RUSTFLAGS = "-D warnings"

View File

@@ -31,7 +31,7 @@ CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
[tasks.verify-flow]
description = "Provides pre and post hooks for verify"
dependencies = ["pre-verify-flow", "verify", "post-verify-flow"]
dependencies = ["pre-verify", "verify", "post-verify"]
[tasks.verify]
description = "Run all quality checks and tests"
@@ -41,16 +41,17 @@ dependencies = ["check-style", "test-unit-and-web"]
description = "Run all unit and web tests"
dependencies = ["test-flow", "web-test-flow"]
[tasks.pre-verify-flow]
[tasks.pre-verify]
[tasks.post-verify-flow]
[tasks.post-verify]
dependencies = ["clean-all"]
[tasks.web-test-flow]
description = "Provides pre and post hooks for web-test"
dependencies = ["pre-web-test-flow", "web-test", "post-web-test-flow"]
dependencies = ["pre-web-test", "web-test", "post-web-test"]
[tasks.pre-web-test-flow]
[tasks.pre-web-test]
[tasks.web-test]
[tasks.post-web-test-flow]
[tasks.post-web-test]

View File

@@ -12,3 +12,12 @@ dependencies = ["check-style", "test-local"]
[tasks.test-local]
description = "Run all tests from an example directory"
dependencies = ["test", "web-test"]
[tasks.clean-trunk]
description = "Runs the trunk clean command."
category = "Cleanup"
command = "trunk"
args = ["clean"]
[tasks.clean-all]
dependencies = ["clean", "clean-trunk"]

View File

@@ -21,7 +21,7 @@ if #[cfg(feature = "ssr")] {
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(options.to_owned(), |cx| error_template(cx, None));
handler(req).await.into_response()

View File

@@ -22,7 +22,7 @@ if #[cfg(feature = "ssr")] {
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
res.into_response()
} else{
let mut errors = Errors::default();
errors.insert_with_default_key(TodoAppError::NotFound);

View File

@@ -20,7 +20,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(
options.to_owned(),

View File

@@ -5,7 +5,7 @@ This is a template demonstrating how to integrate [TailwindCSS](https://tailwind
Install Tailwind and build the CSS:
`npx tailwindcss -i ./input.css -o ./style/output.css --watch`
`Trunk.toml` is configured to build the CSS automatically.
Install trunk to client side render this bundle.

View File

@@ -0,0 +1,4 @@
[[hooks]]
stage = "pre_build"
command = "sh"
command_arguments = ["-c", "npx tailwindcss -i input.css -o style/output.css"]

View File

@@ -1,5 +1,5 @@
/*
! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com
! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com
*/
/*
@@ -31,6 +31,7 @@
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
*/
html {
@@ -47,6 +48,8 @@ html {
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
}
/*
@@ -433,6 +436,9 @@ video {
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
@@ -480,6 +486,9 @@ video {
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;

View File

@@ -22,7 +22,7 @@ if #[cfg(feature = "ssr")] {
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
res.into_response()
} else{
let mut errors = Errors::default();
errors.insert_with_default_key(TodoAppError::NotFound);

View File

@@ -16,9 +16,9 @@ use actix_web::{
use futures::{Stream, StreamExt};
use http::StatusCode;
use leptos::{
leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context,
leptos_server::{server_fn_by_path, Payload},
server_fn::Encoding,
ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement,
*,
};
use leptos_integration_utils::{build_async_response, html_parts_separated};
@@ -514,6 +514,43 @@ pub fn render_app_to_stream_with_context<IV>(
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
method: Method,
) -> Route
where
IV: IntoView,
{
render_app_to_stream_with_context_and_replace_blocks(
options,
additional_context,
app_fn,
method,
false,
)
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application.
///
/// This function allows you to provide additional information to Leptos for your route.
/// It could be used to pass in Path Info, Connection Info, or anything your heart desires.
///
/// `replace_blocks` additionally lets you specify whether `<Suspense/>` fragments that read
/// from blocking resources should be retrojected into the HTML that's initially served, rather
/// than dynamically inserting them with JavaScript on the client. This means you will have
/// better support if JavaScript is not enabled, in exchange for a marginally slower response time.
///
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
options: LeptosOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
method: Method,
replace_blocks: bool,
) -> Route
where
IV: IntoView,
{
@@ -533,7 +570,14 @@ where
}
};
stream_app(&options, app, res_options, additional_context).await
stream_app(
&options,
app,
res_options,
additional_context,
replace_blocks,
)
.await
}
};
match method {
@@ -653,103 +697,6 @@ where
}
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_stream](leptos::ssr::render_to_stream), and
/// includes everything described in the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use actix_web::{App, HttpServer};
/// use leptos::*;
/// 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> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
/// 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/> },
/// ),
/// )
/// })
/// .bind(&addr)?
/// .run()
/// .await
/// }
/// # }
/// ```
///
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[deprecated = "You can now use `render_app_async` with `create_resource` and \
`<Suspense/>` to achieve async rendering without manually \
preloading data."]
pub fn render_preloaded_data_app<Data, Fut, IV>(
options: LeptosOptions,
data_fn: impl Fn(HttpRequest) -> Fut + Clone + 'static,
app_fn: impl Fn(leptos::Scope, Data) -> IV + Clone + Send + 'static,
) -> Route
where
Data: 'static,
Fut: Future<Output = Result<DataResponse<Data>, actix_web::Error>>,
IV: IntoView + 'static,
{
web::get().to(move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
let data_fn = data_fn.clone();
let res_options = ResponseOptions::default();
async move {
let data = match data_fn(req.clone()).await {
Err(e) => return HttpResponse::from_error(e),
Ok(DataResponse::Response(r)) => return r.into(),
Ok(DataResponse::Data(d)) => d,
};
let app = {
let app_fn = app_fn.clone();
let res_options = res_options.clone();
move |cx| {
provide_contexts(cx, &req, res_options);
(app_fn)(cx, data).into_view(cx)
}
};
stream_app(&options, app, res_options, |_cx| {}).await
}
})
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn provide_contexts(
cx: leptos::Scope,
@@ -781,12 +728,14 @@ async fn stream_app(
app: impl FnOnce(leptos::Scope) -> View + 'static,
res_options: ResponseOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
replace_blocks: bool,
) -> HttpResponse<BoxBody> {
let (stream, runtime, scope) =
render_to_stream_with_prefix_undisposed_with_context(
render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
app,
move |cx| generate_head_metadata_separated(cx).1.into(),
additional_context,
replace_blocks
);
build_stream_response(options, res_options, stream, runtime, scope).await
@@ -985,22 +934,6 @@ pub trait LeptosRoutes {
where
IV: IntoView + 'static;
#[deprecated = "You can now use `leptos_routes` and a `<Route \
mode=SsrMode::Async/>`
to achieve async rendering without manually preloading \
data."]
fn leptos_preloaded_data_routes<Data, Fut, IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
data_fn: impl Fn(HttpRequest) -> Fut + Clone + 'static,
app_fn: impl Fn(leptos::Scope, Data) -> IV + Clone + Send + 'static,
) -> Self
where
Data: 'static,
Fut: Future<Output = Result<DataResponse<Data>, actix_web::Error>>,
IV: IntoView + 'static;
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
@@ -1035,34 +968,7 @@ where
{
self.leptos_routes_with_context(options, paths, |_| {}, app_fn)
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn leptos_preloaded_data_routes<Data, Fut, IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
data_fn: impl Fn(HttpRequest) -> Fut + Clone + 'static,
app_fn: impl Fn(leptos::Scope, Data) -> IV + Clone + Send + 'static,
) -> Self
where
Data: 'static,
Fut: Future<Output = Result<DataResponse<Data>, actix_web::Error>>,
IV: IntoView + 'static,
{
let mut router = self;
for path in paths.iter() {
router = router.route(
path,
#[allow(deprecated)]
render_preloaded_data_app(
options.clone(),
data_fn.clone(),
app_fn.clone(),
),
);
}
router
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn leptos_routes_with_context<IV>(
self,
@@ -1091,6 +997,15 @@ where
method,
)
}
SsrMode::PartiallyBlocked => {
render_app_to_stream_with_context_and_replace_blocks(
options.clone(),
additional_context.clone(),
app_fn.clone(),
method,
true,
)
}
SsrMode::InOrder => {
render_app_to_stream_in_order_with_context(
options.clone(),

View File

@@ -129,23 +129,6 @@ pub fn redirect(cx: leptos::Scope, path: &str) {
}
}
/// Decomposes an HTTP request into its parts, allowing you to read its headers
/// and other data without consuming the body.
#[deprecated(note = "Replaced with generate_request_and_parts() to allow for \
putting LeptosRequest in the Context")]
pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
// provide request headers as context in server scope
let (parts, body) = req.into_parts();
let body = body::to_bytes(body).await.unwrap_or_default();
RequestParts {
method: parts.method,
uri: parts.uri,
headers: parts.headers,
version: parts.version,
body,
}
}
/// Decomposes an HTTP request into its parts, allowing you to read its headers
/// and other data without consuming the body. Creates a new Request from the
/// original parts for further processing
@@ -622,6 +605,54 @@ pub fn render_app_to_stream_with_context<IV>(
+ 'static
where
IV: IntoView,
{
render_app_to_stream_with_context_and_replace_blocks(
options,
additional_context,
app_fn,
false,
)
}
/// 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.
///
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
/// the data to leptos in a closure.
///
/// `replace_blocks` additionally lets you specify whether `<Suspense/>` fragments that read
/// from blocking resources should be retrojected into the HTML that's initially served, rather
/// than dynamically inserting them with JavaScript on the client. This means you will have
/// better support if JavaScript is not enabled, in exchange for a marginally slower response time.
///
/// Otherwise, this function is identical to [render_app_to_stream_with_context].
///
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [RequestParts]
/// - [ResponseOptions]
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
#[tracing::instrument(level = "info", fields(error), skip_all)]
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
options: LeptosOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
replace_blocks: bool,
) -> impl Fn(
Request<Body>,
) -> Pin<
Box<
dyn Future<Output = Response<StreamBody<PinnedHtmlStream>>>
+ Send
+ 'static,
>,
> + Clone
+ Send
+ 'static
where
IV: IntoView,
{
move |req: Request<Body>| {
Box::pin({
@@ -651,10 +682,11 @@ where
}
};
let (bundle, runtime, scope) =
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
app,
|cx| generate_head_metadata_separated(cx).1.into(),
add_context,
replace_blocks
);
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
@@ -663,6 +695,7 @@ where
})
}
}
#[tracing::instrument(level = "info", fields(error), skip_all)]
async fn generate_response(
res_options: ResponseOptions,
@@ -1173,6 +1206,21 @@ impl LeptosRoutes for axum::Router {
leptos_router::Method::Patch => patch(s),
}
}
SsrMode::PartiallyBlocked => {
let s = render_app_to_stream_with_context_and_replace_blocks(
options.clone(),
additional_context.clone(),
app_fn.clone(),
true
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
}
}
SsrMode::InOrder => {
let s = render_app_to_stream_in_order_with_context(
options.clone(),

View File

@@ -506,6 +506,48 @@ pub fn render_app_to_stream_with_context<IV>(
+ 'static
where
IV: IntoView,
{
render_app_to_stream_with_context_and_replace_blocks(
options,
additional_context,
app_fn,
false,
)
}
/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application.
///
/// This version allows us to pass Viz State/Extractor or other infro from Viz or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
/// the data to leptos in a closure.
///
/// `replace_blocks` additionally lets you specify whether `<Suspense/>` fragments that read
/// from blocking resources should be retrojected into the HTML that's initially served, rather
/// than dynamically inserting them with JavaScript on the client. This means you will have
/// better support if JavaScript is not enabled, in exchange for a marginally slower response time.
///
/// Otherwise, this function is identical to [render_app_to_stream_with_context].
///
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [RequestParts]
/// - [ResponseOptions]
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
pub fn render_app_to_stream_with_context_and_replace_blocks<IV>(
options: LeptosOptions,
additional_context: impl Fn(leptos::Scope) + Clone + Send + 'static,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
replace_blocks: bool,
) -> impl Fn(
Request,
) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + 'static>>
+ Clone
+ Send
+ 'static
where
IV: IntoView,
{
move |req: Request| {
Box::pin({
@@ -548,10 +590,11 @@ where
};
let (bundle, runtime, scope) =
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context(
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
app,
|cx| generate_head_metadata_separated(cx).1.into(),
add_context,
replace_blocks
);
forward_stream(&options, res_options2, bundle, runtime, scope, tx).await;
@@ -1091,6 +1134,22 @@ impl LeptosRoutes for Router {
leptos_router::Method::Patch => router.patch(path, s),
}
}
SsrMode::PartiallyBlocked => {
let s =
render_app_to_stream_with_context_and_replace_blocks(
options.clone(),
additional_context.clone(),
app_fn.clone(),
true,
);
match method {
leptos_router::Method::Get => router.get(path, s),
leptos_router::Method::Post => router.post(path, s),
leptos_router::Method::Put => router.put(path, s),
leptos_router::Method::Delete => router.delete(path, s),
leptos_router::Method::Patch => router.patch(path, s),
}
}
SsrMode::InOrder => {
let s = render_app_to_stream_in_order_with_context(
options.clone(),

View File

@@ -152,7 +152,6 @@ pub use leptos_config::{self, get_configuration, LeptosOptions};
pub mod ssr {
pub use leptos_dom::{ssr::*, ssr_in_order::*};
}
#[allow(deprecated)]
pub use leptos_dom::{
self, create_node_ref, debug_warn, document, error, ev,
helpers::{
@@ -161,7 +160,6 @@ pub use leptos_dom::{
request_idle_callback, request_idle_callback_with_handle, set_interval,
set_interval_with_handle, set_timeout, set_timeout_with_handle,
window_event_listener, window_event_listener_untyped,
window_event_listener_with_precast,
},
html, log, math, mount_to, mount_to_body, svg, warn, window, Attribute,
Class, CollectView, Errors, Fragment, HtmlElement, IntoAttribute,

View File

@@ -56,16 +56,17 @@ use std::rc::Rc;
tracing::instrument(level = "info", skip_all)
)]
#[component(transparent)]
pub fn Suspense<F, E>(
pub fn Suspense<F, E, V>(
cx: Scope,
/// Returns a fallback UI that will be shown while `async` [Resources](leptos_reactive::Resource) are still loading.
fallback: F,
/// Children will be displayed once all `async` [Resources](leptos_reactive::Resource) have resolved.
children: Box<dyn Fn(Scope) -> Fragment>,
children: Box<dyn Fn(Scope) -> V>,
) -> impl IntoView
where
F: Fn() -> E + 'static,
E: IntoView,
V: IntoView + 'static,
{
let context = SuspenseContext::new(cx);
@@ -74,7 +75,6 @@ where
let orig_child = Rc::new(children);
let before_me = HydrationCtx::peek();
let current_id = HydrationCtx::next_component();
#[cfg(any(feature = "csr", feature = "hydrate"))]
let prev_disposer = Rc::new(RefCell::new(None::<ScopeDisposer>));
@@ -161,7 +161,7 @@ where
_ => unreachable!(),
};
HydrationCtx::continue_from(before_me);
HydrationCtx::continue_from(current_id.clone());
leptos_dom::View::Suspense(current_id, core_component)
}

View File

@@ -78,7 +78,7 @@ where
F: Fn() -> E + 'static,
E: IntoView,
{
let prev_children = Rc::new(RefCell::new(None::<Vec<View>>));
let prev_children = Rc::new(RefCell::new(None::<View>));
let first_run = Rc::new(std::cell::Cell::new(true));
let child_runs = Cell::new(0);
@@ -112,13 +112,13 @@ where
}
})
.children(Box::new(move |cx| {
let frag = children(cx);
let frag = children(cx).into_view(cx);
let suspense_context = use_context::<SuspenseContext>(cx)
.expect("there to be a SuspenseContext");
if cfg!(feature = "hydrate") || !first_run.get() {
*prev_children.borrow_mut() = Some(frag.nodes.clone());
*prev_children.borrow_mut() = Some(frag.clone());
}
if is_first_run(&first_run, &suspense_context) {
let has_local_only = suspense_context.has_local_only()

View File

@@ -14,16 +14,13 @@ fn simple_ssr_test() {
</div>
};
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<!--leptos-view|leptos-tests-ssr.rs-8|open--><div \
id=\"_0-1\"><button id=\"_0-2\">-1</button><span \
assert!(rendered.into_view(cx).render_to_string(cx).contains(
"<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><!--leptos-view|leptos-tests-ssr.\
rs-8|close-->"
);
id=\"_0-5\">+1</button></div>"
));
});
}
@@ -54,28 +51,13 @@ fn ssr_test_with_components() {
</div>
};
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<!--leptos-view|leptos-tests-ssr.rs-49|open--><div id=\"_0-1\" \
class=\"counters\"><!--hk=_0-1-0o|leptos-counter-start--><!\
--leptos-view|leptos-tests-ssr.rs-38|open--><div \
id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
assert!(rendered.into_view(cx).render_to_string(cx).contains(
"<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><!--leptos-view|leptos-tests-ssr.\
rs-38|close--><!--hk=_0-1-0c|leptos-counter-end--><!\
--hk=_0-1-5-0o|leptos-counter-start--><!\
--leptos-view|leptos-tests-ssr.rs-38|open--><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><!\
--leptos-view|leptos-tests-ssr.rs-38|close--><!\
--hk=_0-1-5-0c|leptos-counter-end--></div><!\
--leptos-view|leptos-tests-ssr.rs-49|close-->"
);
id=\"_0-1-5\">+1</button></div>"
));
});
}
@@ -106,29 +88,13 @@ fn ssr_test_with_snake_case_components() {
</div>
};
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<!--leptos-view|leptos-tests-ssr.rs-101|open--><div id=\"_0-1\" \
class=\"counters\"><!\
--hk=_0-1-0o|leptos-snake-case-counter-start--><!\
--leptos-view|leptos-tests-ssr.rs-90|open--><div \
id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span \
assert!(rendered.into_view(cx).render_to_string(cx).contains(
"<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><!--leptos-view|leptos-tests-ssr.\
rs-90|close--><!--hk=_0-1-0c|leptos-snake-case-counter-end--><!\
--hk=_0-1-5-0o|leptos-snake-case-counter-start--><!\
--leptos-view|leptos-tests-ssr.rs-90|open--><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><!\
--leptos-view|leptos-tests-ssr.rs-90|close--><!\
--hk=_0-1-5-0c|leptos-snake-case-counter-end--></div><!\
--leptos-view|leptos-tests-ssr.rs-101|close-->"
);
id=\"_0-1-5\">+1</button></div>"
));
});
}
@@ -144,12 +110,10 @@ fn test_classes() {
<div class="my big" class:a={move || value.get() > 10} class:red=true class:car={move || value.get() > 1}></div>
};
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<!--leptos-view|leptos-tests-ssr.rs-142|open--><div id=\"_0-1\" \
class=\"my big red \
car\"></div><!--leptos-view|leptos-tests-ssr.rs-142|close-->"
);
assert!(rendered
.into_view(cx)
.render_to_string(cx)
.contains("<div id=\"_0-1\" class=\"my big red car\"></div>"));
});
}
@@ -168,13 +132,10 @@ fn ssr_with_styles() {
</div>
};
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<!--leptos-view|leptos-tests-ssr.rs-164|open--><div id=\"_0-1\" \
class=\" myclass\"><button id=\"_0-2\" class=\"btn \
myclass\">-1</button></div><!--leptos-view|leptos-tests-ssr.\
rs-164|close-->"
);
assert!(rendered.into_view(cx).render_to_string(cx).contains(
"<div id=\"_0-1\" class=\" myclass\"><button id=\"_0-2\" \
class=\"btn myclass\">-1</button></div>"
));
});
}
@@ -190,11 +151,9 @@ fn ssr_option() {
<option/>
};
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<!--leptos-view|leptos-tests-ssr.rs-188|open--><option \
id=\"_0-1\"></option><!--leptos-view|leptos-tests-ssr.\
rs-188|close-->"
);
assert!(rendered
.into_view(cx)
.render_to_string(cx)
.contains("<option id=\"_0-1\"></option>"));
});
}

View File

@@ -332,32 +332,8 @@ impl IntervalHandle {
any(debug_assertions, features = "ssr"),
instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
#[deprecated = "use set_interval_with_handle() instead. In the future, \
set_interval() will no longer return a handle, for consistency \
with other timer helper functions."]
pub fn set_interval(
cb: impl Fn() + 'static,
duration: Duration,
) -> Result<IntervalHandle, JsValue> {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
let span = ::tracing::Span::current();
let cb = move || {
leptos_reactive::SpecialNonReactiveZone::enter();
let _guard = span.enter();
cb();
leptos_reactive::SpecialNonReactiveZone::exit();
};
}
}
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))
pub fn set_interval(cb: impl Fn() + 'static, duration: Duration) {
_ = set_interval_with_handle(cb, duration);
}
/// Repeatedly calls the given function, with a delay of the given duration between calls,
@@ -402,24 +378,6 @@ pub fn set_interval_with_handle(
si(Box::new(cb), duration)
}
/// Adds an event listener to the `Window`.
#[cfg_attr(
any(debug_assertions, features = "ssr"),
instrument(level = "trace", skip_all, fields(event_name = %event_name))
)]
#[inline(always)]
#[deprecated = "In the next release, `window_event_listener` will become \
typed. You can switch now to `window_event_listener_untyped` \
for the current behavior or use \
`window_event_listener_with_precast`, which will become the \
new`window_event_listener`."]
pub fn window_event_listener(
event_name: &str,
cb: impl Fn(web_sys::Event) + 'static,
) {
window_event_listener_untyped(event_name, cb)
}
/// Adds an event listener to the `Window`, typed as a generic `Event`.
#[cfg_attr(
debug_assertions,
@@ -456,8 +414,21 @@ pub fn window_event_listener_untyped(
}
}
/// Creates a window event listener where the event in the callback is already appropriately cast.
pub fn window_event_listener_with_precast<E: ev::EventDescriptor + 'static>(
/// Creates a window event listener from a typed event.
/// ```
/// use leptos::{leptos_dom::helpers::window_event_listener, *};
///
/// #[component]
/// fn App(cx: Scope) -> impl IntoView {
/// window_event_listener(ev::keypress, |ev| {
/// // ev is typed as KeyboardEvent automatically,
/// // so .code() can be called
/// let code = ev.code();
/// log!("code = {code:?}");
/// })
/// }
/// ```
pub fn window_event_listener<E: ev::EventDescriptor + 'static>(
event: E,
cb: impl Fn(E::EventType) + 'static,
) where

View File

@@ -382,26 +382,6 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
}
}
#[doc(hidden)]
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
#[deprecated = "Use HtmlElement::from_chunks() instead."]
pub fn from_html(
cx: Scope,
element: El,
html: impl Into<Cow<'static, str>>,
) -> Self {
Self {
cx,
attrs: smallvec![],
children: ElementChildren::Chunks(vec![StringOrView::String(
html.into(),
)]),
element,
#[cfg(debug_assertions)]
view_marker: None,
}
}
#[doc(hidden)]
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub fn from_chunks(

View File

@@ -79,12 +79,6 @@ pub fn create_node_ref<T: ElementDescriptor + 'static>(
}
impl<T: ElementDescriptor + 'static> NodeRef<T> {
/// Creates an empty reference.
#[deprecated = "Use `create_node_ref` instead of `NodeRef::new()`."]
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.

View File

@@ -146,6 +146,44 @@ 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,
) -> (impl Stream<Item = String>, RuntimeId, ScopeId) {
render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
view,
prefix,
additional_context,
false,
)
}
/// Renders a function to a stream of HTML strings and returns the [Scope] and [RuntimeId] that were created, so
/// they can be disposed when appropriate. After the `view` runs, the `prefix` will run with
/// the same scope. This can be used to generate additional HTML that has access to the same `Scope`.
///
/// If `replace_blocks` is true, this will wait for any fragments with blocking resources and
/// actually replace them in the initial HTML. This is slower to render (as it requires walking
/// back over the HTML for string replacement) but has the advantage of never including those fallbacks
/// in the HTML.
///
/// This renders:
/// 1) the prefix
/// 2) the application shell
/// a) HTML for everything that is not under a `<Suspense/>`,
/// b) the `fallback` for any `<Suspense/>` component that is not already resolved, and
/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data.
/// 3) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the
/// server and are sent down to the browser to resolve. On the browser, if the app sees that
/// it is waiting for a resource to resolve from the server, it doesn't run it initially.
/// 4) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
/// read under that `<Suspense/>` resolve.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "info", skip_all,)
)]
pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
view: impl FnOnce(Scope) -> View + 'static,
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
additional_context: impl FnOnce(Scope) + 'static,
replace_blocks: bool,
) -> (impl Stream<Item = String>, RuntimeId, ScopeId) {
HydrationCtx::reset_id();
@@ -177,7 +215,7 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
});
let cx = Scope { runtime, id: scope };
let blocking_fragments = FuturesUnordered::new();
let mut blocking_fragments = FuturesUnordered::new();
let fragments = FuturesUnordered::new();
for (fragment_id, data) in pending_fragments {
@@ -198,24 +236,46 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
// HTML for the view function and script to store resources
let stream = futures::stream::once(async move {
let mut blocking = String::new();
let mut blocking_fragments = fragments_to_chunks(blocking_fragments);
while let Some(fragment) = blocking_fragments.next().await {
blocking.push_str(&fragment);
let resolvers = format!(
"<script>__LEPTOS_PENDING_RESOURCES = \
{pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \
Map();__LEPTOS_RESOURCE_RESOLVERS = new Map();</script>"
);
if replace_blocks {
let mut blocks = Vec::with_capacity(blocking_fragments.len());
while let Some((blocked_id, blocked_fragment)) =
blocking_fragments.next().await
{
blocks.push((blocked_id, blocked_fragment));
}
let prefix = prefix(cx);
let mut shell = shell;
for (blocked_id, blocked_fragment) in blocks {
let open = format!("<!--suspense-open-{blocked_id}-->");
let close = format!("<!--suspense-close-{blocked_id}-->");
let (first, rest) = shell.split_once(&open).unwrap_or_default();
let (_fallback, rest) =
rest.split_once(&close).unwrap_or_default();
shell = format!("{first}{blocked_fragment}{rest}").into();
}
format!("{prefix}{shell}{resolvers}")
} else {
let mut blocking = String::new();
let mut blocking_fragments =
fragments_to_chunks(blocking_fragments);
while let Some(fragment) = blocking_fragments.next().await {
blocking.push_str(&fragment);
}
let prefix = prefix(cx);
format!("{prefix}{shell}{resolvers}{blocking}")
}
let prefix = prefix(cx);
format!(
r#"
{prefix}
{shell}
<script>
__LEPTOS_PENDING_RESOURCES = {pending_resources};
__LEPTOS_RESOLVED_RESOURCES = new Map();
__LEPTOS_RESOURCE_RESOLVERS = new Map();
</script>
{blocking}
"#
)
})
// TODO these should be combined again in a way that chains them appropriately
// such that individual resources can resolve before all fragments are done
@@ -229,6 +289,7 @@ pub fn render_to_stream_with_prefix_undisposed_with_context(
(stream, runtime, scope)
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)

View File

@@ -301,10 +301,12 @@ fn root_element_to_tokens_ssr(
let chunks = chunks.into_iter().map(|chunk| match chunk {
SsrElementChunks::String { template, holes } => {
if holes.is_empty() {
let template = template.replace("\\{", "{").replace("\\}", "}");
quote! {
leptos::leptos_dom::html::StringOrView::String(#template.into())
}
} else {
let template = template.replace("\\{", "{{").replace("\\}", "}}");
quote! {
leptos::leptos_dom::html::StringOrView::String(
format!(
@@ -490,8 +492,8 @@ fn element_to_tokens_ssr(
};
template.push_str(
&value
.replace('{', "{{")
.replace('}', "}}"),
.replace('{', "\\{")
.replace('}', "\\}"),
);
} else {
template.push_str("{}");

View File

@@ -42,7 +42,7 @@ web-sys = { version = "0.3", optional = true, features = [
] }
cfg-if = "1"
indexmap = "1"
ouroboros = { version = "0.15.6", default-features = false }
self_cell = "1.0.0"
[dev-dependencies]
criterion = { version = "0.4.0", features = ["html_reports"] }

View File

@@ -188,7 +188,6 @@ impl Runtime {
}
}
#[allow(clippy::await_holding_refcell_ref)] // not using this part of ouroboros
pub(crate) fn mark_dirty(&self, node: NodeId) {
//crate::macros::debug_warn!("marking {node:?} dirty");
let mut nodes = self.nodes.borrow_mut();
@@ -217,23 +216,24 @@ impl Runtime {
* `Check` or `DirtyMarked`.
*
* Because `RefCell`, borrowing the iterators all at once is difficult,
* so a self-referential struct is used instead. ouroboros produces safe
* so a self-referential struct is used instead. self_cell produces safe
* code, but it would not be recommended to use this outside of this
* algorithm.
*/
#[ouroboros::self_referencing]
struct RefIter<'a> {
set: std::cell::Ref<'a, FxIndexSet<NodeId>>,
type Dependent<'a> = indexmap::set::Iter<'a, NodeId>;
// Boxes the iterator internally
#[borrows(set)]
#[covariant]
iter: indexmap::set::Iter<'this, NodeId>,
self_cell::self_cell! {
struct RefIter<'a> {
owner: std::cell::Ref<'a, FxIndexSet<NodeId>>,
#[not_covariant] // avoids extra codegen, harmless to mark it as such
dependent: Dependent,
}
}
/// Due to the limitations of ouroboros, we cannot borrow the
/// stack and iter simultaneously, or directly within the loop,
/// Due to the limitations of self-referencing, we cannot borrow the
/// stack and iter simultaneously within the closure or the loop,
/// therefore this must be used to command the outside scope
/// of what to do.
enum IterResult<'a> {
@@ -251,7 +251,7 @@ impl Runtime {
}
while let Some(iter) = stack.last_mut() {
let res = iter.with_iter_mut(|iter| {
let res = iter.with_dependent_mut(|_, iter| {
let Some(mut child) = iter.next().copied() else {
return IterResult::Empty;
};

View File

@@ -167,18 +167,6 @@ pub trait SignalUpdate<T> {
#[track_caller]
fn update(&self, f: impl FnOnce(&mut T));
/// Applies a function to the current value to mutate it in place
/// and notifies subscribers that the signal has changed. Returns
/// [`Some(O)`] if the signal is still valid, [`None`] otherwise.
///
/// **Note:** `update()` does not auto-memoize, i.e., it will notify subscribers
/// even if the value has not actually changed.
#[deprecated = "Please use `try_update` instead. This method will be \
removed in a future version of this crate"]
fn update_returning<O>(&self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
self.try_update(f)
}
/// Applies a function to the current value to mutate it in place
/// and notifies subscribers that the signal has changed. Returns
/// [`Some(O)`] if the signal is still valid, [`None`] otherwise.
@@ -249,19 +237,6 @@ pub trait SignalUpdateUntracked<T> {
#[track_caller]
fn update_untracked(&self, f: impl FnOnce(&mut T));
/// Runs the provided closure with a mutable reference to the current
/// value without notifying dependents and returns
/// the value the closure returned.
#[deprecated = "Please use `try_update_untracked` instead. This method \
will be removed in a future version of `leptos`"]
#[inline(always)]
fn update_returning_untracked<U>(
&self,
f: impl FnOnce(&mut T) -> U,
) -> Option<U> {
self.try_update_untracked(f)
}
/// Runs the provided closure with a mutable reference to the current
/// value without notifying dependents and returns
/// the value the closure returned.
@@ -930,27 +905,6 @@ impl<T> SignalUpdateUntracked<T> for WriteSignal<T> {
self.id.update_with_no_effect(self.runtime, f);
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "WriteSignal::update_returning_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[inline(always)]
fn update_returning_untracked<U>(
&self,
f: impl FnOnce(&mut T) -> U,
) -> Option<U> {
self.id.update_with_no_effect(self.runtime, f)
}
#[inline(always)]
fn try_update_untracked<O>(
&self,
@@ -1345,27 +1299,6 @@ impl<T> SignalUpdateUntracked<T> for RwSignal<T> {
self.id.update_with_no_effect(self.runtime, f);
}
#[cfg_attr(
any(debug_assertions, features="ssr"),
instrument(
level = "trace",
name = "RwSignal::update_returning_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[inline(always)]
fn update_returning_untracked<U>(
&self,
f: impl FnOnce(&mut T) -> U,
) -> Option<U> {
self.id.update_with_no_effect(self.runtime, f)
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(

View File

@@ -68,16 +68,41 @@ use crate::{
/// // setting name only causes name to log, not count
/// set_name("Bob".into());
/// ```
pub fn create_slice<T, O>(
pub fn create_slice<T, O, S>(
cx: Scope,
signal: RwSignal<T>,
getter: impl Fn(&T) -> O + Clone + Copy + 'static,
setter: impl Fn(&mut T, O) + Clone + Copy + 'static,
) -> (Signal<O>, SignalSetter<O>)
setter: impl Fn(&mut T, S) + Clone + Copy + 'static,
) -> (Signal<O>, SignalSetter<S>)
where
O: PartialEq,
{
let getter = create_memo(cx, move |_| signal.with(getter));
let setter = move |value| signal.update(|x| setter(x, value));
(getter.into(), setter.mapped_signal_setter(cx))
(
create_read_slice(cx, signal, getter),
create_write_slice(cx, signal, setter),
)
}
/// Takes a memoized, read-only slice of a signal. This is equivalent to the
/// read-only half of [`create_slice`].
pub fn create_read_slice<T, O>(
cx: Scope,
signal: RwSignal<T>,
getter: impl Fn(&T) -> O + Clone + Copy + 'static,
) -> Signal<O>
where
O: PartialEq,
{
create_memo(cx, move |_| signal.with(getter)).into()
}
/// Creates a setter to access one slice of a signal. This is equivalent to the
/// write-only half of [`create_slice`].
pub fn create_write_slice<T, O>(
cx: Scope,
signal: RwSignal<T>,
setter: impl Fn(&mut T, O) + Clone + Copy + 'static,
) -> SignalSetter<O> {
let setter = move |value| signal.update(|x| setter(x, value));
setter.mapped_signal_setter(cx)
}

View File

@@ -56,42 +56,8 @@ impl<T> StoredValue<T> {
/// }
/// let data = store_value(cx, MyCloneableData { value: "a".into() });
///
/// // calling .get() clones and returns the value
/// assert_eq!(data.get().value, "a");
/// // there's a short-hand getter form
/// assert_eq!(data().value, "a");
/// # });
/// ```
#[track_caller]
#[deprecated = "Please use `get_value` instead, as this method does not \
track the stored value. This method will also be removed \
in a future version of `leptos`"]
pub fn get(&self) -> T
where
T: Clone,
{
self.get_value()
}
/// Returns a clone of the signals current value, subscribing the effect
/// to this signal.
///
/// # Panics
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
///
/// #[derive(Clone)]
/// pub struct MyCloneableData {
/// pub value: String,
/// }
/// let data = store_value(cx, MyCloneableData { value: "a".into() });
///
/// // calling .get() clones and returns the value
/// assert_eq!(data.get().value, "a");
/// // calling .get_value() clones and returns the value
/// assert_eq!(data.get_value().value, "a");
/// // there's a short-hand getter form
/// assert_eq!(data().value, "a");
/// # });
@@ -104,18 +70,6 @@ impl<T> StoredValue<T> {
self.try_get_value().expect("could not get stored value")
}
/// Same as [`StoredValue::get`] but will not panic by default.
#[track_caller]
#[deprecated = "Please use `try_get_value` instead, as this method does \
not track the stored value. This method will also be \
removed in a future version of `leptos`"]
pub fn try_get(&self) -> Option<T>
where
T: Clone,
{
self.try_get_value()
}
/// Same as [`StoredValue::get`] but will not panic by default.
#[track_caller]
pub fn try_get_value(&self) -> Option<T>
@@ -125,33 +79,6 @@ impl<T> StoredValue<T> {
self.try_with_value(T::clone)
}
/// Applies a function to the current stored value.
///
/// # Panics
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
///
/// pub struct MyUncloneableData {
/// pub value: String
/// }
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
///
/// // calling .with() to extract the value
/// assert_eq!(data.with(|data| data.value.clone()), "a");
/// });
/// ```
#[track_caller]
#[deprecated = "Please use `with_value` instead, as this method does not \
track the stored value. This method will also be removed \
in a future version of `leptos`"]
pub fn with<U>(&self, f: impl FnOnce(&T) -> U) -> U {
self.with_value(f)
}
/// Applies a function to the current stored value.
///
/// # Panics
@@ -167,8 +94,8 @@ impl<T> StoredValue<T> {
/// }
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
///
/// // calling .with() to extract the value
/// assert_eq!(data.with(|data| data.value.clone()), "a");
/// // calling .with_value() to extract the value
/// assert_eq!(data.with_value(|data| data.value.clone()), "a");
/// # });
/// ```
#[track_caller]
@@ -178,15 +105,6 @@ impl<T> StoredValue<T> {
self.try_with_value(f).expect("could not get stored value")
}
/// Same as [`StoredValue::with`] but returns [`Some(O)]` only if
/// the signal is still valid. [`None`] otherwise.
#[deprecated = "Please use `try_with_value` instead, as this method does \
not track the stored value. This method will also be \
removed in a future version of `leptos`"]
pub fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
self.try_with_value(f)
}
/// Same as [`StoredValue::with`] but returns [`Some(O)]` only if
/// the signal is still valid. [`None`] otherwise.
pub fn try_with_value<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
@@ -214,8 +132,8 @@ impl<T> StoredValue<T> {
/// pub value: String
/// }
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
/// data.update(|data| data.value = "b".into());
/// assert_eq!(data.with(|data| data.value.clone()), "b");
/// data.update_value(|data| data.value = "b".into());
/// assert_eq!(data.with_value(|data| data.value.clone()), "b");
/// });
/// ```
///
@@ -228,54 +146,12 @@ impl<T> StoredValue<T> {
/// }
///
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
/// let updated = data.update_returning(|data| {
/// let updated = data.try_update_value(|data| {
/// data.value = "b".into();
/// data.value.clone()
/// });
///
/// assert_eq!(data.with(|data| data.value.clone()), "b");
/// assert_eq!(updated, Some(String::from("b")));
/// # });
/// ```
#[track_caller]
#[deprecated = "Please use `update_value` instead, as this method does not \
track the stored value. This method will also be removed \
in a future version of `leptos`"]
pub fn update(&self, f: impl FnOnce(&mut T)) {
self.update_value(f);
}
/// Updates the stored value.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
///
/// pub struct MyUncloneableData {
/// pub value: String
/// }
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
/// data.update(|data| data.value = "b".into());
/// assert_eq!(data.with(|data| data.value.clone()), "b");
/// });
/// ```
///
/// ```
/// use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
///
/// pub struct MyUncloneableData {
/// pub value: String,
/// }
///
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
/// let updated = data.update_returning(|data| {
/// data.value = "b".into();
/// data.value.clone()
/// });
///
/// assert_eq!(data.with(|data| data.value.clone()), "b");
/// assert_eq!(data.with_value(|data| data.value.clone()), "b");
/// assert_eq!(updated, Some(String::from("b")));
/// # });
/// ```
@@ -285,18 +161,6 @@ impl<T> StoredValue<T> {
.expect("could not set stored value");
}
/// Updates the stored value.
#[track_caller]
#[deprecated = "Please use `try_update_value` instead, as this method does \
not track the stored value. This method will also be \
removed in a future version of `leptos`"]
pub fn update_returning<U>(
&self,
f: impl FnOnce(&mut T) -> U,
) -> Option<U> {
self.try_update_value(f)
}
/// Same as [`Self::update`], but returns [`Some(O)`] if the
/// signal is still valid, [`None`] otherwise.
pub fn try_update_value<O>(self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
@@ -311,29 +175,6 @@ impl<T> StoredValue<T> {
.flatten()
}
/// Sets the stored value.
///
/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
///
/// pub struct MyUncloneableData {
/// pub value: String
/// }
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
/// data.set(MyUncloneableData { value: "b".into() });
/// assert_eq!(data.with(|data| data.value.clone()), "b");
/// });
/// ```
#[track_caller]
#[deprecated = "Please use `set_value` instead, as this method does not \
track the stored value. This method will also be removed \
in a future version of `leptos`"]
pub fn set(&self, value: T) {
self.set_value(value);
}
/// Sets the stored value.
///
/// # Examples
@@ -345,8 +186,8 @@ impl<T> StoredValue<T> {
/// pub value: String,
/// }
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
/// data.set(MyUncloneableData { value: "b".into() });
/// assert_eq!(data.with(|data| data.value.clone()), "b");
/// data.set_value(MyUncloneableData { value: "b".into() });
/// assert_eq!(data.with_value(|data| data.value.clone()), "b");
/// # });
/// ```
#[track_caller]
@@ -403,10 +244,10 @@ impl<T> StoredValue<T> {
/// pub value: String,
/// }
///
/// // ✅ you can move the `StoredValue` and access it with .with()
/// // ✅ you can move the `StoredValue` and access it with .with_value()
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
/// let callback_a = move || data.with(|data| data.value == "a");
/// let callback_b = move || data.with(|data| data.value == "b");
/// let callback_a = move || data.with_value(|data| data.value == "a");
/// let callback_b = move || data.with_value(|data| data.value == "b");
/// # }).dispose();
/// ```
#[track_caller]

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.3.0-alpha"
version = "0.3.1"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.3.0-alpha"
version = "0.3.1"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -28,6 +28,7 @@ js-sys = { version = "0.3" }
wasm-bindgen = { version = "0.2" }
wasm-bindgen-futures = { version = "0.4" }
lru = { version = "0.10", optional = true }
serde_json = "1.0.96"
[dependencies.web-sys]
version = "0.3"

View File

@@ -1,5 +1,6 @@
use crate::{use_navigate, use_resolved_path, ToHref, Url};
use leptos::{html::form, *};
use serde::{de::DeserializeOwned, Serialize};
use std::{error::Error, rc::Rc};
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use wasm_bindgen_futures::JsFuture;
@@ -240,7 +241,7 @@ pub fn ActionForm<I, O>(
) -> impl IntoView
where
I: Clone + ServerFn + 'static,
O: Clone + Serializable + 'static,
O: Clone + Serialize + DeserializeOwned + 'static,
{
let action_url = if let Some(url) = action.url() {
url
@@ -268,7 +269,6 @@ where
let on_response = Rc::new(move |resp: &web_sys::Response| {
let resp = resp.clone().expect("couldn't get Response");
let status = resp.status();
spawn_local(async move {
let redirected = resp.redirected();
@@ -277,23 +277,33 @@ where
resp.text().expect("couldn't get .text() from Response"),
)
.await;
let status = resp.status();
match body {
Ok(json) => {
// 500 just returns text of error, not JSON
if status == 500 {
let err = ServerFnError::ServerError(
json.as_string().unwrap_or_default(),
);
if let Some(error) = error {
error.try_set(Some(Box::new(err.clone())));
let json = json
.as_string()
.expect("couldn't get String from JsString");
if (500..=599).contains(&status) {
match serde_json::from_str::<ServerFnError>(&json) {
Ok(res) => {
value.try_set(Some(Err(res)));
if let Some(error) = error {
error.try_set(None);
}
}
Err(e) => {
value.try_set(Some(Err(
ServerFnError::Deserialization(
e.to_string(),
),
)));
if let Some(error) = error {
error.try_set(Some(Box::new(e)));
}
}
}
value.try_set(Some(Err(err)));
} else {
match O::de(
&json.as_string().expect(
"couldn't get String from JsString",
),
) {
match serde_json::from_str::<O>(&json) {
Ok(res) => {
value.try_set(Some(Ok(res)));
if let Some(error) = error {

View File

@@ -135,14 +135,14 @@ impl History for BrowserIntegration {
/// The wrapper type that the [Router](crate::Router) uses to interact with a [History].
/// This is automatically provided in the browser. For the server, it should be provided
/// as a context.
/// as a context. Be sure that it can survive conversion to a URL in the browser.
///
/// ```
/// # use leptos_router::*;
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// let integration = ServerIntegration {
/// path: "insert/current/path/here".to_string(),
/// path: "http://leptos.rs/".to_string(),
/// };
/// provide_context(cx, RouterIntegrationContext::new(integration));
/// # });
@@ -167,7 +167,24 @@ impl History for RouterIntegrationContext {
}
}
/// A generic router integration for the server side. All its need is the current path.
/// A generic router integration for the server side.
///
/// This should match what the browser history will show.
///
/// Generally, this will already be provided if you are using the leptos
/// server integrations.
///
/// ```
/// # use leptos_router::*;
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// let integration = ServerIntegration {
/// // Swap out with your URL if integrating manually.
/// path: "http://leptos.rs/".to_string(),
/// };
/// provide_context(cx, RouterIntegrationContext::new(integration));
/// # });
/// ```
#[derive(Clone, Debug)]
pub struct ServerIntegration {
pub path: String,

View File

@@ -8,11 +8,14 @@
/// 2. **Out-of-order streaming**: Serve an HTML shell that includes `fallback` for any `Suspense`. Load data on the **server**, streaming it down to the client as it resolves, and streaming down HTML for `Suspense` nodes.
/// - *Pros*: Combines the best of **synchronous** and **`async`**, with a very fast shell and resources that begin loading on the server.
/// - *Cons*: Requires JS for suspended fragments to appear in correct order. Weaker meta tag support when it depends on data that's under suspense (has already streamed down `<head>`)
/// 3. **In-order streaming**: Walk through the tree, returning HTML synchronously as in synchronous rendering and out-of-order streaming until you hit a `Suspense`. At that point, wait for all its data to load, then render it, then the rest of the tree.
/// 3. **Partially-blocked out-of-order streaming**: Using `create_blocking_resource` with out-of-order streaming still sends fallbacks and relies on JavaScript to fill them in with the fragments. Partially-blocked streaming does this replacement on the server, making for a slower response but requiring no JavaScript to show blocking resources.
/// - *Pros*: Works better if JS is disabled.
/// - *Cons*: Slower initial response because of additional string manipulation on server.
/// 4. **In-order streaming**: Walk through the tree, returning HTML synchronously as in synchronous rendering and out-of-order streaming until you hit a `Suspense`. At that point, wait for all its data to load, then render it, then the rest of the tree.
/// - *Pros*: Does not require JS for HTML to appear in correct order.
/// - *Cons*: Loads the shell more slowly than out-of-order streaming or synchronous rendering because it needs to pause at every `Suspense`. Cannot begin hydration until the entire page has loaded, so earlier pieces
/// of the page will not be interactive until the suspended chunks have loaded.
/// 4. **`async`**: Load all resources on the server. Wait until all data are loaded, and render HTML in one sweep.
/// 5. **`async`**: Load all resources on the server. Wait until all data are loaded, and render HTML in one sweep.
/// - *Pros*: Better handling for meta tags (because you know async data even before you render the `<head>`). Faster complete load than **synchronous** because async resources begin loading on server.
/// - *Cons*: Slower load time/TTFB: you need to wait for all async resources to load before displaying anything on the client.
///
@@ -23,6 +26,7 @@
pub enum SsrMode {
#[default]
OutOfOrder,
PartiallyBlocked,
InOrder,
Async,
}