Correctly set `value` and `input` when using `<ActionForm/>` so we can do real optimistic UI (see issue #51)
This commit is contained in:
parent
c17c6549cf
commit
77504de8f1
|
@ -240,8 +240,6 @@ pub async fn call_server_fn<T>(url: &str, args: impl ServerFn) -> Result<T, Serv
|
||||||
where
|
where
|
||||||
T: Serializable + Sized,
|
T: Serializable + Sized,
|
||||||
{
|
{
|
||||||
use leptos_dom::*;
|
|
||||||
|
|
||||||
let args_form_data = serde_urlencoded::to_string(&args)
|
let args_form_data = serde_urlencoded::to_string(&args)
|
||||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
||||||
|
|
||||||
|
@ -391,11 +389,29 @@ where
|
||||||
self.input.read_only()
|
self.input.read_only()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The argument that was dispatched to 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 input
|
||||||
|
/// based on its internal logic.
|
||||||
|
pub fn set_input(&self, value: I) {
|
||||||
|
self.input.set(Some(value));
|
||||||
|
}
|
||||||
|
|
||||||
/// The most recent return value of the `async` function.
|
/// The most recent return value of the `async` function.
|
||||||
pub fn value(&self) -> ReadSignal<Option<O>> {
|
pub fn value(&self) -> ReadSignal<Option<O>> {
|
||||||
self.value.read_only()
|
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.)
|
/// The URL associated with the action (typically as part of a server function.)
|
||||||
/// This enables integration with the `ActionForm` component in `leptos_router`.
|
/// This enables integration with the `ActionForm` component in `leptos_router`.
|
||||||
pub fn url(&self) -> Option<&str> {
|
pub fn url(&self) -> Option<&str> {
|
||||||
|
|
|
@ -22,6 +22,7 @@ url = { version = "2", optional = true }
|
||||||
urlencoding = "2"
|
urlencoding = "2"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
typed-builder = "0.10"
|
typed-builder = "0.10"
|
||||||
|
serde_urlencoded = "0.7"
|
||||||
js-sys = { version = "0.3" }
|
js-sys = { version = "0.3" }
|
||||||
wasm-bindgen = { version = "0.2" }
|
wasm-bindgen = { version = "0.2" }
|
||||||
wasm-bindgen-futures = { version = "0.4" }
|
wasm-bindgen-futures = { version = "0.4" }
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
use std::error::Error;
|
use crate::{use_navigate, use_resolved_path, ToHref};
|
||||||
|
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
|
use std::{error::Error, rc::Rc};
|
||||||
use typed_builder::TypedBuilder;
|
use typed_builder::TypedBuilder;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
|
use wasm_bindgen_futures::JsFuture;
|
||||||
use crate::{use_navigate, use_resolved_path, ToHref};
|
|
||||||
|
|
||||||
/// Properties that can be passed to the [Form] component, which is an HTML
|
/// 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)
|
/// [`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.
|
/// A signal that will be set if the form submission ends in an error.
|
||||||
#[builder(default, setter(strip_option))]
|
#[builder(default, setter(strip_option))]
|
||||||
pub error: Option<RwSignal<Option<Box<dyn Error>>>>,
|
pub error: Option<RwSignal<Option<Box<dyn Error>>>>,
|
||||||
|
/// 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<Rc<dyn Fn(&web_sys::FormData)>>,
|
||||||
|
/// 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<Rc<dyn Fn(&web_sys::Response)>>,
|
||||||
/// Component children; should include the HTML of the form elements.
|
/// Component children; should include the HTML of the form elements.
|
||||||
pub children: Box<dyn Fn() -> Vec<Element>>,
|
pub children: Box<dyn Fn() -> Vec<Element>>,
|
||||||
}
|
}
|
||||||
|
@ -51,6 +57,8 @@ where
|
||||||
children,
|
children,
|
||||||
version,
|
version,
|
||||||
error,
|
error,
|
||||||
|
on_form_data,
|
||||||
|
on_response,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
let action_version = version;
|
let action_version = version;
|
||||||
|
@ -150,6 +158,9 @@ where
|
||||||
};
|
};
|
||||||
|
|
||||||
let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw();
|
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 =
|
let params =
|
||||||
web_sys::UrlSearchParams::new_with_str_sequence_sequence(&form_data).unwrap_throw();
|
web_sys::UrlSearchParams::new_with_str_sequence_sequence(&form_data).unwrap_throw();
|
||||||
let action = use_resolved_path(cx, move || action.clone())
|
let action = use_resolved_path(cx, move || action.clone())
|
||||||
|
@ -157,6 +168,7 @@ where
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
// POST
|
// POST
|
||||||
if method == "post" {
|
if method == "post" {
|
||||||
|
let on_response = on_response.clone();
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
let res = gloo_net::http::Request::post(&action)
|
let res = gloo_net::http::Request::post(&action)
|
||||||
.header("Accept", "application/json")
|
.header("Accept", "application/json")
|
||||||
|
@ -178,6 +190,9 @@ where
|
||||||
if let Some(error) = error {
|
if let Some(error) = error {
|
||||||
error.set(None);
|
error.set(None);
|
||||||
}
|
}
|
||||||
|
if let Some(on_response) = on_response.clone() {
|
||||||
|
on_response(&resp.as_raw());
|
||||||
|
}
|
||||||
|
|
||||||
if resp.status() == 303 {
|
if resp.status() == 303 {
|
||||||
if let Some(redirect_url) = resp.headers().get("Location") {
|
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
|
/// 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
|
/// by default using [create_server_action](leptos_server::create_server_action) or added
|
||||||
/// manually using [leptos_server::Action::using_server_fn].
|
/// manually using [leptos_server::Action::using_server_fn].
|
||||||
pub action: Action<I, O>,
|
pub action: Action<I, Result<O, ServerFnError>>,
|
||||||
/// Component children; should include the HTML of the form elements.
|
/// Component children; should include the HTML of the form elements.
|
||||||
pub children: Box<dyn Fn() -> Vec<Element>>,
|
pub children: Box<dyn Fn() -> Vec<Element>>,
|
||||||
}
|
}
|
||||||
|
@ -233,8 +248,8 @@ where
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn ActionForm<I, O>(cx: Scope, props: ActionFormProps<I, O>) -> Element
|
pub fn ActionForm<I, O>(cx: Scope, props: ActionFormProps<I, O>) -> Element
|
||||||
where
|
where
|
||||||
I: 'static,
|
I: Clone + ServerFn + 'static,
|
||||||
O: 'static,
|
O: Clone + Serializable + 'static,
|
||||||
{
|
{
|
||||||
let action = if let Some(url) = props.action.url() {
|
let action = if let Some(url) = props.action.url() {
|
||||||
url
|
url
|
||||||
|
@ -244,11 +259,57 @@ where
|
||||||
}.to_string();
|
}.to_string();
|
||||||
let version = props.action.version;
|
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::<I>(&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::<O>()
|
||||||
|
);
|
||||||
|
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(
|
Form(
|
||||||
cx,
|
cx,
|
||||||
FormProps::builder()
|
FormProps::builder()
|
||||||
.action(action)
|
.action(action)
|
||||||
.version(version)
|
.version(version)
|
||||||
|
.on_form_data(on_form_data)
|
||||||
|
.on_response(on_response)
|
||||||
.method("post")
|
.method("post")
|
||||||
.children(props.children)
|
.children(props.children)
|
||||||
.build(),
|
.build(),
|
||||||
|
|
Loading…
Reference in New Issue