diff --git a/lib/route-recognizer.ts b/lib/route-recognizer.ts index ade9924..13c53af 100644 --- a/lib/route-recognizer.ts +++ b/lib/route-recognizer.ts @@ -688,32 +688,47 @@ class RouteRecognizer { } generateQueryString(params: Params): string { - const pairs: string[] = []; - const keys: string[] = Object.keys(params); - keys.sort(); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const value = params[key]; - if (value == null) { - continue; + const reducer = (obj: Params, parentPrefix: string | null = null) => (prev: string[], key: string) => { + const val = obj[key]; + if (val === null || val === undefined) { + return prev; } - let pair = encodeURIComponent(key); - if (isArray(value)) { - for (let j = 0; j < value.length; j++) { - const arrayPair = key + "[]" + "=" + encodeURIComponent(value[j]); - pairs.push(arrayPair); - } - } else { - pair += "=" + encodeURIComponent(value as string); - pairs.push(pair); + + const prefix = parentPrefix ? `${parentPrefix}[${key}]` : key; + + if (val == null || typeof val === 'function') { + prev.push(`${prefix}=`); + return prev; } - } + + if (isArray(val)) { + for (let j = 0; j < val.length; j++) { + // handle array query params. array format brackets. (Other options are indices a[0]=b&a[1]=c or repeat a=b&a=c) + if (['string', 'number', 'boolean'].includes(typeof val[j])) { + const arrayPair = key + "[]" + "=" + encodeURIComponent(val[j]); + prev.push(arrayPair); + } else { + prev.push(Object.keys(val[j] as Params).sort().reduce(reducer(val[j] as Params, prefix + `[${j}]`), []).join('&')); + } + } - if (pairs.length === 0) { - return ""; + return prev; + } else if (['string', 'number', 'boolean'].includes(typeof val)) { + prev.push(`${prefix}=${encodeURIComponent(val as string)}`); + return prev; + } + + prev.push(Object.keys(val as Params).sort().reduce(reducer(val as Params, parentPrefix ? prefix : `${prefix}=`), []).join('&')); + return prev; + }; + + const sortedKeys = Object.keys(params).sort(); + // avoid appending unnecessary '?' + if (!sortedKeys.length || sortedKeys.every((k) => params[k] === undefined || params[k] === null)) { + return ''; } - return "?" + pairs.join("&"); + return '?' + sortedKeys.reduce(reducer(params), []).join('&'); } parseQueryString(queryString: string): QueryParams { diff --git a/tests/recognizer-tests.ts b/tests/recognizer-tests.ts index af9ac5c..99cfd63 100644 --- a/tests/recognizer-tests.ts +++ b/tests/recognizer-tests.ts @@ -823,8 +823,7 @@ QUnit.test("Deserialize query param nested object", (assert: Assert) => { router.add([{ path: "/foo/bar", handler }]); const results = router.recognize("/foo/bar?filter=[user][name][$contains]=nick"); - const p = results && results.queryParams; - assert.deepEqual(p, { filter: { user: { name: { $contains: 'scoot' } } } }); + assert.deepEqual(results!.queryParams, { filter: { user: { name: { $contains: 'nick' } } } }); }); QUnit.test("Multiple `/` routes recognize", (assert: Assert) => { @@ -1639,7 +1638,7 @@ QUnit.module("Route Generation", hooks => { QUnit.test( "Parsing and generation results into the same input string", (assert: Assert) => { - const query = "filter%20data=date"; + const query = "filter data=date"; assert.equal( router.generateQueryString(router.parseQueryString(query)), "?" + query @@ -1647,7 +1646,7 @@ QUnit.module("Route Generation", hooks => { } ); - QUnit.only("Generation works with query params", (assert: Assert) => { + QUnit.test("Generation works with query params", (assert: Assert) => { assert.equal( router.generate("index", { queryParams: { filter: "date" } }), "/?filter=date" @@ -1732,8 +1731,16 @@ QUnit.module("Route Generation", hooks => { "/?filter=date&sort=0" ); assert.equal( - router.generate("index", { queryParams: { filter: { user: { name: { $contains: 'scoot' } } }, sort: 0 } }), - "/?filter=[user][name][$contains]=scoot" + router.generate("index", { queryParams: { filter: { age: 10, user: { name: { $contains: 'scoot' } } }, sort: 0 } }), + "/?filter=[age]=10&filter=[user][name][$contains]=scoot&sort=0" + ); + assert.equal( + router.generate("index", { queryParams: { sort: 0, filter: { age: 0, user: { name: { $contains: 'scoot' } } } } }), + "/?filter=[age]=0&filter=[user][name][$contains]=scoot&sort=0" + ); + assert.equal( + router.generate("index", { queryParams: { sort: 0, filter: { name: 'bike', children: [{ name: { $contains: 'scoot' } }] } }, sort: 0 }), + "/?filter=[children][0][name][$contains]=scoot&filter=[name]=bike&sort=0" ); });