provide your own (de-)serializers, you can pass them to QueryParamBuilder#param.
+
+
+ Deserializers can also be asynchronous by returning an Observable instead. This can be useful, e.g., if you
+ have a user's name in the URL and need to deserialize it into a user object, but fetching the list of users
+ is an asynchronous operation.
+
+
+ Note that for asynchronous deserializers, ngqp will use the first emitted value rather than the
+ last one. Furthermore, the valueChanges observables will only emit after the deserializer has
+ emitted.
+
+
Components with multiple values
diff --git a/projects/ngqp-demo/src/app/home/home.component.ts b/projects/ngqp-demo/src/app/home/home.component.ts
index 43aed8e..82ccef9 100644
--- a/projects/ngqp-demo/src/app/home/home.component.ts
+++ b/projects/ngqp-demo/src/app/home/home.component.ts
@@ -1,5 +1,5 @@
import { Component } from '@angular/core';
-import { createStringDeserializer, QueryParamBuilder, QueryParamGroup } from '@ngqp/core';
+import { QueryParamBuilder, QueryParamGroup } from '@ngqp/core';
import { faAlignLeft, faCogs, faGlassCheers, faHeart, IconDefinition } from '@fortawesome/free-solid-svg-icons';
import { faGithub } from '@fortawesome/free-brands-svg-icons';
diff --git a/projects/ngqp/core/src/lib/directives/query-param-group.service.ts b/projects/ngqp/core/src/lib/directives/query-param-group.service.ts
index c512343..4ff692f 100644
--- a/projects/ngqp/core/src/lib/directives/query-param-group.service.ts
+++ b/projects/ngqp/core/src/lib/directives/query-param-group.service.ts
@@ -1,6 +1,6 @@
import { Inject, Injectable, isDevMode, OnDestroy, Optional } from '@angular/core';
import { Params } from '@angular/router';
-import { EMPTY, from, Observable, Subject, zip } from 'rxjs';
+import { EMPTY, forkJoin, from, Observable, Subject, zip } from 'rxjs';
import {
catchError,
concatMap,
@@ -15,7 +15,7 @@ import {
} from 'rxjs/operators';
import { compareParamMaps, filterParamMap, isMissing, isPresent, NOP } from '../util';
import { QueryParamGroup } from '../model/query-param-group';
-import { MultiQueryParam, QueryParam, PartitionedQueryParam } from '../model/query-param';
+import { MultiQueryParam, PartitionedQueryParam, QueryParam } from '../model/query-param';
import { NGQP_ROUTER_ADAPTER, NGQP_ROUTER_OPTIONS, RouterAdapter, RouterOptions } from '../router-adapter/router-adapter.interface';
import { QueryParamAccessor } from './query-param-accessor.interface';
@@ -226,32 +226,49 @@ export class QueryParamGroupService implements OnDestroy {
return compareParamMaps(filterParamMap(previousMap, keys), filterParamMap(currentMap, keys));
}),
)),
- takeUntil(this.destroy$),
- ).subscribe(queryParamMap => {
- const synthetic = this.isSyntheticNavigation();
- const groupValue: Record = {};
-
- Object.keys(this.getQueryParamGroup().queryParams).forEach(queryParamName => {
- const partitionedQueryParam = this.getQueryParamAsPartition(queryParamName);
- const newValues = partitionedQueryParam.queryParams.map(queryParam => isMultiQueryParam(queryParam)
- ? queryParam.deserializeValue(queryParamMap.getAll(queryParam.urlParam))
- : queryParam.deserializeValue(queryParamMap.get(queryParam.urlParam))
+ switchMap(queryParamMap => {
+ // We need to capture this right here since this is only set during the on-going navigation.
+ const synthetic = this.isSyntheticNavigation();
+ const queryParamNames = Object.keys(this.getQueryParamGroup().queryParams);
+
+ return forkJoin>(...queryParamNames
+ .map(queryParamName => {
+ const partitionedQueryParam = this.getQueryParamAsPartition(queryParamName);
+
+ return forkJoin(...partitionedQueryParam.queryParams
+ .map(queryParam => isMultiQueryParam(queryParam)
+ ? queryParam.deserializeValue(queryParamMap.getAll(queryParam.urlParam))
+ : queryParam.deserializeValue(queryParamMap.get(queryParam.urlParam))
+ )
+ ).pipe(
+ map(newValues => partitionedQueryParam.reduce(newValues)),
+ tap(newValue => {
+ const directives = this.directives.get(queryParamName);
+ if (directives) {
+ directives.forEach(directive => directive.valueAccessor.writeValue(newValue));
+ }
+ }),
+ map(newValue => {
+ return { [ queryParamName ]: newValue };
+ }),
+ takeUntil(this.destroy$),
+ );
+ })
+ ).pipe(
+ map((values: Record[]) => values.reduce((groupValue, value) => {
+ return {
+ ...groupValue,
+ ...value,
+ };
+ }, {})),
+ tap(groupValue => this.getQueryParamGroup().setValue(groupValue, {
+ emitEvent: !synthetic,
+ emitModelToViewChange: false,
+ })),
);
- const newValue = partitionedQueryParam.reduce(newValues);
-
- const directives = this.directives.get(queryParamName);
- if (directives) {
- directives.forEach(directive => directive.valueAccessor.writeValue(newValue));
- }
-
- groupValue[ queryParamName ] = newValue;
- });
-
- this.getQueryParamGroup().setValue(groupValue, {
- emitEvent: !synthetic,
- emitModelToViewChange: false,
- });
- });
+ }),
+ takeUntil(this.destroy$),
+ ).subscribe();
}
/** Listens for newly added parameters and starts synchronization for them. */
@@ -350,7 +367,7 @@ export class QueryParamGroupService implements OnDestroy {
return {
...(this.globalRouterOptions || {}),
- ...groupOptions,
+ ...(groupOptions || {}),
};
}
diff --git a/projects/ngqp/core/src/lib/model/query-param.ts b/projects/ngqp/core/src/lib/model/query-param.ts
index 37b33b7..2d74214 100644
--- a/projects/ngqp/core/src/lib/model/query-param.ts
+++ b/projects/ngqp/core/src/lib/model/query-param.ts
@@ -1,5 +1,6 @@
-import { Observable, Subject } from 'rxjs';
-import { areEqualUsing, isFunction, isMissing, isPresent, undefinedToNull, wrapTryCatch } from '../util';
+import { forkJoin, isObservable, Observable, of, Subject } from 'rxjs';
+import { first } from 'rxjs/operators';
+import { areEqualUsing, isFunction, isMissing, isPresent, undefinedToNull, wrapIntoObservable, wrapTryCatch } from '../util';
import { Comparator, OnChangeFunction, ParamCombinator, ParamDeserializer, ParamSerializer, Partitioner, Reducer } from '../types';
import { QueryParamGroup } from './query-param-group';
import { MultiQueryParamOpts, PartitionedQueryParamOpts, QueryParamOpts, QueryParamOptsBase } from './query-param-opts';
@@ -117,12 +118,6 @@ export abstract class AbstractQueryParam extends AbstractQueryParamBase
this.combineWith = combineWith;
}
- /** @internal */
- public abstract serializeValue(value: T | null): (string | null) | (string | null)[];
-
- /** @internal */
- public abstract deserializeValue(value: (string | null) | (string | null)[]): T | null;
-
/**
* Updates the value of this parameter.
*
@@ -180,12 +175,12 @@ export class QueryParam extends AbstractQueryParam implem
}
/** @internal */
- public deserializeValue(value: string | null): T | null {
+ public deserializeValue(value: string | null): Observable {
if (this.emptyOn !== undefined && value === null) {
- return this.emptyOn;
+ return of(this.emptyOn);
}
- return this.deserialize(value);
+ return wrapIntoObservable(this.deserialize(value)).pipe(first());
}
}
@@ -212,12 +207,14 @@ export class MultiQueryParam extends AbstractQueryParam {
+ if (this.emptyOn !== undefined && (values || []).length === 0) {
+ return of(this.emptyOn);
}
- return (value || []).map(this.deserialize.bind(this));
+ return forkJoin(...(values || [])
+ .map(value => wrapIntoObservable(this.deserialize(value)).pipe(first()))
+ );
}
}
diff --git a/projects/ngqp/core/src/lib/types.ts b/projects/ngqp/core/src/lib/types.ts
index 1a168b9..1404782 100644
--- a/projects/ngqp/core/src/lib/types.ts
+++ b/projects/ngqp/core/src/lib/types.ts
@@ -1,4 +1,5 @@
import { Params } from '@angular/router';
+import { Observable } from 'rxjs';
/**
* A serializer defines how the represented form control's
@@ -10,7 +11,7 @@ export type ParamSerializer = (model: T | null) => string | null;
* A deserializer defines how a URL parameter is converted
* into the represented form control's value.
*/
-export type ParamDeserializer = (value: string | null) => T | null;
+export type ParamDeserializer = (value: string | null) => (T | null) | Observable;
/**
* A partitioner can split a value into an array of parts.
diff --git a/projects/ngqp/core/src/lib/util.spec.ts b/projects/ngqp/core/src/lib/util.spec.ts
index 7a600b3..bb2899f 100644
--- a/projects/ngqp/core/src/lib/util.spec.ts
+++ b/projects/ngqp/core/src/lib/util.spec.ts
@@ -1,4 +1,5 @@
import { convertToParamMap, Params } from '@angular/router';
+import { of } from 'rxjs';
import {
areEqualUsing,
compareParamMaps,
@@ -9,6 +10,7 @@ import {
isPresent,
LOOSE_IDENTITY_COMPARATOR,
undefinedToNull,
+ wrapIntoObservable,
wrapTryCatch
} from './util';
import { Comparator } from './types';
@@ -266,4 +268,27 @@ describe(compareParamMaps.name, () => {
{ a: [ 'a2', 'a1' ] },
)).toBe(true);
});
+});
+
+describe(wrapIntoObservable.name, async () => {
+ it('wraps null', async () => {
+ const obs$ = wrapIntoObservable(null);
+ expect(obs$).toBeDefined();
+
+ obs$.subscribe(v => expect(v).toBe(null));
+ });
+
+ it('wraps a primitive value', async () => {
+ const obs$ = wrapIntoObservable(42);
+ expect(obs$).toBeDefined();
+
+ obs$.subscribe(v => expect(v).toBe(42));
+ });
+
+ it('does not wrap an observable', async () => {
+ const obs$ = wrapIntoObservable(of(42));
+ expect(obs$).toBeDefined();
+
+ obs$.subscribe(v => expect(v).toBe(42));
+ });
});
\ No newline at end of file
diff --git a/projects/ngqp/core/src/lib/util.ts b/projects/ngqp/core/src/lib/util.ts
index 2509765..81de7cd 100644
--- a/projects/ngqp/core/src/lib/util.ts
+++ b/projects/ngqp/core/src/lib/util.ts
@@ -1,4 +1,5 @@
import { convertToParamMap, ParamMap, Params } from '@angular/router';
+import { isObservable, Observable, of } from 'rxjs';
import { Comparator } from './types';
/** @internal */
@@ -96,4 +97,13 @@ export function compareStringArraysUnordered(first: string[], second: string[]):
const sortedFirst = first.sort();
const sortedSecond = second.sort();
return sortedFirst.every((firstKey, index) => firstKey === sortedSecond[index]);
+}
+
+/** @internal */
+export function wrapIntoObservable(input: T | Observable): Observable {
+ if (isObservable(input)) {
+ return input;
+ }
+
+ return of(input);
}
\ No newline at end of file
diff --git a/projects/ngqp/core/src/test/serialize-deserialize.spec.ts b/projects/ngqp/core/src/test/serialize-deserialize.spec.ts
index dd3de97..eb62535 100644
--- a/projects/ngqp/core/src/test/serialize-deserialize.spec.ts
+++ b/projects/ngqp/core/src/test/serialize-deserialize.spec.ts
@@ -2,6 +2,8 @@ import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
+import { of } from 'rxjs';
+import { delay } from 'rxjs/operators';
import { QueryParamBuilder, QueryParamGroup, QueryParamModule } from '../public_api';
import { setupNavigationWarnStub } from './util';
@@ -27,6 +29,28 @@ class BasicTestComponent {
}
+@Component({
+ template: `
+