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
Essential services for component I/O management, reflection utilities, and dependency injection infrastructure that power the ng-dynamic-component library.
Abstract service for managing component inputs and outputs with extensible implementation.
/**
* Abstract service for setting inputs and getting outputs on dynamic components
* Provides extensible interface for custom I/O management implementations
*/
@Injectable({
providedIn: 'root',
useClass: ClassicComponentIO
})
export abstract class ComponentIO {
/**
* Sets an input property on a component
* @param componentRef - Reference to the target component
* @param name - Name of the input property (must be a valid component input)
* @param value - Value to set for the input
*/
abstract setInput<T, K extends ComponentInputKey<T>>(
componentRef: ComponentRef<T>,
name: K,
value: T[K]
): void;
/**
* Gets an output observable from a component
* @param componentRef - Reference to the target component
* @param name - Name of the output property
* @returns Observable that emits when the output event occurs
*/
abstract getOutput<T, K extends ComponentInputKey<T>>(
componentRef: ComponentRef<T>,
name: K
): Observable<unknown>;
}
/**
* Type constraint for component input property names
* Ensures type safety when accessing component properties
*/
type ComponentInputKey<T> = keyof T & string;Factory service for creating and configuring IoService instances with custom options.
/**
* Factory for creating IoService instances with custom configuration
* Enables per-component customization of I/O behavior
*/
@Injectable({ providedIn: 'root' })
export class IoFactoryService {
/**
* Creates a new IoService instance with specified configuration
* @param componentInjector - Injector that provides ComponentRef access
* @param ioOptions - Combined configuration options for the service
* @returns Configured IoService instance ready for use
*/
create(
componentInjector: DynamicComponentInjector,
ioOptions?: IoServiceOptions & IoFactoryServiceOptions
): IoService;
}
/**
* Additional options specific to IoFactoryService.create method
*/
interface IoFactoryServiceOptions {
/** Optional custom injector for dependency resolution */
injector?: Injector;
}Configuration service for customizing I/O service behavior.
/**
* Configuration options for IoService instances
* Controls various aspects of input/output management behavior
*/
@Injectable({ providedIn: 'root' })
export class IoServiceOptions {
/**
* Whether to track output changes for performance optimization
* When enabled, prevents unnecessary subscription updates
*/
trackOutputChanges: boolean = false;
}
/**
* Core service for managing dynamic inputs and outputs on components
* Handles property binding, event subscription, and lifecycle management
*/
@Injectable()
export class IoService implements OnDestroy {
/**
* Updates component inputs and outputs
* Applies new input values and manages output event subscriptions
* @param inputs - Object mapping input names to values (null to clear)
* @param outputs - Object mapping output names to handlers (null to clear)
*/
update(inputs?: InputsType | null, outputs?: OutputsType | null): void;
}Usage Examples:
import { Component, Injectable, Injector } from '@angular/core';
import {
IoFactoryService,
IoService,
IoServiceOptions,
DynamicComponentInjector,
ComponentIO
} from 'ng-dynamic-component';
// Custom I/O service configuration
@Injectable()
export class CustomIoServiceOptions extends IoServiceOptions {
trackOutputChanges = true; // Enable output change tracking
// Add custom configuration properties
debugMode = false;
logInputChanges = true;
}
// Using IoFactoryService for custom I/O management
@Component({
template: `
<ndc-dynamic
[ndcDynamicComponent]="component"
#dynamicComponent="ndcDynamicIo">
</ndc-dynamic>
<button (click)="createCustomIoService(dynamicComponent)">
Create Custom I/O Service
</button>
`
})
export class CustomIoServiceComponent {
component = MyComponent;
constructor(
private ioFactory: IoFactoryService,
private injector: Injector
) {}
createCustomIoService(componentInjector: DynamicComponentInjector) {
// Create custom I/O service with specific configuration
const customIoService = this.ioFactory.create(componentInjector, {
trackOutputChanges: true,
injector: this.injector
});
// Use the custom service
customIoService.update(
{ title: 'Custom I/O Service', data: [1, 2, 3] },
{ onSave: (data: any) => console.log('Custom save:', data) }
);
}
}
// Advanced I/O service usage with lifecycle management
@Component({
template: `
<div class="io-controls">
<button (click)="startManualIoManagement()">Start Manual I/O</button>
<button (click)="stopManualIoManagement()">Stop Manual I/O</button>
<button (click)="updateInputs()">Update Inputs</button>
</div>
<ndc-dynamic
[ndcDynamicComponent]="component"
#componentRef="ndcComponentOutletInjector">
</ndc-dynamic>
`
})
export class ManualIoManagementComponent implements OnDestroy {
component = DataComponent;
private manualIoService?: IoService;
private inputUpdateInterval?: number;
constructor(private ioFactory: IoFactoryService) {}
startManualIoManagement() {
if (this.manualIoService) {
return; // Already started
}
// Create I/O service with custom options
this.manualIoService = this.ioFactory.create(
{ componentRef: this.getComponentRef() },
{
trackOutputChanges: true,
injector: this.getCustomInjector()
}
);
// Set up initial I/O
this.manualIoService.update(
{ title: 'Manual Management', refreshRate: 1000 },
{
onDataChange: (data: any) => this.handleDataChange(data),
onError: (error: any) => this.handleError(error)
}
);
// Set up periodic input updates
this.inputUpdateInterval = window.setInterval(() => {
this.updateInputs();
}, 2000);
console.log('Manual I/O management started');
}
stopManualIoManagement() {
if (this.manualIoService) {
// Clear inputs and outputs
this.manualIoService.update(null, null);
this.manualIoService = undefined;
}
if (this.inputUpdateInterval) {
clearInterval(this.inputUpdateInterval);
this.inputUpdateInterval = undefined;
}
console.log('Manual I/O management stopped');
}
updateInputs() {
if (this.manualIoService) {
this.manualIoService.update({
title: `Updated at ${new Date().toLocaleTimeString()}`,
data: Array.from({ length: 5 }, () => Math.floor(Math.random() * 100)),
timestamp: Date.now()
});
}
}
ngOnDestroy() {
this.stopManualIoManagement();
}
private getComponentRef() {
// Implementation to get component reference
return null; // Placeholder
}
private getCustomInjector() {
// Create custom injector with additional providers
return Injector.create({
providers: [
{ provide: 'CUSTOM_CONFIG', useValue: { apiUrl: '/api' } }
]
});
}
private handleDataChange(data: any) {
console.log('Data changed:', data);
}
private handleError(error: any) {
console.error('Component error:', error);
}
}Services for accessing component metadata and reflection information.
/**
* Contract for Reflect API subsystem required by the library
* Abstracts the reflection capabilities needed for metadata access
*/
interface ReflectApi {
/**
* Gets metadata from an object using reflection
* @param type - The metadata type key to retrieve
* @param obj - The object to get metadata from
* @returns Array of metadata values
*/
getMetadata(type: string, obj: unknown): unknown[];
}
/**
* Injection token for ReflectApi implementation
* Provides access to reflection capabilities
* Default factory returns window.Reflect
*/
const ReflectRef: InjectionToken<ReflectApi>;
/**
* Service for accessing reflection metadata on classes and objects
* Used internally for component analysis and dependency injection
*/
@Injectable({ providedIn: 'root' })
export class ReflectService {
/**
* Gets constructor parameter types using reflection
* @param ctor - Constructor function to analyze
* @returns Array of parameter types
*/
getCtorParamTypes(ctor: Type<unknown>): unknown[];
}Usage Examples:
import { Injectable, Type } from '@angular/core';
import { ReflectService, ReflectApi, ReflectRef } from 'ng-dynamic-component';
// Custom reflection implementation
@Injectable()
export class CustomReflectApi implements ReflectApi {
getMetadata(type: string, obj: unknown): unknown[] {
// Custom metadata retrieval logic
if (typeof obj === 'function' && (obj as any).__metadata__) {
return (obj as any).__metadata__[type] || [];
}
return [];
}
}
// Using reflection service for component analysis
@Injectable({ providedIn: 'root' })
export class ComponentAnalyzer {
constructor(private reflectService: ReflectService) {}
analyzeComponent<T>(componentType: Type<T>): ComponentMetadata {
// Get constructor parameter types
const paramTypes = this.reflectService.getCtorParamTypes(componentType);
console.log('Component constructor parameters:', paramTypes);
return {
name: componentType.name,
parameterTypes: paramTypes,
hasCustomInjection: paramTypes.length > 0
};
}
}
interface ComponentMetadata {
name: string;
parameterTypes: unknown[];
hasCustomInjection: boolean;
}
// Providing custom reflection implementation
@Component({
providers: [
{ provide: ReflectRef, useClass: CustomReflectApi }
],
template: `<!-- Component with custom reflection -->`
})
export class CustomReflectionComponent {
constructor(private componentAnalyzer: ComponentAnalyzer) {
// Analyze various component types
this.analyzeComponents();
}
private analyzeComponents() {
const componentsToAnalyze = [
MyComponent,
AnotherComponent,
ComplexComponent
];
componentsToAnalyze.forEach(component => {
const metadata = this.componentAnalyzer.analyzeComponent(component);
console.log('Component analysis:', metadata);
});
}
}Core interfaces and tokens for dependency injection throughout the library.
/**
* Interface for objects that can provide access to ComponentRef instances
* Core abstraction for component reference access across the library
*/
interface DynamicComponentInjector {
/** Reference to the dynamic component instance (null if not created) */
componentRef: ComponentRef<unknown> | null;
}
/**
* Angular DI token for injecting DynamicComponentInjector instances
* Used throughout the library for accessing component references
*/
const DynamicComponentInjectorToken: InjectionToken<DynamicComponentInjector>;Usage Examples:
import { Component, Inject, Injectable } from '@angular/core';
import { DynamicComponentInjector, DynamicComponentInjectorToken } from 'ng-dynamic-component';
// Service that works with dynamic components
@Injectable()
export class DynamicComponentService {
constructor(
@Inject(DynamicComponentInjectorToken)
private componentInjector: DynamicComponentInjector
) {}
getComponentInstance<T>(): T | null {
const ref = this.componentInjector.componentRef;
return ref ? (ref.instance as T) : null;
}
executeOnComponent<T>(action: (instance: T) => void): boolean {
const instance = this.getComponentInstance<T>();
if (instance) {
action(instance);
return true;
}
return false;
}
triggerComponentMethod(methodName: string, ...args: any[]): any {
const instance = this.getComponentInstance();
if (instance && typeof (instance as any)[methodName] === 'function') {
return (instance as any)[methodName](...args);
}
return null;
}
}
// Custom component injector implementation
@Injectable()
export class CustomComponentInjector implements DynamicComponentInjector {
private _componentRef: ComponentRef<unknown> | null = null;
get componentRef(): ComponentRef<unknown> | null {
return this._componentRef;
}
setComponentRef(ref: ComponentRef<unknown> | null) {
this._componentRef = ref;
}
}
// Component using dependency injection infrastructure
@Component({
providers: [
DynamicComponentService,
{ provide: DynamicComponentInjectorToken, useClass: CustomComponentInjector }
],
template: `
<div class="component-controls">
<button (click)="executeAction()">Execute Action</button>
<button (click)="callMethod()">Call Method</button>
<button (click)="getInfo()">Get Component Info</button>
</div>
<ndc-dynamic
[ndcDynamicComponent]="component"
(ndcDynamicCreated)="onComponentCreated($event)">
</ndc-dynamic>
`
})
export class DependencyInjectionExampleComponent {
component = InteractiveComponent;
constructor(
private dynamicService: DynamicComponentService,
@Inject(DynamicComponentInjectorToken)
private componentInjector: DynamicComponentInjector
) {}
onComponentCreated(componentRef: ComponentRef<any>) {
// Update the injector with the new component reference
if (this.componentInjector instanceof CustomComponentInjector) {
this.componentInjector.setComponentRef(componentRef);
}
}
executeAction() {
this.dynamicService.executeOnComponent<any>(instance => {
console.log('Executing action on:', instance.constructor.name);
if (instance.performAction) {
instance.performAction('custom-action');
}
});
}
callMethod() {
const result = this.dynamicService.triggerComponentMethod('getData');
console.log('Method result:', result);
}
getInfo() {
const instance = this.dynamicService.getComponentInstance();
console.log('Component instance:', instance);
console.log('Component type:', instance?.constructor.name);
}
}Complex scenarios involving multiple services and custom configurations.
// Multi-service integration with custom configuration
@Injectable({ providedIn: 'root' })
export class AdvancedDynamicComponentManager {
constructor(
private ioFactory: IoFactoryService,
private reflectService: ReflectService,
private componentIO: ComponentIO
) {}
createManagedComponent<T>(
componentType: Type<T>,
container: ViewContainerRef,
config: ManagedComponentConfig<T>
): ManagedComponentRef<T> {
// Analyze component using reflection
const metadata = this.analyzeComponent(componentType);
// Create component with custom injector
const componentRef = container.createComponent(componentType, {
injector: config.injector
});
// Create custom I/O service
const ioService = this.ioFactory.create(
{ componentRef },
config.ioOptions
);
// Set up inputs and outputs
if (config.inputs) {
ioService.update(config.inputs, null);
}
if (config.outputs) {
ioService.update(null, config.outputs);
}
return new ManagedComponentRef(componentRef, ioService, metadata);
}
private analyzeComponent<T>(componentType: Type<T>) {
return {
parameterTypes: this.reflectService.getCtorParamTypes(componentType),
name: componentType.name
};
}
}
interface ManagedComponentConfig<T> {
injector?: Injector;
inputs?: InputsType;
outputs?: OutputsType;
ioOptions?: IoServiceOptions;
}
class ManagedComponentRef<T> {
constructor(
public componentRef: ComponentRef<T>,
public ioService: IoService,
public metadata: any
) {}
updateIO(inputs?: InputsType, outputs?: OutputsType) {
this.ioService.update(inputs, outputs);
}
destroy() {
this.ioService.update(null, null); // Clear I/O
this.componentRef.destroy();
}
}Tokens for managing custom contexts in output event handlers.
/**
* Factory function for the default event argument token value
* @returns The string '$event' used as the default event argument placeholder
*/
function defaultEventArgumentFactory(): string;
/**
* Injection token for customizing the event argument placeholder string
* Default value is '$event', can be overridden for custom event handling
*/
const IoEventArgumentToken: InjectionToken<string>;
/**
* @deprecated Since v10.4.0 - Use IoEventArgumentToken instead
*/
const EventArgumentToken: InjectionToken<string>;
/**
* Injection token for providing custom context objects to output handlers
* Allows output functions to be bound to specific context objects
*/
const IoEventContextToken: InjectionToken<unknown>;
/**
* Injection token for providing custom context provider configuration
* Used to configure how IoEventContextToken should be resolved
*/
const IoEventContextProviderToken: InjectionToken<StaticProvider>;Usage Examples:
import { Component, Injectable, InjectionToken } from '@angular/core';
import {
IoEventContextToken,
IoEventContextProviderToken,
IoEventArgumentToken,
OutputsType
} from 'ng-dynamic-component';
// Custom event context implementation
@Injectable()
export class CustomEventContext {
private apiService = inject(ApiService);
private logger = inject(LoggerService);
async handleSaveEvent(data: any, eventInfo: any) {
this.logger.log('Save event triggered:', { data, eventInfo });
try {
const result = await this.apiService.save(data);
this.logger.log('Save successful:', result);
return result;
} catch (error) {
this.logger.error('Save failed:', error);
throw error;
}
}
handleDeleteEvent(id: string) {
return this.apiService.delete(id);
}
}
// Component using custom event context
@Component({
providers: [
CustomEventContext,
{
provide: IoEventContextProviderToken,
useValue: { provide: IoEventContextToken, useClass: CustomEventContext }
}
],
template: `
<ndc-dynamic
[ndcDynamicComponent]="component"
[ndcDynamicInputs]="inputs"
[ndcDynamicOutputs]="outputs">
</ndc-dynamic>
`
})
export class EventContextExampleComponent {
component = DataFormComponent;
inputs = {
title: 'Event Context Demo',
data: { name: 'John', email: 'john@example.com' }
};
// Output handlers will be bound to CustomEventContext instance
outputs: OutputsType = {
// Handler method will be called with CustomEventContext as 'this'
onSave: function(this: CustomEventContext, data: any) {
return this.handleSaveEvent(data, { timestamp: Date.now() });
},
onDelete: function(this: CustomEventContext, id: string) {
return this.handleDeleteEvent(id);
},
// Event argument customization
onValidate: {
handler: function(this: CustomEventContext, formData: any, customArg: string) {
console.log('Validation with custom context:', { formData, customArg });
return formData && Object.keys(formData).length > 0;
},
args: ['$event', 'validation-context'] // '$event' will be replaced with actual event
}
};
}
// Global event context configuration
@Component({
providers: [
// Custom event argument placeholder
{ provide: IoEventArgumentToken, useValue: '$data' },
// Global event context
{ provide: IoEventContextToken, useClass: GlobalEventContext }
],
template: `
<div class="global-context-demo">
<ndc-dynamic
[ndcDynamicComponent]="component"
[ndcDynamicOutputs]="globalOutputs">
</ndc-dynamic>
</div>
`
})
export class GlobalEventContextComponent {
component = NotificationComponent;
globalOutputs: OutputsType = {
// Using custom event argument placeholder '$data'
onNotify: {
handler: (message: string, level: string) => {
console.log(`[${level.toUpperCase()}] ${message}`);
},
args: ['$data', 'info'] // '$data' matches IoEventArgumentToken value
},
// Direct function (will be bound to GlobalEventContext)
onDismiss: function(this: GlobalEventContext) {
this.handleDismiss();
}
};
}
@Injectable()
export class GlobalEventContext {
handleDismiss() {
console.log('Notification dismissed via global context');
}
}NgModule classes for organizing and importing library functionality.
/**
* Main module that includes all dynamic component functionality
* Exports DynamicComponent and DynamicIoModule
*/
@NgModule({
imports: [DynamicIoModule, DynamicComponent],
exports: [DynamicIoModule, DynamicComponent]
})
export class DynamicModule {}
/**
* Module for dynamic I/O directives and related functionality
* Includes ComponentOutletInjectorModule
*/
@NgModule({
imports: [DynamicIoDirective],
exports: [DynamicIoDirective, ComponentOutletInjectorModule]
})
export class DynamicIoModule {}
/**
* Module for dynamic attributes directive
* Includes ComponentOutletInjectorModule for NgComponentOutlet support
*/
@NgModule({
imports: [DynamicAttributesDirective],
exports: [DynamicAttributesDirective, ComponentOutletInjectorModule]
})
export class DynamicAttributesModule {}
/**
* Module for experimental dynamic directives functionality
* Includes ComponentOutletInjectorModule
*/
@NgModule({
imports: [DynamicDirectivesDirective],
exports: [DynamicDirectivesDirective, ComponentOutletInjectorModule]
})
export class DynamicDirectivesModule {}
/**
* Module for NgComponentOutlet enhancement directives
* Provides component reference access and I/O capabilities
*/
@NgModule({
imports: [ComponentOutletInjectorDirective, ComponentOutletIoDirective],
exports: [ComponentOutletInjectorDirective, ComponentOutletIoDirective]
})
export class ComponentOutletInjectorModule {}