@this-dot/route-config
Version:
A library containing directives and services for configuring components via Route's routeData property
349 lines (337 loc) • 18.4 kB
JavaScript
import { CommonModule } from '@angular/common';
import * as i0 from '@angular/core';
import { InjectionToken, Injectable, Optional, Inject, Pipe, Directive, Input, NgModule } from '@angular/core';
import { filter, map, startWith, switchMap, distinctUntilChanged, tap, takeUntil } from 'rxjs/operators';
import * as i1 from '@angular/router';
import { ActivationEnd } from '@angular/router';
import { combineLatest, BehaviorSubject, Subject } from 'rxjs';
import { isTruthy } from '@this-dot/utils';
const ROUTE_DATA_DEFAULT_VALUE = new InjectionToken('Route Data default value');
const gatherRoutes = (activatedRoute) => {
const routes = activatedRoute.pathFromRoot;
let route = activatedRoute.firstChild;
while (route) {
routes.push(route);
route = route.firstChild;
}
return routes;
};
class RouteConfigService {
get injectedDefaultValue() {
return this._injectedDefaultValue || {};
}
constructor(activatedRoute, router, _injectedDefaultValue) {
this.activatedRoute = activatedRoute;
this.router = router;
this._injectedDefaultValue = _injectedDefaultValue;
}
/**
* Returns an Observable which emits the route config set for the activated route.
*
* @example
* export class AppComponent {
* data$ = this.routeConfigService.getActivatedRouteConfig();
* dataWithDefaultValue$ = this.routeConfigService.getActivatedRouteConfig({
* routeTags: ['defaultTag'],
* title: 'Default Title',
* });
* }
*
* @param defaultValue - the default value that should be returned, it allows overriding the injected default values.
*
* @returns Observable<Partial<C>>
*/
getActivatedRouteConfig(defaultValue = {}) {
return this.router.events.pipe(filter((event) => event instanceof ActivationEnd), map(() => this.activatedRoute), startWith(this.activatedRoute), map(gatherRoutes), switchMap((routes) => combineLatest(routes.map(({ data }) => data)).pipe(map((dataArr) => Object.assign({}, this.injectedDefaultValue, defaultValue, ...dataArr)))));
}
getLeafConfig(paramName, defaultValue) {
return this.getActivatedRouteConfig(defaultValue
? {
[paramName]: defaultValue,
}
: {}).pipe(map((data) => data[paramName] || defaultValue));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.7", ngImport: i0, type: RouteConfigService, deps: [{ token: i1.ActivatedRoute }, { token: i1.Router }, { token: ROUTE_DATA_DEFAULT_VALUE, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.1.7", ngImport: i0, type: RouteConfigService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.7", ngImport: i0, type: RouteConfigService, decorators: [{
type: Injectable
}], ctorParameters: function () { return [{ type: i1.ActivatedRoute }, { type: i1.Router }, { type: undefined, decorators: [{
type: Optional
}, {
type: Inject,
args: [ROUTE_DATA_DEFAULT_VALUE]
}] }]; } });
/**
* Pipes an array of route tags and returns an Observable<boolean> that emits a boolean.
*
* The boolean is true if any of the values in the provided array is in the configured route tags of the activated route.
*
* @example
* <!-- Use it chained with the async pipe -->
* <ng-container *ngIf="routesWhereTheElementIsDisplayed | inRouteTags$ | async">
* The contents of this ng-container
* </ng-container>
*/
class InRouteTags$Pipe {
constructor(routeTagService) {
this.routeTagService = routeTagService;
this.routeTags$ = this.routeTagService.getLeafConfig('routeTags', []);
}
transform(tags) {
return this.routeTags$.pipe(map((routeTags) => !!tags.find((tag) => routeTags.includes(tag))));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.7", ngImport: i0, type: InRouteTags$Pipe, deps: [{ token: RouteConfigService }], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "16.1.7", ngImport: i0, type: InRouteTags$Pipe, name: "inRouteTags$" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.7", ngImport: i0, type: InRouteTags$Pipe, decorators: [{
type: Pipe,
args: [{
name: 'inRouteTags$',
}]
}], ctorParameters: function () { return [{ type: RouteConfigService }]; } });
class RouteDataHasService {
setTags(tags) {
const tagArray = Array.isArray(tags) ? tags : [tags];
this.tags$.next(tagArray);
}
setPropName(propName) {
this.propName$.next(propName);
}
setElseTemplate(elseTemplate) {
this.elseTemplate$.next(elseTemplate);
}
constructor(routeConfigService) {
this.routeConfigService = routeConfigService;
this.tags$ = new BehaviorSubject([]);
this.propName$ = new BehaviorSubject(undefined);
this.elseTemplate$ = new BehaviorSubject(null);
this.destroy$ = new Subject();
this.display$ = combineLatest([
this.tags$,
this.propName$.pipe(filter(isTruthy), switchMap((propName) => this.routeConfigService.getLeafConfig(propName, []))),
]).pipe(map(([tags, routeValues]) => !!tags.find((tag) => routeValues.includes(tag))), distinctUntilChanged());
this.createView$ = combineLatest([this.display$, this.elseTemplate$]).pipe(tap(() => this.viewContainer.clear()), tap(([show, elseTemplate]) => show
? void this.viewContainer.createEmbeddedView(this.template)
: void (elseTemplate && this.viewContainer.createEmbeddedView(elseTemplate))));
}
init(template, viewContainer) {
this.template = template;
this.viewContainer = viewContainer;
this.createView$.pipe(takeUntil(this.destroy$)).subscribe();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.7", ngImport: i0, type: RouteDataHasService, deps: [{ token: RouteConfigService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.1.7", ngImport: i0, type: RouteDataHasService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.7", ngImport: i0, type: RouteDataHasService, decorators: [{
type: Injectable
}], ctorParameters: function () { return [{ type: RouteConfigService }]; } });
/**
* Retrieves the provided route config property and binds it into a template variable
*
* @example
* <!-- Display an element only if the "show" routeTag is truthy -->
* <p *tdRouteTag="'show'">
* This text is only visible, if there is a 'show' tag in the route data's `routeTags` Array
* </p>
*
* @example
* <!-- Display an element only if the "show" routeTag is truthy and display a fallback template if it is falsy -->
* <p *tdRouteTag="'show'; else noShowTag">
* This text is only visible, if there is a 'show' tag in the route data's `routeTags` Array
* </p>
* <ng-template #noShowTag>
* <p>There is no 'show' tag in this route's config</p>
* </ng-template>
*/
class RouteTagDirective {
set tdRouteTag(tags) {
this.routeDataHasService.setTags(tags);
}
set tdRouteTagElse(elseTemplate) {
this.routeDataHasService.setElseTemplate(elseTemplate);
}
constructor(template, viewContainer, routeDataHasService) {
this.template = template;
this.viewContainer = viewContainer;
this.routeDataHasService = routeDataHasService;
}
ngOnInit() {
this.routeDataHasService.setPropName('routeTags');
this.routeDataHasService.init(this.template, this.viewContainer);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.7", ngImport: i0, type: RouteTagDirective, deps: [{ token: i0.TemplateRef }, { token: i0.ViewContainerRef }, { token: RouteDataHasService }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.1.7", type: RouteTagDirective, selector: "[tdRouteTag]", inputs: { tdRouteTag: "tdRouteTag", tdRouteTagElse: "tdRouteTagElse" }, providers: [RouteDataHasService], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.7", ngImport: i0, type: RouteTagDirective, decorators: [{
type: Directive,
args: [{ selector: '[tdRouteTag]', providers: [RouteDataHasService] }]
}], ctorParameters: function () { return [{ type: i0.TemplateRef }, { type: i0.ViewContainerRef }, { type: RouteDataHasService }]; }, propDecorators: { tdRouteTag: [{
type: Input
}], tdRouteTagElse: [{
type: Input
}] } });
/**
* This directive allows for access to the whole `data` property defined in the current [Route](https://angular.io/api/router/Route#data) from a Component's template.
*
* @example
* <!-- We can use it as following: -->
* <h1 *tdRouteData="let data">
* Current title is: {{ data.title }}
* </h1>
*
* @example
* <!-- It is also possible to pass a default value so that if a property is not defined in the Route we will still receive some value: -->
* <h1 *tdRouteData="let data; defaultValue: { title: 'DefaultTitle', routeTags: ['defaultTag'] }">
* Current title is: {{ data.title }}
* </h1>
*
* @example
* <!-- If you want to access multiple properties in one component's template it is **recommended** to wrap the whole template with only one `*tdRouteData` directive. This approach follows DRY principle and is efficient as it only creates one subscription per template. -->
* <ng-container *tdRouteData="let data; defaultValue: { title: 'DefaultTitle', routeTags: ['defaultTag'] }">
* <h1>
* Current title is: {{ data.title }}
* </h1>
* <p>
* Current route contains the following tags: {{ data.routeTags | json }}
* </p>
* </ng-container>
*/
class RouteDataDirective {
set tdRouteDataDefaultValue(defaultValue) {
this.defaultValue$.next(defaultValue);
}
constructor(routeConfigService, template, entry) {
this.routeConfigService = routeConfigService;
this.template = template;
this.entry = entry;
this.destroy$ = new Subject();
this.defaultValue$ = new BehaviorSubject({});
this.data$ = this.defaultValue$.pipe(switchMap((defaultValue) => this.routeConfigService.getActivatedRouteConfig(defaultValue)), distinctUntilChanged());
this.createView$ = this.data$.pipe(tap((data) => {
this.view.context = {
$implicit: data,
};
this.view.markForCheck();
}));
}
ngOnInit() {
this.view = this.entry.createEmbeddedView(this.template, {
$implicit: this.defaultValue$.value,
});
this.createView$.pipe(takeUntil(this.destroy$)).subscribe();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.7", ngImport: i0, type: RouteDataDirective, deps: [{ token: RouteConfigService }, { token: i0.TemplateRef }, { token: i0.ViewContainerRef }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.1.7", type: RouteDataDirective, selector: "[tdRouteData]", inputs: { tdRouteDataDefaultValue: "tdRouteDataDefaultValue" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.7", ngImport: i0, type: RouteDataDirective, decorators: [{
type: Directive,
args: [{
selector: '[tdRouteData]',
}]
}], ctorParameters: function () { return [{ type: RouteConfigService }, { type: i0.TemplateRef }, { type: i0.ViewContainerRef }]; }, propDecorators: { tdRouteDataDefaultValue: [{
type: Input
}] } });
class RouteDataModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.7", ngImport: i0, type: RouteDataModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "16.1.7", ngImport: i0, type: RouteDataModule, declarations: [RouteDataDirective], imports: [CommonModule], exports: [RouteDataDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "16.1.7", ngImport: i0, type: RouteDataModule, imports: [CommonModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.7", ngImport: i0, type: RouteDataModule, decorators: [{
type: NgModule,
args: [{
declarations: [RouteDataDirective],
imports: [CommonModule],
exports: [RouteDataDirective],
}]
}] });
/**
* If you need to use a different route data property to store the tags, you can use the `*tdRouteDataHas` directive. It works very similar to `*tdRouteTag` directive but provides a way to use different properties as a source of data.
*
* @example
* <!-- You can configure the directive to use the `customDataProperty` property of the route data object, where it looks for the `customShow` property to determine if the element needs to show:
* <p *tdRouteDataHas="'customShow'; propName: 'customDataProperty'">
* This text is only visible, if there is a 'show' tag in the route data's `customDataProperty` Array
* </p>
*
* @example
* <!-- `*tdRouteDataHas` also provides a way do display a fallback template if a given tag is not present -->
* <p *tdRouteDataHas="'customShow'; propName: 'customDataProperty'; else noShowTag">
* This text is only visible, if there is a 'customShow' tag in the route data's `customDataProperty` Array
* </p>
* <ng-template #noShowTag>
* <p>There is no 'customShow' tag in this route's config</p>
* </ng-template>
*/
class RouteDataHasDirective {
set tdRouteDataHas(tags) {
this.routeDataHasService.setTags(tags);
}
set tdRouteDataHasPropName(propName) {
this.routeDataHasService.setPropName(propName);
}
set tdRouteDataHasElse(elseTemplate) {
this.routeDataHasService.setElseTemplate(elseTemplate);
}
constructor(template, viewContainer, routeDataHasService) {
this.template = template;
this.viewContainer = viewContainer;
this.routeDataHasService = routeDataHasService;
}
ngOnInit() {
this.routeDataHasService.init(this.template, this.viewContainer);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.7", ngImport: i0, type: RouteDataHasDirective, deps: [{ token: i0.TemplateRef }, { token: i0.ViewContainerRef }, { token: RouteDataHasService }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.1.7", type: RouteDataHasDirective, selector: "[tdRouteDataHas]", inputs: { tdRouteDataHas: "tdRouteDataHas", tdRouteDataHasPropName: "tdRouteDataHasPropName", tdRouteDataHasElse: "tdRouteDataHasElse" }, providers: [RouteDataHasService], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.7", ngImport: i0, type: RouteDataHasDirective, decorators: [{
type: Directive,
args: [{
selector: '[tdRouteDataHas]',
providers: [RouteDataHasService],
}]
}], ctorParameters: function () { return [{ type: i0.TemplateRef }, { type: i0.ViewContainerRef }, { type: RouteDataHasService }]; }, propDecorators: { tdRouteDataHas: [{
type: Input
}], tdRouteDataHasPropName: [{
type: Input
}], tdRouteDataHasElse: [{
type: Input
}] } });
class RouteConfigModule {
/**
* Registers the RouteConfigModule and sets the providers globally.
* Make sure you call the forRoot method in your root module.
*
* @remarks You still need to import the module without calling the forRoot method in other modules so you can use the pipes and directives from this module.
*/
static forRoot() {
return {
ngModule: RouteConfigModule,
providers: [RouteConfigService],
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.7", ngImport: i0, type: RouteConfigModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "16.1.7", ngImport: i0, type: RouteConfigModule, declarations: [RouteTagDirective, InRouteTags$Pipe, RouteDataHasDirective], imports: [CommonModule, RouteDataModule], exports: [RouteTagDirective, InRouteTags$Pipe, RouteDataModule, RouteDataHasDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "16.1.7", ngImport: i0, type: RouteConfigModule, imports: [CommonModule, RouteDataModule, RouteDataModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.7", ngImport: i0, type: RouteConfigModule, decorators: [{
type: NgModule,
args: [{
declarations: [RouteTagDirective, InRouteTags$Pipe, RouteDataHasDirective],
imports: [CommonModule, RouteDataModule],
exports: [RouteTagDirective, InRouteTags$Pipe, RouteDataModule, RouteDataHasDirective],
}]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { InRouteTags$Pipe, ROUTE_DATA_DEFAULT_VALUE, RouteConfigModule, RouteConfigService, RouteDataDirective, RouteDataHasDirective, RouteDataModule, RouteTagDirective };
//# sourceMappingURL=this-dot-route-config.mjs.map