Sending nested FormData on AJAX
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"];
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
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");