Einleitung

Moin moin und hallo,

und willkommen zu meinem Angular 2 JS E-Book / Kurs.

In den nächsten Wochen und Monaten möchte ich an dieser Stelle Angular 2 in all seinen Facetten vorstellen.

Dabei setzt das Material kein großes Vorwissen voraus. Es reicht wenn man die Grundlagen von JavaScript verstanden hat.

Nichtsdestotrotz sollte man alle Kapitel der Reihe nach lesen, da sie auf einander aufbauen werden.

Es werden außerdem hier echte und wiederverwendbare Komponenten entwickelt, die ich allen Lesern unbegrenzt zur Verfügung stelle.

Die meisten Beispiele werden für alle frei verfügbar sein, um aber in Zukunft alle Kursmaterialien auf dem neusten Stand zu halten, werde ich den Quellcode, für die komplexeren Komponenten, gegen eine kleine Gebühr zur Verfügung stellen.

Eine Besonderheit in dem folgenden Material wird sein, dass alle Beispiele in JavaScript vorliegen.

Warum das so ist, liegt darin bergründet das ich persönlich zwar nichts gegen TypeScript habe, aber im Kontext von Angular, als Werkzeug für wiederverwendbare Web-Kompomenten, den Transpiler von TypeScript als eine Abhägigkeit und Komplexitätstreiber, ohne langfristigen Mehrwert, empfinde.

Einzig die Import-Funktion erscheint derzeit als durchaus fortgeschritten, wird aber durch die kommende Adaption von ES6 ebenfalls obsolet werden.

Da die Adaption aber noch lange nicht in Browsern stattgefunden hat, werden die kommenden Beispiele alle noch in ES5 entwickelt.

Mit der Zeit und den notwendigen Mitteln, werde ich aber auch eine ES6 Version der Beispiele nach liefern.

Vorab sollte aber klar sein, dass Angular 2 und Angular 1 nicht sofort kompatibel sind.

Eine alte Angular 1 App wird aber mit ein wenig Aufwand zu einer Angular 2 App leichter migriert werden können, als z.B.in Ember JS.

***Hinweis***

Dieser Kurs befindet sich noch im Aufbau und wird in regelmäßien Abständen (max. alle 10 Tage) um neue Inhalte erweitert.


Motivation

Warum solltest du ausgerechnet Angular 2 lernen?

Bereits Angular 1 hat schon sehr große Wellen bei so ziemlich allen Frontend Entwicklern geschlagen.

Dinge, die früher mit einem jQuery-Stack (Backbone, Marionette), mit etwas Aufwand umgesetzt wurden, konnte man auf einmal mit wenigen Zeilen in Angular erledigen.

Die Tehcnologie war vielleicht nicht im Sinne “disruptiv”, aber durch Funktionen wie bideriktionales Databinding, Dependency Injection und Direktiven hat es bei vielen für ein Umdenken in Bezug auf Frontend-Engineering gesorgt.

Es hat sogar ein kleines eigenes Ökosystem geschaffen, in dem Bibliothken und Widgets, die vorher nur in jQuery vorlagen, mit Wraper und eigenen Schnittstellen ausgestattet wurden, damit man ja nahtlos mit Ihnen in Angular arbeiten konnte.

Ob es Angular 2 gelingt ihren Vorgänger noch zu übertreffen?

Die Chancen stehen nicht schlecht, denn gerade die neue Kernänderung, nämlich Komponenten, ist etwas das viele Entwickler bereits von Angular 1, in die Arme von React JS, das von Facebook entwickelt wird, getrieben.

Man könnte jetzt hingehen und kezertisch behaupten, dass Angular 1 doch bereits ähnliche Funktionalitäten mit Hilfe der Direktiven ermöglicht hat.

Das wäre grundsätzlich auch nicht verkehrt, aber das Neue ist eben auch, die Abwendung vom klassichen MVC-Pattern und der Fokus auf nur die Funktionalität einer in sich geschlossenen Komponente.

Warum aber gerade Komponenten?

Die Architektur dieser Komponten erinnert stark an viele Konzepte von Java, wie z.B. Observer-Pattern.

Diese haben sich in der Enterprise-Welt über viele Jahre sehr gut bewährt und finden mit Angular 2 nun endlich Einzug in die Browser-Welt.

Selbst wenn Angular 2 nichts für dich sein sollte, solltest du in jedem Fall irgendein anderes (z.B. React JS, Marionette JS, Ember, Knockout, usw.) Frontend-Framework erlernen.

Die Ansprüche der Nutzer an die Bedienbarkeit, Verhalten und Reaktionszeiten eienr Web-Applikation haben sich stark verändert und können mit dem alten Request-Response-Konzept nicht mehr erfüllt werden.

Jeder seriöse Fullstack Entwickler sollte sich deshalb ein solches Framework ein seinen Werkzeugkasten packen.

Was außerdem für Angular 2 spricht, ist vor allem natürlich der große Erfolg seines Vorgängers, der vielen Entwickler die Arbeit sehr erleichtert hat. Und viele dieser Entwickler sind bereits dabei Ihren Werkzeugkasten um das Upgrade auf Version 2 zu erweitern.

Mit dem Erlernen von Angular 2 tritts du also einer bereits jetzt großen, hilfsbereiten und sehr aktiven Community bei.


Schnellstart

Um möglichst schnell und bequem in ein neuen Angular 2 Projekt zu starten, habe ich auf Basis des Quickstart-Guides der offiziellen Dokumentation, ein fertiges Packet geschnürrt.

Fast alle Komponenten und Projekte werden auf dieser Vorlage aufgebaut werden.

In diesem Packet sind alle Abhängigkeiten für die Laufzeit enthalten, die es einem ermöglichen Codeänderungen unmittelbar im Browser (per Push) an zu sehen.

Wie man die Vorlage, nach dem Download und entpacken zum Laufen bringt und welche Bestandteile enthalten sind, erkläre ich in meinem Angular 2 Quickstart Video unten (auf das Bild klicken):

Die Vorlage könnt ihr entweder selbst zusammen bauen (die Anleitung auf der offiziellen Seite ist zwar lang aber vollständig) oder ihr könnt euch das Packet für alle eure zukünftigen Projekte hier kaufen:


Aufbau einer Angular 2 App

Wenn Sie sich die Quickstart-Vorlage herunter geladen und zum Laufen gebracht haben, wirst du dich vielleicht Fragen “Wie funktioniert denn diese App nun?”.

Schauen wir uns doch mal alle Projekt relevanten Dateien an:

├── app
│   ├── main.js
│   ├── app.module.js
│   ├── app.component.js*
│
├── index.html

Wenn man einen Blick in das **/app/** Verzeichnis wagt, findet man drei Dateien vor:

1. /app/main.js

(function (app) {

    //warte bis der DOM geladen wurde
    document.addEventListener('DOMContentLoaded', function () {

        //sobald der DOM geladen ist, starte angular für den Browser
        ng.platformBrowserDynamic
            .platformBrowserDynamic()
            .bootstrapModule(app.AppModule); // starte unser Modul

    });
})
(window.app || (window.app = {})); //initialisere unter App-Objekt, falls noch keines global verfügbar ist

Dieses Code Fragment wird in der Regel erst nachdem unser App und alle anderen Abhängigkeiten geladen worden sind ausgeführt.

Es sorgt dafür dass zunächst Angular initalisiert wird:

 ng.platformBrowserDynamic.platformBrowserDynamic()

Weiter startet es dann unsere App:

.bootstrapModule(app.AppModule);

Man beachte das unser Hauptpaket in diesem Fall window.app heißt. Das kann im Einzelfall natürlich variieren, je nachdem wie du es benennen möchtest.

2. /app/app.module.js

(function (app) {

    //Definiere das App Modul
    app.AppModule = ng.core.NgModule({ 

        //wir wollen Angular im Browser nutzen, also importieren wir hier das entsprechende Modul
        imports: [ng.platformBrowser.BrowserModule], 

        //wir importieren außerdem die **AppComponent**, die wir im folgenden in Punkt 3. definieren
        declarations: [app.AppComponent], 

        //nachdem unser Modul gestartet wurde, soll die importierte **AppComponenten** starten
        bootstrap: [app.AppComponent] 
    })

    //es wird mit Hilfe von Angular eine Klasse definiert und eine neue Instanz von **app.AppModule** erzeugt
    .Class({ 
        constructor: function () {}
    });

})(window.app || (window.app = {})); //initialisere unter App-Objekt, falls noch keines global verfügbar ist

Bitte lasse dich nicht von unbekannten Anweisungen oder Abhängigkeiten abschrecken. Das was wir sehen ist nun leider das minimale Setup in Angular 2. Alle beschriebenen Imports sind zwingend notwendig um die App zum Laufen zu bekommen.

Alle Module müssen mit Hilfe von NgModule erzeugt werden:


    app.AppModule = ng.core.NgModule().Class()

3. /app/app.component.js

(function (app) {
    //Definiere die Komponnte und dem Namen **AppComponent**
    app.AppComponent = ng.core.Component({ 

         //wie heißt der Tag / die Direktive unter der wir unsere Komponente zeichnen möchten ?
        selector: 'hallo-app', 

        //wie sieht unser Inhalt / Template aus, den wir darstellen möchten?
        template: '<h1>Hallo Welt!</h1>'  
    })

    //es wird mit Hilfe von Angular eine Klasse definiert und eine neue Instanz von **app.AppComponent** erzeugt
    .Class({  
        constructor: function () {}
    });

})(window.app || (window.app = {})); //initialisere unter App-Objekt, falls noch keines global verfügbar ist

Nachdem wir den Rahmen aufgesetzt haben, ist das endlich die aller erste Komponente, die wir mit Hilfe von Angular 2 erzeugen.

Alle Komponenten müssen mit Hilfe von Component erzeugt werden:


    app.AppComponent = ng.core.Component().Class()

Was ist ein Modul?

Ein Modul kann man sich vorstellen wie ein übergordnete Sammlung von verschiedenen, häufig wieder verwendbaren Komponenten und Funktionen. Ein Modul aggregiert also eine oder mehrere Komponenten.

Wenn man eine Komponte verwende möchte muss man in der Regel erst dazu gehörige übergeordnete Modul importieren.

Was ist eine Komponente?

Eine Komponente ist ein für einen speziellen Anwendungsfall, in sich geschlossene Klasse.

Eine Komponte kann aber wiederrum andere Komponenten beinhalten, bzw. indirekt durch die Verwendung bestimmter Direktiven auslösen.

Auch wenn eine Komponente andere beherbergt kümmert sich diese nur um sich und Ihren Kontext.

Andere Komponten, die parallel dazu liegen, sollten in keinem Fall direkt mit ihr kommunizieren bzw. Daten austauschen.

Denn das sorgt für eine zu starke Kopplung unter den Geschwistern, und das möchte man tunlichst vermeiden, denn Komponten sollen auch möglichst einfach autauschbar sein, was nur funktioniert wenn man kaum bzw. Keine Kopplung zwischen Komponenten hat.

Was das alles zu bedeuten hat und wie man das bewerkstelligt, werde wir noch in den folgenden Kapiteln sehen.

***ACHTUNG***

Am einfachsten man stellt sich eine Tabellen Komponente vor:

  • Die Tabelle sorgt sich nur darum dass die Tabelle und alle Zeilen erfolgreich auf gebaut werden
  • Jede Zeile ist wiederum selbst eine Komponente und sorgt dafür das die Zeile und die Daten erfolgreich dargestellt werden
  • Die Komponente, die parallel zur Tabelle existiert, sollte in keinem Fall von der direkt Tabelle angesprochen
Hier das Beispiel in HTML-Form:
    <div>
        <h2>Meine Tabellen-Komponente</h2>
        <lade-tabelle-button></lade-tabelle-button>
        <meine-tabelle>
            <meine-zeile></meine-zeile>
            <meine-zeile></meine-zeile>
            <meine-zeile></meine-zeile>
        </meine-tabelle>
    </div>

Komponenten schachteln

Als nächstes möchte ich zeigen wie man Komponenten in anderen Komponenten wiederverwenden kann, aka. in einander schachtelt.

Dazu nutzen wir ein einfaches Formular, dass wir in unserer einbetten möchten.

Dazu legen wir ein einfaches Registrierungs-Formular unter folgendem Pfad an:

├── app
│   ├── views
│   │   ├── register-form.html

Das Formular selbst ist recht einfach gestrickt:

<form autocomplete="off">
    <div class="form-group">
       <label>Name:</label>
        <input name="name" type="text" placeholder="Deine Name">
    </div>
    <div class="form-group">
       <label>Email:</label>
        <input name="email" type="email" placeholder="Deine Email">
    </div>
    <div class="form-group">
       <label>Geburtsdatum:</label>
        <input name="birthdate" type="date" placeholder="Dein Geburtsdatum">
    </div>
    <div class="form-group">
       <label>Passwort:</label>
        <input name="password" type="password" placeholder="Dein Passwort">
    </div>
    <button type="submit">Registrieren</button>
</form>

Nun da wir unser Formular-Template parat haben, brauchen wir noch eine neue Komponente.

Für die Arbeit mit Formularen benötigt Angular 2 jedoch noch das FormularModule, dass wir erst in unsere index.html einpflegen:

    <script src="node_modules/@angular/forms/bundles/forms.umd.min.js"></script>

Anschließend importieren wir das Modul in unserem eigenen app/app.module.js:

 app.AppModule = ng.core.NgModule({
        imports: [
            ng.platformBrowser.BrowserModule, 
            ng.forms.FormsModule //Notwendiger, neuer Import
            ],
        declarations: [
            app.AppComponent,
            app.FormComponent  //Notwendige Deklaration der neuen Komponente
        ],
        bootstrap: [app.AppComponent]
    })

Jetzt können wir endlich die neue FormComponent definieren.

app/form.component.js

(function (app) {
    app.FormComponent = ng.core.Component({
        selector: 'registerForm',
        templateUrl: 'app/views/register-form.html'
    })

    .Class({

        constructor: function () {

        }
    });

})(window.app || (window.app = {}));

Wie man sehen kann ist das schon etwas dass ein Konstrukt, das wir schon aus den Schnellstart kennen.

Diese neue Komponente hat den registerForm-Selektor erhalten.

Was ist ein Selektor?

Oberflächlich beschrieben, ist es ein Marker irgendwo im DOM, den die Komponente ein Einstiegspunkt erkennt, und dann weiß dass sie sich an genau dieser Stelle in den Dom einhaken muss.

Wie in der Einleitung kurz erwähnt, ist dieses Konzept recht ähnlich zu den Direktiven in Angular 1.

Nur die Verarbeitung intern erfolgt etwas anders.

Um diese Komponente an irgendeiner beliebigen Stelle im HTML ein zu setzen, müssen wir lediglich den Tag eintragen.

Wie angekündigt möchte ich eine Komponente in einer anderen Komponente verwenden, und das ist jetzt denkbar einfach.

Wir erweitern dazu das template von AppComponent:

(function (app) {
    app.AppComponent = ng.core.Component({
        selector: 'hallo-app',
        template: '<h1>Registerierung!</h1><registerForm></registerForm>'
    })

    .Class({
        constructor: function () {}
    });

})(window.app || (window.app = {}));

Wie das ganze nun in echt aussieht zeige ich in folgendem Video:

Den vollständigen Projektcode gibt es hier käuflich, zum Download zu erwerben, inklusive Databinding, das erst im nächsten Kapitel folgt:


Databinding mit NgModel

Unser Formular und die dazugehörige Komponente sind nun auf gesetzt, aber leblos.

Wir können beide aber bidirektional verbinden und etwas mehr Leben in die Geschichte bringen.

Der Weg aus der Komponente in die Oberfläche fängt zunächst mit der Definition eines neuen Models an:

Das Formular selbst ist recht einfach gestrickt:

(function (app) {
    app.FormComponent = ng.core.Component({
        selector: 'registerForm',
        templateUrl: 'app/views/register-form.html'
    })

    .Class({

        constructor: function () {

            this.registerForm = {
                name: "Mein Name",
                email: "meine@email.de",
                birth: "2016-01-01",
                password: ""
            }      
        }

    });

})(window.app || (window.app = {}));

Wie man sehen kann habe ich für das Formular ein Model registerForm definiert.

Dieses Model muss jetzt nur noch an die View (HTML) angebunden werden.

Dazu gibt es in Angular 2 eine eigene Notation:

    [(ngModel)]="meineModelEigenschaft"

Man muss also dem jeweiligen Eingabefeld ein neues Attribut [(ngModel)] definieren und diesem dann den Wert, den man anbinden möchte, zuweisen.

Eingesetzt in unser Registrierungsformular, sieht das dann vollständig wie folgt aus:

  <form autocomplete="off">
    <div class="form-group">
       <label>Name:</label>
        <input [(ngModel)]="registerForm.name" name="name" type="text" placeholder="Dein Name">
    </div>
    <div class="form-group">
       <label>Email:</label>
        <input [(ngModel)]="registerForm.email" name="email" type="email" placeholder="Deine Email">
    </div>
    <div class="form-group">
       <label>Geburtsdatum:</label>
        <input [(ngModel)]="registerForm.birth" name="birthdate" type="date" placeholder="Dein Geburtsdatum">
    </div>
    <div class="form-group">
       <label>Passwort:</label>
        <input [(ngModel)]="registerForm.password" name="password" type="password" placeholder="Dein Passwort">
    </div>
    <button type="submit">Registrieren</button>
</form>

Damit haben wir tatsächlich bereits die Daten nun aus der Komponente mit der Oberfläche verknüpft.


Wie wissen wir in Angular 2 wann ein Formular abgeschickt wird?

Das Anbindung von Element aus der Komponente ist nicht nur auf Modelle reduziert. Wir können auch Funktionen anbinden.

Um das “Submit”-Eriegnis ab zu fangen gibt es in Angular eine weitere Notation:

    (submit)="meineKomponentFunktion($event)"

Diese Notation gilt natürlich nicht nur für das Abschicken von Formularen sondern für alle möglichen Ergeignisse (Click, Hover, etc.).

Unser vollständiges Formular sieht nun also wie folgt aus:

<form (submit)="doRegister($event)" autocomplete="off">
    <div class="form-group">
       <label>Name:</label>
        <input [(ngModel)]="registerForm.name" name="name" type="text" placeholder="Dein Name">
    </div>
    <div class="form-group">
       <label>Email:</label>
        <input [(ngModel)]="registerForm.email" name="email" type="email" placeholder="Deine Email">
    </div>
    <div class="form-group">
       <label>Geburtsdatum:</label>
        <input [(ngModel)]="registerForm.birth" name="birthdate" type="date" placeholder="Dein Geburtsdatum">
    </div>
    <div class="form-group">
       <label>Passwort:</label>
        <input [(ngModel)]="registerForm.password" name="password" type="password" placeholder="Dein Passwort">
    </div>
    <button type="submit">Registrieren</button>
</form>

Auf der Gegenseite fehlt jetzt aber noch die entsprechende Funktion doRegister(), die nach dem Auslösen weitere Aktionen auslöst:

(function (app) {
    app.FormComponent = ng.core.Component({
        selector: 'registerForm',
        templateUrl: 'app/views/register-form.html'
    })

    .Class({

        constructor: function () {
            this.registerForm = {
                name: "Mein Name",
                email: "meine@email.de",
                birth: "2016-01-01",
                password: ""
            }      
        },

        doRegister: function (event) {
            event.preventDefault()

            console.log(this.registerForm)
        }

    });

})(window.app || (window.app = {}));

Das event wird gefangen und von der Weiterverarbeitung abgehalten und wir geben den aktuellen Status unseres registerForm aus.

Dazu gibt es auch ein kleines Video-Tutorial von mir:


Klassen, Services, Dependency Injection

Wir haben bisher gesehen, wie man Modelle in einzelnen, in sich geschlossenen Komponenten mit der Oberfäche verbindet.

Mit dem bisherigen Wissen könnte man auch schon einige einfache Komponenten und Widgets bauen.

Wie sieht es aber aus, wenn die Anwendung wächst, die Geschäftslogik komplexer wird und man plötzlich Daten zwischen Komponenten austauschen muss.

Dafür gibt es zwei Möglichkeiten:

  1. EventEmitter
    • Daten aktiv erfragen und auf Antwort reagieren
  2. Service Singleton
    • Eine einzelne Klassen-Instanz kümmert sich um dei Verwaltung der Daten und ermöglicht direkten Zugriff, sodass alle Komponeten auf dem selben Datensatz, da Singleton, arbeiten

In der Praxis hat sich gezeigt, dass man mit EventEmitter sehr gut Komponenten entkoppeln kann und nach dem PubSub-Prinzip trotzdem zusammen arbeiten lassen kann.

Für den Datenautausch jedoch hat sich der Service Singleton als “Best Practise” herausgestellt, weshalb wir diesen Ansatz im folgenden näher beleuchten möchten.

Klassen in JavaScript?

Bevor in die Singletons rein springen muss man verinnerlichen, dass Angular 2 verstärkt in Richtung Objekt-Orientierte Entwicklung sich bewegt.

Wer schonmal ernsthaft versucht hat Ojekt-Orientiert in JavaScript zu entwickeln, weiß wie merkwürdig sich allein die Definitionen von Klassen anfühlt.

Angular 2 liefert hier unter anderem deshalb auch einen eignen Ansatz wie man Klassen implementieren soll, nämlich per ng.core.Class Objekt.

Deklarieren wir mal zum Beispiel einen RegistrationService, der sich um die Verwaltung einer Benutzerliste kümmert:

(function (app) {

    app.RegistrationService = ng.core.Class({
        constructor: function () {
            this.userList = []
        },

        addUser: function (user) {

            this.userList.push(user)
        },

        getUserList: function () {
            return this.userList
        }
    })

})(window.app || (window.app = {}));

Wie in anderen Programmiersprachen auch, gibt es hier einen contructor() der bei der Erzeugung einer neuen Instanz auf gerufen wird.

Die anderen Methoden wurden frei definiert, wichtig ist hier nur, dass der Aufruf ng.core.Class() eine neue Klasse zurück gibt, von der man jetzt per new-Operator neue Instanzen erzeugen kann.

In Angular 2 wird jede Komponente, jeder Service und jedes Model mit Hilfe dieser Methode erzeugt.


Service Singletons

***Was sind Singletons?***
*Zitat Wikipedia:*

Das Singleton (selten auch Einzelstück genannt) ist ein in der Softwareentwicklung eingesetztes Entwurfsmuster und gehört zur Kategorie der Erzeugungsmuster (engl. creational patterns). Es stellt sicher, dass von einer Klasse genau ein Objekt existiert.[1] Dieses Singleton ist darüber hinaus üblicherweise global verfügbar. Das Muster ist eines der von der sogenannten Viererbande (GoF) publizierten Muster. Mehr Details auf Wikipedia

Kommen wir also endlich zu den angekündigten Service Sigletons. Im Grunde haben wir oben einen solchen schon deklariert, allerdings muss man jetzt dem App-Module noch mitteilen, dass wir den RegistrationService auf diese weise nutzen möchten.

Dafür erweitern wir unsere App-Module-Definition um den Parameter providers:

(function (app) {

    app.AppModule = ng.core.NgModule({
        imports: [ng.platformBrowser.BrowserModule, ng.forms.FormsModule],
        declarations: [app.AppComponent, app.FormComponent, app.RegistrationListComponent],
        providers: [app.RegistrationService],
        bootstrap: [app.AppComponent]
    })

    .Class({
        constructor: function () {}
    });

})(window.app || (window.app = {}));

Damit sind wir fast fertig, denn jetzt müssen wir nur noch den Service in allen Komponenten injezieren lassen.

***Was ist Dependency Injection?***
*Zitat Wikipedia:*

Als Dependency Injection (englisch dependency ‚Abhängigkeit‘ und injection ‚Injektion‘; Abkürzung DI) wird in der objektorientierten Programmierung ein Entwurfsmuster bezeichnet, welches die Abhängigkeiten eines Objekts zur Laufzeit reglementiert: Benötigt ein Objekt beispielsweise bei seiner Initialisierung ein anderes Objekt, ist diese Abhängigkeit an einem zentralen Ort hinterlegt – es wird also nicht vom initialisierten Objekt selbst erzeugt. Mehr Details auf Wikipedia

(function (app) {
    app.FormComponent = ng.core.Component({
        selector: 'registerForm',
        templateUrl: 'app/views/register-form.html'
    })

    .Class({

        constructor: [app.RegistrationService, function (registrationService) {

            this.registrationService = registrationService;

            this.userModel = {
                name: "",
                email: "",
                birthdate: null,
                password: ""
            }

        }],


        doRegister: function (event) {

            console.log(this.userModel)

            event.preventDefault()

            this.registrationService.addUser(this.userModel)
        }

    });

})(window.app || (window.app = {}));

Wie man sehen kann hat sich die Definition vom constructor von einer Funktion zu einem Array verändert. Hier finden die Injektion statt. Alle Klassen, die man in der Komponente verwenden möchte, muss man an den Anfang des Array stellen und als abschließender Eintrag folgt immer eine Funktion, die mit der Anzahl der Paramter des Array über einstimmen muss. Diese Funktion, ist der neue Kunstruktoraufruf, der mit Instanzen zu den vorangegangenen Klassen befüllt wird.

Schaut man sich also den Konstruktor oben nochmal an, liest sich das wie folgt:

  • Erzeuge eine neue Instanz der app.FormComponent, die mit einer Instanz vom app.RegistrationService initialisiert wird.

Da wir den Provider für den Service im App-Module deklariert haben, wird auch nur dort eine neue Instanz des +RegistrationService* erzeugt und ansonten nur an alle, die es erfordern, als Referenz, verteilt.

Bauen wir doch mal zur Überprüfung eine weitere Komponente, die die Liste aller Benutzer ausgibt. Die Liste selbst wird in der doregister()-Methode vom app.FormComponent befüllt.

(function (app) {
    app.RegistrationListComponent = ng.core.Component({
        selector: 'registrationList',
        templateUrl: 'app/views/registration-list.html'
    })

    .Class({

        constructor: [app.RegistrationService, function (registrationService) {

            this.registrationService = registrationService;
        }]
    });

})(window.app || (window.app = {}));

Die View zu diesere Komponente sieht dabei wie folgt aus:

<ul>
    <li *ngFor="let user of registrationService.getUserList()">{{user.name}}</li>

</ul>

Wenn man den Service also als Eigenschaft der Komponente deklariert, kann man auf diesen auch direkt in der View zugreifen, das was man früher, in Angular 1, im $scope deklariert hat.

Das Ganze kann man sich auch hier in Videoform anschauen:


Warenkorb App

Wir haben nun genug Hintergrundwissen gesammelt um die erste, nicht triviale Web-App zu konstruieren, nämlich einen Shop mit einem Warenkorb.

Für den Shop verwenden wir jedoch zunächst nur Mock-Daten und der Bestellprozess wird noch im Browser beendet, da wir uns HTTP und REST erst in dem nächsten Kapitel anschauen werden.

Nun aber ans Eingemachte, wir benötigen folgende Komponenten:

  1. Shop
    • Das ist unsere übergeordnete Komponente, die alle anderen bündelt
  2. Artikelliste
    • Hier werden die Daten für die Shop-Liste zusammengetragen und präsentiert
  3. Warenkorb
    • Der Warenkorb soll sich automatisch füllen, wenn wir einen Artikel hinein legen

Um mit den Daten zu arbeiten brauchen wir also foglende Singleton Services:

  1. Artikel-Service
    • Dieser soll unsere Mock-Daten konstruieren und liefern
  2. Warenkorb-Service
    • Unsere Warenkorb-Daten werden hier zusammen gehalten und zwischen den Komponenten ausgetauscht

Wenn man sich so ein Shop-System nochmal vor augen führt benötigen wir mindestens folgende Daten-Modelle:

  1. Artikel
    • Enthält Namen, Beschreibung und Preis
  2. Position
    • Diese enspricht einem Eintrag im Warenkorb. Hier wird einmal auf einen konkreten Artikel verwiesen als auch die Menge festgehalten

Model Klassen

Die einfachste Kerneinheit, unserer App, stellen die Modelle dar.

Schauen wir uns zunächst die Artikel an:

(function (app) {

    app.Article = ng.core.Class({
        constructor: function (id, name, description, price) {
            this.id = id
            this.name = name
            this.description = description
            this.price = price
        }
    })

})(window.app || (window.app = {}));

Um einen Aritkel eindeutig wieder zu finden habe ich noch ein Attribut “Id” eingeführt.

Ansonsten ist die Klasse recht schlicht.

Da wir nun eine Instanz eines Artikels bilden können schauen wir uns darauf aufbauend eine Position (Item) an:

(function (app) {

    app.Item = ng.core.Class({
        constructor: function (article, amount) {
            this.article = article
            this.amount = amount || 1
        },


        increaseAmount: function () {
            this.amount += 1;
        },

        decreaseAmount: function () {
            this.amount -= 1;
        },

        getTotal: function () {
            return this.article.price * this.amount
        }
    })

})(window.app || (window.app = {}));

Eine Position (Item) hält also nicht nur den Verweis auf einen Artikel und die Menge, sondern ermöglicht es uns jetzt auch die Mengen dynamsich zu steuern, als auch eine Gesamtsumme zu ermitteln.


Services

Bei den Diensten ist es wie bei den Modellen, man sollte bei der einfachsten Einheit starten und zu den komplexeren übergehen. In diesem Fall erneut von artikel zu Position.

ArticleService

(function (app) {

    app.ArticleService = ng.core.Class({
        constructor: function () {
            this.articleList = []

            for (var i = 1; i < 7; i++) {
                this.addArticle(i, "Artikel " + i, "Beschreiung " + i, i)
            }

            console.log(this.articleList)
        },

        getArticleList: function () {
            return this.articleList
        },

        addArticle: function (id, title, desc, price) {
            this.articleList.push(new app.Article(id, title, desc, price));
        }
    })

})(window.app || (window.app = {}));

Wie man sehen kann wird für die Artikel-Klasse kein Injektor verwendet, da wir ja jedes Mal einen neuen Artikel erzeugen möchten und uns nicht eine einzige Instanz über alle Komponenten teilen.

Dafür reicht es den Code der Artikel-Klasse nur zu laden und kann direkt per new-Operator neue Instanzen erzeugen. Der Konstruktor entspricht dabei unserer vorherigen Deifnition, sprich die Parameter werden der Reihe nach auf die constructor-Methode abgebildet.

CartService

(function (app) {

    app.CartService = ng.core.Class({
        constructor: function () {
            this.itemStore = {}
        },

        addItem: function (article) {

            if (!this.itemStore[article.id]) {
                this.itemStore[article.id] = new app.Item(article, 1)
            } else {
                this.itemStore[article.id].increaseAmount()
            }
        },

        getItemStore: function () {
            return this.itemStore
        },

        getItemList: function () {
            let result = []
            let self = this

            result = Object.keys(this.itemStore).map(function (key) {
                return self.itemStore[key]
            });

            return result
        },

        getTotal: function () {

            let sum = 0

            this.getItemList().forEach(function (item) {
                sum += item.getTotal()
            })

            return sum
        }
    })

})(window.app || (window.app = {}));

Der Warenkorb-Service hat nur eine Besonderheit, die man vielleicht nicht sofort vermuten würde; Die Positionen werden nicht in einer Liste sondern in einem Objekt (Map) gespeichert.
Um den Zugriff auf Positionen im Warenkorb möglichst effizient zu gestalten und feststellen zu können ob z.B. schon bestimmte Artikel enthalten sind, wird je Artikel-ID eine Position (Item) erzeugt und in unserem "ItemStore" geparkt.
Für die Ausgabe im HTML benötigen wir aber am besten eine Liste, um einfacher darüber zu iterieren. Daher wurde eine **getItemList**-Methode eingeführt.
Diese Methode beinhaltet einen kleinen Trick, wie man aus einem Objekt eine Liste (Array) von Eigenschaften erzeugen kann, nämlich mit Hilfe der ***Object.keys** und der ***map** Funktion.
Natürlich muss unser Service auch den gesammten wert des Warenkorb feststellen können, was in der ***getTotal***-Methode passiert.

Komponenten

Dein schwierigsten Teil haben wir eigentlich damit hinter uns gelassen. Die Komponeten müssen jetzt nur die Services nur noch sinnvol mit der Oberfläche zusammen bringen.

ShopComponent

(function (app) {
    app.ShopComponent = ng.core.Component({
        selector: 'shop',
        templateUrl: 'app/views/shop.html'
    })

    .Class({
        constructor: function () {}
    });

})(window.app || (window.app = {}));

ArticleListComponent

(function (app) {
    app.ArticleListComponent = ng.core.Component({
        selector: 'articleList',
        templateUrl: 'app/views/article-list.html'
    })

    .Class({

        constructor: [app.ArticleService, app.CartService, function (articleService, cartService) {

            this.articleService = articleService
            this.cartService = cartService
        }],


        addArticle: function (event, article) {

            event.preventDefault()

            this.cartService.addItem(article)
        }

    });

})(window.app || (window.app = {}));

CartComponent

(function (app) {
    app.CartComponent = ng.core.Component({
        selector: 'shoppingCart',
        templateUrl: 'app/views/cart.html'
    })

    .Class({

        constructor: [app.CartService, function (cartService) {

            this.cartService = cartService;
        }]
    });

})(window.app || (window.app = {}));

App Module

Die Module-Definition beinhaltet keine unerwarteten Neuerungen:

(function (app) {

    app.ShopModule = ng.core.NgModule({
        imports: [ng.platformBrowser.BrowserModule],
        declarations: [app.ShopComponent, app.CartComponent, app.ArticleListComponent],
        providers: [app.ArticleService, app.CartService],
        bootstrap: [app.ShopComponent]
    })

    .Class({
        constructor: function () {}
    });

})(window.app || (window.app = {}));

Views

Zu guter Letzt müssen wir natürlich noch die Oberfläche gestalten.

Diese habe ich schnörkellos gehalten, da es uns ja um die Funktionalität geht.

shop.html

<div class="shop">

    <div style="float: left;cursor: pointer;">
        <articleList></articleList>
    </div>
    <div style="float: right;">
        <shoppingCart></shoppingCart>
    </div>
</div>

article-list.html

<h2>Artikel Liste</h2>
<ul>
    <li *ngFor="let article of articleService.getArticleList()" (click)="addArticle($event, article)" style="-webkit-user-select: none;">
        <h3>{{article.name}}</h3>

        <p>{{article.description}}</p>

        <b>{{article.price}} €</b>
    </li>

</ul>

cart.html

<h2>Warenkorb</h2>
<ul>
    <li *ngFor="let item of cartService.getItemList()">
        <h3>{{item.article.name}}</h3>

        <p>
            <span>{{item.amount}} x {{item.article.price}}</span> =
            <span><b>{{item.getTotal()}} €</b></span>
        </p>


    </li>

</ul>

<hr/>
<h3>Summe: {{cartService.getTotal()}}</h3>

Das war auch tatsächlich schon alles. Wenn man jetzt auf einen Eintrag in der Artikelliste klickt, wird ein neuen Eintrag im Warenkorb vorgenommen. Ist der Artikel bereits im Warenkorb, dann wird die Menge einfach nur um 1 erhöht.

Das finale Produkt gibt es zunächst hier nur als Screenshot und später dann als Video:

Angular 2 Shopping Cart