use crate::{use_navigate, use_resolved_path, ToHref};
use leptos::*;
use std::{error::Error, rc::Rc};
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
#[component]
pub fn Form<A>(
cx: Scope,
#[prop(optional)]
method: Option<&'static str>,
action: A,
#[prop(optional)]
enctype: Option<String>,
#[prop(optional)]
version: Option<RwSignal<usize>>,
#[prop(optional)]
error: Option<RwSignal<Option<Box<dyn Error>>>>,
#[prop(optional)]
#[allow(clippy::type_complexity)]
on_form_data: Option<Rc<dyn Fn(&web_sys::FormData)>>,
#[prop(optional)]
#[allow(clippy::type_complexity)]
on_response: Option<Rc<dyn Fn(&web_sys::Response)>>,
children: Box<dyn FnOnce(Scope) -> Fragment>,
) -> impl IntoView
where
A: ToHref + 'static,
{
let action_version = version;
let action = use_resolved_path(cx, move || action.to_href()());
let on_submit = move |ev: web_sys::SubmitEvent| {
if ev.default_prevented() {
return;
}
let navigate = use_navigate(cx);
let (form, method, action, enctype) = extract_form_attributes(&ev);
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())
.get()
.unwrap_or_default();
if method == "post" {
ev.prevent_default();
let on_response = on_response.clone();
spawn_local(async move {
let res = gloo_net::http::Request::post(&action)
.header("Accept", "application/json")
.header("Content-Type", &enctype)
.body(params)
.send()
.await;
match res {
Err(e) => {
log::error!("<Form/> error while POSTing: {e:#?}");
if let Some(error) = error {
error.set(Some(Box::new(e)));
}
}
Ok(resp) => {
if let Some(version) = action_version {
version.update(|n| *n += 1);
}
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") {
_ = navigate(&redirect_url, Default::default());
}
}
}
}
});
}
else {
let params = params.to_string().as_string().unwrap_or_default();
if navigate(&format!("{action}?{params}"), Default::default()).is_ok() {
ev.prevent_default();
}
}
};
let method = method.unwrap_or("get");
view! { cx,
<form
method=method
action=move || action.get()
enctype=enctype
on:submit=on_submit
>
{children(cx)}
</form>
}
}
#[component]
pub fn ActionForm<I, O>(
cx: Scope,
action: Action<I, Result<O, ServerFnError>>,
children: Box<dyn FnOnce(Scope) -> Fragment>,
) -> impl IntoView
where
I: Clone + ServerFn + 'static,
O: Clone + Serializable + 'static,
{
let action_url = if let Some(url) = action.url() {
url
} else {
debug_warn!("<ActionForm/> action needs a URL. Either use create_server_action() or Action::using_server_fn().");
String::new()
};
let version = action.version();
let value = action.value();
let input = action.input();
let on_form_data = Rc::new(move |form_data: &web_sys::FormData| {
let data = action_input_from_form_data(form_data);
match data {
Ok(data) => {
input.set(Some(data));
action.set_pending(true);
}
Err(e) => log::error!("{e}"),
}
});
let on_response = Rc::new(move |resp: &web_sys::Response| {
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) => {
match O::from_json(
&json.as_string().expect("couldn't get String from JsString"),
) {
Ok(res) => value.set(Some(Ok(res))),
Err(e) => {
value.set(Some(Err(ServerFnError::Deserialization(e.to_string()))))
}
}
}
Err(e) => log::error!("{e:?}"),
};
input.set(None);
action.set_pending(false);
});
});
Form(
cx,
FormProps::builder()
.action(action_url)
.version(version)
.on_form_data(on_form_data)
.on_response(on_response)
.method("post")
.children(children)
.build(),
)
}
#[component]
pub fn MultiActionForm<I, O>(
cx: Scope,
action: MultiAction<I, Result<O, ServerFnError>>,
children: Box<dyn FnOnce(Scope) -> Fragment>,
) -> impl IntoView
where
I: Clone + ServerFn + 'static,
O: Clone + Serializable + 'static,
{
let multi_action = action;
let action = if let Some(url) = multi_action.url() {
url
} else {
debug_warn!("<MultiActionForm/> action needs a URL. Either use create_server_action() or Action::using_server_fn().");
String::new()
};
let on_submit = move |ev: web_sys::SubmitEvent| {
if ev.default_prevented() {
return;
}
let (form, _, _, _) = extract_form_attributes(&ev);
let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw();
let data = action_input_from_form_data(&form_data);
match data {
Err(e) => log::error!("{e}"),
Ok(input) => {
ev.prevent_default();
multi_action.dispatch(input);
}
}
};
view! { cx,
<form
method="POST"
action=action
on:submit=on_submit
>
{children(cx)}
</form>
}
}
fn extract_form_attributes(
ev: &web_sys::Event,
) -> (web_sys::HtmlFormElement, String, String, String) {
let submitter = ev.unchecked_ref::<web_sys::SubmitEvent>().submitter();
match &submitter {
Some(el) => {
if let Some(form) = el.dyn_ref::<web_sys::HtmlFormElement>() {
(
form.clone(),
form.get_attribute("method")
.unwrap_or_else(|| "get".to_string())
.to_lowercase(),
form.get_attribute("action")
.unwrap_or_default()
.to_lowercase(),
form.get_attribute("enctype")
.unwrap_or_else(|| "application/x-www-form-urlencoded".to_string())
.to_lowercase(),
)
} else if let Some(input) = el.dyn_ref::<web_sys::HtmlInputElement>() {
let form = ev
.target()
.unwrap()
.unchecked_into::<web_sys::HtmlFormElement>();
(
form.clone(),
input.get_attribute("method").unwrap_or_else(|| {
form.get_attribute("method")
.unwrap_or_else(|| "get".to_string())
.to_lowercase()
}),
input.get_attribute("action").unwrap_or_else(|| {
form.get_attribute("action")
.unwrap_or_default()
.to_lowercase()
}),
input.get_attribute("enctype").unwrap_or_else(|| {
form.get_attribute("enctype")
.unwrap_or_else(|| "application/x-www-form-urlencoded".to_string())
.to_lowercase()
}),
)
} else if let Some(button) = el.dyn_ref::<web_sys::HtmlButtonElement>() {
let form = ev
.target()
.unwrap()
.unchecked_into::<web_sys::HtmlFormElement>();
(
form.clone(),
button.get_attribute("method").unwrap_or_else(|| {
form.get_attribute("method")
.unwrap_or_else(|| "get".to_string())
.to_lowercase()
}),
button.get_attribute("action").unwrap_or_else(|| {
form.get_attribute("action")
.unwrap_or_default()
.to_lowercase()
}),
button.get_attribute("enctype").unwrap_or_else(|| {
form.get_attribute("enctype")
.unwrap_or_else(|| "application/x-www-form-urlencoded".to_string())
.to_lowercase()
}),
)
} else {
leptos_dom::debug_warn!("<Form/> cannot be submitted from a tag other than <form>, <input>, or <button>");
panic!()
}
}
None => match ev.target() {
None => {
leptos_dom::debug_warn!("<Form/> SubmitEvent fired without a target.");
panic!()
}
Some(form) => {
let form = form.unchecked_into::<web_sys::HtmlFormElement>();
(
form.clone(),
form.get_attribute("method")
.unwrap_or_else(|| "get".to_string()),
form.get_attribute("action").unwrap_or_default(),
form.get_attribute("enctype")
.unwrap_or_else(|| "application/x-www-form-urlencoded".to_string()),
)
}
},
}
}
fn action_input_from_form_data<I: serde::de::DeserializeOwned>(
form_data: &web_sys::FormData,
) -> Result<I, serde_urlencoded::de::Error> {
let data = web_sys::UrlSearchParams::new_with_str_sequence_sequence(form_data).unwrap_throw();
let data = data.to_string().as_string().unwrap_or_default();
serde_urlencoded::from_str::<I>(&data)
}