Several Minor Updates on Examples (#427)

This commit is contained in:
IcosaHedron 2023-02-01 19:20:34 -05:00 committed by GitHub
parent 1f6a326268
commit 63a7a4dec1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 712 additions and 774 deletions

View File

@ -1,9 +1,8 @@
# Leptos Todo App Sqlite with Axum
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
# Leptos Errors Demonstration with Axum
This example demonstrates how Leptos Errors can work with an Axum backend on a server.
## Client Side Rendering
This example cannot be built as a trunk standalone CSR-only app as it requires the server to send HTTP Status Codes.
This example cannot be built as a trunk standalone CSR-only app as it requires the server to send status codes.
## Server Side Rendering with cargo-leptos
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)

View File

@ -1,14 +1,13 @@
use crate::errors::AppError;
use cfg_if::cfg_if;
use leptos::Errors;
use leptos::{
component, create_rw_signal, use_context, view, For, ForProps, IntoView, RwSignal, Scope,
};
use leptos::*;
#[cfg(feature = "ssr")]
use leptos_axum::ResponseOptions;
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them
// A basic function to display errors served by the error boundaries.
// Feel free to do more complicated things here than just displaying them.
#[component]
pub fn ErrorTemplate(
cx: Scope,
@ -35,16 +34,14 @@ 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{
if let Some(response) = response {
response.set_status(errors[0].status_code());
}
}
}
}}
view! {cx,
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
@ -55,8 +52,7 @@ pub fn ErrorTemplate(
view= move |error| {
let error_string = error.1.to_string();
let error_code= error.1.status_code();
view! {
cx,
view! { cx,
<h2>{error_code.to_string()}</h2>
<p>"Error: " {error_string}</p>
}

View File

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

View File

@ -2,17 +2,14 @@ use crate::{
error_template::{ErrorTemplate, ErrorTemplateProps},
errors::AppError,
};
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
cfg_if! { if #[cfg(feature = "ssr")] {
pub fn register_server_functions() {
#[cfg(feature = "ssr")]
pub fn register_server_functions() {
_ = CauseInternalServerError::register();
_ = CauseNotFoundError::register();
}
}}
}
#[server(CauseInternalServerError, "/api")]
pub async fn cause_internal_server_error() -> Result<(), ServerFnError> {
@ -24,11 +21,6 @@ pub async fn cause_internal_server_error() -> Result<(), ServerFnError> {
))
}
#[server(CauseNotFoundError, "/api")]
pub async fn cause_not_found_error() -> Result<(), ServerFnError> {
Err(ServerFnError::ServerError("Not Found".to_string()))
}
#[component]
pub fn App(cx: Scope) -> impl IntoView {
//let id = use_context::<String>(cx);
@ -45,9 +37,7 @@ pub fn App(cx: Scope) -> impl IntoView {
<Routes>
<Route path="" view=|cx| view! {
cx,
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
<ExampleErrors/>
</ErrorBoundary>
}/>
</Routes>
</main>
@ -57,16 +47,25 @@ pub fn App(cx: Scope) -> impl IntoView {
#[component]
pub fn ExampleErrors(cx: Scope) -> impl IntoView {
view! {
cx,
let generate_internal_error = create_server_action::<CauseInternalServerError>(cx);
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>
"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>
"The following <div> will always contain an error and cause the page to produce status 500. Check browser dev tools. "
"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>

View File

@ -1,35 +1,42 @@
use cfg_if::cfg_if;
use leptos::*;
// boilerplate to run in different modes
cfg_if! {
if #[cfg(feature = "ssr")] {
cfg_if! { if #[cfg(feature = "ssr")] {
use crate::fallback::file_and_error_handler;
use crate::landing::*;
use axum::body::Body as AxumBody;
use axum::{
routing::{post, get},
extract::{Extension, Path},
http::Request,
response::{IntoResponse, Response},
routing::{get, post},
Router,
};
use axum::body::Body as AxumBody;
use crate::landing::*;
use errors_axum::*;
use crate::fallback::file_and_error_handler;
use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use std::sync::Arc;
}}
//Define a handler to test extractor with state
async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
//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/> }
|cx| view! { cx, <App/> },
);
handler(req).await.into_response()
}
}
#[tokio::main]
async fn main() {
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
crate::landing::register_server_functions();
@ -44,7 +51,7 @@ if #[cfg(feature = "ssr")] {
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)));
@ -55,20 +62,11 @@ if #[cfg(feature = "ssr")] {
.serve(app.into_make_service())
.await
.unwrap();
}
}
// client-only stuff for Trunk
else {
use todo_app_sqlite_axum::landing::*;
pub fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! { cx, <App/> }
});
}
}
// this is if we were using client-only rending with Trunk
#[cfg(not(feature = "ssr"))]
pub fn main() {
// This example cannot be built as a trunk standalone CSR-only app.
// The server is needed to demonstrate the error statuses.
}

View File

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

View File

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

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

View File

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

View File

@ -1,42 +1,15 @@
use convert_case::{
Case::{
Pascal,
Snake,
},
Case::{Pascal, Snake},
Casing,
};
use itertools::Itertools;
use proc_macro2::{
Ident,
TokenStream,
};
use quote::{
format_ident,
ToTokens,
TokenStreamExt,
};
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, ToTokens, TokenStreamExt};
use std::collections::HashSet;
use syn::{
parse::Parse,
parse_quote,
AngleBracketedGenericArguments,
Attribute,
FnArg,
GenericArgument,
ItemFn,
LitStr,
Meta,
MetaList,
MetaNameValue,
NestedMeta,
Pat,
PatIdent,
Path,
PathArguments,
ReturnType,
Type,
TypePath,
Visibility,
parse::Parse, parse_quote, AngleBracketedGenericArguments, Attribute, FnArg, GenericArgument,
ItemFn, LitStr, Meta, MetaList, MetaNameValue, NestedMeta, Pat, PatIdent, Path, PathArguments,
ReturnType, Type, TypePath, Visibility,
};
pub struct Model {
@ -117,10 +90,7 @@ impl Parse for Model {
// implemented manually because Vec::drain_filter is nightly only
// follows std recommended parallel
fn drain_filter<T>(
vec: &mut Vec<T>,
mut some_predicate: impl FnMut(&mut T) -> bool,
) {
fn drain_filter<T>(vec: &mut Vec<T>, mut some_predicate: impl FnMut(&mut T) -> bool) {
let mut i = 0;
while i < vec.len() {
if some_predicate(&mut vec[i]) {
@ -291,9 +261,7 @@ impl Prop {
});
// Make sure conflicting options are not present
if prop_opts.contains(&PropOpt::Optional)
&& prop_opts.contains(&PropOpt::OptionalNoStrip)
{
if prop_opts.contains(&PropOpt::Optional) && prop_opts.contains(&PropOpt::OptionalNoStrip) {
abort!(
typed,
"`optional` and `optional_no_strip` options are mutually exclusive"
@ -360,13 +328,11 @@ impl Docs {
}
fn padded(&self) -> TokenStream {
self
.0
self.0
.iter()
.enumerate()
.map(|(idx, attr)| {
if let Meta::NameValue(MetaNameValue { lit: doc, .. }) =
attr.parse_meta().unwrap()
if let Meta::NameValue(MetaNameValue { lit: doc, .. }) = attr.parse_meta().unwrap()
{
let doc_str = quote!(#doc);
@ -401,8 +367,7 @@ impl Docs {
.0
.iter()
.map(|attr| {
if let Meta::NameValue(MetaNameValue { lit: doc, .. }) =
attr.parse_meta().unwrap()
if let Meta::NameValue(MetaNameValue { lit: doc, .. }) = attr.parse_meta().unwrap()
{
let mut doc_str = quote!(#doc).to_string();
@ -507,8 +472,7 @@ struct TypedBuilderOpts {
impl TypedBuilderOpts {
fn from_opts(opts: &HashSet<PropOpt>, is_ty_option: bool) -> Self {
Self {
default: opts.contains(&PropOpt::Optional)
|| opts.contains(&PropOpt::OptionalNoStrip),
default: opts.contains(&PropOpt::Optional) || opts.contains(&PropOpt::OptionalNoStrip),
default_with_value: opts.iter().find_map(|p| match p {
PropOpt::OptionalWithDefault(v) => Some(v.to_owned()),
_ => None,
@ -601,8 +565,7 @@ fn generate_component_fn_prop_docs(props: &[Prop]) -> TokenStream {
let optional_prop_docs = props
.iter()
.filter(|Prop { prop_opts, .. }| {
prop_opts.contains(&PropOpt::Optional)
|| prop_opts.contains(&PropOpt::OptionalNoStrip)
prop_opts.contains(&PropOpt::Optional) || prop_opts.contains(&PropOpt::OptionalNoStrip)
})
.map(|p| prop_to_doc(p, PropDocStyle::List))
.collect::<TokenStream>();
@ -660,8 +623,7 @@ fn unwrap_option(ty: &Type) -> Option<Type> {
if let [first] = &segments.iter().collect::<Vec<_>>()[..] {
if first.ident == "Option" {
if let PathArguments::AngleBracketed(AngleBracketedGenericArguments {
args,
..
args, ..
}) = &first.arguments
{
if let [first] = &args.iter().collect::<Vec<_>>()[..] {