Support for object in queryParams
See original GitHub issueWhich @angular/* package(s) are relevant/related to the feature request?
router
Description
When we create an url tree, the “tree” function will serialize the queryParams for us.
While this serialization is mandatory for the object to fit into an url, that one is in my understanding not done in the right place. The UrlSerializer
is supposed to do that job, but it cannot do it properly anymore because the queryParams
has already been serialized. For instance, if you want to pass an object into a queryParam
, you will end up having a serialized string: [object Object]
. There is another ticket suggesting allowing overriding this serializing.
The typing of queryParams
is also confusing. Here is the type
export declare type Params = {
[key: string]: any;
};
While accessing the params through ParamMap returns a string
export declare interface ParamMap {
/**
* Reports whether the map contains a given parameter.
* @param name The parameter name.
* @returns True if the map contains the given parameter, false otherwise.
*/
has(name: string): boolean;
/**
* Retrieves a single value for a parameter.
* @param name The parameter name.
* @return The parameter's single value,
* or the first value if the parameter has multiple values,
* or `null` when there is no such parameter.
*/
get(name: string): string | null;
/**
* Retrieves multiple values for a parameter.
* @param name The parameter name.
* @return An array containing one or more values,
* or an empty array if there is no such parameter.
*
*/
getAll(name: string): string[];
/** Names of the parameters in the map. */
readonly keys: string[];
}
While I understand that we have at some point to serialize the queryParams
, the current way is not flexible enough for my needs.
Proposed solution
The first solution I do think about is keeping the queryParams as is, i.e. without serializing them. A Tree has Params which are of type Record<string, any>
just keep them so that we can retrieve the original value later.
But I can imagine there are other problems because we want to keep a snapshot of the routes, and we therefore need to serialize those parameters at some point (and this is not even related to the URL but the internal snapshot). I can think there are other as well as internal queryParams
comparison from one route to another (where strings or array of strings are easy to compare). Keeping both (the original and the stringified version) would also perhaps be an overkill. Note that I did not browse all the code base to see if there are such problems.
The cleanest solution I can think about would be to delegate the job of serializing to a service ‘ParamsSerializer’ so that we can then inject any logic we want.
// bonus to explicitly show those are generically parsable values, but using "Params" would be fine too
type JSONValue =
| string
| number
| boolean
| { [x: string]: JSONValue }
| Array<JSONValue>;
class ParamsSerializer {
serialize(params: Params): string
parse(stringParams: string): Record<string, JSONValue>
}
The serialize method would be called at that point
And the parse function to reconstruct the Params (there is no equivalent in the code yet). The ParamMap would also be able to return the JSONValue instead (currently string
).
export declare interface ParamMap {
...
get(name: string): JSONValue | null;
getAll(name: string): JSONValue[];
}
Alternatives considered
What I do really want is to pass any object within the queryParams
and being able to retrieve it back later.
We have 3 problems to solve:
- The object we pass must be serializable: we need a generic or custom way to convert our object to a JSONValue one.
- Once serialized, they should fit the URL : This is the responsibility of the UrlSerializer
- We should be able to revive those objects from the URL:
Currently, I’m simply passing queryParams which are of type Record<string, string>;
I just manually serialize my objects and then use my custom parser function.
Here is my helper:
import { Injectable } from '@angular/core';
import JsonURL from '@jsonurl/jsonurl';
import { mapValues } from 'lodash';
import { JSONValue } from '@medulla-core/utils/json';
@Injectable({
providedIn: 'root',
})
export class QueryParamsService {
static encode(queryParams?: Record<string, JSONValue>): Record<string, string> | undefined | null {
if (queryParams === undefined) {
return undefined;
}
if (queryParams === null) {
return null;
}
return mapValues(queryParams, (value) => JsonURL.stringify(value, { noEmptyComposite: true }));
}
static decode(queryParams?: Record<string, string>): Record<string, JSONValue> {
if (queryParams === undefined) {
return undefined;
}
if (queryParams === null) {
return null;
}
return mapValues(queryParams, (value) => JsonURL.parse(value, { noEmptyComposite: true }));
}
}
I’m using jsonurl to keep a valid, consistent and human readable json into the URL.
For instance, here is an example of the queryParams
of a userSearch
page:
queryParams = {
filter: {name:"", surname:""},
page: {offset: 0, count: 100},
}
If I try passing this object to the queryParams, without my helper I will get something like this:
?filter=%5Bobject%20Object%5D&page=%5Bobject%20Object%5D
Using my helper, the queryParams becomes:
queryParams = {
filter: "(name:'',surname:'')",
page: "(offset:0,count:100)"
}
Which is url friendly and gives me this clean url
?filter=(name:'',surname:'')&page=(offset:0,count:100)
To get back the object, I’m simply sending the parsed queryParams into my helper.
The goal of this ticket is to automate this process without using my helper.
Side note: As queryParams are related to routes, perhaps it be consistent to allow registering an optional parser / serialize to the routes themselves. We could then even register a reviver for the activeroute’s query params.
Issue Analytics
- State:
- Created a year ago
- Reactions:26
- Comments:15 (8 by maintainers)
Top GitHub Comments
As discussed, I made both changes (UrlTree should not stringify params, Router should always create a
UrlTree
from a string) and there were only ~15 global test failures in Google. This is a pretty manageable size for making this sort of change.I think we are on the same page, there must be a unique source of truth and the url should hold this responsibility (when one goes on a page, we need to rebuild the page state using the url). Also, while we could possibly set anything to the url (manually or a url tree through the “UrlSerializer.serialize”, the active route’s queryParams should always results from parsing that url to be consistent. Now if your UrlSerializer is not bijective, you might set a number and get a string back (like the
DefaultUrlSerializer
). Thankfully, a smarter UrlSerializer would not be hard to implement.