From f5c007df7b76bbde4025f0da0ff03ab587460dec Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Sun, 7 Jan 2024 19:05:35 -0500 Subject: [PATCH] use server fns directly in ActionForm and MultiActionForm --- examples/todo_app_sqlite_axum/Todos.db | Bin 16384 -> 16384 bytes examples/todo_app_sqlite_axum/src/todo.rs | 2 - router/src/components/form.rs | 264 +++++----------------- router/src/components/router.rs | 23 +- server_fn/src/request/axum.rs | 1 - server_fn_macro/src/lib.rs | 1 - 6 files changed, 81 insertions(+), 210 deletions(-) diff --git a/examples/todo_app_sqlite_axum/Todos.db b/examples/todo_app_sqlite_axum/Todos.db index 3c695e04da231da42f617ca414e350f9ce9b1441..ec85d2b07f9ac6b3b931f4599e3e7a35107050f5 100644 GIT binary patch delta 133 zcmZo@U~Fh$oFL8UK2gS*(S2jWB7T`q49xsb82Eqkzvh3!|B3%D|91ZMFt}OJU?RU4 zn Result<(), ServerFnError> { pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> { let mut conn = db().await?; - leptos_axum::redirect("/foo"); Ok(sqlx::query("DELETE FROM todos WHERE id = $1") .bind(id) .execute(&mut conn) @@ -90,7 +89,6 @@ pub fn TodoApp() -> impl IntoView { //let id = use_context::(); provide_meta_context(); view! { - diff --git a/router/src/components/form.rs b/router/src/components/form.rs index 048951aaa..f16d22324 100644 --- a/router/src/components/form.rs +++ b/router/src/components/form.rs @@ -17,9 +17,10 @@ use leptos::{ use send_wrapper::SendWrapper; use serde::{de::DeserializeOwned, Serialize}; use std::{error::Error, fmt::Debug, rc::Rc}; -use wasm_bindgen::{JsCast, UnwrapThrowExt}; +use thiserror::Error; +use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt}; use web_sys::{ - FormData, HtmlButtonElement, HtmlFormElement, HtmlInputElement, + Event, FormData, HtmlButtonElement, HtmlFormElement, HtmlInputElement, RequestRedirect, SubmitEvent, }; @@ -436,17 +437,11 @@ pub fn ActionForm( /// Sets the `class` attribute on the underlying `
` tag, making it easier to style. #[prop(optional, into)] class: Option, - /// A signal that will be set if the form submission ends in an error. - #[prop(optional)] - error: Option>>>, /// A [`NodeRef`] in which the `` element should be stored. #[prop(optional)] node_ref: Option>, - /// Sets whether the page should be scrolled to the top when navigating. - #[prop(optional)] - noscroll: bool, /// Arbitrary attributes to add to the `` - #[prop(optional, into)] + #[prop(attrs, optional)] attributes: Vec<(&'static str, Attribute)>, /// Component children; should include the HTML of the form elements. children: Children, @@ -454,14 +449,16 @@ pub fn ActionForm( where ServFn: Clone + DeserializeOwned + ServerFn + 'static, - ServerFnError: Debug + Clone, - ServFn::Output: Debug, - ServFn::Error: Debug + 'static, <>::Request as ClientReq< ServFn::Error, >>::FormData: From, { let has_router = has_router(); + if !has_router { + _ = server_fn::redirect::set_redirect_hook(|path: &str| { + _ = window().location().set_href(path); + }); + } let action_url = if let Some(url) = action.url() { url } else { @@ -486,76 +483,18 @@ where ev.prevent_default(); - let navigate = has_router.then(use_navigate); - let navigate_options = SendWrapper::new(NavigateOptions { - scroll: !noscroll, - ..Default::default() - }); - let redirect_hook = navigate.map(|navigate| { - let navigate = SendWrapper::new(navigate); - Box::new(move |path: &str| { - let path = path.to_string(); - // delay by a tick here, so that the Action updates *before* the redirect - request_animation_frame({ - let navigate = navigate.clone(); - let navigate_options = navigate_options.clone(); - move || { - navigate(&path, navigate_options.take()); - } - }); - }) as RedirectHook - }); - - let form = - form_from_event(&ev).expect("couldn't find form submitter"); - let form_data = FormData::new_with_form(&form).unwrap(); - match ServFn::from_form_data(&form_data) { + match ServFn::from_event(&ev) { Ok(new_input) => { - input.try_set(Some(new_input)); + action.dispatch(new_input); } Err(err) => { - if let Some(error) = error { - error.set(Some(Box::new(err))); - } - } - } - let req = <>::Request as ClientReq< - ServFn::Error, - >>::try_new_post_form_data( - &action_url, - ServFn::OutputEncoding::CONTENT_TYPE, - ServFn::InputEncoding::CONTENT_TYPE, - form_data.into(), - ); - match req { - Ok(req) => { - action.set_pending(true); - spawn_local(async move { - let res = ::run_on_client_with_req( - req, - redirect_hook.as_ref(), - ) - .await; - batch(move || { - version.update(|n| *n += 1); - action.set_pending(false); - match res { - Ok(res) => { - value.try_set(Some(Ok(res))); - } - Err(err) => { - value.set(Some(Err(err.clone()))); - if let Some(error) = error { - error.set(Some(Box::new( - ServerFnErrorErr::from(err), - ))); - } - } - } - }); + batch(move || { + value.set(Some(Err(ServerFnError::Serialization( + err.to_string(), + )))); + version.update(|n| *n += 1); }); } - Err(_) => todo!(), } } }; @@ -573,106 +512,6 @@ where action_form = action_form.attr(attr_name, attr_value); } action_form - - /* let on_error = Rc::new(move |e: &gloo_net::Error| { - batch(move || { - action.set_pending(false); - let e = ServerFnError::Request(e.to_string()); - value.try_set(Some(Err(e.clone()))); - if let Some(error) = error { - error.try_set(Some(Box::new(ServerFnErrorErr::from(e)))); - } - }); - }); - - let on_form_data = Rc::new(move |form_data: &web_sys::FormData| { - let data = I::from_form_data(form_data); - match data { - Ok(data) => { - batch(move || { - input.try_set(Some(data)); - action.set_pending(true); - }); - } - Err(e) => { - error!("{e}"); - let e = ServerFnError::Serialization(e.to_string()); - batch(move || { - value.try_set(Some(Err(e.clone()))); - if let Some(error) = error { - error - .try_set(Some(Box::new(ServerFnErrorErr::from(e)))); - } - }); - } - } - }); - - let on_response = Rc::new(move |resp: &web_sys::Response| { - let resp = resp.clone().expect("couldn't get Response"); - - // If the response was redirected then a JSON will not be available in the response, instead - // it will be an actual page, so we don't want to try to parse it. - if resp.redirected() { - return; - } - - spawn_local(async move { - let body = JsFuture::from( - resp.text().expect("couldn't get .text() from Response"), - ) - .await; - let status = resp.status(); - match body { - Ok(json) => { - let json = json - .as_string() - .expect("couldn't get String from JsString"); - if (400..=599).contains(&status) { - let res = ServerFnError::::de(&json); - value.try_set(Some(Err(res))); - if let Some(error) = error { - error.try_set(None); - } - } else { - match serde_json::from_str::(&json) { - Ok(res) => { - value.try_set(Some(Ok(res))); - if let Some(error) = error { - error.try_set(None); - } - } - Err(e) => { - value.try_set(Some(Err( - ServerFnError::Deserialization( - e.to_string(), - ), - ))); - if let Some(error) = error { - error.try_set(Some(Box::new(e))); - } - } - } - } - } - Err(e) => { - error!("{e:?}"); - // TODO - /* if let Some(error) = error { - error.try_set(Some(Box::new( - ServerFnErrorErr::Request( - e.as_string().unwrap_or_default(), - ), - ))); - }*/ - } - }; - batch(move || { - input.try_set(None); - action.set_pending(false); - }); - }); - });*/ } /// Automatically turns a server [MultiAction](leptos_server::MultiAction) into an HTML @@ -683,11 +522,11 @@ where tracing::instrument(level = "trace", skip_all,) )] #[component] -pub fn MultiActionForm( +pub fn MultiActionForm( /// The action from which to build the form. This should include a URL, which can be generated /// by default using [create_server_action](leptos_server::create_server_action) or added /// manually using [leptos_server::Action::using_server_fn]. - action: MultiAction>, + action: MultiAction>, /// Sets the `class` attribute on the underlying `` tag, making it easier to style. #[prop(optional, into)] class: Option, @@ -698,17 +537,25 @@ pub fn MultiActionForm( #[prop(optional)] node_ref: Option>, /// Arbitrary attributes to add to the `` - #[prop(optional, into)] + #[prop(attrs, optional)] attributes: Vec<(&'static str, Attribute)>, /// Component children; should include the HTML of the form elements. children: Children, ) -> impl IntoView where - I: Clone + ServerFn + DeserializeOwned + 'static, - O: Clone + Serializable + 'static, + ServFn: + Clone + DeserializeOwned + ServerFn + 'static, + <>::Request as ClientReq< + ServFn::Error, + >>::FormData: From, { - let multi_action = action; - let action = if let Some(url) = multi_action.url() { + let has_router = has_router(); + if !has_router { + _ = server_fn::redirect::set_redirect_hook(|path: &str| { + _ = window().location().set_href(path); + }); + } + let action_url = if let Some(url) = action.url() { url } else { debug_warn!( @@ -718,22 +565,21 @@ where String::new() }; - let on_submit = move |ev: web_sys::SubmitEvent| { + let on_submit = move |ev: SubmitEvent| { if ev.default_prevented() { return; } - match I::from_event(&ev) { + ev.prevent_default(); + + match ServFn::from_event(&ev) { Err(e) => { - error!("{e}"); if let Some(error) = error { error.try_set(Some(Box::new(e))); } } Ok(input) => { - ev.prevent_default(); - ev.stop_propagation(); - multi_action.dispatch(input); + action.dispatch(input); if let Some(error) = error { error.try_set(None); } @@ -742,19 +588,19 @@ where }; let class = class.map(|bx| bx.into_attribute_boxed()); - let mut form = form() - .attr("method", "POST") - .attr("action", action) - .on(ev::submit, on_submit) + let mut action_form = form() + .attr("action", action_url) + .attr("method", "post") .attr("class", class) + .on(ev::submit, on_submit) .child(children()); if let Some(node_ref) = node_ref { - form = form.node_ref(node_ref) + action_form = action_form.node_ref(node_ref) }; for (attr_name, attr_value) in attributes { - form = form.attr(attr_name, attr_value); + action_form = action_form.attr(attr_name, attr_value); } - form + action_form } fn form_from_event(ev: &SubmitEvent) -> Option { @@ -892,7 +738,7 @@ where Self: Sized + serde::de::DeserializeOwned, { /// Tries to deserialize the data, given only the `submit` event. - fn from_event(ev: &web_sys::Event) -> Result; + fn from_event(ev: &web_sys::Event) -> Result; /// Tries to deserialize the data, given the actual form data. fn from_form_data( @@ -900,6 +746,16 @@ where ) -> Result; } +#[derive(Error, Debug)] +pub enum FromFormDataError { + #[error("Could not find connected to event.")] + MissingForm(Event), + #[error("Could not create FormData from : {0:?}")] + FormData(JsValue), + #[error("Deserialization error: {0:?}")] + Deserialization(serde_qs::Error), +} + impl FromFormData for T where T: serde::de::DeserializeOwned, @@ -908,13 +764,15 @@ where any(debug_assertions, feature = "ssr"), tracing::instrument(level = "trace", skip_all,) )] - fn from_event(ev: &web_sys::Event) -> Result { - let (form, _, _, _) = extract_form_attributes(ev); - - let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw(); - + fn from_event(ev: &Event) -> Result { + let form = form_from_event(ev.unchecked_ref()) + .ok_or_else(|| FromFormDataError::MissingForm(ev.clone()))?; + let form_data = FormData::new_with_form(&form) + .map_err(FromFormDataError::FormData)?; Self::from_form_data(&form_data) + .map_err(FromFormDataError::Deserialization) } + #[cfg_attr( any(debug_assertions, feature = "ssr"), tracing::instrument(level = "trace", skip_all,) diff --git a/router/src/components/router.rs b/router/src/components/router.rs index 023882130..dbcf54505 100644 --- a/router/src/components/router.rs +++ b/router/src/components/router.rs @@ -1,13 +1,15 @@ use crate::{ - create_location, matching::resolve_path, scroll_to_el, Branch, History, - Location, LocationChange, RouteContext, RouterIntegrationContext, State, + create_location, matching::resolve_path, scroll_to_el, use_navigate, + Branch, History, Location, LocationChange, RouteContext, + RouterIntegrationContext, State, }; #[cfg(not(feature = "ssr"))] use crate::{unescape, Url}; use cfg_if::cfg_if; -use leptos::*; +use leptos::{server_fn::redirect::RedirectHook, *}; #[cfg(feature = "transition")] use leptos_reactive::use_transition; +use send_wrapper::SendWrapper; use std::{ cell::RefCell, rc::Rc, @@ -45,6 +47,21 @@ pub fn Router( provide_context(SetIsRouting(set_is_routing)); } + // set server function redirect hook + let navigate = use_navigate(); + let navigate = SendWrapper::new(navigate); + let router_hook = Box::new(move |path: &str| { + let path = path.to_string(); + // delay by a tick here, so that the Action updates *before* the redirect + request_animation_frame({ + let navigate = navigate.clone(); + move || { + navigate(&path, Default::default()); + } + }); + }) as RedirectHook; + server_fn::redirect::set_redirect_hook(router_hook); + children() } diff --git a/server_fn/src/request/axum.rs b/server_fn/src/request/axum.rs index 696f01e8f..799c82ecc 100644 --- a/server_fn/src/request/axum.rs +++ b/server_fn/src/request/axum.rs @@ -25,7 +25,6 @@ impl Req for Request { } async fn try_into_string(self) -> Result> { - println!("accepts = {:?}", self.headers().get(http::header::ACCEPT)); let bytes = self.try_into_bytes().await?; String::from_utf8(bytes.to_vec()) .map_err(|e| ServerFnError::Deserialization(e.to_string())) diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index b797fee2d..e6e90486c 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -235,7 +235,6 @@ pub fn server_macro_impl( #struct_name::PATH, <#struct_name as ServerFn>::InputEncoding::METHOD, |req| { - println!("running {:?}", stringify!(#struct_name)); Box::pin(#struct_name::run_on_server(req)) }, #struct_name::middlewares