All files / src/lib/components/component-renderer component-renderer.component.ts

14% Statements 7/50
0% Branches 0/30
0% Functions 0/7
13.95% Lines 6/43

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 2307x                                       7x   7x   7x 7x                                                                                                 7x                                                                                                                                                                                                                                                                                                                      
import {
  Component,
  ComponentMirror,
  Injector,
  Input,
  OnDestroy,
  OnInit,
  reflectComponentType,
  TemplateRef,
  Type,
} from '@angular/core';
/**
 * @module module:lib/components/component-renderer/component-renderer.component
 * @description Component renderer module.
 * @summary Provides `ComponentRendererComponent` which renders dynamic child
 * components based on configuration or data. Useful for rendering custom
 * fields, nested components or templated content at runtime.
 *
 * @link {@link ComponentRendererComponent}
 */
import { NgComponentOutlet } from '@angular/common';
 
import { NgxRenderingEngine } from '../../engine/NgxRenderingEngine';
import { KeyValue } from '../../engine/types';
import { AngularEngineKeys, BaseComponentProps } from '../../engine/constants';
import { NgxRenderableComponentDirective } from '../../engine/NgxRenderableComponentDirective';
 
/**
 * @description Dynamic component renderer for Decaf Angular applications.
 * @summary This component provides a flexible way to dynamically render Angular components
 * at runtime based on a tag name. It handles the creation, property binding, and event
 * subscription for dynamically loaded components. This is particularly useful for
 * building configurable UIs where components need to be determined at runtime.
 *
 * @component {ComponentRendererComponent}
 * @example
 * <ngx-decaf-component-renderer
 *   [tag]="tag"
 *   [globals]="globals"
 *   (listenEvent)="listenEvent($event)">
 * </ngx-decaf-component-renderer>
 *
 * @mermaid
 * classDiagram
 *   class ComponentRendererComponent {
 *     +ViewContainerRef vcr
 *     +string tag
 *     +Record~string, unknown~ globals
 *     +EnvironmentInjector injector
 *     +ComponentRef~unknown~ component
 *     +EventEmitter~IBaseCustomEvent~ listenEvent
 *     +ngOnInit()
 *     +ngOnDestroy()
 *     +ngOnChanges(changes)
 *     -createComponent(tag, globals)
 *     -subscribeEvents()
 *     -unsubscribeEvents()
 *   }
 *   ComponentRendererComponent --|> OnInit
 *   ComponentRendererComponent --|> OnChanges
 *   ComponentRendererComponent --|> OnDestroy
 *
 * @implements {OnInit}
 * @implements {OnChanges}
 * @implements {OnDestroy}
 */
@Component({
  selector: 'ngx-decaf-component-renderer',
  templateUrl: './component-renderer.component.html',
  styleUrls: ['./component-renderer.component.scss'],
  imports: [NgComponentOutlet],
  standalone: true,
  host: { '[attr.id]': 'uid' },
})
export class ComponentRendererComponent
  extends NgxRenderableComponentDirective
  implements OnInit, OnDestroy
{
  /**
   * @description The tag name of the component to be dynamically rendered.
   * @summary This input property specifies which component should be rendered by providing
   * its registered tag name. The tag must correspond to a component that has been registered
   * with the NgxRenderingEngine. This is a required input as it determines which component
   * to create.
   *
   * @type {string}
   * @required
   * @memberOf ComponentRendererComponent
   */
  @Input({ required: true })
  tag!: string;
 
  @Input()
  children: KeyValue[] = [];
 
  @Input()
  override projectable: boolean = true;
 
  @Input()
  override pk: string = '';
 
  @Input()
  parent: undefined | KeyValue = undefined;
 
  /**
   * @description Initializes the component after Angular first displays the data-bound properties.
   * @summary Sets up the component by creating the dynamic component specified by the tag input.
   * This method is called once when the component is initialized and triggers the dynamic
   * component creation process with the provided tag name and global properties.
   *
   * @mermaid
   * sequenceDiagram
   *   participant A as Angular Lifecycle
   *   participant C as ComponentRendererComponent
   *   participant R as NgxRenderingEngine
   *
   *   A->>C: ngOnInit()
   *   C->>C: createComponent(tag, globals)
   *   C->>R: components(tag)
   *   R-->>C: Return component constructor
   *   C->>C: Process component inputs
   *   C->>C: Create component instance
   *   C->>C: subscribeEvents()
   *
   * @return {void}
   * @memberOf ComponentRendererComponent
   */
  ngOnInit(): void {
    if (!this.parent) {
      this.createComponent(this.tag, this.globals);
    } else {
      this.createParentComponent();
    }
  }
 
  /**
   * @description Creates and renders a dynamic component.
   * @summary This method handles the creation of a dynamic component based on the provided tag.
   * It retrieves the component constructor from the rendering engine, processes its inputs,
   * filters out unmapped properties, creates the component instance, and sets up event subscriptions.
   *
   * @param {string} tag - The tag name of the component to create
   * @param {KeyValue} globals - Global properties to pass to the component
   * @return {void}
   *
   * @mermaid
   * sequenceDiagram
   *   participant C as ComponentRendererComponent
   *   participant R as NgxRenderingEngine
   *   participant V as ViewContainerRef
   *
   *   C->>R: components(tag)
   *   R-->>C: Return component constructor
   *   C->>C: reflectComponentType(component)
   *   C->>C: Process input properties
   *   C->>C: Filter unmapped properties
   *   C->>V: clear()
   *   C->>R: createComponent(component, props, metadata, vcr, injector, [])
   *   R-->>C: Return component reference
   *   C->>C: subscribeEvents()
   *
   * @private
   * @memberOf ComponentRendererComponent
   */
  private createComponent(tag: string, globals: KeyValue = {}): void {
    const component = NgxRenderingEngine.components(tag)?.constructor as Type<unknown>;
    const metadata = reflectComponentType(component);
    const componentInputs = (metadata as ComponentMirror<unknown>).inputs;
    const props = globals?.['item'] || globals?.['props'] || {};
    Iif (props?.['tag']) delete props['tag'];
    Iif (props?.[AngularEngineKeys.CHILDREN] && !this.children.length)
      this.children = props[AngularEngineKeys.CHILDREN] as KeyValue[];
    props[AngularEngineKeys.CHILDREN] = this.children || [];
    const inputKeys = Object.keys(props);
    const unmappedKeys: string[] = [];
 
    for (const input of inputKeys) {
      Iif (!inputKeys.length) break;
      const prop = componentInputs.find((item: { propName: string }) => item.propName === input);
      Iif (!prop) {
        delete props[input];
        unmappedKeys.push(input);
      }
    }
 
    function hasProperty(key: string): boolean {
      return Object.values(componentInputs).some(({ propName }) => propName === key);
    }
 
    const hasChildrenInput = hasProperty(AngularEngineKeys.CHILDREN);
    Iif (!this.projectable && hasChildrenInput) props[AngularEngineKeys.CHILDREN] = this.children;
 
    const hasRootForm = hasProperty(BaseComponentProps.PARENT_FORM);
    Iif (hasRootForm && this.parentForm) props[BaseComponentProps.PARENT_FORM] = this.parentForm;
 
    props['className'] = props['className']
      ? props['className'] + ' ' + this.className
      : this.className || '';
    this.vcr.clear();
    // const projectable = (this.children?.length && this.projectable);
    // const template = projectable ? this.vcr.createEmbeddedView(this.inner as TemplateRef<unknown>, this.injector).rootNodes : [];
    this.instance = NgxRenderingEngine.createComponent(
      component,
      { ...props, ...{ modelId: props?.modelId || this.modelId, pk: props?.pk || this.pk } },
      this.injector as Injector,
      metadata as ComponentMirror<unknown>,
      this.vcr,
      [],
    );
    this.subscribeEvents(component);
  }
 
  private createParentComponent() {
    const { component, inputs } = this.parent as KeyValue;
    const metadata = reflectComponentType(component) as ComponentMirror<unknown>;
    const template = this.projectable
      ? this.vcr.createEmbeddedView(this.inner as TemplateRef<unknown>, this.injector).rootNodes
      : [];
    this.instance = NgxRenderingEngine.createComponent(
      component,
      inputs,
      this.injector,
      metadata,
      this.vcr,
      template,
    );
    this.subscribeEvents(component);
  }
}