UNPKG

angular-traversal

Version:
556 lines (543 loc) 20.2 kB
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