From a5ce625c68d5c82b56c5121be8c2ff6290dfb290 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Fri, 5 Jan 2024 17:50:02 -0500 Subject: [PATCH] actually use server functions in `ActionForm` --- examples/todo_app_sqlite/Todos.db | Bin 16384 -> 16384 bytes leptos/Cargo.toml | 5 ++ router/src/components/form.rs | 145 ++++++++++++++++++++++++------ server_fn/src/lib.rs | 18 ++-- server_fn/src/request/browser.rs | 28 +++++- server_fn/src/request/mod.rs | 7 ++ server_fn/src/request/reqwest.rs | 15 ++++ 7 files changed, 185 insertions(+), 33 deletions(-) diff --git a/examples/todo_app_sqlite/Todos.db b/examples/todo_app_sqlite/Todos.db index 9942abb2ddf8f508b6b84b026be4e647088e8ded..75df92c61b2a092a5699ba63b8abfd2f86ea785f 100644 GIT binary patch delta 90 zcmZo@U~Fh$oFL7(XrhcWchc03$~eH~;_u diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index 78f47ad3b5..bbc0ddc4bc 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -74,6 +74,10 @@ experimental-islands = [ "dep:serde", "dep:serde_json", ] +trace-component-props = [ + "leptos_dom/trace-component-props", + "leptos_macro/trace-component-props" +] [package.metadata.cargo-all-features] denylist = [ @@ -83,6 +87,7 @@ denylist = [ "rustls", "default-tls", "wasm-bindgen", + "trace-component-props" ] skip_feature_sets = [ [ diff --git a/router/src/components/form.rs b/router/src/components/form.rs index c34a33d6c1..27c1723770 100644 --- a/router/src/components/form.rs +++ b/router/src/components/form.rs @@ -5,14 +5,22 @@ use crate::{ use leptos::{ html::form, logging::*, - server_fn::{error::ServerFnErrorSerde, ServerFn}, + server_fn::{ + client::Client, + codec::{Encoding, PostUrl}, + request::ClientReq, + ServerFn, + }, *, }; use serde::{de::DeserializeOwned, Serialize}; use std::{error::Error, fmt::Debug, rc::Rc}; use wasm_bindgen::{JsCast, UnwrapThrowExt}; use wasm_bindgen_futures::JsFuture; -use web_sys::RequestRedirect; +use web_sys::{ + FormData, HtmlButtonElement, HtmlFormElement, HtmlInputElement, + RequestRedirect, SubmitEvent, +}; type OnFormData = Rc; type OnResponse = Rc; @@ -416,11 +424,14 @@ fn current_window_origin() -> String { tracing::instrument(level = "trace", skip_all,) )] #[component] -pub fn ActionForm( +pub fn ActionForm( /// The action from which to build the form. This should include a URL, which can be generated /// by default using [`create_server_action`](l:eptos_server::create_server_action) or added /// manually using [`using_server_fn`](leptos_server::Action::using_server_fn). - action: Action>>, + action: Action< + ServFn, + Result>, + >, /// Sets the `class` attribute on the underlying `
` tag, making it easier to style. #[prop(optional, into)] class: Option, @@ -440,11 +451,15 @@ pub fn ActionForm( children: Children, ) -> impl IntoView where - I: Clone + DeserializeOwned + ServerFn + 'static, - O: Clone + Serialize + DeserializeOwned + 'static, - ServerFnError: Debug + Clone, // Enc: FormDataEncoding, - I::Error: Debug + 'static, + ServFn: + Clone + DeserializeOwned + ServerFn + 'static, + ServerFnError: Debug + Clone, + ServFn::Error: Debug + 'static, + <>::Request as ClientReq< + ServFn::Error, + >>::FormData: From, { + let has_router = has_router(); let action_url = if let Some(url) = action.url() { url } else { @@ -458,7 +473,81 @@ where let value = action.value(); let input = action.input(); - let on_error = Rc::new(move |e: &gloo_net::Error| { + let class = class.map(|bx| bx.into_attribute_boxed()); + + let on_submit = { + let action_url = action_url.clone(); + move |ev: SubmitEvent| { + if ev.default_prevented() { + return; + } + + ev.prevent_default(); + + let navigate = has_router.then(use_navigate); + let navigate_options = NavigateOptions { + scroll: !noscroll, + ..Default::default() + }; + + let form = + form_from_event(&ev).expect("couldn't find form submitter"); + let form_data = FormData::new_with_form(&form).unwrap(); + 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) => { + spawn_local(async move { + // TODO check order of setting things here, and use batch as needed + // TODO set version? + match ::run_on_client_with_req(req) + .await + { + Ok(res) => { + batch(move || { + version.update(|n| *n += 1); + value.try_set(Some(Ok(res))); + }); + } + Err(err) => { + batch(move || { + value.set(Some(Err(err.clone()))); + if let Some(error) = error { + error.set(Some(Box::new( + ServerFnErrorErr::from(err), + ))); + } + }); + } + } + }); + } + Err(_) => todo!(), + } + } + }; + + 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 { + action_form = action_form.node_ref(node_ref) + }; + for (attr_name, attr_value) in attributes { + 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()); @@ -556,24 +645,7 @@ where action.set_pending(false); }); }); - }); - let class = class.map(|bx| bx.into_attribute_boxed()); - - let mut props = FormProps::builder() - .action(action_url) - .version(version) - .on_form_data(on_form_data) - .on_response(on_response) - .on_error(on_error) - .method("post") - .class(class) - .noscroll(noscroll) - .children(children) - .build(); - props.error = error; - props.node_ref = node_ref; - props.attributes = attributes; - Form(props) + });*/ } /// Automatically turns a server [MultiAction](leptos_server::MultiAction) into an HTML @@ -657,6 +729,25 @@ where } form } + +fn form_from_event(ev: &SubmitEvent) -> Option { + let submitter = ev.unchecked_ref::().submitter(); + match &submitter { + Some(el) => { + if let Some(form) = el.dyn_ref::() { + Some(form.clone()) + } else if el.is_instance_of::() + || el.is_instance_of::() + { + Some(ev.target().unwrap().unchecked_into()) + } else { + None + } + } + None => ev.target().map(|form| form.unchecked_into()), + } +} + #[cfg_attr( any(debug_assertions, feature = "ssr"), tracing::instrument(level = "trace", skip_all,) diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index a8a0ee85cc..250ea27a62 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -104,6 +104,15 @@ where // create and send request on client let req = self.into_req(Self::PATH, Self::OutputEncoding::CONTENT_TYPE)?; + Self::run_on_client_with_req(req).await + } + } + + fn run_on_client_with_req( + req: >::Request, + ) -> impl Future>> + Send + { + async move { let res = Self::Client::send(req).await?; let status = res.status(); @@ -122,7 +131,6 @@ where if (300..=399).contains(&status) { redirect::call_redirect_hook(&location); } - res } } @@ -288,11 +296,11 @@ pub mod axum { path: &str, ) -> Option, Response>> { REGISTERED_SERVER_FUNCTIONS.get(path).map(|server_fn| { - //let middleware = (server_fn.middleware)(); + let middleware = (server_fn.middleware)(); let mut service = BoxedService::new(server_fn.clone()); - //for middleware in middleware { - //service = middleware.layer(service); - //} + for middleware in middleware { + service = middleware.layer(service); + } service }) } diff --git a/server_fn/src/request/browser.rs b/server_fn/src/request/browser.rs index 228feee1d8..1bff976688 100644 --- a/server_fn/src/request/browser.rs +++ b/server_fn/src/request/browser.rs @@ -4,7 +4,7 @@ use bytes::Bytes; pub use gloo_net::http::Request; use js_sys::Uint8Array; use send_wrapper::SendWrapper; -use web_sys::FormData; +use web_sys::{FormData, UrlSearchParams}; #[derive(Debug)] pub struct BrowserRequest(pub(crate) SendWrapper); @@ -89,4 +89,30 @@ impl ClientReq for BrowserRequest { .map_err(|e| ServerFnError::Request(e.to_string()))?, ))) } + + fn try_new_post_form_data( + path: &str, + accepts: &str, + content_type: &str, + body: Self::FormData, + ) -> Result> { + let form_data = body.0.take(); + let url_params = + UrlSearchParams::new_with_str_sequence_sequence(&form_data) + .map_err(|e| { + ServerFnError::Serialization(e.as_string().unwrap_or_else( + || { + "Could not serialize FormData to URLSearchParams" + .to_string() + }, + )) + })?; + Ok(Self(SendWrapper::new( + Request::post(path) + .header("Content-Type", content_type) + .header("Accept", accepts) + .body(url_params) + .map_err(|e| ServerFnError::Request(e.to_string()))?, + ))) + } } diff --git a/server_fn/src/request/mod.rs b/server_fn/src/request/mod.rs index a62349eac7..2b55d9ef22 100644 --- a/server_fn/src/request/mod.rs +++ b/server_fn/src/request/mod.rs @@ -40,6 +40,13 @@ where body: Bytes, ) -> Result>; + fn try_new_post_form_data( + path: &str, + accepts: &str, + content_type: &str, + body: Self::FormData, + ) -> Result>; + fn try_new_multipart( path: &str, accepts: &str, diff --git a/server_fn/src/request/reqwest.rs b/server_fn/src/request/reqwest.rs index c1ecb649fe..ced0c05d4c 100644 --- a/server_fn/src/request/reqwest.rs +++ b/server_fn/src/request/reqwest.rs @@ -88,4 +88,19 @@ impl ClientReq for Request { .build() .map_err(|e| ServerFnError::Request(e.to_string())) } + + fn try_new_post_form_data( + path: &str, + accepts: &str, + content_type: &str, + body: Self::FormData, + ) -> Result> { + /*CLIENT + .post(path) + .header(ACCEPT, accepts) + .multipart(body) + .build() + .map_err(|e| ServerFnError::Request(e.to_string()))*/ + todo!() + } }