ngx-router-meta
Version:
Configure HTML meta tags in route configuration of Angular
348 lines (339 loc) • 15.1 kB
JavaScript
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