use crate::{children::Children, component, prelude::*, IntoView};
use leptos_dom::helpers::window;
use leptos_server::{ServerAction, ServerMultiAction};
use serde::de::DeserializeOwned;
use server_fn::{
client::Client,
codec::PostUrl,
error::{IntoAppError, ServerFnErrorErr},
request::ClientReq,
Http, ServerFn,
};
use tachys::{
either::Either,
html::{
element::{form, Form},
event::submit,
},
reactive_graph::node_ref::NodeRef,
};
use thiserror::Error;
use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
use web_sys::{
Event, FormData, HtmlButtonElement, HtmlFormElement, HtmlInputElement,
SubmitEvent,
};
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
#[component]
pub fn ActionForm<ServFn, OutputProtocol>(
action: ServerAction<ServFn>,
#[prop(optional)]
node_ref: Option<NodeRef<Form>>,
children: Children,
) -> impl IntoView
where
ServFn: DeserializeOwned
+ ServerFn<Protocol = Http<PostUrl, OutputProtocol>>
+ Clone
+ Send
+ Sync
+ 'static,
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<
ServFn::Error,
>>::FormData: From<FormData>,
ServFn: Send + Sync + 'static,
ServFn::Output: Send + Sync + 'static,
ServFn::Error: Send + Sync + 'static,
<ServFn as ServerFn>::Client: Client<<ServFn as ServerFn>::Error>,
{
_ = server_fn::redirect::set_redirect_hook(|loc: &str| {
if let Some(url) = resolve_redirect_url(loc) {
_ = window().location().set_href(&url.href());
}
});
let version = action.version();
let value = action.value();
let on_submit = {
move |ev: SubmitEvent| {
if ev.default_prevented() {
return;
}
ev.prevent_default();
match ServFn::from_event(&ev) {
Ok(new_input) => {
action.dispatch(new_input);
}
Err(err) => {
crate::logging::error!(
"Error converting form field into server function \
arguments: {err:?}"
);
value.set(Some(Err(ServerFnErrorErr::Serialization(
err.to_string(),
)
.into_app_error())));
version.update(|n| *n += 1);
}
}
}
};
let action_form = form()
.action(ServFn::url())
.method("post")
.on(submit, on_submit)
.child(children());
if let Some(node_ref) = node_ref {
Either::Left(action_form.node_ref(node_ref))
} else {
Either::Right(action_form)
}
}
#[component]
pub fn MultiActionForm<ServFn, OutputProtocol>(
action: ServerMultiAction<ServFn>,
#[prop(optional)]
node_ref: Option<NodeRef<Form>>,
children: Children,
) -> impl IntoView
where
ServFn: Send
+ Sync
+ Clone
+ DeserializeOwned
+ ServerFn<Protocol = Http<PostUrl, OutputProtocol>>
+ 'static,
ServFn::Output: Send + Sync + 'static,
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<
ServFn::Error,
>>::FormData: From<FormData>,
ServFn::Error: Send + Sync + 'static,
<ServFn as ServerFn>::Client: Client<<ServFn as ServerFn>::Error>,
{
_ = server_fn::redirect::set_redirect_hook(|loc: &str| {
if let Some(url) = resolve_redirect_url(loc) {
_ = window().location().set_href(&url.href());
}
});
let on_submit = move |ev: SubmitEvent| {
if ev.default_prevented() {
return;
}
ev.prevent_default();
match ServFn::from_event(&ev) {
Ok(new_input) => {
action.dispatch(new_input);
}
Err(err) => {
action.dispatch_sync(Err(ServerFnErrorErr::Serialization(
err.to_string(),
)
.into_app_error()));
}
}
};
let action_form = form()
.action(ServFn::url())
.method("post")
.attr("method", "post")
.on(submit, on_submit)
.child(children());
if let Some(node_ref) = node_ref {
Either::Left(action_form.node_ref(node_ref))
} else {
Either::Right(action_form)
}
}
pub(crate) fn resolve_redirect_url(loc: &str) -> Option<web_sys::Url> {
let origin = match window().location().origin() {
Ok(origin) => origin,
Err(e) => {
leptos::logging::error!("Failed to get origin: {:#?}", e);
return None;
}
};
let base = origin;
match web_sys::Url::new_with_base(loc, &base) {
Ok(url) => Some(url),
Err(e) => {
leptos::logging::error!(
"Invalid redirect location: {}",
e.as_string().unwrap_or_default(),
);
None
}
}
}
pub trait FromFormData
where
Self: Sized + serde::de::DeserializeOwned,
{
fn from_event(ev: &web_sys::Event) -> Result<Self, FromFormDataError>;
fn from_form_data(
form_data: &web_sys::FormData,
) -> Result<Self, serde_qs::Error>;
}
#[derive(Error, Debug)]
pub enum FromFormDataError {
#[error("Could not find <form> connected to event.")]
MissingForm(Event),
#[error("Could not create FormData from <form>: {0:?}")]
FormData(JsValue),
#[error("Deserialization error: {0:?}")]
Deserialization(serde_qs::Error),
}
impl<T> FromFormData for T
where
T: serde::de::DeserializeOwned,
{
fn from_event(ev: &Event) -> Result<Self, FromFormDataError> {
let submit_ev = ev.unchecked_ref();
let form_data = form_data_from_event(submit_ev)?;
Self::from_form_data(&form_data)
.map_err(FromFormDataError::Deserialization)
}
fn from_form_data(
form_data: &web_sys::FormData,
) -> Result<Self, serde_qs::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_qs::Config::new(5, false).deserialize_str::<Self>(&data)
}
}
fn form_data_from_event(
ev: &SubmitEvent,
) -> Result<FormData, FromFormDataError> {
let submitter = ev.submitter();
let mut submitter_name_value = None;
let opt_form = match &submitter {
Some(el) => {
if let Some(form) = el.dyn_ref::<HtmlFormElement>() {
Some(form.clone())
} else if let Some(input) = el.dyn_ref::<HtmlInputElement>() {
submitter_name_value = Some((input.name(), input.value()));
Some(ev.target().unwrap().unchecked_into())
} else if let Some(button) = el.dyn_ref::<HtmlButtonElement>() {
submitter_name_value = Some((button.name(), button.value()));
Some(ev.target().unwrap().unchecked_into())
} else {
None
}
}
None => ev.target().map(|form| form.unchecked_into()),
};
match opt_form.as_ref().map(FormData::new_with_form) {
None => Err(FromFormDataError::MissingForm(ev.clone().into())),
Some(Err(e)) => Err(FromFormDataError::FormData(e)),
Some(Ok(form_data)) => {
if let Some((name, value)) = submitter_name_value {
form_data
.append_with_str(&name, &value)
.map_err(FromFormDataError::FormData)?;
}
Ok(form_data)
}
}
}