Master the new APIs that make dynamic component creation cleaner, type-safe, and developer-friendly
Ever struggled with creating dynamic components in Angular and wished there was a cleaner, more intuitive way? 🤔
If you've been working with Angular's dynamic components using ComponentFactory or ViewContainerRef, you know the pain of verbose code, complex event handling, and the constant worry about memory leaks. Well, Angular 20 just dropped some absolute game-changers that will make you rethink how you approach dynamic components entirely.
What you'll learn by the end of this article:
- How to use Angular 20's new
inputBinding()
,outputBinding()
, andtwoWayBinding()
APIs - Best practices for memory management and cleanup
- Complete unit testing strategies
- Performance optimization techniques
- Real-world implementation examples with clean, maintainable code
Ready to level up your Angular game? Let's dive in! 👇
The Old Way vs. The New Way: A Quick Reality Check
Before we jump into the good stuff, let's be honest about what we used to deal with:
// The old, painful way 😫
const componentRef = this.viewContainer.createComponent(MyComponent);
componentRef.instance.someInput = 'value';
componentRef.instance.someOutput.subscribe(data => {
// Handle output
});
// Don't forget cleanup... if you remember 🤷♂️
ngOnDestroy() {
// Manual subscription cleanup
}
Sound familiar? We've all been there—wrestling with subscriptions, forgetting cleanup, and ending up with memory leaks that haunt our apps.
💬 Quick question for you: How many times have you forgotten to unsubscribe from dynamic component outputs? Be honest in the comments!
Meet the New Heroes: inputBinding(), outputBinding(), and twoWayBinding()
Angular 20 introduces three powerful methods that transform how we work with dynamic components. Let's break them down:
1. inputBinding() - Clean Input Handling
import { Component, ViewContainerRef, inject } from '@angular/core';
import { inputBinding } from '@angular/core';
@Component({
selector: 'app-dynamic-host',
template: `<div #dynamicContainer></div>`
})
export class DynamicHostComponent {
private viewContainer = inject(ViewContainerRef);
addDynamicComponent() {
const componentRef = this.viewContainer.createComponent(UserCardComponent);
// ✨ New way - Clean and type-safe!
inputBinding(componentRef, 'userName', 'John Doe');
inputBinding(componentRef, 'isActive', true);
inputBinding(componentRef, 'userData', { id: 1, email: 'john@example.com' });
}
}
2. outputBinding() - Effortless Event Handling
addDynamicComponentWithEvents() {
const componentRef = this.viewContainer.createComponent(UserCardComponent);
// Set inputs
inputBinding(componentRef, 'userName', 'Jane Smith');
// ✨ Handle outputs the new way
outputBinding(componentRef, 'userClicked', (userData) => {
console.log('User clicked:', userData);
this.handleUserSelection(userData);
});
outputBinding(componentRef, 'deleteRequested', (userId) => {
this.confirmDelete(userId);
});
}
3. twoWayBinding() - The Holy Grail
This is where it gets really exciting. Two-way binding with dynamic components used to be a nightmare. Not anymore:
setupTwoWayBinding() {
const componentRef = this.viewContainer.createComponent(EditableCardComponent);
// ✨ Two-way binding made simple
twoWayBinding(componentRef, 'value',
// Initial value
this.currentValue,
// Callback when value changes
(newValue) => {
this.currentValue = newValue;
this.onValueChanged(newValue);
}
);
}
Pretty clean, right? 🎉
Real-World Example: Dynamic Dashboard Widgets
Let's build something practical - a dashboard that can dynamically add different widget types:
@Component({
selector: 'app-dashboard',
template: `
<div class="dashboard-header">
<h2>My Dashboard</h2>
<button (click)="addWidget('chart')" class="btn-primary">Add Chart</button>
<button (click)="addWidget('table')" class="btn-primary">Add Table</button>
<button (click)="addWidget('counter')" class="btn-primary">Add Counter</button>
</div>
<div class="widgets-container" #widgetContainer></div>
`,
styles: [`
.widgets-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
padding: 1rem;
}
`]
})
export class DashboardComponent implements OnDestroy {
private viewContainer = inject(ViewContainerRef);
private activeComponents: ComponentRef<any>[] = [];
private subscriptions = new Set<() => void>();
addWidget(type: 'chart' | 'table' | 'counter') {
let componentRef: ComponentRef<any>;
switch (type) {
case 'chart':
componentRef = this.createChartWidget();
break;
case 'table':
componentRef = this.createTableWidget();
break;
case 'counter':
componentRef = this.createCounterWidget();
break;
}
this.activeComponents.push(componentRef);
}
private createChartWidget() {
const componentRef = this.viewContainer.createComponent(ChartWidgetComponent);
// Set initial data
inputBinding(componentRef, 'title', 'Sales Chart');
inputBinding(componentRef, 'data', this.generateChartData());
inputBinding(componentRef, 'chartType', 'line');
// Handle widget events
const unsubscribe1 = outputBinding(componentRef, 'refreshRequested', () => {
inputBinding(componentRef, 'data', this.generateChartData());
});
const unsubscribe2 = outputBinding(componentRef, 'deleteRequested', () => {
this.removeWidget(componentRef);
});
// Store cleanup functions
this.subscriptions.add(unsubscribe1);
this.subscriptions.add(unsubscribe2);
return componentRef;
}
private createCounterWidget() {
const componentRef = this.viewContainer.createComponent(CounterWidgetComponent);
// Two-way binding for counter value
const unsubscribe = twoWayBinding(componentRef, 'count',
0, // initial value
(newCount) => {
// Sync with parent state or save to backend
this.saveWidgetState(componentRef.instance.id, { count: newCount });
}
);
this.subscriptions.add(unsubscribe);
return componentRef;
}
private removeWidget(componentRef: ComponentRef<any>) {
const index = this.activeComponents.indexOf(componentRef);
if (index > -1) {
this.activeComponents.splice(index, 1);
componentRef.destroy();
}
}
ngOnDestroy() {
// ✨ Clean shutdown - no memory leaks!
this.subscriptions.forEach(unsubscribe => unsubscribe());
this.subscriptions.clear();
this.activeComponents.forEach(ref => ref.destroy());
this.activeComponents.length = 0;
}
}
💡 Pro tip: Notice how we're storing unsubscribe functions and cleaning them up properly? This is crucial for preventing memory leaks!
Unit Testing Your Dynamic Components
Testing dynamic components can be tricky, but Angular 20's new APIs make it much more straightforward:
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DashboardComponent, ChartWidgetComponent, CounterWidgetComponent],
imports: [CommonModule]
}).compileComponents();
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
});
describe('Dynamic Widget Creation', () => {
it('should create chart widget with correct inputs', () => {
// Act
component.addWidget('chart');
fixture.detectChanges();
// Assert
const chartComponent = fixture.debugElement.query(
By.directive(ChartWidgetComponent)
);
expect(chartComponent).toBeTruthy();
expect(chartComponent.componentInstance.title).toBe('Sales Chart');
expect(chartComponent.componentInstance.chartType).toBe('line');
});
it('should handle widget events correctly', fakeAsync(() => {
// Arrange
spyOn(component, 'removeWidget');
// Act
component.addWidget('chart');
fixture.detectChanges();
const chartComponent = fixture.debugElement.query(
By.directive(ChartWidgetComponent)
);
// Simulate delete event
chartComponent.componentInstance.deleteRequested.emit();
tick();
// Assert
expect(component.removeWidget).toHaveBeenCalled();
}));
it('should handle two-way binding for counter widget', fakeAsync(() => {
// Arrange
spyOn(component, 'saveWidgetState');
// Act
component.addWidget('counter');
fixture.detectChanges();
const counterComponent = fixture.debugElement.query(
By.directive(CounterWidgetComponent)
);
// Simulate count change
counterComponent.componentInstance.count = 5;
counterComponent.componentInstance.countChange.emit(5);
tick();
// Assert
expect(component.saveWidgetState).toHaveBeenCalledWith(
jasmine.any(String),
{ count: 5 }
);
}));
});
describe('Memory Management', () => {
it('should clean up subscriptions on destroy', () => {
// Arrange
component.addWidget('chart');
component.addWidget('counter');
const subscriptionCount = (component as any).subscriptions.size;
expect(subscriptionCount).toBeGreaterThan(0);
// Act
component.ngOnDestroy();
// Assert
expect((component as any).subscriptions.size).toBe(0);
expect((component as any).activeComponents.length).toBe(0);
});
});
});
Testing tip: Always test both the happy path and the cleanup! Memory leaks in tests can be just as problematic as in production. 🧪
Best Practices & Memory Management 🧠
1. Always Clean Up Your Subscriptions
export class DynamicComponentManager implements OnDestroy {
private cleanup: (() => void)[] = [];
addComponent() {
const componentRef = this.createComponent();
// Store cleanup function
const unsubscribe = outputBinding(componentRef, 'someEvent', this.handleEvent);
this.cleanup.push(unsubscribe);
}
ngOnDestroy() {
// Execute all cleanup functions
this.cleanup.forEach(fn => fn());
this.cleanup.length = 0;
}
}
2. Use WeakMap for Component Metadata
export class AdvancedDynamicManager {
private componentMetadata = new WeakMap<ComponentRef<any>, ComponentMetaData>();
addComponent() {
const componentRef = this.createComponent();
// Store metadata that gets garbage collected with the component
this.componentMetadata.set(componentRef, {
createdAt: Date.now(),
type: 'chart',
subscriptions: []
});
}
}
3. Implement Change Detection Optimization
export class OptimizedDynamicHost {
addComponent() {
const componentRef = this.viewContainer.createComponent(MyComponent);
// Optimize change detection
componentRef.changeDetectorRef.detach();
inputBinding(componentRef, 'data', this.data);
// Manually trigger when needed
componentRef.changeDetectorRef.detectChanges();
}
}
4. Monitor Performance with Angular DevTools
export class PerformanceAwareDynamic {
private performanceMetrics = new Map<string, number>();
addComponent(type: string) {
const startTime = performance.now();
const componentRef = this.createComponent(type);
const endTime = performance.now();
this.performanceMetrics.set(type, endTime - startTime);
// Log slow component creation
if (endTime - startTime > 100) {
console.warn(`Slow component creation for ${type}: ${endTime - startTime}ms`);
}
}
}
Quick question: Are you currently monitoring your dynamic component performance? Drop a comment and let me know what tools you use! 💬
Bonus Tips 🎁
1. Type Safety with Generics
function createTypedComponent<T>(
viewContainer: ViewContainerRef,
componentType: Type<T>,
inputs: Partial<T> = {}
): ComponentRef<T> {
const componentRef = viewContainer.createComponent(componentType);
Object.entries(inputs).forEach(([key, value]) => {
inputBinding(componentRef, key as keyof T, value);
});
return componentRef;
}
// Usage with full type safety
const chartRef = createTypedComponent(this.viewContainer, ChartComponent, {
title: 'My Chart',
data: chartData,
showLegend: true
});
2. Component Factory Service
@Injectable({
providedIn: 'root'
})
export class DynamicComponentFactory {
private registry = new Map<string, Type<any>>();
registerComponent(name: string, component: Type<any>) {
this.registry.set(name, component);
}
createComponent(name: string, viewContainer: ViewContainerRef) {
const componentType = this.registry.get(name);
if (!componentType) {
throw new Error(`Component '${name}' not registered`);
}
return viewContainer.createComponent(componentType);
}
}
3. Error Boundary for Dynamic Components
export class SafeDynamicHost {
addComponentSafely(componentType: Type<any>) {
try {
const componentRef = this.viewContainer.createComponent(componentType);
// Wrap in error handling
const originalNgOnInit = componentRef.instance.ngOnInit;
componentRef.instance.ngOnInit = () => {
try {
originalNgOnInit?.call(componentRef.instance);
} catch (error) {
console.error('Dynamic component initialization failed:', error);
this.handleComponentError(componentRef, error);
}
};
return componentRef;
} catch (error) {
console.error('Failed to create dynamic component:', error);
return null;
}
}
}
Recap: Your Dynamic Component Toolkit 📚
Let's wrap up what we've covered:
Key Angular 20 Features:
-
inputBinding()
- Clean, type-safe input handling -
outputBinding()
- Simplified event subscription with automatic cleanup options -
twoWayBinding()
- Effortless two-way data binding for dynamic components
Best Practices Checklist:
- ✅ Always clean up subscriptions in
ngOnDestroy
- ✅ Use WeakMap for component metadata
- ✅ Monitor performance of dynamic component creation
- ✅ Implement error boundaries for robust apps
- ✅ Leverage TypeScript generics for type safety
- ✅ Test both functionality and memory management
Memory Management Essentials:
- Store cleanup functions and execute them on destroy
- Use
ChangeDetectorRef.detach()
for performance optimization - Monitor component lifecycle with Angular DevTools
- Implement proper error handling to prevent memory leaks
Your Turn! 👇
💬 What did you think? Have you already started experimenting with Angular 20's dynamic component features? I'd love to hear about your experience! Drop a comment below and share:
- What's your biggest pain point with dynamic components?
- Have you tried these new APIs yet?
- What other Angular 20 features are you excited about?
👏 Found this helpful? If this article saved you some debugging time or sparked new ideas, smash that clap button! It helps other developers discover these game-changing features too.
📬 Want more Angular insights like this? I share practical Angular tips, advanced techniques, and the latest framework updates every week. Follow me here on Medium and never miss out on the good stuff!
🚀 Take Action:
- Try implementing one of these examples in your current project
- Refactor an existing dynamic component using the new APIs
- Share this article with your team - they'll thank you later!
- Star the GitHub repository with the complete examples (link in my bio)
Keep coding, keep learning, and remember - the best way to master these features is to get your hands dirty with code! 💪
🎯 Your Turn, Devs!
👀 Did this article spark new ideas or help solve a real problem?
💬 I'd love to hear about it!
✅ Are you already using this technique in your Angular or frontend project?
🧠 Got questions, doubts, or your own twist on the approach?
Drop them in the comments below — let’s learn together!
🙌 Let’s Grow Together!
If this article added value to your dev journey:
🔁 Share it with your team, tech friends, or community — you never know who might need it right now.
📌 Save it for later and revisit as a quick reference.
🚀 Follow Me for More Angular & Frontend Goodness:
I regularly share hands-on tutorials, clean code tips, scalable frontend architecture, and real-world problem-solving guides.
- 💼 LinkedIn — Let’s connect professionally
- 🎥 Threads — Short-form frontend insights
- 🐦 X (Twitter) — Developer banter + code snippets
- 👥 BlueSky — Stay up to date on frontend trends
- 🌟 GitHub Projects — Explore code in action
- 🌐 Website — Everything in one place
- 📚 Medium Blog — Long-form content and deep-dives
- 💬 Dev Blog — Free Long-form content and deep-dives
- ✉️ Substack — Weekly frontend stories & curated resources
- 🧩 Portfolio — Projects, talks, and recognitions
- ✍️ Hashnode — Developer blog posts & tech discussions
🎉 If you found this article valuable:
- Leave a 👏 Clap
- Drop a 💬 Comment
- Hit 🔔 Follow for more weekly frontend insights
Let’s build cleaner, faster, and smarter web apps — together.
Stay tuned for more Angular tips, patterns, and performance tricks! 🧪🧠🚀
Top comments (0)