Source

lib/components/model-renderer/model-renderer.component.ts

import {
  Component,
  EventEmitter,
  inject,
  Injector,
  Input,
  OnChanges,
  OnDestroy,
  Output,
  SimpleChanges,
  TemplateRef,
  ViewChild,
  ViewContainerRef,
} from '@angular/core';
import { Model, sf } from '@decaf-ts/decorator-validation';
import { NgComponentOutlet } from '@angular/common';
import {
  AngularDynamicOutput,
  AngularEngineKeys,
  BaseComponentProps,
  BaseCustomEvent,
  NgxRenderingEngine,
  RenderedModel,
} from '../../engine';
import { KeyValue, RendererCustomEvent } from '../../engine/types';
import { ForAngularModule } from '../../for-angular.module';
import { Renderable } from '@decaf-ts/ui-decorators';
import { ComponentRendererComponent } from '../component-renderer/component-renderer.component';

/**
 * @description Component for rendering dynamic models
 * @summary This component is responsible for dynamically rendering models,
 * handling model changes, and managing event subscriptions for the rendered components.
 * It uses the NgxRenderingEngine to render the models and supports both string and Model inputs.
 * @class
 * @template M - Type extending Model
 * @param {Injector} injector - Angular Injector for dependency injection
 * @example
 * <ngx-decaf-model-renderer
 *   [model]="myModel"
 *   [globals]="globalVariables"
 *   (listenEvent)="handleEvent($event)">
 * </ngx-decaf-model-renderer>
 * @mermaid
 * sequenceDiagram
 *   participant App
 *   participant ModelRenderer
 *   participant RenderingEngine
 *   participant Model
 *   App->>ModelRenderer: Input model
 *   ModelRenderer->>Model: Parse if string
 *   Model-->>ModelRenderer: Parsed model
 *   ModelRenderer->>RenderingEngine: Render model
 *   RenderingEngine-->>ModelRenderer: Rendered output
 *   ModelRenderer->>ModelRenderer: Subscribe to events
 *   ModelRenderer-->>App: Emit events
 */
@Component({
  standalone: true,
  imports: [ForAngularModule, NgComponentOutlet, ComponentRendererComponent],
  selector: 'ngx-decaf-model-renderer',
  templateUrl: './model-renderer.component.html',
  styleUrl: './model-renderer.component.scss',
  host: {'[attr.id]': 'rendererId'},
})
export class ModelRendererComponent<M extends Model>
  implements OnChanges, OnDestroy, RenderedModel {

  /**
   * @description Input model to be rendered
   * @summary Can be a Model instance or a JSON string representation of a model
   */
  @Input({ required: true })
  model!: M | string | undefined;

  /**
   * @description Global variables to be passed to the rendered component
   */
  @Input()
  globals: Record<string, unknown> = {};

  /**
   * @description Template reference for inner content
   */
  @ViewChild('inner', { read: TemplateRef, static: true })
  inner?: TemplateRef<unknown>;

  /**
   * @description Output of the rendered model
   */
  output?: AngularDynamicOutput;

  /**
   * @description Unique identifier for the renderer
   */
  @Input()
  rendererId?: string;

  /**
   * @description View container reference for dynamic component rendering
   */
  @ViewChild('componentOuter', { static: true, read: ViewContainerRef })
  vcr!: ViewContainerRef;

  /**
   * @description Event emitter for custom events from the rendered component
   */
  @Output()
  listenEvent = new EventEmitter<RendererCustomEvent>();

  /**
   * @description Instance of the rendered component
   */
  private instance!: KeyValue | undefined;

  private injector: Injector = inject(Injector);

  // constructor() {}

  /**
   * @description Refreshes the rendered model
   * @param {string | M} model - The model to be rendered
   */
  private refresh(model: string | M) {
    model =
      typeof model === 'string'
        ? (Model.build({}, model) as M)
        : model;
    this.output = (model as unknown as Renderable).render<AngularDynamicOutput>(
      this.globals || {},
      this.vcr,
      this.injector,
      this.inner,
    );
    if (this.output?.inputs)
      this.rendererId = sf(
        AngularEngineKeys.RENDERED_ID,
        (this.output.inputs as Record<string, unknown>)['rendererId'] as string,
      );
    this.instance = this.output?.instance;
    this.subscribeEvents();
  }

  /**
   * @description Lifecycle hook that is called when data-bound properties of a directive change
   * @param {SimpleChanges} changes - Object containing changes
   */
  ngOnChanges(changes: SimpleChanges): void {
    if (changes[BaseComponentProps.MODEL]) {
      const { currentValue } = changes[BaseComponentProps.MODEL];
      this.refresh(currentValue);
    }
  }

  /**
   * @description Lifecycle hook that is called when a directive, pipe, or service is destroyed
   * @return {Promise<void>}
   */
  async ngOnDestroy(): Promise<void> {
    if (this.instance) {
      this.unsubscribeEvents();
      await NgxRenderingEngine.destroy();
    }
    this.output = undefined;
  }

  private subscribeEvents(): void {
    const component = this?.output?.component;
    if (this.instance && component) {
      const componentKeys = Object.keys(this.instance);
      for (const key of componentKeys) {
        const value = this.instance[key];
        if (value instanceof EventEmitter)
          (this.instance as KeyValue)[key].subscribe((event: Partial<BaseCustomEvent>) => {
            this.listenEvent.emit({
              component: component.name || '',
              name: key,
              ...event,
            } as RendererCustomEvent);
          });
      }
    }
  }

  /**
   * @description Unsubscribes from events emitted by the rendered component
   */
  private unsubscribeEvents(): void {
    if (this.instance) {
      const componentKeys = Object.keys(this.instance);
      for (const key of componentKeys) {
        const value = this.instance[key];
        if (value instanceof EventEmitter)
          this.instance[key].unsubscribe();
      }
    }
  }

  protected readonly JSON = JSON;
}