diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml index cdf6bd4325..1e9a881072 100644 --- a/leptos/Cargo.toml +++ b/leptos/Cargo.toml @@ -45,7 +45,7 @@ typed-builder = { workspace = true, default-features = true } typed-builder-macro = { workspace = true, default-features = true } serde = { workspace = true, default-features = true } serde_json = { workspace = true, default-features = true } -server_fn = { workspace = true, features = ["form-redirects", "browser"] } +server_fn = { workspace = true, features = ["form-redirects", "browser", "multipart"] } web-sys = { features = [ "ShadowRoot", "ShadowRootInit", diff --git a/leptos/src/form.rs b/leptos/src/form.rs index 529b3f9025..61ea081fa5 100644 --- a/leptos/src/form.rs +++ b/leptos/src/form.rs @@ -1,10 +1,8 @@ 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, @@ -75,7 +73,7 @@ use web_sys::{ /// ``` #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))] #[component] -pub fn ActionForm( +pub fn ActionForm( /// The action from which to build the form. action: ServerAction, /// A [`NodeRef`] in which the `
` element should be stored. @@ -85,8 +83,8 @@ pub fn ActionForm( children: Children, ) -> impl IntoView where - ServFn: DeserializeOwned - + ServerFn> + ServFn: FromFormData + + ServerFn> + Clone + Send + Sync @@ -98,6 +96,7 @@ where ServFn::Output: Send + Sync + 'static, ServFn::Error: Send + Sync + 'static, ::Client: Client<::Error>, + InputProtocol: enc_type::EncType, { // if redirect hook has not yet been set (by a router), defaults to a browser redirect _ = server_fn::redirect::set_redirect_hook(|loc: &str| { @@ -139,6 +138,7 @@ where let action_form = form() .action(ServFn::url()) .method("post") + .enctype(::CONTENT_TYPE) .on(submit, on_submit) .child(children()); if let Some(node_ref) = node_ref { @@ -152,7 +152,7 @@ where /// [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) /// progressively enhanced to use client-side routing. #[component] -pub fn MultiActionForm( +pub fn MultiActionForm( /// The action from which to build the form. action: ServerMultiAction, /// A [`NodeRef`] in which the `` element should be stored. @@ -165,8 +165,8 @@ where ServFn: Send + Sync + Clone - + DeserializeOwned - + ServerFn> + + FromFormData + + ServerFn> + 'static, ServFn::Output: Send + Sync + 'static, <>::Request as ClientReq< @@ -174,6 +174,7 @@ where >>::FormData: From, ServFn::Error: Send + Sync + 'static, ::Client: Client<::Error>, + InputProtocol: enc_type::EncType, { // if redirect hook has not yet been set (by a router), defaults to a browser redirect _ = server_fn::redirect::set_redirect_hook(|loc: &str| { @@ -206,6 +207,7 @@ where .action(ServFn::url()) .method("post") .attr("method", "post") + .enctype(::CONTENT_TYPE) .on(submit, on_submit) .child(children()); if let Some(node_ref) = node_ref { @@ -244,10 +246,15 @@ pub(crate) fn resolve_redirect_url(loc: &str) -> Option { /// validation during form submission. pub trait FromFormData where - Self: Sized + serde::de::DeserializeOwned, + Self: Sized, { /// Tries to deserialize the data, given only the `submit` event. - fn from_event(ev: &web_sys::Event) -> Result; + fn from_event(ev: &web_sys::Event) -> Result { + 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) + } /// Tries to deserialize the data, given the actual form data. fn from_form_data( @@ -273,13 +280,6 @@ impl FromFormData for T where T: serde::de::DeserializeOwned, { - fn from_event(ev: &Event) -> Result { - 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 { @@ -325,3 +325,10 @@ fn form_data_from_event( } } } + +mod enc_type { + pub trait EncType: server_fn::ContentType {} + + impl EncType for server_fn::codec::PostUrl {} + impl EncType for server_fn::codec::MultipartFormData {} +} diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index ad70ca17a2..81c93b3940 100644 --- a/leptos/src/lib.rs +++ b/leptos/src/lib.rs @@ -345,6 +345,7 @@ pub mod task { pub use serde; #[doc(hidden)] pub use serde_json; +pub use serde_qs; #[cfg(feature = "tracing")] #[doc(hidden)] pub use tracing; diff --git a/server_fn/src/codec/multipart.rs b/server_fn/src/codec/multipart.rs index 7cce7aa2c9..3e1496e978 100644 --- a/server_fn/src/codec/multipart.rs +++ b/server_fn/src/codec/multipart.rs @@ -31,6 +31,20 @@ pub enum MultipartData { Server(multer::Multipart<'static>), } +impl Clone for MultipartData { + fn clone(&self) -> Self { + match self { + Self::Client(data) => { + let form_data = (*data.0).clone(); + Self::from(form_data) + } + Self::Server(_) => { + panic!("Cannot clone server side multipart data") + } + } + } +} + impl MultipartData { /// Extracts the inner data to handle as a stream. /// diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index b37c414a9b..e8cb778f5b 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -341,9 +341,10 @@ impl ServerFnCall { Clone, #server_fn_path::rkyv::Archive, #server_fn_path::rkyv::Serialize, #server_fn_path::rkyv::Deserialize }, ), - Some("MultipartFormData") - | Some("Streaming") - | Some("StreamingText") => (PathInfo::None, quote! {}), + Some("MultipartFormData") => (PathInfo::None, quote! { Clone }), + Some("Streaming") | Some("StreamingText") => { + (PathInfo::None, quote! {}) + } Some("SerdeLite") => ( PathInfo::Serde, quote! { @@ -744,6 +745,31 @@ impl ServerFnCall { } } + fn impl_from_form(&self) -> TokenStream2 { + if self.input_ident().as_deref() != Some("MultipartFormData") { + return quote! {}; + } + + let Some((field_name, _)) = self.single_field() else { + return quote! {}; + }; + + let server_fn_path = self.server_fn_path(); + let struct_name = self.struct_name(); + + quote! { + impl ::leptos::form::FromFormData for #struct_name { + fn from_form_data( + form_data: &::leptos::web_sys::FormData, + ) -> Result { + Ok(Self { + #field_name: #server_fn_path::codec::MultipartData::from(form_data.clone()), + }) + } + } + } + } + fn func_tokens(&self) -> TokenStream2 { let body = &self.body; // default values for args @@ -811,6 +837,8 @@ impl ToTokens for ServerFnCall { let impl_from = self.impl_from(); + let impl_from_form = self.impl_from_form(); + let deref_impl = self.deref_impl(); let inventory = self.submit_to_inventory(); @@ -826,6 +854,8 @@ impl ToTokens for ServerFnCall { #impl_from + #impl_from_form + #deref_impl #server_fn_impl