Sie sind hier: Weblog

Eigene Komponenten mit Angular 2

Foto ,
28.03.2015 15:15:00

Eine der Stärken von AngularJS ist seit jeher die Möglichkeit, mit Direktiven eigene Komponenten zu schaffen. Ab Angular 2 basieren diese Direktiven auf Web Components. Dieser Beitrag zeigt anhand eines Beispiels, wie man damit eigene Steuerelemente erstellen kann. Im Zuge dessen geht er auf die folgenden Aspekte, die sich durch die Umstellung auf Web Components nun anders gestalten, ein:

  • Binden von Ereignissen in eigenen Komponenten
  • Binden von Werten in eigenen Komponenten
  • Transclusion
  • Kommunikation zwischen Komponenten

Überblick über das hier gezeigte Beispiel

Das hier besprochene Beispiel, welches via GitHub zum Download bereit steht, zeigt die Erstellung einer benutzerdefinierten Auswahlliste. Der Benutzer kann eine der angebotenen Optionen per Klick aktivieren. Diese wird daraufhin mit blauen Hintergrund hervorgehoben dargestellt.

Implementierung der Direktive

Die Implementierung der vorgestellten Direktive besteht aus einer Web-Component (Direktive). Hierbei handelt es sich um eine Klasse mit dem Namen OptionBox (siehe nächstes Listing). Dadurch, dass sie mit der Annotation @Component markiert wurde, erkennt Angular 2 sie als Web-Component. Die Eigenschaft selector von @Component legt über einen CSS-Selector fest, welches HTML-Element die Komponente repräsentieren soll. Der verwendete Wert optionbox führt dazu, dass Elemente mit dem Namen optionbox (<optionbox>…</optionbox>) diese Aufgabe wahrnehmen. Die Eigenschaft bind definiert hingegen, dass das Attribut selected dieses Elements an die gleichnamige Eigenschaft der Klasse zu binden ist. Diese Eigenschaft spiegelt den ausgewählten Wert wieder.

Die Annotation @Template legt fest, wie die Komponente darzustellen ist. Anstatt dazu auf eine Template-Datei zu verweisen, legt sie im betrachteten Fall das Template über die Eigenschaft inline fest. Es besteht lediglich aus einem content-Element, das die Transclusion durchführt. Mehr dazu später.

Die Klasse OptionBox lässt sich mittels @EventEmitter eine Funktion injizieren, mit der sie ein gebundenes Ereignis auslösen kann. Den Name des Attributs, über das der Entwickler das Ereignis bindet, übergibt sie an @EventEmitter. Somit erwartet Angular 2 im gezeigten Fall eine Eigenschaft changed, über die die Anwendung das Event-Binding festlegt. Diese Funktion hinterlegt OptionBox in der Eigenschaft changed. Mit der Eigenschaft options, welche ein Array repräsentiert, verwaltet OptionBox die einzelnen Auswahlmöglichkeiten. Diese werden durch Instanzen der Klasse Item wiedergespiegelt. Über die Methode registerOption nimmt die OptionBox die einzelnen Item-Objekte entgegen.

Mit der Methode select gibt die OptionBox ihrer Umgebung die Möglichkeit, sie auf eine Auswahl hinzuweisen. Dazu nimmt select das gewählte Item entgegen und verstaut ihren Wert (value) in der Eigenschaft select. Dann löst sie das besprochene changed-Ereignis aus und geht sämtliche Optionen durch. Im Zuge dessen setzt sie bei den Items, die diese Optionen repräsentieren, die Eigenschaft selected. Diese drückt aus, ob die jeweilige Option gewählt wurde. Beim übergebenen Item setzt select diese Eigenschaft auf den Wert true; bei allen anderen auf false.

import {Component, Decorator, Template, If} from angular2/angular2;
import {EventEmitter} from angular2/src/core/annotations/events;
import {Item} from item;

@Component({
    selector: optionbox,
    bind: {
        selected: selected
    }
})
@Template({
    inline: <content></content>
})
export class OptionBox {

    options: Array<Item> = [];
    selected;
    changed;

    constructor(@EventEmitter(changed) changed: Function) {
        this.changed = changed;
    }

    registerOption(option: Item) {
        this.options.push(option);
    }

    select(option: Item) {

        this.selected = option.value;

        this.changed();

        for (let o of this.options) {
            o.selected = (option == o);   
        }
    }

}

Komponente für Auswahlmöglichkeiten

Wie im letzten Absatz bereits angemerkt, repräsentiert die Klasse Item in diesem Beispiel eine einzelne Auswahlmöglichkeit. Auch hierbei handelt es sich um eine Web-Component, welche mit @Component annotiert ist. Diese Annotation legt fest, dass Item-Komponenten durch HTML-Elemente mit dem Namen item darzustellen sind (selector), sowie, dass Angular 2 das Attribut value an die gleichnamige Eigenschaft binden soll. Die Annotation @Template teilt Angular 2 mit, dass eine Item-Komponente mit dem Template item.html darzustellen ist, sowie, dass dieses die Direktive If benötigt. Die Eigenschaft selected gibt Auskunft darüber, ob das Item ausgewählt ist und bei optionBox handelt es sich um eine Referenz auf die OptionBox, in der sich das Item befindet. Letztere lässt sich der Konstruktor injizieren. Dazu nutzt er die Annotation @Parent, welche festlegt, dass die gewühlte OptionBox-Komponente das Parent-Element der Item-Komponente im DOM ist. Um nicht das Parent-Element, sondern einen beliebigen Vorgänger zu referenzieren, könnte der Entwickler stattdessen die Direktive @Ancestor heranziehen.

Der Konstruktor registriert das Item bei der gefundenen OptionBox-Komponente durch Aufruf deren registerOption-Methode. Daneben weist Item auch eine Methode select auf. Diese gibt der OptionBox bekannt, dass sie gerade vom Benutzer ausgewählt wurde.

import {Component, Decorator, Template, If, Parent} from angular2/angular2;
import {OptionBox} from optionBox

@Component({
    selector: item,
    bind: {
        value: value   
    }
})
@Template({
  url: item.html,
  directives: [If]
})
export class Item {

    selected = false;
    optionBox: OptionBox;

    constructor(@Parent() optionBox: OptionBox) {
        this.optionBox = optionBox;
        optionBox.registerOption(this);
    }

    select() {
        this.optionBox.select(this);
    }
}

Das Template der Item-Komponente findet sich im nächsten Listing. Da der den Web-Components zugrundeliegende Mechanismus Shadow-DOM die Styles von Komponenten von den Styles der restlichen Anwendung abschottet, definiert das Template ihre eigenen Styles. Hierbei handelt es sich um Styles für Klassen boxOn und boxOff, welche dem Darstellen der ausgewählten bzw. nicht ausgewählten Optionen dienen.

Das div-Element stellt die Option dar. Ihre Bindings [class.boxOn] und [class.boxOff] legen fest, dass Angular 2 in Abhängigkeit der Eigenschaft selected die Klasse boxOn bzw. boxOff zuweisen soll. Das div-Element enthält einen Link, dessen Beschriftung den Wert der Eigenschaft value wiederspiegelt. Darüber hinaus führt die Nutzung von *if dazu, dass das Template ausgewählte Optionen mit einem Stern kennzeichnet. Daneben bindet es über ein Event-Binding das click-Ereignis des Links an die zuvor besprochene Methode select, welche sich in der Item-Komponente befindet.

<style>
    .boxOn { … }
    .boxOff { … }
</style>
<div [class.boxOn]="selected" [class.boxOff]="!selected"><a (click)="select()"> {{value}} <span *if="selected">*</span></a> 
</div>

Auswahlliste verwenden

Zum Testen der OptionBox benötigt man eine weitere Komponente, welche die Web-Anwendung oder zumindest einen Teil davon wiederspiegelt. Die Implementierung dieser Komponente findet sich im nachfolgenden Listing. Sie nennt sich App und weist, wie gewohnt, die Annotationen @Component und @Template auf.

Die Aufgabe dieser Komponente besteht im Verwalten einer Versandart. Dabei handelt es sich um einen String, den sie über die Eigenschaft versandart verfügbar macht. Zum Ändern der Versandart bietet sie eine Methode changed an.

Um Angular 2 anzuzeigen, dass es sich bei dieser Komponente um jene Komponente, welche die Anwendung repräsentiert, handelt, wird sie am Ende an die Funktion bootstrap übergeben.

import {Component, Template} from angular2/angular2;
import {bootstrap} from angular2/angular2;
import {OptionBox} from optionBox;    
import {Item} from item;

@Component({
    selector: app
})
@Template({
  url: app.html,
  directives: [OptionBox, Item]
})
class App {

    versandart = "<nicht gewählt>";

    changed(art) {
        this.versandart = art;
    }

}

bootstrap(App);

Das Template für App findet sich im nächsten Listing. Es bindet das Stylesheet von Twitter Bootstrap ein und weist ein Html-Element optionbox auf. Ein Value-Binding verknüpft die Eigenschaft versandart von App mit dem Attribut selected von OptionBox. Das Attribut #box legt fest, dass die dargestellte OptionBox-Instanz über das Handle box erreichbar sein soll. Dieses Handle kommt beim Event-Binding für das Ereignis changed zum Einsatz. Es legt fest, dass beim Auslösen dieses Ereignisses die Funktion changed der App-Komponente aufzurufen ist. Dabei ist der aktuelle Wert von selected zu übergeben. Dies ist notwendig, da Angular 2 zur Steigerung der Leistung auf das von React bekannte Flux-Muster setzt und somit lediglich One-Way-Binding anbindet. Die Nutzung jener Objekte, die bei der Arbeit mit Formularen eine Art Two-Way-Binding ermöglichen sollen, wird in einem separaten Beitrag beleuchtet.

Innerhalb des optionbox-Elements repräsentieren item-Elemente, hinter denen Item-Komponenten stehen, die einzelnen Auswahlmöglichkeiten. Diese Elemente platziert Angular 2 im Template der OptionBox-Komponente an der Stelle, welche mit dem content-Element markiert wurde. Diesen Vorgang nennt man auch Transclusion. Die Ausgewählte Option zeigt das Template zusätzlich am Ende an.

<style>
    @import url(bower_components/bootstrap/dist/css/bootstrap.css);
</style>

<h2>Direktiven-Sample</h2>

<optionbox #box [selected]="versandart" (changed)="changed(box.selected)">
    <item value="Per Express"></item>
    <item value="Per Einschreiben"></item>
</optionbox>

<div>
    Gewählt: {{ versandart }}
</div>

AngularJS-Anwendung ausführen

Was noch fehlt, ist eine HTML-Seite, welche die gezeigten Komponenten darstellt und EcmaScript 6 simuliert. Diese Seite, welche aus dem Quick-Start-Beispiel von angular.io entnommen wurde, findet sich im nachfolgenden Listing. Wichtig hierbei ist die Nutzung des Elements app, hinter dem die gleichnamige Komponente steht. Der Aufruf von System.import am Ende importiert die nach EcmaScript 5 übersetzte Datei app.js und bringt sie zur Ausführung. Der darin zu findende bootstrap-Aufruf registriert App als Komponente. Durch Nutzung des Modulsystems von EcmaScript 6 werden alle anderen benötigten Skripte geladen und von System nach EcmaScript 5 transpiliert. Hierzu nutzt das Quick-Start-Sample von angular.io den Transpiler Traceur.

<html>
  <head>
    <title>Angular 2 Direktiven-Demo</title>

        <script src="dist/es6-shim.js"></script>
        <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">

    </head>
  <body>

      <div class="container">
        <app></app>
      </div>

    <script>

      // Rewrite the paths to load the files
      System.paths = {
        angular2/*:angular2/*.js, // Angular
        rtts_assert/*: rtts_assert/*.js, //Runtime assertions
        flugService: flugService.js,
        css/bootstrap.css: bower_components/bootstrap/dist/css/bootstrap.css,
        *: *.js
      };

      // Kick off the application
      System.import(app);
    </script>

  </body>
</html>

 

Link zum Beispiel