Compare commits

..

5 Commits

Author SHA1 Message Date
Greg Johnston
52c5a88e53 Construct all fragments lazily (closes #299 and #421) 2023-01-31 23:34:33 -05:00
Greg Johnston
25dfd88978 Add html-escape dependency 2023-01-31 23:26:31 -05:00
Greg Johnston
066ad56dbd Fix HTML entities 2023-01-31 22:31:15 -05:00
Greg Johnston
decd5fbfdb clippy 2023-01-31 22:28:01 -05:00
Greg Johnston
fdb6425504 Correctly escape special HTML characters in static strings in view macro 2023-01-31 22:07:17 -05:00
44 changed files with 332 additions and 455 deletions

View File

@@ -8,7 +8,7 @@
default_to_workspace = false
[tasks.ci]
dependencies = ["build", "check-examples", "test"]
dependencies = ["build", "build-examples", "test"]
[tasks.build]
clear = true
@@ -19,24 +19,22 @@ command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check-examples]
[tasks.build-examples]
clear = true
dependencies = [
{ name = "check", path = "examples/counter" },
{ name = "check", path = "examples/counter_isomorphic" },
{ name = "check", path = "examples/counter_without_macros" },
{ name = "check", path = "examples/counters" },
{ name = "check", path = "examples/counters_stable" },
{ name = "check", path = "examples/errors_axum" },
{ name = "check", path = "examples/fetch" },
{ name = "check", path = "examples/hackernews" },
{ name = "check", path = "examples/hackernews_axum" },
{ name = "check", path = "examples/parent_child" },
{ name = "check", path = "examples/router" },
{ name = "check", path = "examples/tailwind" },
{ name = "check", path = "examples/todo_app_sqlite" },
{ name = "check", path = "examples/todo_app_sqlite_axum" },
{ name = "check", path = "examples/todomvc" },
{ name = "build", path = "examples/counter" },
{ name = "build", path = "examples/counter_isomorphic" },
{ name = "build", path = "examples/counters" },
{ name = "build", path = "examples/counters_stable" },
{ name = "build", path = "examples/fetch" },
{ name = "build", path = "examples/hackernews" },
{ name = "build", path = "examples/hackernews_axum" },
{ name = "build", path = "examples/parent_child" },
{ name = "build", path = "examples/router" },
{ name = "build", path = "examples/tailwind" },
{ name = "build", path = "examples/todo_app_sqlite" },
{ name = "build", path = "examples/todo_app_sqlite_axum" },
{ name = "build", path = "examples/todomvc" },
]
[tasks.test]

View File

@@ -2,8 +2,3 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -2,8 +2,3 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

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

View File

@@ -2,8 +2,3 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -2,8 +2,3 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -1,8 +1,9 @@
# Leptos Errors Demonstration with Axum
This example demonstrates how Leptos Errors can work with an Axum backend on a server.
# 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 status codes.
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)

View File

@@ -1,13 +1,14 @@
use crate::errors::AppError;
use cfg_if::cfg_if;
use leptos::Errors;
use leptos::*;
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.
// 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,
@@ -34,29 +35,32 @@ pub fn ErrorTemplate(
// Only the response code for the first error is actually sent from the server
// this may be customized by the specific application
cfg_if! { if #[cfg(feature="ssr")] {
cfg_if! {
if #[cfg(feature="ssr")]{
let response = use_context::<ResponseOptions>(cx);
if let Some(response) = response {
response.set_status(errors[0].status_code());
if let Some(response) = response{
response.set_status(errors[0].status_code());
}
}}
}
}
view! { cx,
<h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each= move || {errors.clone().into_iter().enumerate()}
// a unique key for each item as a reference
key=|(index, _error)| *index
// renders each item to a view
view= move |error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! { cx,
<h2>{error_code.to_string()}</h2>
<p>"Error: " {error_string}</p>
}
}
/>
view! {cx,
<h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each= move || {errors.clone().into_iter().enumerate()}
// a unique key for each item as a reference
key=|(index, _error)| *index
// renders each item to a view
view= move |error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! {
cx,
<h2>{error_code.to_string()}</h2>
<p>"Error: " {error_string}</p>
}
}
/>
}
}

View File

@@ -1,6 +1,7 @@
use cfg_if::cfg_if;
cfg_if! { if #[cfg(feature = "ssr")] {
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::Extension,
@@ -42,4 +43,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
)),
}
}
}}
}
}

View File

@@ -2,14 +2,17 @@ use crate::{
error_template::{ErrorTemplate, ErrorTemplateProps},
errors::AppError,
};
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[cfg(feature = "ssr")]
pub fn register_server_functions() {
_ = CauseInternalServerError::register();
}
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> {
@@ -21,6 +24,11 @@ pub async fn cause_internal_server_error() -> Result<(), ServerFnError> {
))
}
#[server(CauseNotFoundError, "/api")]
pub async fn cause_not_found_error() -> Result<(), ServerFnError> {
Err(ServerFnError::ServerError("Not Found".to_string()))
}
#[component]
pub fn App(cx: Scope) -> impl IntoView {
//let id = use_context::<String>(cx);
@@ -37,7 +45,9 @@ pub fn App(cx: Scope) -> impl IntoView {
<Routes>
<Route path="" view=|cx| view! {
cx,
<ExampleErrors/>
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
<ExampleErrors/>
</ErrorBoundary>
}/>
</Routes>
</main>
@@ -47,29 +57,20 @@ pub fn App(cx: Scope) -> impl IntoView {
#[component]
pub fn ExampleErrors(cx: Scope) -> impl IntoView {
let generate_internal_error = create_server_action::<CauseInternalServerError>(cx);
view! { cx,
<p>
"These links will load 404 pages since they do not exist. Verify with browser development tools: " <br/>
<a href="/404">"This links to a page that does not exist"</a><br/>
<a href="/404" target="_blank">"Same link, but in a new tab"</a>
</p>
<p>
"After pressing this button check browser network tools. Can be used even when WASM is blocked:"
<ActionForm action=generate_internal_error>
<input name="error1" type="submit" value="Generate Internal Server Error"/>
</ActionForm>
</p>
<p>"The following <div> will always contain an error and cause this page to produce status 500. Check browser dev tools. "</p>
<div>
// note that the error boundries could be placed above in the Router or lower down
// in a particular route. The generated errors on the entire page contribue to the
// final status code sent by the server when producing ssr pages.
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
<ReturnsError/>
</ErrorBoundary>
</div>
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>
}
}

View File

@@ -1,72 +1,74 @@
use cfg_if::cfg_if;
cfg_if! { if #[cfg(feature = "ssr")] {
use crate::fallback::file_and_error_handler;
use crate::landing::*;
use axum::body::Body as AxumBody;
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},
routing::{get, post},
Router,
};
use axum::body::Body as AxumBody;
use crate::landing::*;
use errors_axum::*;
use leptos::*;
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
#[cfg(feature = "ssr")]
async fn custom_handler(
Path(id): Path<String>,
Extension(options): Extension<Arc<LeptosOptions>>,
req: Request<AxumBody>,
) -> Response {
let handler = leptos_axum::render_app_to_stream_with_context(
(*options).clone(),
move |cx| {
provide_context(cx, id.clone());
},
|cx| view! { cx, <App/> },
);
handler(req).await.into_response()
}
//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()
}
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
crate::landing::register_server_functions();
crate::landing::register_server_functions();
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_address;
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_address;
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
// build our application with a route
let app = Router::new()
// build our application with a route
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.route("/special/:id", get(custom_handler))
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> })
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> } )
.fallback(file_and_error_handler)
.layer(Extension(Arc::new(leptos_options)));
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on http://{}", &addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on http://{}", &addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
}
// this is if we were using client-only rending with Trunk
#[cfg(not(feature = "ssr"))]
pub fn main() {
// This example cannot be built as a trunk standalone CSR-only app.
// The server is needed to demonstrate the error statuses.
// 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

@@ -2,8 +2,3 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -2,8 +2,3 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -2,8 +2,3 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -2,8 +2,3 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -7,8 +7,7 @@ use web_sys::MouseEvent;
// 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
// the child component to call
// 3) <ButtonC/>: adding an `on:` event listener to a component
// 4) <ButtonD/>: providing a context that is used in the component (rather than prop drilling)
// 4) <ButtonC/>: providing a context that is used in the component (rather than prop drilling)
#[derive(Copy, Clone)]
struct SmallcapsContext(WriteSignal<bool>);
@@ -18,7 +17,6 @@ pub fn App(cx: Scope) -> impl IntoView {
// just some signals to toggle three classes on our <p>
let (red, set_red) = create_signal(cx, false);
let (right, set_right) = create_signal(cx, false);
let (italics, set_italics) = create_signal(cx, false);
let (smallcaps, set_smallcaps) = create_signal(cx, false);
// the newtype pattern isn't *necessary* here but is a good practice
@@ -33,7 +31,6 @@ pub fn App(cx: Scope) -> impl IntoView {
// class: attributes take F: Fn() => bool, and these signals all implement Fn()
class:red=red
class:right=right
class:italics=italics
class:smallcaps=smallcaps
>
"Lorem ipsum sit dolor amet."
@@ -45,13 +42,8 @@ pub fn App(cx: Scope) -> impl IntoView {
// Button B: pass a closure
<ButtonB on_click=move |_| set_right.update(|value| *value = !*value)/>
// Button B: use a regular event listener
// setting an event listener on a component like this applies it
// to each of the top-level elements the component returns
<ButtonC on:click=move |_| set_italics.update(|value| *value = !*value)/>
// Button D gets its setter from context rather than props
<ButtonD/>
<ButtonC/>
</main>
}
}
@@ -61,7 +53,7 @@ pub fn App(cx: Scope) -> impl IntoView {
pub fn ButtonA(
cx: Scope,
/// Signal that will be toggled when the button is clicked.
setter: WriteSignal<bool>,
setter: WriteSignal<bool>
) -> impl IntoView {
view! {
cx,
@@ -78,7 +70,7 @@ pub fn ButtonA(
pub fn ButtonB<F>(
cx: Scope,
/// Callback that will be invoked when the button is clicked.
on_click: F,
on_click: F
) -> impl IntoView
where
F: Fn(MouseEvent) + 'static,
@@ -105,22 +97,10 @@ where
// if Rust ever had named function arguments we could drop this requirement
}
/// Button C is a dummy: it renders a button but doesn't handle
/// its click. Instead, the parent component adds an event listener.
#[component]
pub fn ButtonC(cx: Scope) -> impl IntoView {
view! {
cx,
<button>
"Toggle Italics"
</button>
}
}
/// Button D is very similar to Button A, but instead of passing the setter as a prop
/// we get it from the context
#[component]
pub fn ButtonD(cx: Scope) -> impl IntoView {
pub fn ButtonC(cx: Scope) -> impl IntoView {
let setter = use_context::<SmallcapsContext>(cx).unwrap().0;
view! {
@@ -132,7 +112,3 @@ pub fn ButtonD(cx: Scope) -> impl IntoView {
</button>
}
}
fn main() {
leptos::mount_to_body(|cx| view! { cx, <App/> })
}

View File

@@ -2,8 +2,3 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -2,8 +2,3 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -2,8 +2,3 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -12,13 +12,13 @@ console_log = "0.2.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
cfg-if = "1.0.0"
leptos = { path = "../../leptos", default-features = false, features = [
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
"serde",
] }
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
leptos_reactive = { path = "../../leptos_reactive", default-features = false }
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"] }
@@ -55,27 +55,27 @@ denylist = [
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "todo_app_sqlite_axum"
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "todo_app_sqlite_axum"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end-cmd = "npx playwright test"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
browserquery = "defaults"
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
watch = false
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target

View File

@@ -2,8 +2,3 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -1,8 +1,9 @@
use crate::errors::TodoAppError;
use cfg_if::cfg_if;
use leptos::Errors;
use leptos::*;
use leptos::{
component, create_rw_signal, use_context, view, For, ForProps, IntoView, RwSignal, Scope,
};
#[cfg(feature = "ssr")]
use leptos_axum::ResponseOptions;
@@ -15,7 +16,10 @@ pub fn ErrorTemplate(
#[prop(optional)] errors: Option<RwSignal<Errors>>,
) -> impl IntoView {
let errors = match outside_errors {
Some(e) => create_rw_signal(cx, e),
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!"),
@@ -28,7 +32,8 @@ pub fn ErrorTemplate(
// Downcast lets us take a type that implements `std::error::Error`
let errors: Vec<TodoAppError> = errors
.into_iter()
.filter_map(|(_k, v)| v.downcast_ref::<TodoAppError>().cloned())
.map(|(_k, v)| v.downcast_ref::<TodoAppError>().cloned())
.flatten()
.collect();
println!("Errors: {errors:#?}");
@@ -49,7 +54,7 @@ pub fn ErrorTemplate(
// a function that returns the items we're iterating over; a signal is fine
each= move || {errors.clone().into_iter().enumerate()}
// a unique key for each item as a reference
key=|(index, _error)| *index
key=|(index, _error)| index.clone()
// renders each item to a view
view= move |error| {
let error_string = error.1.to_string();

View File

@@ -33,13 +33,14 @@ if #[cfg(feature = "ssr")] {
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
let root_path = format!("{root}");
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
match ServeDir::new(&root_path).oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
format!("Something went wrong: {}", err),
)),
}
}

View File

@@ -43,7 +43,7 @@ if #[cfg(feature = "ssr")] {
// Setting this to None means we'll be using cargo-leptos and its env vars
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_address;
let addr = leptos_options.site_address.clone();
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> }).await;
// build our application with a route
@@ -56,7 +56,7 @@ if #[cfg(feature = "ssr")] {
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on http://{}", &addr);
log!("listening on {}", &addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
@@ -66,9 +66,15 @@ if #[cfg(feature = "ssr")] {
// client-only stuff for Trunk
else {
use todo_app_sqlite_axum::todo::*;
pub fn main() {
// This example cannot be built as a trunk standalone CSR-only app.
// Only the server may directly connect to the database.
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! { cx, <TodoApp/> }
});
}
}
}

View File

@@ -11,7 +11,7 @@ cfg_if! {
// use http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode};
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))
Ok(SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
}
pub fn register_server_functions() {

View File

@@ -2,8 +2,3 @@
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -16,5 +16,5 @@ leptos = { workspace = true, features = ["ssr"] }
leptos_meta = { workspace = true, features = ["ssr"] }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_config = { workspace = true }
tokio = { version = "1", features = ["full"] }
tokio = { version = "1.0", features = ["full"] }
parking_lot = "0.12.1"

View File

@@ -17,6 +17,7 @@ leptos_server = { workspace = true }
leptos_config = { workspace = true }
tracing = "0.1"
typed-builder = "0.12"
once_cell = "1.17.0"
[dev-dependencies]
leptos = { path = ".", default-features = false }

View File

@@ -1,6 +1,8 @@
use crate::Children;
use leptos::component;
use leptos_dom::{Fragment, IntoView};
use leptos_reactive::{create_memo, Scope};
use leptos_dom::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
@@ -28,7 +30,7 @@ pub fn Show<F, W, IV>(
/// The scope the component is running in
cx: Scope,
/// The components Show wraps
children: Box<dyn Fn(Scope) -> Fragment>,
children: Children,
/// 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
@@ -39,10 +41,12 @@ where
F: Fn(Scope) -> IV + 'static,
IV: IntoView,
{
let memoized_when = create_memo(cx, move |_| when());
// now you don't render until `when` is actually true
let children = Lazy::new(move || children(cx).into_view(cx));
let fallback = Lazy::new(move || fallback(cx).into_view(cx));
move || match memoized_when.get() {
true => children(cx).into_view(cx),
false => fallback(cx).into_view(cx),
move || match when() {
true => children.clone(),
false => fallback.clone(),
}
}

View File

@@ -87,35 +87,50 @@ impl Default for Env {
}
}
fn from_str(input: &str) -> Result<Env, String> {
let sanitized = input.to_lowercase();
match sanitized.as_ref() {
"dev" | "development" => Ok(Env::DEV),
"prod" | "production" => Ok(Env::PROD),
_ => Err(format!(
"{} is not a supported environment. Use either `dev` or `production`.",
input
)),
}
}
impl FromStr for Env {
type Err = ();
fn from_str(input: &str) -> Result<Self, Self::Err> {
from_str(input).or_else(|_| Ok(Self::default()))
let sanitized = input.to_lowercase();
match sanitized.as_ref() {
"dev" => Ok(Self::DEV),
"development" => Ok(Self::DEV),
"prod" => Ok(Self::PROD),
"production" => Ok(Self::PROD),
_ => Ok(Self::DEV),
}
}
}
impl From<&str> for Env {
fn from(str: &str) -> Self {
from_str(str).unwrap_or_else(|err| panic!("{}", err))
let sanitized = str.to_lowercase();
match sanitized.as_str() {
"dev" => Self::DEV,
"development" => Self::DEV,
"prod" => Self::PROD,
"production" => Self::PROD,
_ => {
panic!("Env var is not recognized. Maybe try `dev` or `prod`")
}
}
}
}
impl From<&Result<String, VarError>> for Env {
fn from(input: &Result<String, VarError>) -> Self {
match input {
Ok(str) => from_str(str).unwrap_or_else(|err| panic!("{}", err)),
Err(_) => Self::default(),
Ok(str) => {
let sanitized = str.to_lowercase();
match sanitized.as_ref() {
"dev" => Self::DEV,
"development" => Self::DEV,
"prod" => Self::PROD,
"production" => Self::PROD,
_ => {
panic!("Env var is not recognized. Maybe try `dev` or `prod`")
}
}
}
Err(_) => Self::DEV,
}
}
}
@@ -124,7 +139,15 @@ impl TryFrom<String> for Env {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
from_str(s.as_str())
match s.to_lowercase().as_str() {
"dev" => Ok(Self::DEV),
"development" => Ok(Self::DEV),
"prod" => Ok(Self::PROD),
"production" => Ok(Self::PROD),
other => Err(format!(
"{other} is not a supported environment. Use either `dev` or `production`."
)),
}
}
}

View File

@@ -1,7 +1,4 @@
use crate::{
hydration::{HydrationCtx, HydrationKey},
Comment, IntoView, View,
};
use crate::{hydration::HydrationCtx, Comment, IntoView, View};
use cfg_if::cfg_if;
use leptos_reactive::Scope;
use std::{borrow::Cow, cell::RefCell, fmt, ops::Deref, rc::Rc};
@@ -10,6 +7,8 @@ cfg_if! {
use crate::{mount_child, prepare_to_move, unmount_child, MountKind, Mountable};
use leptos_reactive::{create_effect, ScopeDisposer};
use wasm_bindgen::JsCast;
} else {
use crate::hydration::HydrationKey;
}
}
@@ -78,7 +77,9 @@ impl Mountable for DynChildRepr {
}
impl DynChildRepr {
fn new_with_id(id: HydrationKey) -> Self {
fn new() -> Self {
let id = HydrationCtx::id();
let markers = (
Comment::new(Cow::Borrowed("</DynChild>"), &id, true),
#[cfg(debug_assertions)]
@@ -123,7 +124,6 @@ where
CF: Fn() -> N + 'static,
N: IntoView,
{
id: crate::HydrationKey,
child_fn: CF,
}
@@ -135,12 +135,7 @@ where
/// Creates a new dynamic child which will re-render whenever it's
/// signal dependencies change.
pub fn new(child_fn: CF) -> Self {
Self::new_with_id(HydrationCtx::id(), child_fn)
}
#[doc(hidden)]
pub fn new_with_id(id: HydrationKey, child_fn: CF) -> Self {
Self { id, child_fn }
Self { child_fn }
}
}
@@ -154,9 +149,9 @@ where
instrument(level = "trace", name = "<DynChild />", skip_all)
)]
fn into_view(self, cx: Scope) -> View {
let Self { id, child_fn } = self;
let Self { child_fn } = self;
let component = DynChildRepr::new_with_id(id);
let component = DynChildRepr::new();
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let closing = component.closing.node.clone();

View File

@@ -34,6 +34,7 @@ where
on_cleanup(cx, move || {
queue_microtask(move || {
errors.update(|errors: &mut Errors| {
crate::log!("removing error at {id}");
errors.remove::<E>(&id);
});
});

View File

@@ -13,17 +13,21 @@ proc-macro = true
[dependencies]
cfg-if = "1"
doc-comment = "0.3"
html-escape = "0.2"
itertools = "0.10"
pad-adapter = "0.1"
prettyplease = "0.1"
proc-macro-error = "1"
proc-macro2 = "1"
quote = "1"
syn = { version = "1", features = ["full"] }
syn-rsx = "0.9"
uuid = { version = "1", features = ["v4"] }
leptos_dom = { workspace = true }
leptos_reactive = { workspace = true }
leptos_server = { workspace = true }
lazy_static = "1.4"
convert_case = "0.6.0"
[dev-dependencies]

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

@@ -230,22 +230,6 @@ mod server;
/// # });
/// ```
///
/// 10. You can set any HTML elements `innerHTML` with the `inner_html` attribute on an
/// element. Be careful: this HTML will not be escaped, so you should ensure that it
/// only contains trusted input.
/// ```rust
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let html = "<p>This HTML will be injected.</p>";
/// view! { cx,
/// <div inner_html=html/>
/// }
/// # ;
/// # }
/// # });
/// ```
///
/// Heres a simple example that shows off several of these features, put together
/// ```rust
/// # use leptos::*;

View File

@@ -262,22 +262,11 @@ fn root_element_to_tokens_ssr(
};
let tag_name = node.name.to_string();
let typed_element_name = {
let camel_cased =
camel_case_tag_name(&tag_name.replace("svg::", "").replace("math::", ""));
Ident::new(&camel_cased, node.name.span())
};
let typed_element_name = if is_svg_element(&tag_name) {
quote! { svg::#typed_element_name }
} else if is_math_ml_element(&tag_name) {
quote! { math::#typed_element_name }
} else {
quote! { #typed_element_name }
};
let typed_element_name = Ident::new(&camel_case_tag_name(&tag_name), node.name.span());
quote! {
{
#(#exprs_for_compiler)*
::leptos::HtmlElement::from_html(cx, leptos::leptos_dom::#typed_element_name::default(), #template)
::leptos::HtmlElement::from_html(cx, leptos::#typed_element_name::default(), #template)
}
}
}
@@ -299,13 +288,8 @@ fn element_to_tokens_ssr(
{#component}.into_view(cx).render_to_string(cx),
})
} else {
let tag_name = node
.name
.to_string()
.replace("svg::", "")
.replace("math::", "");
template.push('<');
template.push_str(&tag_name);
template.push_str(&node.name.to_string());
for attr in &node.attributes {
if let Node::Attribute(attr) = attr {
@@ -408,8 +392,30 @@ fn attribute_to_tokens_ssr(
let name = node.key.to_string();
if name == "ref" || name == "_ref" {
// ignore refs on SSR
} else if name.strip_prefix("on:").is_some() {
let (event_type, handler) = event_from_attribute_node(node);
} else if let Some(name) = name.strip_prefix("on:") {
let handler = node
.value
.as_ref()
.expect("event listener attributes need a value")
.as_ref();
#[allow(unused_variables)]
let (name, is_force_undelegated) = parse_event(name);
let event_type = TYPED_EVENTS
.iter()
.find(|e| **e == name)
.copied()
.unwrap_or("Custom");
let event_type = event_type
.parse::<TokenStream>()
.expect("couldn't parse event name");
let event_type = if is_force_undelegated {
quote! { ::leptos::ev::undelegated(::leptos::ev::#event_type) }
} else {
quote! { ::leptos::ev::#event_type }
};
exprs_for_compiler.push(quote! {
leptos::ssr_event_listener(#event_type, #handler);
})
@@ -925,9 +931,7 @@ fn component_to_tokens(
let props = attrs
.clone()
.filter(|attr| {
!attr.key.to_string().starts_with("clone:") && !attr.key.to_string().starts_with("on:")
})
.filter(|attr| !attr.key.to_string().starts_with("clone:"))
.map(|attr| {
let name = &attr.key;
@@ -946,7 +950,6 @@ fn component_to_tokens(
});
let items_to_clone = attrs
.clone()
.filter(|attr| attr.key.to_string().starts_with("clone:"))
.map(|attr| {
let ident = attr
@@ -960,17 +963,6 @@ fn component_to_tokens(
})
.collect::<Vec<_>>();
let events = attrs
.filter(|attr| attr.key.to_string().starts_with("on:"))
.map(|attr| {
let (event_type, handler) = event_from_attribute_node(attr);
quote! {
.on(#event_type, #handler)
}
})
.collect::<Vec<_>>();
let children = if node.children.is_empty() {
quote! {}
} else {
@@ -996,55 +988,17 @@ fn component_to_tokens(
}
};
let component = quote! {
quote! {
#name(
#cx,
#component_props_name::builder()
#(#props)*
#children
.build()
.build(),
)
};
if events.is_empty() {
component
} else {
quote! {
#component.into_view(#cx)
#(#events)*
}
}
}
fn event_from_attribute_node(attr: &NodeAttribute) -> (TokenStream, &Expr) {
let event_name = attr.key.to_string().strip_prefix("on:").unwrap().to_owned();
let handler = attr
.value
.as_ref()
.expect("event listener attributes need a value")
.as_ref();
#[allow(unused_variables)]
let (name, is_force_undelegated) = parse_event(&event_name);
let event_type = TYPED_EVENTS
.iter()
.find(|e| **e == name)
.copied()
.unwrap_or("Custom");
let event_type = event_type
.parse::<TokenStream>()
.expect("couldn't parse event name");
let event_type = if is_force_undelegated {
quote! { ::leptos::ev::undelegated(::leptos::ev::#event_type) }
} else {
quote! { ::leptos::ev::#event_type }
};
(event_type, handler)
}
fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
match tag_name {
NodeName::Path(path) => path

View File

@@ -83,17 +83,6 @@ impl Scope {
self.id
}
/// Returns the chain of scope IDs beginning with this one, going to its parent, grandparents, etc.
pub fn ancestry(&self) -> Vec<ScopeId> {
let mut ids = vec![self.id];
let mut cx = *self;
while let Some(parent) = cx.parent() {
ids.push(parent.id());
cx = parent;
}
ids
}
/// Creates a child scope and runs the given function within it, returning a handle to dispose of it.
///
/// The child scope has its own lifetime and disposer, but will be disposed when the parent is

View File

@@ -14,9 +14,12 @@ leptos_reactive = { workspace = true }
form_urlencoded = "1"
gloo-net = "0.2"
lazy_static = "1"
linear-map = "1"
log = "0.4"
serde = { version = "1", features = ["derive"] }
serde_urlencoded = "0.7"
thiserror = "1"
rmp-serde = "1.1.1"
serde_json = "1.0.89"
quote = "1"
syn = { version = "1", features = ["full", "parsing", "extra-traits"] }
@@ -28,23 +31,23 @@ leptos = { path = "../leptos" }
[features]
csr = [
#"leptos/csr",
"leptos_dom/web",
"leptos_reactive/csr",
#"leptos/csr",
"leptos_dom/web",
"leptos_reactive/csr",
]
hydrate = [
#"leptos/hydrate",
"leptos_dom/web",
"leptos_reactive/hydrate",
#"leptos/hydrate",
"leptos_dom/web",
"leptos_reactive/hydrate",
]
ssr = [
#"leptos/ssr",
"leptos_reactive/ssr",
#"leptos/ssr",
"leptos_reactive/ssr",
]
stable = [
#"leptos/stable",
"leptos_dom/stable",
"leptos_reactive/stable",
#"leptos/stable",
"leptos_dom/stable",
"leptos_reactive/stable",
]
[package.metadata.cargo-all-features]

View File

@@ -13,10 +13,12 @@ leptos = { workspace = true }
cfg-if = "1"
common_macros = "0.1"
gloo-net = "0.2"
itertools = "0.10"
lazy_static = "1"
linear-map = "1"
log = "0.4"
regex = { version = "1", optional = true }
bincode = "1"
url = { version = "2", optional = true }
percent-encoding = "2"
thiserror = "1"
@@ -29,26 +31,26 @@ wasm-bindgen-futures = { version = "0.4" }
[dependencies.web-sys]
version = "0.3"
features = [
# History/Routing
"History",
"HtmlAnchorElement",
"MouseEvent",
"Url",
# Form
"FormData",
"HtmlButtonElement",
"HtmlFormElement",
"HtmlInputElement",
"SubmitEvent",
"Url",
"UrlSearchParams",
# Fetching in Hydrate Mode
"Headers",
"Request",
"RequestInit",
"RequestMode",
"Response",
"Window",
# History/Routing
"History",
"HtmlAnchorElement",
"MouseEvent",
"Url",
# Form
"FormData",
"HtmlButtonElement",
"HtmlFormElement",
"HtmlInputElement",
"SubmitEvent",
"Url",
"UrlSearchParams",
# Fetching in Hydrate Mode
"Headers",
"Request",
"RequestInit",
"RequestMode",
"Response",
"Window",
]
[features]

View File

@@ -7,7 +7,6 @@ use leptos::*;
/// that child route is displayed. Renders nothing if there is no nested child.
#[component]
pub fn Outlet(cx: Scope) -> impl IntoView {
let id = HydrationCtx::id();
let route = use_route(cx);
let is_showing = Rc::new(Cell::new(None::<(usize, Scope)>));
let (outlet, set_outlet) = create_signal(cx, None::<View>);
@@ -28,10 +27,10 @@ pub fn Outlet(cx: Scope) -> impl IntoView {
}
is_showing.set(Some((child.id(), child.cx())));
provide_context(child.cx(), child.clone());
set_outlet.set(Some(child.outlet(cx).into_view(cx)))
set_outlet.set(Some(child.outlet().into_view(cx)))
}
}
});
leptos::DynChild::new_with_id(id, move || outlet.get())
move || outlet.get()
}

View File

@@ -104,7 +104,7 @@ impl RouteContext {
path: RefCell::new(path),
original_path: route.original_path.to_string(),
params,
outlet: Box::new(move |cx| Some(element(cx))),
outlet: Box::new(move || Some(element(cx))),
}),
})
}
@@ -155,7 +155,7 @@ impl RouteContext {
path: RefCell::new(path.to_string()),
original_path: path.to_string(),
params: create_memo(cx, |_| ParamsMap::new()),
outlet: Box::new(move |cx| fallback.as_ref().map(move |f| f(cx))),
outlet: Box::new(move || fallback.as_ref().map(move |f| f(cx))),
}),
}
}
@@ -171,8 +171,8 @@ impl RouteContext {
}
/// The view associated with the current route.
pub fn outlet(&self, cx: Scope) -> impl IntoView {
(self.inner.outlet)(cx)
pub fn outlet(&self) -> impl IntoView {
(self.inner.outlet)()
}
}
@@ -184,7 +184,7 @@ pub(crate) struct RouteContextInner {
pub(crate) path: RefCell<String>,
pub(crate) original_path: String,
pub(crate) params: Memo<ParamsMap>,
pub(crate) outlet: Box<dyn Fn(Scope) -> Option<View>>,
pub(crate) outlet: Box<dyn Fn() -> Option<View>>,
}
impl PartialEq for RouteContextInner {

View File

@@ -29,6 +29,7 @@ pub fn Routes(
let base_route = router.base();
let mut branches = Vec::new();
let id_before = HydrationCtx::peek();
let frag = children(cx);
let children = frag
.as_children()
@@ -194,12 +195,11 @@ pub fn Routes(
});
// show the root route
let id = HydrationCtx::id();
let root = create_memo(cx, move |prev| {
provide_context(cx, route_states);
route_states.with(|state| {
if state.routes.borrow().is_empty() {
Some(base_route.outlet(cx).into_view(cx))
Some(base_route.outlet().into_view(cx))
} else {
let root = state.routes.borrow();
let root = root.get(0);
@@ -208,7 +208,7 @@ pub fn Routes(
}
if prev.is_none() || !root_equal.get() {
root.as_ref().map(|route| route.outlet(cx).into_view(cx))
root.as_ref().map(|route| route.outlet().into_view(cx))
} else {
prev.cloned().unwrap()
}
@@ -216,8 +216,8 @@ pub fn Routes(
})
});
//HydrationCtx::continue_from(id_before);
leptos::DynChild::new_with_id(id, move || root.get())
HydrationCtx::continue_from(id_before);
(move || root.get()).into_view(cx)
}
#[derive(Clone, Debug, PartialEq)]

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