Skip to content

Conversation

DzenanJupic
Copy link

In the chapter about ActionForms, it says:

Note: <ActionForm/> only works with the default URL-encoded POST encoding for server functions, to ensure graceful degradation/correct behaviour as an HTML form.

This, however, prevents file uploads from working without JavaScript being enabled. As multi part forms are also supported natively, I feel like it should be possible to allow using them as well.

This is a very naive - but functional - approach that implements support for this. There are currently a few issues though:

  1. The Clone for MultipartData impl on the server side panics. I'm not sure how to best clone the server value. It's not required for this use case, but should obviously be fixed. I think I need some guidance here.
  2. The server_fn_macro impl now refers to leptos, which is probably not desirable, as it is a standalone crate. I don't know the code base well enough to move this implementation somewhere else. How could I best solve this?
  3. The server_fn/multipart feature is now enabled unconditionally. The easy solution would be to add another feature to leptos. But I feel like resolving issue 2 will also lead to a nicer solution here.

Also, while testing, I found what I believe is a bug. Before specifying the enctype on the ActionForm, the server-side code would panic with couldn't parse boundary when submitting the form. It seems like the FromReq<MultipartFormData, Request, E> impl doesn't handle encoding errors at all.

Here is some example code for testing:

use leptos::prelude::*;

#[component]
pub fn Upload() -> impl IntoView {
    let upload_file = ServerAction::<Upload>::new();

    view! {
        <ActionForm action=upload_file>
            <label>
                "File"
                <input
                    type="file"
                    name="file"
                    accept=".csv, text/*"
                />
            </label>
            <input type="submit" value="Upload" />
        </ActionForm>
    }
}

#[server(input = server_fn::codec::MultipartFormData)]
async fn upload(data: server_fn::codec::MultipartData) -> Result<(), ServerFnError> {
  let _ = data;
  Ok(())
}

Instead of only allowing `PostUrl`, we take a generic `InputProtocol`
argument, which is restricted to either `PostUrl` or
`MultiPartFormData`. Also, we don't require `DeserializeOwned` on
`FromFormData`, so that we can implement it for server functions.
This currently panics on the server side, which is far from ideal.
All we need to do is derive `Clone` and implement `FromFormData`. The
former is trivial. The later however is quite hacky, as it requires us
to 1. reference `leptos`, which is not necessarily there when just using
`server_fn`, and 2. makes the assumption that the only field is of type
`MultipartData`. I think that's a fair assumption though.
Copy link
Collaborator

@gbj gbj left a comment

Choose a reason for hiding this comment

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

Thanks for the PR! Apologies for my delay in reviewing it.

I think this is great. Making multipart data easier to work on is something that comes up occasionally, and this is a good step in that direction. I left some comments, which I hope are helpful.

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.

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants