From 5fa8489b417b491df8d718a8b06452fa11e5043e 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 | 63 +++++++++++-------- .../ngqp/core/src/lib/model/query-param.ts | 34 ++++++---- projects/ngqp/core/src/lib/types.ts | 3 +- 4 files changed, 60 insertions(+), 42 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..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.