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:
- Shop
- Das ist unsere übergeordnete Komponente, die alle anderen bündelt
- Artikelliste
- Hier werden die Daten für die Shop-Liste zusammengetragen und präsentiert
- 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:
- Artikel-Service
- Dieser soll unsere Mock-Daten konstruieren und liefern
- 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:
- Artikel
- Enthält Namen, Beschreibung und Preis
- 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:
