htmx icon indicating copy to clipboard operation
htmx copied to clipboard

Problem with Arrays in FormData Proxy

Open GPla opened this issue 5 months ago • 5 comments

This is an issue which is related to #2616. When trying to add an array of objects, the objects will be represented as [object Object].

What do I want to do? I want to select one or multiple files. Then, using htmx:configRequest, I want to map the files to their metadata which is sent to my endpoint.

Behavior in HTMX 1.x:

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/htmx.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/ext/json-enc.js"></script>

<script>
    function convertFilesToMetadata(event) {
        var files = event.detail.parameters.files;
        event.detail.parameters.files = files.map((file) => ({
            name: file.name,
            size: file.size,
            lastModified: file.lastModified,
            type: file.type,
        }));
    }    
</script>

<input id="files"
       name="files"
       type="file"
       hx-post="/upload"
       hx-trigger="change"
       hx-ext="json-enc"
       hx-on:htmx:config-request="convertFilesToMetadata(event)"
       multiple>
Image

Bug in HTMX 2.x:

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/htmx.min.js"></script>
<script src="https://unpkg.com/[email protected]/json-enc.js"></script>

<script>
    function convertFilesToMetadata(event) {
        var files = event.detail.parameters.files;

        if (files.length < 1) {
            // cancel event, no files selected
            event.preventDefault();
            return;
        }

        // convert files to dictionary of metadata
        event.detail.parameters.files = files.map((file) => ({
            name: file.name,
            size: file.size,
            lastModified: file.lastModified,
            type: file.type,
        }));
    }    
</script>

<input id="files"
       name="files"
       type="file"
       hx-post="/upload"
       hx-trigger="change"
       hx-ext="json-enc"
       hx-on:htmx:config-request="convertFilesToMetadata(event)"
       multiple>
Image

This will return undefined, since the Proxy does not implement map.

Changing map to a simple for, will result in the issue of [object Object]:

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/htmx.min.js"></script>
<script src="https://unpkg.com/[email protected]/json-enc.js"></script>

<script>
    function convertFilesToMetadata(event) {
        console.log(event);
        var files = event.detail.parameters.files;

        if (files.length < 1) {
            // cancel event, no files selected
            event.preventDefault();
            return;
        }
        
        var metadata = new Array(files.length);
        for (let index = 0; index < files.length; index++) {
            let file = files[index];
            metadata[index] = {
                name: file.name,
                size: file.size,
                lastModified: file.lastModified,
                type: file.type,
            }
        }
        event.detail.parameters.files = metadata;
    }
</script>

<input id="files"
       name="files"
       type="file"
       hx-post="/upload"
       hx-trigger="change"
       hx-ext="json-enc"
       hx-on:htmx:config-request="convertFilesToMetadata(event)"
       multiple>

Image

Applying, JSON.stringify on the object or metadata does not help because then it will be parsed as a string of JSON and not the object itself. Trying to assigning it directly event.detail.parameters.files[index] also does not work.

Other issues/breaking changes:

  • A single file is now the file itself and not an array as before in 1.x
  • Assigning an array with 1 element to the formData will also unpack the array
  • In the other issue #2616, there is also a mention that the FormData object can be accesses via event.detail.formData, however, upon inspection it will be empty.

GPla avatar Aug 04 '25 11:08 GPla

A quick test with event.detail.formData seems to be working for me. can you try console.log(Array.from(event.detail.formData.entries())) maybe?

MichaelWest22 avatar Aug 04 '25 14:08 MichaelWest22

That shows me the values. What confuses me is that logging/inspecting event.detail.formData does not show anything in the console:

Image

GPla avatar Aug 04 '25 14:08 GPla

Yeah FormData is a bit different and you need to use .entries() and treat it as an iterable or .get() or .getAll().

Can you try using event.detail.parameters.getAll('files') to return the proper array of files value and see if this makes it easier to operate on?

MichaelWest22 avatar Aug 04 '25 22:08 MichaelWest22

    function convertFilesToMetadata(event) {
    console.log(event);
    var files = event.detail.parameters.getAll('files');
   if (files.length < 1) {
        event.preventDefault();
        return;
    }

    // Collect metadata for all files
    const filesMetadata = files.map(file => ({
        name: file.name,
        size: file.size,
        lastModified: file.lastModified,
        type: file.type,
    }));

    event.detail.parameters.files = JSON.stringify(filesMetadata);
}

This seems to work with JSON.stringify()

FormData cannot accept plain JS objects directly—must use strings or Blob/File.

MichaelWest22 avatar Aug 05 '25 06:08 MichaelWest22

Yes, this makes it easier and more similar to the old behavior. I figured that I will to have parse the data from a JSON string or array of strings in my backend.

I guess the problem also comes from the json-enc extension or the attempt to send JSON. One could modify this extension to fully restore the old behavior but for me its good now. I just would have been happy to find this behavior change in the migration guide or somewhere else in the documentation :)

GPla avatar Aug 05 '25 09:08 GPla