Compare commits

..

5 Commits

Author SHA1 Message Date
Greg Johnston
9f704f91fe Use local observers 2023-01-27 13:52:59 -05:00
Greg Johnston
4b0e212f1c Rc/RefCell => Arc/RwLock 2023-01-27 13:07:34 -05:00
Greg Johnston
8fa89cffef Make tests work on stable 2023-01-27 12:54:44 -05:00
Greg Johnston
18ceb4c4f8 Remove unnecessary log dependency 2023-01-27 12:54:11 -05:00
Greg Johnston
0d3e2baa9b remove unused base64 dependency 2023-01-27 12:52:08 -05:00
77 changed files with 575 additions and 1558 deletions

View File

@@ -24,17 +24,17 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.1.3"
version = "0.1.1"
[workspace.dependencies]
leptos = { path = "./leptos", default-features = false, version = "0.1.3" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.1.3" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.1.3" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.1.3" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.1.3" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.1.3" }
leptos_router = { path = "./router", version = "0.1.3" }
leptos_meta = { path = "./meta", default-feature = false, version = "0.1.3" }
leptos = { path = "./leptos", default-features = false, version = "0.1.1" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.1.1" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.1.1" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.1.1" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.1.1" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.1.1" }
leptos_router = { path = "./router", version = "0.1.1" }
leptos_meta = { path = "./meta", default-feature = false, version = "0.1.1" }
[profile.release]
codegen-units = 1

View File

@@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.1"
leptos = "0.0.18"

View File

@@ -1,5 +1,5 @@
use leptos::*;
fn main() {
mount_to_body(|cx| view! { cx, <p>"Hello, world!"</p> })
mount_to_body(|_cx| view! { cx, <p>"Hello, world!"</p> })
}

View File

@@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.1"
leptos = "0.0.18"

View File

@@ -4,9 +4,7 @@ fn main() {
mount_to_body(|cx| {
let name = "gbj";
let userid = 0;
// This will be filled by _ref=input below.
let input_element = NodeRef::<HtmlElement<Input>>::new(cx);
let _input_element: Element;
view! {
cx,
@@ -19,7 +17,7 @@ fn main() {
prop:value="todo" // `prop:` lets you set a property on a DOM node
value="initial" // side note: the DOM `value` attribute only sets *initial* value
// this is very important when working with forms!
_ref=input_element // `_ref` stores tis element in a variable
_ref=_input_element // `_ref` stores tis element in a variable
/>
<ul data-user=userid> // attributes can take expressions as values
<li class="todo my-todo" // here we set the `class` attribute

View File

@@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.1"
leptos = "0.0.18"

View File

@@ -1,94 +0,0 @@
[package]
name = "errors_axum"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1.0.66"
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 = [
"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 }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
serde_json = "1.0.89"
gloo-net = { version = "0.2.5", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.3.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8" }
thiserror = "1.0.38"
tracing = "0.1.37"
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = ["dep:axum", "dep:tower", "dep:tower-http", "dep:tokio", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "dep:leptos_axum"]
[package.metadata.cargo-all-features]
denylist = [
"axum",
"tower",
"tower-http",
"tokio",
"leptos_axum",
]
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 = "errors_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"
# [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"
# The port to use for automatic reload monitoring
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"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Greg Johnston
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,4 +0,0 @@
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"

View File

@@ -1,42 +0,0 @@
# 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.
## 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.
## 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. Install cargo-leptos
```bash
cargo install --locked cargo-leptos
```
2. Build the site in watch mode, recompiling on file changes
```bash
cargo leptos watch
```
Open browser on [http://localhost:3000/](http://localhost:3000/)
3. When ready to deploy, run
```bash
cargo leptos build --release
```
## Server Side Rendering without cargo-leptos
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
1. Install wasm-pack
```bash
cargo install wasm-pack
```
2. Build the Webassembly used to hydrate the HTML from the server
```bash
wasm-pack build --target=web --debug --no-default-features --features=hydrate
```
3. Run the server to serve the Webassembly, JS, and HTML
```bash
cargo run --no-default-features --features=ssr
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,66 +0,0 @@
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,
};
#[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
#[component]
pub fn ErrorTemplate(
cx: Scope,
#[prop(optional)] outside_errors: Option<Errors>,
#[prop(optional)] errors: Option<RwSignal<Errors>>,
) -> impl IntoView {
let errors = match outside_errors {
Some(e) => create_rw_signal(cx, e),
None => match errors {
Some(e) => e,
None => panic!("No Errors found and we expected errors!"),
},
};
// Get Errors from Signal
let errors = errors.get().0;
// Downcast lets us take a type that implements `std::error::Error`
let errors: Vec<AppError> = errors
.into_iter()
.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")]{
let response = use_context::<ResponseOptions>(cx);
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
// 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 File

@@ -1,19 +0,0 @@
use http::status::StatusCode;
use thiserror::Error;
#[derive(Debug, Clone, Error)]
pub enum AppError {
#[error("Not Found")]
NotFound,
#[error("Internal Server Error")]
InternalServerError,
}
impl AppError {
pub fn status_code(&self) -> StatusCode {
match self {
AppError::NotFound => StatusCode::NOT_FOUND,
AppError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}

View File

@@ -1,49 +0,0 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::Extension,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions, Errors, view};
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
use crate::errors::AppError;
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else{
let mut errors = Errors::default();
errors.insert_with_default_key(AppError::NotFound);
let handler = leptos_axum::render_app_to_stream(options.to_owned(), move |cx| view!{cx, <ErrorTemplate outside_errors=errors.clone()/>});
handler(req).await.into_response()
}
}
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();
// `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).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)),
}
}
}
}

View File

@@ -1,80 +0,0 @@
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();
}
}}
#[server(CauseInternalServerError, "/api")]
pub async fn cause_internal_server_error() -> Result<(), ServerFnError> {
// fake API delay
std::thread::sleep(std::time::Duration::from_millis(1250));
Err(ServerFnError::ServerError(
"Generic Server Error".to_string(),
))
}
#[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);
provide_meta_context(cx);
view! {
cx,
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/errors_axum.css"/>
<Router>
<header>
<h1>"Error Examples:"</h1>
</header>
<main>
<Routes>
<Route path="" view=|cx| view! {
cx,
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
<ExampleErrors/>
</ErrorBoundary>
}/>
</Routes>
</main>
</Router>
}
}
#[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>
}
}
#[component]
pub fn ReturnsError(_cx: Scope) -> impl IntoView {
Err::<String, AppError>(AppError::InternalServerError)
}

View File

@@ -1,25 +0,0 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod error_template;
pub mod errors;
pub mod fallback;
pub mod landing;
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
use crate::landing::*;
#[wasm_bindgen]
pub fn hydrate() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(|cx| {
view! { cx, <App/> }
});
}
}
}

View File

@@ -1,74 +0,0 @@
use cfg_if::cfg_if;
use leptos::*;
// boilerplate to run in different modes
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
routing::{post, get},
extract::{Extension, Path},
http::Request,
response::{IntoResponse, Response},
Router,
};
use axum::body::Body as AxumBody;
use crate::landing::*;
use errors_axum::*;
use crate::fallback::file_and_error_handler;
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()
}
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
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;
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
// 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/> } )
.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();
}
}
// 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/> }
});
}
}
}

View File

@@ -1,3 +0,0 @@
.pending {
color: purple;
}

View File

@@ -6,7 +6,7 @@ const APP_ID: &str = "dev.leptos.Counter";
// Basic GTK app setup from https://gtk-rs.org/gtk4-rs/stable/latest/book/hello_world.html
fn main() {
_ = create_scope(create_runtime(), |cx| {
_ = create_scope(|cx| {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();

View File

@@ -2,12 +2,12 @@
This example highlights four different ways that child components can communicate with their parent:
1. `<ButtonA/>`: passing a WriteSignal as one of the child component props,
1. <ButtonA/>: passing a WriteSignal as one of the child component props,
for the child component to write into and the parent to read
2. `<ButtonB/>`: passing a closure as one of the child component props, for
2. <ButtonB/>: passing a closure as one of the child component props, for
the child component to call
3. `<ButtonC/>`: adding a simple event listener on the child component itself
4. `<ButtonD/>`: providing a context that is used in the component (rather than prop drilling)
3. <ButtonC/>: adding a simple event listener on the child component itself
4. <ButtonD/>: providing a context that is used in the component (rather than prop drilling)
## Client Side Rendering

View File

@@ -34,14 +34,12 @@ sqlx = { version = "0.6.2", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }
thiserror = "1.0.38"
tracing = "0.1.37"
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = ["dep:axum", "dep:tower", "dep:tower-http", "dep:tokio", "dep:sqlx", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "dep:leptos_axum"]
ssr = ["dep:axum", "dep:tower", "dep:tower-http", "dep:tokio", "dep:sqlx", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "leptos_axum"]
[package.metadata.cargo-all-features]
denylist = [

View File

@@ -3,7 +3,8 @@
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.
## Client Side Rendering
This example cannot be built as a trunk standalone CSR-only app. Only the server may directly connect to the database.
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle. Make sure you have trunk installed with `cargo install trunk`.
## 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)

View File

@@ -1,70 +1,28 @@
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,
};
#[cfg(feature = "ssr")]
use leptos_axum::ResponseOptions;
use leptos::{view, For, ForProps, IntoView, RwSignal, Scope, View};
// 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,
#[prop(optional)] outside_errors: Option<Errors>,
#[prop(optional)] errors: Option<RwSignal<Errors>>,
) -> impl IntoView {
let errors = match outside_errors {
Some(e) => {
let errors = create_rw_signal(cx, e);
errors
}
None => match errors {
Some(e) => e,
None => panic!("No Errors found and we expected errors!"),
},
pub fn error_template(cx: Scope, errors: Option<RwSignal<Errors>>) -> View {
let Some(errors) = errors else {
panic!("No Errors found and we expected errors!");
};
// Get Errors from Signal
let errors = errors.get().0;
// 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()
.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")]{
let response = use_context::<ResponseOptions>(cx);
if let Some(response) = response{
response.set_status(errors[0].status_code());
}
}
}
view! {cx,
<h1>"Errors"</h1>
<For
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each= move || {errors.clone().into_iter().enumerate()}
each= move || {errors.get().0.into_iter()}
// a unique key for each item as a reference
key=|(index, _error)| index.clone()
key=|error| error.0.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>
}
}
/>
}
.into_view(cx)
}

View File

@@ -1,19 +0,0 @@
use http::status::StatusCode;
use thiserror::Error;
#[derive(Debug, Clone, Error)]
pub enum TodoAppError {
#[error("Not Found")]
NotFound,
#[error("Internal Server Error")]
InternalServerError,
}
impl TodoAppError {
pub fn status_code(&self) -> StatusCode {
match self {
TodoAppError::NotFound => StatusCode::NOT_FOUND,
TodoAppError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}

View File

@@ -12,9 +12,8 @@ if #[cfg(feature = "ssr")] {
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::{LeptosOptions, Errors, view};
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
use crate::errors::TodoAppError;
use leptos::{LeptosOptions};
use crate::error_template::error_template;
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
@@ -24,9 +23,7 @@ if #[cfg(feature = "ssr")] {
if res.status() == StatusCode::OK {
res.into_response()
} else{
let mut errors = Errors::default();
errors.insert_with_default_key(TodoAppError::NotFound);
let handler = leptos_axum::render_app_to_stream(options.to_owned(), move |cx| view!{cx, <ErrorTemplate outside_errors=errors.clone()/>});
let handler = leptos_axum::render_app_to_stream(options.to_owned(), |cx| error_template(cx, None));
handler(req).await.into_response()
}
}

View File

@@ -1,7 +1,6 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod error_template;
pub mod errors;
pub mod fallback;
pub mod todo;

View File

@@ -7,6 +7,7 @@ if #[cfg(feature = "ssr")] {
routing::{post, get},
extract::{Extension, Path},
http::Request,
body::StreamBody,
response::{IntoResponse, Response},
Router,
};
@@ -16,6 +17,7 @@ if #[cfg(feature = "ssr")] {
use crate::fallback::file_and_error_handler;
use leptos_axum::{generate_route_list, LeptosRoutes};
use std::sync::Arc;
use leptos_reactive::run_scope;
//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{

View File

@@ -1,4 +1,3 @@
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
@@ -41,9 +40,9 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
// this is just an example of how to access server context injected in the handlers
// http::Request doesn't implement Clone, so more work will be needed to do use_context() on this
let req_parts = use_context::<leptos_axum::RequestParts>(cx);
if let Some(req_parts) = req_parts {
println!("Uri = {:?}", req_parts.uri);
if let Some(req_parts) = req_parts{
println!("Uri = {:?}", req_parts.uri);
}
use futures::TryStreamExt;
@@ -108,7 +107,7 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
#[component]
pub fn TodoApp(cx: Scope) -> impl IntoView {
//let id = use_context::<String>(cx);
let id = use_context::<String>(cx);
provide_meta_context(cx);
view! {
cx,
@@ -122,10 +121,8 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
<Routes>
<Route path="" view=|cx| view! {
cx,
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
<Todos/>
</ErrorBoundary>
}/> //Route
}/>
</Routes>
</main>
</Router>

View File

@@ -11,7 +11,6 @@ console_error_panic_hook = "0.1.7"
uuid = { version = "1", features = ["v4", "js", "serde"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
web-sys = { version = "0.3", features = ["Storage"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.0"

View File

@@ -13,5 +13,5 @@ futures = "0.3"
leptos = { workspace = true, features = ["ssr"] }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_router = { workspace = true, features = ["ssr"] }
parking_lot = "0.12.1"
regex = "1.7.0"
tokio = "1.24.1"

View File

@@ -18,9 +18,9 @@ use http::StatusCode;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use parking_lot::RwLock;
use regex::Regex;
use std::sync::Arc;
use tokio::sync::RwLock;
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.
@@ -49,25 +49,25 @@ pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>);
impl ResponseOptions {
/// A less boilerplatey way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`
pub fn overwrite(&self, parts: ResponseParts) {
let mut writable = self.0.write();
pub async fn overwrite(&self, parts: ResponseParts) {
let mut writable = self.0.write().await;
*writable = parts
}
/// Set the status of the returned Response
pub fn set_status(&self, status: StatusCode) {
let mut writeable = self.0.write();
pub async fn set_status(&self, status: StatusCode) {
let mut writeable = self.0.write().await;
let res_parts = &mut *writeable;
res_parts.status = Some(status);
}
/// Insert a header, overwriting any previous value with the same key
pub fn insert_header(&self, key: header::HeaderName, value: header::HeaderValue) {
let mut writeable = self.0.write();
pub async fn insert_header(&self, key: header::HeaderName, value: header::HeaderValue) {
let mut writeable = self.0.write().await;
let res_parts = &mut *writeable;
res_parts.headers.insert(key, value);
}
/// Append a header, leaving any header with the same key intact
pub fn append_header(&self, key: header::HeaderName, value: header::HeaderValue) {
let mut writeable = self.0.write();
pub async fn append_header(&self, key: header::HeaderName, value: header::HeaderValue) {
let mut writeable = self.0.write().await;
let res_parts = &mut *writeable;
res_parts.headers.append(key, value);
}
@@ -76,13 +76,15 @@ impl ResponseOptions {
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
/// it sets a [StatusCode] of 302 and a [LOCATION](header::LOCATION) header with the provided value.
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead.
pub fn redirect(cx: leptos::Scope, path: &str) {
pub async fn redirect(cx: leptos::Scope, path: &str) {
let response_options = use_context::<ResponseOptions>(cx).unwrap();
response_options.set_status(StatusCode::FOUND);
response_options.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path).expect("Failed to create HeaderValue"),
);
response_options.set_status(StatusCode::FOUND).await;
response_options
.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path).expect("Failed to create HeaderValue"),
)
.await;
}
/// An Actix [Route](actix_web::Route) that listens for a `POST` request with
@@ -180,7 +182,7 @@ pub fn handle_server_fns_with_context(
runtime.dispose();
let mut res: HttpResponseBuilder;
let mut res_parts = res_options.0.write();
let mut res_parts = res_options.0.write().await;
if accept_header == Some("application/json")
|| accept_header == Some("application/x-www-form-urlencoded")
@@ -337,7 +339,9 @@ where
}
};
stream_app(&options, app, res_options, additional_context).await
let (head, tail) = html_parts(&options);
stream_app(app, head, tail, res_options, additional_context).await
}
})
}
@@ -425,7 +429,9 @@ where
}
};
stream_app(&options, app, res_options, |_cx| {}).await
let (head, tail) = html_parts(&options);
stream_app(app, head, tail, res_options, |_cx| {}).await
}
})
}
@@ -451,31 +457,23 @@ fn leptos_corrected_path(req: &HttpRequest) -> String {
}
async fn stream_app(
options: &LeptosOptions,
app: impl FnOnce(leptos::Scope) -> View + 'static,
head: String,
tail: String,
res_options: ResponseOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
) -> HttpResponse<BoxBody> {
let (stream, runtime, scope) = render_to_stream_with_prefix_undisposed_with_context(
let (stream, runtime, _) = render_to_stream_with_prefix_undisposed_with_context(
app,
move |cx| {
let meta = use_context::<MetaContext>(cx);
let head = meta
.as_ref()
let head = use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default();
let body_meta = meta
.as_ref()
.and_then(|meta| meta.body.as_string())
.unwrap_or_default();
format!("{head}</head><body{body_meta}>").into()
format!("{head}</head><body>").into()
},
additional_context,
);
let cx = leptos::Scope { runtime, id: scope };
let (head, tail) = html_parts(options, use_context::<MetaContext>(cx).as_ref());
let mut stream = Box::pin(
futures::stream::once(async move { head.clone() })
.chain(stream)
@@ -491,7 +489,7 @@ async fn stream_app(
let second_chunk = stream.next().await;
let third_chunk = stream.next().await;
let res_options = res_options.0.read();
let res_options = res_options.0.read().await;
let (status, mut headers) = (res_options.status, res_options.headers.clone());
let status = status.unwrap_or_default();
@@ -518,7 +516,7 @@ async fn stream_app(
res
}
fn html_parts(options: &LeptosOptions, meta_context: Option<&MetaContext>) -> (String, String) {
fn html_parts(options: &LeptosOptions) -> (String, String) {
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
@@ -559,12 +557,9 @@ fn html_parts(options: &LeptosOptions, meta_context: Option<&MetaContext>) -> (S
false => "".to_string(),
};
let html_metadata = meta_context
.and_then(|mc| mc.html.as_string())
.unwrap_or_default();
let head = format!(
r#"<!DOCTYPE html>
<html{html_metadata}>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
@@ -646,16 +641,6 @@ pub trait LeptosRoutes {
Data: 'static,
Fut: Future<Output = Result<DataResponse<Data>, actix_web::Error>>,
IV: IntoView + 'static;
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static;
}
/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
@@ -702,28 +687,4 @@ where
}
router
}
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
let mut router = self;
for path in paths.iter() {
router = router.route(
path,
render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
),
);
}
router
}
}

View File

@@ -17,4 +17,3 @@ leptos_meta = { workspace = true, features = ["ssr"] }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_config = { workspace = true }
tokio = { version = "1.0", features = ["full"] }
parking_lot = "0.12.1"

View File

@@ -19,9 +19,8 @@ use hyper::body;
use leptos::*;
use leptos_meta::MetaContext;
use leptos_router::*;
use parking_lot::RwLock;
use std::{io, pin::Pin, sync::Arc};
use tokio::{task::spawn_blocking, task::LocalSet};
use tokio::{sync::RwLock, task::spawn_blocking, task::LocalSet};
/// A struct to hold the parts of the incoming Request. Since `http::Request` isn't cloneable, we're forced
/// to construct this for Leptos to use in Axum
@@ -60,25 +59,25 @@ pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>);
impl ResponseOptions {
/// A less boilerplatey way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`
pub fn overwrite(&self, parts: ResponseParts) {
let mut writable = self.0.write();
pub async fn overwrite(&self, parts: ResponseParts) {
let mut writable = self.0.write().await;
*writable = parts
}
/// Set the status of the returned Response
pub fn set_status(&self, status: StatusCode) {
let mut writeable = self.0.write();
pub async fn set_status(&self, status: StatusCode) {
let mut writeable = self.0.write().await;
let res_parts = &mut *writeable;
res_parts.status = Some(status);
}
/// Insert a header, overwriting any previous value with the same key
pub fn insert_header(&self, key: HeaderName, value: HeaderValue) {
let mut writeable = self.0.write();
pub async fn insert_header(&self, key: HeaderName, value: HeaderValue) {
let mut writeable = self.0.write().await;
let res_parts = &mut *writeable;
res_parts.headers.insert(key, value);
}
/// Append a header, leaving any header with the same key intact
pub fn append_header(&self, key: HeaderName, value: HeaderValue) {
let mut writeable = self.0.write();
pub async fn append_header(&self, key: HeaderName, value: HeaderValue) {
let mut writeable = self.0.write().await;
let res_parts = &mut *writeable;
res_parts.headers.append(key, value);
}
@@ -87,13 +86,15 @@ impl ResponseOptions {
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
/// it sets a StatusCode of 302 and a LOCATION header with the provided value.
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead
pub fn redirect(cx: leptos::Scope, path: &str) {
pub async fn redirect(cx: leptos::Scope, path: &str) {
let response_options = use_context::<ResponseOptions>(cx).unwrap();
response_options.set_status(StatusCode::FOUND);
response_options.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path).expect("Failed to create HeaderValue"),
);
response_options.set_status(StatusCode::FOUND).await;
response_options
.insert_header(
header::LOCATION,
header::HeaderValue::from_str(path).expect("Failed to create HeaderValue"),
)
.await;
}
/// Decomposes an HTTP request into its parts, allowing you to read its headers
@@ -225,7 +226,7 @@ async fn handle_server_fns_inner(
// Add headers from ResponseParts if they exist. These should be added as long
// as the server function returns an OK response
let res_options_outer = res_options.unwrap().0;
let res_options_inner = res_options_outer.read();
let res_options_inner = res_options_outer.read().await;
let (status, mut res_headers) = (
res_options_inner.status,
res_options_inner.headers.clone(),
@@ -419,6 +420,61 @@ where
let full_path = format!("http://leptos.dev{path}");
let pkg_path = &options.site_pkg_dir;
let output_name = &options.output_name;
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
let mut wasm_output_name = output_name.clone();
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
wasm_output_name.push_str("_bg");
}
let site_ip = &options.site_address.ip().to_string();
let reload_port = options.reload_port;
let leptos_autoreload = match std::env::var("LEPTOS_WATCH").is_ok() {
true => format!(
r#"
<script crossorigin="">(function () {{
var ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
ws.onmessage = (ev) => {{
let msg = JSON.parse(ev.data);
if (msg.all) window.location.reload();
if (msg.css) {{
const link = document.querySelector("link#leptos");
if (link) {{
let href = link.getAttribute('href').split('?')[0];
let newHref = href + '?version=' + new Date().getMilliseconds();
link.setAttribute('href', newHref);
}} else {{
console.warn("Could not find link#leptos");
}}
}};
}};
ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.');
}})()
</script>
"#
),
false => "".to_string(),
};
let head = format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js">
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
{leptos_autoreload}
"#
);
let tail = "</body></html>";
let (mut tx, rx) = futures::channel::mpsc::channel(8);
spawn_blocking({
@@ -462,24 +518,19 @@ where
},
add_context,
);
let cx = Scope { runtime, id: scope };
let (head, tail) = html_parts(&options, use_context::<MetaContext>(cx).as_ref());
_ = tx.send(head).await;
let mut shell = Box::pin(bundle);
while let Some(fragment) = shell.next().await {
_ = tx.send(fragment).await;
}
_ = tx.send(tail.to_string()).await;
// Extract the value of ResponseOptions from here
let cx = Scope { runtime, id: scope };
let res_options =
use_context::<ResponseOptions>(cx).unwrap();
let new_res_parts = res_options.0.read().clone();
let new_res_parts = res_options.0.read().await.clone();
let mut writable = res_options2.0.write();
let mut writable = res_options2.0.write().await;
*writable = new_res_parts;
runtime.dispose();
@@ -492,7 +543,12 @@ where
}
});
let mut stream = Box::pin(rx.map(|html| Ok(Bytes::from(html))));
let mut stream = Box::pin(
futures::stream::once(async move { head.clone() })
.chain(rx)
.chain(futures::stream::once(async { tail.to_string() }))
.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
let first_chunk = stream.next().await;
@@ -500,7 +556,7 @@ where
let third_chunk = stream.next().await;
// Extract the resources now that they've been rendered
let res_options = res_options3.0.read();
let res_options = res_options3.0.read().await;
let complete_stream = futures::stream::iter([
first_chunk.unwrap(),
@@ -525,65 +581,6 @@ where
}
}
fn html_parts(options: &LeptosOptions, meta: Option<&MetaContext>) -> (String, &'static str) {
let pkg_path = &options.site_pkg_dir;
let output_name = &options.output_name;
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
let mut wasm_output_name = output_name.clone();
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
wasm_output_name.push_str("_bg");
}
let site_ip = &options.site_address.ip().to_string();
let reload_port = options.reload_port;
let leptos_autoreload = match std::env::var("LEPTOS_WATCH").is_ok() {
true => format!(
r#"
<script crossorigin="">(function () {{
var ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
ws.onmessage = (ev) => {{
let msg = JSON.parse(ev.data);
if (msg.all) window.location.reload();
if (msg.css) {{
const link = document.querySelector("link#leptos");
if (link) {{
let href = link.getAttribute('href').split('?')[0];
let newHref = href + '?version=' + new Date().getMilliseconds();
link.setAttribute('href', newHref);
}} else {{
console.warn("Could not find link#leptos");
}}
}};
}};
ws.onclose = () => console.warn('Live-reload stopped. Manual reload necessary.');
}})()
</script>
"#
),
false => "".to_string(),
};
let html_metadata = meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
let head = format!(
r#"<!DOCTYPE html>
<html{html_metadata}>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js">
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
{leptos_autoreload}
"#
);
let tail = "</body></html>";
(head, tail)
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths.
@@ -604,7 +601,7 @@ where
.run_until(async move {
tokio::task::spawn_local(async move {
let routes = leptos_router::generate_route_list_inner(app_fn);
let mut writable = routes_inner.0.write();
let mut writable = routes_inner.0.write().await;
*writable = routes;
})
.await
@@ -612,7 +609,7 @@ where
})
.await;
let routes = routes.0.read().to_owned();
let routes = routes.0.read().await.to_owned();
// Axum's Router defines Root routes as "/" not ""
let routes: Vec<String> = routes
.into_iter()
@@ -637,21 +634,6 @@ pub trait LeptosRoutes {
) -> Self
where
IV: IntoView + 'static;
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static;
fn leptos_routes_with_handler<H, T>(self, paths: Vec<String>, handler: H) -> Self
where
H: axum::handler::Handler<T, (), axum::body::Body>,
T: 'static;
}
/// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests
/// to those paths to Leptos's renderer.
@@ -674,40 +656,4 @@ impl LeptosRoutes for axum::Router {
}
router
}
fn leptos_routes_with_context<IV>(
self,
options: LeptosOptions,
paths: Vec<String>,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
let mut router = self;
for path in paths.iter() {
router = router.route(
path,
get(render_app_to_stream_with_context(
options.clone(),
additional_context.clone(),
app_fn.clone(),
)),
);
}
router
}
fn leptos_routes_with_handler<H, T>(self, paths: Vec<String>, handler: H) -> Self
where
H: axum::handler::Handler<T, (), axum::body::Body>,
T: 'static,
{
let mut router = self;
for path in paths.iter() {
router = router.route(path, get(handler.clone()));
}
router
}
}

View File

@@ -16,7 +16,7 @@ leptos_reactive = { workspace = true }
leptos_server = { workspace = true }
leptos_config = { workspace = true }
tracing = "0.1"
typed-builder = "0.12"
typed-builder = "0.11"
once_cell = "1.17.0"
[dev-dependencies]
@@ -24,30 +24,10 @@ leptos = { path = ".", default-features = false }
[features]
default = ["csr", "serde"]
csr = [
"leptos_dom/web",
"leptos_macro/csr",
"leptos_reactive/csr",
"leptos_server/csr",
]
hydrate = [
"leptos_dom/web",
"leptos_macro/hydrate",
"leptos_reactive/hydrate",
"leptos_server/hydrate",
]
ssr = [
"leptos_dom/ssr",
"leptos_macro/ssr",
"leptos_reactive/ssr",
"leptos_server/ssr",
]
stable = [
"leptos_dom/stable",
"leptos_macro/stable",
"leptos_reactive/stable",
"leptos_server/stable",
]
csr = ["leptos_dom/web", "leptos_macro/csr", "leptos_reactive/csr", "leptos_server/csr"]
hydrate = ["leptos_dom/web", "leptos_macro/hydrate", "leptos_reactive/hydrate", "leptos_server/hydrate"]
ssr = ["leptos_dom/ssr", "leptos_macro/ssr", "leptos_reactive/ssr", "leptos_server/ssr"]
stable = ["leptos_dom/stable", "leptos_macro/stable", "leptos_reactive/stable", "leptos_server/stable"]
serde = ["leptos_reactive/serde"]
serde-lite = ["leptos_reactive/serde-lite"]
miniserde = ["leptos_reactive/miniserde"]

View File

@@ -1,41 +1,17 @@
use crate::Children;
use leptos_dom::{Errors, IntoView};
use leptos_dom::{Errors, Fragment, IntoView};
use leptos_macro::component;
use leptos_reactive::{create_rw_signal, provide_context, RwSignal, Scope};
/// When you render a `Result<_, _>` in your view, in the `Err` case it will
/// render nothing, and search up through the view tree for an `<ErrorBoundary/>`.
/// This component lets you define a fallback that should be rendered in that
/// error case, allowing you to handle errors within a section of the interface.
///
/// ```
/// # use leptos_reactive::*;
/// # use leptos_macro::*;
/// # use leptos_dom::*; use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// let (value, set_value) = create_signal(cx, Ok(0));
/// let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
///
/// view! { cx,
/// <input type="text" on:input=on_input/>
/// <ErrorBoundary
/// fallback=move |_, _| view! { cx, <p class="error">"Enter a valid number."</p>}
/// >
/// <p>"Value is: " {value}</p>
/// </ErrorBoundary>
/// }
/// # });
/// ```
#[component(transparent)]
pub fn ErrorBoundary<F, IV>(
cx: Scope,
/// The components inside the tag which will get rendered
children: Children,
children: Box<dyn FnOnce(Scope) -> Fragment>,
/// A fallback that will be shown if an error occurs.
fallback: F,
) -> impl IntoView
where
F: Fn(Scope, RwSignal<Errors>) -> IV + 'static,
F: Fn(Scope, Option<RwSignal<Errors>>) -> IV + 'static,
IV: IntoView,
{
let errors: RwSignal<Errors> = create_rw_signal(cx, Errors::default());
@@ -47,6 +23,6 @@ where
move || match errors.get().0.is_empty() {
true => children.clone().into_view(cx),
false => fallback(cx, errors).into_view(cx),
false => fallback(cx, Some(errors)).into_view(cx),
}
}

View File

@@ -164,11 +164,3 @@ pub use transition::*;
pub use leptos_reactive::debug_warn;
extern crate self as leptos;
/// The most common type for the `children` property on components,
/// which can only be called once.
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>;

View File

@@ -1,36 +1,16 @@
use crate::Children;
use leptos::component;
use leptos_dom::IntoView;
use leptos_dom::{Fragment, IntoView};
use leptos_reactive::Scope;
use once_cell::sync::Lazy;
/// 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.
///
/// ```rust
/// # use leptos_reactive::*;
/// # use leptos_macro::*;
/// # use leptos_dom::*; use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// let (value, set_value) = create_signal(cx, 0);
///
/// view! { cx,
/// <Show
/// when=move || value() < 5
/// fallback=|cx| view! { cx, "Big number!" }
/// >
/// "Small number!"
/// </Show>
/// }
/// # });
/// ```
/// A component that will show it's children when the passed in closure is True, and show the fallback
/// when the closure is false
#[component]
pub fn Show<F, W, IV>(
/// The scope the component is running in
cx: Scope,
/// The components Show wraps
children: Children,
children: Box<dyn FnOnce(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

View File

@@ -129,22 +129,3 @@ fn ssr_with_styles() {
);
});
}
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
#[test]
fn ssr_option() {
use leptos::*;
_ = create_scope(create_runtime(), |cx| {
let (value, set_value) = create_signal(cx, 0);
let rendered = view! {
cx,
<option/>
};
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<option id=\"_0-1\"></option>"
);
});
}

View File

@@ -14,4 +14,4 @@ fs = "0.0.5"
regex = "1.7.0"
serde = { version = "1.0.151", features = ["derive"] }
thiserror = "1.0.38"
typed-builder = "0.12"
typed-builder = "0.11"

View File

@@ -12,6 +12,7 @@ cfg-if = "1"
drain_filter_polyfill = "0.1"
educe = "0.4"
futures = "0.3"
gloo = { version = "0.8", features = ["futures"] }
html-escape = "0.2"
indexmap = "1.9"
itertools = "0.10"
@@ -33,11 +34,8 @@ leptos = { path = "../leptos" }
[dependencies.web-sys]
version = "0.3"
features = [
"console",
"Comment",
"Document",
"DomTokenList",
"Location",
"Range",
"Text",
"HtmlCollection",
@@ -51,7 +49,6 @@ features = [
"DeviceMotionEvent",
"DeviceOrientationEvent",
"DragEvent",
"ErrorEvent",
"FocusEvent",
"GamepadEvent",
"HashChangeEvent",

View File

@@ -5,12 +5,12 @@ use std::{collections::HashMap, error::Error, sync::Arc};
/// A struct to hold all the possible errors that could be provided by child Views
#[derive(Debug, Clone, Default)]
pub struct Errors(pub HashMap<HydrationKey, Arc<dyn Error + Send + Sync>>);
pub struct Errors(pub HashMap<HydrationKey, Arc<dyn Error>>);
impl<T, E> IntoView for Result<T, E>
where
T: IntoView + 'static,
E: Error + Send + Sync + 'static,
E: std::error::Error + Send + Sync + 'static,
{
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
match self {
@@ -19,28 +19,23 @@ where
match use_context::<RwSignal<Errors>>(cx) {
Some(errors) => {
let id = HydrationCtx::id();
errors.update({
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let id = id.clone();
move |errors: &mut Errors| errors.insert(id, error)
});
errors.update(move |errors: &mut Errors| errors.insert(id, error));
// remove the error from the list if this drops,
// i.e., if it's in a DynChild that switches from Err to Ok
// Only can run on the client, will panic on the server
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use leptos_reactive::{on_cleanup, queue_microtask};
on_cleanup(cx, move || {
queue_microtask(move || {
errors.update(|errors: &mut Errors| {
crate::log!("removing error at {id}");
errors.remove::<E>(&id);
});
});
if #[cfg(any(feature = "hydrate", feature="csr"))] {
use leptos_reactive::{on_cleanup, queue_microtask};
on_cleanup(cx, move || {
queue_microtask(move || {
errors.update(|errors: &mut Errors| {
errors.remove::<E>(&id);
});
});
});
}
}
}
}
None => {
#[cfg(debug_assertions)]
@@ -59,21 +54,14 @@ impl Errors {
/// Add an error to Errors that will be processed by `<ErrorBoundary/>`
pub fn insert<E>(&mut self, key: HydrationKey, error: E)
where
E: Error + Send + Sync + 'static,
E: Error + 'static,
{
self.0.insert(key, Arc::new(error));
}
/// Add an error with the default key for errors outside the reactive system
pub fn insert_with_default_key<E>(&mut self, error: E)
where
E: Error + Send + Sync + 'static,
{
self.0.insert(HydrationKey::default(), Arc::new(error));
}
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
pub fn remove<E>(&mut self, key: &HydrationKey)
where
E: Error + Send + Sync + 'static,
E: Error + 'static,
{
self.0.remove(key);
}

View File

@@ -6,8 +6,12 @@ use cfg_if::cfg_if;
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
use crate::events::*;
use crate::macro_helpers::*;
use crate::macro_helpers::Property;
use crate::macro_helpers::{
attribute_expression, class_expression, property_expression,
};
use crate::{mount_child, MountKind};
use leptos_reactive::create_render_effect;
use once_cell::unsync::Lazy as LazyCell;
use wasm_bindgen::JsCast;
@@ -42,7 +46,7 @@ cfg_if! {
use crate::{
ev::EventDescriptor,
hydration::HydrationCtx,
macro_helpers::{IntoAttribute, IntoClass, IntoProperty},
macro_helpers::{Attribute, Class, IntoAttribute, IntoClass, IntoProperty},
Element, Fragment, IntoView, NodeRef, Text, View,
};
use leptos_reactive::Scope;
@@ -199,8 +203,10 @@ impl Custom {
el.unchecked_into()
} else {
crate::warn!(
"element with id {id} not found, ignoring it for hydration"
gloo::console::warn!(
"element with id",
format!("_{id}"),
"not found, ignoring it for hydration"
);
crate::document().create_element(&name).unwrap()
@@ -489,18 +495,26 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
attribute_helper(
self.element.as_ref(),
name,
attr.into_attribute(self.cx),
);
let el = self.element.as_ref();
let value = attr.into_attribute(self.cx);
match value {
Attribute::Fn(cx, f) => {
let el = el.clone();
create_render_effect(cx, move |old| {
let new = f();
if old.as_ref() != Some(&new) {
attribute_expression(&el, &name, new.clone());
}
new
});
}
_ => attribute_expression(el, &name, value),
};
self
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
use crate::macro_helpers::Attribute;
let mut this = self;
let mut attr = attr.into_attribute(this.cx);
@@ -540,16 +554,26 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
{
let el = self.element.as_ref();
let class_list = el.class_list();
let value = class.into_class(self.cx);
class_helper(el, name, value);
match value {
Class::Fn(cx, f) => {
create_render_effect(cx, move |old| {
let new = f();
if old.as_ref() != Some(&new) && (old.is_some() || new) {
class_expression(&class_list, &name, new)
}
new
});
}
Class::Value(value) => class_expression(&class_list, &name, value),
};
self
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
{
use crate::macro_helpers::Class;
let mut this = self;
let class = class.into_class(this.cx);
@@ -585,7 +609,25 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
let name = name.into();
let value = value.into_property(self.cx);
let el = self.element.as_ref();
property_helper(el, name, value);
match value {
Property::Fn(cx, f) => {
let el = el.clone();
create_render_effect(cx, move |old| {
let new = f();
let prop_name = wasm_bindgen::intern(&name);
if old.as_ref() != Some(&new)
&& !(old.is_none() && new == wasm_bindgen::JsValue::UNDEFINED)
{
property_expression(&el, prop_name, new.clone())
}
new
});
}
Property::Value(value) => {
let prop_name = wasm_bindgen::intern(&name);
property_expression(el, prop_name, value)
}
};
}
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
@@ -806,18 +848,63 @@ macro_rules! generate_html_tags {
let id = HydrationCtx::id();
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let element = create_leptos_element(
&stringify!([<$tag:upper>]),
id,
|| {
let element = if HydrationCtx::is_hydrating() {
if let Some(el) = crate::document().get_element_by_id(
&format!("_{id}")
) {
#[cfg(debug_assertions)]
assert_eq!(
el.node_name().to_ascii_uppercase(),
stringify!([<$tag:upper>]),
"SSR and CSR elements have the same `TopoId` \
but different node kinds. This is either a \
discrepancy between SSR and CSR rendering
logic, which is considered a bug, or it \
can also be a leptos hydration issue."
);
el.remove_attribute("id").unwrap();
el.unchecked_into()
} else if let Ok(Some(el)) = crate::document().query_selector(
&format!("[leptos-hk=_{id}]")
) {
#[cfg(debug_assertions)]
assert_eq!(
el.node_name().to_ascii_uppercase(),
stringify!([<$tag:upper>]),
"SSR and CSR elements have the same `TopoId` \
but different node kinds. This is either a \
discrepancy between SSR and CSR rendering
logic, which is considered a bug, or it \
can also be a leptos hydration issue."
);
el.remove_attribute("leptos-hk").unwrap();
el.unchecked_into()
} else {
gloo::console::warn!(
"element with id",
format!("_{id}"),
"not found, ignoring it for hydration"
);
[<$tag:upper>]
.with(|el|
el.clone_node()
.unwrap()
.unchecked_into()
)
}
} else {
[<$tag:upper>]
.with(|el|
el.clone_node()
.unwrap()
.unchecked_into()
)
}
);
};
Self {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@@ -892,70 +979,24 @@ macro_rules! generate_html_tags {
}
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
fn create_leptos_element(
tag: &str,
id: crate::HydrationKey,
clone_element: fn() -> web_sys::HtmlElement,
) -> web_sys::HtmlElement {
if HydrationCtx::is_hydrating() {
if let Some(el) = crate::document().get_element_by_id(&format!("_{id}")) {
#[cfg(debug_assertions)]
assert_eq!(
&el.node_name().to_ascii_uppercase(),
tag,
"SSR and CSR elements have the same `TopoId` but different node \
kinds. This is either a discrepancy between SSR and CSR rendering
logic, which is considered a bug, or it can also be a leptos \
hydration issue."
);
el.remove_attribute("id").unwrap();
el.unchecked_into()
} else if let Ok(Some(el)) =
crate::document().query_selector(&format!("[leptos-hk=_{id}]"))
{
#[cfg(debug_assertions)]
assert_eq!(
el.node_name().to_ascii_uppercase(),
tag,
"SSR and CSR elements have the same `TopoId` but different node \
kinds. This is either a discrepancy between SSR and CSR rendering
logic, which is considered a bug, or it can also be a leptos \
hydration issue."
);
el.remove_attribute("leptos-hk").unwrap();
el.unchecked_into()
} else {
crate::warn!("element with id {id} not found, ignoring it for hydration");
clone_element()
}
} else {
clone_element()
}
}
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
fn warn_on_ambiguous_a(parent: &web_sys::Element, child: &View) {
if let View::Element(el) = &child {
if (el.name == "a"
if el.name == "a"
|| el.name == "script"
|| el.name == "style"
|| el.name == "title")
&& parent.namespace_uri() != el.element.namespace_uri()
|| el.name == "title"
{
crate::warn!(
"Warning: you are appending an SVG element to an HTML element, or an \
HTML element to an SVG. Typically, this occurs when you create an \
<a/> or <script/> with the `view` macro and append it to an SVG, but \
the framework assumed it was HTML when you created it. To specify \
that it is an SVG element, use <svg::{{tag name}}/> in the view \
macro."
)
if parent.namespace_uri() != el.element.namespace_uri() {
crate::warn!(
"Warning: you are appending an SVG element to an HTML element, or \
an HTML element to an SVG. Typically, this occurs when you create \
an <a/> or <script/> with the `view` macro and append it to an \
SVG, but the framework assumed it was HTML when you created it. To \
specify that it is an SVG element, use <svg::{{tag name}}/> in the \
view macro."
)
}
}
}
}

View File

@@ -95,8 +95,10 @@ macro_rules! generate_math_tags {
el.unchecked_into()
} else {
crate::warn!(
"element with id {id} not found, ignoring it for hydration"
gloo::console::warn!(
"element with id",
format!("_{id}"),
"not found, ignoring it for hydration"
);
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]

View File

@@ -92,8 +92,10 @@ macro_rules! generate_svg_tags {
el.unchecked_into()
} else {
crate::warn!(
"element with id {id} not found, ignoring it for hydration"
gloo::console::warn!(
"element with id",
format!("_{id}"),
"not found, ignoring it for hydration"
);
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]

View File

@@ -21,7 +21,7 @@ cfg_if! {
while let Ok(Some(node)) = walker.next_node() {
if let Some(content) = node.text_content() {
if let Some(hk) = content.strip_prefix("hk=") {
if let Some(hk) = hk.split('|').next() {
if let Some(hk) = hk.split("|").next() {
map.insert(hk.into(), node.unchecked_into());
}
}

View File

@@ -308,8 +308,10 @@ impl Comment {
marker.remove();
} else {
crate::warn!(
"component with id {id} not found, ignoring it for hydration"
gloo::console::warn!(
"component with id",
id,
"not found, ignoring it for hydration"
);
}
}

View File

@@ -49,9 +49,9 @@ impl Attribute {
/// Converts the attribute to its HTML value at that moment, not including
/// the attribute name, so it can be rendered on the server.
pub fn as_nameless_value_string(&self) -> Option<String> {
pub fn as_nameless_value_string(&self) -> String {
match self {
Attribute::String(value) => Some(value.to_string()),
Attribute::String(value) => value.to_string(),
Attribute::Fn(_, f) => {
let mut value = f();
while let Attribute::Fn(_, f) = value {
@@ -59,16 +59,11 @@ impl Attribute {
}
value.as_nameless_value_string()
}
Attribute::Option(_, value) => {
value.as_ref().map(|value| value.to_string())
}
Attribute::Bool(include) => {
if *include {
Some("".to_string())
} else {
None
}
}
Attribute::Option(_, value) => value
.as_ref()
.map(|value| value.to_string())
.unwrap_or_default(),
Attribute::Bool(_) => String::new(),
}
}
}
@@ -174,31 +169,7 @@ attr_type!(f64);
attr_type!(char);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use std::borrow::Cow;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn attribute_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
value: Attribute,
) {
use leptos_reactive::create_render_effect;
match value {
Attribute::Fn(cx, f) => {
let el = el.clone();
create_render_effect(cx, move |old| {
let new = f();
if old.as_ref() != Some(&new) {
attribute_expression(&el, &name, new.clone());
}
new
});
}
_ => attribute_expression(el, &name, value),
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn attribute_expression(
pub fn attribute_expression(
el: &web_sys::Element,
attr_name: &str,
value: Attribute,

View File

@@ -67,33 +67,7 @@ impl<T: IntoClass> IntoClass for (Scope, T) {
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use std::borrow::Cow;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn class_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
value: Class,
) {
use leptos_reactive::create_render_effect;
let class_list = el.class_list();
match value {
Class::Fn(cx, f) => {
create_render_effect(cx, move |old| {
let new = f();
if old.as_ref() != Some(&new) && (old.is_some() || new) {
class_expression(&class_list, &name, new)
}
new
});
}
Class::Value(value) => class_expression(&class_list, &name, value),
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn class_expression(
pub fn class_expression(
class_list: &web_sys::DomTokenList,
class_name: &str,
value: bool,

View File

@@ -77,39 +77,7 @@ prop_type!(f64);
prop_type!(bool);
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use std::borrow::Cow;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn property_helper(
el: &web_sys::Element,
name: Cow<'static, str>,
value: Property,
) {
use leptos_reactive::create_render_effect;
match value {
Property::Fn(cx, f) => {
let el = el.clone();
create_render_effect(cx, move |old| {
let new = f();
let prop_name = wasm_bindgen::intern(&name);
if old.as_ref() != Some(&new)
&& !(old.is_none() && new == wasm_bindgen::JsValue::UNDEFINED)
{
property_expression(&el, prop_name, new.clone())
}
new
});
}
Property::Value(value) => {
let prop_name = wasm_bindgen::intern(&name);
property_expression(el, prop_name, value)
}
};
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
pub(crate) fn property_expression(
pub fn property_expression(
el: &web_sys::Element,
prop_name: &str,
value: JsValue,

View File

@@ -14,7 +14,6 @@ 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"
@@ -32,7 +31,7 @@ convert_case = "0.6.0"
[dev-dependencies]
log = "0.4"
typed-builder = "0.12"
typed-builder = "0.11"
leptos = { path = "../leptos" }
[features]

View File

@@ -167,7 +167,7 @@ impl ToTokens for Model {
let component = if *is_transparent {
quote! {
#body_name(#scope_name, #prop_names)
#body_name(cx, #prop_names)
}
} else {
quote! {
@@ -287,8 +287,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,9 +402,11 @@ 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;
@@ -611,9 +613,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, .. },

View File

@@ -171,7 +171,7 @@ pub(crate) fn render_view(
cx,
Span::call_site(),
nodes,
true,
false,
TagType::Unknown,
global_class,
)
@@ -219,7 +219,7 @@ fn fragment_to_tokens_ssr(
});
quote! {
{
leptos::Fragment::lazy(|| vec![
leptos::Fragment::new(vec![
#(#nodes),*
])
}
@@ -336,7 +336,7 @@ fn element_to_tokens_ssr(
),
Node::Text(text) => {
if let Some(value) = value_to_string(&text.value) {
template.push_str(&html_escape::encode_safe(&value));
template.push_str(&value);
} else {
template.push_str("{}");
let value = text.value.as_ref();
@@ -427,21 +427,18 @@ fn attribute_to_tokens_ssr(
if name != "class" {
template.push(' ');
template.push_str(&name);
if let Some(value) = node.value.as_ref() {
if let Some(value) = value_to_string(value) {
template.push_str(&name);
template.push_str("=\"");
template.push_str(&value);
template.push('"');
} else {
template.push_str("{}");
template.push_str("=\"{}\"");
let value = value.as_ref();
holes.push(quote! {
&{#value}.into_attribute(#cx)
.as_nameless_value_string()
.map(|a| format!("{}=\"{}\"", #name, leptos::escape_attr(&a)))
.unwrap_or_default(),
leptos::escape_attr(&{#value}.into_attribute(#cx).as_nameless_value_string()),
})
}
}
@@ -558,9 +555,7 @@ fn set_class_attribute_ssr(
template.push_str(" {}");
let value = value.as_ref();
holes.push(quote! {
&(cx, #value).into_attribute(#cx).as_nameless_value_string()
.map(|a| leptos::escape_attr(&a).to_string())
.unwrap_or_default(),
leptos::escape_attr(&(cx, #value).into_attribute(#cx).as_nameless_value_string()),
});
}
}
@@ -626,7 +621,7 @@ fn node_to_tokens(
cx,
Span::call_site(),
&fragment.children,
true,
false,
parent_type,
global_class,
),
@@ -708,7 +703,7 @@ fn element_to_tokens(
cx,
Span::call_site(),
&fragment.children,
true,
false,
parent_type,
global_class,
),
@@ -1063,17 +1058,11 @@ fn is_self_closing(node: &NodeElement) -> bool {
fn camel_case_tag_name(tag_name: &str) -> String {
let mut chars = tag_name.chars();
let first = chars.next();
let underscore = if tag_name == "option" || tag_name == "use" {
"_"
} else {
""
};
first
.map(|f| f.to_ascii_uppercase())
.into_iter()
.chain(chars)
.collect::<String>()
+ underscore
.collect()
}
fn is_svg_element(tag: &str) -> bool {

View File

@@ -8,16 +8,16 @@ repository = "https://github.com/leptos-rs/leptos"
description = "Reactive system for the Leptos web framework."
[dependencies]
log = "0.4"
slotmap = { version = "1", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde-lite = { version = "0.3", optional = true }
futures = { version = "0.3" }
js-sys = "0.3"
lazy_static = "1"
miniserde = { version = "0.1", optional = true }
parking_lot = "0.12"
serde-wasm-bindgen = "0.4"
serde_json = "1"
base64 = "0.21"
thiserror = "1"
tokio = { version = "1", features = ["rt"], optional = true }
tracing = "0.1"

View File

@@ -57,7 +57,7 @@ where
let id = value.type_id();
_ = with_runtime(cx.runtime, |runtime| {
let mut contexts = runtime.scope_contexts.borrow_mut();
let mut contexts = runtime.scope_contexts.write();
let context = contexts.entry(cx.id).unwrap().or_insert_with(HashMap::new);
context.insert(id, Box::new(value) as Box<dyn Any>);
});
@@ -116,7 +116,7 @@ where
let id = TypeId::of::<T>();
with_runtime(cx.runtime, |runtime| {
let local_value = {
let contexts = runtime.scope_contexts.borrow();
let contexts = runtime.scope_contexts.read();
let context = contexts.get(cx.id);
context
.and_then(|context| context.get(&id).and_then(|val| val.downcast_ref::<T>()))
@@ -124,16 +124,12 @@ where
};
match local_value {
Some(val) => Some(val),
None => runtime
.scope_parents
.borrow()
.get(cx.id)
.and_then(|parent| {
use_context::<T>(Scope {
runtime: cx.runtime,
id: *parent,
})
}),
None => runtime.scope_parents.read().get(cx.id).and_then(|parent| {
use_context::<T>(Scope {
runtime: cx.runtime,
id: *parent,
})
}),
}
})
.ok()

View File

@@ -1,8 +1,8 @@
#![forbid(unsafe_code)]
use crate::runtime::{with_runtime, RuntimeId};
use crate::runtime::{with_runtime, LocalObserver, RuntimeId};
use crate::{debug_warn, Runtime, Scope, ScopeProperty};
use cfg_if::cfg_if;
use std::cell::RefCell;
use parking_lot::RwLock;
use std::fmt::Debug;
/// Effects run a certain chunk of code whenever the signals they depend on change.
@@ -23,7 +23,6 @@ use std::fmt::Debug;
/// the server, use [create_isomorphic_effect].
/// ```
/// # use leptos_reactive::*;
/// # use log::*;
/// # create_scope(create_runtime(), |cx| {
/// let (a, set_a) = create_signal(cx, 0);
/// let (b, set_b) = create_signal(cx, 0);
@@ -31,7 +30,7 @@ use std::fmt::Debug;
/// // ✅ use effects to interact between reactive state and the outside world
/// create_effect(cx, move |_| {
/// // immediately prints "Value: 0" and subscribes to `a`
/// log::debug!("Value: {}", a());
/// println!("Value: {}", a());
/// });
///
/// set_a(1);
@@ -80,7 +79,6 @@ where
/// the server as well as the client.
/// ```
/// # use leptos_reactive::*;
/// # use log::*;
/// # create_scope(create_runtime(), |cx| {
/// let (a, set_a) = create_signal(cx, 0);
/// let (b, set_b) = create_signal(cx, 0);
@@ -88,7 +86,7 @@ where
/// // ✅ use effects to interact between reactive state and the outside world
/// create_isomorphic_effect(cx, move |_| {
/// // immediately prints "Value: 0" and subscribes to `a`
/// log::debug!("Value: {}", a());
/// println!("Value: {}", a());
/// });
///
/// set_a(1);
@@ -152,7 +150,7 @@ where
F: Fn(Option<T>) -> T,
{
pub(crate) f: F,
pub(crate) value: RefCell<Option<T>>,
pub(crate) value: RwLock<Option<T>>,
#[cfg(debug_assertions)]
pub(crate) defined_at: &'static std::panic::Location<'static>,
}
@@ -179,22 +177,22 @@ where
)
)
)]
fn run(&self, id: EffectId, runtime: RuntimeId) {
_ = with_runtime(runtime, |runtime| {
fn run(&self, id: EffectId, runtime_id: RuntimeId) {
_ = with_runtime(runtime_id, |runtime| {
// clear previous dependencies
id.cleanup(runtime);
// set this as the current observer
let prev_observer = runtime.observer.take();
runtime.observer.set(Some(id));
let prev_observer = LocalObserver::take(runtime_id);
LocalObserver::set(runtime_id, Some(id));
// run the effect
let value = self.value.take();
let value = std::mem::take(&mut *self.value.write());
let new_value = (self.f)(value);
*self.value.borrow_mut() = Some(new_value);
*self.value.write() = Some(new_value);
// restore the previous observer
runtime.observer.set(prev_observer);
LocalObserver::set(runtime_id, prev_observer);
})
}
}
@@ -203,7 +201,7 @@ impl EffectId {
pub(crate) fn run<T>(&self, runtime_id: RuntimeId) {
_ = with_runtime(runtime_id, |runtime| {
let effect = {
let effects = runtime.effects.borrow();
let effects = runtime.effects.read();
effects.get(*self).cloned()
};
if let Some(effect) = effect {
@@ -226,12 +224,12 @@ impl EffectId {
)
)]
pub(crate) fn cleanup(&self, runtime: &Runtime) {
let sources = runtime.effect_sources.borrow();
let sources = runtime.effect_sources.read();
if let Some(sources) = sources.get(*self) {
let subs = runtime.signal_subscribers.borrow();
for source in sources.borrow().iter() {
let subs = runtime.signal_subscribers.read();
for source in sources.read().iter() {
if let Some(source) = subs.get(*source) {
source.borrow_mut().remove(self);
source.write().remove(self);
}
}
}

View File

@@ -146,7 +146,7 @@ macro_rules! debug_warn {
{
#[cfg(debug_assertions)]
{
log::warn!($($x)*)
tracing::warn!($($x)*)
}
#[cfg(not(debug_assertions))]
{ }

View File

@@ -33,7 +33,7 @@ use std::fmt::Debug;
/// let expensive = move || really_expensive_computation(value()); // lazy: doesn't run until called
/// create_effect(cx, move |_| {
/// // 🆗 run #1: calls `really_expensive_computation` the first time
/// log::debug!("expensive = {}", expensive());
/// println!("expensive = {}", expensive());
/// });
/// create_effect(cx, move |_| {
/// // ❌ run #2: this calls `really_expensive_computation` a second time!
@@ -46,7 +46,7 @@ use std::fmt::Debug;
/// let memoized = create_memo(cx, move |_| really_expensive_computation(value()));
/// create_effect(cx, move |_| {
/// // 🆗 reads the current value of the memo
/// log::debug!("memoized = {}", memoized());
/// println!("memoized = {}", memoized());
/// });
/// create_effect(cx, move |_| {
/// // ✅ reads the current value **without re-running the calculation**
@@ -103,7 +103,7 @@ where
/// let expensive = move || really_expensive_computation(value()); // lazy: doesn't run until called
/// create_effect(cx, move |_| {
/// // 🆗 run #1: calls `really_expensive_computation` the first time
/// log::debug!("expensive = {}", expensive());
/// println!("expensive = {}", expensive());
/// });
/// create_effect(cx, move |_| {
/// // ❌ run #2: this calls `really_expensive_computation` a second time!
@@ -116,7 +116,7 @@ where
/// let memoized = create_memo(cx, move |_| really_expensive_computation(value()));
/// create_effect(cx, move |_| {
/// // 🆗 reads the current value of the memo
/// log::debug!("memoized = {}", memoized());
/// println!("memoized = {}", memoized());
/// });
/// create_effect(cx, move |_| {
/// // ✅ reads the current value **without re-running the calculation**

View File

@@ -6,15 +6,10 @@ use crate::{
spawn::spawn_local,
use_context, Memo, ReadSignal, Scope, ScopeProperty, SuspenseContext, WriteSignal,
};
use parking_lot::RwLock;
use std::{
any::Any,
cell::{Cell, RefCell},
collections::HashSet,
fmt::Debug,
future::Future,
marker::PhantomData,
pin::Pin,
rc::Rc,
any::Any, collections::HashSet, fmt::Debug, future::Future, marker::PhantomData, pin::Pin,
sync::Arc,
};
/// Creates [Resource](crate::Resource), which is a signal that reflects the
@@ -113,10 +108,10 @@ where
let (loading, set_loading) = create_signal(cx, false);
let fetcher = Rc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
let fetcher = Arc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
let source = create_memo(cx, move |_| source());
let r = Rc::new(ResourceState {
let r = Arc::new(ResourceState {
scope: cx,
value,
set_value,
@@ -124,18 +119,18 @@ where
set_loading,
source,
fetcher,
resolved: Rc::new(Cell::new(resolved)),
scheduled: Rc::new(Cell::new(false)),
resolved: Arc::new(RwLock::new(resolved)),
scheduled: Arc::new(RwLock::new(false)),
suspense_contexts: Default::default(),
});
let id = with_runtime(cx.runtime, |runtime| {
runtime.create_serializable_resource(Rc::clone(&r))
runtime.create_serializable_resource(Arc::clone(&r))
})
.expect("tried to create a Resource in a Runtime that has been disposed.");
create_isomorphic_effect(cx, {
let r = Rc::clone(&r);
let r = Arc::clone(&r);
move |_| {
load_resource(cx, id, r.clone());
}
@@ -233,10 +228,10 @@ where
let (loading, set_loading) = create_signal(cx, false);
let fetcher = Rc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
let fetcher = Arc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
let source = create_memo(cx, move |_| source());
let r = Rc::new(ResourceState {
let r = Arc::new(ResourceState {
scope: cx,
value,
set_value,
@@ -244,18 +239,18 @@ where
set_loading,
source,
fetcher,
resolved: Rc::new(Cell::new(resolved)),
scheduled: Rc::new(Cell::new(false)),
resolved: Arc::new(RwLock::new(resolved)),
scheduled: Arc::new(RwLock::new(false)),
suspense_contexts: Default::default(),
});
let id = with_runtime(cx.runtime, |runtime| {
runtime.create_unserializable_resource(Rc::clone(&r))
runtime.create_unserializable_resource(Arc::clone(&r))
})
.expect("tried to create a Resource in a runtime that has been disposed.");
create_effect(cx, {
let r = Rc::clone(&r);
let r = Arc::clone(&r);
// This is a local resource, so we're always going to handle it on the
// client
move |_| r.load(false)
@@ -274,7 +269,7 @@ where
}
#[cfg(not(feature = "hydrate"))]
fn load_resource<S, T>(_cx: Scope, _id: ResourceId, r: Rc<ResourceState<S, T>>)
fn load_resource<S, T>(_cx: Scope, _id: ResourceId, r: Arc<ResourceState<S, T>>)
where
S: PartialEq + Debug + Clone + 'static,
T: 'static,
@@ -283,7 +278,7 @@ where
}
#[cfg(feature = "hydrate")]
fn load_resource<S, T>(cx: Scope, id: ResourceId, r: Rc<ResourceState<S, T>>)
fn load_resource<S, T>(cx: Scope, id: ResourceId, r: Arc<ResourceState<S, T>>)
where
S: PartialEq + Debug + Clone + 'static,
T: Serializable + 'static,
@@ -291,12 +286,12 @@ where
use wasm_bindgen::{JsCast, UnwrapThrowExt};
_ = with_runtime(cx.runtime, |runtime| {
let mut context = runtime.shared_context.borrow_mut();
let mut context = runtime.shared_context.write();
if let Some(data) = context.resolved_resources.remove(&id) {
// The server already sent us the serialized resource value, so
// deserialize & set it now
context.pending_resources.remove(&id); // no longer pending
r.resolved.set(true);
*r.resolved.write() = true;
let res = T::from_json(&data).expect_throw("could not deserialize Resource JSON");
@@ -318,7 +313,7 @@ where
move |res: String| {
let res =
T::from_json(&res).expect_throw("could not deserialize Resource JSON");
resolved.set(true);
*resolved.write() = true;
set_value.update(|n| *n = Some(res));
set_loading.update(|n| *n = false);
}
@@ -551,10 +546,10 @@ where
set_loading: WriteSignal<bool>,
source: Memo<S>,
#[allow(clippy::type_complexity)]
fetcher: Rc<dyn Fn(S) -> Pin<Box<dyn Future<Output = T>>>>,
resolved: Rc<Cell<bool>>,
scheduled: Rc<Cell<bool>>,
suspense_contexts: Rc<RefCell<HashSet<SuspenseContext>>>,
fetcher: Arc<dyn Fn(S) -> Pin<Box<dyn Future<Output = T>>>>,
resolved: Arc<RwLock<bool>>,
scheduled: Arc<RwLock<bool>>,
suspense_contexts: Arc<RwLock<HashSet<SuspenseContext>>>,
}
impl<S, T> ResourceState<S, T>
@@ -583,7 +578,7 @@ where
let increment = move |_: Option<()>| {
if let Some(s) = &suspense_cx {
let mut contexts = suspense_contexts.borrow_mut();
let mut contexts = suspense_contexts.write();
if !contexts.contains(s) {
contexts.insert(*s);
@@ -607,21 +602,22 @@ where
fn load(&self, refetching: bool) {
// doesn't refetch if already refetching
if refetching && self.scheduled.get() {
let mut scheduled = self.scheduled.write();
if refetching && *scheduled {
return;
}
self.scheduled.set(false);
*scheduled = false;
_ = self.source.try_with(|source| {
let fut = (self.fetcher)(source.clone());
// `scheduled` is true for the rest of this code only
self.scheduled.set(true);
*self.scheduled.write() = true;
queue_microtask({
let scheduled = Rc::clone(&self.scheduled);
let scheduled = Arc::clone(&self.scheduled);
move || {
scheduled.set(false);
*scheduled.write() = false;
}
});
@@ -630,7 +626,7 @@ where
// increment counter everywhere it's read
let suspense_contexts = self.suspense_contexts.clone();
for suspense_context in suspense_contexts.borrow().iter() {
for suspense_context in suspense_contexts.read().iter() {
suspense_context.increment();
}
@@ -642,12 +638,12 @@ where
async move {
let res = fut.await;
resolved.set(true);
*resolved.write() = true;
set_value.update(|n| *n = Some(res));
set_loading.update(|n| *n = false);
for suspense_context in suspense_contexts.borrow().iter() {
for suspense_context in suspense_contexts.read().iter() {
suspense_context.decrement();
}
}
@@ -686,8 +682,8 @@ where
}
pub(crate) enum AnyResource {
Unserializable(Rc<dyn UnserializableResource>),
Serializable(Rc<dyn SerializableResource>),
Unserializable(Arc<dyn UnserializableResource + Send + Sync>),
Serializable(Arc<dyn SerializableResource + Send + Sync>),
}
pub(crate) trait SerializableResource {

View File

@@ -6,28 +6,30 @@ use crate::{
};
use cfg_if::cfg_if;
use futures::stream::FuturesUnordered;
use lazy_static::lazy_static;
use parking_lot::RwLock;
use slotmap::{SecondaryMap, SlotMap, SparseSecondaryMap};
use std::{
any::{Any, TypeId},
cell::{Cell, RefCell},
cell::RefCell,
collections::{HashMap, HashSet},
fmt::Debug,
future::Future,
marker::PhantomData,
pin::Pin,
rc::Rc,
sync::Arc,
};
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
thread_local! {
pub(crate) static RUNTIME: Runtime = Runtime::new();
lazy_static! {
pub(crate) static ref RUNTIME: Runtime = Runtime::new();
}
} else {
thread_local! {
pub(crate) static RUNTIMES: RefCell<SlotMap<RuntimeId, Runtime>> = Default::default();
lazy_static! {
pub(crate) static ref RUNTIMES: RwLock<SlotMap<RuntimeId, Runtime>> = Default::default();
}
}
}
@@ -39,15 +41,13 @@ pub(crate) fn with_runtime<T>(id: RuntimeId, f: impl FnOnce(&Runtime) -> T) -> R
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
_ = id;
Ok(RUNTIME.with(|runtime| f(runtime)))
Ok(f(&RUNTIME))
} else {
RUNTIMES.with(|runtimes| {
let runtimes = runtimes.borrow();
match runtimes.get(id) {
None => Err(()),
Some(runtime) => Ok(f(runtime))
}
})
let runtimes = RUNTIMES.read();
match runtimes.get(id) {
None => Err(()),
Some(runtime) => Ok(f(runtime))
}
}
}
}
@@ -60,7 +60,7 @@ pub fn create_runtime() -> RuntimeId {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
Default::default()
} else {
RUNTIMES.with(|runtimes| runtimes.borrow_mut().insert(Runtime::new()))
RUNTIMES.with(|runtimes| runtimes.write().insert(Runtime::new()))
}
}
}
@@ -75,7 +75,7 @@ impl RuntimeId {
pub fn dispose(self) {
cfg_if! {
if #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
let runtime = RUNTIMES.with(move |runtimes| runtimes.borrow_mut().remove(self));
let runtime = RUNTIMES.with(move |runtimes| runtimes.write().remove(self));
drop(runtime);
}
}
@@ -83,7 +83,7 @@ impl RuntimeId {
pub(crate) fn raw_scope_and_disposer(self) -> (Scope, ScopeDisposer) {
with_runtime(self, |runtime| {
let id = { runtime.scopes.borrow_mut().insert(Default::default()) };
let id = { runtime.scopes.write().insert(Default::default()) };
let scope = Scope { runtime: self, id };
let disposer = ScopeDisposer(Box::new(move || scope.dispose()));
(scope, disposer)
@@ -97,9 +97,9 @@ impl RuntimeId {
parent: Option<Scope>,
) -> (T, ScopeId, ScopeDisposer) {
with_runtime(self, |runtime| {
let id = { runtime.scopes.borrow_mut().insert(Default::default()) };
let id = { runtime.scopes.write().insert(Default::default()) };
if let Some(parent) = parent {
runtime.scope_parents.borrow_mut().insert(id, parent.id);
runtime.scope_parents.write().insert(id, parent.id);
}
let scope = Scope { runtime: self, id };
let val = f(scope);
@@ -121,10 +121,7 @@ impl RuntimeId {
T: Any + 'static,
{
let id = with_runtime(self, |runtime| {
runtime
.signals
.borrow_mut()
.insert(Rc::new(RefCell::new(value)))
runtime.signals.write().insert(Arc::new(RwLock::new(value)))
})
.expect("tried to create a signal in a runtime that has been disposed");
(
@@ -150,10 +147,7 @@ impl RuntimeId {
T: Any + 'static,
{
let id = with_runtime(self, |runtime| {
runtime
.signals
.borrow_mut()
.insert(Rc::new(RefCell::new(value)))
runtime.signals.write().insert(Arc::new(RwLock::new(value)))
})
.expect("tried to create a signal in a runtime that has been disposed");
RwSignal {
@@ -176,11 +170,11 @@ impl RuntimeId {
with_runtime(self, |runtime| {
let effect = Effect {
f,
value: RefCell::new(None),
value: RwLock::new(None),
#[cfg(debug_assertions)]
defined_at,
};
let id = { runtime.effects.borrow_mut().insert(Rc::new(effect)) };
let id = { runtime.effects.write().insert(Arc::new(effect)) };
id.run::<T>(self);
id
})
@@ -219,33 +213,63 @@ impl RuntimeId {
#[derive(Default)]
pub(crate) struct Runtime {
pub shared_context: RefCell<SharedContext>,
pub observer: Cell<Option<EffectId>>,
pub scopes: RefCell<SlotMap<ScopeId, RefCell<Vec<ScopeProperty>>>>,
pub scope_parents: RefCell<SparseSecondaryMap<ScopeId, ScopeId>>,
pub scope_children: RefCell<SparseSecondaryMap<ScopeId, Vec<ScopeId>>>,
pub shared_context: RwLock<SharedContext>,
pub scopes: RwLock<SlotMap<ScopeId, RwLock<Vec<ScopeProperty>>>>,
pub scope_parents: RwLock<SparseSecondaryMap<ScopeId, ScopeId>>,
pub scope_children: RwLock<SparseSecondaryMap<ScopeId, Vec<ScopeId>>>,
#[allow(clippy::type_complexity)]
pub scope_contexts: RefCell<SparseSecondaryMap<ScopeId, HashMap<TypeId, Box<dyn Any>>>>,
pub scope_contexts: RwLock<SparseSecondaryMap<ScopeId, HashMap<TypeId, Box<dyn Any>>>>,
#[allow(clippy::type_complexity)]
pub scope_cleanups: RefCell<SparseSecondaryMap<ScopeId, Vec<Box<dyn FnOnce()>>>>,
pub signals: RefCell<SlotMap<SignalId, Rc<RefCell<dyn Any>>>>,
pub signal_subscribers: RefCell<SecondaryMap<SignalId, RefCell<HashSet<EffectId>>>>,
pub effects: RefCell<SlotMap<EffectId, Rc<dyn AnyEffect>>>,
pub effect_sources: RefCell<SecondaryMap<EffectId, RefCell<HashSet<SignalId>>>>,
pub resources: RefCell<SlotMap<ResourceId, AnyResource>>,
pub scope_cleanups: RwLock<SparseSecondaryMap<ScopeId, Vec<Box<dyn FnOnce()>>>>,
pub signals: RwLock<SlotMap<SignalId, Arc<RwLock<dyn Any>>>>,
pub signal_subscribers: RwLock<SecondaryMap<SignalId, RwLock<HashSet<EffectId>>>>,
pub effects: RwLock<SlotMap<EffectId, Arc<dyn AnyEffect>>>,
pub effect_sources: RwLock<SecondaryMap<EffectId, RwLock<HashSet<SignalId>>>>,
pub resources: RwLock<SlotMap<ResourceId, AnyResource>>,
}
// track current observer thread-locally
// because effects run synchronously, the current observer
// *in this thread* will not change during the execution of an effect.
// but if we track this across threads, it's possible for overlapping
// executions to cause the stack to be out of order
// so we store at most one current observer per runtime, per thread
thread_local! {
static OBSERVER: RefCell<HashMap<RuntimeId, EffectId>> = Default::default();
}
pub(crate) struct LocalObserver {}
impl LocalObserver {
pub fn take(runtime: RuntimeId) -> Option<EffectId> {
OBSERVER.with(|observer| observer.borrow_mut().remove(&runtime))
}
pub fn get(runtime: RuntimeId) -> Option<EffectId> {
OBSERVER.with(|observer| observer.borrow().get(&runtime).copied())
}
pub fn set(runtime: RuntimeId, effect: Option<EffectId>) {
OBSERVER.with(|observer| {
if let Some(value) = effect {
observer.borrow_mut().insert(runtime, value)
} else {
observer.borrow_mut().remove(&runtime)
}
});
}
}
impl Debug for Runtime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Runtime")
.field("shared_context", &self.shared_context)
.field("observer", &self.observer)
.field("scopes", &self.scopes)
.field("scope_parents", &self.scope_parents)
.field("scope_children", &self.scope_children)
.field("signals", &self.signals)
.field("signal_subscribers", &self.signal_subscribers)
.field("effects", &self.effects.borrow().len())
.field("effects", &self.effects.read().len())
.field("effect_sources", &self.effect_sources)
.finish()
}
@@ -258,27 +282,27 @@ impl Runtime {
pub(crate) fn create_unserializable_resource<S, T>(
&self,
state: Rc<ResourceState<S, T>>,
state: Arc<ResourceState<S, T>>,
) -> ResourceId
where
S: Clone + 'static,
T: 'static,
{
self.resources
.borrow_mut()
.write()
.insert(AnyResource::Unserializable(state))
}
pub(crate) fn create_serializable_resource<S, T>(
&self,
state: Rc<ResourceState<S, T>>,
state: Arc<ResourceState<S, T>>,
) -> ResourceId
where
S: Clone + 'static,
T: Serializable + 'static,
{
self.resources
.borrow_mut()
.write()
.insert(AnyResource::Serializable(state))
}
@@ -291,7 +315,7 @@ impl Runtime {
S: 'static,
T: 'static,
{
let resources = self.resources.borrow();
let resources = self.resources.read();
let res = resources.get(id);
if let Some(res) = res {
let res_state = match res {
@@ -317,7 +341,7 @@ impl Runtime {
/// Returns IDs for all [Resource]s found on any scope.
pub(crate) fn all_resources(&self) -> Vec<ResourceId> {
self.resources
.borrow()
.read()
.iter()
.map(|(resource_id, _)| resource_id)
.collect()
@@ -326,7 +350,7 @@ impl Runtime {
/// Returns IDs for all [Resource]s found on any scope, pending from the server.
pub(crate) fn pending_resources(&self) -> Vec<ResourceId> {
self.resources
.borrow()
.read()
.iter()
.filter_map(|(resource_id, res)| {
if matches!(res, AnyResource::Serializable(_)) {
@@ -342,7 +366,7 @@ impl Runtime {
&self,
) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
let f = FuturesUnordered::new();
for (id, resource) in self.resources.borrow().iter() {
for (id, resource) in self.resources.read().iter() {
if let AnyResource::Serializable(resource) = resource {
f.push(resource.to_serialization_resolver(id));
}

View File

@@ -1,6 +1,6 @@
#![forbid(unsafe_code)]
use crate::{
runtime::{with_runtime, RuntimeId},
runtime::{with_runtime, LocalObserver, RuntimeId},
EffectId, PinnedFuture, ResourceId, SignalId, SuspenseContext,
};
use futures::stream::FuturesUnordered;
@@ -108,7 +108,7 @@ impl Scope {
pub fn run_child_scope<T>(self, f: impl FnOnce(Scope) -> T) -> (T, ScopeDisposer) {
let (res, child_id, disposer) = self.runtime.run_scope_undisposed(f, Some(self));
_ = with_runtime(self.runtime, |runtime| {
let mut children = runtime.scope_children.borrow_mut();
let mut children = runtime.scope_children.write();
children
.entry(self.id)
.expect("trying to add a child to a Scope that has already been disposed")
@@ -145,9 +145,9 @@ impl Scope {
/// ```
pub fn untrack<T>(&self, f: impl FnOnce() -> T) -> T {
with_runtime(self.runtime, |runtime| {
let prev_observer = runtime.observer.take();
let prev_observer = LocalObserver::take(self.runtime);
let untracked_result = f();
runtime.observer.set(prev_observer);
LocalObserver::set(self.runtime, prev_observer);
untracked_result
})
.expect("tried to run untracked function in a runtime that has been disposed")
@@ -167,7 +167,7 @@ impl Scope {
_ = with_runtime(self.runtime, |runtime| {
// dispose of all child scopes
let children = {
let mut children = runtime.scope_children.borrow_mut();
let mut children = runtime.scope_children.write();
children.remove(self.id)
};
@@ -182,7 +182,7 @@ impl Scope {
}
// run cleanups
if let Some(cleanups) = runtime.scope_cleanups.borrow_mut().remove(self.id) {
if let Some(cleanups) = runtime.scope_cleanups.write().remove(self.id) {
for cleanup in cleanups {
cleanup();
}
@@ -190,34 +190,34 @@ impl Scope {
// remove everything we own and run cleanups
let owned = {
let owned = runtime.scopes.borrow_mut().remove(self.id);
owned.map(|owned| owned.take())
let owned = runtime.scopes.write().remove(self.id);
owned.map(|owned| std::mem::take(&mut *owned.write()))
};
if let Some(owned) = owned {
for property in owned {
match property {
ScopeProperty::Signal(id) => {
// remove the signal
runtime.signals.borrow_mut().remove(id);
let subs = runtime.signal_subscribers.borrow_mut().remove(id);
runtime.signals.write().remove(id);
let subs = runtime.signal_subscribers.write().remove(id);
// each of the subs needs to remove the signal from its dependencies
// so that it doesn't try to read the (now disposed) signal
if let Some(subs) = subs {
let source_map = runtime.effect_sources.borrow();
for effect in subs.borrow().iter() {
let source_map = runtime.effect_sources.read();
for effect in subs.read().iter() {
if let Some(effect_sources) = source_map.get(*effect) {
effect_sources.borrow_mut().remove(&id);
effect_sources.write().remove(&id);
}
}
}
}
ScopeProperty::Effect(id) => {
runtime.effects.borrow_mut().remove(id);
runtime.effect_sources.borrow_mut().remove(id);
runtime.effects.write().remove(id);
runtime.effect_sources.write().remove(id);
}
ScopeProperty::Resource(id) => {
runtime.resources.borrow_mut().remove(id);
runtime.resources.write().remove(id);
}
}
}
@@ -227,18 +227,18 @@ impl Scope {
pub(crate) fn with_scope_property(&self, f: impl FnOnce(&mut Vec<ScopeProperty>)) {
_ = with_runtime(self.runtime, |runtime| {
let scopes = runtime.scopes.borrow();
let scopes = runtime.scopes.read();
let scope = scopes
.get(self.id)
.expect("tried to add property to a scope that has been disposed");
f(&mut scope.borrow_mut());
f(&mut scope.write());
})
}
/// Returns the the parent Scope, if any.
pub fn parent(&self) -> Option<Scope> {
with_runtime(self.runtime, |runtime| {
runtime.scope_parents.borrow().get(self.id).copied()
runtime.scope_parents.read().get(self.id).copied()
})
.ok()
.flatten()
@@ -255,7 +255,7 @@ impl Scope {
/// are invalidated.
pub fn on_cleanup(cx: Scope, cleanup_fn: impl FnOnce() + 'static) {
_ = with_runtime(cx.runtime, |runtime| {
let mut cleanups = runtime.scope_cleanups.borrow_mut();
let mut cleanups = runtime.scope_cleanups.write();
let cleanups = cleanups
.entry(cx.id)
.expect("trying to clean up a Scope that has already been disposed")
@@ -327,7 +327,7 @@ impl Scope {
use futures::StreamExt;
_ = with_runtime(self.runtime, |runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
let mut shared_context = runtime.shared_context.write();
let (tx, mut rx) = futures::channel::mpsc::unbounded();
create_isomorphic_effect(*self, move |_| {
@@ -355,7 +355,7 @@ impl Scope {
/// `<Suspense/>` HTML when all resources are resolved.
pub fn pending_fragments(&self) -> HashMap<String, (String, PinnedFuture<String>)> {
with_runtime(self.runtime, |runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
let mut shared_context = runtime.shared_context.write();
std::mem::take(&mut shared_context.pending_fragments)
})
.unwrap_or_default()

View File

@@ -1,7 +1,7 @@
#![forbid(unsafe_code)]
use crate::{
debug_warn,
runtime::{with_runtime, RuntimeId},
runtime::{with_runtime, LocalObserver, RuntimeId},
Runtime, Scope, ScopeProperty, UntrackedGettableSignal, UntrackedSettableSignal,
};
use cfg_if::cfg_if;
@@ -246,7 +246,9 @@ where
#[cfg(feature = "hydrate")]
pub(crate) fn subscribe(&self) {
_ = with_runtime(self.runtime, |runtime| self.id.subscribe(runtime))
_ = with_runtime(self.runtime, |runtime| {
self.id.subscribe(self.runtime, runtime)
})
}
/// Clones and returns the current value of the signal, and subscribes
@@ -286,7 +288,9 @@ where
/// Applies the function to the current Signal, if it exists, and subscribes
/// the running effect.
pub(crate) fn try_with<U>(&self, f: impl FnOnce(&T) -> U) -> Result<U, SignalError> {
match with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f)) {
match with_runtime(self.runtime, |runtime| {
self.id.try_with(self.runtime, runtime, f)
}) {
Ok(Ok(v)) => Ok(v),
Ok(Err(e)) => Err(e),
Err(_) => Err(SignalError::RuntimeDisposed),
@@ -1169,20 +1173,20 @@ pub(crate) enum SignalError {
}
impl SignalId {
pub(crate) fn subscribe(&self, runtime: &Runtime) {
pub(crate) fn subscribe(&self, runtime_id: RuntimeId, runtime: &Runtime) {
// add subscriber
if let Some(observer) = runtime.observer.get() {
if let Some(observer) = LocalObserver::get(runtime_id) {
// add this observer to the signal's dependencies (to allow notification)
let mut subs = runtime.signal_subscribers.borrow_mut();
let mut subs = runtime.signal_subscribers.write();
if let Some(subs) = subs.entry(*self) {
subs.or_default().borrow_mut().insert(observer);
subs.or_default().write().insert(observer);
}
// add this signal to the effect's sources (to allow cleanup)
let mut effect_sources = runtime.effect_sources.borrow_mut();
let mut effect_sources = runtime.effect_sources.write();
if let Some(effect_sources) = effect_sources.entry(observer) {
let sources = effect_sources.or_default();
sources.borrow_mut().insert(*self);
sources.write().insert(*self);
}
}
}
@@ -1197,7 +1201,7 @@ impl SignalId {
{
// get the value
let value = {
let signals = runtime.signals.borrow();
let signals = runtime.signals.read();
match signals.get(*self).cloned().ok_or(SignalError::Disposed) {
Ok(s) => Ok(s),
Err(e) => {
@@ -1206,12 +1210,13 @@ impl SignalId {
}
}
}?;
let value = value.try_borrow().unwrap_or_else(|e| {
debug_warn!(
"Signal::try_with_no_subscription failed on Signal<{}>. It seems you're trying to read the value of a signal within an effect caused by updating the signal.",
let value = value.try_read().unwrap_or_else(|| {
panic!(
"Signal::try_with_no_subscription failed on Signal<{}>. \
It seems you're trying to read the value of a signal within an effect \
caused by updating the signal.",
std::any::type_name::<T>()
);
panic!("{e}");
});
let value = value
.downcast_ref::<T>()
@@ -1221,13 +1226,14 @@ impl SignalId {
pub(crate) fn try_with<T, U>(
&self,
runtime_id: RuntimeId,
runtime: &Runtime,
f: impl FnOnce(&T) -> U,
) -> Result<U, SignalError>
where
T: 'static,
{
self.subscribe(runtime);
self.subscribe(runtime_id, runtime);
self.try_with_no_subscription(runtime, f)
}
@@ -1246,12 +1252,14 @@ impl SignalId {
.expect("tried to access a signal in a runtime that has been disposed")
}
pub(crate) fn with<T, U>(&self, runtime: RuntimeId, f: impl FnOnce(&T) -> U) -> U
pub(crate) fn with<T, U>(&self, runtime_id: RuntimeId, f: impl FnOnce(&T) -> U) -> U
where
T: 'static,
{
with_runtime(runtime, |runtime| self.try_with(runtime, f).unwrap())
.expect("tried to access a signal in a runtime that has been disposed")
with_runtime(runtime_id, |runtime| {
self.try_with(runtime_id, runtime, f).unwrap()
})
.expect("tried to access a signal in a runtime that has been disposed")
}
fn update_value<T, U>(&self, runtime: RuntimeId, f: impl FnOnce(&mut T) -> U) -> Option<U>
@@ -1260,11 +1268,11 @@ impl SignalId {
{
with_runtime(runtime, |runtime| {
let value = {
let signals = runtime.signals.borrow();
let signals = runtime.signals.read();
signals.get(*self).cloned()
};
if let Some(value) = value {
let mut value = value.borrow_mut();
let mut value = value.write();
if let Some(value) = value.downcast_mut::<T>() {
Some(f(value))
} else {
@@ -1300,14 +1308,14 @@ impl SignalId {
// notify subscribers
if updated.is_some() {
let subs = {
let subs = runtime.signal_subscribers.borrow();
let subs = runtime.signal_subscribers.read();
let subs = subs.get(*self);
subs.map(|subs| subs.borrow().clone())
subs.map(|subs| subs.read().clone())
};
if let Some(subs) = subs {
for sub in subs {
let effect = {
let effects = runtime.effects.borrow();
let effects = runtime.effects.read();
effects.get(sub).cloned()
};
if let Some(effect) = effect {

View File

@@ -89,30 +89,6 @@ where
self.0.update_untracked(f);
}
/// Applies a function to the current value to mutate it in place.
/// Forwards the return value of the closure if the closure was called.
/// ```
/// use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
///
/// pub struct MyUncloneableData {
/// pub value: String
/// }
///
/// let data = store_value(cx, MyUncloneableData { value: "a".into() });
/// let updated = data.update_returning(|data| {
/// data.value = "b".into();
/// data.value.clone()
/// });
///
/// assert_eq!(data.with(|data| data.value.clone()), "b");
/// assert_eq!(updated, Some(String::from("b")));
/// });
/// ```
pub fn update_returning<U>(&self, f: impl FnOnce(&mut T) -> U) -> Option<U> {
self.0.update_returning_untracked(f)
}
/// Sets the stored value.
/// ```
/// # use leptos_reactive::*;

View File

@@ -19,7 +19,7 @@ fn untracked_set_doesnt_trigger_effect() {
create_isomorphic_effect(cx, {
let b = b.clone();
move |_| {
let formatted = format!("Value is {}", a());
let formatted = format!("Value is {}", a.get());
*b.borrow_mut() = formatted;
}
});
@@ -52,7 +52,7 @@ fn untracked_get_doesnt_trigger_effect() {
create_isomorphic_effect(cx, {
let b = b.clone();
move |_| {
let formatted = format!("Values are {} and {}", a(), a2.get_untracked());
let formatted = format!("Values are {} and {}", a.get(), a2.get_untracked());
*b.borrow_mut() = formatted;
}
});

View File

@@ -96,11 +96,6 @@ where
self.0.with(|a| a.pending.read_only())
}
/// Updates whether the action is currently pending.
pub fn set_pending(&self, pending: bool) {
self.0.with(|a| a.pending.set(pending))
}
/// The URL associated with the action (typically as part of a server function.)
/// This enables integration with the `ActionForm` component in `leptos_router`.
pub fn url(&self) -> Option<String> {

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.1.3"
version = "0.1.1"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -11,7 +11,7 @@ description = "Tools to set HTML metadata in the Leptos web framework."
cfg-if = "1"
leptos = { workspace = true }
tracing = "0.1"
typed-builder = "0.12"
typed-builder = "0.11"
[dependencies.web-sys]
version = "0.3"

View File

@@ -1,74 +0,0 @@
use crate::TextProp;
use cfg_if::cfg_if;
use leptos::*;
use std::{cell::RefCell, rc::Rc};
/// Contains the current metadata for the document's `<body>`.
#[derive(Clone, Default)]
pub struct BodyContext {
class: Rc<RefCell<Option<TextProp>>>,
}
impl BodyContext {
/// Converts the `<body>` metadata into an HTML string.
pub fn as_string(&self) -> Option<String> {
self.class
.borrow()
.as_ref()
.map(|class| format!(" class=\"{}\"", class.get()))
}
}
impl std::fmt::Debug for BodyContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("TitleContext").finish()
}
}
/// A component to set metadata on the documents `<body>` element from
/// within the application.
///
/// ```
/// use leptos::*;
/// use leptos_meta::*;
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
/// let (prefers_dark, set_prefers_dark) = create_signal(cx, false);
/// let body_class = move || if prefers_dark() {
/// "dark".to_string()
/// } else {
/// "light".to_string()
/// };
///
/// view! { cx,
/// <main>
/// <Body class=body_class/>
/// </main>
/// }
/// }
/// ```
#[component(transparent)]
pub fn Body(
cx: Scope,
/// The `class` attribute on the `<body>`.
#[prop(optional, into)]
class: Option<TextProp>,
) -> impl IntoView {
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
let el = document().body().expect("there to be a <body> element");
if let Some(class) = class {
create_render_effect(cx, move |_| {
let value = class.get();
_ = el.set_attribute("class", &value);
});
}
} else {
let meta = crate::use_head(cx);
*meta.body.class.borrow_mut() = class;
}
}
}

View File

@@ -1,85 +0,0 @@
use crate::TextProp;
use cfg_if::cfg_if;
use leptos::*;
use std::{cell::RefCell, rc::Rc};
/// Contains the current metadata for the document's `<html>`.
#[derive(Clone, Default)]
pub struct HtmlContext {
lang: Rc<RefCell<Option<TextProp>>>,
dir: Rc<RefCell<Option<TextProp>>>,
}
impl HtmlContext {
/// Converts the `<html>` metadata into an HTML string.
pub fn as_string(&self) -> Option<String> {
match (self.lang.borrow().as_ref(), self.dir.borrow().as_ref()) {
(None, None) => None,
(Some(lang), None) => Some(format!(" lang=\"{}\"", lang.get())),
(None, Some(dir)) => Some(format!(" dir=\"{}\"", dir.get())),
(Some(lang), Some(dir)) => {
Some(format!(" lang=\"{}\" dir=\"{}\"", lang.get(), dir.get()))
}
}
}
}
impl std::fmt::Debug for HtmlContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("TitleContext").finish()
}
}
/// A component to set metadata on the documents `<html>` element from
/// within the application.
///
/// ```
/// use leptos::*;
/// use leptos_meta::*;
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
///
/// view! { cx,
/// <main>
/// <Html lang="he" dir="rtl"/>
/// </main>
/// }
/// }
/// ```
#[component(transparent)]
pub fn Html(
cx: Scope,
/// The `lang` attribute on the `<html>`.
#[prop(optional, into)]
lang: Option<TextProp>,
/// The `dir` attribute on the `<html>`.
#[prop(optional, into)]
dir: Option<TextProp>,
) -> impl IntoView {
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
let el = document().document_element().expect("there to be a <html> element");
if let Some(lang) = lang {
let el = el.clone();
create_render_effect(cx, move |_| {
let value = lang.get();
_ = el.set_attribute("lang", &value);
});
}
if let Some(dir) = dir {
create_render_effect(cx, move |_| {
let value = dir.get();
_ = el.set_attribute("dir", &value);
});
}
} else {
let meta = crate::use_head(cx);
*meta.html.lang.borrow_mut() = lang;
*meta.html.dir.borrow_mut() = dir;
}
}
}

View File

@@ -55,16 +55,12 @@ use std::{
use leptos::{leptos_dom::debug_warn, *};
mod body;
mod html;
mod link;
mod meta_tags;
mod script;
mod style;
mod stylesheet;
mod title;
pub use body::*;
pub use html::*;
pub use link::*;
pub use meta_tags::*;
pub use script::*;
@@ -78,19 +74,13 @@ pub use title::*;
/// [provide_meta_context].
#[derive(Clone, Default, Debug)]
pub struct MetaContext {
/// Metadata associated with the `<html>` element
pub html: HtmlContext,
/// Metadata associated with the `<title>` element.
pub title: TitleContext,
/// Metadata associated with the `<body>` element
pub body: BodyContext,
/// Other metadata tags.
pub tags: MetaTagsContext,
pub(crate) title: TitleContext,
pub(crate) tags: MetaTagsContext,
}
/// Manages all of the element created by components.
#[derive(Clone, Default)]
pub struct MetaTagsContext {
pub(crate) struct MetaTagsContext {
next_id: Rc<Cell<MetaTagId>>,
#[allow(clippy::type_complexity)]
els: Rc<RefCell<HashMap<String, (HtmlElement<AnyElement>, Scope, Option<web_sys::Element>)>>>,
@@ -103,8 +93,7 @@ impl std::fmt::Debug for MetaTagsContext {
}
impl MetaTagsContext {
/// Converts metadata tags into an HTML string.
#[cfg(any(feature = "ssr", docs))]
#[cfg(feature = "ssr")]
pub fn as_string(&self) -> String {
self.els
.borrow()
@@ -113,7 +102,6 @@ impl MetaTagsContext {
.collect()
}
#[doc(hidden)]
pub fn register(&self, cx: Scope, id: String, builder_el: HtmlElement<AnyElement>) {
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
@@ -221,7 +209,7 @@ impl MetaContext {
///
/// # #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
/// run_scope(create_runtime(), |cx| {
/// provide_meta_context(cx);
/// provide_context(cx, MetaContext::new());
///
/// let app = view! { cx,
/// <main>

View File

@@ -55,7 +55,7 @@ where
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// provide_meta_context(cx);
/// provide_context(cx, MetaContext::new());
/// let formatter = |text| format!("{text} — Leptos Online");
///
/// view! { cx,

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.1.3"
version = "0.1.1"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -38,7 +38,7 @@ pub fn Form<A>(
#[allow(clippy::type_complexity)]
on_response: Option<Rc<dyn Fn(&web_sys::Response)>>,
/// Component children; should include the HTML of the form elements.
children: Children,
children: Box<dyn FnOnce(Scope) -> Fragment>,
) -> impl IntoView
where
A: ToHref + 'static,
@@ -136,7 +136,7 @@ pub fn ActionForm<I, O>(
/// manually using [leptos_server::Action::using_server_fn].
action: Action<I, Result<O, ServerFnError>>,
/// Component children; should include the HTML of the form elements.
children: Children,
children: Box<dyn FnOnce(Scope) -> Fragment>,
) -> impl IntoView
where
I: Clone + ServerFn + 'static,
@@ -155,10 +155,7 @@ where
let on_form_data = Rc::new(move |form_data: &web_sys::FormData| {
let data = action_input_from_form_data(form_data);
match data {
Ok(data) => {
input.set(Some(data));
action.set_pending(true);
}
Ok(data) => input.set(Some(data)),
Err(e) => log::error!("{e}"),
}
});
@@ -170,6 +167,11 @@ where
JsFuture::from(resp.text().expect("couldn't get .text() from Response")).await;
match body {
Ok(json) => {
log::debug!(
"body is {:?}\nO is {:?}",
json.as_string().unwrap(),
std::any::type_name::<O>()
);
match O::from_json(
&json.as_string().expect("couldn't get String from JsString"),
) {
@@ -180,9 +182,7 @@ where
}
}
Err(e) => log::error!("{e:?}"),
};
input.set(None);
action.set_pending(false);
}
});
});
@@ -210,7 +210,7 @@ pub fn MultiActionForm<I, O>(
/// manually using [leptos_server::Action::using_server_fn].
action: MultiAction<I, Result<O, ServerFnError>>,
/// Component children; should include the HTML of the form elements.
children: Children,
children: Box<dyn FnOnce(Scope) -> Fragment>,
) -> impl IntoView
where
I: Clone + ServerFn + 'static,

View File

@@ -65,7 +65,7 @@ pub fn A<H>(
#[prop(optional, into)]
class: Option<MaybeSignal<String>>,
/// The nodes or elements to be shown inside the link.
children: Children,
children: Box<dyn FnOnce(Scope) -> Fragment>,
) -> impl IntoView
where
H: ToHref + 'static,

View File

@@ -29,7 +29,7 @@ pub fn Route<E, F, P>(
view: F,
/// `children` may be empty or include nested routes.
#[prop(optional)]
children: Option<Children>,
children: Option<Box<dyn FnOnce(Scope) -> Fragment>>,
) -> impl IntoView
where
E: IntoView,

View File

@@ -32,7 +32,7 @@ pub fn Router(
/// The `<Router/>` should usually wrap your whole page. It can contain
/// any elements, and should include a [Routes](crate::Routes) component somewhere
/// to define and display [Route](crate::Route)s.
children: Children,
children: Box<dyn FnOnce(Scope) -> Fragment>,
) -> impl IntoView {
// create a new RouterContext and provide it to every component beneath the router
let router = RouterContext::new(cx, base, fallback);

View File

@@ -22,7 +22,7 @@ use crate::{
pub fn Routes(
cx: Scope,
#[prop(optional)] base: Option<String>,
children: Children,
children: Box<dyn FnOnce(Scope) -> Fragment>,
) -> impl IntoView {
let router = use_context::<RouterContext>(cx)
.expect("<Routes/> component should be nested within a <Router/>.");

View File

@@ -202,7 +202,6 @@ mod history;
mod hooks;
#[doc(hidden)]
pub mod matching;
pub use matching::RouteDefinition;
pub use components::*;
#[cfg(any(feature = "ssr", doc))]

View File

@@ -3,18 +3,11 @@ 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>,
}