DEV Community

Cover image for Angular’s Game-Changing Dynamic Component Features: inputBinding(), outputBinding(), and twoWayBinding()
Rajat
Rajat

Posted on

Angular’s Game-Changing Dynamic Component Features: inputBinding(), outputBinding(), and twoWayBinding()

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(), and twoWayBinding() 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
}

Enter fullscreen mode Exit fullscreen mode

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' });
  }
}

Enter fullscreen mode Exit fullscreen mode

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);
  });
}

Enter fullscreen mode Exit fullscreen mode

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);
    }
  );
}

Enter fullscreen mode Exit fullscreen mode

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;
  }
}

Enter fullscreen mode Exit fullscreen mode

💡 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);
    });
  });
});

Enter fullscreen mode Exit fullscreen mode

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;
  }
}

Enter fullscreen mode Exit fullscreen mode

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: []
    });
  }
}

Enter fullscreen mode Exit fullscreen mode

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();
  }
}

Enter fullscreen mode Exit fullscreen mode

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`);
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

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
});

Enter fullscreen mode Exit fullscreen mode

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);
  }
}

Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

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:

  1. Try implementing one of these examples in your current project
  2. Refactor an existing dynamic component using the new APIs
  3. Share this article with your team - they'll thank you later!
  4. 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! 🧪🧠🚀

✨ Share Your Thoughts To 📣 Set Your Notification Preference

Top comments (0)