Dynamic components with full life-cycle support for inputs and outputs
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
⚠️ 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.
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>[]>;
}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 {}/**
* 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>;/**
* 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');
}
}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');
}
}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
}
});
}
}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()
};
}
}⚠️ 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;
}
}
}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 {}