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..ffe959a 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'; @@ -227,31 +227,40 @@ export class QueryParamGroupService implements OnDestroy { }), )), 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 => { + const synthetic = this.isSyntheticNavigation(); + + return forkJoin(...Object.keys(this.getQueryParamGroup().queryParams).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 }; + }), + ); + })).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, - }); - }); + }), + ).subscribe(); } /** Listens for newly added parameters and starts synchronization for them. */ @@ -350,7 +359,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..eeebd21 100644 --- a/projects/ngqp/core/src/lib/model/query-param.ts +++ b/projects/ngqp/core/src/lib/model/query-param.ts @@ -1,4 +1,4 @@ -import { Observable, Subject } from 'rxjs'; +import { forkJoin, isObservable, Observable, of, Subject } from 'rxjs'; import { areEqualUsing, isFunction, isMissing, isPresent, undefinedToNull, wrapTryCatch } from '../util'; import { Comparator, OnChangeFunction, ParamCombinator, ParamDeserializer, ParamSerializer, Partitioner, Reducer } from '../types'; import { QueryParamGroup } from './query-param-group'; @@ -117,12 +117,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 +174,17 @@ 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); + const deserialized = this.deserialize(value); + if (isObservable(deserialized)) { + return deserialized; + } + + return of(deserialized); } } @@ -212,12 +211,21 @@ export class MultiQueryParam extends AbstractQueryParam { if (this.emptyOn !== undefined && (value || []).length === 0) { - return this.emptyOn; + return of(this.emptyOn); } - return (value || []).map(this.deserialize.bind(this)); + return forkJoin(...(value || []) + .map(v => { + const deserialized = this.deserialize(v); + if (isObservable(deserialized)) { + return deserialized; + } + + return of(deserialized); + }) + ); } } 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.