mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 14:54:16 +00:00
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
3 changed files with 87 additions and 9 deletions
|
@ -240,8 +240,6 @@ pub async fn call_server_fn<T>(url: &str, args: impl ServerFn) -> Result<T, Serv
|
|||
where
|
||||
T: Serializable + Sized,
|
||||
{
|
||||
use leptos_dom::*;
|
||||
|
||||
let args_form_data = serde_urlencoded::to_string(&args)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
||||
|
||||
|
@ -391,11 +389,29 @@ where
|
|||
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.
|
||||
pub fn value(&self) -> ReadSignal<Option<O>> {
|
||||
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> {
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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<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.
|
||||
pub children: Box<dyn Fn() -> Vec<Element>>,
|
||||
}
|
||||
|
@ -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<I, O>,
|
||||
pub action: Action<I, Result<O, ServerFnError>>,
|
||||
/// Component children; should include the HTML of the form elements.
|
||||
pub children: Box<dyn Fn() -> Vec<Element>>,
|
||||
}
|
||||
|
@ -233,8 +248,8 @@ where
|
|||
#[allow(non_snake_case)]
|
||||
pub fn ActionForm<I, O>(cx: Scope, props: ActionFormProps<I, O>) -> 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::<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(
|
||||
cx,
|
||||
FormProps::builder()
|
||||
.action(action)
|
||||
.version(version)
|
||||
.on_form_data(on_form_data)
|
||||
.on_response(on_response)
|
||||
.method("post")
|
||||
.children(props.children)
|
||||
.build(),
|
||||
|
|
Loading…
Reference in a new issue