Angular von 0 auf 100

Avatar von Maximilian Berghoff

Durch die Regeln unseres Kanban-Boards bin ich im aktuellen Projekt an die Aufgabe gelangt, einen Ablauf prototypisch in einer Frontend-Application darzustellen. Es galt, sich nur einmal vorzustellen, ob und wie wir eine komplexe Funktion abbilden können. Der definierte Timeslot lag bei zwei Tagen.

Angular als Framework der Wahl

Da ich schon vor Jahren eine Affinität für AngularJS entwickelt habe und dazu auch noch den Release-Prozess zu Angular 2 mit schmerzlichen Migrationen mitgemacht habe, lag es nahe, auch hier schnell eine Angular-App aufzusetzen. Da Angular 4 inzwischen erschienen ist, welches der Version 2 sehr stark ähnelt, fiel die Wahl darauf. Angular 4 ist eine wirkliche Weiterentwicklung von der Version 2 (3 wurde ausgelassen). Angular 2 war ein kompletter Rewrite von AngularJS.

Gesagt, getan – innerhalb von zwei Tagen entstand eine App, die im folgenden Review einige Personen begeistert hat. Sowohl im Team, als auch beim Kunden war die Begeisterung so groß, dass wir die Applikation jetzt vervollständigen und zugänglich machen sollen.

Damit das ganze Team den Prototypen erweitern kann, habe ich mein Vorgehen aufgeschrieben. Normalerweise kann Wissenstransfer auch andere Wege gehen – Lightning-Talk, Workshop oder einfach nur Vorführen – hier habe ich mich jetzt aber dafür entschieden diese Einführung mit einem größeren Kreis zu teilen. Mit dem Post soll mein Team und jeder andere interessierte Leser in die Grundzüge von Angular eintauchen können.
[smartblock id=6342]

Los geht’s – Angular CLI

Eine Möglichkeit, ein Projekt aufzusetzen – und sicher die komfortabelste – ist es, das Angular-CLI zu benutzen. Obwohl es sich bei Angular um eine Frontend-Applikation handelt, brauchen wir dafür NodeJS und npm, denn mit das CLI haben wir ein mächtiges Werkzeug zum Starten und Bauen (build) von Projekten, zum Generieren von einzelnen Bausteinen und zum Anzeigen bzw. Arbeiten mit der Applikation in der Entwicklung. Das CLI lässt sich einfach durch Package-Manager installieren:

npm install -g @angular/cli

Mit ng new angular-blog-post lasse ich eine Angular-App mit allem, was ich brauche, aufsetzen. Danach folgt:

cd anglar-blog-post
ng serve --open

Und ich sehe ein freundliches „app works“ in meinem Browser.

Struktur

Mit ng new habe ich natürlich auch Einfluss auf die Struktur meiner Applikation und auf den Prozess des Bauens indem ich Argumente an den Funktionsaufruf anhänge. Ohne Argumente beim Erstellen sehe ich in der IDE meiner Wahl folgendes Bild:

Eigentlich ist es nur ein Ordner src/ für die Source-Dateien – also meine App, ein Ordner für die End-To-End-Tests und ein wenig Konfiguration (node_modules/ entsteht durch npm install). Doch trotzdem kann die Menge von Dateien im ersten Moment erschlagend wirken, und dabei ist der app/-Ordner noch nicht einmal geöffnet. Viele der Dateien dienen aber einfach der Konfiguration, denn hier versteckt sich sehr viel Automation. Diese App hat eigentlich schon alles, was man zum Starten braucht. Sowohl das Testing (e2e/, protractor.js, karma.conf.js) als auch das Umwandeln und Linten von TypeScript in Javascript (tsconfig.json, tslint.json) ist bereits integriert und kann direkt über npm-Skripte angesprochen werden:

"scripts": {
  "postinstall": "bower install",
 "ng": "ng",
 "start": "ng serve",
 "build": "ng build",
 "test": "ng test",
 "lint": "ng lint",
 "e2e": "ng e2e"
},

Man kann es aber auch gleich über das Angular-CLI lösen.  Die eigentliche App findet man im Ordner src/app/. Dazu gehört noch ein File src/main.js und zum Verfolgen während der Entwicklung ein src/index.html. In seiner Rohform ist die index.html beinahe gänzlich ohne Inhalt. Relevant ist für uns nur

<app-root>Loading...</app-root>

Was wir hier sehen: es wird irgendwo eine Komponente app-root initialisiert. Wo diese liegt und was überhaupt eine Komponente ist, kommt im Folgenden. Um den kompletten Rest kümmert sich das CLI und Angular selbst. Sowohl die Skripte, die wir für die App benötigen, als auch externe Libraries werden nachträglich durch webpack eingebunden. Dasselbe gilt natürlich auch für Style-Sheets. Das DOM für die eigentliche App manipuliert Angular selbst. Das heißt, hier gibt es weder nicht sichtbare Container noch Einträge im Shadow-DOM. Elemente, die von der App benötigt werden, werden on-the-fly eingehangen.

Die App

So, nun geht es endlich los. Zum Skizzieren der Möglichkeiten mit Angular möchte ich nun ungern die unendliche Liste der ToDo-Apps verlängern.  Darum möchte ich ein Thema aufgreifen, was mich als Speaker zusammen mit dem Marketing derzeit tangiert. Wir Speaker wollen dem Marketing schnell und ohne Redundanzen sichtbar machen, wann wer wo einen Auftritt hat. Darum möchte ich hier eine kleine App skizzieren, die Speaker-Auftritte sammelt und darstellt.

Wie wir eben schon gesehen haben, existiert in der Demo-App bereits schon eine Komponente app-root. Die Definition dieser finden wir in src/app/app.component.ts. In dem Decorator (ein Konstrukt ähnlich einer Annotation) der Komponente kann man erkennen, dass zu dieser auch ein Template gehört. Hier kann man auch schon das sog. Property-Data-Binding erkennen. Die Property title ist sowohl in der Komponente als auch im Template direkt zugänglich. Ändere ich sie in der Komponente, so ändert sie sich direkt im View.
Nun möchte ich den title umformulieren. Das heißt ich setze hier auf:

export class AppComponent {
  title = 'Mayflower Speaker - Konferenzen, UserGroups, Meetups';
}

Und siehe da, meine App aktualisiert automatisch. Im Hintergrund werden Scripte komplett neu gebaut und vom Browser geladen.

Der Anfang

Nun geht es mir ja um Auftritte an sich, darum möchte ich erst einmal ein Interface dafür definieren:

export interface Gig {
    speaker: string;
    event: string;
    type: string;
    date: string;
    comment?: string; // Das Fragezeichen markiert optionale Properties.
    talk: string;
}

Ja wir haben Typen, denn Angular lässt sich am besten mit TypeScript umsetzen – und es macht mir einfach nur Spaß. Typen sind ein Feature, das es in ES2015 neben den Klassen nicht gibt. Eine tiefere Einführung in TypeScript gibt euch Christopher in seinem Blog-Post . Um eine Liste von Speakern zu haben, definiere ich einen Service, der ganz einfach die Liste inMemory hält – es gibt also kein Backend, welches die Liste speichert.

import {Gig} from './gig.value';
import {Injectable} from '@angular/core';

@Injectable()
export class GigService {
    private gigs: Gig[];

    constructor() {
        this.gigs = [];
        this.gigs.push({
            talk: 'Der Content-Manager mag den Baum',
            event: 'IPC Spring',
            date: '30.05.2017',
            type: 'Konferenz',
            speaker: 'Maximilian Berghoff'
        });
    }

    getGigs(): Gig[] {
        return this.gigs;
    }

    addGig(gig: Gig): void {
        this.gigs.push(gig);
    }
}

Ein Service, den ich irgendwo anders verwenden möchte, braucht @Injectable() als Annotation. Damit machen wir den Service zugänglich für die Dependency Injection (DI) von Angular. Nun können wir uns sowohl in einer Komponente als auch in einem anderem Service aus dem DI-Container bedienen. Dazu brauchen wir nur im Konstruktor danach zu verlangen:

import { Component } from '@angular/core';
import {GigService} from './gig.service';
import {Gig} from './gig.value';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [GigService]
})
export class AppComponent {
  title = 'Mayflower Speaker - Konferenzen, UserGroups, Meetups';

  constructor (private gigService: GigService) {}

  get list (): Gig[] {
    return this.gigService.getGigs();
  }
}

Hier hänge ich den Service ebenfalls im providers-Block der Komponente ein, da ich ihn hier und in allen allen Kind-Komponenten als Singelton verfügbar haben will. Diese Art der Definition im Konstruktor erstellt mir nun eine private Property gigService, auf die ich im Getter der Property list zugreifen kann. Im Template wirkt list nun wie eine publicProperty, ist aber private mit einem Getter:

<ul>
    <li *ngFor="let gig of list">{{ gig.talk }}</li>
</ul>

Hier sehen wir nun die Art und Weise, wie wir in Angular eine ForEach-Schleife definieren.

Mehr Komponenten

Nun möchte ich in der Root-Komponente nicht allzuviel unterbringen. Die Darstellung meines einzelnen Gigs möchte ich gerne auslagern. Das heißt, ähnlich wie die app-root hätte ich gerne:

<ul>
    <gig *ngFor="let gig of list" [gig]="gig"></gig>
</ul>

Gut, dann muss ich ich eben nur die Komponente definieren:

import {Component, Input} from '@angular/core';
import {Gig} from './gig.value';

@Component({
    selector: 'gig',
    templateUrl: './gig.component.html'
})
export class GigComponent {
    @Input() gig: Gig;
}

Den eingehenden Wert, welchen ich durch das sogenannte Attribute-Binding [gig] = "gig" übergebe, fange ich per @Input() auf; damit steht mir nun jeweils immer nur einen Gig zur Verfügung und ich kann mich in der Komponente auch nur genau auf einen Gig konzentrieren. Das Template sähe dann so aus:

<div>
    <div>{{ gig.talk }}</div>
    <div>{{ gig.speaker }}</div>
    <div>{{ gig.event }}</div>
    <div>{{ gig.date }}</div>
    <div><button type="button">Edit</button></div>
</div>

Damit die Gig-Komponente in der App verfügbar ist, muss diese dann auch in app.module.ts registriert werden:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';
import {GigComponent} from './gig.component';

@NgModule({
  declarations: [
    AppComponent,
    GigComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Ein neuer Gig

Ein neuer Gig sollte sich jetzt auch hinzufügen lassen. Der Service stellt ja schon eine Methode dafür bereit. Wir müssen nur ein Formular dafür bereitstellen und die Werte dem Service überreichen:

<form>
    <div>
        <div>
            <label for="talk">Talk:</label>
            <input type="text" [(ngModel)]="valueToAdd.talk" id="talk" name="talk"/>
        </div>
        <div>
            <label for="speaker">Speaker:</label>
            <input type="text" [(ngModel)]="valueToAdd.speaker" id="speaker" name="speacker"/>
        </div>
        <div>
            <label for="event">Event:</label>
            <input type="text" [(ngModel)]="valueToAdd.event" id="event" name="event"/>
        </div>
        <div>
            <label for="date">Date:</label>
            <input type="text" [(ngModel)]="valueToAdd.date" id="date" name="date"/>
        </div>
        <div>
            <button type="button" (click)="addGigValue()">Create</button>
            <button type="button" (click)="revertGigValue()">Restet</button>
        </div>
    </div>
</form>

Mit dem Event-Binding (click) = "addGigValue()" wird die angegebene Methode aufgerufen, sobald der Button geklickt wird. Es gibt neben dem (click), ähnlich wie im nativen JavaScript, auch alle anderen Events, auf die ich durch das Angular-Event-Binding reagieren kann.

export class AppComponent {
  title = 'Mayflower Speaker - Konferenzen, UserGroups, Meetups';
  valueToAdd: Gig;

  constructor (private gigService: GigService) {
    this.revertGigValue();
  }

  get list (): Gig[] {
    return this.gigService.getGigs();
  }

  addGigValue() {
    this.gigService.addGig(this.valueToAdd);
    this.revertGigValue();
  }

  revertGigValue () {
    this.valueToAdd = {talk: '', event: '', speaker: '', date: ''};
  }
}

Hier sehen wir die Methoden für die Event-Bindings. Um die Formular-Werte mit [(ngModel)] an eine Property in der Komponente binden zu können, muss zusätzlich das Modul FormsModule aktiviert werden. An der Notation kann man erahnen, dass \[(ngModel)] eine Kombination aus Attribute- und Event-Binding darstellt. Man hätte hier eben auch mit (input) = "addInput($event)" auf das Input-Event reagieren können, um selbst die Property mit neuen Inhalt zu versehen. \[(ngModel)] fasst das für uns zusammen.

Editieren – aber richtig

Eine letzte Aufgabe wäre dann noch das Editieren von Gigs. Dazu bauen wir uns das Template für den Gig dahingehend um, dass wir zwischen der Text-Ansicht und einer Formular-Ansicht hin und her wechseln können.

<div class="row" *ngIf="!editMode">
 ... show values
</div>
<div class="row" *ngIf="editMode">
 ... show form
</div>

Wir müssten jetzt nur noch dafür die geänderten Werte in unsere Liste übertragen. Dazu könnte man jetzt einfach den Service der Gig-Liste auch in die Gig-Komponente einbinden, um den Eintrag direkt auf dem Service zu ändern. Da wir den Service in der äußeren Komponente registriert haben, existiert er als Singelton auch für alle Kind-Komponenten. Doch dafür gibt es einen saubereren Weg – wir verwenden ein Event:

@Output() gigChanged: EventEmitter<Gig> = new EventEmitter<Gig>();
editGig () {
    this.gigChanged.emit(this.gig);
    this.editMode = false;
}

Das heißt: Anstelle selbst die Werte zu ändern, hoffen wir darauf, dass sich jemand auf das Event registriert und es tut.

<app-root (gigChanged)="onGigChange()">Loading...</app-root>

Dieser „jemand“ sind natürlich wir selbst. Denn damit registriert sich die AppComponent auf das neu geschaffene Event und reicht die Weiterverarbeitung dann einfach nur an den Service weiter:

onGigChange (gig: Gig) {
  this.gigService.saveGig(gig);
}

Auch wenn der prozeduale Flow im ersten Augenblick einen schneller durchblicken lässt, entkoppelt man hier doch ungemein die Komponenten. Theoretisch könnte man hier jetzt andere Prozesse auf genau dieses Event reagieren lassen (z.B. einen Tweet absenden). Dazu muss die GigComponent nichts von dem GigService wissen.

Conclusion

Die fertige Angular-Applikation
Für eine bessere Ansicht habe ich das HTML noch um ein paar Bootstrap-Klassen ergänzt.

Ich denke das war jetzt ein schneller, kurzer Ritt durch die Angular-Welt. Wir konnten Themen wie Komponenten, Services, Templates und Events eigentlich nur anreißen, doch mit dem Code sollte man hoffentlich schnell in ein Angular-Projekt einsteigen können. Für alles Weitere hat Angular natürlich eine hervorragende Dokumentation. Denn die Reise kann hier noch weiter gehen. Themen, die ich hier nicht ansprechen konnte sind: Routing, HTTP und Asyncrones im Allgemeinen und Authentication.

Avatar von Maximilian Berghoff

Kommentare

3 Antworten zu „Angular von 0 auf 100“

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert


Für das Handling unseres Newsletters nutzen wir den Dienst HubSpot. Mehr Informationen, insbesondere auch zu Deinem Widerrufsrecht, kannst Du jederzeit unserer Datenschutzerklärung entnehmen.