DEV Community

Connie Leung
Connie Leung

Posted on

1

Dynamically-created components support two-way bindings

Angular 20's createComponent function will support two-way binding in addition to input and output bindings and directives.

The feature is in 20.0.0-next.3; therefore, it can be tested after updating the Angular dependencies to the next version.

ng update @angular/cli --next
ng update @angular/core --next
Enter fullscreen mode Exit fullscreen mode

This demo will show how to invoke the createComponent method to create a dynamic component displaying the information of Star Wars characters. The dynamic component has a model input that will be bound to the signal of the App component. When users click the dynamic component's button, the click event updates the model input and synchronizes the value of the signal. Finally, the App component displays the latest value of the model input.

Define the AppStarWarCharacterComponent Compnent

@Component({
 selector: 'app-star-war-character',
 template: `
   template: `
      @if(person(); as person) {
        <p><span>Id:</span> {{ person.id }} </p>
        @if (isSith()) {
          <p>A Sith, he is evil.</p>
        }         
        <p><span>Name: </span>{{ person.name }}</p>
        <p><span>Height: </span>{{ person.height }}</p>
        <button (click)="voted()">Voted for {{ person.name }}</button>
      }
  `,
 changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppStarWarCharacterComponent { 
  id = input(1);
  isSith = input(false);
  lastClicked = model('')

  voted() {
    const name = this.person()?.name || 'NA';
    const currentTime = new Date(Date.now()).toISOString();
    this.lastClicked.set(`You voted for ${name} at ${currentTime}`);
  }

  getPersonFn = getPerson();

  person = toSignal(toObservable(this.id)
    .pipe(switchMap((id) => this.getPersonFn(id)))
  );
}
Enter fullscreen mode Exit fullscreen mode

The AppStarWarCharacterComponent component is responsible for displaying the details of a Star Wars character. The component has a lastClicked model input that will be bound to the parent component's signal. It also has a voted method that sets the lastClicked model input after the button click.

Create AppStarWarCharacterComponent Dynamically with the CreateComponent Function

<div class="container">
   <ng-container #vcr />
</div>

<ng-container [ngTemplateOutlet]="starwars"
       [ngTemplateOutletContext]="{ items: jediFighters(), isSith: false  }" />

<ng-template let-items="items" let-isSith="isSith" #starwars>
    <select [ngModel]="items[0].id" #id="ngModel">
        @for (item of items; track item.id) {
          <option [ngValue]="item.id">{{ item.name }}</option>
        }
    </select>
    @let text = isSith ? 'Add a Sith' : 'Add a Jedi';

   <button (click)="addAJedi(id.value, isSith)">{{ text }}</button>
</ng-template>
Enter fullscreen mode Exit fullscreen mode

The inline template has a &lt;ng-container #vcr&gt; with vcr template variable. It also displays a dropdown list containing the names of the Jedi fighters.

vcr = viewChild.required('vcr', { read: ViewContainerRef });
Enter fullscreen mode Exit fullscreen mode

In the component class, the viewChild function queries the VierContainerRef and assigns to the vcr field.

When users select a Jedi fighter and click the "Add a Jedi" button, the addAJedi method is triggered. The method imports the AppStarWarCharacterComponent class to create the component dynamically.

lastClicked = signal('');

async addAJedi(id: number, isSith = false) {
   const { AppStarWarCharacterComponent } = await import ('./star-war/star-war-character.component');
   const componentRef = this.vcr()
.createComponent(AppStarWarCharacterComponent,
     {
       bindings: [
          inputBinding('id', () => id),
          inputBinding('isSith', () => isSith),
          twoWayBinding('lastClicked', this.lastClicked),
       ]
     }
   );
   this.componentRefs.push(componentRef);
}
Enter fullscreen mode Exit fullscreen mode

The ViewContainerRef class has a createComponent method; therefore, it is executed to create an instance of AppStarWarCharacterComponent and set the bindings. The twoWayBinding function binds the lastClicked model input to the lastClicked signal.

<div>
      <span>Two-way Bindings: {{ lastClicked() }}</span>
</div>
Enter fullscreen mode Exit fullscreen mode

The template displays the lastClicked signal, which is the timestamp of the most recent button click.

ngOnDestroy(): void {
    if (this.componentRefs) {
      for (const ref of this.componentRefs) {
        ref.destroy();
      }
    }
}
Enter fullscreen mode Exit fullscreen mode

The createComponent method returns a ComponentRef inserted to the ViewContainerRef. The ComponentRef is tracked by the componentRefs array. When the App component is destroyed, the ngOnDestroy lifecycle hook method also runs to destroy the ComponentRef to release the memory to avoid memory leaks.

Creating dynamic components is easier in v20 when it supports two-way bindings, input and output bindings, and directives.

References:

Top comments (0)

👋 Kindness is contagious

If this post resonated with you, feel free to hit ❤️ or leave a quick comment to share your thoughts!

Okay