@@ -7,40 +7,119 @@ type FilterFn = (a: EntriesTouple) => boolean;
7
7
8
8
type FormDataObjectEntry = FormDataEntryValue | number | boolean | bigint ;
9
9
10
+ const nameKeyExtractor = / (?< name > [ a - z A - z ] + [ a - z A - Z - ] + ) \[ (?< index > .* ) \] / ;
11
+ const digitCheck = / ^ \d + $ / ;
12
+
13
+ type MagicObject = {
14
+ readonly type : 'array' | 'object' ;
15
+ add ( key : string , value : string | File ) : void ;
16
+ toJS ( ) : unknown [ ] | Record < string , unknown > ;
17
+ } ;
18
+
19
+ function makeMagicObject ( init : EntriesTouple [ ] = [ ] ) : MagicObject {
20
+ const entries : EntriesTouple [ ] = ( [ ] as EntriesTouple [ ] ) . concat ( init ) ;
21
+ return {
22
+ get type ( ) {
23
+ if ( entries . every ( ( [ k ] ) => digitCheck . test ( k ) || k === '' ) )
24
+ return 'array' ;
25
+ return 'object' ;
26
+ } ,
27
+ add ( key : string , value : string | File ) {
28
+ entries . push ( [ key , value ] ) ;
29
+ } ,
30
+ toJS ( ) {
31
+ // TODO: Figure out how to clean this up
32
+ if ( this . type === 'array' ) {
33
+ const arr : unknown [ ] = [ ] ;
34
+ for ( const [ k , v ] of entries ) {
35
+ if ( k === '' )
36
+ arr . push ( typeof v === 'string' ? stringToJSValue ( v ) : v ) ;
37
+ else arr [ Number ( k ) ] = typeof v === 'string' ? stringToJSValue ( v ) : v ;
38
+ }
39
+ return arr ;
40
+ }
41
+ const ret : Record < string , unknown > = { } ;
42
+ for ( const [ k , v ] of entries )
43
+ ret [ k ] = typeof v === 'string' ? stringToJSValue ( v ) : v ;
44
+ return ret ;
45
+ } ,
46
+ } ;
47
+ }
48
+
10
49
/**
11
50
*
12
51
* @param fd the form data object
13
52
* @param filterFn an optional filtering function to remove some values from the end object
14
- * @param pruneKeyNames if true, then the keynames matching the pattern of `key[]` will be pruned down to just
15
- * `key` in the resulting object
16
53
* @returns an object mapped from the entries.
17
54
*/
18
55
export function formDataToObject (
19
56
fd : FormData ,
20
57
filterFn : FilterFn = ( ) => true ,
21
- pruneKeyNames = true ,
22
- ) : Record < string , FormDataObjectEntry | FormDataObjectEntry [ ] > {
23
- const ret : Record < string , FormDataObjectEntry | FormDataObjectEntry [ ] > = { } ;
24
-
25
- for ( let key of fd . keys ( ) ) {
26
- if ( filterFn ( [ key , '' ] ) ) {
27
- const all = fd . getAll ( key ) ;
28
-
29
- if ( all . length === 1 && ! key . endsWith ( '[]' ) ) {
30
- // regular stuff
31
- if ( typeof all [ 0 ] === 'string' ) ret [ key ] = stringToJSValue ( all [ 0 ] ) ;
32
- else if ( all [ 0 ] instanceof File ) ret [ key ] = all [ 0 ] ;
33
- } else {
34
- if ( pruneKeyNames && / \[ .? \] / . test ( key ) )
35
- key = key . replace ( / \[ .? \] / , '' ) ;
36
-
37
- ret [ key ] = all . map ( v =>
38
- typeof v === 'string' ? stringToJSValue ( v ) : v ,
39
- ) ;
58
+ ) {
59
+ const ret : Record < string , unknown > = { } ;
60
+ // Map of key name into magic type which converts its entries to either an object
61
+ // or an array.
62
+ const info = new Map < string , ReturnType < typeof makeMagicObject > > ( ) ;
63
+ // eslint-disable-next-line prefer-const
64
+ for ( let [ iterator , value ] of fd . entries ( ) ) {
65
+ // If the key does not match this filter, continue the loop
66
+ if ( ! filterFn ( [ iterator , value ] ) ) continue ;
67
+ // run the iterator against the name key extractor
68
+ const matches = iterator . match ( nameKeyExtractor ) ;
69
+ // If we do not have matches, or the index match is empty, we go here.
70
+ if ( matches === null || matches [ 2 ] === '' ) {
71
+ // Grab all of the values for the current iterator
72
+ const all = fd . getAll ( iterator ) ;
73
+ // If the length of all entries for this iterator is 1 AND the iterator name does not end
74
+ // with [] (indicating the user wants this to be an array) we drop in here
75
+ if ( all . length === 1 && ! iterator . endsWith ( '[]' ) ) {
76
+ // set the value on the return object
77
+ ret [ iterator ] =
78
+ typeof all [ 0 ] === 'string' ? stringToJSValue ( all [ 0 ] ) : all [ 0 ] ;
79
+ // don't need the rest of the loop values here, so we forcibly continue
80
+ continue ;
40
81
}
82
+ // If the iterator includes an opening [], let's assume we don't want the `[]` to be included
83
+ // so we trim it out here
84
+ if ( iterator . includes ( '[' ) )
85
+ iterator = iterator . slice ( 0 , iterator . indexOf ( '[' ) ) ;
86
+
87
+ // Set the iterator value on returned object
88
+ ret [ iterator ] = all . map ( v =>
89
+ typeof v === 'string' ? stringToJSValue ( v ) : v ,
90
+ ) ;
91
+ } else {
92
+ // If we have matches, there's a bit more processing required
93
+ const { groups } = matches ;
94
+ // Check to ensure our groups are there. It shouldn't be possible to have matches
95
+ // without groups in modern JS, but c'est la vie
96
+ if ( groups === undefined ) continue ;
97
+ // pull out the name and index. we add defaults so TS doesn't yell at us about possibly
98
+ // being undefined
99
+ const { name = '' , index = '' } = groups ;
100
+ // Grab the magic item from the info map.
101
+ let magic = info . get ( name ) ;
102
+ // If we don't have a magic item, make one and set it
103
+ if ( ! magic ) {
104
+ magic = makeMagicObject ( ) ;
105
+ info . set ( name , magic ) ;
106
+ }
107
+
108
+ // If the index is '', it means we were given something like `name[]`, or `age[]`
109
+ if ( index === '' ) {
110
+ // Get all the values for this iterator
111
+ const all = fd . getAll ( iterator ) ;
112
+ // Loop over
113
+ for ( const a of all ) magic . add ( '' , a as string ) ;
114
+ } else magic . add ( index , value ) ;
41
115
}
42
116
}
43
117
118
+ // Consolidation of items in the info values.
119
+ for ( const [ key , magic ] of info . entries ( ) ) {
120
+ if ( ! ret [ key ] ) ret [ key ] = magic . toJS ( ) ;
121
+ else console . error ( `Key ${ key } already exists in object.` ) ;
122
+ }
44
123
return ret ;
45
124
}
46
125
0 commit comments