@@ -7,40 +7,119 @@ type FilterFn = (a: EntriesTouple) => boolean;
77
88type FormDataObjectEntry = FormDataEntryValue | number | boolean | bigint ;
99
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+
1049/**
1150 *
1251 * @param fd the form data object
1352 * @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
1653 * @returns an object mapped from the entries.
1754 */
1855export function formDataToObject (
1956 fd : FormData ,
2057 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 ;
4081 }
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 ) ;
41115 }
42116 }
43117
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+ }
44123 return ret ;
45124}
46125
0 commit comments