CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-ng-dynamic-component

Dynamic components with full life-cycle support for inputs and outputs

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

dynamic-directives.mddocs/

Dynamic Directive Injection (Experimental)

⚠️ Experimental Feature: Advanced system for dynamically attaching directives to components at runtime with input/output binding support. This API is experimental and has known limitations.

Capabilities

Dynamic Directives Directive

Main directive for creating and managing dynamic directives on components.

/**
 * Creates and manages dynamic directives on components (Experimental API)
 * Note: OnChanges hook is not triggered on dynamic directives (known limitation)
 */
@Directive({
  selector: '[ndcDynamicDirectives],[ngComponentOutletNdcDynamicDirectives]',
  standalone: true
})
export class DynamicDirectivesDirective implements OnDestroy, DoCheck {
  /** Array of directive definitions for ndc-dynamic components */
  @Input() ndcDynamicDirectives?: DynamicDirectiveDef<unknown>[] | null;
  
  /** Array of directive definitions for NgComponentOutlet components */
  @Input() ngComponentOutletNdcDynamicDirectives?: DynamicDirectiveDef<unknown>[] | null;
  
  /** Emitted when new directives are created */
  @Output() ndcDynamicDirectivesCreated: EventEmitter<DirectiveRef<unknown>[]>;
}

Dynamic Directives Module

NgModule that provides dynamic directives functionality for traditional module-based applications.

/**
 * Module for dynamic directives functionality
 * Includes component outlet integration
 */
@NgModule({
  imports: [DynamicDirectivesDirective, ComponentOutletInjectorModule],
  exports: [DynamicDirectivesDirective, ComponentOutletInjectorModule]
})
export class DynamicDirectivesModule {}

Types

Directive Definition Types

/**
 * Definition object for dynamic directives
 * Specifies the directive type and optional I/O bindings
 */
interface DynamicDirectiveDef<T> {
  /** The directive class type to instantiate */
  type: Type<T>;
  /** Optional input bindings for the directive */
  inputs?: InputsType;
  /** Optional output bindings for the directive */
  outputs?: OutputsType;
}

/**
 * Helper function to create DynamicDirectiveDef objects with type safety
 * @param type - The directive class
 * @param inputs - Optional input bindings
 * @param outputs - Optional output bindings
 * @returns Typed directive definition
 */
function dynamicDirectiveDef<T>(
  type: Type<T>,
  inputs?: InputsType,
  outputs?: OutputsType
): DynamicDirectiveDef<T>;

Directive Reference Types

/**
 * Reference object for created dynamic directives (similar to ComponentRef)
 * Provides access to the directive instance and lifecycle management
 */
interface DirectiveRef<T> {
  /** The directive instance */
  instance: T;
  /** The directive class type */
  type: Type<T>;
  /** The injector used for the directive */
  injector: Injector;
  /** Reference to the host component */
  hostComponent: unknown;
  /** Reference to the host view */
  hostView: ViewRef;
  /** Element reference where directive is attached */
  location: ElementRef;
  /** Change detector reference for the directive */
  changeDetectorRef: ChangeDetectorRef;
  /** Method to register destroy callbacks */
  onDestroy: (callback: Function) => void;
}

Usage Examples:

import { Component, Directive, Input, Output, EventEmitter } from '@angular/core';
import { 
  DynamicComponent, 
  DynamicDirectivesDirective, 
  DynamicDirectiveDef, 
  DirectiveRef,
  dynamicDirectiveDef 
} from 'ng-dynamic-component';

// Example directives to attach dynamically
@Directive({
  selector: '[tooltipDirective]',
  standalone: true
})
export class TooltipDirective {
  @Input() tooltip = '';
  @Input() position: 'top' | 'bottom' | 'left' | 'right' = 'top';
  @Output() tooltipShow = new EventEmitter<void>();
  @Output() tooltipHide = new EventEmitter<void>();

  // Directive implementation...
}

@Directive({
  selector: '[highlightDirective]',
  standalone: true
})
export class HighlightDirective {
  @Input() highlightColor = 'yellow';
  @Input() highlightDuration = 1000;
  @Output() highlightStart = new EventEmitter<void>();
  @Output() highlightEnd = new EventEmitter<void>();

  // Directive implementation...
}

// Basic dynamic directive usage
@Component({
  standalone: true,
  imports: [DynamicComponent, DynamicDirectivesDirective],
  template: `
    <ndc-dynamic 
      [ndcDynamicComponent]="component"
      [ndcDynamicDirectives]="directives"
      (ndcDynamicDirectivesCreated)="onDirectivesCreated($event)">
    </ndc-dynamic>
  `
})
export class BasicDynamicDirectivesComponent {
  component = MyComponent;
  
  directives: DynamicDirectiveDef<unknown>[] = [
    {
      type: TooltipDirective,
      inputs: {
        tooltip: 'This is a dynamic tooltip',
        position: 'top'
      },
      outputs: {
        tooltipShow: () => console.log('Tooltip shown'),
        tooltipHide: () => console.log('Tooltip hidden')
      }
    },
    {
      type: HighlightDirective,
      inputs: {
        highlightColor: 'lightblue',
        highlightDuration: 2000
      },
      outputs: {
        highlightStart: () => console.log('Highlight started'),
        highlightEnd: () => console.log('Highlight ended')
      }
    }
  ];

  onDirectivesCreated(directiveRefs: DirectiveRef<unknown>[]) {
    console.log('Created directives:', directiveRefs);
    
    // Access directive instances
    directiveRefs.forEach(ref => {
      console.log('Directive type:', ref.type.name);
      console.log('Directive instance:', ref.instance);
    });
  }
}

// Using dynamicDirectiveDef helper for type safety
@Component({
  template: `
    <ndc-dynamic 
      [ndcDynamicComponent]="component"
      [ndcDynamicDirectives]="typeSafeDirectives">
    </ndc-dynamic>
  `
})
export class TypeSafeDynamicDirectivesComponent {
  component = ContentComponent;
  
  typeSafeDirectives = [
    // Type-safe directive definitions
    dynamicDirectiveDef(TooltipDirective, {
      tooltip: 'Type-safe tooltip',
      position: 'bottom'
    }, {
      tooltipShow: () => this.handleTooltipShow(),
      tooltipHide: () => this.handleTooltipHide()
    }),
    
    dynamicDirectiveDef(HighlightDirective, {
      highlightColor: 'lightgreen'
    })
  ];

  handleTooltipShow() {
    console.log('Tooltip is now visible');
  }

  handleTooltipHide() {
    console.log('Tooltip is now hidden');
  }
}

Dynamic Directive Management

Manage directives dynamically based on application state:

@Component({
  template: `
    <div class="controls">
      <label>
        <input type="checkbox" [(ngModel)]="enableTooltip" />
        Enable Tooltip
      </label>
      <label>
        <input type="checkbox" [(ngModel)]="enableHighlight" />
        Enable Highlight
      </label>
      <label>
        <input type="checkbox" [(ngModel)]="enableCustomDirective" />
        Enable Custom Directive
      </label>
    </div>
    
    <ndc-dynamic 
      [ndcDynamicComponent]="component"
      [ndcDynamicDirectives]="activeDirectives"
      (ndcDynamicDirectivesCreated)="trackDirectives($event)">
    </ndc-dynamic>
    
    <div class="directive-info">
      <h3>Active Directives: {{ activeDirectiveRefs.length }}</h3>
      <ul>
        <li *ngFor="let ref of activeDirectiveRefs">
          {{ ref.type.name }}
        </li>
      </ul>
    </div>
  `
})
export class ManagedDynamicDirectivesComponent {
  component = InteractiveComponent;
  
  enableTooltip = true;
  enableHighlight = false;
  enableCustomDirective = false;
  
  activeDirectiveRefs: DirectiveRef<unknown>[] = [];

  get activeDirectives(): DynamicDirectiveDef<unknown>[] {
    const directives: DynamicDirectiveDef<unknown>[] = [];

    if (this.enableTooltip) {
      directives.push(dynamicDirectiveDef(TooltipDirective, {
        tooltip: 'Dynamic tooltip content',
        position: 'top'
      }, {
        tooltipShow: () => console.log('Tooltip displayed'),
        tooltipHide: () => console.log('Tooltip hidden')
      }));
    }

    if (this.enableHighlight) {
      directives.push(dynamicDirectiveDef(HighlightDirective, {
        highlightColor: this.getHighlightColor(),
        highlightDuration: 1500
      }, {
        highlightStart: () => this.onHighlightStart(),
        highlightEnd: () => this.onHighlightEnd()
      }));
    }

    if (this.enableCustomDirective) {
      directives.push(dynamicDirectiveDef(CustomBehaviorDirective, {
        behavior: 'enhanced',
        sensitivity: 0.8
      }));
    }

    return directives;
  }

  trackDirectives(directiveRefs: DirectiveRef<unknown>[]) {
    this.activeDirectiveRefs = directiveRefs;
    console.log(`Active directives updated: ${directiveRefs.length} directives`);
    
    // Set up cleanup for each directive
    directiveRefs.forEach(ref => {
      ref.onDestroy(() => {
        console.log(`Directive ${ref.type.name} destroyed`);
      });
    });
  }

  private getHighlightColor(): string {
    const colors = ['yellow', 'lightblue', 'lightgreen', 'pink'];
    return colors[Math.floor(Math.random() * colors.length)];
  }

  private onHighlightStart() {
    console.log('Highlight effect started');
  }

  private onHighlightEnd() {
    console.log('Highlight effect completed');
  }
}

Working with NgComponentOutlet

Apply dynamic directives to components created by NgComponentOutlet:

import { ComponentOutletIoDirective, DynamicDirectivesDirective } from 'ng-dynamic-component';

@Component({
  standalone: true,
  imports: [ComponentOutletIoDirective, DynamicDirectivesDirective],
  template: `
    <ng-container 
      *ngComponentOutlet="selectedComponent"
      [ngComponentOutletNdcDynamicDirectives]="outletDirectives"
      [ngComponentOutletNdcDynamicInputs]="componentInputs"
      (ndcDynamicDirectivesCreated)="onOutletDirectivesCreated($event)">
    </ng-container>
  `
})
export class OutletDynamicDirectivesComponent {
  selectedComponent = DashboardComponent;
  
  componentInputs = {
    title: 'Dashboard with Dynamic Directives',
    refreshRate: 5000
  };
  
  outletDirectives: DynamicDirectiveDef<unknown>[] = [
    dynamicDirectiveDef(TooltipDirective, {
      tooltip: 'This dashboard updates every 5 seconds',
      position: 'bottom'
    }),
    
    dynamicDirectiveDef(HighlightDirective, {
      highlightColor: 'lightcyan',
      highlightDuration: 3000
    }, {
      highlightStart: () => console.log('Dashboard highlighted'),
      highlightEnd: () => console.log('Dashboard highlight ended')
    })
  ];

  onOutletDirectivesCreated(directiveRefs: DirectiveRef<unknown>[]) {
    console.log('Outlet directives created:', directiveRefs.length);
    
    // Access the outlet component through directive refs
    directiveRefs.forEach(ref => {
      console.log('Host component:', ref.hostComponent);
      
      // Interact with directive instance
      if (ref.instance instanceof TooltipDirective) {
        // Tooltip-specific logic
      } else if (ref.instance instanceof HighlightDirective) {
        // Highlight-specific logic
      }
    });
  }
}

Advanced Directive Lifecycle Management

Handle complex directive lifecycle scenarios:

@Component({
  template: `
    <div class="lifecycle-demo">
      <button (click)="addDirective()">Add Random Directive</button>
      <button (click)="removeLastDirective()">Remove Last Directive</button>
      <button (click)="updateDirectiveInputs()">Update Inputs</button>
      <button (click)="clearAllDirectives()">Clear All</button>
    </div>
    
    <ndc-dynamic 
      [ndcDynamicComponent]="component"
      [ndcDynamicDirectives]="managedDirectives"
      (ndcDynamicDirectivesCreated)="handleDirectiveLifecycle($event)">
    </ndc-dynamic>
    
    <div class="debug-info">
      <pre>{{ debugInfo | json }}</pre>
    </div>
  `
})
export class DirectiveLifecycleComponent {
  component = TestComponent;
  
  private directiveTypes = [TooltipDirective, HighlightDirective, CustomBehaviorDirective];
  private directiveCounter = 0;
  
  managedDirectives: DynamicDirectiveDef<unknown>[] = [];
  debugInfo: any = {};

  addDirective() {
    const randomType = this.directiveTypes[Math.floor(Math.random() * this.directiveTypes.length)];
    const directiveId = ++this.directiveCounter;
    
    let newDirective: DynamicDirectiveDef<unknown>;
    
    if (randomType === TooltipDirective) {
      newDirective = dynamicDirectiveDef(TooltipDirective, {
        tooltip: `Dynamic tooltip #${directiveId}`,
        position: ['top', 'bottom', 'left', 'right'][Math.floor(Math.random() * 4)] as any
      }, {
        tooltipShow: () => this.logEvent(`Tooltip ${directiveId} shown`),
        tooltipHide: () => this.logEvent(`Tooltip ${directiveId} hidden`)
      });
    } else if (randomType === HighlightDirective) {
      newDirective = dynamicDirectiveDef(HighlightDirective, {
        highlightColor: ['yellow', 'lightblue', 'lightgreen'][Math.floor(Math.random() * 3)],
        highlightDuration: Math.random() * 3000 + 1000
      }, {
        highlightStart: () => this.logEvent(`Highlight ${directiveId} started`),
        highlightEnd: () => this.logEvent(`Highlight ${directiveId} ended`)
      });
    } else {
      newDirective = dynamicDirectiveDef(CustomBehaviorDirective, {
        behavior: `mode-${directiveId}`,
        sensitivity: Math.random()
      });
    }
    
    this.managedDirectives = [...this.managedDirectives, newDirective];
    this.updateDebugInfo();
  }

  removeLastDirective() {
    if (this.managedDirectives.length > 0) {
      this.managedDirectives = this.managedDirectives.slice(0, -1);
      this.updateDebugInfo();
    }
  }

  updateDirectiveInputs() {
    this.managedDirectives = this.managedDirectives.map(directive => ({
      ...directive,
      inputs: {
        ...directive.inputs,
        updatedAt: new Date().toISOString()
      }
    }));
    this.updateDebugInfo();
  }

  clearAllDirectives() {
    this.managedDirectives = [];
    this.updateDebugInfo();
  }

  handleDirectiveLifecycle(directiveRefs: DirectiveRef<unknown>[]) {
    console.log('Directive lifecycle event:', directiveRefs.length, 'directives');
    
    // Track directive creation
    directiveRefs.forEach((ref, index) => {
      console.log(`Directive ${index + 1}:`, ref.type.name);
      
      // Set up destroy callback
      ref.onDestroy(() => {
        console.log(`Directive ${ref.type.name} is being destroyed`);
      });
      
      // Access directive-specific properties
      if (ref.instance instanceof TooltipDirective) {
        // Tooltip directive specific handling
        console.log('Tooltip text:', (ref.instance as any).tooltip);
      }
    });
    
    this.updateDebugInfo();
  }

  private logEvent(message: string) {
    console.log(`[${new Date().toLocaleTimeString()}] ${message}`);
  }

  private updateDebugInfo() {
    this.debugInfo = {
      totalDirectives: this.managedDirectives.length,
      directiveTypes: this.managedDirectives.map(d => d.type.name),
      lastUpdate: new Date().toISOString()
    };
  }
}

Known Limitations

OnChanges Hook Limitation

⚠️ Important: Dynamic directives do not trigger the OnChanges lifecycle hook. This is a known limitation of the experimental API.

// This will NOT work as expected
@Directive({
  selector: '[problematicDirective]'
})
export class ProblematicDirective implements OnChanges {
  @Input() value: string = '';

  ngOnChanges(changes: SimpleChanges) {
    // This method will NOT be called when 'value' input changes
    // through dynamic directive inputs
    console.log('Changes:', changes); // Never executed
  }
}

// Workaround: Use DoCheck instead
@Directive({
  selector: '[workingDirective]'
})
export class WorkingDirective implements DoCheck {
  @Input() value: string = '';
  private previousValue: string = '';

  ngDoCheck() {
    // Manually detect changes
    if (this.value !== this.previousValue) {
      console.log('Value changed:', this.previousValue, '->', this.value);
      this.previousValue = this.value;
    }
  }
}

Module Usage

For NgModule-based applications:

import { NgModule } from '@angular/core';
import { DynamicDirectivesModule } from 'ng-dynamic-component';

@NgModule({
  imports: [
    DynamicDirectivesModule
  ],
  // Components can now use dynamic directives
})
export class MyFeatureModule {}

docs

component-outlet.md

core-services.md

dynamic-attributes.md

dynamic-component.md

dynamic-directives.md

index.md

input-output.md

tile.json