Skip to content

Commit

Permalink
feat: added DbxRouteParamDefaultInstance
Browse files Browse the repository at this point in the history
- added updateParams to DbxRouterService
- DbxFirebaseDocumentStoreRouteIdDirective now redirects to the default value by default
- added DefaultForwardFunctionFactory
  • Loading branch information
dereekb committed Jun 20, 2022
1 parent a855283 commit 2608580
Show file tree
Hide file tree
Showing 12 changed files with 267 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Observable } from 'rxjs';
import { filterMaybe } from '@dereekb/rxjs';
import { Observable, combineLatest, firstValueFrom } from 'rxjs';
import { asObservable, filterMaybe, ObservableOrValue } from '@dereekb/rxjs';
import { DbxRouterService, DbxRouterTransitionService } from '../../service';
import { asSegueRef, SegueRefOrSegueRefRouterLink, SegueRefRawSegueParams } from '../../../segue';
import { asSegueRef, SegueRef, SegueRefOrSegueRefRouterLink, SegueRefRawSegueParams } from '../../../segue';
import { DbxRouterTransitionEvent, DbxRouterTransitionEventType } from '../../transition/transition';
import { ActivatedRoute, NavigationBehaviorOptions, NavigationEnd, NavigationExtras, NavigationStart, Router, UrlTree } from '@angular/router';
import { ActivatedRoute, NavigationBehaviorOptions, NavigationEnd, NavigationExtras, NavigationStart, Params, Router, UrlTree } from '@angular/router';
import { Injectable } from '@angular/core';
import { isArray } from 'class-validator';
import { map } from 'rxjs/operators';
import { Maybe } from '@dereekb/util';
import { KeyValueTypleValueFilter, Maybe, mergeObjects } from '@dereekb/util';

/**
* AngularRouter implementation of DbxRouterService and DbxRouterTransitionService.
Expand Down Expand Up @@ -37,20 +37,42 @@ export class DbxAngularRouterService implements DbxRouterService, DbxRouterTrans

constructor(readonly router: Router, readonly activatedRoute: ActivatedRoute) {}

go(input: SegueRefOrSegueRefRouterLink<NavigationExtras | NavigationBehaviorOptions>): Promise<boolean> {
const segueRef = asSegueRef(input);
const ref = segueRef.ref;

if (isArray(ref)) {
return this.router.navigate(ref as unknown[], {
...segueRef.refOptions,
queryParams: segueRef.refParams
});
} else {
return this.router.navigateByUrl(ref as string | UrlTree, {
...segueRef.refOptions
});
}
go(input: ObservableOrValue<SegueRefOrSegueRefRouterLink<NavigationExtras | NavigationBehaviorOptions>>): Promise<boolean> {
const inputObs = asObservable(input);
return firstValueFrom(inputObs).then((inputSegueRef) => {
const segueRef = asSegueRef(inputSegueRef);
const ref = segueRef.ref;

if (isArray(ref)) {
return this.router.navigate(ref as unknown[], {
...segueRef.refOptions,
queryParams: segueRef.refParams
});
} else {
return this.router.navigateByUrl(ref as string | UrlTree, {
...segueRef.refOptions
});
}
});
}

updateParams(inputParams: ObservableOrValue<SegueRefRawSegueParams>): Promise<boolean> {
const segueUpdate: Observable<SegueRefOrSegueRefRouterLink<NavigationExtras | NavigationBehaviorOptions>> = combineLatest([this.activatedRoute.params, asObservable(inputParams)]).pipe(
map(([currentParams, params]) => {
const refParams = mergeObjects([currentParams, params], KeyValueTypleValueFilter.UNDEFINED);
const segueRef: SegueRef<NavigationExtras | NavigationBehaviorOptions> = {
ref: this.activatedRoute.pathFromRoot,
refParams,
refOptions: {
replaceUrl: true
}
};

return segueRef;
})
);

return this.go(segueUpdate);
}

isActive(segueRef: SegueRefOrSegueRefRouterLink): boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Subject, BehaviorSubject } from 'rxjs';
import { filterNullAndUndefinedValues, KeyValueTypleValueFilter, mergeObjects } from '@dereekb/util';
import { Subject, BehaviorSubject, Observable, switchMap, firstValueFrom, map } from 'rxjs';
import { DbxRouterService, DbxRouterTransitionService } from '../../service';
import { asSegueRef, asSegueRefString, SegueRefOrSegueRefRouterLink, SegueRefRawSegueParams } from '../../../segue';
import { asSegueRef, asSegueRefString, SegueRef, SegueRefOrSegueRefRouterLink, SegueRefRawSegueParams } from '../../../segue';
import { StateService, UIRouterGlobals, TransitionOptions, TransitionService } from '@uirouter/core';
import { Injectable, OnDestroy } from '@angular/core';
import { DbxRouterTransitionEvent, DbxRouterTransitionEventType } from '../../transition/transition';
import { ObservableOrValue, asObservable } from '@dereekb/rxjs';

/**
* UIRouter implementation of DbxRouterService and DbxRouterTransitionService.
Expand Down Expand Up @@ -42,13 +44,38 @@ export class DbxUIRouterService implements DbxRouterService, DbxRouterTransition
return this.uiRouterGlobals.params;
}

go(input: SegueRefOrSegueRefRouterLink<TransitionOptions>): Promise<boolean> {
const segueRef = asSegueRef(input);
const params = { ...this.uiRouterGlobals.current.params, ...segueRef.refParams };
return this.state
.go(segueRef.ref as string, params, segueRef.refOptions)
.then(() => true)
.catch(() => false);
go(input: ObservableOrValue<SegueRefOrSegueRefRouterLink<TransitionOptions>>): Promise<boolean> {
const inputObs = asObservable(input);
return firstValueFrom(inputObs).then((inputSegueRef) => {
const segueRef = asSegueRef(inputSegueRef);
const params = { ...this.uiRouterGlobals.current.params, ...segueRef.refParams };
return this.state
.go(segueRef.ref as string, params, segueRef.refOptions)
.then(() => true)
.catch(() => false);
});
}

updateParams(inputParams: ObservableOrValue<SegueRefRawSegueParams>): Promise<boolean> {
const segueUpdate: Observable<SegueRefOrSegueRefRouterLink<TransitionOptions>> = asObservable(inputParams).pipe(
map((params) => {
const currentParams = this.uiRouterGlobals.params;
const refParams = mergeObjects([currentParams, params], KeyValueTypleValueFilter.UNDEFINED);

const ref: SegueRef<TransitionOptions> = {
ref: '.',
refParams,
refOptions: {
location: 'replace',
inherit: true
}
};

return ref;
})
);

return this.go(segueUpdate);
}

isActive(input: SegueRefOrSegueRefRouterLink): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ export abstract class DbxRouterService {
*/
abstract go(segueRef: ObservableOrValue<SegueRefOrSegueRefRouterLink>): Promise<boolean>;

/**
* Navigates to the current url with updated parameters. Will be merged with the existing parameters.
*
* @param segueRef
*/
abstract updateParams(params: ObservableOrValue<SegueRefRawSegueParams>): Promise<boolean>;

/**
* Returns true if the input segue ref is considered active.
*
Expand Down
1 change: 1 addition & 0 deletions packages/dbx-core/src/lib/router/router/util/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './route.reader';
export * from './route.default';
73 changes: 73 additions & 0 deletions packages/dbx-core/src/lib/router/router/util/route.default.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { BehaviorSubject, EMPTY, switchMap, of } from 'rxjs';
import { DbxRouteParamReaderInstance } from './route.reader';
import { Destroyable, Initialized, Maybe, DefaultForwardFunctionFactory, defaultForwardFunctionFactory } from '@dereekb/util';
import { SubscriptionObject, switchMapToDefault, SwitchMapToDefaultFilterFunction, filterMaybe } from '@dereekb/rxjs';

const DEFAULT_REDIRECT_INSTANCE_FORWARD_FACTORY = defaultForwardFunctionFactory<SwitchMapToDefaultFilterFunction<unknown>>((value) => of(value == null));

/**
* Utility class used in conjuction with a DbxRouteParamReaderInstance to redirect when the default param does not equal the
*/
export class DbxRouteParamDefaultRedirectInstance<T> implements Initialized, Destroyable {
private _enabled = new BehaviorSubject<boolean>(true);
private _useDefaultFilter = new BehaviorSubject<Maybe<SwitchMapToDefaultFilterFunction<T>>>(undefined);
private _sub = new SubscriptionObject();

constructor(readonly instance: DbxRouteParamReaderInstance<T>) {}

init(): void {
this._sub.subscription = this._enabled
.pipe(
switchMap((enabled) => {
if (enabled) {
return this.instance.paramValue$.pipe(
switchMapToDefault(this.instance.defaultValue$, (value) => {
return this._useDefaultFilter.pipe(switchMap((fn) => (DEFAULT_REDIRECT_INSTANCE_FORWARD_FACTORY as DefaultForwardFunctionFactory<SwitchMapToDefaultFilterFunction<T>>)(fn)(value)));
}),
filterMaybe(), // do not redirect on MaybeNot values
switchMap((defaultValue) => {
return this.redirectWithDefaultValue(defaultValue);
})
);
} else {
return EMPTY;
}
})
)
.subscribe();
}

destroy(): void {
this._enabled.complete();
this._useDefaultFilter.complete();
this._sub.destroy();
}

protected redirectWithDefaultValue(value: Maybe<T>): Promise<boolean> {
if (value != null) {
// perform a segue once
return this.redirectWithValue(value);
} else {
// do nothing
return Promise.resolve(false);
}
}

protected redirectWithValue(value: Maybe<T>): Promise<boolean> {
return this.instance.dbxRouterService.updateParams({
[this.instance.paramKey]: value
});
}

get enabled(): boolean {
return this._enabled.value;
}

set enabled(enabled: boolean) {
this._enabled.next(enabled);
}

setUseDefaultFilter(useValueFilter: Maybe<SwitchMapToDefaultFilterFunction<T>>): void {
this._useDefaultFilter.next(useValueFilter);
}
}
8 changes: 6 additions & 2 deletions packages/dbx-core/src/lib/router/router/util/route.reader.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ObservableOrValueGetter, MaybeObservableOrValueGetter, switchMapToDefault, maybeValueFromObservableOrValueGetter } from '@dereekb/rxjs';
import { ObservableOrValueGetter, MaybeObservableOrValueGetter, switchMapToDefault, maybeValueFromObservableOrValueGetter, SwitchMapToDefaultFilterFunction } from '@dereekb/rxjs';
import { Destroyable, Maybe } from '@dereekb/util';
import { BehaviorSubject, combineLatest, distinctUntilChanged, map, Observable, shareReplay } from 'rxjs';
import { BehaviorSubject, combineLatest, distinctUntilChanged, map, Observable, of, shareReplay, switchMap } from 'rxjs';
import { DbxRouterService } from '../service/router.service';

/**
Expand All @@ -20,6 +20,9 @@ export interface DbxRouteParamReader<T> {
*/
readonly value$: Observable<Maybe<T>>;

/**
* Param key getter/setters.
*/
get paramKey(): string;
set paramKey(paramKey: Maybe<string>);

Expand Down Expand Up @@ -48,6 +51,7 @@ export class DbxRouteParamReaderInstance<T> implements DbxRouteParamReader<T>, D
shareReplay(1)
);

readonly nextDefaultValue$: Observable<Maybe<T>> = this._defaultValue.pipe(maybeValueFromObservableOrValueGetter(), shareReplay(1));
readonly defaultValue$: Observable<Maybe<T>> = this._defaultValue.pipe(maybeValueFromObservableOrValueGetter(), shareReplay(1));

readonly value$: Observable<Maybe<T>> = this.paramValue$.pipe(switchMapToDefault(this.defaultValue$), shareReplay(1));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Observable } from 'rxjs';
import { Observable, BehaviorSubject, map, shareReplay, of, switchMap } from 'rxjs';
import { OnDestroy, Directive, Host, Input, OnInit } from '@angular/core';
import { DbxRouterService, AbstractSubscriptionDirective, DbxRouteParamReaderInstance } from '@dereekb/dbx-core';
import { DbxRouterService, AbstractSubscriptionDirective, DbxRouteParamReaderInstance, DbxRouteParamDefaultRedirectInstance } from '@dereekb/dbx-core';
import { DbxFirebaseDocumentStoreDirective } from './store.document.directive';
import { Maybe, ModelKey } from '@dereekb/util';
import { MaybeObservableOrValueGetter } from '@dereekb/rxjs';
import { MaybeObservableOrValueGetter, SwitchMapToDefaultFilterFunction } from '@dereekb/rxjs';

export const DBX_FIREBASE_ROUTER_SYNC_DEFAULT_ID_PARAM_KEY = 'id';
export const DBX_FIREBASE_ROUTER_SYNC_USE_DEFAULT_PARAM_VALUE = '0';

/**
* Used for synchronizing the document store id to the param of the route.
Expand All @@ -15,6 +16,22 @@ export const DBX_FIREBASE_ROUTER_SYNC_DEFAULT_ID_PARAM_KEY = 'id';
})
export class DbxFirebaseDocumentStoreRouteIdDirective<T = unknown> extends AbstractSubscriptionDirective implements OnInit, OnDestroy {
private _paramReader = new DbxRouteParamReaderInstance<ModelKey>(this.dbxRouterService, DBX_FIREBASE_ROUTER_SYNC_DEFAULT_ID_PARAM_KEY);
private _paramRedirect = new DbxRouteParamDefaultRedirectInstance<ModelKey>(this._paramReader);
private _useDefaultParam = new BehaviorSubject<string | SwitchMapToDefaultFilterFunction<ModelKey>>(DBX_FIREBASE_ROUTER_SYNC_USE_DEFAULT_PARAM_VALUE);
private _useDefaultParam$: Observable<SwitchMapToDefaultFilterFunction<ModelKey>> = this._useDefaultParam.pipe(
map((x) => {
let result: SwitchMapToDefaultFilterFunction<ModelKey>;

if (typeof x === 'string') {
result = (value: Maybe<ModelKey>) => of(value === x);
} else {
result = x;
}

return result;
}),
shareReplay(1)
);

readonly idParamKey$ = this._paramReader.paramKey$;
readonly idFromParams$: Observable<Maybe<ModelKey>> = this._paramReader.paramValue$;
Expand All @@ -26,11 +43,17 @@ export class DbxFirebaseDocumentStoreRouteIdDirective<T = unknown> extends Abstr

ngOnInit(): void {
this.sub = this.dbxFirebaseDocumentStoreDirective.store.setId(this.idFromParams$);
this._paramRedirect.setUseDefaultFilter((value: Maybe<string>) => {
return this._useDefaultParam$.pipe(switchMap((x) => x(value)));
});
this._paramRedirect.init();
}

override ngOnDestroy(): void {
super.ngOnDestroy();
this._paramReader.destroy();
this._paramRedirect.destroy();
this._useDefaultParam.complete();
}

// MARK: Input
Expand All @@ -47,4 +70,17 @@ export class DbxFirebaseDocumentStoreRouteIdDirective<T = unknown> extends Abstr
set dbxFirebaseDocumentStoreRouteIdDefault(defaultValue: MaybeObservableOrValueGetter<ModelKey>) {
this._paramReader.setDefaultValue(defaultValue);
}

/**
* Whether or not to enable the redirection. Is enabled by default.
*/
@Input()
set dbxFirebaseDocumentStoreRouteIdDefaultRedirect(redirect: Maybe<boolean> | '') {
this._paramRedirect.enabled = redirect !== false; // true by default
}

@Input()
set dbxFirebaseDocumentStoreRouteIdDefaultDecision(decider: string | SwitchMapToDefaultFilterFunction<ModelKey>) {
this._useDefaultParam.next(decider);
}
}
7 changes: 6 additions & 1 deletion packages/rxjs/src/lib/rxjs/map.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { map, OperatorFunction, switchMap, shareReplay } from 'rxjs';
import { map, OperatorFunction, switchMap, shareReplay, Observable } from 'rxjs';
import { MapKeysIntersectionObject, mapKeysIntersectionObjectToArray } from '@dereekb/util';
import { asObservable, ObservableOrValue } from './getter';

/**
* Decision-like that takes in a value and returns an Observable with a boolean.
*/
export type ObservableDecisionFunction<T> = (value: T) => Observable<boolean>;

/**
* OperatorFunction that pipes the input from the object with a keys observable to produce the result of mapKeysIntersectionObjectToArray.
*
Expand Down
30 changes: 21 additions & 9 deletions packages/rxjs/src/lib/rxjs/value.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { combineLatest, filter, skipWhile, startWith, switchMap, timeout, MonoTypeOperatorFunction, Observable, of, OperatorFunction, map, delay } from 'rxjs';
import { combineLatest, filter, skipWhile, startWith, switchMap, timeout, MonoTypeOperatorFunction, Observable, of, OperatorFunction, map, delay, first } from 'rxjs';
import { GetterOrValue, getValueFromGetter, Maybe } from '@dereekb/util';
import { asObservableFromGetter, MaybeObservableOrValueGetter, ObservableOrValueGetter } from './getter';
import { ObservableDecisionFunction } from './map';

// MARK: Types
export type IsCheckFunction<T = unknown> = (value: T) => Observable<boolean>;
Expand Down Expand Up @@ -64,6 +65,11 @@ export function switchMapMaybeDefault<T = unknown>(defaultValue: Maybe<T> = unde
});
}

/**
* Details when to pass the default value through.
*/
export type SwitchMapToDefaultFilterFunction<T> = ObservableDecisionFunction<Maybe<T>>;

/**
* Provides a switchMap that will emit the observable value if the observable is defined, otherwise will use the input default.
*
Expand All @@ -72,14 +78,20 @@ export function switchMapMaybeDefault<T = unknown>(defaultValue: Maybe<T> = unde
*/
export function switchMapToDefault<T = unknown>(defaultObs: MaybeObservableOrValueGetter<T>): OperatorFunction<Maybe<T>, Maybe<T>>;
export function switchMapToDefault<T = unknown>(defaultObs: ObservableOrValueGetter<T>): OperatorFunction<Maybe<T>, T>;
export function switchMapToDefault<T = unknown>(defaultObs: MaybeObservableOrValueGetter<T>): OperatorFunction<Maybe<T>, Maybe<T>> {
return switchMap((x: Maybe<T>) => {
if (x != null) {
return of(x);
} else {
return asObservableFromGetter(defaultObs);
}
});
export function switchMapToDefault<T = unknown>(defaultObs: MaybeObservableOrValueGetter<T>, useDefault?: SwitchMapToDefaultFilterFunction<T>): OperatorFunction<Maybe<T>, Maybe<T>>;
export function switchMapToDefault<T = unknown>(defaultObs: MaybeObservableOrValueGetter<T>, useDefault?: SwitchMapToDefaultFilterFunction<T>): OperatorFunction<Maybe<T>, Maybe<T>> {
const useDefaultFn = useDefault ? useDefault : (x: Maybe<T>) => of(x == null);
return switchMap((x: Maybe<T>) =>
useDefaultFn(x).pipe(
switchMap((useDefault) => {
if (useDefault) {
return asObservableFromGetter(defaultObs);
} else {
return of(x);
}
})
)
);
}

/**
Expand Down
Loading

0 comments on commit 2608580

Please sign in to comment.