angular-traversal
Version:
A traversal navigation library for Angular (v2+)
556 lines (543 loc) • 20.2 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, InjectionToken, ComponentFactoryResolver, Compiler, Injector, Optional, Inject, Directive, ViewContainerRef, ChangeDetectorRef, Input, HostListener, HostBinding, NgModule } from '@angular/core';
import { HttpParams, HttpClientModule, HttpHeaders, HttpClient } from '@angular/common/http';
import * as i1 from '@angular/common';
import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common';
import { Subject, BehaviorSubject, of } from 'rxjs';
import { withLatestFrom, take, map, takeUntil, skip } from 'rxjs/operators';
class Resolver {
}
Resolver.ɵprov = i0.ɵɵdefineInjectable({ factory: function Resolver_Factory() { return new Resolver(); }, token: Resolver, providedIn: "root" });
Resolver.decorators = [
{ type: Injectable, args: [{
providedIn: 'root',
},] }
];
class Marker {
}
Marker.ɵprov = i0.ɵɵdefineInjectable({ factory: function Marker_Factory() { return new Marker(); }, token: Marker, providedIn: "root" });
Marker.decorators = [
{ type: Injectable, args: [{
providedIn: 'root',
},] }
];
class Normalizer {
normalize(path) {
return path;
}
}
Normalizer.ɵprov = i0.ɵɵdefineInjectable({ factory: function Normalizer_Factory() { return new Normalizer(); }, token: Normalizer, providedIn: "root" });
Normalizer.decorators = [
{ type: Injectable, args: [{
providedIn: 'root',
},] }
];
const NAVIGATION_PREFIX = new InjectionToken('traversal.prefix');
class Traverser {
constructor(location, resolver, marker, normalizer, ngResolver, compiler, injector, prefix) {
this.location = location;
this.resolver = resolver;
this.marker = marker;
this.normalizer = normalizer;
this.ngResolver = ngResolver;
this.compiler = compiler;
this.injector = injector;
this.prefix = prefix;
this.beforeTraverse = new Subject();
this.traverseTo = new Subject();
this.echo = new Subject();
this.canTraverse = new Subject();
this.tilesContexts = {};
this.tileUpdates = new Subject();
this.views = {};
this.lazy = {};
this.lazyModules = {};
this.tiles = {};
this.target = new BehaviorSubject(this.getEmptyTarget());
let answers = [];
this.echo.subscribe((ok) => {
answers.push(ok);
if (answers.length === this.beforeTraverse.observers.length) {
this.canTraverse.next(answers.every((yes) => yes));
}
});
this.traverseTo.pipe(withLatestFrom(this.canTraverse)).subscribe(([traverse, ok]) => {
if (ok) {
this._traverse(traverse.path, traverse.navigate);
}
else {
this.location.replaceState(this.target.getValue().path);
}
answers = [];
});
}
traverse(path, navigate = true) {
path = this.normalizer.normalize(this.getFullPath(path));
const obsCount = this.beforeTraverse.observers.length;
if (obsCount > 0) {
this.beforeTraverse.next([this.echo, path]);
}
else {
this.canTraverse.next(true);
}
this.traverseTo.next({ path, navigate });
}
_traverse(path, navigate) {
let contextPath = path;
let queryString = '';
let view = 'view';
if (path.indexOf('?') > -1) {
[contextPath, queryString] = contextPath.split('?');
}
else if (path.indexOf(';') > -1) {
[contextPath, queryString] = contextPath.split(';');
}
if (path.indexOf('@@') > -1) {
view = contextPath.split('@@')[1];
contextPath = contextPath.split('@@')[0];
if (contextPath.length > 1 && contextPath.slice(-1) === '/') {
contextPath = contextPath.slice(0, -1);
}
}
if (navigate) {
let navigateTo = path;
if (!contextPath) {
// if no contextPath, preserve the previous one
if (navigateTo[0] !== '/') {
navigateTo = '/' + navigateTo;
}
navigateTo = this.target.value.contextPath + navigateTo;
}
if (this.location.path() !== this.getPrefix() + navigateTo) {
this.location.go(this.getPrefix() + navigateTo);
}
}
let components = this.views[view];
if (!components && view.includes('/')) {
const prefix = view.split('/')[0];
if (!!this.lazyModules[prefix] && this.views[prefix]) {
components = this.views[prefix];
}
}
this.emitTarget(path, contextPath, queryString, view, this.target, components);
}
traverseHere(includeHash = true) {
const here = this.location.path().replace(this.getPrefix(), '');
this.traverse(!includeHash ? here.split('?')[0] : here);
}
addView(name, target, component) {
if (!this.views[name]) {
this.views[name] = {};
}
this.views[name][target] = component;
}
addLazyView(name, target, loader) {
if (!this.views[name]) {
this.views[name] = {};
}
const id = name + ';' + target;
this.views[name][target] = id;
this.lazy[id] = loader;
this.lazyModules[name] = true;
}
loadLazyView(moduleId, viewName, isTile = false) {
return this.lazy[moduleId]().then((module) => {
this.compiler.compileModuleAsync(module).then((factory) => {
factory.create(this.injector);
});
const moduleViews = module.traverserViews || [];
moduleViews.forEach((view) => {
this.views[view.name] = !!this.views[view.name]
? Object.assign(Object.assign({}, this.views[view.name]), view.components) : view.components;
});
const moduleTiles = module.traverserTiles || [];
moduleTiles.forEach((tile) => {
this.tiles[tile.name] = !!this.tiles[tile.name]
? Object.assign(Object.assign({}, this.tiles[tile.name]), tile.components) : tile.components;
});
const target = moduleId.split(';')[1];
return (isTile ? this.tiles : this.views)[viewName][target];
});
}
addTile(name, target, component) {
if (!this.tiles[name]) {
this.tiles[name] = {};
}
this.tiles[name][target] = component;
this.tilesContexts[name] = new BehaviorSubject(this.getEmptyTarget());
}
loadTile(name, path) {
path = this.normalizer.normalize(this.getFullPath(path));
let contextPath = path;
let queryString = '';
if (path.includes('?')) {
[contextPath, queryString] = contextPath.split('?');
}
this.emitTarget(path, contextPath, queryString, name, this.tilesContexts[name], this.tiles[name], true);
}
emptyTile(name) {
if (!!this.tilesContexts[name]) {
this._emit(this.tilesContexts[name], this.getEmptyTarget(), name, true);
}
}
addLazyTile(name, target, loader) {
if (!this.tiles[name]) {
this.tiles[name] = {};
}
const id = name + ';' + target;
this.tiles[name][target] = id;
this.tilesContexts[name] = new BehaviorSubject(this.getEmptyTarget());
this.lazy[id] = loader;
this.lazyModules[name] = true;
}
emitTarget(path, contextPath, queryString, viewOrTile, targetObs, components, isTile = false, currentContext) {
if (!!targetObs && !!components) {
let resolver;
if (!!currentContext) {
resolver = of(currentContext);
}
else if (!contextPath && // if we have no context path
Object.keys(targetObs.value.context).length > 0 && // and we have context
// and query string did not change
!!targetObs.value.query &&
queryString === Object.assign(new HttpParams(), targetObs.value.query).toString()) {
// then we keep the current context
resolver = of(targetObs.value.context);
contextPath = targetObs.value.contextPath;
}
else {
resolver = this._resolve(contextPath, viewOrTile, queryString);
}
if (resolver) {
resolver.pipe(take(1)).subscribe((context) => {
const marker = this.marker.mark(context);
let component = '';
if (marker instanceof Array) {
const matches = marker.filter((m) => components[m]);
if (matches.length > 0) {
component = components[matches[0]];
}
}
else {
component = components[marker];
}
if (!component) {
component = components['*'];
}
if (!!component) {
const promise = typeof component === 'string'
? this.loadLazyView(component, viewOrTile, isTile)
: Promise.resolve(component);
promise.then((comp) => {
const target = !!component
? {
context,
path,
prefixedPath: this.getPrefix() + path,
contextPath,
prefixedContextPath: this.getPrefix() + contextPath,
view: viewOrTile,
component: comp,
query: new HttpParams({ fromString: queryString || '' }),
}
: this.getEmptyTarget();
this._emit(targetObs, target, viewOrTile, isTile);
});
}
else {
this._emit(targetObs, this.getEmptyTarget(), viewOrTile, isTile);
}
});
}
}
}
_emit(targetObs, target, viewOrTile, isTile) {
targetObs.next(target);
if (isTile) {
this.tileUpdates.next({
tile: viewOrTile,
target,
});
}
}
_resolve(path, view, queryString) {
return this.resolver.resolve(path, view, queryString);
}
resolve(path, view, queryString) {
return this._resolve(this.normalizer.normalize(this.getFullPath(path)), view, queryString);
}
getFullPath(path) {
if (path === '.') {
path = this.target.value.contextPath;
}
else if (path.startsWith('./')) {
path =
this.target.value.contextPath === '/' ? path.slice(1) : this.target.value.contextPath + path.slice(1);
}
else if (path.startsWith('../') || path === '..') {
const current = this.target.value.contextPath.split('/');
path = path
.split('/')
.reduce((all, chunk) => {
if (chunk === '..') {
all.pop();
}
else {
all.push(chunk);
}
return all;
}, current)
.join('/');
}
return path;
}
getComponent(component) {
return this.ngResolver.resolveComponentFactory(component);
}
getEmptyTarget() {
return {
component: null,
context: {},
contextPath: '',
prefixedContextPath: this.getPrefix(),
path: '',
prefixedPath: this.getPrefix(),
query: new HttpParams(),
view: 'view',
};
}
getQueryParams() {
return this.target.pipe(take(1), map((target) => {
const queryParams = Object.assign(new HttpParams(), target.query);
if (!queryParams) {
return {};
}
else {
return queryParams.keys().reduce((all, key) => {
const value = queryParams.getAll(key);
if (!value) {
return all;
}
if (value.length > 1) {
all[key] = value;
}
else {
all[key] = value[0];
}
return all;
}, {});
}
}));
}
getPrefix() {
return !!this.prefix ? this.prefix.getValue() : '';
}
setPrefix(prefix) {
this.prefix.next(prefix.startsWith('/') ? prefix : `/${prefix}`);
}
}
Traverser.ɵprov = i0.ɵɵdefineInjectable({ factory: function Traverser_Factory() { return new Traverser(i0.ɵɵinject(i1.Location), i0.ɵɵinject(Resolver), i0.ɵɵinject(Marker), i0.ɵɵinject(Normalizer), i0.ɵɵinject(i0.ComponentFactoryResolver), i0.ɵɵinject(i0.Compiler), i0.ɵɵinject(i0.INJECTOR), i0.ɵɵinject(NAVIGATION_PREFIX, 8)); }, token: Traverser, providedIn: "root" });
Traverser.decorators = [
{ type: Injectable, args: [{
providedIn: 'root',
},] }
];
Traverser.ctorParameters = () => [
{ type: Location },
{ type: Resolver },
{ type: Marker },
{ type: Normalizer },
{ type: ComponentFactoryResolver },
{ type: Compiler },
{ type: Injector },
{ type: BehaviorSubject, decorators: [{ type: Optional }, { type: Inject, args: [NAVIGATION_PREFIX,] }] }
];
class TraverserOutlet {
constructor(traverser, location, container, cdr) {
this.traverser = traverser;
this.location = location;
this.container = container;
this.cdr = cdr;
this.terminator = new Subject();
}
ngOnInit() {
this.traverser.target.pipe(takeUntil(this.terminator)).subscribe((target) => this.render(target));
this.traverser.traverse(this.location.path().replace(this.traverser.getPrefix(), ''), false);
this.location.subscribe((loc) => {
const path = (loc.url || '').replace(this.traverser.getPrefix(), '');
this.traverser.traverse(path || '/', false); // when empty string traverse to root
});
}
ngOnDestroy() {
if (this.viewInstance) {
this.viewInstance.destroy();
}
this.terminator.next();
}
render(target) {
if (this.viewInstance) {
this.viewInstance.destroy();
}
if (target.component) {
const componentFactory = this.traverser.getComponent(target.component);
this.viewInstance = this.container.createComponent(componentFactory);
}
if (!this.cdr.destroyed) {
this.cdr.detectChanges();
}
}
}
TraverserOutlet.decorators = [
{ type: Directive, args: [{
selector: 'traverser-outlet',
},] }
];
TraverserOutlet.ctorParameters = () => [
{ type: Traverser },
{ type: Location },
{ type: ViewContainerRef },
{ type: ChangeDetectorRef }
];
class TraverserButton {
constructor(traverser) {
this.traverser = traverser;
}
onClick(event) {
event.preventDefault();
if (!!this.traverseTo) {
this.traverser.traverse(this.traverseTo);
}
}
}
TraverserButton.decorators = [
{ type: Directive, args: [{
selector: ':not(a)[traverseTo]',
},] }
];
TraverserButton.ctorParameters = () => [
{ type: Traverser }
];
TraverserButton.propDecorators = {
traverseTo: [{ type: Input }],
onClick: [{ type: HostListener, args: ['click', ['$event'],] }]
};
class TraverserLink extends TraverserButton {
constructor(privateTraverser, normalizer) {
super(privateTraverser);
this.privateTraverser = privateTraverser;
this.normalizer = normalizer;
}
ngOnInit() {
if (!!this.traverseTo) {
this.href =
this.privateTraverser.getPrefix() +
this.normalizer.normalize(this.privateTraverser.getFullPath(this.traverseTo));
}
}
}
TraverserLink.decorators = [
{ type: Directive, args: [{
selector: 'a[traverseTo]',
},] }
];
TraverserLink.ctorParameters = () => [
{ type: Traverser },
{ type: Normalizer }
];
TraverserLink.propDecorators = {
href: [{ type: HostBinding }]
};
class TraverserTile {
constructor(traverser, container, cdr) {
this.traverser = traverser;
this.container = container;
this.cdr = cdr;
this.noUpdateOnTraverse = false;
this.terminator = new Subject();
}
ngOnInit() {
if (!!this.name) {
if (!this.noUpdateOnTraverse) {
this.traverser.target
.pipe(takeUntil(this.terminator), skip(1))
.subscribe((target) => this.traverser.loadTile(this.name, target.contextPath));
}
const tileContext = this.traverser.tilesContexts[this.name];
if (!!tileContext) {
tileContext.pipe(takeUntil(this.terminator)).subscribe((target) => this.render(target));
}
}
}
ngOnDestroy() {
if (this.viewInstance) {
this.viewInstance.destroy();
}
this.terminator.next();
}
render(target) {
if (this.viewInstance) {
this.viewInstance.destroy();
}
if (target.component) {
const componentFactory = this.traverser.getComponent(target.component);
this.viewInstance = this.container.createComponent(componentFactory);
}
if (!this.cdr.destroyed) {
this.cdr.detectChanges();
}
}
}
TraverserTile.decorators = [
{ type: Directive, args: [{
selector: 'traverser-tile',
},] }
];
TraverserTile.ctorParameters = () => [
{ type: Traverser },
{ type: ViewContainerRef },
{ type: ChangeDetectorRef }
];
TraverserTile.propDecorators = {
name: [{ type: Input }],
noUpdateOnTraverse: [{ type: Input }]
};
class TraversalModule {
}
TraversalModule.decorators = [
{ type: NgModule, args: [{
declarations: [TraverserOutlet, TraverserButton, TraverserLink, TraverserTile],
imports: [HttpClientModule],
exports: [TraverserOutlet, TraverserButton, TraverserLink, TraverserTile],
providers: [Location, { provide: LocationStrategy, useClass: PathLocationStrategy }],
},] }
];
// the actual value will be provided by the module
let BACKEND_BASE_URL = new InjectionToken('backend.base.url');
class BasicHttpResolver extends Resolver {
constructor(http, backend) {
super();
this.http = http;
this.backend = backend;
}
resolve(path, view, queryString) {
const headers = new HttpHeaders()
.append('Accept', 'application/json')
.append('Content-Type', 'application/json');
return this.http.get(this.backend + path, { headers });
}
}
BasicHttpResolver.decorators = [
{ type: Injectable }
];
BasicHttpResolver.ctorParameters = () => [
{ type: HttpClient },
{ type: String, decorators: [{ type: Inject, args: [BACKEND_BASE_URL,] }] }
];
/*
* Public API Surface of traversal
*/
/**
* Generated bundle index. Do not edit.
*/
export { BACKEND_BASE_URL, BasicHttpResolver, Marker, NAVIGATION_PREFIX, Normalizer, Resolver, TraversalModule, Traverser, TraverserButton, TraverserLink, TraverserOutlet, TraverserTile };
//# sourceMappingURL=angular-traversal.js.map