Entwicklung wiederverwendbarer Komponenten mit AngularJS 2

AngularJS 2: Lifecycle-Events und Two-Way Data Binding
Kommentare

Wie auch schon der Vorgänger AngularJS 1.x bietet AngularJS 2 ein mächtiges Direktivenkonzept zum Bereitstellen wiederverwendbarer Komponenten, die in der Lage sind, das Vokabular von HTML zu erweitern.

Nachdem der vorangegangene Artikel „AngularJS 2.0: Frameworkneutrale Komponenten einsetzen“ die Grundlagen zur Entwicklung wiederverwendbarer Komponenten mit AngularJS 2 vorgestellt hat, wendet sich dieser zweite Teil den Themen Lifecycle-Events, Two-Way Data Binding und Content Reprojection zu. Dazu greift er das im ersten Teil verwendete Beispiel, das eine Alternative zu Auswahlfeldern bereitstellt (Abb. 1), erneut auf und erweitert es. Die vollständigen Quellcodedateien finden sich hier und stützen sich auf TypeScript 1.5. Zur Betrachtung und Bearbeitung empfiehlt sich deswegen ein Editor mit TypeScript-Integration, beispielsweise der leichtgewichtige Visual Studio Code. Informationen über die Nutzung von TypeScript in Visual Studio Code findet man hier.

Bei der hier verwendeten Version von AngularJS 2 handelt es sich um die Alpha 33. Somit können sich Details bis zur Veröffentlichung der finalen Version von AngularJS 2 noch ändern. Nichtsdestotrotz geben die hier gezeigten Beispiele ein Gefühl für die Ideen und Konzepte hinter AngularJS 2.

Artikelserie

Teil 1: AngularJS 2.0: Frameworkneutrale Komponenten einsetzen
Teil 2: Lifecycle-Events und Two-Way Data Binding

Abb. 1: Beispiel für eigene Direktive

Abb. 1: Beispiel für eigene Direktive

Lifecycle-Events

Die so genannten Lifecycle-Events werden von AngularJS 2 zu bestimmten Zeitpunkten des Lebenszyklus von Direktiven ausgelöst. Um darauf zu reagieren, muss eine Komponente lediglich eine oder mehrere von AngularJS 2 vorgegebenen Methoden implementieren. Zusätzlich muss die Komponente über eine Annotation anzeigen, über welche Lifecycle-Events sie benachrichtigt werden möchte.

Schnell und überall: Datenzugriff mit Entity Framework Core 2.0

Dr. Holger Schwichtenberg (www.IT-Visions.de/5Minds IT-Solutions)

C# 7.0 – Neues im Detail

Christian Nagel (CN innovation)

Um dies zu veranschaulichen, erweitert dieser Abschnitt die Klasse OptionBox um eine Eigenschaft value (Listing 1). Diese zeigt an, welches OptionItem aktiv ist. Eingangs ist das OptionItem zu aktivieren, das diesen Wert aufweist. Bei jeder Zustandsänderung ist der Wert des aktiven OptionItems in diese Eigenschaft zu übernehmen. Demnach würde im folgenden Fall nach dem Laden der Anwendung die zweite Option, also die mit dem Wert 2, ausgewählt sein:

<option-box [value]="2">
  <option-item [value]="1">Per Express</option-item>
  <option-item [value]="2">Per Einschreiben</option-item>
</option-box>

Listing 1

[...]
export class OptionBox {

  items = new Array<OptionItem>();
  value: string;

  [...]
}

Allerdings steht im Konstruktor von OptionItem, der die OptionBox samt ihrer neuen Eigenschaft value injiziert bekommt, der an value zu bindende Wert noch nicht zur Verfügung. Der Grund dafür ist, dass die Datenbindung erst nach dem Instanziieren und somit auch erst nach dem Ausführen der Konstruktoren erfolgen kann. Die im Konstruktor auskommentierte Zeile kann so deswegen nicht verwendet werden.

Abhilfe schafft hier das Lifecycle-Event onInit, das AngularJS 2 nach der initialen Datenbindung auslöst. Um dieses Ereignis zu behandeln, implementiert die Komponente OptionItem die damit assoziierte Methode, die sich ebenfalls onInit nennt (Listing 2). Zusätzlich gibt die Eigenschaft lifecycle des Component Decorators bekannt, dass die Komponente dieses Ereignis behandeln möchte. Hierzu bietet diese Eigenschaft ein Array mit dem Wert LifecycleEvent.onInit. Bei LifecycleEvent handelt es sich um eine Enumeration, die für jedes Lifecycle-Event einen Eintrag aufweist.

In onInit kann das OptionItem auf den an die OptionBox gebundenen Wert zugreifen und diesen mit dem eigenen Wert vergleichen, um herauszufinden, ob sie sich aktivieren soll.

Listing 2

[...]
export class OptionItem {

  [...]
  constructor(@Optional() @Ancestor() box: OptionBox ) {

    // Funktioniert nicht, weil _value_ noch nicht gebunden ist: 
    // this.selected = (this.box.value == this.value); 

    [...]
  }

  select() { [...] }

  onInit() {
    this.selected = (this.box.value == this.value);
  }
}

Neben onInit stehen noch weitere Lifecycle-Events zur Verfügung. Tabelle 1 informiert darüber, wobei die Bezeichnung zum einen den Namen der einzurichtenden Methode sowie zum anderen den jeweiligen Eintrag in der Enumeration LifecycleEvent darstellt.

Tabelle 1: Lifecycle-Events

Tabelle 1: Lifecycle-Events

Der Aufruf von onChange erhält auch ein Objekt mit den alten und neuen Werten der geänderten Eigenschaften. Somit kann die Komponente herausfinden, welche Eigenschaften sich wie geändert haben (Listing 3). Dabei ist zu beachten, dass AngularJS 2 onChange nur dann aufruft, wenn der Datenbindungsmechanismus eine Änderung ausgelöst hat. Bei direkten Änderungen durch benachbarte Direktiven ist dies nicht der Fall.

Listing 3

onChange(changes: any) {
  if (changes['selected']) {
    console.log(
      'selected wurde geändert von ' + 
      changes['selected'].previousValue + ' zu ' +
      changes['selected'].currentValue);
  }
}

Two-Way Data Binding

Wie eingangs erwähnt, bietet AngularJS 2 ein One-Way Binding, das Daten aus dem Controller an Eigenschaften im DOM überträgt. Ob diese Eigenschaften zu einer Komponente oder zu einem HTML-Element gehören, ist dabei nebensächlich. Auch die Nutzung von Platzhaltern, wie beispielsweise {{title}}, gehört in diese Kategorie und ist streng genommen auch nur eine etwas einfachere Schreibweise für die Bindung an die DOM-Eigenschaft textContent des umschließenden Elements.

Um Änderungen von einer Komponente zurück in den jeweiligen Controller zu übertragen, nutzt der Entwickler hingegen Event Bindings. Ein Two-Way Binding kann somit durch den gemeinsamen Einsatz von Property- und Event Binding implementiert werden. AngularJS 2 unterstützt dabei sogar mit ein wenig Syntaxzucker. Die Entscheidung, Two-Way Bindings auf diese Art zu realisieren, ist ein Schlüssel zu äußerst hoher Performance, die AngularJS 2 im Gegensatz zum Vorgänger sowie zu anderen Frameworks bietet.

Listing 4 demonstriert dies anhand einer Erweiterung, die ein Two-Way Data Binding für die Eigenschaft value der OptionBox bietet. Dazu erhält die betrachtete Direktive neben der Eigenschaft value auch ein Ereignis valueChanged. Die Schreibweise events: [‚valueChanged: value‘] bei der Nutzung des Decorators Directive legt fest, dass dieses Ereignis innerhalb der Komponente zwar durch den EventEmitter in der Variablen valueChanged, außerhalb jedoch durch das Ereignis value repräsentiert wird. Konsumenten der Direktive sehen also eine Eigenschaft value und ein gleichnamiges Ereignis. Diese Namensgleichheit ist die Voraussetzung für die Nutzung des von AngularJS 2 gebotenen Syntaxzuckers, auf den die nachfolgenden Ausführungen eingehen.

Um das Auslösen des besprochenen Ereignisses kümmert sich die Methode notifySelected. Im Zuge dessen übergibt sie den geänderten Wert auf direkte Weise, also ohne Kapselung in einem Ereignisobjekt.

Listing 4

import {Directive, View, NgIf, EventEmitter, bootstrap} from 'angular2/angular2';
import {OptionItem} from 'option-item';

@Directive({
  selector: 'option-box',
  properties: ['value'],
  events: ['valueChanged: value']
})
export class OptionBox {

  items = new Array<OptionItem>();
  value: string;
  valueChanged: any = new EventEmitter();

  registerItem(item: OptionItem) {
    this.items.push(item);
  }

    notifySelected(selectedItem: OptionItem) {
      [...]
    this.valueChanged.next(this.value);
    [...]
  }
}

Nun kann der Entwickler Eigenschaften der AppComponent, die die gesamte Beispielanwendung repräsentiert, an den value der OptionBox binden. Um dies zu demonstrieren, spendiert Listing 5 der AppComponent eine Eigenschaft id sowie einen dazu passenden Setter setId. Letzterer kommt lediglich in einem ersten Näherungsschritt, nicht jedoch bei der endgültigen Lösung zum Einsatz.

Listing 5

[...]
class AppComponent {
  title: string;
  value: string = "2";
  
  setValue(value: string) {
    this.value = value;
  }

  [...]
}
bootstrap(AppComponent);

Zum Binden an die Eigenschaft value ist nun lediglich ein Property sowie ein Event Binding einzurichten. Dieses könnte sich wie folgt gestalten:

<option-box [value]="id" (value)="setId($event)">[...]</option-box>

Das erste Binding transportiert die ID zur OptionBox, und das zweite Binding ruft bei jeder Änderung die Methode setId auf. Dabei nimmt sie die Pseudovariable $event entgegen. Diese beherbergt den Wert, den die OptionBox beim Auslösen des Events an den EventEmitter übergeben hat. Im betrachteten Fall ist das die neue ID.

Um ohne Setter auszukommen, könnte man die dahinterstehende Zuweisung direkt in das Event Binding eintragen. Das würde dann so aussehen:

<option-box [value]="id" (value)="id = $event">[...]</option-box>

An dieser Stelle kommt der versprochene Syntaxzucker ins Spiel, denn als Kurzform für die gezeigte Schreibweise akzeptiert AngularJS 2 Folgendes:

<option-box [(value)]="id">[...]</option-box>

Die Kombination aus eckiger und runder Klammer deutet an, dass es sich sowohl um ein Property als auch um ein Event Binding handelt. Wer sich hiermit nicht anfreunden kann, kann auch auf die alternative Schreibweise zurückgreifen, die den Einsatz des Präfixes bindon- vorsieht:

<option-box bindon-value="value">[...]</option-box>

Der Vollständigkeit halber zeigt Listing 6 das gesamte Template.

Listing 6

<h1>{{title}}</h1>

<div>
  <option-box [(value)]="id">
    <option-item #i1 value="1" (change)="change($event, i1.selected)">
      Per Express
    </option-item>
    <option-item #i2 value="2" (change)="change($event, i2.selected)">
      Per Einschreiben
    </option-item>
  </option-box>
</div> 

<div>
    Value: {{ id }}
</div>

Mehr Freiheiten bei Content Reprojection

Wie das einführende Beispiel im vorangegangenen Teil gezeigt hat, erlaubt Content Reprojection das Definieren eines Platzhalters in Templates. Dieser Platzhalter wird mit dem Element ng-content dargestellt. Im Gegensatz zu seinem Vorgänger erlaubt AngularJS 2 jedoch auch die deklarative Nutzung mehrerer Platzhalter. Dieser Abschnitt erläutert dies durch eine zusätzliche Erweiterung am betrachteten Beispiel. Listing 7 zeigt dazu ein modifiziertes Template. Bei genauerem Hinsehen fällt auf, dass die OptionItems nicht nur eine Beschriftung, sondern Markup, bestehend aus zwei span-Elementen beinhalten. Die Namen der zugewiesenen Klassen legen nahe, dass es sich dabei um eine Beschriftung (label) und eine Beschreibung (text) handelt.

Listing 7

<h1>{{title}}</h1>

<div>
  <option-box [(value)]="value">

    <option-item #i1 value="1" (change)="change($event, i1.selected)">
      <span class="label">Per Express</span>
      <span class="text">Schnelle Lieferung</span>
    </option-item>

    <option-item #i2 value="2" (change)="change($event, i2.selected)">
      <span class="label">Per Einschreiben</span>
      <span class="text">Sichere und schnelle Lieferung</span>
    </option-item>

  </option-box>

</div> 

<div>
  Value: {{ value }}
</div>

Um die beiden span-Elemente in unterschiedliche Platzhalter zu projizieren, ist jeder Platzhalter des Templates von OptionItem mit einem CSS-Selektor zu versehen. Diesen erwartet AngularJS 2 in einem Attribut select von ng-content. Listing 8 demonstriert dies, indem das erste ng-content-Element den Selektor .label und das zweite den Selektor .text aufweist. Somit werden die zuvor besprochenen Elemente mit den Klassen label und text referenziert und in den jeweiligen Platzhalter eingefügt.

Listing 8

[...]
<div 
  [class.itemOn]="selected" 
  [class.itemOff]="!selected"
  (^click)="select()"> 

    <div class="item-label">
      <ng-content select=".label"></ng-content> 
        <span *ng-if="selected">*</span>
      </div>
    <div class="item-text">
      <ng-content select=".text"></ng-content>
    </div>

</div>

Fazit

Eines der wohl wichtigsten Konzepte in AngularJS ist seit jeher das Direktivenkonzept. Mit AngularJS 2 wurde es komplett überarbeitet und teilweise auch vereinfacht, ohne die gewohnte Mächtigkeit zu verlieren. Abstriche gibt es jedoch im Bereich des Two-Way Bindings, das man durch eine Kombination aus Property und Event Bindings nachbilden muss. Geschuldet ist dies den hohen Performancezielen des Produktteams: Während AngularJS 1.x in diesem Bereich nicht gerade geglänzt hat, soll sich AngularJS 2 in punkto Leistung mit den besten, allen voran React, messen können.

Artikelserie

Teil 1: AngularJS 2.0: Frameworkneutrale Komponenten einsetzen
Teil 2: Lifecycle-Events und Two-Way Data Binding

Windows Developer

Windows DeveloperDieser Artikel ist im Windows Developer erschienen. Windows Developer informiert umfassend und herstellerneutral über neue Trends und Möglichkeiten der Software- und Systementwicklung rund um Microsoft-Technologien.

Natürlich können Sie den Windows Developer über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist der Windows Developer ferner im Abonnement oder als Einzelheft erhältlich.

Aufmacherbild: Abstract background with blue stripes corner und vector illustration of light red shield with A letter for javascript via Shutterstock / Urheberrecht: sumkinn & gdainti

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -