RSS Git Download  Clone
Raw Blame History 4kB 144 lines
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) {
          <ng-container *ngComponentOutlet="dynamicComponent; ngModuleFactory: dynamicModule;"></ng-container>
        }
    `,
    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> | any;

    @Input('p3x-compile-module')
    module: NgModule | undefined;

    @Input('p3x-compile-imports')
    imports: Array<Type<any> | ModuleWithProviders<any> | 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;
    }
}