updates toward `todo_app_sqlite`

This commit is contained in:
Greg Johnston 2024-04-10 08:56:12 -04:00
parent e837e9fded
commit a01640cafd
19 changed files with 270 additions and 107 deletions

View File

@ -14,8 +14,6 @@ http = "1.0"
leptos = { path = "../../leptos" }
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_meta = { path = "../../meta" }
leptos_router = { path = "../../router" }
log = "0.4"
simple_logger = "4.0"
serde = { version = "1", features = ["derive"] }

View File

@ -1,5 +1,8 @@
use crate::errors::TodoAppError;
use leptos::*;
use leptos::context::use_context;
use leptos::signals::RwSignal;
use leptos::{component, server, view, For, IntoView};
use leptos::{prelude::*, Errors};
#[cfg(feature = "ssr")]
use leptos_axum::ResponseOptions;
@ -8,10 +11,10 @@ use leptos_axum::ResponseOptions;
#[component]
pub fn ErrorTemplate(
#[prop(optional)] outside_errors: Option<Errors>,
#[prop(optional)] errors: Option<RwSignal<Errors>>,
#[prop(optional, into)] errors: Option<RwSignal<Errors>>,
) -> impl IntoView {
let errors = match outside_errors {
Some(e) => create_rw_signal(e),
Some(e) => RwSignal::new(e),
None => match errors {
Some(e) => e,
None => panic!("No Errors found and we expected errors!"),

View File

@ -5,7 +5,8 @@ use axum::{
http::{Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse},
};
use leptos::{view, Errors, LeptosOptions};
use leptos::config::LeptosOptions;
use leptos::{view, Errors};
use tower::ServiceExt;
use tower_http::services::ServeDir;

View File

@ -4,6 +4,7 @@ pub mod errors;
pub mod fallback;
pub mod todo;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use crate::todo::TodoApp;
@ -11,5 +12,5 @@ pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Error);
console_error_panic_hook::set_once();
leptos::mount_to_body(TodoApp);
leptos::hydrate_body(TodoApp);
}

View File

@ -7,7 +7,11 @@ use axum::{
routing::get,
Router,
};
use leptos::*;
use leptos::{
config::{get_configuration, LeptosOptions},
view,
};
use leptos::{context::provide_context, HydrationScripts};
use leptos_axum::{generate_route_list, LeptosRoutes};
use todo_app_sqlite_axum::*;
@ -48,14 +52,35 @@ async fn main() {
// build our application with a route
let app = Router::new()
.route("/special/:id", get(custom_handler))
.leptos_routes(&leptos_options, routes, || view! { <TodoApp/> })
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || {
use leptos::prelude::*;
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
//<AutoReload options=app_state.leptos_options.clone() />
<HydrationScripts options=leptos_options.clone()/>
<link rel="stylesheet" id="leptos" href="/pkg/benwis_leptos.css"/>
<link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
</head>
<body>
<TodoApp/>
</body>
</html>
}
}})
.fallback(file_and_error_handler)
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
logging::log!("listening on http://{}", &addr);
println!("listening on http://{}", &addr);
axum::serve(listener, app.into_make_service())
.await
.unwrap();

View File

@ -1,9 +1,14 @@
use crate::error_template::ErrorTemplate;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use leptos::context::use_context;
use leptos::server::{Resource, ServerAction};
use leptos::tachys::either::Either;
use leptos::{
component, server, suspend, view, ActionForm, ErrorBoundary, IntoView,
};
use leptos::{prelude::*, Transition};
use serde::{Deserialize, Serialize};
use server_fn::codec::SerdeLite;
use server_fn::ServerFnError;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]
@ -16,7 +21,7 @@ pub struct Todo {
#[cfg(feature = "ssr")]
pub mod ssr {
// use http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode};
use leptos::ServerFnError;
use leptos::server_fn::ServerFnError;
use sqlx::{Connection, SqliteConnection};
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
@ -87,82 +92,65 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
#[component]
pub fn TodoApp() -> impl IntoView {
//let id = use_context::<String>();
provide_meta_context();
view! {
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/todo_app_sqlite_axum.css"/>
<Router>
<header>
<h1>"My Tasks"</h1>
</header>
<main>
<Routes>
<Route path="" view=Todos/>
</Routes>
</main>
</Router>
<header>
<h1>"My Tasks"</h1>
</header>
<main>
<Todos/>
</main>
}
}
#[component]
pub fn Todos() -> impl IntoView {
let add_todo = create_server_multi_action::<AddTodo>();
let delete_todo = create_server_action::<DeleteTodo>();
let submissions = add_todo.submissions();
//let add_todo = create_server_multi_action::<AddTodo>();
let delete_todo = ServerAction::<DeleteTodo>::new();
//let submissions = add_todo.submissions();
// list of todos is loaded from the server in reaction to changes
let todos = create_resource(
move || (add_todo.version().get(), delete_todo.version().get()),
let todos = Resource::new_serde(
move || (delete_todo.version().get()), //(add_todo.version().get(), delete_todo.version().get()),
move |_| get_todos(),
);
view! {
<div>
<MultiActionForm action=add_todo>
/*<MultiActionForm action=add_todo>
<label>
"Add a Todo"
<input type="text" name="title"/>
</label>
<input type="submit" value="Add"/>
</MultiActionForm>
</MultiActionForm>*/
<Transition fallback=move || view! {<p>"Loading..."</p> }>
<ErrorBoundary fallback=|errors| view!{<ErrorTemplate errors=errors/>}>
{move || {
let existing_todos = {
move || {
todos.get()
.map(move |todos| match todos {
Err(e) => {
view! { <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view()
}
Ok(todos) => {
if todos.is_empty() {
view! { <p>"No tasks were found."</p> }.into_view()
} else {
todos
.into_iter()
.map(move |todo| {
view! {
<li>
{todo.title}
<ActionForm action=delete_todo>
<input type="hidden" name="id" value={todo.id}/>
<input type="submit" value="X"/>
</ActionForm>
</li>
}
})
.collect_view()
<ErrorBoundary fallback=|errors| view!{<ErrorTemplate errors/>}>
<ul>
{suspend!(
todos.await.map(|todos| {
if todos.is_empty() {
Either::Left(view! { <p>"No tasks were found."</p> })
} else {
Either::Right(todos
.into_iter()
.map(move |todo| {
view! {
<li>
{todo.title}
<ActionForm action=delete_todo>
<input type="hidden" name="id" value={todo.id}/>
<input type="submit" value="X"/>
</ActionForm>
</li>
}
}
})
.unwrap_or_default()
}
};
})
.collect::<Vec<_>>()
)
}
})
)}
let pending_todos = move || {
/*let pending_todos = move || {
submissions
.get()
.into_iter()
@ -173,18 +161,12 @@ pub fn Todos() -> impl IntoView {
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
}
})
.collect_view()
};
.collect::<Vec<_>>()
};*/
view! {
<ul>
{existing_todos}
{pending_todos}
</ul>
}
}
}
// {existing_todos}
//{pending_todos}
</ul>
</ErrorBoundary>
</Transition>
</div>

View File

@ -1435,7 +1435,7 @@ where
additional_context();
RouteList::generate(&app_fn)
})
.expect("could not generate routes");
.unwrap_or_default();
// Axum's Router defines Root routes as "/" not ""
let mut routes = routes

View File

@ -27,7 +27,7 @@ paste = "1"
rand = { version = "0.8", optional = true }
reactive_graph = { workspace = true, features = ["serde"] }
rustc-hash = "1"
tachys = { workspace = true, features = ["reactive_graph"] }
tachys = { workspace = true, features = ["reactive_graph", "oco"] }
thiserror = "1"
tracing = "0.1"
typed-builder = "0.18"
@ -86,7 +86,7 @@ rkyv = ["leptos_reactive/rkyv", "server_fn/rkyv"]
tracing = [
"reactive_graph/tracing",
] #, "leptos_macro/tracing", "leptos_dom/tracing"]
nonce = ["leptos_dom/nonce"]
nonce = ["base64", "leptos_dom/nonce", "rand"]
spin = ["leptos_reactive/spin", "leptos-spin-macro"]
experimental-islands = [
"leptos_dom/experimental-islands",

View File

@ -115,7 +115,16 @@ where
const MIN_LENGTH: usize = Chil::MIN_LENGTH;
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
todo!()
self.fallback.to_html_with_buf(buf, position);
}
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
self,
buf: &mut StreamBuilder,
position: &mut Position,
) where
Self: Sized,
{
}
fn hydrate<const FROM_SERVER: bool>(

View File

@ -2,11 +2,11 @@ use crate::ServerMetaContext;
use indexmap::IndexMap;
use leptos::{
component,
error::Result,
oco::Oco,
reactive_graph::{effect::RenderEffect, owner::use_context},
tachys::{
dom::document,
error::Result,
html::{
attribute::{
any_attribute::{
@ -96,6 +96,7 @@ struct BodyViewState {
impl Render<Dom> for BodyView {
type State = BodyViewState;
type FallibleState = BodyViewState;
type AsyncOutput = Self;
fn build(self) -> Self::State {
let el = document().body().expect("there to be a <body> element");
@ -121,6 +122,10 @@ impl Render<Dom> for BodyView {
self.rebuild(state);
Ok(())
}
async fn resolve(self) -> Self {
self
}
}
impl RenderHtml<Dom> for BodyView {

View File

@ -2,11 +2,11 @@ use crate::ServerMetaContext;
use indexmap::IndexMap;
use leptos::{
component,
error::Result,
oco::Oco,
reactive_graph::{effect::RenderEffect, owner::use_context},
tachys::{
dom::document,
error::Result,
html::{
attribute::{
self,
@ -107,6 +107,7 @@ struct HtmlViewState {
impl Render<Dom> for HtmlView {
type State = HtmlViewState;
type FallibleState = HtmlViewState;
type AsyncOutput = Self;
fn build(self) -> Self::State {
let el = document()
@ -134,6 +135,10 @@ impl Render<Dom> for HtmlView {
self.rebuild(state);
Ok(())
}
async fn resolve(self) -> Self {
self
}
}
impl RenderHtml<Dom> for HtmlView {

View File

@ -165,8 +165,8 @@ impl ServerMetaContext {
/// included.
pub async fn inject_meta_context(
self,
mut stream: impl Stream<Item = String> + Send + Sync + Unpin,
) -> impl Stream<Item = String> + Send + Sync {
mut stream: impl Stream<Item = String> + Send + Unpin,
) -> impl Stream<Item = String> + Send {
let mut first_chunk = stream.next().await.unwrap_or_default();
let meta_buf =
@ -324,6 +324,7 @@ where
{
type State = RegisteredMetaTagState<E, At, Ch>;
type FallibleState = RegisteredMetaTagState<E, At, Ch>;
type AsyncOutput = Self;
fn build(self) -> Self::State {
let state = self.el.unwrap().build();
@ -334,17 +335,21 @@ where
self.el.unwrap().rebuild(&mut state.state);
}
fn try_build(self) -> leptos::tachys::error::Result<Self::FallibleState> {
fn try_build(self) -> leptos::error::Result<Self::FallibleState> {
Ok(self.build())
}
fn try_rebuild(
self,
state: &mut Self::FallibleState,
) -> leptos::tachys::error::Result<()> {
) -> leptos::error::Result<()> {
self.rebuild(state);
Ok(())
}
async fn resolve(self) -> Self {
self
}
}
impl<E, At, Ch> RenderHtml<Dom> for RegisteredMetaTag<E, At, Ch>
@ -435,21 +440,26 @@ struct MetaTagsView {
impl Render<Dom> for MetaTagsView {
type State = ();
type FallibleState = ();
type AsyncOutput = Self;
fn build(self) -> Self::State {}
fn rebuild(self, state: &mut Self::State) {}
fn try_build(self) -> leptos::tachys::error::Result<Self::FallibleState> {
fn try_build(self) -> leptos::error::Result<Self::FallibleState> {
Ok(())
}
fn try_rebuild(
self,
state: &mut Self::FallibleState,
) -> leptos::tachys::error::Result<()> {
) -> leptos::error::Result<()> {
Ok(())
}
async fn resolve(self) -> Self::AsyncOutput {
self
}
}
impl RenderHtml<Dom> for MetaTagsView {

View File

@ -1,6 +1,7 @@
use crate::{use_head, MetaContext, ServerMetaContext};
use leptos::{
component,
error::Result,
oco::Oco,
reactive_graph::{
effect::RenderEffect,
@ -8,7 +9,6 @@ use leptos::{
},
tachys::{
dom::document,
error::Result,
hydration::Cursor,
renderer::{dom::Dom, Renderer},
view::{Mountable, Position, PositionState, Render, RenderHtml},
@ -193,6 +193,7 @@ struct TitleViewState {
impl Render<Dom> for TitleView {
type State = TitleViewState;
type FallibleState = TitleViewState;
type AsyncOutput = Self;
fn build(self) -> Self::State {
let el = self.el();
@ -229,6 +230,10 @@ impl Render<Dom> for TitleView {
self.rebuild(state);
Ok(())
}
async fn resolve(self) -> Self {
self
}
}
impl RenderHtml<Dom> for TitleView {

View File

@ -198,6 +198,13 @@ impl<T: Send + Sync + 'static> From<ArcRwSignal<T>> for RwSignal<T> {
}
}
impl<'a, T: Send + Sync + 'static> From<&'a ArcRwSignal<T>> for RwSignal<T> {
#[track_caller]
fn from(value: &'a ArcRwSignal<T>) -> Self {
value.clone().into()
}
}
impl<T: Send + Sync + 'static> From<RwSignal<T>> for ArcRwSignal<T> {
#[track_caller]
fn from(value: RwSignal<T>) -> Self {

View File

@ -7,7 +7,7 @@ where
Self: 'static,
R: Renderer + 'static,
{
type Output: Render<R>;
type Output: Render<R> + Send;
fn choose(self, route_data: RouteData<R>) -> Self::Output;
}
@ -15,7 +15,7 @@ where
impl<F, View, R> ChooseView<R> for F
where
F: Fn(RouteData<R>) -> View + 'static,
View: Render<R>,
View: Render<R> + Send,
R: Renderer + 'static,
{
type Output = View;

View File

@ -91,7 +91,7 @@ where
R: Renderer + 'static,
{
type Child: MatchInterface<R> + MatchParams + 'static;
type View: Render<R> + RenderHtml<R> + 'static;
type View: Render<R> + RenderHtml<R> + Send + 'static;
fn as_id(&self) -> RouteMatchId;

View File

@ -112,7 +112,7 @@ where
Rndr: Renderer + 'static,
Child: MatchInterface<Rndr> + MatchParams + 'static,
ViewFn: Fn(RouteData<Rndr>) -> View + 'static,
View: Render<Rndr> + RenderHtml<Rndr> + 'static,
View: Render<Rndr> + RenderHtml<Rndr> + Send + 'static,
{
type Child = Child;
type View = ViewFn::Output;
@ -148,7 +148,7 @@ where
Children: 'static,
<Children::Match as MatchParams>::Params: Clone,
ViewFn: Fn(RouteData<Rndr>) -> View + Clone + 'static,
View: Render<Rndr> + RenderHtml<Rndr> + 'static,
View: Render<Rndr> + RenderHtml<Rndr> + Send + 'static,
{
type Data = Data;
type View = View;

View File

@ -120,6 +120,7 @@ where
>,
>;
type FallibleState = (); // TODO
type AsyncOutput = (); // TODO
fn build(self) -> Self::State {
let location = Loc::new().unwrap(); // TODO
@ -176,16 +177,18 @@ where
fn rebuild(self, state: &mut Self::State) {}
fn try_build(self) -> tachys::error::Result<Self::FallibleState> {
fn try_build(self) -> leptos::error::Result<Self::FallibleState> {
todo!()
}
fn try_rebuild(
self,
state: &mut Self::FallibleState,
) -> tachys::error::Result<()> {
) -> leptos::error::Result<()> {
todo!()
}
async fn resolve(self) -> Self::AsyncOutput {}
}
impl<Rndr, Loc, FallbackFn, Fallback, Children> RenderHtml<Rndr>
@ -631,6 +634,7 @@ where
{
type State = Outlet<R>;
type FallibleState = ();
type AsyncOutput = Self;
fn build(self) -> Self::State {
self
@ -640,16 +644,20 @@ where
todo!()
}
fn try_build(self) -> tachys::error::Result<Self::FallibleState> {
fn try_build(self) -> leptos::error::Result<Self::FallibleState> {
todo!()
}
fn try_rebuild(
self,
state: &mut Self::FallibleState,
) -> tachys::error::Result<()> {
) -> leptos::error::Result<()> {
todo!()
}
async fn resolve(self) -> Self {
self
}
}
impl<R> RenderHtml<R> for Outlet<R>
@ -857,6 +865,7 @@ where
{
type State = NestedRouteState<Matcher, R>;
type FallibleState = ();
type AsyncOutput = Self;
fn build(self) -> Self::State {
let NestedRouteView {
@ -892,16 +901,20 @@ where
view.rebuild(&mut state.view);
}
fn try_build(self) -> tachys::error::Result<Self::FallibleState> {
fn try_build(self) -> leptos::error::Result<Self::FallibleState> {
todo!()
}
fn try_rebuild(
self,
state: &mut Self::FallibleState,
) -> tachys::error::Result<()> {
) -> leptos::error::Result<()> {
todo!()
}
async fn resolve(self) -> Self {
self
}
}
impl<Matcher, R> RenderHtml<R> for NestedRouteView<Matcher, R>
@ -1075,6 +1088,7 @@ where
>,
>;
type FallibleState = Self::State;
type AsyncOutput = Self;
fn build(self) -> Self::State {
let location = Loc::new().unwrap(); // TODO
@ -1158,16 +1172,20 @@ where
fn rebuild(self, state: &mut Self::State) {}
fn try_build(self) -> tachys::error::Result<Self::FallibleState> {
fn try_build(self) -> leptos::error::Result<Self::FallibleState> {
todo!()
}
fn try_rebuild(
self,
state: &mut Self::FallibleState,
) -> tachys::error::Result<()> {
) -> leptos::error::Result<()> {
todo!()
}
async fn resolve(self) -> Self {
self
}
}
impl<Rndr, Loc, FallbackFn, Fallback, Children> RenderHtml<Rndr>

View File

@ -1,5 +1,14 @@
use crate::renderer::Renderer;
use std::borrow::Cow;
use std::{
borrow::Cow,
fmt::Write,
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
num::{
NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8,
NonZeroIsize, NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64,
NonZeroU8, NonZeroUsize,
},
};
pub trait AttributeValue<R: Renderer> {
type State;
@ -326,3 +335,88 @@ where
fn escape_attr(value: &str) -> Cow<'_, str> {
html_escape::encode_double_quoted_attribute(value)
}
macro_rules! render_primitive {
($($child_type:ty),* $(,)?) => {
$(
impl<R> AttributeValue<R> for $child_type
where
R: Renderer,
{
type State = (R::Element, $child_type);
fn html_len(&self) -> usize {
0
}
fn to_html(self, key: &str, buf: &mut String) {
<String as AttributeValue<R>>::to_html(self.to_string(), key, buf);
}
fn to_template(_key: &str, _buf: &mut String) {}
fn hydrate<const FROM_SERVER: bool>(
self,
key: &str,
el: &R::Element,
) -> Self::State {
// if we're actually hydrating from SSRed HTML, we don't need to set the attribute
// if we're hydrating from a CSR-cloned <template>, we do need to set non-StaticAttr attributes
if !FROM_SERVER {
R::set_attribute(el, key, &self.to_string());
}
(el.clone(), self)
}
fn build(self, el: &R::Element, key: &str) -> Self::State {
R::set_attribute(el, key, &self.to_string());
(el.to_owned(), self)
}
fn rebuild(self, key: &str, state: &mut Self::State) {
let (el, prev_value) = state;
if self != *prev_value {
R::set_attribute(el, key, &self.to_string());
}
*prev_value = self;
}
}
)*
}
}
render_primitive![
usize,
u8,
u16,
u32,
u64,
u128,
isize,
i8,
i16,
i32,
i64,
i128,
f32,
f64,
char,
IpAddr,
SocketAddr,
SocketAddrV4,
SocketAddrV6,
Ipv4Addr,
Ipv6Addr,
NonZeroI8,
NonZeroU8,
NonZeroI16,
NonZeroU16,
NonZeroI32,
NonZeroU32,
NonZeroI64,
NonZeroU64,
NonZeroI128,
NonZeroU128,
NonZeroIsize,
NonZeroUsize,
];