@dotglitch/ngx-common
Version:
Angular components and utilities that are commonly used.
1,067 lines (1,055 loc) • 126 kB
JavaScript
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