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 4ff692f..832e702 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, forkJoin, from, Observable, Subject, zip } from 'rxjs'; +import { EMPTY, forkJoin, from, Observable, of, Subject, zip } from 'rxjs'; import { catchError, concatMap, @@ -123,7 +123,7 @@ export class QueryParamGroupService implements OnDestroy { ).pipe( // Do not synchronize while the param is detached from the group filter(() => !!this.getQueryParamGroup().get(queryParamName)), - map((newValue: unknown[]) => this.getParamsForValue(partitionedQueryParam, partitionedQueryParam.reduce(newValue))), + switchMap((newValue: unknown[]) => this.getParamsForValue(partitionedQueryParam, partitionedQueryParam.reduce(newValue))), takeUntil(this.destroy$), ).subscribe(params => this.enqueueNavigation(new NavigationData(params))); @@ -175,17 +175,23 @@ export class QueryParamGroupService implements OnDestroy { throw new Error(`Received null value from QueryParamGroup.`); } - let params: Params = {}; - Object.keys(newValue).forEach(queryParamName => { - const queryParam = this.getQueryParamGroup().get(queryParamName); - if (isMissing(queryParam)) { - return; - } - - params = { ...params, ...this.getParamsForValue(queryParam, newValue[ queryParamName ]) }; - }); + // TODO: Maybe we need to proxy registerOnChange through a subject here to avoid race conditions + // But how to make sure we unsubscribe? + forkJoin( + ...Object.keys(newValue) + .map(queryParamName => { + const queryParam = this.getQueryParamGroup().get(queryParamName); + if (isMissing(queryParam)) { + return of({}); + } - this.enqueueNavigation(new NavigationData(params, true)); + return this.getParamsForValue(queryParam, newValue[ queryParamName ]); + }) + ).pipe( + map(paramsList => paramsList.reduce((a, b) => { + return { ...a, ...b }; + }, {})), + ).subscribe(params => this.enqueueNavigation(new NavigationData(params, true))); }); } @@ -202,7 +208,10 @@ export class QueryParamGroupService implements OnDestroy { } queryParam._registerOnChange((newValue: unknown) => - this.enqueueNavigation(new NavigationData(this.getParamsForValue(queryParam, newValue), true)) + // TODO: Maybe we need to proxy registerOnChange through a subject here to avoid race conditions + // But how to make sure we unsubscribe? + this.getParamsForValue(queryParam, newValue) + .subscribe(params => this.enqueueNavigation(new NavigationData(params, true))) ); } @@ -329,7 +338,9 @@ export class QueryParamGroupService implements OnDestroy { * This consists mainly of properly serializing the model value and ensuring to take * side effect changes into account that may have been configured. */ - private getParamsForValue(queryParam: QueryParam | MultiQueryParam | PartitionedQueryParam, value: any): Params { + private getParamsForValue( + queryParam: QueryParam | MultiQueryParam | PartitionedQueryParam, value: any + ): Observable { const partitionedQueryParam = this.wrapIntoPartition(queryParam); const partitioned = partitionedQueryParam.partition(value); @@ -339,22 +350,28 @@ export class QueryParamGroupService implements OnDestroy { return { ...(a || {}), ...(b || {}) }; }, {}); - const newValues = partitionedQueryParam.queryParams - .map((current, index) => { + return forkJoin( + ...partitionedQueryParam.queryParams + .map((current, index) => (>current.serializeValue(partitioned[ index ] as any)).pipe( + map(serialized => { + return { + [ current.urlParam ]: serialized, + }; + }), + )) + ).pipe( + map(parts => parts.reduce((a, b) => { + return { ...a, ...b }; + }, {})), + map(newValues => { + // Note that we list the side-effect parameters first so that our actual parameter can't be + // overridden by it. return { - [ current.urlParam ]: current.serializeValue(partitioned[index] as any), + ...combinedParams, + ...newValues, }; - }) - .reduce((a, b) => { - return { ...a, ...b }; - }, {}); - - // Note that we list the side-effect parameters first so that our actual parameter can't be - // overridden by it. - return { - ...combinedParams, - ...newValues, - }; + }), + ); } /** diff --git a/projects/ngqp/core/src/lib/model/query-param.ts b/projects/ngqp/core/src/lib/model/query-param.ts index 2d74214..eb186a9 100644 --- a/projects/ngqp/core/src/lib/model/query-param.ts +++ b/projects/ngqp/core/src/lib/model/query-param.ts @@ -166,12 +166,12 @@ export class QueryParam extends AbstractQueryParam implem } /** @internal */ - public serializeValue(value: T | null): string | null { + public serializeValue(value: T | null): Observable { if (this.emptyOn !== undefined && areEqualUsing(value, this.emptyOn, this.compareWith!)) { - return null; + return of(null); } - return this.serialize(value); + return wrapIntoObservable(this.serialize(value)).pipe(first()); } /** @internal */ @@ -198,12 +198,14 @@ export class MultiQueryParam extends AbstractQueryParam { + if (this.emptyOn !== undefined && areEqualUsing(values, this.emptyOn, this.compareWith!)) { + return of(null); } - return (value || []).map(this.serialize.bind(this)); + return forkJoin(...(values || []) + .map(value => wrapIntoObservable(this.serialize(value)).pipe(first())) + ); } /** @internal */ diff --git a/projects/ngqp/core/src/lib/types.ts b/projects/ngqp/core/src/lib/types.ts index 1404782..7e633d3 100644 --- a/projects/ngqp/core/src/lib/types.ts +++ b/projects/ngqp/core/src/lib/types.ts @@ -5,7 +5,7 @@ import { Observable } from 'rxjs'; * A serializer defines how the represented form control's * value is converted into a string to be used in the URL. */ -export type ParamSerializer = (model: T | null) => string | null; +export type ParamSerializer = (model: T | null) => (string | null) | Observable; /** * A deserializer defines how a URL parameter is converted