From 4722ca3fe2dc83266faf8b913b33683571e61a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingo=20B=C3=BCrk?= Date: Fri, 12 Apr 2019 13:45:05 +0200 Subject: [PATCH] feat(core): support asynchronous deserializers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now support deserializers which run asynchronously by returning an observable instead. This is useful when the deserializer relies on some asynchronous process or data which has to be fetched first. relates #93 Signed-off-by: Ingo Bürk --- .../ngqp-demo/src/app/home/home.component.ts | 2 +- .../directives/query-param-group.service.ts | 73 ++++++++++++------- .../ngqp/core/src/lib/model/query-param.ts | 35 +++++---- projects/ngqp/core/src/lib/types.ts | 3 +- 4 files changed, 70 insertions(+), 43 deletions(-) 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..893e42e 100644 --- a/projects/ngqp/core/src/lib/model/query-param.ts +++ b/projects/ngqp/core/src/lib/model/query-param.ts @@ -1,4 +1,5 @@ -import { Observable, Subject } from 'rxjs'; +import { Observable, Subject, of, forkJoin, isObservable } from 'rxjs'; +import { first } from 'rxjs/operators'; 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 +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,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.pipe(first()); + } + + return of(deserialized); } } @@ -212,12 +212,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.pipe(first()); + } + + 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.