Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion leptos/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
41 changes: 24 additions & 17 deletions leptos/src/form.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -75,7 +73,7 @@ use web_sys::{
/// ```
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
#[component]
pub fn ActionForm<ServFn, OutputProtocol>(
pub fn ActionForm<ServFn, InputProtocol, OutputProtocol>(
/// The action from which to build the form.
action: ServerAction<ServFn>,
/// A [`NodeRef`] in which the `<form>` element should be stored.
Expand All @@ -85,8 +83,8 @@ pub fn ActionForm<ServFn, OutputProtocol>(
children: Children,
) -> impl IntoView
where
ServFn: DeserializeOwned
+ ServerFn<Protocol = Http<PostUrl, OutputProtocol>>
ServFn: FromFormData
+ ServerFn<Protocol = Http<InputProtocol, OutputProtocol>>
+ Clone
+ Send
+ Sync
Expand All @@ -98,6 +96,7 @@ where
ServFn::Output: Send + Sync + 'static,
ServFn::Error: Send + Sync + 'static,
<ServFn as ServerFn>::Client: Client<<ServFn as ServerFn>::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| {
Expand Down Expand Up @@ -139,6 +138,7 @@ where
let action_form = form()
.action(ServFn::url())
.method("post")
.enctype(<InputProtocol as server_fn::ContentType>::CONTENT_TYPE)
.on(submit, on_submit)
.child(children());
if let Some(node_ref) = node_ref {
Expand All @@ -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<ServFn, OutputProtocol>(
pub fn MultiActionForm<ServFn, InputProtocol, OutputProtocol>(
/// The action from which to build the form.
action: ServerMultiAction<ServFn>,
/// A [`NodeRef`] in which the `<form>` element should be stored.
Expand All @@ -165,15 +165,16 @@ where
ServFn: Send
+ Sync
+ Clone
+ DeserializeOwned
+ ServerFn<Protocol = Http<PostUrl, OutputProtocol>>
+ FromFormData
+ ServerFn<Protocol = Http<InputProtocol, 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>,
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| {
Expand Down Expand Up @@ -206,6 +207,7 @@ where
.action(ServFn::url())
.method("post")
.attr("method", "post")
.enctype(<InputProtocol as server_fn::ContentType>::CONTENT_TYPE)
.on(submit, on_submit)
.child(children());
if let Some(node_ref) = node_ref {
Expand Down Expand Up @@ -244,10 +246,15 @@ pub(crate) fn resolve_redirect_url(loc: &str) -> Option<web_sys::Url> {
/// 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<Self, FromFormDataError>;
fn from_event(ev: &web_sys::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)
}

/// Tries to deserialize the data, given the actual form data.
fn from_form_data(
Expand All @@ -273,13 +280,6 @@ 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> {
Expand Down Expand Up @@ -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 {}
}
1 change: 1 addition & 0 deletions leptos/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 14 additions & 0 deletions server_fn/src/codec/multipart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you that this is awkward. We should not implement Clone on a type that is not actually cloneable by panicking on the server. I think the piece of context I'm missing is — Why does this need to have a Clone implementation at all? (If you have a MultipartData and need to clone it you can call .into_client_data().map(|data| data.take()) and clone that instead)

}
}
}
}

impl MultipartData {
/// Extracts the inner data to handle as a stream.
///
Expand Down
36 changes: 33 additions & 3 deletions server_fn_macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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! {
Expand Down Expand Up @@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest that this should be handled in a similar way to the server_fn_path: have a from_form_data field in the ServerFnCall struct that takes Option<Path>, which allows leptos to provide ::leptos::form::FromFormData, but would allow other crates to provide their own path here. If from_form_data is None, omit this implementation.

fn from_form_data(
form_data: &::leptos::web_sys::FormData,
) -> Result<Self, ::leptos::serde_qs::Error> {
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
Expand Down Expand Up @@ -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();
Expand All @@ -826,6 +854,8 @@ impl ToTokens for ServerFnCall {

#impl_from

#impl_from_form

#deref_impl

#server_fn_impl
Expand Down