import { Compiler, Component, inject, Injectable, Input, ModuleWithProviders, NgModule, NgModuleFactory, OnChanges, SimpleChanges, Type, } from '@angular/core'; import { CommonModule, NgComponentOutlet } from '@angular/common'; function reverse(str: string) { return str.split('').reverse().join(''); } function random() { return ( Math.floor(Math.random() * (99999999999999999 - 10000000000000000)) + 10000000000000000 ).toString(16); } let currentIdTime: number; let currentId = 0; function nextId(): string { const now = Date.now(); if (currentIdTime !== now) { currentId = 0; currentIdTime = now; } const comingId = ++currentId; const randomHex = reverse(random()).padStart(15, '0'); const timeHex = reverse(currentIdTime.toString(16).padStart(12, '0')); const comingIdHex = reverse(comingId.toString(16).padStart(3, '0')); return `p3x-angular-compile-${timeHex}${comingIdHex}${randomHex}`; } @Component({ selector: '[p3x-compile]', template: ` @if (renderComponent) { } `, standalone: true, imports: [CommonModule, NgComponentOutlet], }) @Injectable() export class CompileAttribute implements OnChanges { @Input('p3x-compile') html!: string; @Input('p3x-compile-ctx') context: any; @Input('p3x-compile-error-handler') errorHandler: Function | undefined = undefined; dynamicComponent: any; dynamicModule: NgModuleFactory | any; @Input('p3x-compile-module') module: NgModule | undefined; @Input('p3x-compile-imports') imports: Array | ModuleWithProviders | any[]> | undefined; private readonly compiler = inject(Compiler); get renderComponent() { return typeof this.html === 'string' && this.html.trim() !== ''; } ngOnChanges(_changes: SimpleChanges) { this.update(); } update() { try { if (this.html === undefined || this.html === null || this.html.trim() === '') { this.dynamicComponent = undefined; this.dynamicModule = undefined; return; } this.dynamicComponent = this.createNewComponent(this.html, this.context); this.dynamicModule = this.compiler.compileModuleSync( this.createComponentModule(this.dynamicComponent), ); } catch (e) { if (this.errorHandler === undefined) { throw e; } else { this.errorHandler(e); } } } private createComponentModule(componentType: any) { let module: NgModule = {}; if (this.module !== undefined) { module = Object.assign({}, this.module); } module.imports = module.imports || []; module.imports.push(CommonModule); if (this.imports !== undefined) { module.imports = module.imports.concat(this.imports); } if (module.declarations === undefined) { module.declarations = [componentType]; } else { module.declarations.push(componentType); } @NgModule(module) class RuntimeComponentModule {} return RuntimeComponentModule; } private createNewComponent(html: string, context: any) { const selector: string = nextId(); @Component({ selector: selector, template: html, standalone: false, }) class DynamicComponent { context: any = context; } return DynamicComponent; } }