UNPKG

@dotglitch/ngx-common

Version:

Angular components and utilities that are commonly used.

1,067 lines (1,055 loc) 126 kB
import * as i0 from '@angular/core'; import { InjectionToken, Input, Optional, Inject, Directive, Pipe, Injectable, EventEmitter, isDevMode, ViewContainerRef, Output, ViewChild, Component, HostListener, NgModule, TemplateRef, ContentChild } from '@angular/core'; import { createInstance, INDEXEDDB } from 'localforage'; import * as i1 from '@angular/platform-browser'; import { createApplication } from '@angular/platform-browser'; import { DOCUMENT, NgComponentOutlet, NgTemplateOutlet } from '@angular/common'; import * as i2$1 from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog'; import { debounceTime, of, Subject, BehaviorSubject, firstValueFrom } from 'rxjs'; import * as i2 from '@angular/cdk/dialog'; import { retry } from 'rxjs/operators'; import * as i1$1 from '@angular/common/http'; import { __decorate, __param } from 'tslib'; import * as i4 from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon'; import * as i5 from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import * as i3 from '@angular/cdk/portal'; import { ComponentPortal, PortalModule } from '@angular/cdk/portal'; import { ulid } from 'ulidx'; const storage = createInstance({ name: "@dotglitch", storeName: "image-cache", driver: INDEXEDDB, version: 1 }); const imageCache = {}; const loadingSvg = `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32px" height="32px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid"><circle cx="50" cy="50" fill="none" stroke="%2340c4ff" stroke-width="10" r="35" stroke-dasharray="164.93361431346415 56.97787143782138"><animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" values="0 50 50;360 50 50" keyTimes="0;1"></animateTransform></circle><!-- [ldio] generated by https://loading.io/ --></svg>`; const brokenSvg = `data:image/svg+xml;utf8,<svg width="800" height="800" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"><line x1="10.08" y1="8.29" x2="10.18" y2="8.29" style="fill:none;stroke:#000000;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round" /><path d="m 10.51,14.8 5.2,5.2 H 20 a 1,1 0 0 0 1,-1 V 15.73 L 15.29,10 Z M 3,16.71 V 19 a 1,1 0 0 0 1,1 h 11.71 l -8,-8 z M 21,5 v 14 a 1,1 0 0 1 -1,1 H 4 A 1,1 0 0 1 3,19 V 5 A 1,1 0 0 1 4,4 h 16 a 1,1 0 0 1 1,1 z" style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round" /><path d="M 21.193388,21.193388 2.8066108,2.8066108 m 18.3867772,0 L 2.8066108,21.193388" style="stroke:%23ff0000;stroke-width:2.62668;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" /></svg>`; const NGX_IMAGE_CACHE_CONFIG = new InjectionToken('ngx-image-cache-config'); class NgxImageCacheDirective { get el() { return this.element.nativeElement; } constructor(element, cacheConfig) { this.element = element; this.cacheConfig = cacheConfig; } ngOnChanges() { this.getCachedImage(); } async getCachedImage() { if (this.el.src?.trim() == this.url?.trim() || // Check that there's an actual change this.url?.trim().length == 0 // Check that there's an actual URL ) return; // Check if it's in the memory cache if (imageCache[this.url]) { const image = imageCache[this.url]; // If the image is currently loading, show the loader // and add it to the reflist if (image['_loading'] == true) { image['_refs'].push(this.el); this.el.setAttribute("loading", "true"); this.el.src = this.cacheConfig?.loadingPlaceholder || loadingSvg; } else { // The image is fully loaded, swap out the src with a data-uri this.el.setAttribute("loading", "false"); this.el.src = image.src; } // If it's already in the image cache, we're going to trust that it loads properly. return; } // Check if it's in indexedDB if (this.configuration?.cacheInIndexedDB != false) { const cached = await storage.getItem(this.url); if (cached) { // Attempt to load the base64 data from indexeddb. // If this fails, we'll fall back to attempting to download the image this.el.src = cached.data; const evt = await new Promise(res => { this.el.addEventListener('load', res); this.el.addEventListener('error', res); }); // If the event isn't an error if (evt.type == "load") { this.el.setAttribute("loading", "false"); if (this.configuration?.cacheInMemory != false) { // Successfully loaded into element // Create an entry in the memory cache const image = imageCache[this.url] = new Image(); image.src = cached.data; image['_createdAt'] = Date.now(); } return; } else { // Else, we try to load again. this.el.src = this.cacheConfig?.loadingPlaceholder || loadingSvg; } } } const image = (() => { if (this.configuration?.cacheInMemory != false) { return imageCache[this.url] = new Image(); } return new Image(); })(); // const clone = image.cloneNode(true) as HTMLImageElement; image['_refs'] = image['_refs'] ?? []; image['_refs'].push(this.el); image['_loading'] = true; image['_createdAt'] = Date.now(); // Show a loader while the image downloads. this.el.setAttribute("loading", "true"); this.el.src = this.cacheConfig?.loadingPlaceholder || loadingSvg; // Fetch the image via JS and cache it as base64 window.fetch(this.url) .then(response => response.blob()) .then(blob => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => { image.src = reader.result; storage.setItem(this.url, { timestamp: Date.now(), data: reader.result }); image['_refs'].forEach((ref) => { ref.src = image.src; }); image['_loading'] = false; resolve(0); }; reader.onerror = reject; reader.readAsDataURL(blob); })) .catch(err => { // If a failure occurs, purge this entry from the cache // TODO: Render better "broken" image delete imageCache[this.url]; image['_refs'].forEach((ref) => { ref.src = this.cacheConfig?.brokenPlaceholder || brokenSvg; ref.setAttribute("loading", "failed"); }); }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NgxImageCacheDirective, deps: [{ token: i0.ElementRef }, { token: NGX_IMAGE_CACHE_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.3.12", type: NgxImageCacheDirective, isStandalone: true, selector: "img[ngx-cache]", inputs: { url: ["source", "url"], configuration: ["ngx-cache-config", "configuration"] }, usesOnChanges: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NgxImageCacheDirective, decorators: [{ type: Directive, args: [{ selector: 'img[ngx-cache]', standalone: true }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [NGX_IMAGE_CACHE_CONFIG] }] }], propDecorators: { url: [{ type: Input, args: ["source"] }, { type: Input, args: ["ngx-cache"] }], configuration: [{ type: Input, args: ["ngx-cache-config"] }] } }); /** * Url Sanitizer pipe. * * This trusts URLs that exist in a safe list defined in our environments.ts file. * Any other URLs will NOT be trusted, thus will not be loaded. */ class HtmlBypass { constructor(sanitizer) { this.sanitizer = sanitizer; } transform(url) { return this.sanitizer.bypassSecurityTrustHtml(url); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: HtmlBypass, deps: [{ token: i1.DomSanitizer }], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "17.3.12", ngImport: i0, type: HtmlBypass, isStandalone: true, name: "htmlbypass" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: HtmlBypass, decorators: [{ type: Pipe, args: [{ name: 'htmlbypass', standalone: true }] }], ctorParameters: () => [{ type: i1.DomSanitizer }] }); /** * Url Sanitizer pipe. * * This trusts URLs that exist in a safe list defined in our environments.ts file. * Any other URLs will NOT be trusted, thus will not be loaded. */ class ResourceBypass { constructor(sanitizer) { this.sanitizer = sanitizer; } transform(url) { return this.sanitizer.bypassSecurityTrustResourceUrl(url); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ResourceBypass, deps: [{ token: i1.DomSanitizer }], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "17.3.12", ngImport: i0, type: ResourceBypass, isStandalone: true, name: "resourcebypass" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ResourceBypass, decorators: [{ type: Pipe, args: [{ name: 'resourcebypass', standalone: true }] }], ctorParameters: () => [{ type: i1.DomSanitizer }] }); /** * Url Sanitizer pipe. * * This trusts URLs that exist in a safe list defined in our environments.ts file. * Any other URLs will NOT be trusted, thus will not be loaded. */ class ScriptBypass { constructor(sanitizer) { this.sanitizer = sanitizer; } transform(url) { return this.sanitizer.bypassSecurityTrustScript(url); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ScriptBypass, deps: [{ token: i1.DomSanitizer }], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "17.3.12", ngImport: i0, type: ScriptBypass, isStandalone: true, name: "scriptbypass" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ScriptBypass, decorators: [{ type: Pipe, args: [{ name: 'scriptbypass', standalone: true }] }], ctorParameters: () => [{ type: i1.DomSanitizer }] }); /** * Url Sanitizer pipe. * * This trusts URLs that exist in a safe list defined in our environments.ts file. * Any other URLs will NOT be trusted, thus will not be loaded. */ class StyleBypass { constructor(sanitizer) { this.sanitizer = sanitizer; } transform(url) { return this.sanitizer.bypassSecurityTrustStyle(url); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: StyleBypass, deps: [{ token: i1.DomSanitizer }], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "17.3.12", ngImport: i0, type: StyleBypass, isStandalone: true, name: "stylebypass" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: StyleBypass, decorators: [{ type: Pipe, args: [{ name: 'stylebypass', standalone: true }] }], ctorParameters: () => [{ type: i1.DomSanitizer }] }); /** * Url Sanitizer pipe. * * This trusts URLs that exist in a safe list defined in our environments.ts file. * Any other URLs will NOT be trusted, thus will not be loaded. */ class UrlBypass { constructor(sanitizer) { this.sanitizer = sanitizer; } transform(url) { return this.sanitizer.bypassSecurityTrustUrl(url); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: UrlBypass, deps: [{ token: i1.DomSanitizer }], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "17.3.12", ngImport: i0, type: UrlBypass, isStandalone: true, name: "urlbypass" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: UrlBypass, decorators: [{ type: Pipe, args: [{ name: 'urlbypass', standalone: true }] }], ctorParameters: () => [{ type: i1.DomSanitizer }] }); const sleep = ms => new Promise(r => setTimeout(r, ms)); /** * Prompt the user to save a json file of the given object. */ const saveObjectAsFile = (name, data) => { const a = document.createElement("a"); const file = new Blob([JSON.stringify(data)], { type: "application/json" }); a.href = URL.createObjectURL(file); a.download = name; a.click(); a.remove(); }; /** * Convert a string `fooBAR baz_160054''"1]"` into a slug: `foobar-baz-1600541` */ const stringToSlug = (text) => (text || '') .trim() .toLowerCase() .replace(/[\-_+ ]/g, '-') .replace(/[^a-z0-9\-\/]/g, ''); /** * Helper to update the page URL. * @param page component page ID to load. * @param data string or JSON data for query params. */ const updateUrl = (page, data = {}, replaceState = false) => { const [oldHash, qstring] = location.hash.split('?'); if (!page) page = oldHash.split('/')[1]; const hash = `#/${page}`; // Convert the data object to JSON. if (data instanceof URLSearchParams) { data = [...data.entries()].map(([k, v]) => ({ [k]: v })).reduce((a, b) => ({ ...a, ...b }), {}); } const query = new URLSearchParams(data); const prevParams = new URLSearchParams(qstring); // If the hash is the same, retain params. if (hash == oldHash) { replaceState = true; for (const [key, value] of prevParams.entries()) if (!query.has(key)) query.set(key, prevParams.get(key)); } for (const [key, val] of query.entries()) { if (val == null || val == undefined || val == '' || val == 'null' || Number.isNaN(val) || val == 'NaN') query.delete(key); } if (!(hash.toLowerCase() == "#/frame") || data['id'] == -1) query.delete('id'); const strQuery = query.toString(); console.log(data, hash, strQuery); if (replaceState) { window.history.replaceState(data, '', hash + (strQuery ? ('?' + strQuery) : '')); } else { window.history.pushState(data, '', hash + (strQuery ? ('?' + strQuery) : '')); } }; const getUrlData = (source = window.location.hash) => { const [hash, query] = source.split('?'); let data = new URLSearchParams(query); return [...data.entries()].map(([k, v]) => ({ [k]: v })).reduce((a, b) => ({ ...a, ...b }), {}); }; const SCRIPT_INIT_TIMEOUT = 500; // ms /** * Service that installs CSS/JS dynamically */ class DependencyService { constructor(document) { this.document = document; } /** * Install a Javascript file into the webpage on-demand * @param id Unique identifier for the JS script * @param src URL of the script * @param globalkey A global object the script will provide. * Providing this will ensure a promise only resolves after the * specified global object is provided, with a timeout of 500ms */ loadScript(id, src, globalkey = null) { return new Promise((res, rej) => { if (this.document.getElementById(id)) return res(); const script = this.document.createElement('script'); script.id = id; script.setAttribute("async", ''); script.setAttribute("src", src); script.onload = async () => { if (typeof globalkey == "string") { let i = 0; for (; !window[globalkey] && i < SCRIPT_INIT_TIMEOUT; i += 10) await sleep(10); if (i >= SCRIPT_INIT_TIMEOUT) { return rej(new Error("Timed out waiting for script to self-initialize.")); } } res(); }; this.document.body.appendChild(script); }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DependencyService, deps: [{ token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DependencyService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DependencyService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: Document, decorators: [{ type: Inject, args: [DOCUMENT] }] }] }); var ComponentResolveStrategy; (function (ComponentResolveStrategy) { /** * Match the fist component we find * (best used for standalone components) * @default */ ComponentResolveStrategy[ComponentResolveStrategy["PickFirst"] = 0] = "PickFirst"; /** * Perform an Exact ID to Classname of the Component * case sensitive, zero tolerance. */ ComponentResolveStrategy[ComponentResolveStrategy["MatchIdToClassName"] = 1] = "MatchIdToClassName"; /** * Perform a fuzzy ID to classname match * case insensitive, mutes symbols * ignores "Component" and "Module" postfixes on class * names */ ComponentResolveStrategy[ComponentResolveStrategy["FuzzyIdClassName"] = 2] = "FuzzyIdClassName"; /** * Use a user-provided component match function */ ComponentResolveStrategy[ComponentResolveStrategy["Custom"] = 3] = "Custom"; })(ComponentResolveStrategy || (ComponentResolveStrategy = {})); // Monkey-patch the type of these symbols. const $id = Symbol("id"); const $group = Symbol("group"); const NGX_LAZY_LOADER_CONFIG = new InjectionToken('lazyloader-config'); class LazyLoaderService { get err() { return LazyLoaderService.config.logger.err; } get log() { return LazyLoaderService.config.logger.log; } get warn() { return LazyLoaderService.config.logger.warn; } // A proxied registry that mutates reference keys static { this.registry = {}; } constructor(config = {}) { // Ensure this is singleton and works regardless of special instancing requirements. LazyLoaderService.configure(config); } static configure(config) { this.config = { componentResolveStrategy: ComponentResolveStrategy.PickFirst, logger: { log: console.log, warn: console.warn, err: console.error }, ...config }; config?.entries?.forEach(e => this.addComponentToRegistry(e)); // If a custom resolution strategy is provided but no resolution function is passed, // we throw an error if (this.config.componentResolveStrategy == ComponentResolveStrategy.Custom && !this.config.customResolver) { throw new Error("Cannot initialize. Configuration specifies a custom resolve matcher but none was provided"); } if (this.config.loaderDistractorComponent && this.config.loaderDistractorTemplate) throw new Error("Cannot have both a Component and Template for Distractor view."); if (this.config.errorComponent && this.config.errorTemplate) throw new Error("Cannot have both a Component and Template for Error view."); if (this.config.notFoundComponent && this.config.notFoundTemplate) throw new Error("Cannot have both a Component and Template for NotFound view."); } static addComponentToRegistry(registration) { if (!registration) throw new Error("Cannot add <undefined> component into registry."); // Clone the object into our repository and transfer the id into a standardized slug format const id = stringToSlug(registration.id ?? Date.now().toString()); // purge non-basic ASCII chars const group = registration.group || "default"; registration[$id] = id; registration[$group] = id; if (!this.registry[group]) this.registry[group] = []; // Check if we already have a registration for the component // if (this.registry[group] && typeof this.registry[group]['load'] == "function") { // // Warn the developer that the state is problematic // this.config.logger.warn( // `A previous entry already exists for ${id}! The old registration will be overridden.` + // `Please ensure you use groups if you intend to have duplicate component ids. ` + // `If this was intentional, first remove the old component from the registry before adding a new instance` // ); // // If we're in dev mode, break the loader surface // if (isDevMode()) // return; // } this.registry[group].push(registration); } /** * Register an Angular component * @param id identifier that is used to resolve the component * @param group * @param component Angular Component Class constructor */ registerComponent(args) { if (this.isComponentRegistered(args.id, args.group)) { this.log(`Will not re-register component '${args.id}' in group '${args.group || 'default'}' `); return; } LazyLoaderService.addComponentToRegistry({ id: stringToSlug(args.id), matcher: args.matcher, group: stringToSlug(args.group || "default"), load: args.load || (() => args.component) }); } /** * * @param id * @param group */ unregisterComponent(id, group = "default") { const _id = stringToSlug(id); const _group = stringToSlug(group); if (!this.resolveRegistrationEntry(id, group)) throw new Error("Cannot unregister component ${}! Component is not present in registry"); // TODO: handle clearing running instances delete LazyLoaderService.registry[_group][_id]; } /** * Get the registration entry for a component. * Returns null if component is not in the registry. */ resolveRegistrationEntry(value, group = "default") { const _id = stringToSlug(value); const _group = stringToSlug(group); const targetGroup = (LazyLoaderService.registry[_group] || []); let items = targetGroup.filter(t => { if (!t) return false; // No matcher, check id if (!t.matcher) return t.id == value || t[$id] == _id; // Matcher is regex if (t.matcher instanceof RegExp) return t.matcher.test(value) || t.matcher.test(_id); // Matcher is string => regex if (typeof t.matcher == 'string') { const rx = new RegExp(t.matcher, 'ui'); return rx.test(value) || rx.test(_id); } // Matcher is array if (Array.isArray(t.matcher)) { return !!t.matcher.find(e => stringToSlug(e) == _id); } // Custom matcher function if (typeof t.matcher == "function") return t.matcher(_id); return false; }); if (items.length > 1) { this.warn("Resolved multiple components for the provided `[component]` binding. This may cause UI conflicts."); } if (items.length == 0) { return null; } const out = items[0]; if (out.matcher instanceof RegExp) { const result = value.match(out.matcher) || _id.match(out.matcher); return { entry: out, matchGroups: result?.groups }; } return { entry: out }; } /** * Check if a component is currently registered * Can be used to validate regex matchers and aliases. */ isComponentRegistered(value, group = "default") { return !!this.resolveRegistrationEntry(value, group); } /** * * @param bundle * @returns The component `Object` if a component was resolved, `null` if no component was found * `false` if the specified strategy was an invalid selection */ resolveComponent(id, group, modules) { switch (LazyLoaderService.config.componentResolveStrategy) { case ComponentResolveStrategy.PickFirst: { return modules[0]; } // Exact id -> classname match case ComponentResolveStrategy.MatchIdToClassName: { const matches = modules .filter(k => k.name == id); if (matches.length == 0) return null; return matches[0]; } // Fuzzy id -> classname match case ComponentResolveStrategy.FuzzyIdClassName: { const _id = id.replace(/[^a-z0-9_\-]/ig, ''); if (_id.length == 0) { LazyLoaderService.config.logger.err("Fuzzy classname matching stripped all symbols from the ID specified!"); return false; } const rx = new RegExp(`^${id}(component|module)?$`, "i"); const matches = modules .filter(mod => { let kid = mod.name.replace(/[^a-z0-9_\-]/ig, ''); return rx.test(kid); }); if (matches.length > 1) { LazyLoaderService.config.logger.err("Fuzzy classname matching resolved multiple targets!"); return false; } if (matches.length == 0) { LazyLoaderService.config.logger.err("Fuzzy classname matching resolved no targets!"); return null; } return matches[0]; } case ComponentResolveStrategy.Custom: { return LazyLoaderService.config.customResolver(modules); } default: { return false; } } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LazyLoaderService, deps: [{ token: NGX_LAZY_LOADER_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LazyLoaderService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LazyLoaderService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [NGX_LAZY_LOADER_CONFIG] }] }] }); class LazyLoaderComponent { /** * The id of the component that will be lazy loaded */ set id(data) { this.originalId = data; const id = stringToSlug(data); // Check if there is a change to the loaded component's id // if it's updated, we destroy and rehydrate the entire container if (this.initialized && this._id != id) { this._id = id; this.ngAfterViewInit(); } else { this._id = id; } } ; set group(data) { this.originalGroup = data; const group = stringToSlug(data); if (typeof group != "string" || !group) return; // If the group was updated, retry to bootstrap something into the container. if (this.initialized && this._group != group) { this._group = group; this.ngAfterViewInit(); return; } this._group = group; } get group() { return this._group; } /** * A map of inputs to bind to the child. * Supports change detection. (May fail on deep JSON changes) * * ```html * <lazy-loader component="MyLazyComponent" * [inputs]="{ * prop1: true, * prop2: false, * complex: { * a: true, * b: 0 * } * }" * > * </lazy-loader> * ``` */ set inputs(data) { if (data == undefined) return; let previous = this._inputs; this._inputs = data; if (data == undefined) console.trace(data); if (this.targetComponentFactory) { const { inputs } = this.targetComponentFactory.ɵcmp; const currentKeys = Object.keys(inputs); const oldKeys = Object.keys(previous).filter(key => currentKeys.includes(key)); const newKeys = Object.keys(data).filter(key => currentKeys.includes(key)); const removed = oldKeys.filter(key => !newKeys.includes(key)); // ? perhaps set to null or undefined instead removed.forEach(k => this.targetComponentInstance[k] = null); this.bindInputs(); } } /** * A map of outputs to bind from the child. * Should support change detection. * ```html * <lazy-loader component="MyLazyComponent" * [outputs]="{ * prop3: onOutputFire * }" * > * </lazy-loader> * ``` */ set outputs(data) { let previous = this._outputs; this._outputs = data; if (this.targetComponentFactory) { const { inputs } = this.targetComponentFactory.ɵcmp; const currentKeys = Object.keys(inputs); const removed = Object.keys(previous).filter(key => !currentKeys.includes(key)); removed.forEach(k => { // Unsubscribe from observable this.outputSubscriptions[k]?.unsubscribe(); delete this.targetComponentInstance[k]; }); this.bindOutputs(); } } constructor(service, viewContainerRef, dialog, dialogArguments) { this.service = service; this.viewContainerRef = viewContainerRef; this.dialog = dialog; this.dialogArguments = dialogArguments; this._group = "default"; this.outputSubscriptions = {}; /** * Emits errors encountered when loading components */ this.componentLoadError = new EventEmitter(); /** * Emits when the component is fully constructed * and had it's inputs and outputs bound * > before `OnInit` * * Returns the active class instance of the lazy-loaded component */ this.componentLoaded = new EventEmitter(); // Force 500ms delay before revealing the spinner this.clearEmitter = new EventEmitter(); this.clearLoader$ = this.clearEmitter.pipe(debounceTime(300)); this.showEmitter = new EventEmitter(); this.showLoader$ = this.showEmitter.pipe(debounceTime(1)); this.subscriptions = [ this.clearLoader$.subscribe(() => { this.isClearingLoader = true; setTimeout(() => { this.renderSpinner = false; }, 300); }), this.showLoader$.subscribe(() => { this.isClearingLoader = false; this.renderSpinner = true; }) ]; this.renderSpinner = true; // whether we render the DOM for the spinner this.isClearingLoader = false; // should the spinner start fading out this.initialized = false; this.config = LazyLoaderService.config; this.err = LazyLoaderService.config.logger.err; this.warn = LazyLoaderService.config.logger.warn; this.log = LazyLoaderService.config.logger.log; // First, check for dialog arguments if (this.dialogArguments) { this.inputs = this.dialogArguments.inputs || this.dialogArguments.data; this.outputs = this.dialogArguments.outputs; this.id = this.dialogArguments.id; this.group = this.dialogArguments.group; } } async ngAfterViewInit() { this.ngOnDestroy(false); this.isClearingLoader = false; this.renderSpinner = true; this.initialized = true; if (!this._id) { this.warn("No component was specified!"); return this.loadDefault(); } try { const _entry = this.service.resolveRegistrationEntry(this.originalId, this.originalGroup); if (!_entry || !_entry.entry) { this.err(`Failed to find Component '${this._id}' in group '${this._group}' in registry!`); return this.loadDefault(); } const { entry, matchGroups } = _entry; this._matchGroups = matchGroups; // Download the "module" (the standalone component) const bundle = this.targetModule = await entry.load(); // Check if there is some corruption on the bundle. if (!bundle || typeof bundle != 'object') { this.err(`Failed to load component/module for '${this._id}'! Parsed resource is invalid.`); return this.loadError(); } const modules = Object.keys(bundle) .map(k => { const entry = bundle[k]; // Strictly check for exported modules or standalone components if (typeof entry == "function" && typeof entry["ɵfac"] == "function") return entry; return null; }) .filter(e => e != null) .filter(entry => { entry['_isModule'] = !!entry['ɵmod']; // module entry['_isComponent'] = !!entry['ɵcmp']; // component return (entry['_isModule'] || entry['_isComponent']); }); if (modules.length == 0) { this.err(`Component/Module loaded for '${this._id}' has no exported components or modules!`); return this.loadError(); } const component = this.targetComponentFactory = this.service.resolveComponent(this._id, "default", modules); if (!component) { this.err(`Component '${this._id}' is invalid or corrupted!`); return this.loadError(); } // const componentRef = this.targetComponentContainerRef = createComponent(component as any, { // environmentInjector: this.appRef.injector, // elementInjector: this.injector, // hostElement: this.viewContainerRef.element.nativeElement, // // projectableNodes: // }); // // this.targetRef = this.targetContainer.insert(this.targetComponentContainerRef.hostView); // this.appRef.attachView(componentRef.hostView); // Bootstrap the component into the container const componentRef = this.targetComponentContainerRef = this.targetContainer.createComponent(component); this.targetRef = this.targetContainer.insert(this.targetComponentContainerRef.hostView); const instance = this.targetComponentInstance = componentRef['instance']; this.bindInputs(); this.bindOutputs(); this.componentLoaded.next(instance); this.instance = instance; // Look for an observable called isLoading$ that will make us show/hide // the same distractor that is used on basic loading const isLoading$ = instance['ngxShowDistractor$']; if (isLoading$ && typeof isLoading$.subscribe == "function") { this.distractorSubscription = isLoading$.subscribe(loading => { loading ? this.showEmitter.emit() : this.clearEmitter.emit(); }); } else { this.clearEmitter.emit(); } const name = Object.keys(bundle)[0]; this.log(`Loaded '${name}'`); this.clearEmitter.emit(); return componentRef; } catch (ex) { if (isDevMode()) { console.warn("Component DDD " + this._id + " threw an error on mount!"); console.warn("This will cause you to see a 404 panel."); console.error(ex); } // Network errors throw a toast and return an error component if (ex && !isDevMode()) { console.error("Uncaught error when loading component"); throw ex; } return this.loadDefault(); } } ngOnDestroy(clearAll = true) { // unsubscribe from all subscriptions Object.entries(this.outputSubscriptions).forEach(([key, sub]) => { sub.unsubscribe(); }); this.outputSubscriptions = {}; // Clear all things if (clearAll) { Object.entries(this.subscriptions).forEach(([key, sub]) => { sub.unsubscribe(); }); } this.distractorSubscription?.unsubscribe(); // Clear target container this.targetRef?.destroy(); this.targetComponentContainerRef?.destroy(); this.targetContainer?.clear(); // Wipe the rest of the state clean this.targetRef = null; this.targetComponentContainerRef = null; } /** * Bind the input values to the child component. */ bindInputs() { if (!this._inputs || !this.targetComponentInstance) return; // Merge match groups if (typeof this._matchGroups == "object") { Object.entries(this._matchGroups).forEach(([key, val]) => { if (typeof this._inputs[key] == 'undefined') this._inputs[key] = val; }); } // forward-bind inputs const { inputs } = this.targetComponentFactory.ɵcmp; // Returns a list of entries that need to be set // This makes it so that unnecessary setters are not invoked. const updated = Object.entries(inputs).filter(([parentKey, childKey]) => { return this.targetComponentInstance[childKey] != this._inputs[parentKey]; }); updated.forEach(([parentKey, childKey]) => { if (this._inputs.hasOwnProperty(parentKey)) { // Angular 19.2+ if (Array.isArray(childKey)) { this.targetComponentInstance[childKey[0]] = this._inputs[parentKey]; } else { this.targetComponentInstance[childKey] = this._inputs[parentKey]; } } }); } /** * Bind the output handlers to the loaded child component */ bindOutputs() { if (!this._outputs || !this.targetComponentInstance) return; const { outputs } = this.targetComponentFactory.ɵcmp; // Get a list of unregistered outputs const newOutputs = Object.entries(outputs).filter(([parentKey, childKey]) => { return !this.outputSubscriptions[parentKey]; }); // Reverse bind via subscription newOutputs.forEach(([parentKey, childKey]) => { if (this._outputs.hasOwnProperty(parentKey)) { const target = this.targetComponentInstance[childKey]; const outputs = this._outputs; // Angular folks, stop making this so difficult. const ctx = this.viewContainerRef['_hostLView'][8]; const sub = target.subscribe(outputs[parentKey].bind(ctx)); // Subscription this.outputSubscriptions[parentKey] = sub; } }); } /** * Load the "Default" component (404) screen normally. * This is shown when the component id isn't in the * registry or otherwise doesn't match * * This */ loadDefault() { if (this.config.notFoundComponent) this.targetContainer.createComponent(this.config.notFoundComponent); this.clearEmitter.emit(); } /** * Load the "Error" component. * This is shown when we are able to resolve the component * in the registry, but have some issue boostrapping the * component into the viewContainer */ loadError() { if (this.config.errorComponent) this.targetContainer.createComponent(this.config.errorComponent); this.clearEmitter.emit(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LazyLoaderComponent, deps: [{ token: LazyLoaderService }, { token: i0.ViewContainerRef, optional: true }, { token: i2.DialogRef, optional: true }, { token: MAT_DIALOG_DATA, optional: true }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: LazyLoaderComponent, isStandalone: true, selector: "ngx-lazy-loader", inputs: { id: ["component", "id"], group: "group", inputs: "inputs", outputs: "outputs" }, outputs: { componentLoadError: "componentLoadError", componentLoaded: "componentLoaded" }, viewQueries: [{ propertyName: "targetContainer", first: true, predicate: ["content"], descendants: true, read: ViewContainerRef }], ngImport: i0, template: "<ng-container #content></ng-container>\n\n@if (renderSpinner) {\n <div\n class=\"ngx-lazy-loader-distractor\"\n [class.destroying]=\"isClearingLoader\"\n >\n @if (config.loaderDistractorComponent) {\n <ng-container\n [ngComponentOutlet]=\"config.loaderDistractorComponent\"\n />\n }\n @if (config.loaderDistractorTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"config.loaderDistractorTemplate\"\n [ngTemplateOutletContext]=\"{ '$implicit': inputs }\"\n />\n }\n </div>\n}\n", styles: [":host{display:contents;contain:content;z-index:1;position:relative}.ngx-lazy-loader-distractor{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;background-color:var(--background-color, #212121);opacity:1;transition:opacity .3s ease;z-index:999999;animation:fade-in .3s ease}.ngx-lazy-loader-distractor.destroying{opacity:0;pointer-events:none}@keyframes fade-in{0%{opacity:0;pointer-events:none}to{opacity:1;pointer-events:all}}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: LazyLoaderComponent, decorators: [{ type: Component, args: [{ selector: 'ngx-lazy-loader', imports: [NgComponentOutlet, NgTemplateOutlet], standalone: true, template: "<ng-container #content></ng-container>\n\n@if (renderSpinner) {\n <div\n class=\"ngx-lazy-loader-distractor\"\n [class.destroying]=\"isClearingLoader\"\n >\n @if (config.loaderDistractorComponent) {\n <ng-container\n [ngComponentOutlet]=\"config.loaderDistractorComponent\"\n />\n }\n @if (config.loaderDistractorTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"config.loaderDistractorTemplate\"\n [ngTemplateOutletContext]=\"{ '$implicit': inputs }\"\n />\n }\n </div>\n}\n", styles: [":host{display:contents;contain:content;z-index:1;position:relative}.ngx-lazy-loader-distractor{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;background-color:var(--background-color, #212121);opacity:1;transition:opacity .3s ease;z-index:999999;animation:fade-in .3s ease}.ngx-lazy-loader-distractor.destroying{opacity:0;pointer-events:none}@keyframes fade-in{0%{opacity:0;pointer-events:none}to{opacity:1;pointer-events:all}}\n"] }] }], ctorParameters: () => [{ type: LazyLoaderService }, { type: i0.ViewContainerRef, decorators: [{ type: Optional }] }, { type: i2.DialogRef, decorators: [{ type: Optional }] }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [MAT_DIALOG_DATA] }] }], propDecorators: { targetContainer: [{ type: ViewChild, args: ["content", { read: ViewContainerRef }] }], id: [{ type: Input, args: ["component"] }], group: [{ type: Input, args: ["group"] }], inputs: [{ type: Input, args: ["inputs"] }], outputs: [{ type: Input, args: ["outputs"] }], componentLoadError: [{ type: Output }], componentLoaded: [{ type: Output }] } }); class DialogService { constructor(dialog, lazyLoader) { this.dialog = dialog; this.lazyLoader = lazyLoader; this.dialogs = []; } open(name, groupOrOptions, opts = {}) { const group = typeof groupOrOptions == "string" ? groupOrOptions : 'default'; if (typeof groupOrOptions == 'object') opts = groupOrOptions; return new Promise((resolve, reject) => { const registration = this.lazyLoader.resolveRegistrationEntry(name, group); if (!registration) return reject(new Error("Cannot open dialog for " + name + ". Could not find in registry.")); const args = { closeOnNavigation: true, restoreFocus: true, width: registration['width'], height: registration['height'], ...opts, data: { id: name, inputs: opts.inputs || {}, outputs: opts.outputs || {}, group: group }, panelClass: [ "dialog-" + name, ...(Array.isArray(opts.panelClass) ? opts.panelClass : [opts.panelClass]) ] }; let dialog = this.dialog.open(LazyLoaderComponent, args); dialog['idx'] = name; this.dialogs.push(dialog); dialog.afterClosed().subscribe(result => { console.info("Dialog closed " + name, result); resolve(result); }); }); } // Close all dialogs matching the given name close(name) { const dialogs = this.dialogs.filter(d => d['idx'] == name); dialogs.forEach(dialog => dialog.close()); } /** * Method to close _all_ dialogs. * Should be used sparingly. */ clearDialog() { this.dialogs.forEach(dialog => dialog.close()); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DialogService, deps: [{ token: i2$1.MatDialog }, { token: LazyLoaderService }], target: i0.ɵɵFactoryTarget.Injectable }); } st