Skip to content

Commit e0fbdd5

Browse files
committed
Improve and refactor code of bracket notation PR
1 parent 89acf02 commit e0fbdd5

File tree

4 files changed

+90
-69
lines changed

4 files changed

+90
-69
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
All notable changes to the library will be documented in this file.
44

5+
## vX.X.X (Month DD, YYYY)
6+
7+
- Add support for array bracket notation (pull request #15)
8+
59
## v0.7.5 (June 01, 2024)
610

711
- Republish previous version because build step was forgotten

README.md

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ import { decode } from 'https://deno.land/x/decode_formdata/mod.ts';
3131

3232
`FormData` stores the names of your fields and their values. However, there is a problem. Only strings and files are accepted as values, but complex forms can contain booleans, strings and dates. This leads to the fact that the boolean value `true` must be mapped with `"on"` and `false` values are simply ignored. Numbers and dates are also converted to strings.
3333

34-
Another problem are objects and arrays, which are usually mapped using dot notation. For example, the input field `<input name="todos.0.label" />` should map to the object `{ todos: [{ label: "" }] }`. By telling `decode` where arrays, booleans, dates, files, and numbers are located, the function can decode your `FormData` back into a complex JavaScript object.
34+
Another problem are objects and arrays, which are usually mapped using dot and bracket notation. For example, the input field `<input name="todos.0.label" />` should map to the object `{ todos: [{ label: "" }] }`. By telling `decode` where arrays, booleans, dates, files, and numbers are located, the function can decode your `FormData` back into a complex JavaScript object.
35+
36+
> Both dot and bracket notation are supported for arrays.
3537
3638
Consider the following form to add a new product to an online store:
3739

@@ -46,17 +48,17 @@ Consider the following form to add a new product to an online store:
4648
<input name="active" type="checkbox" />
4749

4850
<!-- Tags -->
49-
<input name="tags[0]" type="text" />
50-
<input name="tags[1]" type="text" />
51-
<input name="tags[2]" type="text" />
51+
<input name="tags.0" type="text" />
52+
<input name="tags.1" type="text" />
53+
<input name="tags.2" type="text" />
5254

5355
<!-- Images -->
54-
<input name="images[0].title" type="text" />
55-
<input name="images[0].created" type="date" />
56-
<input name="images[0].file" type="file" />
57-
<input name="images[1].title" type="text" />
58-
<input name="images[1].created" type="date" />
59-
<input name="images[1].file" type="file" />
56+
<input name="images.0.title" type="text" />
57+
<input name="images.0.created" type="date" />
58+
<input name="images.0.file" type="file" />
59+
<input name="images.1.title" type="text" />
60+
<input name="images.1.created" type="date" />
61+
<input name="images.1.file" type="file" />
6062
</form>
6163
```
6264

@@ -68,15 +70,15 @@ const formEntries = [
6870
['price', '0.89'],
6971
['created', '2023-10-09'],
7072
['active', 'on'],
71-
['tags[0]', 'fruit'],
72-
['tags[1]', 'healthy'],
73-
['tags[2]', 'sweet'],
74-
['images[0].title', 'Close up of an apple'],
75-
['images[0].created', '2023-08-24'],
76-
['images[0].file', Blob],
77-
['images[1].title', 'Our fruit fields at Lake Constance'],
78-
['images[1].created', '2023-08-12'],
79-
['images[1].file', Blob],
73+
['tags.0', 'fruit'],
74+
['tags.1', 'healthy'],
75+
['tags.2', 'sweet'],
76+
['images.0.title', 'Close up of an apple'],
77+
['images.0.created', '2023-08-24'],
78+
['images.0.file', Blob],
79+
['images.1.title', 'Our fruit fields at Lake Constance'],
80+
['images.1.created', '2023-08-12'],
81+
['images.1.file', Blob],
8082
];
8183
```
8284

src/decode.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ describe('decode', () => {
6464
expect(decode(formData, { files: ['file'] })).toEqual({ file });
6565
});
6666

67-
test('should decode indexed arrays', () => {
67+
test('should decode indexed arrays with dot notation', () => {
6868
const formData = new FormData();
6969
formData.append('array.0', 'index_0');
7070
formData.append('array.1', 'index_1');
@@ -98,7 +98,7 @@ describe('decode', () => {
9898
});
9999
});
100100

101-
test('should decode numbers in array', () => {
101+
test('should decode numbers in array with dot notation', () => {
102102
const formData = new FormData();
103103
formData.append('array.0', '111');
104104
formData.append('array.1', '222');
@@ -116,7 +116,7 @@ describe('decode', () => {
116116
formData.append('array[1]', '222');
117117
formData.append('array[2]', '333');
118118
expect(
119-
decode(formData, { arrays: ['array'], numbers: ['array.$'] })
119+
decode(formData, { arrays: ['array'], numbers: ['array[$]'] })
120120
).toEqual({
121121
array: [111, 222, 333],
122122
});
@@ -130,7 +130,7 @@ describe('decode', () => {
130130
});
131131
});
132132

133-
test('should decode nested arrays', () => {
133+
test('should decode nested arrays with dot notation', () => {
134134
const formData = new FormData();
135135
formData.append('nested.0.array.0', 'index_0');
136136
formData.append('nested.0.array.1', 'index_1');
@@ -152,7 +152,7 @@ describe('decode', () => {
152152
formData.append('nested[0].array[2]', 'index_2');
153153
expect(
154154
decode(formData, {
155-
arrays: ['nested.$.array', 'empty.array'],
155+
arrays: ['nested[$].array', 'empty.array'],
156156
})
157157
).toEqual({
158158
nested: [{ array: ['index_0', 'index_1', 'index_2'] }],

src/decode.ts

Lines changed: 60 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -43,68 +43,83 @@ export function decode<
4343
const [info, transform] =
4444
typeof arg2 === 'function' ? [undefined, arg2] : [arg2, arg3];
4545

46+
// Normalize info arrays to dot notation
47+
if (info) {
48+
for (const key of [
49+
'arrays',
50+
'booleans',
51+
'dates',
52+
'files',
53+
'numbers',
54+
] as const) {
55+
if (info[key]?.length) {
56+
info[key] = info[key]!.map((templateName) =>
57+
templateName.replace(/\[\$\]/g, '.$')
58+
);
59+
}
60+
}
61+
}
62+
4663
// Create empty values object
4764
const values: any = {};
4865

4966
// Add each form entry to values
5067
for (const [path, input] of formData.entries()) {
68+
// Normalize path to dot notation
69+
const normlizedPath = path.replace(/\[(\d+)\]/g, '.$1');
70+
5171
// Create template name and keys
52-
const templateName = path
72+
const templateName = normlizedPath
5373
.replace(/\.\d+\./g, '.$.')
54-
.replace(/\.\d+$/, '.$')
55-
.replace(/\[\d+\]/g, '.$&')
56-
.replace(/\[(\d+)\]/g, '$');
74+
.replace(/\.\d+$/, '.$');
5775
const templateKeys = templateName.split('.');
5876

5977
// Add value of current field to values
60-
path
61-
.replace(/\[(\d+)\]/g, '.$1')
62-
.split('.')
63-
.reduce((object, key, index, keys) => {
64-
// If it is not last index, return array or object
65-
if (index < keys.length - 1) {
66-
// If array or object already exists, return it
67-
if (object[key]) {
68-
return object[key];
69-
}
70-
71-
// Otherwise, check if value is an array
72-
const isArray =
73-
index < keys.length - 2
74-
? templateKeys[index + 1] === '$'
75-
: info?.arrays?.includes(templateKeys.slice(0, -1).join('.'));
76-
77-
// Add and return empty array or object
78-
return (object[key] = isArray ? [] : {});
78+
normlizedPath.split('.').reduce((object, key, index, keys) => {
79+
// If it is not last index, return array or object
80+
if (index < keys.length - 1) {
81+
// If array or object already exists, return it
82+
if (object[key]) {
83+
return object[key];
7984
}
8085

81-
// Otherwise, if it is not an empty file, add value
82-
if (
83-
!info?.files?.includes(templateName) ||
84-
(input && (typeof input === 'string' || input.size))
85-
) {
86-
// Get field value
87-
let output = getFieldValue(info, templateName, input);
88-
89-
// Transform value if necessary
90-
if (transform) {
91-
output = transform({ path, input, output });
92-
}
86+
// Otherwise, check if value is an array
87+
const isArray =
88+
index < keys.length - 2
89+
? templateKeys[index + 1] === '$'
90+
: info?.arrays?.includes(templateKeys.slice(0, -1).join('.'));
91+
92+
// Add and return empty array or object
93+
return (object[key] = isArray ? [] : {});
94+
}
9395

94-
// If it is an non-indexed array, add value to array
95-
if (info?.arrays?.includes(templateName)) {
96-
if (object[key]) {
97-
object[key].push(output);
98-
} else {
99-
object[key] = [output];
100-
}
96+
// Otherwise, if it is not an empty file, add value
97+
if (
98+
!info?.files?.includes(templateName) ||
99+
(input && (typeof input === 'string' || input.size))
100+
) {
101+
// Get field value
102+
let output = getFieldValue(info, templateName, input);
103+
104+
// Transform value if necessary
105+
if (transform) {
106+
output = transform({ path, input, output });
107+
}
101108

102-
// Otherwise, add value directly to key
109+
// If it is an non-indexed array, add value to array
110+
if (info?.arrays?.includes(templateName)) {
111+
if (object[key]) {
112+
object[key].push(output);
103113
} else {
104-
object[key] = output;
114+
object[key] = [output];
105115
}
116+
117+
// Otherwise, add value directly to key
118+
} else {
119+
object[key] = output;
106120
}
107-
}, values);
121+
}
122+
}, values);
108123
}
109124

110125
// Supplement empty arrays if necessary

0 commit comments

Comments
 (0)