Erste Schritte mit Direktiven und Komponenten

AngularJS 2.0: Frameworkneutrale Komponenten einsetzen
Kommentare

Durch Unterstützung für Web Components können AngularJS-Komponenten künftig auch in anderen Frameworks zum Einsatz kommen. Daneben können AngularJS-Anwendungen ohne Umwege Web Components, die mit anderen Frameworks geschaffen wurden, nutzen.

Artikelserie

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

Mittlerweile können Webentwickler auf eine Vielzahl an optisch ansprechenden Steuerelementen für JavaScript-Projekte zurückgreifen. Ob Grids, Trees oder interaktive Diagramme – für all diese Aufgaben stehen freie sowie kommerzielle Lösungen zur Verfügung. Da es bislang jedoch keinen Standard für wiederverwendbare JavaScript-Komponenten gegeben hat, hat jedes Framework hierfür einen eigenen Rahmen geschaffen. Genau das verhindert allzu oft, dass eine bestehende Komponente problemlos mit dem Framework der Wahl zusammenspielt.

Diese Hürde möchte das Produktteam hinter AngularJS 2.0 beseitigen. Deswegen orientiert sich die kommende Version von Googles SPA-Flaggschiff an den derzeit viel diskutierten Web Components. Entwickler können somit Komponenten für den Einsatz mit anderen Frameworks bereitstellen, aber auch Web Components, die mit anderen Frameworks entwickelt wurden, konsumieren.

Dieser Artikel betrachtet das Direktivenkonzept, das die Basis für solche Komponenten in AngularJS 2.0 darstellt. Dazu greift er das schon in vorangegangenen Ausgaben verwendete Beispiel auf, das eine Alternative zu Auswahlfeldern darstellt (Abb. 1), und beschreibt eine mögliche Umsetzung mit AngularJS 2.0. Die vollständigen Quellcodedateien findet man hier und stützen sich auf TypeScript 1.5. Zur Betrachtung und Bearbeitung empfiehlt sich deswegen ein Editor mit TypeScript-Integration, beispielsweise das 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.0 handelt es sich um die Alpha 33. Somit können sich Details bis zur Veröffentlichung der finalen Version von AngularJS 2.0 noch ändern. Nichtsdestotrotz geben die hier gezeigten Beispiele ein Gefühl für die Ideen und Konzepte hinter AngularJS 2.0.

Abb. 1: Beispiel für eigene Direktive

Abb. 1: Beispiel für eigene Direktive

Direktiven und Komponenten

Direktiven sind Klassen, die AngularJS 2.0 mit DOM-Elementen in der View verknüpft. Diese Elemente nennt AngularJS 2.0 auch Hostelemente. Jede Direktive erhält einen CSS-Selektor, der diese Elemente identifiziert. Beispielsweise würde AngularJS 2.0 das nachfolgende Element mit einer Direktive, deren Selektor auf das Attribut selected oder den Elementnamen option-item verweist, in Verbindung bringen: <option-item selected=“true“></option-item>.

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)


Zur Kommunikation mit der Außenwelt nutzen Direktiven Eigenschaften und Ereignisse. Über Eigenschaften nehmen sie Daten entgegen. Hierzu hinterlegt der Entwickler entweder direkt die gewünschten Werte oder er richtet ein One Way Data Binding, das Daten an die Eigenschaft weitergibt, ein. Das Veröffentlichen von Daten erfolgt hingegen ausschließlich über Ereignisse. Das nachfolgende Snippet veranschaulicht dies, indem es die Eigenschaft selected an die Variable perExpress und das Ereignis change an die gleichnamige Methode bindet. Die eckigen Klammern drücken ein Binding an eine Eigenschaft und die runden Klammern ein Binding an ein Ereignis aus:

<option-item [selected]="perExpress" 
  (change)="change()">Per Express</option-item>

Wie der zweite Teil dieser Artikelserie zeigen wird, erlaubt die Kombination von Eigenschaften und Ereignissen das Nachbilden von Two Way Data Binding.

Eine besondere Form von Direktiven stellen Komponenten dar. Es handelt sich dabei um Direktiven mit einem Template, das AngularJS 2.0 zur Anzeige bringt. Dazu fügt es das Template in das Element der Komponente ein. Zur Abgrenzung von diesen Templates nennt man die Klassen, die die Logik für eine Komponente bereitstellen, auch Controller.

Web Components und AngularJS 2.0

Komponenten sind in AngularJS 2.0 omnipräsent. Genaugenommen ist eine AngularJS-Anwendung nichts anderes als eine Komponente, die wiederum aus weiteren Komponenten besteht. Während das Produktteam ursprünglich sämtliche Komponenten in Form von Web Components bereitstellen wollte, hat man sich im Laufe der Alphaphase von dieser Idee ein wenig distanziert. AngularJS-Komponenten orientieren sich zwar stark an Web Components, können per se jedoch nicht als solche genutzt werden. Damit möchte sich das Produktteam wohl von den Standardisierungsbestrebungen zu Web Components unabhängig machen und so seine Flexibilität steigern. Das erscheint sinnvoll, zumal diese Bestrebungen in letzter Zeit schleppend vorangehen.

Allerdings werden sämtliche AngularJS-2.0-Komponenten ohne großen Aufwand als Web Components exportiert werden können. Das Produktteam plant hierzu den Einsatz von Wrappern. Somit erhalten Entwickler das Beste aus beiden Welten, nämlich zum einen die Möglichkeit der frameworkübergreifenden Nutzung und zum anderen die nötige Leichtgewichtigkeit durch die lose Kopplung zum Standard. Viel wichtiger ist jedoch, dass AngularJS 2.0 bei der Nutzung von Komponenten keinen Unterschied zwischen den eigenen und Web Components macht. Somit können AngularJS-2.0-Entwickler auf Komponenten, die mit anderen Frameworks geschrieben wurden, ohne Umwege zurückgreifen.

Erste Schritte mit Komponenten

Listing 1 zeigt, wie eine Komponente mit Eigenschaften und Ereignissen erstellt wird. Es importiert zunächst die benötigten Elemente aus dem Modul von AngularJS 2.0 und definiert anschließend eine Klasse OptionItem, die eine Auswahloption (vgl. Abb. 1) repräsentiert. Das Attribut selected gibt Auskunft über den Zustand der Option, also darüber, ob sie ausgewählt ist, und value ist mit einer Variablen, die die Auswahloption repräsentiert, verknüpft. Ein Ereignis, das die Außenwelt über Zustandsänderungen informiert, spiegelt das Attribut change wieder. Solche Ereignisse stellt AngularJS 2.0 mit Instanzen von EventEmitter dar.

OptionItem weist auch eine Methode select auf. Wie später gezeigt wird, ist sie an Click-Ereignisse ihrer View gekoppelt. Ihre Aufgabe ist das Ändern des Zustands der Option und das Auslösen des change-Ereignisses. Im Zuge dessen übermittelt sie Informationen über den aktuellen Zustand an den Ereignisempfänger.

Damit AngularJS 2.0 die Klasse OptionItem als Komponente erkennt, ist sie mit dem Dekorator Component zu annotieren. Im Zuge dessen gibt der Entwickler über das Feld selector einen CSS-Selektor an. Dieser adressiert jenes Element, das die Komponente innerhalb der Anwendung repräsentiert. Das Feld properties erhält die Namen jener Attribute, die die Komponente in Form von Eigenschaften der Außenwelt zur Verfügung stellt und events erwähnt auf dieselbe Weise jene Eigenschaften, die die Komponente als Ereignis veröffentlicht.

Die Klasse OptionItem wurde in Listing 1 zusätzlich mit dem Dekorator View annotiert. Sie gibt Auskunft über das Template, das die Komponente visualisiert. Dazu verweist es im betrachteten Fall mit dem Feld templateUrl auf eine HTML-Datei. Alternativ dazu steht auch eine Eigenschaft template zur Verfügung. Diese nimmt das zu nutzende Markup direkt in Form eines Strings entgegen. Über das Feld directives verweist View auf jene Direktiven, die innerhalb des Templates genutzt werden können. Im betrachteten Fall handelt es sich dabei um die Direktive NgIf.

Anzeigen von Direktiven
Die Tatsache, dass der Entwickler über den Dekorator View angeben muss, welche Direktiven er nutzen möchte, erhöht die Testbarkeit. Zudem versetzt dies Unit Tests in die Lage, Direktiven gegen Attrappen (Mocks) zu tauschen. Darüber hinaus verhindert es auch die Notwendigkeit für globale Objekte, bei denen sämtliche zu nutzende Direktiven zu registrieren sind. Da es jedoch lästig ist, ständig sämtliche Standarddirektiven anführen zu müssen, hat das Produktteam vor, in künftigen Versionen die wichtigsten Direktiven, wie NgIf und NgFor, automatisch zu berücksichtigen. Um weitere Direktiven automatisch zu berücksichtigen, kann der Entwickler vom Dekorator View ableiten und die so geschaffene Subklasse anweisen, die gewünschten Direktiven zu beachten.

Listing 1

import {Component, View, NgIf, EventEmitter, bootstrap} 
  from 'angular2/angular2';
@Component({
  selector: 'option-item',
  properties: ['selected', 'value'],
  events: ['change']
})
@View({
  templateUrl: 'option-item.html',
  directives: [NgIf]
})
export class OptionItem {
  
  selected: boolean;
  value: string;
  change: EventEmitter = new EventEmitter(); 
  
  select() {
    this.selected = !this.selected;    
    this.change.next({ 
      target: this, 
        selected: this.selected, 
        value: this.value });
  }
  
}

Das Template der Komponente OptionItem findet sich in Listing 2. Es referenziert zunächst die zu nutzende CSS-Datei. Das ist notwendig, zumal das hinter Web Components stehende Konzept des Shadow DOMs eine Komponente vom Rest der Anwendung isoliert. Zwar wird so die Kapselung erhöht, es führt jedoch auch dazu, dass die Komponente auf außerhalb definierte oder referenzierte Formatierungsvorlagen nicht zugreifen kann.

Anschließend definiert das Template ein div-Element. An dieses bindet es die Eigenschaft selected der Komponente über One Way Bindings. Ist diese Eigenschaft true (bzw. truthy), weist das erste Binding die CSS-Klasse itemOn zu. Ansonsten kümmert sich das zweite Binding um das Zuweisen der CSS-Klasse itemOff. Das danach folgende Event-Binding verknüpft das click-Event mit der vorhin betrachteten Methode select der Komponente. Das Symbol ^ vor click veranlasst AngularJS 2.0 dazu, das Ereignis auch dann zu berücksichtigen, wenn es auf einem untergeordneten Element auftritt und über das Event-Bubbling von JavaScript das betrachtete div erreicht.
Das Element ng-content kommt für die so genannte Content Reprojection zum Einsatz. Dieses Konzept dürfte AngularJS-1.x-Entwicklern unter Transclusion bekannt sein. Das bedeutet, dass ng-content den ursprünglichen Inhalt des HTML-Elements, das die Komponente repräsentiert, darstellt. Entscheidet sich der Entwickler beispielsweise, die hier betrachtete Komponente mittels <option-item>Option A</option-item> in eine Seite einzubinden, so ersetzt AngularJS 2.0 zunächst den Text Option A des option-item-Elements mit dem Template. Damit der Text Option A jedoch nicht verloren geht, platziert AngularJS diesen Text innerhalb des Templates an der mit ng-content markierten Stelle.

Listing 2

<style>@import url('option-item.css');</style>

<div 
  [class.itemOn]="selected" 
  [class.itemOff]="!selected"
  (^click)="select()"> 
  
  <ng-content></ng-content> <span *ng-if="selected">*</span> 
  
</div>

Das hier betrachtete Beispiel zeigt, dass AngularJS 2.0 in der Tradition seines Vorgängers auf Sonderzeichen setzt. Diese mögen auf den ersten Blick ein wenig gewöhnungsbedürftig sein. Allerdings erleichtern sie auch die Lesbarkeit des Codes, zumal sie die genutzten Konzepte explizit machen: Eckige Klammern definieren zum Beispiel ein Property Binding. Dabei handelt es sich um ein One Way Binding, das Daten von der aktuellen Komponente in die View transportiert. Runde Klammern deuten hingegen auf ein Event Binding hin und die optionale Angabe des Dachsymbols ^ ermöglicht die Nutzung von Event-Bubbling. Eine Raute definiert eine Variable und die Nutzung des Sterns zeigt an, dass die jeweilige Direktive den Inhalt des markierten Elements als Vorlage nutzt. Die Direktive ng-if blendet diese Vorlage zum Beispiel ein und aus; andere Direktiven können den Inhalt aber auch duplizieren.

Wer sich mit den Klammern nicht anfreunden kann oder Werkzeuge verwendet, die damit nicht umgehen können, kann sie auch meiden und stattdessen auf eine etwas längere Schreibweise setzen. Property Bindings können zum Beispiel auch durch das Präfix bind- definiert werden und zum Festlegen von Event Bindings steht das Präfix on- zur Verfügung. Somit könnte der Entwickler das hier betrachtete Template auch wie folgt darstellen:

<div bind-class.itemOn="selected" 
  bind-class.itemOff="!selected" 
  on-^click="select()">...</div>

Komponenten einbinden

Um die Komponente zu nutzen, ist im Template der Wahl an der gewünschten Stelle lediglich ein Element, das dem definierten CSS-Selektor entspricht, zu positionieren. Darüber hinaus sind Attribute für sämtliche Bindings einzurichten. Listing 3 veranschaulicht dies anhand des Templates einer weiteren Komponente, die eine Demoanwendung repräsentiert und auf den Namen AppComponent hört. Dieses stellt die beiden in Abbildung 1 dargestellten Optionen bereit.

Das Listing bindet auch das Ereignis change über ein Event Binding an die gleichnamige Methode der AppComponent. Dabei wird die Pseudovariable $event übergeben. Diese Variable beinhaltet den Wert, den die Komponente beim Auslösen des Ereignisses an die Methode next des EventEmitters übergeben hat (vgl. Listing 1). Im betrachteten Fall handelt es sich um ein Objekt, das den aktuellen Zustand der Option widerspiegelt.

Zur Demonstration weist Listing 3 den beiden option-item-Elementen auch einen Variablennamen zu. Diese lauten i1 und i2, wobei zur Deklaration dieser Namen eine Raute vorangestellt wird. Diese Variablen nutzt es, um an den zweiten Parameter von change den aktuellen Wert der Eigenschaft selected zu übergeben.

Listing 3

<h1>{{title}}</h1>
<div>
  <option-item #i1 [selected]="true" [value]="1" 
    (change)="change($event, i1.selected)">Per Express</option-item>
  <option-item #i2 [selected]="false" [value]="2" 
    (change)="change($event, i2.selected)">Per Einschreiben</option-item>

Den Controller von AppComponent findet man in Listing 4. Er legt den Selektor my-app fest, verweist auf das zuvor betrachtete Template und referenziert die OptionItem-Komponente. Darüber hinaus stellt er die über das Event Binding referenzierte Methode change zur Verfügung. Am Ende ruft Listing 4 die Funktion bootstrap auf und übergibt dabei die AppComponent. Das führt dazu, dass der Entwickler diese Komponente auf oberster Ebene einer AngularJS-2.0-Anwendung nutzen kann. Somit benötigt er nur mehr eine Seite, die zum einen AngularJS 2.0 und den Programmcode der Beispielanwendung referenziert und zum anderen ein Element my-app aufweist. Eine solche Datei, die den Namen index.html trägt, findet man in den vom Autor bereitgestellten Quellcodedateien.

Listing 4

import {Component, View, bootstrap} from 'angular2/angular2';
import {OptionItem} from 'option-item';

@Component({
  selector: 'my-app'
})
@View({
  templateUrl: 'app.html',
  directives: [OptionItem] 
}) 
class AppComponent {
  title: string;
   
  constructor() {
    this.title = 'Component-Demo';
  }
  
  change(event, info) {
    console.log('change!');
    console.debug(event);
    console.debug(info);
  }
}

bootstrap(AppComponent);

Direkter Zugriff auf gerenderte Inhalte

In manchen Fällen kann es nützlich sein, direkten Zugriff auf das Hostelement zu erlangen. Dazu lässt sich der Entwickler lediglich ein Element vom Typ ElementRef injizieren. Hierbei handelt es sich jedoch nicht unmittelbar um das zugrunde liegende DOM-Element, sondern nur um einen Wrapper dafür. Das ist notwendig, weil das Rendering bei AngularJS 2.0 auch außerhalb des von JavaScript genutzten Haupthreads stattfinden kann, beispielsweise am Server oder in einem Web-Worker.

Auch wenn eine Komponente über eine injizierte ElementRef-Instanz direkt auf die Attribute des Hostelements zugreifen kann, gibt es dafür auch einen einfacheren Weg: Durch Nutzung des Dekorators Attribute kann sich die Komponente Attributwerte injizieren lassen.

Das OptionItem in Listing 5 demonstriert das. Es lässt sich den Element-Wrapper in den Konstruktor gemeinsam mit dem Wert des Attributs round injizieren. Weist Letzterer den Wert true auf, verpasst der Konstruktor dem ersten Kindelement im Template runde Ecken. Dazu setzt er die CSS-Eigenschaft border-radius. Diesen Effekt könnte man natürlich auch über die klassische zuvor vorgestellte Datenbindung realisieren. Somit dient dieses Beispiel in erster Linie der Demonstration.

Listing 5

[...]
export class OptionItem {
  
  [...]
  
  constructor(elm: ElementRef, @Attribute('round') round: string) {
    if (round == 'true') {
      elm.nativeElement.firstElementChild
                       .style.borderRadius = "5px";
    }
  }
  [...]
}

Kommunikation zwischen Direktiven

Ähnlich wie in AngularJS 1.x kann eine Direktive eine andere Direktive, die sich im DOM an derselben Stelle oder oberhalb befindet, via Dependency Injection beziehen. Auf diese Weise lassen sich Kommunikationswege zwischen einzelnen Direktiven einrichten. Dieser Abschnitt demonstriert das anhand einer Erweiterung zum betrachteten Beispiel. Das Ziel ist es, option-item-Elemente mit einem option-box-Element zu gruppieren:

<option-box>
  <option-item [...]>Per Express</option-item>
  <option-item [...]>Per Einschreiben</option-item>
</option-box>

Die hinter dem Element option-box stehende OptionBox-Direktive soll dabei sicherstellen, dass immer nur maximal eine OptionItem-Komponente aktiviert ist. Dazu lassen sich alle OptionItems in die OptionBox, die sich im DOM-Baum über diesem befindet, injizieren. Zusätzlich registrieren sich alle OptionItems bei der OptionBox über einen Methodenaufruf. Auf diese Weise erhält die OptionBox eine Referenz auf alle untergeordneten OptionItems. Ändert sich der Zustand eines OptionItems, so informiert es die OptionBox. Diese deaktiviert alle anderen OptionItems.

Die Implementierung der OptionBox findet sich in Listing 6. Da sie keine eigene View benötigt, handelt es sich dabei lediglich um eine herkömmliche Direktive und nicht um eine Komponente. Deswegen wurde sie auch mit dem Dekorator Directive annotiert. Der Selektor adressiert option-box-Elemente.

Über die Methode registerItem können sich OptionItems bei der OptionBox registrieren. Im Zuge dessen hinterlegt registerItem das OptionItem im Array items. Durch Aufruf der Methode notifySelected signalisiert ein OptionItem, dass sich ihr Zustand geändert hat. Diese Methode iteriert sämtliche registrierte OptionItems und deaktiviert alle außer dem aktuellen. Auf diese Weise stellt sie sicher, dass zu einem Zeitpunkt nur maximal ein OptionItem aktiv sein kann.

Listing 6

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

@Directive({
  selector: 'option-box'
})
export class OptionBox {
  
  items = new Array();
  
  registerItem(item: OptionItem) {
    this.items.push(item);
  }
  
  notifySelected(selectedItem: OptionItem) {
    
    for(var item of this.items) {
      if (item != selectedItem) {
        item.selected = false;
      }
    }
    
  }
}

Das aktualisierte OptionItem findet sich in Listing 7. Der Konstruktor lässt sich eine OptionBox injizieren. Das Voranstellen des Dekorators Ancestor bewirkt, dass AngularJS 2.0 ausgehend von der OptionBox den DOM-Baum nach oben hin durchsucht und die erste gefundene OptionBox-Direktive heranzieht.

Der Dekorator Optional drückt aus, dass die OptionBox nicht zwingend notwendig ist. Das hat zur Folge, dass AngularJS 2.0 den Wert null injiziert, wenn es keine OptionBox findet. Ohne Angabe von Optional würde solch ein Fall zu einer Exception führen.

Die auf diese Weise erhaltene OptionBox-Instanz hinterlegt der Konstruktor in einer Instanzvariablen. Sofern die Instanz nicht null ist, registriert sich das OptionItem bei der OptionBox durch Aufruf von registerItem. Dabei übergibt sie sich selbst (this). Daneben wurde auch select erweitert, sodass bei jedem Zustandswechsel die Methode notifySelected der OptionBox aufgerufen wird. Auch hier erfolgt eine Prüfung gegen null für den Fall, bei dem keine OptionBox vorliegt.

Listing 7

[...]
export class OptionItem {
  
  [...]
  box: OptionBox;
  
  constructor(@Optional() @Ancestor() box: OptionBox) {
    this.box = box;
    if (this.box != null) {
      this.box.registerItem(this);
    }
  }
  
  select() {
    this.selected = !this.selected;
    [...]
    if (this.box != null) {
      this.box.notifySelected(this);
    }
  }
}

Zusammenfassung

Mit AngularJS 2.0 möchte das Produktteam die Zeiten, in denen Komponenten an das Framework der Wahl angepasst werden mussten, beenden. Erreicht werden soll das durch die breite Unterstützung von Web Components. Während Komponenten in AngularJS 2.0 nicht per se, wie ursprünglich geplant, Web Components darstellen, orientieren sie sich stark an dieser aufkommenden Technologie und können somit für andere Frameworks als solche exportiert werden. Daneben können Entwickler ohne Umwege Web Components, die mit anderen Werkzeugen erstellt wurden, einsetzen.

Der vorliegende erste Teil dieses Zweiteilers hat anhand eines einfachen Beispiels die wichtigsten Kniffe bei der Entwicklung von Komponenten und Direktiven in AngularJS 2.0 veranschaulicht. Dabei ist er auch auf weiterführende Themen, wie Content Reprojection und die Kommunikation zwischen Direktiven eingegangen. Der zweite Artikel wird sich um Lifecycle-Events und Two Way Data Binding kümmern sowie das Thema Content Reprojection erneut aufgreifen.

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: Internet of Things concept von Shutterstock / Urheberrecht: a-image

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -