1use crate::{children::Children, component, prelude::*, IntoView};
2use leptos_dom::helpers::window;
3use leptos_server::{ServerAction, ServerMultiAction};
4use serde::de::DeserializeOwned;
5use server_fn::{
6 client::Client,
7 codec::PostUrl,
8 error::{IntoAppError, ServerFnErrorErr},
9 request::ClientReq,
10 Http, ServerFn,
11};
12use tachys::{
13 either::Either,
14 html::{
15 element::{form, Form},
16 event::submit,
17 },
18 reactive_graph::node_ref::NodeRef,
19};
20use thiserror::Error;
21use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
22use web_sys::{
23 Event, FormData, HtmlButtonElement, HtmlFormElement, HtmlInputElement,
24 SubmitEvent,
25};
26
27#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
77#[component]
78pub fn ActionForm<ServFn, OutputProtocol>(
79 action: ServerAction<ServFn>,
81 #[prop(optional)]
83 node_ref: Option<NodeRef<Form>>,
84 children: Children,
86) -> impl IntoView
87where
88 ServFn: DeserializeOwned
89 + ServerFn<Protocol = Http<PostUrl, OutputProtocol>>
90 + Clone
91 + Send
92 + Sync
93 + 'static,
94 <<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<
95 ServFn::Error,
96 >>::FormData: From<FormData>,
97 ServFn: Send + Sync + 'static,
98 ServFn::Output: Send + Sync + 'static,
99 ServFn::Error: Send + Sync + 'static,
100 <ServFn as ServerFn>::Client: Client<<ServFn as ServerFn>::Error>,
101{
102 _ = server_fn::redirect::set_redirect_hook(|loc: &str| {
104 if let Some(url) = resolve_redirect_url(loc) {
105 _ = window().location().set_href(&url.href());
106 }
107 });
108
109 let version = action.version();
110 let value = action.value();
111
112 let on_submit = {
113 move |ev: SubmitEvent| {
114 if ev.default_prevented() {
115 return;
116 }
117
118 ev.prevent_default();
119
120 match ServFn::from_event(&ev) {
121 Ok(new_input) => {
122 action.dispatch(new_input);
123 }
124 Err(err) => {
125 crate::logging::error!(
126 "Error converting form field into server function \
127 arguments: {err:?}"
128 );
129 value.set(Some(Err(ServerFnErrorErr::Serialization(
130 err.to_string(),
131 )
132 .into_app_error())));
133 version.update(|n| *n += 1);
134 }
135 }
136 }
137 };
138
139 let action_form = form()
140 .action(ServFn::url())
141 .method("post")
142 .on(submit, on_submit)
143 .child(children());
144 if let Some(node_ref) = node_ref {
145 Either::Left(action_form.node_ref(node_ref))
146 } else {
147 Either::Right(action_form)
148 }
149}
150
151#[component]
155pub fn MultiActionForm<ServFn, OutputProtocol>(
156 action: ServerMultiAction<ServFn>,
158 #[prop(optional)]
160 node_ref: Option<NodeRef<Form>>,
161 children: Children,
163) -> impl IntoView
164where
165 ServFn: Send
166 + Sync
167 + Clone
168 + DeserializeOwned
169 + ServerFn<Protocol = Http<PostUrl, OutputProtocol>>
170 + 'static,
171 ServFn::Output: Send + Sync + 'static,
172 <<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<
173 ServFn::Error,
174 >>::FormData: From<FormData>,
175 ServFn::Error: Send + Sync + 'static,
176 <ServFn as ServerFn>::Client: Client<<ServFn as ServerFn>::Error>,
177{
178 _ = server_fn::redirect::set_redirect_hook(|loc: &str| {
180 if let Some(url) = resolve_redirect_url(loc) {
181 _ = window().location().set_href(&url.href());
182 }
183 });
184
185 let on_submit = move |ev: SubmitEvent| {
186 if ev.default_prevented() {
187 return;
188 }
189
190 ev.prevent_default();
191
192 match ServFn::from_event(&ev) {
193 Ok(new_input) => {
194 action.dispatch(new_input);
195 }
196 Err(err) => {
197 action.dispatch_sync(Err(ServerFnErrorErr::Serialization(
198 err.to_string(),
199 )
200 .into_app_error()));
201 }
202 }
203 };
204
205 let action_form = form()
206 .action(ServFn::url())
207 .method("post")
208 .attr("method", "post")
209 .on(submit, on_submit)
210 .child(children());
211 if let Some(node_ref) = node_ref {
212 Either::Left(action_form.node_ref(node_ref))
213 } else {
214 Either::Right(action_form)
215 }
216}
217
218pub(crate) fn resolve_redirect_url(loc: &str) -> Option<web_sys::Url> {
220 let origin = match window().location().origin() {
221 Ok(origin) => origin,
222 Err(e) => {
223 leptos::logging::error!("Failed to get origin: {:#?}", e);
224 return None;
225 }
226 };
227
228 let base = origin;
230
231 match web_sys::Url::new_with_base(loc, &base) {
232 Ok(url) => Some(url),
233 Err(e) => {
234 leptos::logging::error!(
235 "Invalid redirect location: {}",
236 e.as_string().unwrap_or_default(),
237 );
238 None
239 }
240 }
241}
242
243pub trait FromFormData
246where
247 Self: Sized + serde::de::DeserializeOwned,
248{
249 fn from_event(ev: &web_sys::Event) -> Result<Self, FromFormDataError>;
251
252 fn from_form_data(
254 form_data: &web_sys::FormData,
255 ) -> Result<Self, serde_qs::Error>;
256}
257
258#[derive(Error, Debug)]
260pub enum FromFormDataError {
261 #[error("Could not find <form> connected to event.")]
263 MissingForm(Event),
264 #[error("Could not create FormData from <form>: {0:?}")]
266 FormData(JsValue),
267 #[error("Deserialization error: {0:?}")]
269 Deserialization(serde_qs::Error),
270}
271
272impl<T> FromFormData for T
273where
274 T: serde::de::DeserializeOwned,
275{
276 fn from_event(ev: &Event) -> Result<Self, FromFormDataError> {
277 let submit_ev = ev.unchecked_ref();
278 let form_data = form_data_from_event(submit_ev)?;
279 Self::from_form_data(&form_data)
280 .map_err(FromFormDataError::Deserialization)
281 }
282
283 fn from_form_data(
284 form_data: &web_sys::FormData,
285 ) -> Result<Self, serde_qs::Error> {
286 let data =
287 web_sys::UrlSearchParams::new_with_str_sequence_sequence(form_data)
288 .unwrap_throw();
289 let data = data.to_string().as_string().unwrap_or_default();
290 serde_qs::Config::new(5, false).deserialize_str::<Self>(&data)
291 }
292}
293
294fn form_data_from_event(
295 ev: &SubmitEvent,
296) -> Result<FormData, FromFormDataError> {
297 let submitter = ev.submitter();
298 let mut submitter_name_value = None;
299 let opt_form = match &submitter {
300 Some(el) => {
301 if let Some(form) = el.dyn_ref::<HtmlFormElement>() {
302 Some(form.clone())
303 } else if let Some(input) = el.dyn_ref::<HtmlInputElement>() {
304 submitter_name_value = Some((input.name(), input.value()));
305 Some(ev.target().unwrap().unchecked_into())
306 } else if let Some(button) = el.dyn_ref::<HtmlButtonElement>() {
307 submitter_name_value = Some((button.name(), button.value()));
308 Some(ev.target().unwrap().unchecked_into())
309 } else {
310 None
311 }
312 }
313 None => ev.target().map(|form| form.unchecked_into()),
314 };
315 match opt_form.as_ref().map(FormData::new_with_form) {
316 None => Err(FromFormDataError::MissingForm(ev.clone().into())),
317 Some(Err(e)) => Err(FromFormDataError::FormData(e)),
318 Some(Ok(form_data)) => {
319 if let Some((name, value)) = submitter_name_value {
320 form_data
321 .append_with_str(&name, &value)
322 .map_err(FromFormDataError::FormData)?;
323 }
324 Ok(form_data)
325 }
326 }
327}