UNPKG

@dotglitch/ngx-common

Version:

Angular components and utilities that are commonly used.

1 lines 182 kB
{"version":3,"file":"dotglitch-ngx-common-core.mjs","sources":["../../packages/common/core/directives/image-cache.directive.ts","../../packages/common/core/pipes/html-bypass.pipe.ts","../../packages/common/core/pipes/resource-bypass.pipe.ts","../../packages/common/core/pipes/script-bypass.pipe.ts","../../packages/common/core/pipes/style-bypass.pipe.ts","../../packages/common/core/pipes/url-bypass.pipe.ts","../../packages/common/core/utils/index.ts","../../packages/common/core/services/dependency.service.ts","../../packages/common/core/components/lazy-loader/types.ts","../../packages/common/core/components/lazy-loader/lazy-loader.service.ts","../../packages/common/core/components/lazy-loader/lazy-loader.component.ts","../../packages/common/core/components/lazy-loader/lazy-loader.component.html","../../packages/common/core/services/dialog.service.ts","../../packages/common/core/services/fetch.service.ts","../../packages/common/core/services/file.service.ts","../../packages/common/core/services/keyboard.service.ts","../../packages/common/core/services/navigation.service.ts","../../packages/common/core/services/theme.service.ts","../../packages/common/core/components/lazy-loader/lazy-loader.module.ts","../../packages/common/core/components/parallax-card/parallax-card.component.ts","../../packages/common/core/components/parallax-card/parallax-card.component.html","../../packages/common/core/components/types.ts","../../packages/common/core/components/menu/menu.component.ts","../../packages/common/core/components/menu/menu.component.html","../../packages/common/core/directives/utils.ts","../../packages/common/core/components/menu/menu.directive.ts","../../packages/common/core/index.ts","../../packages/common/core/dotglitch-ngx-common-core.ts"],"sourcesContent":["import { Directive, ElementRef, Inject, InjectionToken, Input, Optional } from '@angular/core';\nimport { INDEXEDDB, createInstance } from 'localforage';\n\nconst storage = createInstance({\n name: \"@dotglitch\",\n storeName: \"image-cache\",\n driver: INDEXEDDB,\n version: 1\n});\n\n\nconst imageCache: {\n [key: string]: HTMLImageElement;\n} = {};\n\nconst 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>`;\nconst 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>`;\n\nexport type NgxImageCacheConfig = {\n /**\n * Image to use as a placeholder while loading the main image\n * Recommended to use inlined SVG or a base64 encoded image\n */\n loadingPlaceholder?: string,\n /**\n * Image to use as a placeholder where images fail to load\n * Recommended to use inlined SVG or a base64 encoded image\n */\n brokenPlaceholder?: string\n}\n\nexport const NGX_IMAGE_CACHE_CONFIG = new InjectionToken<NgxImageCacheConfig>('ngx-image-cache-config');\n\n\nexport type NgxImageCacheConfiguration = {\n /**\n * Max age to cache an image in milliseconds.\n * If set to `0` or a negative number, images will never expire.\n */\n maxAge: number,\n /**\n * Set to `false` to disable memory caching for the image\n * If both `cacheInMemory` and `cacheInIndexedDB` are false,\n * no caching will happen. (You'll still get the loader)\n */\n cacheInMemory: boolean,\n /**\n * Set to `false` to disable indexedDB caching for the image\n * If both `cacheInMemory` and `cacheInIndexedDB` are false,\n * no caching will happen. (You'll still get the loader)\n */\n cacheInIndexedDB: boolean\n}\n\n@Directive({\n selector: 'img[ngx-cache]',\n standalone: true\n})\nexport class NgxImageCacheDirective {\n\n @Input(\"source\")\n @Input(\"ngx-cache\") url: string;\n\n @Input(\"ngx-cache-config\") configuration: NgxImageCacheConfiguration;\n\n private get el() { return this.element.nativeElement as HTMLImageElement }\n\n constructor(\n private readonly element: ElementRef,\n @Optional() @Inject(NGX_IMAGE_CACHE_CONFIG) private readonly cacheConfig: NgxImageCacheConfig\n ) { }\n\n ngOnChanges() {\n this.getCachedImage();\n }\n\n async getCachedImage() {\n if (\n this.el.src?.trim() == this.url?.trim() || // Check that there's an actual change\n this.url?.trim().length == 0 // Check that there's an actual URL\n ) return;\n\n // Check if it's in the memory cache\n if (imageCache[this.url]) {\n const image = imageCache[this.url];\n\n // If the image is currently loading, show the loader\n // and add it to the reflist\n if (image['_loading'] == true) {\n image['_refs'].push(this.el);\n\n this.el.setAttribute(\"loading\", \"true\");\n this.el.src = this.cacheConfig?.loadingPlaceholder || loadingSvg;\n }\n else {\n // The image is fully loaded, swap out the src with a data-uri\n this.el.setAttribute(\"loading\", \"false\");\n this.el.src = image.src;\n }\n\n // If it's already in the image cache, we're going to trust that it loads properly.\n return;\n }\n\n // Check if it's in indexedDB\n if (this.configuration?.cacheInIndexedDB != false) {\n const cached = await storage.getItem<any>(this.url);\n if (cached) {\n // Attempt to load the base64 data from indexeddb.\n // If this fails, we'll fall back to attempting to download the image\n this.el.src = cached.data;\n\n const evt: Event = await new Promise(res => {\n this.el.addEventListener('load', res);\n this.el.addEventListener('error', res);\n });\n\n // If the event isn't an error\n if (evt.type == \"load\") {\n this.el.setAttribute(\"loading\", \"false\");\n\n if (this.configuration?.cacheInMemory != false) {\n // Successfully loaded into element\n // Create an entry in the memory cache\n const image = imageCache[this.url] = new Image();\n image.src = cached.data;\n image['_createdAt'] = Date.now();\n }\n return;\n }\n else {\n // Else, we try to load again.\n this.el.src = this.cacheConfig?.loadingPlaceholder || loadingSvg;\n }\n }\n }\n\n const image = (() => {\n if (this.configuration?.cacheInMemory != false) {\n return imageCache[this.url] = new Image();\n }\n return new Image();\n })();\n\n // const clone = image.cloneNode(true) as HTMLImageElement;\n image['_refs'] = image['_refs'] ?? [];\n image['_refs'].push(this.el);\n image['_loading'] = true;\n image['_createdAt'] = Date.now();\n\n // Show a loader while the image downloads.\n this.el.setAttribute(\"loading\", \"true\");\n this.el.src = this.cacheConfig?.loadingPlaceholder || loadingSvg;\n\n // Fetch the image via JS and cache it as base64\n window.fetch(this.url)\n .then(response => response.blob())\n .then(blob => new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onloadend = () => {\n image.src = reader.result as string;\n\n storage.setItem(this.url, {\n timestamp: Date.now(),\n data: reader.result\n });\n\n image['_refs'].forEach((ref: HTMLImageElement) => {\n ref.src = image.src;\n });\n\n image['_loading'] = false;\n resolve(0);\n };\n reader.onerror = reject;\n reader.readAsDataURL(blob);\n }))\n .catch(err => {\n // If a failure occurs, purge this entry from the cache\n // TODO: Render better \"broken\" image\n delete imageCache[this.url];\n image['_refs'].forEach((ref: HTMLImageElement) => {\n ref.src = this.cacheConfig?.brokenPlaceholder || brokenSvg;\n ref.setAttribute(\"loading\", \"failed\");\n });\n });\n }\n}\n","import { Pipe, PipeTransform, SecurityContext } from '@angular/core';\nimport { DomSanitizer, SafeUrl } from '@angular/platform-browser';\n\n/**\n * Url Sanitizer pipe.\n *\n * This trusts URLs that exist in a safe list defined in our environments.ts file.\n * Any other URLs will NOT be trusted, thus will not be loaded.\n */\n@Pipe({\n name: 'htmlbypass',\n standalone: true\n})\nexport class HtmlBypass implements PipeTransform {\n\n constructor(private sanitizer: DomSanitizer) { }\n\n public transform(url: string): SafeUrl {\n return this.sanitizer.bypassSecurityTrustHtml(url);\n }\n}\n","import { Pipe, PipeTransform, SecurityContext } from '@angular/core';\nimport { DomSanitizer, SafeUrl } from '@angular/platform-browser';\n\n/**\n * Url Sanitizer pipe.\n *\n * This trusts URLs that exist in a safe list defined in our environments.ts file.\n * Any other URLs will NOT be trusted, thus will not be loaded.\n */\n@Pipe({\n name: 'resourcebypass',\n standalone: true\n})\nexport class ResourceBypass implements PipeTransform {\n\n constructor(private sanitizer: DomSanitizer) { }\n\n public transform(url: string): SafeUrl {\n return this.sanitizer.bypassSecurityTrustResourceUrl(url);\n }\n}\n","import { Pipe, PipeTransform, SecurityContext } from '@angular/core';\nimport { DomSanitizer, SafeUrl } from '@angular/platform-browser';\n\n/**\n * Url Sanitizer pipe.\n *\n * This trusts URLs that exist in a safe list defined in our environments.ts file.\n * Any other URLs will NOT be trusted, thus will not be loaded.\n */\n@Pipe({\n name: 'scriptbypass',\n standalone: true\n})\nexport class ScriptBypass implements PipeTransform {\n\n constructor(private sanitizer: DomSanitizer) { }\n\n public transform(url: string): SafeUrl {\n return this.sanitizer.bypassSecurityTrustScript(url);\n }\n}\n","import { Pipe, PipeTransform, SecurityContext } from '@angular/core';\nimport { DomSanitizer, SafeUrl } from '@angular/platform-browser';\n\n/**\n * Url Sanitizer pipe.\n *\n * This trusts URLs that exist in a safe list defined in our environments.ts file.\n * Any other URLs will NOT be trusted, thus will not be loaded.\n */\n@Pipe({\n name: 'stylebypass',\n standalone: true\n})\nexport class StyleBypass implements PipeTransform {\n\n constructor(private sanitizer: DomSanitizer) { }\n\n public transform(url: string): SafeUrl {\n return this.sanitizer.bypassSecurityTrustStyle(url);\n }\n}\n","import { Pipe, PipeTransform, SecurityContext } from '@angular/core';\nimport { DomSanitizer, SafeUrl } from '@angular/platform-browser';\n\n/**\n * Url Sanitizer pipe.\n *\n * This trusts URLs that exist in a safe list defined in our environments.ts file.\n * Any other URLs will NOT be trusted, thus will not be loaded.\n */\n@Pipe({\n name: 'urlbypass',\n standalone: true\n})\nexport class UrlBypass implements PipeTransform {\n\n constructor(private sanitizer: DomSanitizer) { }\n\n public transform(url: string): SafeUrl {\n return this.sanitizer.bypassSecurityTrustUrl(url);\n }\n}\n","export const sleep = ms => new Promise(r => setTimeout(r, ms));\n\n/**\n * Prompt the user to save a json file of the given object.\n */\nexport const saveObjectAsFile = (name: string, data: Object) => {\n const a = document.createElement(\"a\");\n const file = new Blob([JSON.stringify(data)], { type: \"application/json\" });\n a.href = URL.createObjectURL(file);\n a.download = name;\n a.click();\n a.remove();\n};\n\n/**\n * Convert a string `fooBAR baz_160054''\"1]\"` into a slug: `foobar-baz-1600541`\n */\nexport const stringToSlug = (text: string) =>\n (text || '')\n .trim()\n .toLowerCase()\n .replace(/[\\-_+ ]/g, '-')\n .replace(/[^a-z0-9\\-\\/]/g, '');\n\n\n/**\n* Helper to update the page URL.\n* @param page component page ID to load.\n* @param data string or JSON data for query params.\n*/\nexport const updateUrl = (page?: string, data: string | string[][] | Record<string, string | number> | URLSearchParams = {}, replaceState = false) => {\n const [oldHash, qstring] = location.hash.split('?');\n\n if (!page)\n page = oldHash.split('/')[1];\n\n const hash = `#/${page}`;\n\n // Convert the data object to JSON.\n if (data instanceof URLSearchParams) {\n data = [...(data as any).entries()].map(([k, v]) => ({ [k]: v })).reduce((a, b) => ({ ...a, ...b }), {});\n }\n\n const query = new URLSearchParams(data as any) as any;\n const prevParams = new URLSearchParams(qstring) as any;\n\n // If the hash is the same, retain params.\n if (hash == oldHash) {\n replaceState = true;\n for (const [key, value] of prevParams.entries())\n if (!query.has(key))\n query.set(key, prevParams.get(key));\n }\n\n for (const [key, val] of query.entries()) {\n if (\n val == null ||\n val == undefined ||\n val == '' ||\n val == 'null' ||\n Number.isNaN(val) ||\n val == 'NaN'\n )\n query.delete(key);\n }\n\n if (!(hash.toLowerCase() == \"#/frame\") || data['id'] == -1)\n query.delete('id');\n\n\n const strQuery = query.toString();\n console.log(data, hash, strQuery);\n if (replaceState) {\n window.history.replaceState(data, '', hash + (strQuery ? ('?' + strQuery) : ''));\n }\n else {\n window.history.pushState(data, '', hash + (strQuery ? ('?' + strQuery) : ''));\n }\n};\n\nexport const getUrlData = (source = window.location.hash) => {\n const [hash, query] = source.split('?');\n let data = new URLSearchParams(query) as any;\n return [...data.entries()].map(([k, v]) => ({ [k]: v })).reduce((a, b) => ({ ...a, ...b }), {});\n};\n","import { DOCUMENT } from '@angular/common';\nimport { Injectable, Inject } from '@angular/core';\nimport { sleep } from '../utils';\n\nconst SCRIPT_INIT_TIMEOUT = 500; // ms\n\n/**\n * Service that installs CSS/JS dynamically\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class DependencyService {\n\n constructor(\n @Inject(DOCUMENT) private document: Document\n ) { }\n\n /**\n * Install a Javascript file into the webpage on-demand\n * @param id Unique identifier for the JS script\n * @param src URL of the script\n * @param globalkey A global object the script will provide.\n * Providing this will ensure a promise only resolves after the\n * specified global object is provided, with a timeout of 500ms\n */\n loadScript(id: string, src: string, globalkey: string = null): Promise<void> {\n return new Promise((res, rej) => {\n if (this.document.getElementById(id)) return res();\n\n const script = this.document.createElement('script');\n script.id = id;\n\n script.setAttribute(\"async\", '');\n script.setAttribute(\"src\", src);\n\n script.onload = async () => {\n if (typeof globalkey == \"string\") {\n let i = 0;\n\n for (; !window[globalkey] && i < SCRIPT_INIT_TIMEOUT; i += 10)\n await sleep(10);\n\n if (i >= SCRIPT_INIT_TIMEOUT) {\n return rej(new Error(\"Timed out waiting for script to self-initialize.\"));\n }\n }\n\n res();\n }\n\n this.document.body.appendChild(script);\n })\n }\n\n // loadStylesheet(id: string, href: string) {\n // let themeLink = this.document.getElementById(id) as HTMLLinkElement;\n // if (themeLink) {\n // themeLink.href = href;\n // }\n // else {\n // const style = this.document.createElement('link');\n // style.id = id;\n // style.rel = 'stylesheet';\n // style.href = href;\n\n // const head = this.document.getElementsByTagName('head')[0];\n\n // head.appendChild(style);\n // }\n // }\n}\n","import { ComponentType } from '@angular/cdk/portal';\nimport { TemplateRef } from '@angular/core';\n\nexport enum ComponentResolveStrategy {\n /**\n * Match the fist component we find\n * (best used for standalone components)\n * @default\n */\n PickFirst,\n /**\n * Perform an Exact ID to Classname of the Component\n * case sensitive, zero tolerance.\n */\n MatchIdToClassName,\n /**\n * Perform a fuzzy ID to classname match\n * case insensitive, mutes symbols\n * ignores \"Component\" and \"Module\" postfixes on class\n * names\n */\n FuzzyIdClassName,\n\n /**\n * Use a user-provided component match function\n */\n Custom\n}\n\nexport type NgxLazyLoaderConfig = Partial<{\n entries: ComponentRegistration[],\n\n notFoundTemplate: TemplateRef<any>,\n notFoundComponent: ComponentType<any>,\n\n errorTemplate: TemplateRef<any>,\n errorComponent: ComponentType<any>,\n\n loaderDistractorTemplate: TemplateRef<any>,\n loaderDistractorComponent: ComponentType<any>,\n\n logger: {\n log: (...args: any) => void,\n warn: (...args: any) => void,\n err: (...args: any) => void;\n },\n /**\n * What strategy should be used to resolve components\n * @default ComponentResolveStrategy.FuzzyIdClassName\n */\n componentResolveStrategy: ComponentResolveStrategy,\n customResolver: (registry: (CompiledComponent | CompiledModule)[]) => Object\n}>;\n\ntype RegistrationConfig = {\n /**\n * Specify a group to categorize components. If not specified,\n * will default to the `default` group.\n */\n group?: string,\n /**\n * load: () => import('./pages/my-page/my-page.component')\n */\n load: () => any,\n\n /**\n * Called before a component is loaded.\n * If it returns `false` the component will not be loaded.\n */\n // canActivate: () => boolean\n\n [key: string]: any\n}\n\nexport type ComponentRegistration = (\n ({ id: string } & RegistrationConfig) |\n ({ matcher: string[] | RegExp | ((value: string) => boolean); } & RegistrationConfig)\n);\n\nexport type DynamicRegistrationArgs<T = any> = {\n id: string,\n group?: string,\n matcher?: string[] | RegExp | ((val: string) => boolean),\n component?: T,\n load?: () => any;\n}\n\n/**\n * This is roughly a compiled component\n */\nexport type CompiledComponent = {\n (): CompiledComponent,\n ɵfac: Function,\n ɵcmp: {\n consts;\n contentQueries;\n data;\n declaredInputs;\n decls;\n dependencies;\n directiveDefs;\n encapsulation;\n exportAs;\n factory;\n features;\n findHostDirectiveDefs;\n getStandaloneInjector;\n hostAttrs;\n hostBindings;\n hostDirectives;\n hostVars;\n id: string;\n inputs;\n ngContentSelectors;\n onPush: boolean;\n outputs;\n pipeDefs;\n providersResolver;\n schemas;\n selectors: string[];\n setInput;\n standalone: boolean;\n styles: string[];\n tView;\n template;\n type: Function;\n vars: number;\n viewQuery;\n };\n};\n\n/**\n * This is roughly a compiled module\n */\nexport type CompiledModule = {\n (): CompiledModule,\n ɵfac: Function,\n ɵinj: {\n providers: any[],\n imports: any[];\n },\n ɵmod: {\n bootstrap: any[],\n declarations: Function[],\n exports: any[],\n id: unknown,\n imports: any[],\n schemas: unknown,\n transitiveCompileScopes: unknown,\n type: Function;\n };\n};\n\nexport type CompiledBundle = { [key: string]: CompiledComponent | CompiledModule; };\n\n\n","import { Inject, Injectable, InjectionToken, Optional } from '@angular/core';\nimport { stringToSlug } from '../../utils';\nimport { CompiledComponent, CompiledModule, ComponentRegistration, ComponentResolveStrategy, DynamicRegistrationArgs, NgxLazyLoaderConfig } from './types';\n\n// Monkey-patch the type of these symbols.\nconst $id = Symbol(\"id\") as any as string;\nconst $group = Symbol(\"group\") as any as string;\n\nexport const NGX_LAZY_LOADER_CONFIG = new InjectionToken<NgxLazyLoaderConfig>('lazyloader-config');\n\n@Injectable({\n providedIn: 'root'\n})\nexport class LazyLoaderService {\n private get err() { return LazyLoaderService.config.logger.err; }\n private get log() { return LazyLoaderService.config.logger.log; }\n private get warn() { return LazyLoaderService.config.logger.warn; }\n\n // A proxied registry that mutates reference keys\n private static registry: {\n [key: string]: ComponentRegistration[];\n } = {};\n\n public static config: NgxLazyLoaderConfig;\n\n constructor(@Optional() @Inject(NGX_LAZY_LOADER_CONFIG) config: NgxLazyLoaderConfig = {}) {\n // Ensure this is singleton and works regardless of special instancing requirements.\n LazyLoaderService.configure(config);\n }\n\n private static configure(config: NgxLazyLoaderConfig) {\n this.config = {\n componentResolveStrategy: ComponentResolveStrategy.PickFirst,\n logger: {\n log: console.log,\n warn: console.warn,\n err: console.error\n },\n ...config\n };\n\n config?.entries?.forEach(e => this.addComponentToRegistry(e));\n\n // If a custom resolution strategy is provided but no resolution function is passed,\n // we throw an error\n if (\n this.config.componentResolveStrategy == ComponentResolveStrategy.Custom &&\n !this.config.customResolver\n ) {\n throw new Error(\"Cannot initialize. Configuration specifies a custom resolve matcher but none was provided\");\n }\n\n if (this.config.loaderDistractorComponent && this.config.loaderDistractorTemplate)\n throw new Error(\"Cannot have both a Component and Template for Distractor view.\");\n if (this.config.errorComponent && this.config.errorTemplate)\n throw new Error(\"Cannot have both a Component and Template for Error view.\");\n if (this.config.notFoundComponent && this.config.notFoundTemplate)\n throw new Error(\"Cannot have both a Component and Template for NotFound view.\");\n\n }\n\n private static addComponentToRegistry(registration: ComponentRegistration) {\n if (!registration)\n throw new Error(\"Cannot add <undefined> component into registry.\");\n\n // Clone the object into our repository and transfer the id into a standardized slug format\n\n const id = stringToSlug(registration.id ?? Date.now().toString()); // purge non-basic ASCII chars\n const group = registration.group || \"default\";\n\n registration[$id] = id;\n registration[$group] = id;\n\n\n if (!this.registry[group])\n this.registry[group] = [];\n\n // Check if we already have a registration for the component\n // if (this.registry[group] && typeof this.registry[group]['load'] == \"function\") {\n // // Warn the developer that the state is problematic\n // this.config.logger.warn(\n // `A previous entry already exists for ${id}! The old registration will be overridden.` +\n // `Please ensure you use groups if you intend to have duplicate component ids. ` +\n // `If this was intentional, first remove the old component from the registry before adding a new instance`\n // );\n\n // // If we're in dev mode, break the loader surface\n // if (isDevMode())\n // return;\n // }\n\n this.registry[group].push(registration);\n }\n\n /**\n * Register an Angular component\n * @param id identifier that is used to resolve the component\n * @param group\n * @param component Angular Component Class constructor\n */\n public registerComponent<T extends { new(...args: any[]): InstanceType<T>; }>(args: DynamicRegistrationArgs<T>) {\n if (this.isComponentRegistered(args.id, args.group)) {\n this.log(`Will not re-register component '${args.id}' in group '${args.group || 'default'}' `);\n return;\n }\n\n LazyLoaderService.addComponentToRegistry({\n id: stringToSlug(args.id),\n matcher: args.matcher,\n group: stringToSlug(args.group || \"default\"),\n load: args.load || (() => args.component)\n });\n }\n\n /**\n *\n * @param id\n * @param group\n */\n public unregisterComponent(id: string, group = \"default\") {\n const _id = stringToSlug(id);\n const _group = stringToSlug(group);\n\n if (!this.resolveRegistrationEntry(id, group))\n throw new Error(\"Cannot unregister component ${}! Component is not present in registry\");\n\n // TODO: handle clearing running instances\n delete LazyLoaderService.registry[_group][_id];\n }\n\n\n /**\n * Get the registration entry for a component.\n * Returns null if component is not in the registry.\n */\n public resolveRegistrationEntry(value: string, group = \"default\") {\n const _id = stringToSlug(value);\n const _group = stringToSlug(group);\n\n const targetGroup = (LazyLoaderService.registry[_group] || []);\n\n let items = targetGroup.filter(t => {\n if (!t) return false;\n\n // No matcher, check id\n if (!t.matcher)\n return t.id == value || t[$id] == _id;\n\n // Matcher is regex\n if (t.matcher instanceof RegExp)\n return t.matcher.test(value) || t.matcher.test(_id);\n\n // Matcher is string => regex\n if (typeof t.matcher == 'string') {\n const rx = new RegExp(t.matcher, 'ui');\n return rx.test(value) || rx.test(_id);\n }\n\n // Matcher is array\n if (Array.isArray(t.matcher)) {\n return !!t.matcher.find(e => stringToSlug(e) == _id);\n }\n\n // Custom matcher function\n if (typeof t.matcher == \"function\")\n return t.matcher(_id);\n\n return false;\n });\n\n if (items.length > 1) {\n this.warn(\"Resolved multiple components for the provided `[component]` binding. This may cause UI conflicts.\");\n }\n if (items.length == 0) {\n return null;\n }\n\n const out = items[0];\n\n if (out.matcher instanceof RegExp) {\n const result = value.match(out.matcher) || _id.match(out.matcher);\n\n return {\n entry: out,\n matchGroups: result?.groups\n };\n }\n\n return { entry: out };\n }\n\n /**\n * Check if a component is currently registered\n * Can be used to validate regex matchers and aliases.\n */\n public isComponentRegistered(value: string, group = \"default\") {\n return !!this.resolveRegistrationEntry(value, group);\n }\n\n /**\n *\n * @param bundle\n * @returns The component `Object` if a component was resolved, `null` if no component was found\n * `false` if the specified strategy was an invalid selection\n */\n public resolveComponent(id: string, group: string, modules: (CompiledComponent | CompiledModule)[]): Object | null | false {\n\n switch (LazyLoaderService.config.componentResolveStrategy) {\n case ComponentResolveStrategy.PickFirst: {\n\n return modules[0];\n }\n\n // Exact id -> classname match\n case ComponentResolveStrategy.MatchIdToClassName: {\n const matches =\n modules\n .filter(k => k.name == id);\n\n if (matches.length == 0)\n return null;\n\n return matches[0];\n }\n // Fuzzy id -> classname match\n case ComponentResolveStrategy.FuzzyIdClassName: {\n const _id = id.replace(/[^a-z0-9_\\-]/ig, '');\n\n if (_id.length == 0) {\n LazyLoaderService.config.logger.err(\"Fuzzy classname matching stripped all symbols from the ID specified!\");\n return false;\n }\n\n const rx = new RegExp(`^${id}(component|module)?$`, \"i\");\n\n const matches = modules\n .filter(mod => {\n let kid = mod.name.replace(/[^a-z0-9_\\-]/ig, '');\n\n return rx.test(kid);\n });\n\n if (matches.length > 1) {\n LazyLoaderService.config.logger.err(\"Fuzzy classname matching resolved multiple targets!\");\n return false;\n }\n\n if (matches.length == 0) {\n LazyLoaderService.config.logger.err(\"Fuzzy classname matching resolved no targets!\");\n return null;\n }\n\n return matches[0];\n }\n case ComponentResolveStrategy.Custom: {\n return LazyLoaderService.config.customResolver(modules as any);\n }\n default: {\n return false;\n }\n }\n }\n}\n","import { Input, ViewContainerRef, isDevMode, ComponentRef, EventEmitter, Optional, ViewChild, Component, Inject, Output, NgModule, AfterViewInit, OnInit } from '@angular/core';\nimport { NgComponentOutlet, NgTemplateOutlet } from '@angular/common';\nimport { MAT_DIALOG_DATA, } from '@angular/material/dialog';\nimport { DialogRef } from '@angular/cdk/dialog';\nimport { BehaviorSubject, debounceTime, Subscription } from 'rxjs';\nimport { LazyLoaderService } from './lazy-loader.service';\nimport { stringToSlug } from '../../utils';\nimport { CompiledBundle, NgxLazyLoaderConfig } from './types';\n\n\n@Component({\n selector: 'ngx-lazy-loader',\n templateUrl: './lazy-loader.component.html',\n styleUrls: [ './lazy-loader.component.scss' ],\n imports: [ NgComponentOutlet, NgTemplateOutlet ],\n standalone: true\n})\nexport class LazyLoaderComponent implements AfterViewInit {\n @ViewChild(\"content\", { read: ViewContainerRef }) targetContainer: ViewContainerRef;\n\n /**\n * ! Here be dragons.\n * Only the bravest of Adventurers can survive the battles below,\n * and they must be trained and ready for the gruelling journey ahead.\n * Many a soul has tried to best these Dragons, yet only one has\n * succeeded since our founding.\n *\n * TL;DR -- Don't mess with this unless you know what you're doing.\n * This is central to a ton of moving parts -- breaking it will\n * cause more collateral damage than you may realize.\n */\n\n private _id: string;\n private originalId: string;\n /**\n * The id of the component that will be lazy loaded\n */\n @Input(\"component\") set id(data: string) {\n this.originalId = data;\n const id = stringToSlug(data);\n\n // Check if there is a change to the loaded component's id\n // if it's updated, we destroy and rehydrate the entire container\n if (this.initialized && this._id != id) {\n this._id = id;\n this.ngAfterViewInit();\n }\n else {\n this._id = id;\n }\n };\n\n private _group = \"default\";\n private originalGroup: string;\n @Input(\"group\") set group(data: string) {\n this.originalGroup = data;\n const group = stringToSlug(data);\n\n if (typeof group != \"string\" || !group) return;\n\n // If the group was updated, retry to bootstrap something into the container.\n if (this.initialized && this._group != group) {\n this._group = group;\n\n this.ngAfterViewInit();\n return;\n }\n\n this._group = group;\n }\n get group() { return this._group }\n\n private _matchGroups: { [key: string]: string };\n private _inputs: { [key: string]: any; };\n /**\n * A map of inputs to bind to the child.\n * Supports change detection. (May fail on deep JSON changes)\n *\n * ```html\n * <lazy-loader component=\"MyLazyComponent\"\n * [inputs]=\"{\n * prop1: true,\n * prop2: false,\n * complex: {\n * a: true,\n * b: 0\n * }\n * }\"\n * >\n * </lazy-loader>\n * ```\n */\n @Input(\"inputs\") set inputs(data: { [key: string]: any; }) {\n if (data == undefined) return;\n\n let previous = this._inputs;\n this._inputs = data;\n if (data == undefined)\n console.trace(data);\n\n if (this.targetComponentFactory) {\n const { inputs } = this.targetComponentFactory.ɵcmp;\n\n const currentKeys = Object.keys(inputs);\n\n const oldKeys = Object.keys(previous).filter(key => currentKeys.includes(key));\n const newKeys = Object.keys(data).filter(key => currentKeys.includes(key));\n\n const removed = oldKeys.filter(key => !newKeys.includes(key));\n\n // ? perhaps set to null or undefined instead\n removed.forEach(k => this.targetComponentInstance[k] = null);\n\n this.bindInputs();\n }\n }\n\n\n private outputSubscriptions: { [key: string]: Subscription; } = {};\n private _outputs: { [key: string]: Function; };\n /**\n * A map of outputs to bind from the child.\n * Should support change detection.\n * ```html\n * <lazy-loader component=\"MyLazyComponent\"\n * [outputs]=\"{\n * prop3: onOutputFire\n * }\"\n * >\n * </lazy-loader>\n * ```\n */\n @Input(\"outputs\") set outputs(data: { [key: string]: Function; }) {\n let previous = this._outputs;\n this._outputs = data;\n\n if (this.targetComponentFactory) {\n const { inputs } = this.targetComponentFactory.ɵcmp;\n\n const currentKeys = Object.keys(inputs);\n const removed = Object.keys(previous).filter(key => !currentKeys.includes(key));\n\n removed.forEach(k => {\n // Unsubscribe from observable\n this.outputSubscriptions[k]?.unsubscribe();\n delete this.targetComponentInstance[k];\n });\n\n this.bindOutputs();\n }\n }\n\n /**\n * Emits errors encountered when loading components\n */\n @Output() componentLoadError = new EventEmitter();\n\n /**\n * Emits when the component is fully constructed\n * and had it's inputs and outputs bound\n * > before `OnInit`\n *\n * Returns the active class instance of the lazy-loaded component\n */\n @Output() componentLoaded = new EventEmitter();\n\n\n /**\n * This is an instance of the component that is currently loaded.\n */\n public instance: any;\n\n\n /**\n * Container that provides the component data\n */\n private targetModule: CompiledBundle;\n\n /**\n * Component definition\n */\n private targetComponentFactory: any;\n\n /**\n * Active component container reference\n */\n private targetComponentContainerRef: ComponentRef<any>;\n private targetRef: any;\n /**\n * Reference to the component class instance\n */\n private targetComponentInstance: any;\n\n /**\n * Subscription with true/false state on whether the distractor should be\n */\n private distractorSubscription: Subscription;\n\n public config: NgxLazyLoaderConfig;\n private err;\n private warn;\n private log;\n\n // Force 500ms delay before revealing the spinner\n private clearEmitter = new EventEmitter();\n private clearLoader$ = this.clearEmitter.pipe(debounceTime(300));\n\n private showEmitter = new EventEmitter();\n private showLoader$ = this.showEmitter.pipe(debounceTime(1));\n\n private subscriptions = [\n this.clearLoader$.subscribe(() => {\n this.isClearingLoader = true;\n\n setTimeout(() => {\n this.renderSpinner = false;\n }, 300)\n }),\n this.showLoader$.subscribe(() => {\n this.isClearingLoader = false;\n this.renderSpinner = true;\n })\n ];\n\n public renderSpinner = true; // whether we render the DOM for the spinner\n public isClearingLoader = false; // should the spinner start fading out\n\n constructor(\n private service: LazyLoaderService,\n @Optional() private viewContainerRef: ViewContainerRef,\n @Optional() public dialog: DialogRef,\n @Optional() @Inject(MAT_DIALOG_DATA) public dialogArguments\n ) {\n this.config = LazyLoaderService.config;\n this.err = LazyLoaderService.config.logger.err;\n this.warn = LazyLoaderService.config.logger.warn;\n this.log = LazyLoaderService.config.logger.log;\n\n // First, check for dialog arguments\n if (this.dialogArguments) {\n this.inputs = this.dialogArguments.inputs || this.dialogArguments.data;\n this.outputs = this.dialogArguments.outputs;\n this.id = this.dialogArguments.id;\n this.group = this.dialogArguments.group;\n }\n }\n\n private initialized = false;\n async ngAfterViewInit() {\n this.ngOnDestroy(false);\n this.isClearingLoader = false;\n this.renderSpinner = true;\n this.initialized = true;\n\n if (!this._id) {\n this.warn(\"No component was specified!\");\n return this.loadDefault();\n }\n\n try {\n const _entry = this.service.resolveRegistrationEntry(this.originalId, this.originalGroup);\n if (!_entry || !_entry.entry) {\n this.err(`Failed to find Component '${this._id}' in group '${this._group}' in registry!`);\n return this.loadDefault();\n }\n\n const { entry, matchGroups } = _entry;\n this._matchGroups = matchGroups;\n\n // Download the \"module\" (the standalone component)\n const bundle: CompiledBundle = this.targetModule = await entry.load();\n\n\n // Check if there is some corruption on the bundle.\n if (!bundle || typeof bundle != 'object') {\n this.err(`Failed to load component/module for '${this._id}'! Parsed resource is invalid.`);\n return this.loadError();\n }\n\n const modules = Object.keys(bundle)\n .map(k => {\n const entry = bundle[k];\n\n // Strictly check for exported modules or standalone components\n if (typeof entry == \"function\" && typeof entry[\"ɵfac\"] == \"function\")\n return entry;\n return null;\n })\n .filter(e => e != null)\n .filter(entry => {\n entry['_isModule'] = !!entry['ɵmod']; // module\n entry['_isComponent'] = !!entry['ɵcmp']; // component\n\n return (entry['_isModule'] || entry['_isComponent']);\n });\n\n if (modules.length == 0) {\n this.err(`Component/Module loaded for '${this._id}' has no exported components or modules!`);\n return this.loadError();\n }\n\n const component = this.targetComponentFactory = this.service.resolveComponent(this._id, \"default\", modules);\n\n if (!component) {\n this.err(`Component '${this._id}' is invalid or corrupted!`);\n return this.loadError();\n }\n\n\n // const componentRef = this.targetComponentContainerRef = createComponent(component as any, {\n // environmentInjector: this.appRef.injector,\n // elementInjector: this.injector,\n // hostElement: this.viewContainerRef.element.nativeElement,\n // // projectableNodes:\n // });\n // // this.targetRef = this.targetContainer.insert(this.targetComponentContainerRef.hostView);\n // this.appRef.attachView(componentRef.hostView);\n\n // Bootstrap the component into the container\n const componentRef = this.targetComponentContainerRef = this.targetContainer.createComponent(component as any);\n this.targetRef = this.targetContainer.insert(this.targetComponentContainerRef.hostView);\n\n const instance: any = this.targetComponentInstance = componentRef['instance'];\n\n this.bindInputs();\n this.bindOutputs();\n\n this.componentLoaded.next(instance);\n this.instance = instance;\n\n // Look for an observable called isLoading$ that will make us show/hide\n // the same distractor that is used on basic loading\n const isLoading$ = instance['ngxShowDistractor$'] as BehaviorSubject<boolean>;\n\n if (isLoading$ && typeof isLoading$.subscribe == \"function\") {\n this.distractorSubscription = isLoading$.subscribe(loading => {\n loading ? this.showEmitter.emit() : this.clearEmitter.emit();\n });\n }\n else {\n this.clearEmitter.emit();\n }\n\n const name = Object.keys(bundle)[0];\n this.log(`Loaded '${name}'`);\n this.clearEmitter.emit();\n\n return componentRef;\n }\n catch (ex) {\n\n if (isDevMode()) {\n console.warn(\"Component DDD \" + this._id + \" threw an error on mount!\");\n console.warn(\"This will cause you to see a 404 panel.\");\n console.error(ex);\n }\n\n // Network errors throw a toast and return an error component\n if (ex && !isDevMode()) {\n console.error(\"Uncaught error when loading component\");\n throw ex;\n }\n\n return this.loadDefault();\n }\n }\n\n ngOnDestroy(clearAll = true) {\n // unsubscribe from all subscriptions\n Object.entries(this.outputSubscriptions).forEach(([key, sub]) => {\n sub.unsubscribe();\n });\n this.outputSubscriptions = {};\n\n // Clear all things\n if (clearAll) {\n Object.entries(this.subscriptions).forEach(([key, sub]) => {\n sub.unsubscribe();\n });\n }\n\n this.distractorSubscription?.unsubscribe();\n\n // Clear target container\n this.targetRef?.destroy();\n this.targetComponentContainerRef?.destroy();\n this.targetContainer?.clear();\n\n // Wipe the rest of the state clean\n this.targetRef = null;\n this.targetComponentContainerRef = null;\n }\n\n /**\n * Bind the input values to the child component.\n */\n private bindInputs() {\n if (!this._inputs || !this.targetComponentInstance) return;\n\n // Merge match groups\n if (typeof this._matchGroups == \"object\") {\n Object.entries(this._matchGroups).forEach(([key, val]) => {\n if (typeof this._inputs[key] == 'undefined')\n this._inputs[key] = val;\n });\n }\n\n // forward-bind inputs\n const { inputs } = this.targetComponentFactory.ɵcmp;\n\n // Returns a list of entries that need to be set\n // This makes it so that unnecessary setters are not invoked.\n const updated = Object.entries(inputs).filter(([parentKey, childKey]: [string, string]) => {\n return this.targetComponentInstance[childKey] != this._inputs[parentKey];\n });\n\n updated.forEach(([parentKey, childKey]: [string, string | [string, number, unknown]]) => {\n if (this._inputs.hasOwnProperty(parentKey)) {\n // Angular 19.2+\n if (Array.isArray(childKey)) {\n this.targetComponentInstance[childKey[0]] = this._inputs[parentKey];\n }\n else {\n this.targetComponentInstance[childKey] = this._inputs[parentKey];\n }\n }\n });\n }\n\n /**\n * Bind the output handlers to the loaded child component\n */\n private bindOutputs() {\n if (!this._outputs || !this.targetComponentInstance) return;\n\n const { outputs } = this.targetComponentFactory.ɵcmp;\n\n // Get a list of unregistered outputs\n const newOutputs = Object.entries(outputs).filter(([parentKey, childKey]: [string, string]) => {\n return !this.outputSubscriptions[parentKey];\n });\n\n // Reverse bind via subscription\n newOutputs.forEach(([parentKey, childKey]: [string, string]) => {\n if (this._outputs.hasOwnProperty(parentKey)) {\n const target: EventEmitter<unknown> = this.targetComponentInstance[childKey];\n const outputs = this._outputs;\n\n // Angular folks, stop making this so difficult.\n const ctx = this.viewContainerRef['_hostLView'][8];\n const sub = target.subscribe(outputs[parentKey].bind(ctx)); // Subscription\n\n this.outputSubscriptions[parentKey] = sub;\n }\n });\n }\n\n /**\n * Load the \"Default\" component (404) screen normally.\n * This is shown when the component id isn't in the\n * registry or otherwise doesn't match\n *\n * This\n */\n private loadDefault() {\n if (this.config.notFoundComponent)\n this.targetContainer.createComponent(this.config.notFoundComponent);\n\n this.clearEmitter.emit();\n }\n\n /**\n * Load the \"Error\" component.\n * This is shown when we are able to resolve the component\n * in the registry, but have some issue boostrapping the\n * component into the viewContainer\n */\n private loadError() {\n if (this.config.errorComponent)\n this.targetContainer.createComponent(this.config.errorComponent);\n\n this.clearEmitter.emit();\n }\n}\n","<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","import { Injectable } from '@angular/core';\nimport { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog';\nimport { LazyLoaderComponent } from '../components/lazy-loader/lazy-loader.component';\nimport { LazyLoaderService } from '../components/lazy-loader/lazy-loader.service';\n\nexport type DialogOptions = Partial<Omit<MatDialogConfig<any>, 'data'> & {\n /**\n * List of properties to be provided to @Input() injectors\n