From 77504de8f1931ea0c251d1c75f9363f619992171 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Wed, 16 Nov 2022 20:16:21 -0500 Subject: [PATCH] Correctly set `value` and `input` when using `` so we can do real optimistic UI (see issue #51) --- leptos_server/src/lib.rs | 20 +++++++++- router/Cargo.toml | 1 + router/src/components/form.rs | 75 +++++++++++++++++++++++++++++++---- 3 files changed, 87 insertions(+), 9 deletions(-) diff --git a/leptos_server/src/lib.rs b/leptos_server/src/lib.rs index e0748701e..f6b5edf10 100644 --- a/leptos_server/src/lib.rs +++ b/leptos_server/src/lib.rs @@ -240,8 +240,6 @@ pub async fn call_server_fn(url: &str, args: impl ServerFn) -> Result ReadSignal> { self.value.read_only() } + /// Sets the most recent return value of the `async` function. + /// + /// You probably don't need to call this unless you are implementing a form + /// or some other kind of wrapper for an action and need to set the value + /// based on its internal logic. + pub fn set_value(&self, value: O) { + self.value.set(Some(value)); + } + /// The URL associated with the action (typically as part of a server function.) /// This enables integration with the `ActionForm` component in `leptos_router`. pub fn url(&self) -> Option<&str> { diff --git a/router/Cargo.toml b/router/Cargo.toml index 95cf8444e..5d7e716f1 100644 --- a/router/Cargo.toml +++ b/router/Cargo.toml @@ -22,6 +22,7 @@ url = { version = "2", optional = true } urlencoding = "2" thiserror = "1" typed-builder = "0.10" +serde_urlencoded = "0.7" js-sys = { version = "0.3" } wasm-bindgen = { version = "0.2" } wasm-bindgen-futures = { version = "0.4" } diff --git a/router/src/components/form.rs b/router/src/components/form.rs index 20fee44ac..bb61532b3 100644 --- a/router/src/components/form.rs +++ b/router/src/components/form.rs @@ -1,10 +1,9 @@ -use std::error::Error; - +use crate::{use_navigate, use_resolved_path, ToHref}; use leptos::*; +use std::{error::Error, rc::Rc}; use typed_builder::TypedBuilder; use wasm_bindgen::JsCast; - -use crate::{use_navigate, use_resolved_path, ToHref}; +use wasm_bindgen_futures::JsFuture; /// Properties that can be passed to the [Form] component, which is an HTML /// [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) @@ -33,6 +32,13 @@ where /// A signal that will be set if the form submission ends in an error. #[builder(default, setter(strip_option))] pub error: Option>>>, + /// A callback will be called with the [FormData](web_sys::FormData) when the form is submitted. + #[builder(default, setter(strip_option))] + pub on_form_data: Option>, + /// A callback will be called with the [Response](web_sys::Response) the server sends in response + /// to a form submission. + #[builder(default, setter(strip_option))] + pub on_response: Option>, /// Component children; should include the HTML of the form elements. pub children: Box Vec>, } @@ -51,6 +57,8 @@ where children, version, error, + on_form_data, + on_response, } = props; let action_version = version; @@ -150,6 +158,9 @@ where }; let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw(); + if let Some(on_form_data) = on_form_data.clone() { + on_form_data(&form_data); + } let params = web_sys::UrlSearchParams::new_with_str_sequence_sequence(&form_data).unwrap_throw(); let action = use_resolved_path(cx, move || action.clone()) @@ -157,6 +168,7 @@ where .unwrap_or_default(); // POST if method == "post" { + let on_response = on_response.clone(); spawn_local(async move { let res = gloo_net::http::Request::post(&action) .header("Accept", "application/json") @@ -178,6 +190,9 @@ where if let Some(error) = error { error.set(None); } + if let Some(on_response) = on_response.clone() { + on_response(&resp.as_raw()); + } if resp.status() == 303 { if let Some(redirect_url) = resp.headers().get("Location") { @@ -222,7 +237,7 @@ where /// 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]. - pub action: Action, + pub action: Action>, /// Component children; should include the HTML of the form elements. pub children: Box Vec>, } @@ -233,8 +248,8 @@ where #[allow(non_snake_case)] pub fn ActionForm(cx: Scope, props: ActionFormProps) -> Element where - I: 'static, - O: 'static, + I: Clone + ServerFn + 'static, + O: Clone + Serializable + 'static, { let action = if let Some(url) = props.action.url() { url @@ -244,11 +259,57 @@ where }.to_string(); let version = props.action.version; + let on_form_data = { + let action = props.action.clone(); + Rc::new(move |form_data: &web_sys::FormData| { + let data = + web_sys::UrlSearchParams::new_with_str_sequence_sequence(&form_data).unwrap_throw(); + let data = data.to_string().as_string().unwrap_or_default(); + let data = serde_urlencoded::from_str::(&data); + match data { + Ok(data) => action.set_input(data), + Err(e) => log::error!("{e}"), + } + }) + }; + + let on_response = { + let action = props.action.clone(); + Rc::new(move |resp: &web_sys::Response| { + let action = action.clone(); + let resp = resp.clone().expect("couldn't get Response"); + spawn_local(async move { + let body = + JsFuture::from(resp.text().expect("couldn't get .text() from Response")).await; + match body { + Ok(json) => { + log::debug!( + "body is {:?}\nO is {:?}", + json.as_string().unwrap(), + std::any::type_name::() + ); + match O::from_json( + &json.as_string().expect("couldn't get String from JsString"), + ) { + Ok(res) => action.set_value(Ok(res)), + Err(e) => { + action.set_value(Err(ServerFnError::Deserialization(e.to_string()))) + } + } + } + Err(e) => log::error!("{e:?}"), + } + }); + }) + }; + Form( cx, FormProps::builder() .action(action) .version(version) + .on_form_data(on_form_data) + .on_response(on_response) .method("post") .children(props.children) .build(),