UNPKG

ngx-router-meta

Version:

Configure HTML meta tags in route configuration of Angular

348 lines (339 loc) 15.1 kB
import * as i0 from '@angular/core'; import { InjectionToken, Injectable, Inject, PLATFORM_ID, Optional, NgModule, SkipSelf } from '@angular/core'; import { switchMapTo, switchAll, catchError, EMPTY, map, scan, startWith, filter, Subject, merge, combineLatest, shareReplay, isObservable, of, switchMap, takeUntil } from 'rxjs'; import { __rest } from 'tslib'; import { isPlatformServer } from '@angular/common'; import * as i2 from '@angular/platform-browser'; import { makeStateKey } from '@angular/platform-browser'; import * as i1 from '@angular/router'; import { NavigationEnd } from '@angular/router'; /** @internal */ const ROUTE_META_CONFIG = new InjectionToken('ROUTE_META_CONFIG'); /** * Check if {@link Route#data} contains {@link RouteMeta} object */ function isDataWithMeta(data) { return !!data.meta; } /** * Allows to accumulate many context objects streams into one stream * while merging all context objects together * and running any transformation function */ function unfoldContext(ctx$$, mapFn) { return (o$) => o$.pipe(switchMapTo(ctx$$.pipe(switchAll(), catchError(() => EMPTY), map((ctx) => mapFn(ctx)), scan((acc, ctx) => (Object.assign(Object.assign({}, acc), ctx))), // Merge contexts startWith({})))); } function isNavigationEndEvent(event) { return event instanceof NavigationEnd; } function escapeRegExp(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string } /** * Service to provide context for meta tags */ class RouterMetaContextService { constructor(config, router) { this.config = config; this.router = router; this.ctxNameCache = {}; this.interpolation = this.config.interpolation || RouterMetaContextService.interpolation; this.interpolationStart = escapeRegExp(this.interpolation.start); this.interpolationEnd = escapeRegExp(this.interpolation.end); this.navigationEnd$ = this.router.events.pipe(filter(isNavigationEndEvent)); this.metaDefaultContext$$ = new Subject(); this.metaContext$$ = new Subject(); this.clearDefaultContext$ = new Subject(); this.clearContext$ = new Subject(); this.metaDefaultContext$ = this.clearDefaultContext$.pipe(startWith(null), unfoldContext(this.metaDefaultContext$$, (ctx) => this._processContext(ctx))); this.metaContext$ = merge(this.clearContext$, this.navigationEnd$).pipe(startWith(null), unfoldContext(this.metaContext$$, (ctx) => this._processContext(ctx))); this.context$ = combineLatest(this.metaDefaultContext$, this.metaContext$).pipe(map(([defaultCtx, ctx]) => (Object.assign(Object.assign({}, defaultCtx), ctx))), shareReplay()); } /** * Provided context will be available between router navigations * * Multiple contexts will be merged together */ provideDefaultContext(ctx) { this.metaDefaultContext$$.next(isObservable(ctx) ? ctx : of(ctx)); } /** * Provided context will be available ONLY until next router navigation * * Multiple contexts will be merged together * * If you want to persist context between navigations - * use {@link RouterMetaContextService#provideDefaultContext()} */ provideContext(ctx) { this.metaContext$$.next(isObservable(ctx) ? ctx : of(ctx)); } /** * Clear default context provided by * {@link RouterMetaContextService#provideDefaultContext()} */ clearDefaultContext() { this.clearDefaultContext$.next(); } /** * Clear context provided by * {@link RouterMetaContextService#provideContext()} */ clearContext() { this.clearContext$.next(); } /** * Observable of computed context for meta tags */ getContext() { return this.context$; } /** @internal */ _templateStr(str, data, extras) { if (!str || !data) { return str || ''; } let template = str; let ctx = data; if (extras) { template = extras.templates && extras.name ? extras.templates[extras.name] || template : template; ctx = Object.assign(Object.assign({}, this.prepareContext(extras.meta, ctx)), ctx); // Recover lost `str` value under provided `extras.name` key in context if (extras.name && extras.name in ctx === false) { const nameCtx = this._processContext({ [extras.name]: str }); ctx = Object.assign(Object.assign({}, ctx), this.prepareContext(nameCtx, ctx)); } } return this.templateReplace(template, ctx); } /** @internal */ _processContext(ctx) { return Object.keys(ctx).reduce((acc, key) => (Object.assign(Object.assign({}, acc), { [key]: { replace: this.getContextReplace(key), value: ctx[key], } })), {}); } prepareContext(ctx, data) { if (!ctx || !data) { return ctx; } return Object.keys(ctx).reduce((c, key) => { c[key] = Object.assign(Object.assign({}, ctx[key]), { value: this.templateReplace(ctx[key].value, data) }); return c; }, {}); } templateReplace(tpl, ctx) { return Object.keys(ctx).reduce((s, key) => s.replace(ctx[key].replace, ctx[key].value), tpl); } getContextReplace(name) { return name in this.ctxNameCache ? this.ctxNameCache[name] : (this.ctxNameCache[name] = new RegExp(`${this.interpolationStart}${escapeRegExp(name)}${this.interpolationEnd}`, 'g')); } } RouterMetaContextService.interpolation = { start: '{', end: '}', }; RouterMetaContextService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.5", ngImport: i0, type: RouterMetaContextService, deps: [{ token: ROUTE_META_CONFIG }, { token: i1.Router }], target: i0.ɵɵFactoryTarget.Injectable }); RouterMetaContextService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.0.5", ngImport: i0, type: RouterMetaContextService, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.5", ngImport: i0, type: RouterMetaContextService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: function () { return [{ type: undefined, decorators: [{ type: Inject, args: [ROUTE_META_CONFIG] }] }, { type: i1.Router }]; } }); const TITLE_STATE = makeStateKey('RouterMetaTitle'); /** * Service responsible for updating meta tags based on router configuration * * To provide context - use {@link RouterMetaContextService} */ class RouterMetaService { constructor(platformId, config, router, title, meta, contextService, transferState) { this.platformId = platformId; this.config = config; this.router = router; this.title = title; this.meta = meta; this.contextService = contextService; this.transferState = transferState; this.initialized = false; this.metaTags = {}; this.defaultMeta = this.config.defaultMeta || {}; this.originalTitle = this.transferState ? this.transferState.get(TITLE_STATE, this.title.getTitle()) : this.title.getTitle(); this.destroyed$ = new Subject(); this.navigationEnd$ = this.router.events.pipe(filter(isNavigationEndEvent)); this.routeData$ = this.navigationEnd$.pipe(map(() => this.router.routerState.root), map((route) => this.getLastRoute(route)), filter((route) => route.outlet === 'primary'), switchMap((route) => route.data)); } /** * Returns original title of index page. * * If SSR was used with {@link StateTransfer} - * then original title will be preserved between server and client states */ getOriginalTitle() { return this.originalTitle; } /** @internal */ ngOnDestroy() { this.destroyed$.next(); } /** @internal */ _setup() { if (this.initialized) { return; } this.initialized = true; if (this.transferState && isPlatformServer(this.platformId)) { this.transferState.set(TITLE_STATE, this.originalTitle); } combineLatest(this.routeData$, this.contextService.getContext()) .pipe(takeUntil(this.destroyed$)) .subscribe(([data, ctx]) => this.updateAllMeta(data, ctx)); } updateAllMeta(data, ctx = {}) { const _a = this.defaultMeta, { _templates_: defaultTemplates } = _a, defaultAllMeta = __rest(_a, ["_templates_"]); if (!isDataWithMeta(data)) { this.resetAllMeta(ctx, defaultAllMeta); return; } const { meta } = data; const { _templates_ } = meta, allMeta = __rest(meta, ["_templates_"]); const { title } = allMeta, otherMeta = __rest(allMeta, ["title"]); const { title: _ } = defaultAllMeta, defaultOtherMeta = __rest(defaultAllMeta, ["title"]); const allMetaDefaulted = Object.assign(Object.assign({}, defaultAllMeta), allMeta); const otherMetaDefaulted = Object.assign(Object.assign({}, defaultOtherMeta), otherMeta); const templates = Object.assign(Object.assign({}, defaultTemplates), _templates_); const processedMeta = this.contextService._processContext(allMetaDefaulted); this.updateTitle(title, ctx, templates, processedMeta); Object.keys(otherMetaDefaulted).forEach((name) => this.updateMeta(name, otherMetaDefaulted[name], ctx, templates, processedMeta)); // Cleanup leftover meta tags Object.keys(this.metaTags) .filter((name) => name in otherMetaDefaulted === false) .forEach((name) => this.resetMeta(name, ctx, templates, processedMeta)); } resetAllMeta(ctx, defaultMeta) { const { title } = defaultMeta, defaultOtherMeta = __rest(defaultMeta, ["title"]); const metaDefaulted = Object.assign(Object.assign({}, defaultOtherMeta), this.metaTags); const meta = this.contextService._processContext(defaultMeta); this.resetTitle(ctx, this.defaultMeta._templates_, meta); Object.keys(metaDefaulted).forEach((name) => this.resetMeta(name, ctx, this.defaultMeta._templates_, meta)); } updateTitle(title, ctx, templates, meta) { if (title) { this.title.setTitle(this.contextService._templateStr(title, ctx, { name: 'title', templates, meta, })); } else { this.resetTitle(ctx, templates, meta); } } resetTitle(ctx, templates, meta) { this.title.setTitle(this.contextService._templateStr(this.defaultMeta.title || this.originalTitle, ctx, { name: 'title', meta, templates, })); } updateMeta(name, content, ctx, templates, meta) { if (content) { const metaDef = typeof content === 'string' ? { name, content } : content; metaDef.content = this.contextService._templateStr(metaDef.content, ctx, { name, meta, templates, }); this.meta.updateTag(metaDef); this.metaTags[name] = true; } else { this.resetMeta(name, ctx, templates, meta); } } resetMeta(name, ctx, templates, meta) { const defaultMeta = this.defaultMeta[name]; if (defaultMeta) { this.updateMeta(name, defaultMeta, ctx, templates, meta); } else { this.meta.removeTag(`name="${name}"`); delete this.metaTags[name]; } } getLastRoute(route) { while (route.firstChild) { route = route.firstChild; } return route; } } RouterMetaService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.5", ngImport: i0, type: RouterMetaService, deps: [{ token: PLATFORM_ID }, { token: ROUTE_META_CONFIG }, { token: i1.Router }, { token: i2.Title }, { token: i2.Meta }, { token: RouterMetaContextService }, { token: i2.TransferState, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); RouterMetaService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.0.5", ngImport: i0, type: RouterMetaService, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.5", ngImport: i0, type: RouterMetaService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: function () { return [{ type: undefined, decorators: [{ type: Inject, args: [PLATFORM_ID] }] }, { type: undefined, decorators: [{ type: Inject, args: [ROUTE_META_CONFIG] }] }, { type: i1.Router }, { type: i2.Title }, { type: i2.Meta }, { type: RouterMetaContextService }, { type: i2.TransferState, decorators: [{ type: Optional }] }]; } }); class RouterMetaModule { constructor(parentModule, routerMetaService) { if (!parentModule) { routerMetaService._setup(); } } static forRoot(config = {}) { return { ngModule: RouterMetaModule, providers: [ config.configProvider || { provide: ROUTE_META_CONFIG, useValue: config.config, }, ], }; } } RouterMetaModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.5", ngImport: i0, type: RouterMetaModule, deps: [{ token: RouterMetaModule, optional: true, skipSelf: true }, { token: RouterMetaService }], target: i0.ɵɵFactoryTarget.NgModule }); RouterMetaModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "14.0.5", ngImport: i0, type: RouterMetaModule }); RouterMetaModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "14.0.5", ngImport: i0, type: RouterMetaModule }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.5", ngImport: i0, type: RouterMetaModule, decorators: [{ type: NgModule, args: [{}] }], ctorParameters: function () { return [{ type: RouterMetaModule, decorators: [{ type: Optional }, { type: SkipSelf }] }, { type: RouterMetaService }]; } }); /* * Public API Surface of ngx-router-meta */ /** * Generated bundle index. Do not edit. */ export { ROUTE_META_CONFIG, RouterMetaContextService, RouterMetaModule, RouterMetaService, isDataWithMeta, unfoldContext }; //# sourceMappingURL=ngx-router-meta.mjs.map