18

I need to send some data using ajax and FormData, because I want to send a file and some other parameters. The way I usually send data is this:

$.ajax({
    type:       'POST',
    url:        'some_url',
    dataType:   'json',
    processData:false,
    contentType:false,
    data:{
        Lvl_1-1: 'something',
        Lvl_1-2: 'something',
        Lvl_1-3: {
            Lvl_1-3-1: "something",
            Lvl_1-3-2: "something",
            Lvl_1-3-3: "something",
        },
    },
    ...
});

If I don't use FormData(), I have no problem, but when using FormData(), only the data on Lvl1 is ok, but anything nested is displayed as string like this

<b>array</b> <i>(size=3)</i>
    'Lvl1-1' <font color='#888a85'>=&gt;</font> <small>string</small> 
        <font color='#cc0000'>'Something'</font> 
        <i>(length=23)</i>
    'Lvl1-2' <font color='#888a85'>=&gt;</font> <small>string</small> 
        <font color='#cc0000'>''Something''</font> <i>(length=3)</i>
    'Lvl1-3' <font color='#888a85'>=&gt;</font> <small>string</small> 
        <font color='#cc0000'>'[object Object]'</font> <i>(length=17)</i>

If I use FormData() to encode the data inside Lvl1-3, instead of [object Object] I get [object FormData]

How do I get an array instead of string on Lvl1-3?

NOTE: If the file is on top level (Lvl_1) I can send the file with no problems using FormData(). I didn't wrote the code of the file attached because that's not the problem, nested data is. I just mentioned the file because that's why I'm using FormData().

kunde
  • 401
  • 1
  • 6
  • 14
  • check https://github.com/foo123/serialiser.js to serialise complex / nested form fields to formData, object, json, url-encoded data (author) – Nikos M. Jan 29 '16 at 11:20

3 Answers3

22

URL Encoded form data doesn't have any native way to express complex data structures. It only supports simple key=value pairs.

?foo=1&bar=2

Most form data parsing libraries allow arrays of data using keys with the same name

?foo=1&foo=2

PHP bolted its own syntax on top of that format:

?foo[]=1&foo[]=2

which allowed for named keys in an associative array:

?foo[bar]=1&foo[baz]=2

and nested arrays:

?foo[bar][level2a]=1&foo[bar][level2b]=2

Due to the prevalence of PHP, jQuery adopted that syntax for generating form data when you pass a JavaScript object to data.

If you want to use FormData then jQuery won't reprocess it for you.

The effect you are seeing is because you are trying to put an object (I'm guessing a FormData instance, but you haven't showed that part of your code) as the second argument to append - where a string is expected.

You need to generate the key names using PHP's syntax yourself.

form_data_instance.append("Lvl_1-3[Lvl_1-3-1]", "something");
form_data_instance.append("Lvl_1-3[Lvl_1-3-2]", "something");
form_data_instance.append("Lvl_1-3[Lvl_1-3-3]", "something");
Quentin
  • 914,110
  • 126
  • 1,211
  • 1,335
  • Thanks! This is what I was looking for. Now I have a better understanding on this subject. – kunde Feb 28 '15 at 23:00
8

On my end, I stringify my nested parameters and parse them on the other end.

For instance, if I want to pass:

{"sthing":
  {"sthing":"sthing"},
  {"picture":
    {"legend":"great legend"},
    {"file":"great_picture.jpg"}
  }
}

Then I do:

// On the client side
const nestedParams = {"sthing":
                       {"sthing":"sthing"},
                       {"picture":
                         {"legend":"great legend"}
                       }
                     };
const pictureFile = document.querySelector('input[type="file"]')[0];
const formDataInstance = new FormData;
formDataInstance.append("nested_params": JSON.stringify(nested_params);
formDataInstance.append("file": document.querySelector('input[type="file"]')[0]);


// On the server side
params["nested_params"] = JSON.parse(params["nested_params"]);
params["nested_params"]["sthing"]["picture"]["file"] = params["file"];
Freddo
  • 523
  • 6
  • 16
  • That only works when you're dealing with js objects (which you could send using type `application/json`). It will not work if you need to send binary file, which is the main reason to use formdata over json. – Thierry J. Nov 18 '20 at 10:47
  • @ThierryJ. why wouldn't this work? Freddo's essentially suggesting formdata for sending the binary file, as you suggested and to also send the JSON they're basically sending it along as a string... – GrowinMan May 21 '21 at 02:43
  • @GrowinMan That is a very good question :) I honestly don't remember why I said that. Maybe I only read the beginning of the answer instead of reading the code as well. In the current state of the answer, it should indeed work as required. – Thierry J. May 21 '21 at 05:14
1

To add on to Quentin's answer, I had some PHP Laravel code like this:

    $tags = [];
    foreach ($request->input('tags') as $tag) {
        if (!is_array($tag)) {
            $new_tag = Tag::generate($tag);
            array_push($tags, $new_tag->id);
        } else {
            array_push($tags, $tag['id']);
        }
    }

You can see I start with an empty array, and then I fill it with values from $request->input('tags'). That request input is a multi-dimensional array, so it suffered the issue described in this question. It only manifested when using FormData and multipart/form-data form encoding type.

I was able to fix it with Quentin's answer plus this client-side JavaScript code here:

this.example.tags.forEach((tag, i) => {
    if (Object.prototype.toString.call(tag) === '[object String]') {
        payload.append(`tags[${i}]`, tag);
    } else {
        Object.keys(tag).forEach(field => payload.append(`tags[${i}][${field}]`, tag[field]));
    }
});

This code is first checking if the tag it is about to add to FormData is a string. If so, it is a new, non-existant tag. Then it adds it by its index number. If the tag already existed, I just send all the values the client had. In my case, the server only cares about the name and ID (for SQL relation to that row ID), so that's fine, but a good exercise in using syntax such as arr[index][field].

If you study Quentin's and my answer, you should be able to see the pattern. My example is also somewhat non trivial in nature, so it will be good to examine in my opinion.

To fully understand, here is what the payload looked like:

  'tags' => 
  array (
    0 => 
    array (
      'id' => '2',
      'name' => 'React JS',
    ),
    1 => 
    array (
      'id' => '5',
      'name' => 'JSON Web Tokens',
    ),
    2 => 'Sandwiches',
  ),

You can see that there are 3 tags. The first two already exist. The third one doesn't exist in my database, so it comes in as a string value not an object. Laravel/PHP doesn't like the nested object "existing tags", so I had to modify the FormData (which had images, thus multipart encoding type).

For context, here is my entire submitForm function:

const payload = new FormData();

Object.keys(this.example).forEach(field => payload.append(field, this.example[field]));

this.example.tags.forEach((tag, i) => {
    if (Object.prototype.toString.call(tag) === '[object String]') {
        payload.append(`tags[${i}]`, tag);
    } else {
        Object.keys(tag).forEach(field => payload.append(`tags[${i}][${field}]`, tag[field]));
    }
});

this.example.images.forEach(image => payload.append('images[]', image));

const response = await axios.post(route('admin.examples.create'), payload);

References: Check if a variable is a string in JavaScript

agm1984
  • 15,500
  • 6
  • 89
  • 113