mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
35 Commits
hydration-
...
fix-html-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3af4c05dc | ||
|
|
7f77910e91 | ||
|
|
76aeb573bf | ||
|
|
e0bf8f5b6d | ||
|
|
5ace580edb | ||
|
|
5d612d9740 | ||
|
|
eacff684ef | ||
|
|
4034aa9c11 | ||
|
|
45275ff8d4 | ||
|
|
3ff5089bf4 | ||
|
|
c28297fe93 | ||
|
|
6d0d70cd17 | ||
|
|
c4e693e01e | ||
|
|
2be4e8d959 | ||
|
|
fec4ff4381 | ||
|
|
25c313aeb5 | ||
|
|
0dbcc323ba | ||
|
|
6b683f9ab6 | ||
|
|
aae4d4445e | ||
|
|
bb9df8937d | ||
|
|
05277f03b6 | ||
|
|
f698f8badd | ||
|
|
98f51fec8a | ||
|
|
65465cad78 | ||
|
|
ddee545e7e | ||
|
|
cbfb724af2 | ||
|
|
0953007f47 | ||
|
|
53f7677258 | ||
|
|
6373fd42fb | ||
|
|
e1bcf77b03 | ||
|
|
b0762bbfb5 | ||
|
|
63a7a4dec1 | ||
|
|
1f6a326268 | ||
|
|
0efc39db8b | ||
|
|
cbf2f73e95 |
@@ -8,7 +8,7 @@
|
||||
default_to_workspace = false
|
||||
|
||||
[tasks.ci]
|
||||
dependencies = ["build", "build-examples", "test"]
|
||||
dependencies = ["build", "check-examples", "test"]
|
||||
|
||||
[tasks.build]
|
||||
clear = true
|
||||
@@ -19,22 +19,24 @@ command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.build-examples]
|
||||
[tasks.check-examples]
|
||||
clear = true
|
||||
dependencies = [
|
||||
{ name = "build", path = "examples/counter" },
|
||||
{ name = "build", path = "examples/counter_isomorphic" },
|
||||
{ name = "build", path = "examples/counters" },
|
||||
{ name = "build", path = "examples/counters_stable" },
|
||||
{ name = "build", path = "examples/fetch" },
|
||||
{ name = "build", path = "examples/hackernews" },
|
||||
{ name = "build", path = "examples/hackernews_axum" },
|
||||
{ name = "build", path = "examples/parent_child" },
|
||||
{ name = "build", path = "examples/router" },
|
||||
{ name = "build", path = "examples/tailwind" },
|
||||
{ name = "build", path = "examples/todo_app_sqlite" },
|
||||
{ name = "build", path = "examples/todo_app_sqlite_axum" },
|
||||
{ name = "build", path = "examples/todomvc" },
|
||||
{ name = "check", path = "examples/counter" },
|
||||
{ name = "check", path = "examples/counter_isomorphic" },
|
||||
{ name = "check", path = "examples/counter_without_macros" },
|
||||
{ name = "check", path = "examples/counters" },
|
||||
{ name = "check", path = "examples/counters_stable" },
|
||||
{ name = "check", path = "examples/errors_axum" },
|
||||
{ name = "check", path = "examples/fetch" },
|
||||
{ name = "check", path = "examples/hackernews" },
|
||||
{ name = "check", path = "examples/hackernews_axum" },
|
||||
{ name = "check", path = "examples/parent_child" },
|
||||
{ name = "check", path = "examples/router" },
|
||||
{ name = "check", path = "examples/tailwind" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite_axum" },
|
||||
{ name = "check", path = "examples/todomvc" },
|
||||
]
|
||||
|
||||
[tasks.test]
|
||||
|
||||
@@ -2,3 +2,8 @@
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -2,3 +2,8 @@
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -37,7 +37,7 @@ cfg_if! {
|
||||
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_address.clone();
|
||||
let addr = conf.leptos_options.site_addr.clone();
|
||||
let routes = generate_route_list(|cx| view! { cx, <Counters/> });
|
||||
|
||||
HttpServer::new(move || {
|
||||
|
||||
9
examples/counter_without_macros/Makefile.toml
Normal file
9
examples/counter_without_macros/Makefile.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
@@ -2,3 +2,8 @@
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -2,3 +2,8 @@
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# Leptos Todo App Sqlite with Axum
|
||||
|
||||
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
|
||||
# Leptos Errors Demonstration with Axum
|
||||
This example demonstrates how Leptos Errors can work with an Axum backend on a server.
|
||||
|
||||
## Client Side Rendering
|
||||
This example cannot be built as a trunk standalone CSR-only app as it requires the server to send HTTP Status Codes.
|
||||
This example cannot be built as a trunk standalone CSR-only app as it requires the server to send status codes.
|
||||
|
||||
## Server Side Rendering with cargo-leptos
|
||||
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
use crate::errors::AppError;
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::Errors;
|
||||
use leptos::{
|
||||
component, create_rw_signal, use_context, view, For, ForProps, IntoView, RwSignal, Scope,
|
||||
};
|
||||
use leptos::*;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_axum::ResponseOptions;
|
||||
|
||||
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
|
||||
// here than just displaying them
|
||||
// A basic function to display errors served by the error boundaries.
|
||||
// Feel free to do more complicated things here than just displaying them.
|
||||
#[component]
|
||||
pub fn ErrorTemplate(
|
||||
cx: Scope,
|
||||
@@ -16,10 +15,7 @@ pub fn ErrorTemplate(
|
||||
#[prop(optional)] errors: Option<RwSignal<Errors>>,
|
||||
) -> impl IntoView {
|
||||
let errors = match outside_errors {
|
||||
Some(e) => {
|
||||
let errors = create_rw_signal(cx, e);
|
||||
errors
|
||||
}
|
||||
Some(e) => create_rw_signal(cx, e),
|
||||
None => match errors {
|
||||
Some(e) => e,
|
||||
None => panic!("No Errors found and we expected errors!"),
|
||||
@@ -32,39 +28,35 @@ pub fn ErrorTemplate(
|
||||
// Downcast lets us take a type that implements `std::error::Error`
|
||||
let errors: Vec<AppError> = errors
|
||||
.into_iter()
|
||||
.map(|(_k, v)| v.downcast_ref::<AppError>().cloned())
|
||||
.flatten()
|
||||
.filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned())
|
||||
.collect();
|
||||
println!("Errors: {errors:#?}");
|
||||
|
||||
// Only the response code for the first error is actually sent from the server
|
||||
// this may be customized by the specific application
|
||||
cfg_if! {
|
||||
if #[cfg(feature="ssr")]{
|
||||
cfg_if! { if #[cfg(feature="ssr")] {
|
||||
let response = use_context::<ResponseOptions>(cx);
|
||||
if let Some(response) = response{
|
||||
response.set_status(errors[0].status_code());
|
||||
if let Some(response) = response {
|
||||
response.set_status(errors[0].status_code());
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
view! {cx,
|
||||
<h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1>
|
||||
<For
|
||||
// a function that returns the items we're iterating over; a signal is fine
|
||||
each= move || {errors.clone().into_iter().enumerate()}
|
||||
// a unique key for each item as a reference
|
||||
key=|(index, _error)| index.clone()
|
||||
// renders each item to a view
|
||||
view= move |error| {
|
||||
let error_string = error.1.to_string();
|
||||
let error_code= error.1.status_code();
|
||||
view! {
|
||||
cx,
|
||||
<h2>{error_code.to_string()}</h2>
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
/>
|
||||
view! { cx,
|
||||
<h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1>
|
||||
<For
|
||||
// a function that returns the items we're iterating over; a signal is fine
|
||||
each= move || {errors.clone().into_iter().enumerate()}
|
||||
// a unique key for each item as a reference
|
||||
key=|(index, _error)| *index
|
||||
// renders each item to a view
|
||||
view= move |error| {
|
||||
let error_string = error.1.to_string();
|
||||
let error_code= error.1.status_code();
|
||||
view! { cx,
|
||||
<h2>{error_code.to_string()}</h2>
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
@@ -33,18 +32,14 @@ if #[cfg(feature = "ssr")] {
|
||||
|
||||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
|
||||
let root_path = format!("{root}");
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(&root_path).oneshot(req).await {
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
format!("Something went wrong: {err}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -2,17 +2,14 @@ use crate::{
|
||||
error_template::{ErrorTemplate, ErrorTemplateProps},
|
||||
errors::AppError,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
pub fn register_server_functions() {
|
||||
_ = CauseInternalServerError::register();
|
||||
_ = CauseNotFoundError::register();
|
||||
}
|
||||
}}
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn register_server_functions() {
|
||||
_ = CauseInternalServerError::register();
|
||||
}
|
||||
|
||||
#[server(CauseInternalServerError, "/api")]
|
||||
pub async fn cause_internal_server_error() -> Result<(), ServerFnError> {
|
||||
@@ -24,11 +21,6 @@ pub async fn cause_internal_server_error() -> Result<(), ServerFnError> {
|
||||
))
|
||||
}
|
||||
|
||||
#[server(CauseNotFoundError, "/api")]
|
||||
pub async fn cause_not_found_error() -> Result<(), ServerFnError> {
|
||||
Err(ServerFnError::ServerError("Not Found".to_string()))
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
//let id = use_context::<String>(cx);
|
||||
@@ -45,9 +37,7 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
<ExampleErrors/>
|
||||
</ErrorBoundary>
|
||||
<ExampleErrors/>
|
||||
}/>
|
||||
</Routes>
|
||||
</main>
|
||||
@@ -57,20 +47,29 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
pub fn ExampleErrors(cx: Scope) -> impl IntoView {
|
||||
view! {
|
||||
cx,
|
||||
<p>
|
||||
"This link will load a 404 page since it does not exist. Verify with browser development tools:"
|
||||
<a href="/404">"This Page Does not Exist"</a>
|
||||
</p>
|
||||
<p>
|
||||
"The following <div> will always contain an error and cause the page to produce status 500. Check browser dev tools. "
|
||||
</p>
|
||||
<div>
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
<ReturnsError/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
let generate_internal_error = create_server_action::<CauseInternalServerError>(cx);
|
||||
|
||||
view! { cx,
|
||||
<p>
|
||||
"These links will load 404 pages since they do not exist. Verify with browser development tools: " <br/>
|
||||
<a href="/404">"This links to a page that does not exist"</a><br/>
|
||||
<a href="/404" target="_blank">"Same link, but in a new tab"</a>
|
||||
</p>
|
||||
<p>
|
||||
"After pressing this button check browser network tools. Can be used even when WASM is blocked:"
|
||||
<ActionForm action=generate_internal_error>
|
||||
<input name="error1" type="submit" value="Generate Internal Server Error"/>
|
||||
</ActionForm>
|
||||
</p>
|
||||
<p>"The following <div> will always contain an error and cause this page to produce status 500. Check browser dev tools. "</p>
|
||||
<div>
|
||||
// note that the error boundries could be placed above in the Router or lower down
|
||||
// in a particular route. The generated errors on the entire page contribue to the
|
||||
// final status code sent by the server when producing ssr pages.
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
<ReturnsError/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,74 +1,72 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use crate::fallback::file_and_error_handler;
|
||||
use crate::landing::*;
|
||||
use axum::body::Body as AxumBody;
|
||||
use axum::{
|
||||
routing::{post, get},
|
||||
extract::{Extension, Path},
|
||||
http::Request,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use axum::body::Body as AxumBody;
|
||||
use crate::landing::*;
|
||||
use errors_axum::*;
|
||||
use crate::fallback::file_and_error_handler;
|
||||
use leptos::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use std::sync::Arc;
|
||||
}}
|
||||
|
||||
//Define a handler to test extractor with state
|
||||
async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
|
||||
let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
|
||||
move |cx| {
|
||||
provide_context(cx, id.clone());
|
||||
},
|
||||
|cx| view! { cx, <App/> }
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
//Define a handler to test extractor with state
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn custom_handler(
|
||||
Path(id): Path<String>,
|
||||
Extension(options): Extension<Arc<LeptosOptions>>,
|
||||
req: Request<AxumBody>,
|
||||
) -> Response {
|
||||
let handler = leptos_axum::render_app_to_stream_with_context(
|
||||
(*options).clone(),
|
||||
move |cx| {
|
||||
provide_context(cx, id.clone());
|
||||
},
|
||||
|cx| view! { cx, <App/> },
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
|
||||
|
||||
crate::landing::register_server_functions();
|
||||
crate::landing::register_server_functions();
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_address.clone();
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.route("/special/:id", get(custom_handler))
|
||||
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> } )
|
||||
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> })
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(Extension(Arc::new(leptos_options)));
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// client-only stuff for Trunk
|
||||
else {
|
||||
use todo_app_sqlite_axum::landing::*;
|
||||
|
||||
pub fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
view! { cx, <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
// this is if we were using client-only rending with Trunk
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
// This example cannot be built as a trunk standalone CSR-only app.
|
||||
// The server is needed to demonstrate the error statuses.
|
||||
}
|
||||
|
||||
@@ -2,3 +2,8 @@
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -2,3 +2,8 @@
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -24,7 +24,7 @@ cfg_if! {
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_address.clone();
|
||||
let addr = conf.leptos_options.site_addr.clone();
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> });
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::{component, Scope, IntoView, view};
|
||||
use leptos::{component, view, IntoView, Scope};
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
@@ -6,7 +6,7 @@ pub fn Nav(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<header class="header">
|
||||
<nav class="inner">
|
||||
<A href="/">
|
||||
<A href="/home">
|
||||
<strong>"HN"</strong>
|
||||
</A>
|
||||
<A href="/new">
|
||||
|
||||
@@ -2,3 +2,8 @@
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -19,7 +19,7 @@ if #[cfg(feature = "ssr")] {
|
||||
|
||||
let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_address.clone();
|
||||
let addr = leptos_options.site_addr.clone();
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||
|
||||
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
|
||||
|
||||
@@ -2,3 +2,8 @@
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -2,3 +2,8 @@
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -20,6 +20,7 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
|
||||
<A exact=true href="/">"Contacts"</A>
|
||||
<A href="about">"About"</A>
|
||||
<A href="settings">"Settings"</A>
|
||||
<A href="redirect-home">"Redirect to Home"</A>
|
||||
</nav>
|
||||
<main>
|
||||
<Routes>
|
||||
@@ -44,6 +45,10 @@ pub fn RouterExample(cx: Scope) -> impl IntoView {
|
||||
path="settings"
|
||||
view=move |cx| view! { cx, <Settings/> }
|
||||
/>
|
||||
<Route
|
||||
path="redirect-home"
|
||||
view=move |cx| view! { cx, <Redirect path="/"/> }
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -2,3 +2,8 @@
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -20,7 +20,7 @@ cfg_if! {
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_address.clone();
|
||||
let addr = conf.leptos_options.site_addr.clone();
|
||||
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> });
|
||||
|
||||
@@ -2,3 +2,8 @@
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -29,7 +29,7 @@ cfg_if! {
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_address.clone();
|
||||
let addr = conf.leptos_options.site_addr.clone();
|
||||
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> });
|
||||
|
||||
@@ -12,13 +12,13 @@ console_log = "0.2.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../../leptos/meta", default-features = false }
|
||||
leptos_router = { path = "../../../leptos/router", default-features = false }
|
||||
leptos_reactive = { path = "../../../leptos/leptos_reactive", default-features = false }
|
||||
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
leptos_reactive = { path = "../../leptos_reactive", default-features = false }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
@@ -55,27 +55,27 @@ denylist = [
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "todo_app_sqlite_axum"
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "todo_app_sqlite_axum"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "./style.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
end2end-cmd = "npx playwright test"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
|
||||
@@ -2,3 +2,8 @@
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::errors::TodoAppError;
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::Errors;
|
||||
use leptos::{
|
||||
component, create_rw_signal, use_context, view, For, ForProps, IntoView, RwSignal, Scope,
|
||||
};
|
||||
use leptos::*;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_axum::ResponseOptions;
|
||||
|
||||
@@ -16,10 +15,7 @@ pub fn ErrorTemplate(
|
||||
#[prop(optional)] errors: Option<RwSignal<Errors>>,
|
||||
) -> impl IntoView {
|
||||
let errors = match outside_errors {
|
||||
Some(e) => {
|
||||
let errors = create_rw_signal(cx, e);
|
||||
errors
|
||||
}
|
||||
Some(e) => create_rw_signal(cx, e),
|
||||
None => match errors {
|
||||
Some(e) => e,
|
||||
None => panic!("No Errors found and we expected errors!"),
|
||||
@@ -32,8 +28,7 @@ pub fn ErrorTemplate(
|
||||
// Downcast lets us take a type that implements `std::error::Error`
|
||||
let errors: Vec<TodoAppError> = errors
|
||||
.into_iter()
|
||||
.map(|(_k, v)| v.downcast_ref::<TodoAppError>().cloned())
|
||||
.flatten()
|
||||
.filter_map(|(_k, v)| v.downcast_ref::<TodoAppError>().cloned())
|
||||
.collect();
|
||||
println!("Errors: {errors:#?}");
|
||||
|
||||
@@ -54,7 +49,7 @@ pub fn ErrorTemplate(
|
||||
// a function that returns the items we're iterating over; a signal is fine
|
||||
each= move || {errors.clone().into_iter().enumerate()}
|
||||
// a unique key for each item as a reference
|
||||
key=|(index, _error)| index.clone()
|
||||
key=|(index, _error)| *index
|
||||
// renders each item to a view
|
||||
view= move |error| {
|
||||
let error_string = error.1.to_string();
|
||||
|
||||
@@ -33,14 +33,13 @@ if #[cfg(feature = "ssr")] {
|
||||
|
||||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
|
||||
let root_path = format!("{root}");
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(&root_path).oneshot(req).await {
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
format!("Something went wrong: {err}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ if #[cfg(feature = "ssr")] {
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_address.clone();
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> }).await;
|
||||
|
||||
// build our application with a route
|
||||
@@ -56,7 +56,7 @@ if #[cfg(feature = "ssr")] {
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on {}", &addr);
|
||||
log!("listening on http://{}", &addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
@@ -66,15 +66,9 @@ if #[cfg(feature = "ssr")] {
|
||||
|
||||
// client-only stuff for Trunk
|
||||
else {
|
||||
use todo_app_sqlite_axum::todo::*;
|
||||
|
||||
pub fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
view! { cx, <TodoApp/> }
|
||||
});
|
||||
// This example cannot be built as a trunk standalone CSR-only app.
|
||||
// Only the server may directly connect to the database.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ cfg_if! {
|
||||
// use http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode};
|
||||
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
Ok(SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
|
||||
SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
}
|
||||
|
||||
pub fn register_server_functions() {
|
||||
|
||||
@@ -2,3 +2,8 @@
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
@@ -269,7 +269,7 @@ pub fn handle_server_fns_with_context(
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
|
||||
/// let addr = conf.leptos_options.site_address.clone();
|
||||
/// let addr = conf.leptos_options.site_addr.clone();
|
||||
/// HttpServer::new(move || {
|
||||
/// let leptos_options = &conf.leptos_options;
|
||||
///
|
||||
@@ -367,7 +367,7 @@ where
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
|
||||
/// let addr = conf.leptos_options.site_address.clone();
|
||||
/// let addr = conf.leptos_options.site_addr.clone();
|
||||
/// HttpServer::new(move || {
|
||||
/// let leptos_options = &conf.leptos_options;
|
||||
///
|
||||
@@ -438,6 +438,7 @@ fn provide_contexts(cx: leptos::Scope, req: &HttpRequest, res_options: ResponseO
|
||||
provide_context(cx, MetaContext::new());
|
||||
provide_context(cx, res_options);
|
||||
provide_context(cx, req.clone());
|
||||
provide_server_redirect(cx, move |path| redirect(cx, path));
|
||||
}
|
||||
|
||||
fn leptos_corrected_path(req: &HttpRequest) -> String {
|
||||
@@ -486,22 +487,17 @@ async fn stream_app(
|
||||
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
|
||||
);
|
||||
|
||||
// Get the first, second, and third chunks in the stream, which renders the app shell, and thus allows Resources to run
|
||||
// Get the first and second in the stream, which renders the app shell, and thus allows Resources to run
|
||||
let first_chunk = stream.next().await;
|
||||
let second_chunk = stream.next().await;
|
||||
let third_chunk = stream.next().await;
|
||||
|
||||
let res_options = res_options.0.read();
|
||||
|
||||
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(),
|
||||
third_chunk.unwrap(),
|
||||
])
|
||||
.chain(stream);
|
||||
let complete_stream =
|
||||
futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()]).chain(stream);
|
||||
let mut res = HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.streaming(complete_stream);
|
||||
@@ -528,7 +524,7 @@ fn html_parts(options: &LeptosOptions, meta_context: Option<&MetaContext>) -> (S
|
||||
wasm_output_name.push_str("_bg");
|
||||
}
|
||||
|
||||
let site_ip = &options.site_address.ip().to_string();
|
||||
let site_ip = &options.site_addr.ip().to_string();
|
||||
let reload_port = options.reload_port;
|
||||
let pkg_path = &options.site_pkg_dir;
|
||||
|
||||
|
||||
@@ -16,5 +16,5 @@ leptos = { workspace = true, features = ["ssr"] }
|
||||
leptos_meta = { workspace = true, features = ["ssr"] }
|
||||
leptos_router = { workspace = true, features = ["ssr"] }
|
||||
leptos_config = { workspace = true }
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
parking_lot = "0.12.1"
|
||||
|
||||
@@ -327,7 +327,7 @@ pub type PinnedHtmlStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>
|
||||
///
|
||||
/// let conf = get_configuration(Some("Cargo.toml")).await.unwrap();
|
||||
/// let leptos_options = conf.leptos_options;
|
||||
/// let addr = leptos_options.site_address.clone();
|
||||
/// let addr = leptos_options.site_addr.clone();
|
||||
///
|
||||
/// // build our application with a route
|
||||
/// let app = Router::new()
|
||||
@@ -447,6 +447,7 @@ where
|
||||
provide_context(cx, MetaContext::new());
|
||||
provide_context(cx, req_parts);
|
||||
provide_context(cx, default_res_options);
|
||||
provide_server_redirect(cx, move |path| redirect(cx, path));
|
||||
app_fn(cx).into_view(cx)
|
||||
}
|
||||
};
|
||||
@@ -494,20 +495,16 @@ where
|
||||
|
||||
let mut stream = Box::pin(rx.map(|html| Ok(Bytes::from(html))));
|
||||
|
||||
// Get the first, second, and third chunks in the stream, which renders the app shell, and thus allows Resources to run
|
||||
// Get the first and second chunks in the stream, which renders the app shell, and thus allows Resources to run
|
||||
let first_chunk = stream.next().await;
|
||||
let second_chunk = stream.next().await;
|
||||
let third_chunk = stream.next().await;
|
||||
|
||||
// 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(),
|
||||
third_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
|
||||
@@ -537,7 +534,7 @@ fn html_parts(options: &LeptosOptions, meta: Option<&MetaContext>) -> (String, &
|
||||
wasm_output_name.push_str("_bg");
|
||||
}
|
||||
|
||||
let site_ip = &options.site_address.ip().to_string();
|
||||
let site_ip = &options.site_addr.ip().to_string();
|
||||
let reload_port = options.reload_port;
|
||||
|
||||
let leptos_autoreload = match std::env::var("LEPTOS_WATCH").is_ok() {
|
||||
|
||||
@@ -17,7 +17,6 @@ leptos_server = { workspace = true }
|
||||
leptos_config = { workspace = true }
|
||||
tracing = "0.1"
|
||||
typed-builder = "0.12"
|
||||
once_cell = "1.17.0"
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = ".", default-features = false }
|
||||
|
||||
@@ -172,3 +172,7 @@ pub type Children = Box<dyn FnOnce(Scope) -> Fragment>;
|
||||
/// A type for the `children` property on components that can be called
|
||||
/// more than once.
|
||||
pub type ChildrenFn = Box<dyn Fn(Scope) -> Fragment>;
|
||||
|
||||
/// A type for the `children` property on components that can be called
|
||||
/// more than once, but may mutate the children.
|
||||
pub type ChildrenFnMut = Box<dyn FnMut(Scope) -> Fragment>;
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use crate::Children;
|
||||
use leptos::component;
|
||||
use leptos_dom::IntoView;
|
||||
use leptos_reactive::Scope;
|
||||
use once_cell::sync::Lazy;
|
||||
use leptos_dom::{Fragment, IntoView};
|
||||
use leptos_reactive::{create_memo, Scope};
|
||||
|
||||
/// A component that will show its children when the `when` condition is `true`,
|
||||
/// and show the fallback when it is `false`, without rerendering every time
|
||||
/// the condition changes.
|
||||
///
|
||||
/// *Note*: Because of the nature of generic arguments, it’s not really possible
|
||||
/// to make the `fallback` optional. If you want an empty fallback state—in other
|
||||
/// words, if you want to show the children if `when` is true and noting otherwise—use
|
||||
/// `fallback=|_| ()` (i.e., a fallback function that returns the unit type `()`).
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos_reactive::*;
|
||||
/// # use leptos_macro::*;
|
||||
@@ -30,7 +33,7 @@ pub fn Show<F, W, IV>(
|
||||
/// The scope the component is running in
|
||||
cx: Scope,
|
||||
/// The components Show wraps
|
||||
children: Children,
|
||||
children: Box<dyn Fn(Scope) -> Fragment>,
|
||||
/// A closure that returns a bool that determines whether this thing runs
|
||||
when: W,
|
||||
/// A closure that returns what gets rendered if the when statement is false
|
||||
@@ -41,12 +44,10 @@ where
|
||||
F: Fn(Scope) -> IV + 'static,
|
||||
IV: IntoView,
|
||||
{
|
||||
// now you don't render until `when` is actually true
|
||||
let children = Lazy::new(move || children(cx).into_view(cx));
|
||||
let fallback = Lazy::new(move || fallback(cx).into_view(cx));
|
||||
let memoized_when = create_memo(cx, move |_| when());
|
||||
|
||||
move || match when() {
|
||||
true => children.clone(),
|
||||
false => fallback.clone(),
|
||||
move || match memoized_when.get() {
|
||||
true => children(cx).into_view(cx),
|
||||
false => fallback(cx).into_view(cx),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Configuraiton for the Leptos web framework."
|
||||
description = "Configuration for the Leptos web framework."
|
||||
readme = "../README.md"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -43,7 +43,7 @@ pub struct LeptosOptions {
|
||||
/// Using an env variable here would allow you to run the same code in dev and prod
|
||||
/// Defaults to `127.0.0.1:3000`
|
||||
#[builder(setter(into), default=SocketAddr::from(([127,0,0,1], 3000)))]
|
||||
pub site_address: SocketAddr,
|
||||
pub site_addr: SocketAddr,
|
||||
/// The port the Websocket watcher listens on. Should match the `reload_port` in cargo-leptos(if using).
|
||||
/// Defaults to `3001`
|
||||
#[builder(default = 3001)]
|
||||
@@ -58,7 +58,7 @@ impl LeptosOptions {
|
||||
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_address: env_w_default("LEPTOS_SITE_ADDR", "127.0.0.1:3000")?.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()?,
|
||||
})
|
||||
}
|
||||
@@ -87,50 +87,34 @@ impl Default for Env {
|
||||
}
|
||||
}
|
||||
|
||||
fn from_str(input: &str) -> Result<Env, String> {
|
||||
let sanitized = input.to_lowercase();
|
||||
match sanitized.as_ref() {
|
||||
"dev" | "development" => Ok(Env::DEV),
|
||||
"prod" | "production" => Ok(Env::PROD),
|
||||
_ => Err(format!(
|
||||
"{input} is not a supported environment. Use either `dev` or `production`.",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Env {
|
||||
type Err = ();
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let sanitized = input.to_lowercase();
|
||||
match sanitized.as_ref() {
|
||||
"dev" => Ok(Self::DEV),
|
||||
"development" => Ok(Self::DEV),
|
||||
"prod" => Ok(Self::PROD),
|
||||
"production" => Ok(Self::PROD),
|
||||
_ => Ok(Self::DEV),
|
||||
}
|
||||
from_str(input).or_else(|_| Ok(Self::default()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Env {
|
||||
fn from(str: &str) -> Self {
|
||||
let sanitized = str.to_lowercase();
|
||||
match sanitized.as_str() {
|
||||
"dev" => Self::DEV,
|
||||
"development" => Self::DEV,
|
||||
"prod" => Self::PROD,
|
||||
"production" => Self::PROD,
|
||||
_ => {
|
||||
panic!("Env var is not recognized. Maybe try `dev` or `prod`")
|
||||
}
|
||||
}
|
||||
from_str(str).unwrap_or_else(|err| panic!("{}", err))
|
||||
}
|
||||
}
|
||||
impl From<&Result<String, VarError>> for Env {
|
||||
fn from(input: &Result<String, VarError>) -> Self {
|
||||
match input {
|
||||
Ok(str) => {
|
||||
let sanitized = str.to_lowercase();
|
||||
match sanitized.as_ref() {
|
||||
"dev" => Self::DEV,
|
||||
"development" => Self::DEV,
|
||||
"prod" => Self::PROD,
|
||||
"production" => Self::PROD,
|
||||
_ => {
|
||||
panic!("Env var is not recognized. Maybe try `dev` or `prod`")
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => Self::DEV,
|
||||
Ok(str) => from_str(str).unwrap_or_else(|err| panic!("{}", err)),
|
||||
Err(_) => Self::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,15 +123,7 @@ impl TryFrom<String> for Env {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"dev" => Ok(Self::DEV),
|
||||
"development" => Ok(Self::DEV),
|
||||
"prod" => Ok(Self::PROD),
|
||||
"production" => Ok(Self::PROD),
|
||||
other => Err(format!(
|
||||
"{other} is not a supported environment. Use either `dev` or `production`."
|
||||
)),
|
||||
}
|
||||
from_str(s.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use crate::{hydration::HydrationCtx, Comment, IntoView, View};
|
||||
use crate::{
|
||||
hydration::{HydrationCtx, HydrationKey},
|
||||
Comment, IntoView, View,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos_reactive::Scope;
|
||||
use std::{borrow::Cow, cell::RefCell, fmt, ops::Deref, rc::Rc};
|
||||
@@ -7,8 +10,6 @@ cfg_if! {
|
||||
use crate::{mount_child, prepare_to_move, unmount_child, MountKind, Mountable};
|
||||
use leptos_reactive::{create_effect, ScopeDisposer};
|
||||
use wasm_bindgen::JsCast;
|
||||
} else {
|
||||
use crate::hydration::HydrationKey;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,9 +78,7 @@ impl Mountable for DynChildRepr {
|
||||
}
|
||||
|
||||
impl DynChildRepr {
|
||||
fn new() -> Self {
|
||||
let id = HydrationCtx::id();
|
||||
|
||||
fn new_with_id(id: HydrationKey) -> Self {
|
||||
let markers = (
|
||||
Comment::new(Cow::Borrowed("</DynChild>"), &id, true),
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -124,6 +123,7 @@ where
|
||||
CF: Fn() -> N + 'static,
|
||||
N: IntoView,
|
||||
{
|
||||
id: crate::HydrationKey,
|
||||
child_fn: CF,
|
||||
}
|
||||
|
||||
@@ -135,7 +135,12 @@ where
|
||||
/// Creates a new dynamic child which will re-render whenever it's
|
||||
/// signal dependencies change.
|
||||
pub fn new(child_fn: CF) -> Self {
|
||||
Self { child_fn }
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,167 +154,182 @@ where
|
||||
instrument(level = "trace", name = "<DynChild />", skip_all)
|
||||
)]
|
||||
fn into_view(self, cx: Scope) -> View {
|
||||
let Self { child_fn } = self;
|
||||
// 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 component = DynChildRepr::new();
|
||||
let child = component.child.clone();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let closing = component.closing.node.clone();
|
||||
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
|
||||
let span = tracing::Span::current();
|
||||
|
||||
let child = component.child.clone();
|
||||
#[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(debug_assertions, target_arch = "wasm32", feature = "web"))]
|
||||
let span = tracing::Span::current();
|
||||
let (new_child, disposer) =
|
||||
cx.run_child_scope(|cx| child_fn().into_view(cx));
|
||||
|
||||
#[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 mut child_borrow = child.borrow_mut();
|
||||
|
||||
let (new_child, disposer) =
|
||||
cx.run_child_scope(|cx| child_fn().into_view(cx));
|
||||
// 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();
|
||||
|
||||
let mut child_borrow = child.borrow_mut();
|
||||
// Dispose of the scope
|
||||
prev_disposer.dispose();
|
||||
|
||||
// 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();
|
||||
// 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
|
||||
|
||||
// Dispose of the scope
|
||||
prev_disposer.dispose();
|
||||
// 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);
|
||||
|
||||
// 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
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
// 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);
|
||||
(Some(prev_t), disposer)
|
||||
} else {
|
||||
mount_child(MountKind::Before(&closing), &new_child);
|
||||
|
||||
**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();
|
||||
}
|
||||
|
||||
// Mount the new child, and we're done
|
||||
mount_child(MountKind::Before(&closing), &new_child);
|
||||
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
(Some(prev_t), disposer)
|
||||
} else {
|
||||
mount_child(MountKind::Before(&closing), &new_child);
|
||||
|
||||
**child_borrow = Some(new_child.clone());
|
||||
|
||||
(Some(new_t.node.clone()), disposer)
|
||||
(None, disposer)
|
||||
}
|
||||
}
|
||||
// Child is not a text node, so we can remove the previous
|
||||
// text node
|
||||
// Otherwise, the new child can still be a text node,
|
||||
// but we know the previous child was not, so no special
|
||||
// treatment here
|
||||
else {
|
||||
if !was_child_moved && child != new_child {
|
||||
// Remove the text
|
||||
closing
|
||||
.previous_sibling()
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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());
|
||||
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
(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>();
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
// Mount the new child, and we're done
|
||||
t.remove();
|
||||
|
||||
mount_child(MountKind::Before(&closing), &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 we are not hydrating, we simply mount the child
|
||||
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);
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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)
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
t.remove();
|
||||
|
||||
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);
|
||||
}
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
{
|
||||
let new_child = child_fn().into_view(cx);
|
||||
|
||||
// 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_mut() = Some(new_child);
|
||||
}
|
||||
|
||||
**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);
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -34,7 +34,6 @@ where
|
||||
on_cleanup(cx, move || {
|
||||
queue_microtask(move || {
|
||||
errors.update(|errors: &mut Errors| {
|
||||
crate::log!("removing error at {id}");
|
||||
errors.remove::<E>(&id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,8 @@ thread_local! {
|
||||
pub fn add_event_listener<E>(
|
||||
target: &web_sys::Element,
|
||||
event_name: Cow<'static, str>,
|
||||
mut cb: impl FnMut(E) + 'static,
|
||||
#[cfg(debug_assertions)] mut cb: impl FnMut(E) + 'static,
|
||||
#[cfg(not(debug_assertions))] cb: impl FnMut(E) + 'static,
|
||||
) where
|
||||
E: FromWasmAbi + 'static,
|
||||
{
|
||||
|
||||
@@ -150,6 +150,7 @@ generate_event_types! {
|
||||
canplaythrough: Event,
|
||||
change: Event,
|
||||
click: MouseEvent,
|
||||
#[does_not_bubble]
|
||||
close: Event,
|
||||
compositionend: CompositionEvent,
|
||||
compositionstart: CompositionEvent,
|
||||
|
||||
@@ -13,20 +13,17 @@ proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
cfg-if = "1"
|
||||
doc-comment = "0.3"
|
||||
html-escape = "0.2"
|
||||
itertools = "0.10"
|
||||
pad-adapter = "0.1"
|
||||
prettyplease = "0.1"
|
||||
proc-macro-error = "1"
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
syn = { version = "1", features = ["full"] }
|
||||
syn-rsx = "0.9"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
leptos_dom = { workspace = true }
|
||||
leptos_reactive = { workspace = true }
|
||||
leptos_server = { workspace = true }
|
||||
lazy_static = "1.4"
|
||||
convert_case = "0.6.0"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -4,6 +4,7 @@ use convert_case::{
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use proc_macro_error::ResultExt;
|
||||
use quote::{format_ident, ToTokens, TokenStreamExt};
|
||||
use std::collections::HashSet;
|
||||
use syn::{
|
||||
@@ -167,7 +168,7 @@ impl ToTokens for Model {
|
||||
|
||||
let component = if *is_transparent {
|
||||
quote! {
|
||||
#body_name(cx, #prop_names)
|
||||
#body_name(#scope_name, #prop_names)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
@@ -287,8 +288,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"
|
||||
);
|
||||
};
|
||||
|
||||
@@ -402,17 +403,15 @@ 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().ok()? {
|
||||
if let Meta::List(MetaList { nested, .. }) = attr.parse_meta().unwrap_or_abort() {
|
||||
Some(
|
||||
nested
|
||||
.iter()
|
||||
@@ -613,9 +612,9 @@ 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";
|
||||
const STD_OPTION_MSG: &str =
|
||||
"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, .. },
|
||||
|
||||
@@ -230,6 +230,22 @@ mod server;
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// 10. You can set any HTML element’s `innerHTML` with the `inner_html` attribute on an
|
||||
/// element. Be careful: this HTML will not be escaped, so you should ensure that it
|
||||
/// only contains trusted input.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// let html = "<p>This HTML will be injected.</p>";
|
||||
/// view! { cx,
|
||||
/// <div inner_html=html/>
|
||||
/// }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// Here’s a simple example that shows off several of these features, put together
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
@@ -441,12 +457,14 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
/// ```
|
||||
///
|
||||
/// 5. You can access the children passed into the component with the `children` property, which takes
|
||||
/// an argument of the form `Box<dyn FnOnce(Scope) -> Fragment>`.
|
||||
/// an argument of the type `Children`. This is an alias for `Box<dyn FnOnce(Scope) -> Fragment>`.
|
||||
/// If you need `children` to be a `Fn` or `FnMut`, you can use the `ChildrenFn` or `ChildrenFnMut`
|
||||
/// type aliases.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// #[component]
|
||||
/// fn ComponentWithChildren(cx: Scope, children: Box<dyn FnOnce(Scope) -> Fragment>) -> impl IntoView {
|
||||
/// fn ComponentWithChildren(cx: Scope, children: Children) -> impl IntoView {
|
||||
/// view! {
|
||||
/// cx,
|
||||
/// <ul>
|
||||
|
||||
@@ -137,7 +137,7 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
||||
};
|
||||
|
||||
Ok(quote::quote! {
|
||||
#[derive(Clone, ::serde::Serialize, ::serde::Deserialize)]
|
||||
#[derive(Clone, Debug, ::serde::Serialize, ::serde::Deserialize)]
|
||||
pub struct #struct_name {
|
||||
#(#fields),*
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ pub(crate) fn render_view(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
nodes,
|
||||
false,
|
||||
true,
|
||||
TagType::Unknown,
|
||||
global_class,
|
||||
)
|
||||
@@ -219,7 +219,7 @@ fn fragment_to_tokens_ssr(
|
||||
});
|
||||
quote! {
|
||||
{
|
||||
leptos::Fragment::new(vec![
|
||||
leptos::Fragment::lazy(|| vec![
|
||||
#(#nodes),*
|
||||
])
|
||||
}
|
||||
@@ -262,11 +262,22 @@ fn root_element_to_tokens_ssr(
|
||||
};
|
||||
|
||||
let tag_name = node.name.to_string();
|
||||
let typed_element_name = Ident::new(&camel_case_tag_name(&tag_name), node.name.span());
|
||||
let typed_element_name = {
|
||||
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) {
|
||||
quote! { svg::#typed_element_name }
|
||||
} else if is_math_ml_element(&tag_name) {
|
||||
quote! { math::#typed_element_name }
|
||||
} else {
|
||||
quote! { #typed_element_name }
|
||||
};
|
||||
quote! {
|
||||
{
|
||||
#(#exprs_for_compiler)*
|
||||
::leptos::HtmlElement::from_html(cx, leptos::#typed_element_name::default(), #template)
|
||||
::leptos::HtmlElement::from_html(cx, leptos::leptos_dom::#typed_element_name::default(), #template)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,8 +299,13 @@ fn element_to_tokens_ssr(
|
||||
{#component}.into_view(cx).render_to_string(cx),
|
||||
})
|
||||
} else {
|
||||
let tag_name = node
|
||||
.name
|
||||
.to_string()
|
||||
.replace("svg::", "")
|
||||
.replace("math::", "");
|
||||
template.push('<');
|
||||
template.push_str(&node.name.to_string());
|
||||
template.push_str(&tag_name);
|
||||
|
||||
for attr in &node.attributes {
|
||||
if let Node::Attribute(attr) = attr {
|
||||
@@ -336,7 +352,7 @@ fn element_to_tokens_ssr(
|
||||
),
|
||||
Node::Text(text) => {
|
||||
if let Some(value) = value_to_string(&text.value) {
|
||||
template.push_str(&value);
|
||||
template.push_str(&html_escape::encode_safe(&value));
|
||||
} else {
|
||||
template.push_str("{}");
|
||||
let value = text.value.as_ref();
|
||||
@@ -390,32 +406,10 @@ fn attribute_to_tokens_ssr(
|
||||
exprs_for_compiler: &mut Vec<TokenStream>,
|
||||
) {
|
||||
let name = node.key.to_string();
|
||||
if name == "ref" || name == "_ref" {
|
||||
if name == "ref" || name == "_ref" || name == "node_ref" {
|
||||
// ignore refs on SSR
|
||||
} else if let Some(name) = name.strip_prefix("on:") {
|
||||
let handler = node
|
||||
.value
|
||||
.as_ref()
|
||||
.expect("event listener attributes need a value")
|
||||
.as_ref();
|
||||
|
||||
#[allow(unused_variables)]
|
||||
let (name, is_force_undelegated) = parse_event(name);
|
||||
|
||||
let event_type = TYPED_EVENTS
|
||||
.iter()
|
||||
.find(|e| **e == name)
|
||||
.copied()
|
||||
.unwrap_or("Custom");
|
||||
let event_type = event_type
|
||||
.parse::<TokenStream>()
|
||||
.expect("couldn't parse event name");
|
||||
|
||||
let event_type = if is_force_undelegated {
|
||||
quote! { ::leptos::ev::undelegated(::leptos::ev::#event_type) }
|
||||
} else {
|
||||
quote! { ::leptos::ev::#event_type }
|
||||
};
|
||||
} else if name.strip_prefix("on:").is_some() {
|
||||
let (event_type, handler) = event_from_attribute_node(node, false);
|
||||
exprs_for_compiler.push(quote! {
|
||||
leptos::ssr_event_listener(#event_type, #handler);
|
||||
})
|
||||
@@ -626,7 +620,7 @@ fn node_to_tokens(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
&fragment.children,
|
||||
false,
|
||||
true,
|
||||
parent_type,
|
||||
global_class,
|
||||
),
|
||||
@@ -708,7 +702,7 @@ fn element_to_tokens(
|
||||
cx,
|
||||
Span::call_site(),
|
||||
&fragment.children,
|
||||
false,
|
||||
true,
|
||||
parent_type,
|
||||
global_class,
|
||||
),
|
||||
@@ -931,7 +925,9 @@ fn component_to_tokens(
|
||||
|
||||
let props = attrs
|
||||
.clone()
|
||||
.filter(|attr| !attr.key.to_string().starts_with("clone:"))
|
||||
.filter(|attr| {
|
||||
!attr.key.to_string().starts_with("clone:") && !attr.key.to_string().starts_with("on:")
|
||||
})
|
||||
.map(|attr| {
|
||||
let name = &attr.key;
|
||||
|
||||
@@ -950,6 +946,7 @@ fn component_to_tokens(
|
||||
});
|
||||
|
||||
let items_to_clone = attrs
|
||||
.clone()
|
||||
.filter(|attr| attr.key.to_string().starts_with("clone:"))
|
||||
.map(|attr| {
|
||||
let ident = attr
|
||||
@@ -963,6 +960,17 @@ fn component_to_tokens(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let events = attrs
|
||||
.filter(|attr| attr.key.to_string().starts_with("on:"))
|
||||
.map(|attr| {
|
||||
let (event_type, handler) = event_from_attribute_node(attr, true);
|
||||
|
||||
quote! {
|
||||
.on(#event_type, #handler)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let children = if node.children.is_empty() {
|
||||
quote! {}
|
||||
} else {
|
||||
@@ -988,17 +996,58 @@ fn component_to_tokens(
|
||||
}
|
||||
};
|
||||
|
||||
quote! {
|
||||
let component = quote! {
|
||||
#name(
|
||||
#cx,
|
||||
#component_props_name::builder()
|
||||
#(#props)*
|
||||
#children
|
||||
.build(),
|
||||
.build()
|
||||
)
|
||||
};
|
||||
|
||||
if events.is_empty() {
|
||||
component
|
||||
} else {
|
||||
quote! {
|
||||
#component.into_view(#cx)
|
||||
#(#events)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 handler = attr
|
||||
.value
|
||||
.as_ref()
|
||||
.expect("event listener attributes need a value")
|
||||
.as_ref();
|
||||
|
||||
#[allow(unused_variables)]
|
||||
let (name, name_undelegated) = parse_event(&event_name);
|
||||
|
||||
let event_type = TYPED_EVENTS
|
||||
.iter()
|
||||
.find(|e| **e == name)
|
||||
.copied()
|
||||
.unwrap_or("Custom");
|
||||
let event_type = event_type
|
||||
.parse::<TokenStream>()
|
||||
.expect("couldn't parse event name");
|
||||
|
||||
let event_type = if force_undelegated || name_undelegated {
|
||||
quote! { ::leptos::ev::undelegated(::leptos::ev::#event_type) }
|
||||
} else {
|
||||
quote! { ::leptos::ev::#event_type }
|
||||
};
|
||||
(event_type, handler)
|
||||
}
|
||||
|
||||
fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
|
||||
match tag_name {
|
||||
NodeName::Path(path) => path
|
||||
|
||||
@@ -200,7 +200,7 @@ where
|
||||
}
|
||||
|
||||
impl EffectId {
|
||||
pub(crate) fn run<T>(&self, runtime_id: RuntimeId) {
|
||||
pub(crate) fn run(&self, runtime_id: RuntimeId) {
|
||||
_ = with_runtime(runtime_id, |runtime| {
|
||||
let effect = {
|
||||
let effects = runtime.effects.borrow();
|
||||
|
||||
@@ -130,7 +130,8 @@ where
|
||||
});
|
||||
|
||||
let id = with_runtime(cx.runtime, |runtime| {
|
||||
runtime.create_serializable_resource(Rc::clone(&r))
|
||||
let r = Rc::clone(&r) as Rc<dyn SerializableResource>;
|
||||
runtime.create_serializable_resource(r)
|
||||
})
|
||||
.expect("tried to create a Resource in a Runtime that has been disposed.");
|
||||
|
||||
@@ -250,7 +251,8 @@ where
|
||||
});
|
||||
|
||||
let id = with_runtime(cx.runtime, |runtime| {
|
||||
runtime.create_unserializable_resource(Rc::clone(&r))
|
||||
let r = Rc::clone(&r) as Rc<dyn UnserializableResource>;
|
||||
runtime.create_unserializable_resource(r)
|
||||
})
|
||||
.expect("tried to create a Resource in a runtime that has been disposed.");
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#![forbid(unsafe_code)]
|
||||
use crate::{
|
||||
hydration::SharedContext, serialization::Serializable, AnyEffect, AnyResource, Effect,
|
||||
EffectId, Memo, ReadSignal, ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId,
|
||||
ScopeProperty, SignalId, 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;
|
||||
@@ -115,18 +115,19 @@ impl RuntimeId {
|
||||
ret
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
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")
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn create_signal<T>(self, value: T) -> (ReadSignal<T>, WriteSignal<T>)
|
||||
where
|
||||
T: Any + 'static,
|
||||
{
|
||||
let id = with_runtime(self, |runtime| {
|
||||
runtime
|
||||
.signals
|
||||
.borrow_mut()
|
||||
.insert(Rc::new(RefCell::new(value)))
|
||||
})
|
||||
.expect("tried to create a signal in a runtime that has been disposed");
|
||||
let id = self.create_concrete_signal(Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>);
|
||||
|
||||
(
|
||||
ReadSignal {
|
||||
runtime: self,
|
||||
@@ -149,13 +150,7 @@ impl RuntimeId {
|
||||
where
|
||||
T: Any + 'static,
|
||||
{
|
||||
let id = with_runtime(self, |runtime| {
|
||||
runtime
|
||||
.signals
|
||||
.borrow_mut()
|
||||
.insert(Rc::new(RefCell::new(value)))
|
||||
})
|
||||
.expect("tried to create a signal in a runtime that has been disposed");
|
||||
let id = self.create_concrete_signal(Rc::new(RefCell::new(value)) as Rc<RefCell<dyn Any>>);
|
||||
RwSignal {
|
||||
runtime: self,
|
||||
id,
|
||||
@@ -165,6 +160,12 @@ 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")
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn create_effect<T>(self, f: impl Fn(Option<T>) -> T + 'static) -> EffectId
|
||||
where
|
||||
@@ -173,18 +174,16 @@ impl RuntimeId {
|
||||
#[cfg(debug_assertions)]
|
||||
let defined_at = std::panic::Location::caller();
|
||||
|
||||
with_runtime(self, |runtime| {
|
||||
let effect = Effect {
|
||||
f,
|
||||
value: RefCell::new(None),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at,
|
||||
};
|
||||
let id = { runtime.effects.borrow_mut().insert(Rc::new(effect)) };
|
||||
id.run::<T>(self);
|
||||
id
|
||||
})
|
||||
.expect("tried to create an effect in a runtime that has been disposed")
|
||||
let effect = Effect {
|
||||
f,
|
||||
value: RefCell::new(None),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at,
|
||||
};
|
||||
|
||||
let id = self.create_concrete_effect(Rc::new(effect));
|
||||
id.run(self);
|
||||
id
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
@@ -256,27 +255,19 @@ impl Runtime {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub(crate) fn create_unserializable_resource<S, T>(
|
||||
pub(crate) fn create_unserializable_resource(
|
||||
&self,
|
||||
state: Rc<ResourceState<S, T>>,
|
||||
) -> ResourceId
|
||||
where
|
||||
S: Clone + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
state: Rc<dyn UnserializableResource>,
|
||||
) -> ResourceId {
|
||||
self.resources
|
||||
.borrow_mut()
|
||||
.insert(AnyResource::Unserializable(state))
|
||||
}
|
||||
|
||||
pub(crate) fn create_serializable_resource<S, T>(
|
||||
pub(crate) fn create_serializable_resource(
|
||||
&self,
|
||||
state: Rc<ResourceState<S, T>>,
|
||||
) -> ResourceId
|
||||
where
|
||||
S: Clone + 'static,
|
||||
T: Serializable + 'static,
|
||||
{
|
||||
state: Rc<dyn SerializableResource>,
|
||||
) -> ResourceId {
|
||||
self.resources
|
||||
.borrow_mut()
|
||||
.insert(AnyResource::Serializable(state))
|
||||
|
||||
@@ -83,6 +83,17 @@ impl Scope {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// Returns the chain of scope IDs beginning with this one, going to its parent, grandparents, etc.
|
||||
pub fn ancestry(&self) -> Vec<ScopeId> {
|
||||
let mut ids = vec![self.id];
|
||||
let mut cx = *self;
|
||||
while let Some(parent) = cx.parent() {
|
||||
ids.push(parent.id());
|
||||
cx = parent;
|
||||
}
|
||||
ids
|
||||
}
|
||||
|
||||
/// Creates a child scope and runs the given function within it, returning a handle to dispose of it.
|
||||
///
|
||||
/// The child scope has its own lifetime and disposer, but will be disposed when the parent is
|
||||
|
||||
@@ -399,6 +399,12 @@ where
|
||||
Dynamic(Signal<T>),
|
||||
}
|
||||
|
||||
impl<T: Default> Default for MaybeSignal<T> {
|
||||
fn default() -> Self {
|
||||
Self::Static(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> UntrackedGettableSignal<T> for MaybeSignal<T>
|
||||
where
|
||||
T: 'static,
|
||||
@@ -580,6 +586,12 @@ impl<T> From<Signal<T>> for MaybeSignal<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for MaybeSignal<String> {
|
||||
fn from(value: &str) -> Self {
|
||||
Self::Static(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl<T> FnOnce<()> for MaybeSignal<T>
|
||||
where
|
||||
|
||||
@@ -14,12 +14,9 @@ leptos_reactive = { workspace = true }
|
||||
form_urlencoded = "1"
|
||||
gloo-net = "0.2"
|
||||
lazy_static = "1"
|
||||
linear-map = "1"
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_urlencoded = "0.7"
|
||||
thiserror = "1"
|
||||
rmp-serde = "1.1.1"
|
||||
serde_json = "1.0.89"
|
||||
quote = "1"
|
||||
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
|
||||
@@ -31,23 +28,23 @@ leptos = { path = "../leptos" }
|
||||
|
||||
[features]
|
||||
csr = [
|
||||
#"leptos/csr",
|
||||
"leptos_dom/web",
|
||||
"leptos_reactive/csr",
|
||||
#"leptos/csr",
|
||||
"leptos_dom/web",
|
||||
"leptos_reactive/csr",
|
||||
]
|
||||
hydrate = [
|
||||
#"leptos/hydrate",
|
||||
"leptos_dom/web",
|
||||
"leptos_reactive/hydrate",
|
||||
#"leptos/hydrate",
|
||||
"leptos_dom/web",
|
||||
"leptos_reactive/hydrate",
|
||||
]
|
||||
ssr = [
|
||||
#"leptos/ssr",
|
||||
"leptos_reactive/ssr",
|
||||
#"leptos/ssr",
|
||||
"leptos_reactive/ssr",
|
||||
]
|
||||
stable = [
|
||||
#"leptos/stable",
|
||||
"leptos_dom/stable",
|
||||
"leptos_reactive/stable",
|
||||
#"leptos/stable",
|
||||
"leptos_dom/stable",
|
||||
"leptos_reactive/stable",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
|
||||
14
logos/Simple_Icon.svg
Normal file
14
logos/Simple_Icon.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M10.1,17.8C7.6,17,5.9,14.6,5.9,12c0-3.4,2.7-6.1,6.1-6.1c0.3,0,0.5,0,0.8,0
|
||||
c0.4-0.9,0.9-1.7,1.5-2.5c-0.5-0.6-0.8-1.4-0.8-2.3c0-0.3,0-0.7,0.1-1c-0.5-0.1-1-0.1-1.5-0.1C5.4,0.1,0.1,5.4,0.1,12
|
||||
c0,4.3,2.3,8.1,5.8,10.2L6,22.1C7.9,21.2,9.3,19.7,10.1,17.8z"/>
|
||||
<path d="M16.9,3c0.7,0,1.3-0.4,1.6-0.9l0,0c-1-0.6-2.1-1.1-3.2-1.5l0,0c-0.1,0.2-0.1,0.4-0.1,0.6
|
||||
C15.1,2.2,15.9,3,16.9,3z"/>
|
||||
<path d="M19.9,3.1c-0.7,1-1.8,1.6-2.9,1.6c-0.3,0-0.5,0-0.8-0.1c-0.2,0-0.3-0.1-0.5-0.1
|
||||
c-0.5,0.6-0.9,1.2-1.2,1.9c2.2,1,3.7,3.1,3.7,5.6c0,3.4-2.7,6.1-6.1,6.1c0,0-0.1,0-0.1,0c-0.8,2.1-2.2,3.8-4.1,5
|
||||
c1.3,0.5,2.7,0.8,4.2,0.8c6.6,0,11.9-5.3,11.9-11.9C23.9,8.4,22.3,5.3,19.9,3.1z"/>
|
||||
<circle cx="12" cy="12" r="4.4"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 907 B |
@@ -13,17 +13,16 @@ leptos = { workspace = true }
|
||||
cfg-if = "1"
|
||||
common_macros = "0.1"
|
||||
gloo-net = "0.2"
|
||||
itertools = "0.10"
|
||||
lazy_static = "1"
|
||||
linear-map = "1"
|
||||
log = "0.4"
|
||||
regex = { version = "1", optional = true }
|
||||
bincode = "1"
|
||||
url = { version = "2", optional = true }
|
||||
percent-encoding = "2"
|
||||
thiserror = "1"
|
||||
serde_urlencoded = "0.7"
|
||||
serde = "1"
|
||||
tracing = "0.1"
|
||||
js-sys = { version = "0.3" }
|
||||
wasm-bindgen = { version = "0.2" }
|
||||
wasm-bindgen-futures = { version = "0.4" }
|
||||
@@ -31,26 +30,26 @@ wasm-bindgen-futures = { version = "0.4" }
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
# History/Routing
|
||||
"History",
|
||||
"HtmlAnchorElement",
|
||||
"MouseEvent",
|
||||
"Url",
|
||||
# Form
|
||||
"FormData",
|
||||
"HtmlButtonElement",
|
||||
"HtmlFormElement",
|
||||
"HtmlInputElement",
|
||||
"SubmitEvent",
|
||||
"Url",
|
||||
"UrlSearchParams",
|
||||
# Fetching in Hydrate Mode
|
||||
"Headers",
|
||||
"Request",
|
||||
"RequestInit",
|
||||
"RequestMode",
|
||||
"Response",
|
||||
"Window",
|
||||
# History/Routing
|
||||
"History",
|
||||
"HtmlAnchorElement",
|
||||
"MouseEvent",
|
||||
"Url",
|
||||
# Form
|
||||
"FormData",
|
||||
"HtmlButtonElement",
|
||||
"HtmlFormElement",
|
||||
"HtmlInputElement",
|
||||
"SubmitEvent",
|
||||
"Url",
|
||||
"UrlSearchParams",
|
||||
# Fetching in Hydrate Mode
|
||||
"Headers",
|
||||
"Request",
|
||||
"RequestInit",
|
||||
"RequestMode",
|
||||
"Response",
|
||||
"Window",
|
||||
]
|
||||
|
||||
[features]
|
||||
|
||||
@@ -43,86 +43,109 @@ pub fn Form<A>(
|
||||
where
|
||||
A: ToHref + 'static,
|
||||
{
|
||||
let action_version = version;
|
||||
let action = use_resolved_path(cx, move || action.to_href()());
|
||||
fn inner(
|
||||
cx: Scope,
|
||||
method: Option<&'static str>,
|
||||
action: Memo<Option<String>>,
|
||||
enctype: Option<String>,
|
||||
version: Option<RwSignal<usize>>,
|
||||
error: Option<RwSignal<Option<Box<dyn Error>>>>,
|
||||
#[allow(clippy::type_complexity)] on_form_data: Option<Rc<dyn Fn(&web_sys::FormData)>>,
|
||||
#[allow(clippy::type_complexity)] on_response: Option<Rc<dyn Fn(&web_sys::Response)>>,
|
||||
children: Children,
|
||||
) -> HtmlElement<Form> {
|
||||
let action_version = version;
|
||||
let on_submit = move |ev: web_sys::SubmitEvent| {
|
||||
if ev.default_prevented() {
|
||||
return;
|
||||
}
|
||||
let navigate = use_navigate(cx);
|
||||
|
||||
let on_submit = move |ev: web_sys::SubmitEvent| {
|
||||
if ev.default_prevented() {
|
||||
return;
|
||||
}
|
||||
let navigate = use_navigate(cx);
|
||||
let (form, method, action, enctype) = extract_form_attributes(&ev);
|
||||
|
||||
let (form, method, action, enctype) = extract_form_attributes(&ev);
|
||||
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();
|
||||
let action = use_resolved_path(cx, move || action.clone())
|
||||
.get()
|
||||
.unwrap_or_default();
|
||||
// POST
|
||||
if method == "post" {
|
||||
ev.prevent_default();
|
||||
|
||||
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();
|
||||
let action = use_resolved_path(cx, move || action.clone())
|
||||
.get()
|
||||
.unwrap_or_default();
|
||||
// POST
|
||||
if method == "post" {
|
||||
ev.prevent_default();
|
||||
|
||||
let on_response = on_response.clone();
|
||||
spawn_local(async move {
|
||||
let res = gloo_net::http::Request::post(&action)
|
||||
.header("Accept", "application/json")
|
||||
.header("Content-Type", &enctype)
|
||||
.body(params)
|
||||
.send()
|
||||
.await;
|
||||
match res {
|
||||
Err(e) => {
|
||||
log::error!("<Form/> error while POSTing: {e:#?}");
|
||||
if let Some(error) = error {
|
||||
error.set(Some(Box::new(e)));
|
||||
}
|
||||
}
|
||||
Ok(resp) => {
|
||||
if let Some(version) = action_version {
|
||||
version.update(|n| *n += 1);
|
||||
}
|
||||
if let Some(error) = error {
|
||||
error.set(None);
|
||||
}
|
||||
if let Some(on_response) = on_response.clone() {
|
||||
on_response(resp.as_raw());
|
||||
let on_response = on_response.clone();
|
||||
spawn_local(async move {
|
||||
let res = gloo_net::http::Request::post(&action)
|
||||
.header("Accept", "application/json")
|
||||
.header("Content-Type", &enctype)
|
||||
.body(params)
|
||||
.send()
|
||||
.await;
|
||||
match res {
|
||||
Err(e) => {
|
||||
log::error!("<Form/> error while POSTing: {e:#?}");
|
||||
if let Some(error) = error {
|
||||
error.set(Some(Box::new(e)));
|
||||
}
|
||||
}
|
||||
Ok(resp) => {
|
||||
if let Some(version) = action_version {
|
||||
version.update(|n| *n += 1);
|
||||
}
|
||||
if let Some(error) = error {
|
||||
error.set(None);
|
||||
}
|
||||
if let Some(on_response) = on_response.clone() {
|
||||
on_response(resp.as_raw());
|
||||
}
|
||||
|
||||
if resp.status() == 303 {
|
||||
if let Some(redirect_url) = resp.headers().get("Location") {
|
||||
_ = navigate(&redirect_url, Default::default());
|
||||
if resp.status() == 303 {
|
||||
if let Some(redirect_url) = resp.headers().get("Location") {
|
||||
_ = navigate(&redirect_url, Default::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// otherwise, GET
|
||||
else {
|
||||
let params = params.to_string().as_string().unwrap_or_default();
|
||||
if navigate(&format!("{action}?{params}"), Default::default()).is_ok() {
|
||||
ev.prevent_default();
|
||||
});
|
||||
}
|
||||
// otherwise, GET
|
||||
else {
|
||||
let params = params.to_string().as_string().unwrap_or_default();
|
||||
if navigate(&format!("{action}?{params}"), Default::default()).is_ok() {
|
||||
ev.prevent_default();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let method = method.unwrap_or("get");
|
||||
|
||||
view! { cx,
|
||||
<form
|
||||
method=method
|
||||
action=move || action.get()
|
||||
enctype=enctype
|
||||
on:submit=on_submit
|
||||
>
|
||||
{children(cx)}
|
||||
</form>
|
||||
}
|
||||
};
|
||||
|
||||
let method = method.unwrap_or("get");
|
||||
|
||||
view! { cx,
|
||||
<form
|
||||
method=method
|
||||
action=move || action.get()
|
||||
enctype=enctype
|
||||
on:submit=on_submit
|
||||
>
|
||||
{children(cx)}
|
||||
</form>
|
||||
}
|
||||
|
||||
let action = use_resolved_path(cx, move || action.to_href()());
|
||||
inner(
|
||||
cx,
|
||||
method,
|
||||
action,
|
||||
enctype,
|
||||
version,
|
||||
error,
|
||||
on_form_data,
|
||||
on_response,
|
||||
children,
|
||||
)
|
||||
}
|
||||
|
||||
/// Automatically turns a server [Action](leptos_server::Action) into an HTML
|
||||
|
||||
@@ -70,35 +70,47 @@ pub fn A<H>(
|
||||
where
|
||||
H: ToHref + 'static,
|
||||
{
|
||||
let location = use_location(cx);
|
||||
let href = use_resolved_path(cx, move || href.to_href()());
|
||||
let is_active = create_memo(cx, move |_| match href.get() {
|
||||
None => false,
|
||||
fn inner(
|
||||
cx: Scope,
|
||||
href: Memo<Option<String>>,
|
||||
exact: bool,
|
||||
state: Option<State>,
|
||||
replace: bool,
|
||||
class: Option<MaybeSignal<String>>,
|
||||
children: Children,
|
||||
) -> HtmlElement<A> {
|
||||
let location = use_location(cx);
|
||||
let is_active = create_memo(cx, move |_| match href.get() {
|
||||
None => false,
|
||||
|
||||
Some(to) => {
|
||||
let path = to
|
||||
.split(['?', '#'])
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
let loc = location.pathname.get().to_lowercase();
|
||||
if exact {
|
||||
loc == path
|
||||
} else {
|
||||
loc.starts_with(&path)
|
||||
Some(to) => {
|
||||
let path = to
|
||||
.split(['?', '#'])
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
let loc = location.pathname.get().to_lowercase();
|
||||
if exact {
|
||||
loc == path
|
||||
} else {
|
||||
loc.starts_with(&path)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<a
|
||||
href=move || href.get().unwrap_or_default()
|
||||
prop:state={state.map(|s| s.to_js_value())}
|
||||
prop:replace={replace}
|
||||
aria-current=move || if is_active.get() { Some("page") } else { None }
|
||||
class=move || class.as_ref().map(|class| class.get())
|
||||
>
|
||||
{children(cx)}
|
||||
</a>
|
||||
view! { cx,
|
||||
<a
|
||||
href=move || href.get().unwrap_or_default()
|
||||
prop:state={state.map(|s| s.to_js_value())}
|
||||
prop:replace={replace}
|
||||
aria-current=move || if is_active.get() { Some("page") } else { None }
|
||||
class=move || class.as_ref().map(|class| class.get())
|
||||
>
|
||||
{children(cx)}
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
let href = use_resolved_path(cx, move || href.to_href()());
|
||||
inner(cx, href, exact, state, replace, class, children)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod form;
|
||||
mod link;
|
||||
mod outlet;
|
||||
mod redirect;
|
||||
mod route;
|
||||
mod router;
|
||||
mod routes;
|
||||
@@ -8,6 +9,7 @@ mod routes;
|
||||
pub use form::*;
|
||||
pub use link::*;
|
||||
pub use outlet::*;
|
||||
pub use redirect::*;
|
||||
pub use route::*;
|
||||
pub use router::*;
|
||||
pub use routes::*;
|
||||
|
||||
@@ -7,6 +7,7 @@ use leptos::*;
|
||||
/// that child route is displayed. Renders nothing if there is no nested child.
|
||||
#[component]
|
||||
pub fn Outlet(cx: Scope) -> impl IntoView {
|
||||
let id = HydrationCtx::id();
|
||||
let route = use_route(cx);
|
||||
let is_showing = Rc::new(Cell::new(None::<(usize, Scope)>));
|
||||
let (outlet, set_outlet) = create_signal(cx, None::<View>);
|
||||
@@ -26,11 +27,11 @@ pub fn Outlet(cx: Scope) -> impl IntoView {
|
||||
prev_scope.dispose();
|
||||
}
|
||||
is_showing.set(Some((child.id(), child.cx())));
|
||||
provide_context(child.cx(), child.clone());
|
||||
set_outlet.set(Some(child.outlet().into_view(cx)))
|
||||
provide_context(cx, child.clone());
|
||||
set_outlet.set(Some(child.outlet(cx).into_view(cx)))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
move || outlet.get()
|
||||
leptos::DynChild::new_with_id(id, move || outlet.get())
|
||||
}
|
||||
|
||||
65
router/src/components/redirect.rs
Normal file
65
router/src/components/redirect.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use crate::{use_navigate, use_resolved_path, NavigateOptions};
|
||||
use leptos::{component, provide_context, use_context, IntoView, Scope};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Redirects the user to a new URL, whether on the client side or on the server
|
||||
/// side. If rendered on the server, this sets a `302` status code and sets a `Location`
|
||||
/// header. If rendered in the browser, it uses client-side navigation to redirect.
|
||||
/// In either case, it resolves the route relative to the current route. (To use
|
||||
/// an absolute path, prefix it with `/`).
|
||||
///
|
||||
/// **Note**: Support for server-side redirects is provided by the server framework
|
||||
/// integrations (`leptos_actix` and `leptos_axum`). If you’re not using one of those
|
||||
/// integrations, you should manually provide a way of redirecting on the server
|
||||
/// using [provide_server_redirect].
|
||||
#[component]
|
||||
pub fn Redirect<P>(
|
||||
cx: Scope,
|
||||
/// The relative path to which the user should be redirected.
|
||||
path: P,
|
||||
/// Navigation options to be used on the client side.
|
||||
#[prop(optional)]
|
||||
options: Option<NavigateOptions>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
P: std::fmt::Display + 'static,
|
||||
{
|
||||
// resolve relative path
|
||||
let path = use_resolved_path(cx, move || path.to_string());
|
||||
let path = path.get().unwrap_or_else(|| "/".to_string());
|
||||
|
||||
// redirect on the server
|
||||
if let Some(redirect_fn) = use_context::<ServerRedirectFunction>(cx) {
|
||||
(redirect_fn.f)(&path);
|
||||
}
|
||||
|
||||
// redirect on the client
|
||||
let navigate = use_navigate(cx);
|
||||
navigate(&path, options.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Wrapping type for a function provided as context to allow for
|
||||
/// server-side redirects. See [provide_server_redirect]
|
||||
/// and [Redirect].
|
||||
#[derive(Clone)]
|
||||
pub struct ServerRedirectFunction {
|
||||
f: Rc<dyn Fn(&str)>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ServerRedirectFunction {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ServerRedirectFunction").finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides a function that can be used to redirect the user to another
|
||||
/// absolute path, on the server. This should set a `302` status code and an
|
||||
/// appropriate `Location` header.
|
||||
pub fn provide_server_redirect(cx: Scope, handler: impl Fn(&str) + 'static) {
|
||||
provide_context(
|
||||
cx,
|
||||
ServerRedirectFunction {
|
||||
f: Rc::new(handler),
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -36,31 +36,47 @@ where
|
||||
F: Fn(Scope) -> E + 'static,
|
||||
P: std::fmt::Display,
|
||||
{
|
||||
let children = children
|
||||
.map(|children| {
|
||||
children(cx)
|
||||
.as_children()
|
||||
.iter()
|
||||
.filter_map(|child| {
|
||||
child
|
||||
.as_transparent()
|
||||
.and_then(|t| t.downcast_ref::<RouteDefinition>())
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let id = ROUTE_ID.with(|id| {
|
||||
let next = id.get() + 1;
|
||||
id.set(next);
|
||||
next
|
||||
});
|
||||
RouteDefinition {
|
||||
id,
|
||||
path: path.to_string(),
|
||||
children,
|
||||
view: Rc::new(move |cx| view(cx).into_view(cx)),
|
||||
fn inner(
|
||||
cx: Scope,
|
||||
children: Option<Children>,
|
||||
path: String,
|
||||
view: Rc<dyn Fn(Scope) -> View>,
|
||||
) -> RouteDefinition {
|
||||
let children = children
|
||||
.map(|children| {
|
||||
children(cx)
|
||||
.as_children()
|
||||
.iter()
|
||||
.filter_map(|child| {
|
||||
child
|
||||
.as_transparent()
|
||||
.and_then(|t| t.downcast_ref::<RouteDefinition>())
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let id = ROUTE_ID.with(|id| {
|
||||
let next = id.get() + 1;
|
||||
id.set(next);
|
||||
next
|
||||
});
|
||||
|
||||
RouteDefinition {
|
||||
id,
|
||||
path,
|
||||
children,
|
||||
view,
|
||||
}
|
||||
}
|
||||
|
||||
inner(
|
||||
cx,
|
||||
children,
|
||||
path.to_string(),
|
||||
Rc::new(move |cx| view(cx).into_view(cx)),
|
||||
)
|
||||
}
|
||||
|
||||
impl IntoView for RouteDefinition {
|
||||
@@ -104,7 +120,7 @@ impl RouteContext {
|
||||
path: RefCell::new(path),
|
||||
original_path: route.original_path.to_string(),
|
||||
params,
|
||||
outlet: Box::new(move || Some(element(cx))),
|
||||
outlet: Box::new(move |cx| Some(element(cx))),
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -155,7 +171,7 @@ impl RouteContext {
|
||||
path: RefCell::new(path.to_string()),
|
||||
original_path: path.to_string(),
|
||||
params: create_memo(cx, |_| ParamsMap::new()),
|
||||
outlet: Box::new(move || fallback.as_ref().map(move |f| f(cx))),
|
||||
outlet: Box::new(move |cx| fallback.as_ref().map(move |f| f(cx))),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -171,8 +187,8 @@ impl RouteContext {
|
||||
}
|
||||
|
||||
/// The view associated with the current route.
|
||||
pub fn outlet(&self) -> impl IntoView {
|
||||
(self.inner.outlet)()
|
||||
pub fn outlet(&self, cx: Scope) -> impl IntoView {
|
||||
(self.inner.outlet)(cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +200,7 @@ pub(crate) struct RouteContextInner {
|
||||
pub(crate) path: RefCell<String>,
|
||||
pub(crate) original_path: String,
|
||||
pub(crate) params: Memo<ParamsMap>,
|
||||
pub(crate) outlet: Box<dyn Fn() -> Option<View>>,
|
||||
pub(crate) outlet: Box<dyn Fn(Scope) -> Option<View>>,
|
||||
}
|
||||
|
||||
impl PartialEq for RouteContextInner {
|
||||
|
||||
@@ -29,7 +29,6 @@ pub fn Routes(
|
||||
let base_route = router.base();
|
||||
|
||||
let mut branches = Vec::new();
|
||||
let id_before = HydrationCtx::peek();
|
||||
let frag = children(cx);
|
||||
let children = frag
|
||||
.as_children()
|
||||
@@ -195,11 +194,12 @@ pub fn Routes(
|
||||
});
|
||||
|
||||
// show the root route
|
||||
let id = HydrationCtx::id();
|
||||
let root = create_memo(cx, move |prev| {
|
||||
provide_context(cx, route_states);
|
||||
route_states.with(|state| {
|
||||
if state.routes.borrow().is_empty() {
|
||||
Some(base_route.outlet().into_view(cx))
|
||||
Some(base_route.outlet(cx).into_view(cx))
|
||||
} else {
|
||||
let root = state.routes.borrow();
|
||||
let root = root.get(0);
|
||||
@@ -208,7 +208,7 @@ pub fn Routes(
|
||||
}
|
||||
|
||||
if prev.is_none() || !root_equal.get() {
|
||||
root.as_ref().map(|route| route.outlet().into_view(cx))
|
||||
root.as_ref().map(|route| route.outlet(cx).into_view(cx))
|
||||
} else {
|
||||
prev.cloned().unwrap()
|
||||
}
|
||||
@@ -216,8 +216,8 @@ pub fn Routes(
|
||||
})
|
||||
});
|
||||
|
||||
HydrationCtx::continue_from(id_before);
|
||||
(move || root.get()).into_view(cx)
|
||||
//HydrationCtx::continue_from(id_before);
|
||||
leptos::DynChild::new_with_id(id, move || root.get())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
||||
@@ -8,10 +8,6 @@
|
||||
//! apps (SPAs), server-side rendering/multi-page apps (MPAs), or to synchronize
|
||||
//! state between the two.
|
||||
//!
|
||||
//! **Note:** This is a work in progress. The feature to pass client-side route [State] in
|
||||
//! [History.state](https://developer.mozilla.org/en-US/docs/Web/API/History/state), in particular,
|
||||
//! is incomplete.
|
||||
//!
|
||||
//! ## Philosophy
|
||||
//!
|
||||
//! Leptos Router is built on a few simple principles:
|
||||
@@ -23,12 +19,7 @@
|
||||
//! and are rendered by different components. This means you can navigate between siblings
|
||||
//! in this tree without re-rendering or triggering any change in the parent routes.
|
||||
//!
|
||||
//! 3. **Route-based data loading.** Each route should know exactly which data it needs
|
||||
//! to render itself when the route is defined. This allows each route’s data to be
|
||||
//! reloaded independently, and allows data from nested routes to be loaded in parallel,
|
||||
//! avoiding waterfalls.
|
||||
//!
|
||||
//! 4. **Progressive enhancement.** The [A] and [Form] components resolve any relative
|
||||
//! 3. **Progressive enhancement.** The [A] and [Form] components resolve any relative
|
||||
//! nested routes, render actual `<a>` and `<form>` elements, and (when possible)
|
||||
//! upgrading them to handle those navigations with client-side routing. If you’re using
|
||||
//! them with server-side rendering (with or without hydration), they just work,
|
||||
@@ -202,6 +193,7 @@ mod history;
|
||||
mod hooks;
|
||||
#[doc(hidden)]
|
||||
pub mod matching;
|
||||
pub use matching::RouteDefinition;
|
||||
|
||||
pub use components::*;
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
|
||||
@@ -3,11 +3,18 @@ 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.
|
||||
#[derive(Clone)]
|
||||
pub struct RouteDefinition {
|
||||
/// A unique ID for each route.
|
||||
pub id: usize,
|
||||
/// The path. This can include params like `:id` or wildcards like `*all`.
|
||||
pub path: String,
|
||||
/// Other route definitions nested within this one.
|
||||
pub children: Vec<RouteDefinition>,
|
||||
/// The view that should be displayed when this route is matched.
|
||||
pub view: Rc<dyn Fn(Scope) -> View>,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user