@@ -220,37 +220,56 @@ export function buildPathWithParams(toPath: string, fromPath: string): string {
220220 * Resolves `:name` placeholders in the query of a URL, like the Angular router does for redirects.
221221 *
222222 * Each query parameter whose value is a `:name` placeholder gets the values of the `name`
223- * query parameter of `fromParams`, and is removed if `fromParams` has no such parameter.
223+ * query parameter of `fromParams`, or `undefined` if `fromParams` has no such parameter.
224+ * Like in the Angular router, placeholders of repeated query parameters are not resolved.
224225 *
225226 * @param toUrl - A URL whose query may contain `:name` placeholders.
226227 * @param fromParams - The query parameters that provide the values for the placeholders.
227228 * @returns The URL with its query placeholders resolved.
228229 *
229230 * @example
230231 * ```typescript
231- * buildQueryWithParams('/search ?q=:term ', new URLSearchParams('term =cats')); // returns '/search ?q=cats'
232+ * buildQueryWithParams('/find ?q=:x ', new URLSearchParams('x =cats')); // returns '/find ?q=cats'
232233 * ```
233234 */
234235export function buildQueryWithParams ( toUrl : string , fromParams : URLSearchParams ) : string {
235236 const [ , path , query , fragment ] = / ^ ( [ ^ ? # ] * ) \? ( [ ^ # ] * ) ( .* ) $ / . exec ( toUrl ) ?? [ ] ;
236- if ( query === undefined || ! query . includes ( '=:' ) ) {
237+ if ( query === undefined ) {
237238 return toUrl ;
238239 }
239240
240- const resolvedParams = new URLSearchParams ( ) ;
241- for ( const [ name , value ] of new URLSearchParams ( query ) ) {
242- if ( value [ 0 ] === ':' ) {
243- for ( const sourceValue of fromParams . getAll ( value . slice ( 1 ) ) ) {
244- resolvedParams . append ( name , sourceValue ) ;
241+ const params = query . split ( '&' ) ;
242+ const names = params . map ( ( param ) => param . split ( '=' , 1 ) [ 0 ] ) ;
243+ const resolvedQuery = params
244+ . map ( ( param , index ) => {
245+ const name = names [ index ] ;
246+ const value = param . slice ( name . length + 1 ) ;
247+ if ( value [ 0 ] !== ':' || names . indexOf ( name ) !== names . lastIndexOf ( name ) ) {
248+ return param ;
245249 }
246- } else {
247- resolvedParams . append ( name , value ) ;
248- }
249- }
250250
251- const resolvedQuery = resolvedParams . toString ( ) ;
251+ const sourceValues = fromParams . getAll ( value . slice ( 1 ) ) ;
252+
253+ return ( sourceValues . length ? sourceValues : [ 'undefined' ] )
254+ . map ( ( sourceValue ) => `${ name } =${ encodeQueryValue ( sourceValue ) } ` )
255+ . join ( '&' ) ;
256+ } )
257+ . join ( '&' ) ;
258+
259+ return `${ path } ?${ resolvedQuery } ${ fragment } ` ;
260+ }
252261
253- return path + ( resolvedQuery ? `?${ resolvedQuery } ` : '' ) + fragment ;
262+ /**
263+ * Encodes a query parameter value like the Angular router (`DefaultUrlSerializer`), which keeps
264+ * `@`, `:`, `$`, `,` and `;` as is.
265+ *
266+ * @param value - The query parameter value to encode.
267+ * @returns The encoded query parameter value.
268+ */
269+ function encodeQueryValue ( value : string ) : string {
270+ return encodeURIComponent ( value ) . replace ( / % ( 4 0 | 3 A | 2 4 | 2 C | 3 B ) / g, ( match ) =>
271+ decodeURIComponent ( match ) ,
272+ ) ;
254273}
255274
256275const MATRIX_PARAMS_REGEX = / ; [ ^ / ] + / g;
0 commit comments