So erstellen Sie eine E-Commerce-Site mit Angular 11, Commerce Layer und Paypal
Veröffentlicht: 2022-03-10Heutzutage ist es unerlässlich, eine Online-Präsenz zu haben, wenn man ein Unternehmen führt. Online wird viel mehr eingekauft als in den Vorjahren. Mit einem E-Commerce-Shop können Ladenbesitzer andere Einnahmequellen erschließen, die sie mit einem stationären Laden nicht nutzen könnten. Andere Shop-Besitzer führen ihre Geschäfte jedoch online ganz ohne physische Präsenz. Dies macht einen Online-Shop von entscheidender Bedeutung.
Websites wie Etsy, Shopify und Amazon machen es einfach, ziemlich schnell einen Shop einzurichten, ohne sich um die Entwicklung einer Website kümmern zu müssen. Es kann jedoch Fälle geben, in denen Ladenbesitzer ein personalisiertes Erlebnis wünschen oder vielleicht die Kosten für den Besitz eines Ladens auf einigen dieser Plattformen sparen möchten.
Headless-E-Commerce-API-Plattformen bieten Backends, mit denen Shop-Sites eine Schnittstelle herstellen können. Sie verwalten alle Prozesse und Daten im Zusammenhang mit dem Geschäft wie Kunden, Bestellungen, Lieferungen, Zahlungen usw. Alles, was benötigt wird, ist ein Frontend, um mit diesen Informationen zu interagieren. Dies gibt Eigentümern viel Flexibilität bei der Entscheidung, wie ihre Kunden ihren Online-Shop erleben und wie sie ihn betreiben möchten.
In diesem Artikel werden wir behandeln, wie man einen E-Commerce-Shop mit Angular 11 erstellt. Wir werden Commerce Layer als unsere Headless-E-Commerce-API verwenden. Obwohl es unzählige Möglichkeiten gibt, Zahlungen zu verarbeiten, zeigen wir Ihnen, wie Sie nur eine verwenden, Paypal.
- Quellcode auf GitHub anzeigen →
Voraussetzungen
Bevor Sie die App erstellen, müssen Sie Angular CLI installiert haben. Wir werden es verwenden, um die App zu initialisieren und zu rüsten. Wenn Sie es noch nicht installiert haben, können Sie es über npm erhalten.
npm install -g @angular/cli
Sie benötigen außerdem ein Commerce Layer-Entwicklerkonto. Mit dem Entwicklerkonto müssen Sie eine Testorganisation erstellen und diese mit Testdaten ausstatten. Das Seeding erleichtert die Entwicklung der App, ohne sich Gedanken darüber machen zu müssen, welche Daten Sie verwenden müssen. Sie können ein Konto unter diesem Link und eine Organisation hier erstellen.
Schließlich benötigen Sie ein Paypal-Sandbox-Konto. Mit dieser Art von Konto können wir Transaktionen zwischen Unternehmen und Benutzern testen, ohne echtes Geld zu riskieren. Hier können Sie eine erstellen. Für ein Sandbox-Konto wurde bereits ein geschäftliches und ein persönliches Testkonto erstellt.
Commerce Layer und Paypal-Konfiguration
Um Paypal-Sandbox-Zahlungen auf Commerce Layer zu ermöglichen, müssen Sie API-Schlüssel einrichten. Wechseln Sie zur Kontenübersicht Ihres Paypal-Entwicklerkontos. Wählen Sie ein Geschäftskonto aus und auf der Registerkarte API-Anmeldeinformationen der Kontodetails finden Sie die Standardanwendung unter REST-Apps .
Um Ihr Paypal-Geschäftskonto mit Ihrer Commerce Layer-Organisation zu verknüpfen, gehen Sie zum Dashboard Ihrer Organisation. Hier fügen Sie ein Paypal-Zahlungsgateway und eine Paypal-Zahlungsmethode für Ihre verschiedenen Märkte hinzu. Wählen Sie unter Einstellungen > Zahlungen Zahlungs-Gateways > Paypal aus und fügen Sie Ihre Paypal-Client-ID und Ihr Geheimnis hinzu.
Nachdem Sie das Gateway erstellt haben, müssen Sie für jeden Zielmarkt eine Paypal-Zahlungsmethode erstellen, um Paypal als Option verfügbar zu machen. Sie tun dies unter Einstellungen > Zahlungen > Zahlungsmethoden > Neue Zahlungsmethode .
Ein Hinweis zu den verwendeten Routen
Commerce Layer stellt eine Route für die Authentifizierung und einen weiteren anderen Satz von Routen für ihre API bereit. Ihre /oauth/token
Authentifizierungsroute tauscht Anmeldeinformationen gegen ein Token aus. Dieses Token ist für den Zugriff auf ihre API erforderlich. Die restlichen API-Routen haben das Muster /api/:resource
.
Der Umfang dieses Artikels deckt nur den Frontend-Teil dieser App ab. Ich habe mich dafür entschieden, die Token serverseitig zu speichern, Sitzungen zu verwenden, um den Besitz zu verfolgen, und dem Client Nur-http-Cookies mit einer Sitzungs-ID bereitzustellen. Dies wird hier nicht behandelt, da es den Rahmen dieses Artikels sprengen würde. Die Routen bleiben jedoch gleich und entsprechen exakt der Commerce Layer API. Es gibt jedoch einige benutzerdefinierte Routen, die nicht über die Commerce Layer-API verfügbar sind, die wir verwenden werden. Diese befassen sich hauptsächlich mit dem Sitzungsmanagement. Ich werde darauf hinweisen, sobald wir zu ihnen kommen, und beschreiben, wie Sie ein ähnliches Ergebnis erzielen können.
Eine weitere Inkonsistenz, die Ihnen möglicherweise auffällt, besteht darin, dass sich die Anforderungstexte von den Anforderungen der Commerce Layer-API unterscheiden. Da die Anfragen an einen anderen Server weitergeleitet werden, um mit einem Token gefüllt zu werden, habe ich die Körper anders strukturiert. Dies sollte das Versenden von Anfragen erleichtern. Bei Unstimmigkeiten in den Antragstexten wird in den Diensten darauf hingewiesen.
Da dies außerhalb des Geltungsbereichs liegt, müssen Sie entscheiden, wie Token sicher gespeichert werden. Sie müssen auch die Anforderungstexte leicht ändern, damit sie genau den Anforderungen der Commerce Layer-API entsprechen. Wenn es eine Inkonsistenz gibt, verlinke ich auf die API-Referenz und Anleitungen, die detailliert beschreiben, wie der Textkörper richtig strukturiert wird.
App-Struktur
Um die App zu organisieren, werden wir sie in vier Hauptteile unterteilen. Eine bessere Beschreibung dessen, was jedes der Module tut, finden Sie in den entsprechenden Abschnitten:
- das Kernmodul,
- das Datenmodul,
- das gemeinsam genutzte Modul,
- die Feature-Module.
Die Funktionsmodule gruppieren verwandte Seiten und Komponenten zusammen. Es wird vier Funktionsmodule geben:
- das Auth-Modul,
- das Produktmodul,
- das Warenkorbmodul,
- das Kassenmodul.
Wenn wir zu jedem Modul kommen, werde ich seinen Zweck erklären und seinen Inhalt aufschlüsseln.
Unten ist ein Baum des src/app
-Ordners und wo sich jedes Modul befindet.
src ├── app │ ├── core │ ├── data │ ├── features │ │ ├── auth │ │ ├── cart │ │ ├── checkout │ │ └── products └── shared
Generieren der App und Hinzufügen von Abhängigkeiten
Wir beginnen mit der Generierung der App. Unsere Organisation wird The LIme Brand heißen und über Testdaten verfügen, die bereits von Commerce Layer gesät wurden.
ng new lime-app
Wir brauchen ein paar Abhängigkeiten. Hauptsächlich Kantiges Material und Bis zur Zerstörung. Angular Material liefert Komponenten und Styling. Until Destroy meldet sich automatisch von Observables ab, wenn Komponenten zerstört werden. Um sie zu installieren, führen Sie Folgendes aus:
npm install @ngneat/until-destroy ng add @angular/material
Vermögenswerte
Beim Hinzufügen von Adressen zu Commerce Layer muss ein Alpha-2-Ländercode verwendet werden. Wir fügen dem assets
-Ordner unter assets/json/country-codes.json
eine JSON-Datei mit diesen Codes hinzu. Sie finden diese Datei hier verlinkt.
Stile
Die Komponenten, die wir erstellen, teilen sich ein gewisses globales Design. Wir werden sie in styles.css
, die unter diesem Link zu finden ist.
Umfeld
Unsere Konfiguration besteht aus zwei Feldern. Die apiUrl
, die auf die Commerce Layer API verweisen sollte. apiUrl
wird von den Diensten verwendet, die wir zum Abrufen von Daten erstellen werden. Die clientUrl
sollte die Domäne sein, auf der die App ausgeführt wird. Wir verwenden dies beim Festlegen von Weiterleitungs-URLs für Paypal. Sie finden diese Datei unter diesem Link.
Freigegebenes Modul
Das gemeinsam genutzte Modul enthält Dienste, Pipes und Komponenten, die von den anderen Modulen gemeinsam genutzt werden.
ng gm shared
Es besteht aus drei Komponenten, einer Pipe und zwei Services. So wird das aussehen.
src/app/shared ├── components │ ├── item-quantity │ │ ├── item-quantity.component.css │ │ ├── item-quantity.component.html │ │ └── item-quantity.component.ts │ ├── simple-page │ │ ├── simple-page.component.css │ │ ├── simple-page.component.html │ │ └── simple-page.component.ts │ └── title │ ├── title.component.css │ ├── title.component.html │ └── title.component.ts ├── pipes │ └── word-wrap.pipe.ts ├── services │ ├── http-error-handler.service.ts │ └── local-storage.service.ts └── shared.module.ts
Wir werden das freigegebene Modul auch verwenden, um einige häufig verwendete Angular Material-Komponenten zu exportieren. Dies macht es einfacher, sie sofort zu verwenden, anstatt jede Komponente über verschiedene Module hinweg zu importieren. Hier ist, was shared.module.ts
enthalten wird.
@NgModule({ declarations: [SimplePageComponent, TitleComponent, WordWrapPipe, ItemQuantityComponent], imports: [CommonModule, MatIconModule, MatButtonModule, MatTooltipModule, MatMenuModule, RouterModule], exports: [ CommonModule, ItemQuantityComponent, MatButtonModule, MatIconModule, MatSnackBarModule, MatTooltipModule, SimplePageComponent, TitleComponent, WordWrapPipe ] }) export class SharedModule { }
Komponenten
Artikelmengenkomponente
Diese Komponente legt die Menge der Artikel beim Hinzufügen zum Warenkorb fest. Es wird in den Warenkorb- und Produktmodulen verwendet. Eine Materialauswahl wäre für diesen Zweck eine einfache Wahl gewesen. Der Stil der Materialauswahl stimmte jedoch nicht mit den Materialeingaben überein, die in allen anderen Formularen verwendet wurden. Ein Materialmenü sah den verwendeten Materialeingaben sehr ähnlich. Also beschloss ich, stattdessen eine ausgewählte Komponente damit zu erstellen.
ng gc shared/components/item-quantity
Die Komponente hat drei Eingabeeigenschaften und eine Ausgabeeigenschaft. quantity
legt die Anfangsmenge der Elemente fest, maxValue
gibt die maximale Anzahl von Elementen an, die auf einmal ausgewählt werden können, und disabled
gibt an, ob die Komponente deaktiviert werden soll oder nicht. Das setQuantityEvent
wird ausgelöst, wenn eine Menge ausgewählt wird.
Wenn die Komponente initialisiert wird, legen wir die Werte fest, die im Materialmenü erscheinen. Es gibt auch eine Methode namens setQuantity
, die setQuantityEvent
Ereignisse ausgibt.
Dies ist die Komponentendatei.
@Component({ selector: 'app-item-quantity', templateUrl: './item-quantity.component.html', styleUrls: ['./item-quantity.component.css'] }) export class ItemQuantityComponent implements OnInit { @Input() quantity: number = 0; @Input() maxValue?: number = 0; @Input() disabled?: boolean = false; @Output() setQuantityEvent = new EventEmitter<number>(); values: number[] = []; constructor() { } ngOnInit() { if (this.maxValue) { for (let i = 1; i <= this.maxValue; i++) { this.values.push(i); } } } setQuantity(value: number) { this.setQuantityEvent.emit(value); } }
Dies ist seine Vorlage.
<button mat-stroked-button [matMenuTriggerFor]="menu" [disabled]="disabled"> {{quantity}} <mat-icon *ngIf="!disabled">expand_more</mat-icon> </button> <mat-menu #menu="matMenu"> <button *ngFor="let no of values" (click)="setQuantity(no)" mat-menu-item>{{no}}</button> </mat-menu>
Hier ist sein Styling.
button { margin: 3px; }
Titelkomponente
Diese Komponente dient gleichzeitig als Stepper-Titel sowie als einfacher Titel auf einigen einfacheren Seiten. Obwohl Angular Material eine Stepper-Komponente bietet, war es nicht die beste Lösung für einen ziemlich langen Checkout-Prozess, war auf kleineren Displays nicht so reaktionsschnell und erforderte viel mehr Zeit für die Implementierung. Ein einfacherer Titel könnte jedoch als Stepper-Indikator umfunktioniert werden und über mehrere Seiten hinweg nützlich sein.
ng gc shared/components/title
Die Komponente hat vier Eingabeeigenschaften: einen title
, einen subtitle
, eine Zahl ( no
) und centerText
, um anzugeben, ob der Text der Komponente zentriert werden soll.
@Component({ selector: 'app-title', templateUrl: './title.component.html', styleUrls: ['./title.component.css'] }) export class TitleComponent { @Input() title: string = ''; @Input() subtitle: string = ''; @Input() no?: string; @Input() centerText?: boolean = false; }
Unten ist seine Vorlage. Das Styling finden Sie hier verlinkt.
<div> <h1 *ngIf="no" class="mat-display-1">{{no}}</h1> <div [ngClass]="{ 'centered-section': centerText}"> <h1 class="mat-display-2">{{title}}</h1> <p>{{subtitle}}</p> </div> </div>
Einfache Seitenkomponente
Es gibt mehrere Fälle, in denen ein Titel, ein Symbol und eine Schaltfläche alles waren, was für eine Seite benötigt wurde. Dazu gehören eine 404-Seite, eine leere Warenkorbseite, eine Fehlerseite, eine Zahlungsseite und eine Bestellseite. Das ist der Zweck der einfachen Seitenkomponente. Wenn auf die Schaltfläche auf der Seite geklickt wird, leitet sie entweder zu einer Route um oder führt als Reaktion auf ein buttonEvent
eine Aktion aus.
Um es zu machen:
ng gc shared/components/simple-page
Dies ist seine Komponentendatei.
@Component({ selector: 'app-simple-page', templateUrl: './simple-page.component.html', styleUrls: ['./simple-page.component.css'] }) export class SimplePageComponent { @Input() title: string = ''; @Input() subtitle?: string; @Input() number?: string; @Input() icon?: string; @Input() buttonText: string = ''; @Input() centerText?: boolean = false; @Input() buttonDisabled?: boolean = false; @Input() route?: string | undefined; @Output() buttonEvent = new EventEmitter(); constructor(private router: Router) { } buttonClicked() { if (this.route) { this.router.navigateByUrl(this.route); } else { this.buttonEvent.emit(); } } }
Und seine Vorlage enthält:
<div> <app-title no="{{number}}" title="{{title}}" subtitle="{{subtitle}}" [centerText]="centerText"></app-title> <div *ngIf="icon"> <mat-icon color="primary" class="icon">{{icon}}</mat-icon> </div> <button mat-flat-button color="primary" (click)="buttonClicked()" [disabled]="buttonDisabled"> {{buttonText}} </button> </div>
Sein Styling finden Sie hier.
Rohre
Zeilenumbruch-Rohr
Einige Produktnamen und andere Arten von Informationen, die auf der Website angezeigt werden, sind sehr lang. In einigen Fällen ist es eine Herausforderung, diese langen Sätze in materielle Komponenten zu packen. Also verwenden wir diese Pipe, um die Sätze auf eine bestimmte Länge zu kürzen und Ellipsen am Ende des Ergebnisses hinzuzufügen.
Um es zu erstellen, führen Sie Folgendes aus:
ng g pipe shared/pipes/word-wrap
Es wird enthalten:
import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'wordWrap' }) export class WordWrapPipe implements PipeTransform { transform(value: string, length: number): string { return `${value.substring(0, length)}...`; } }
Dienstleistungen
HTTP-Error-Handler-Dienst
Es gibt eine ganze Reihe von HTTP-Diensten in diesem Projekt. Das Erstellen eines Fehlerhandlers für jede Methode wiederholt sich. Daher ist es sinnvoll, einen einzigen Handler zu erstellen, der von allen Methoden verwendet werden kann. Der Fehlerhandler kann verwendet werden, um einen Fehler zu formatieren und die Fehler auch an andere externe Protokollierungsplattformen weiterzugeben.
Generieren Sie es, indem Sie Folgendes ausführen:
ng gs shared/services/http-error-handler
Dieser Dienst enthält nur eine Methode. Die Methode formatiert die anzuzeigende Fehlermeldung abhängig davon, ob es sich um einen Client- oder einen Serverfehler handelt. Es gibt jedoch Raum für weitere Verbesserungen.
@Injectable({ providedIn: 'root' }) export class HttpErrorHandler { constructor() { } handleError(err: HttpErrorResponse): Observable { let displayMessage = ''; if (err.error instanceof ErrorEvent) { displayMessage = `Client-side error: ${err.error.message}`; } else { displayMessage = `Server-side error: ${err.message}`; } return throwError(displayMessage); } }
@Injectable({ providedIn: 'root' }) export class HttpErrorHandler { constructor() { } handleError(err: HttpErrorResponse): Observable { let displayMessage = ''; if (err.error instanceof ErrorEvent) { displayMessage = `Client-side error: ${err.error.message}`; } else { displayMessage = `Server-side error: ${err.message}`; } return throwError(displayMessage); } }
Lokaler Speicherdienst
Wir verwenden lokale Speicher, um die Anzahl der Artikel in einem Einkaufswagen zu verfolgen. Es ist auch nützlich, hier die ID einer Bestellung zu speichern. Eine Bestellung entspricht einem Warenkorb auf Commerce Layer.
So generieren Sie den lokalen Speicherdienst:
ng gs shared/services/local-storage
Der Dienst enthält vier Methoden zum Hinzufügen, Löschen und Abrufen von Elementen aus dem lokalen Speicher und eine weitere zum Löschen.
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class LocalStorageService { constructor() { } addItem(key: string, value: string) { localStorage.setItem(key, value); } deleteItem(key: string) { localStorage.removeItem(key); } getItem(key: string): string | null { return localStorage.getItem(key); } clear() { localStorage.clear(); } }
Datenmodul
Dieses Modul ist für den Datenabruf und die Datenverwaltung verantwortlich. Es wird verwendet, um die Daten zu erhalten, die unsere App verbraucht. Unten ist seine Struktur:
src/app/data ├── data.module.ts ├── models └── services
So generieren Sie den Modullauf:
ng gm data
Modelle
Die Modelle definieren, wie die Daten, die wir von der API verwenden, strukturiert sind. Wir haben 16 Schnittstellendeklarationen. Um sie zu erstellen, führen Sie Folgendes aus:
for model in \ address cart country customer-address \ customer delivery-lead-time line-item order \ payment-method payment-source paypal-payment \ price shipment shipping-method sku stock-location; \ do ng g interface "data/models/${model}"; done
Die folgende Tabelle enthält Links zu jeder Datei und eine Beschreibung der einzelnen Schnittstellen.
Schnittstelle | Beschreibung |
---|---|
Adresse | Stellt eine allgemeine Adresse dar. |
Wagen | Clientseitige Version einer Bestellung, die die Anzahl der Produkte verfolgt, die ein Kunde zu kaufen beabsichtigt. |
Land | Alpha-2-Ländercode. |
Kundenadresse | Eine mit einem Kunden verknüpfte Adresse. |
Kunde | Ein registrierter Benutzer. |
Lieferzeit | Stellt die Zeit dar, die für die Zustellung einer Sendung benötigt wird. |
Einzelposten | Ein aufgeschlüsseltes Produkt, das dem Warenkorb hinzugefügt wurde. |
Befehl | Ein Einkaufswagen oder eine Sammlung von Werbebuchungen. |
Zahlungsmethode | Eine Zahlungsart, die für eine Bestellung verfügbar gemacht wird. |
Zahlungsquelle | Eine mit einer Bestellung verbundene Zahlung. |
PayPal Bezahlung | Eine Zahlung über Paypal |
Preis | Mit einer SKU verbundener Preis. |
Sendung | Sammlung von Artikeln, die zusammen versendet werden. |
Versandart | Methode, durch die ein Paket versendet wird. |
Artikelnummer | Eine einzigartige Lagerhaltungseinheit. |
Lagerort | Standort, der SKU-Inventar enthält. |
Dienstleistungen
Dieser Ordner enthält die Dienste, die App-Daten erstellen, abrufen und bearbeiten. Wir werden hier 11 Dienste erstellen.
for service in \ address cart country customer-address \ customer delivery-lead-time line-item \ order paypal-payment shipment sku; \ do ng gs "data/services/${service}"; done
Adressservice
Dieser Dienst erstellt und ruft Adressen ab. Dies ist wichtig beim Erstellen und Zuweisen von Versand- und Rechnungsadressen zu Bestellungen. Es hat zwei Methoden. Eine zum Erstellen einer Adresse und eine zum Abrufen einer Adresse.
Die hier verwendete Route ist /api/addresses
. Wenn Sie die Commerce Layer-API direkt verwenden, stellen Sie sicher, dass Sie die Daten wie in diesem Beispiel gezeigt strukturieren.
@Injectable({ providedIn: 'root' }) export class AddressService { private url: string = `${environment.apiUrl}/api/addresses`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } createAddress(address: Address): Observable<Address> { return this.http.post<Address>(this.url, address) .pipe(catchError(this.eh.handleError)); } getAddress(id: string): Observable<Address> { return this.http.get<Address>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } }
Warenkorb-Service
Der Einkaufswagen ist dafür verantwortlich, die Menge der hinzugefügten Artikel und die Bestell-ID beizubehalten. API-Aufrufe durchzuführen, um jedes Mal, wenn eine neue Werbebuchung erstellt wird, die Anzahl der Artikel in einer Bestellung abzurufen, kann teuer sein. Stattdessen könnten wir einfach den lokalen Speicher verwenden, um die Anzahl auf dem Client aufrechtzuerhalten. Dadurch entfällt die Notwendigkeit, jedes Mal, wenn ein Artikel in den Warenkorb gelegt wird, unnötige Bestellabrufe durchzuführen.
Wir verwenden diesen Dienst auch, um die Bestell-ID zu speichern. Ein Warenkorb entspricht einer Bestellung auf Commerce Layer. Sobald der erste Artikel in den Warenkorb gelegt wird, wird eine Bestellung erstellt. Wir müssen diese Bestell-ID aufbewahren, damit wir sie während des Bestellvorgangs abrufen können.
Außerdem brauchen wir eine Möglichkeit, der Kopfzeile mitzuteilen, dass ein Artikel zum Einkaufswagen hinzugefügt wurde. Die Kopfzeile enthält die Warenkorb-Schaltfläche und zeigt die Anzahl der darin enthaltenen Artikel an. Wir verwenden ein Observable eines BehaviorSubject
mit dem aktuellen Wert des Einkaufswagens. Der Header kann dies abonnieren und Änderungen des Warenkorbwerts verfolgen.
Schließlich muss nach Abschluss einer Bestellung der Warenkorbwert gelöscht werden. Dadurch wird sichergestellt, dass es bei der Erstellung späterer neuerer Bestellungen nicht zu Verwechslungen kommt. Die gespeicherten Werte werden gelöscht, sobald die aktuelle Bestellung als aufgegeben markiert wird.
All dies erreichen wir mit dem zuvor erstellten lokalen Speicherdienst.
@Injectable({ providedIn: 'root' }) export class CartService { private cart = new BehaviorSubject({ orderId: this.orderId, itemCount: this.itemCount }); cartValue$ = this.cart.asObservable(); constructor(private storage: LocalStorageService) { } get orderId(): string { const id = this.storage.getItem('order-id'); return id ? id : ''; } set orderId(id: string) { this.storage.addItem('order-id', id); this.cart.next({ orderId: id, itemCount: this.itemCount }); } get itemCount(): number { const itemCount = this.storage.getItem('item-count'); return itemCount ? parseInt(itemCount) : 0; } set itemCount(amount: number) { this.storage.addItem('item-count', amount.toString()); this.cart.next({ orderId: this.orderId, itemCount: amount }); } incrementItemCount(amount: number) { this.itemCount = this.itemCount + amount; } decrementItemCount(amount: number) { this.itemCount = this.itemCount - amount; } clearCart() { this.storage.deleteItem('item-count'); this.cart.next({ orderId: '', itemCount: 0 }); } }
Landesdienst
Beim Hinzufügen von Adressen auf Commerce Layer muss der Ländercode ein Alpha-2-Code sein. Dieser Dienst liest eine JSON-Datei, die diese Codes für jedes Land enthält, und gibt sie in seiner getCountries
Methode zurück.
@Injectable({ providedIn: 'root' }) export class CountryService { constructor(private http: HttpClient) { } getCountries(): Observable { return this.http.get ('./../../../assets/json/country-codes.json'); } }
@Injectable({ providedIn: 'root' }) export class CountryService { constructor(private http: HttpClient) { } getCountries(): Observable { return this.http.get ('./../../../assets/json/country-codes.json'); } }
@Injectable({ providedIn: 'root' }) export class CountryService { constructor(private http: HttpClient) { } getCountries(): Observable { return this.http.get ('./../../../assets/json/country-codes.json'); } }
Kundenadressendienst
Dieser Dienst wird verwendet, um Adressen mit Kunden zu verknüpfen. Es ruft auch eine bestimmte oder alle Adressen ab, die sich auf einen Kunden beziehen. Es wird verwendet, wenn der Kunde seine Liefer- und Rechnungsadresse zu seiner Bestellung hinzufügt. Die Methode createCustomer
erstellt einen Kunden, getCustomerAddresses
alle Adressen eines Kunden ab und getCustomerAddress
eine bestimmte ab.
Achten Sie beim Anlegen einer Kundenadresse darauf, den Post-Body nach diesem Beispiel aufzubauen.
@Injectable({ providedIn: 'root' }) export class CustomerAddressService { private url: string = `${environment.apiUrl}/api/customer_addresses`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } createCustomerAddress(addressId: string, customerId: string): Observable<CustomerAddress> { return this.http.post<CustomerAddress>(this.url, { addressId: addressId, customerId: customerId }) .pipe(catchError(this.eh.handleError)); } getCustomerAddresses(): Observable<CustomerAddress[]> { return this.http.get<CustomerAddress[]>(`${this.url}`) .pipe(catchError(this.eh.handleError)); } getCustomerAddress(id: string): Observable<CustomerAddress> { return this.http.get<CustomerAddress>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } }
Kundendienst
Mit diesem Dienst werden Kunden erstellt und ihre Informationen abgerufen. Wenn sich ein Benutzer anmeldet, wird er Kunde und wird mit der createCustomerMethod
. getCustomer
gibt den Kunden zurück, der einer bestimmten ID zugeordnet ist. getCurrentCustomer
gibt den aktuell angemeldeten Kunden zurück.
Beim Anlegen eines Kunden strukturieren Sie die Daten wie folgt. Sie können ihren Vor- und Nachnamen zu den Metadaten hinzufügen, wie in ihren Attributen gezeigt.
Die Route /api/customers/current
ist auf Commerce Layer nicht verfügbar. Sie müssen also herausfinden, wie Sie den aktuell angemeldeten Kunden abrufen.
@Injectable({ providedIn: 'root' }) export class CustomerService { private url: string = `${environment.apiUrl}/api/customers`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } createCustomer(email: string, password: string, firstName: string, lastName: string): Observable<Customer> { return this.http.post<Customer>(this.url, { email: email, password: password, firstName: firstName, lastName: lastName }) .pipe(catchError(this.eh.handleError)); } getCurrentCustomer(): Observable<Customer> { return this.http.get<Customer>(`${this.url}/current`) .pipe(catchError(this.eh.handleError)); } getCustomer(id: string): Observable<Customer> { return this.http.get<Customer>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } }
Liefervorlaufzeit-Service
Dieser Service gibt Informationen zu Versandzeiten von verschiedenen Lagerorten zurück.
@Injectable({ providedIn: 'root' }) export class DeliveryLeadTimeService { private url: string = `${environment.apiUrl}/api/delivery_lead_times`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } getDeliveryLeadTimes(): Observable<DeliveryLeadTime[]> { return this.http.get<DeliveryLeadTime[]>(this.url) .pipe(catchError(this.eh.handleError)); } }
Einzelpostendienst
Dem Warenkorb hinzugefügte Artikel werden von diesem Dienst verwaltet. Damit können Sie einen Artikel erstellen, sobald er dem Warenkorb hinzugefügt wird. Die Informationen zu einem Artikel können ebenfalls abgerufen werden. Der Artikel kann auch aktualisiert werden, wenn sich seine Menge ändert, oder gelöscht werden, wenn er aus dem Warenkorb entfernt wird.
Strukturieren Sie beim Erstellen oder Aktualisieren von Elementen den Anforderungstext wie in diesem Beispiel gezeigt.
@Injectable({ providedIn: 'root' }) export class LineItemService { private url: string = `${environment.apiUrl}/api/line_items`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } createLineItem(lineItem: LineItem): Observable<LineItem> { return this.http.post<LineItem>(this.url, lineItem) .pipe(catchError(this.eh.handleError)); } getLineItem(id: string): Observable<LineItem> { return this.http.get<LineItem>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } updateLineItem(id: string, quantity: number): Observable<LineItem> { return this.http.patch<LineItem>(`${this.url}/${id}`, { quantity: quantity }) .pipe(catchError(this.eh.handleError)); } deleteLineItem(id: string): Observable<LineItem> { return this.http.delete<LineItem>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } }
Bestellservice
Ähnlich wie beim Einzelpostenservice ermöglicht Ihnen der Bestellservice, eine Bestellung zu erstellen, zu aktualisieren, zu löschen oder zu erhalten. Darüber hinaus haben Sie die Möglichkeit, die mit einer Bestellung verbundenen Sendungen separat mit der Methode getOrderShipments
. Dieser Service wird während des Bezahlvorgangs stark genutzt.
Es gibt verschiedene Arten von Informationen zu einer Bestellung, die während des Bezahlvorgangs erforderlich sind. Da es teuer sein kann, eine ganze Bestellung und ihre Beziehungen abzurufen, geben wir mit GetOrderParams
an, was wir von einer Bestellung erhalten möchten. Das Äquivalent dazu auf der CL-API ist der Include-Abfrageparameter, in dem Sie die einzuschließenden Bestellbeziehungen auflisten. Sie können hier überprüfen, welche Felder für die Warenkorbzusammenfassung und für die verschiedenen Checkout-Phasen enthalten sein müssen.
Auf die gleiche Weise verwenden wir beim Aktualisieren einer Bestellung UpdateOrderParams
, um Aktualisierungsfelder anzugeben. Dies liegt daran, dass auf dem Server, der das Token füllt, einige zusätzliche Operationen ausgeführt werden, je nachdem, welches Feld aktualisiert wird. Wenn Sie jedoch direkte Anforderungen an die CL-API stellen, müssen Sie dies nicht angeben. Sie können darauf verzichten, da die CL-API Sie nicht dazu auffordert, sie anzugeben. Der Anforderungstext sollte jedoch diesem Beispiel ähneln.
@Injectable({ providedIn: 'root' }) export class OrderService { private url: string = `${environment.apiUrl}/api/orders`; constructor( private http: HttpClient, private eh: HttpErrorHandler) { } createOrder(): Observable<Order> { return this.http.post<Order>(this.url, {}) .pipe(catchError(this.eh.handleError)); } getOrder(id: string, orderParam: GetOrderParams): Observable<Order> { let params = {}; if (orderParam != GetOrderParams.none) { params = { [orderParam]: 'true' }; } return this.http.get<Order>(`${this.url}/${id}`, { params: params }) .pipe(catchError(this.eh.handleError)); } updateOrder(order: Order, params: UpdateOrderParams[]): Observable<Order> { let updateParams = []; for (const param of params) { updateParams.push(param.toString()); } return this.http.patch<Order>( `${this.url}/${order.id}`, order, { params: { 'field': updateParams } } ) .pipe(catchError(this.eh.handleError)); } getOrderShipments(id: string): Observable<Shipment[]> { return this.http.get<Shipment[]>(`${this.url}/${id}/shipments`) .pipe(catchError(this.eh.handleError)); } }
Paypal-Zahlungsservice
Dieser Dienst ist für die Erstellung und Aktualisierung von Paypal-Zahlungen für Bestellungen verantwortlich. Zusätzlich können wir eine Paypal-Zahlung erhalten, wenn wir die ID angeben. Der Post-Body sollte ähnlich aufgebaut sein wie in diesem Beispiel beim Erstellen einer Paypal-Zahlung.
@Injectable({ providedIn: 'root' }) export class PaypalPaymentService { private url: string = `${environment.apiUrl}/api/paypal_payments`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } createPaypalPayment(payment: PaypalPayment): Observable<PaypalPayment> { return this.http.post<PaypalPayment>(this.url, payment) .pipe(catchError(this.eh.handleError)); } getPaypalPayment(id: string): Observable<PaypalPayment> { return this.http.get<PaypalPayment>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } updatePaypalPayment(id: string, paypalPayerId: string): Observable<PaypalPayment> { return this.http.patch<PaypalPayment>( `${this.url}/${id}`, { paypalPayerId: paypalPayerId } ) .pipe(catchError(this.eh.handleError)); } }
Versandservice
Dieser Dienst erhält eine Sendung oder aktualisiert sie anhand ihrer ID. Der Anforderungstext einer Sendungsaktualisierung sollte ähnlich wie in diesem Beispiel aussehen.
@Injectable({ providedIn: 'root' }) export class ShipmentService { private url: string = `${environment.apiUrl}/api/shipments`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } getShipment(id: string): Observable<Shipment> { return this.http.get<Shipment>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } updateShipment(id: string, shippingMethodId: string): Observable<Shipment> { return this.http.patch<Shipment>( `${this.url}/${id}`, { shippingMethodId: shippingMethodId } ) .pipe(catchError(this.eh.handleError)); } }
SKU-Dienst
Der SKU-Dienst ruft Produkte aus dem Geschäft ab. Wenn mehrere Produkte abgerufen werden, können sie paginiert und mit einer festgelegten Seitengröße versehen werden. Seitengröße und Seitennummer sollten wie in diesem Beispiel als Abfrageparameter festgelegt werden, wenn Sie direkte Anfragen an die API stellen. Ein einzelnes Produkt kann auch anhand seiner ID abgerufen werden.
@Injectable({ providedIn: 'root' }) export class SkuService { private url: string = `${environment.apiUrl}/api/skus`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } getSku(id: string): Observable<Sku> { return this.http.get<Sku>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } getSkus(page: number, pageSize: number): Observable<Sku[]> { return this.http.get<Sku[]>( this.url, { params: { 'page': page.toString(), 'pageSize': pageSize.toString() } }) .pipe(catchError(this.eh.handleError)); } }
Kern Modul
Das Kernmodul enthält alles Zentrale und Gemeinsame der Anwendung. Dazu gehören Komponenten wie der Header und Seiten wie die 404-Seite. Hierunter fallen auch Dienste, die für die Authentifizierung und das Sitzungsmanagement zuständig sind, sowie App-weite Interceptors und Guards.
Der Kernmodulbaum wird wie folgt aussehen.
src/app/core ├── components │ ├── error │ │ ├── error.component.css │ │ ├── error.component.html │ │ └── error.component.ts │ ├── header │ │ ├── header.component.css │ │ ├── header.component.html │ │ └── header.component.ts │ └── not-found │ ├── not-found.component.css │ ├── not-found.component.html │ └── not-found.component.ts ├── core.module.ts ├── guards │ └── empty-cart.guard.ts ├── interceptors │ └── options.interceptor.ts └── services ├── authentication.service.ts ├── header.service.ts └── session.service.ts
Um das Modul und seinen Inhalt zu generieren, führen Sie Folgendes aus:
ng gm core ng gg core/guards/empty-cart ng gs core/header/header ng g interceptor core/interceptors/options for comp in header error not-found; do ng gc "core/${comp}"; done for serv in authentication session; do ng gs "core/authentication/${serv}"; done
Die Kernmoduldatei sollte so aussehen. Beachten Sie, dass Routen für NotFoundComponent
und ErrorComponent
.
@NgModule({ declarations: [HeaderComponent, NotFoundComponent, ErrorComponent], imports: [ RouterModule.forChild([ { path: '404', component: NotFoundComponent }, { path: 'error', component: ErrorComponent }, { path: '**', redirectTo: '/404' } ]), MatBadgeModule, SharedModule ], exports: [HeaderComponent] }) export class CoreModule { }
Dienstleistungen
Der Dienstordner enthält die Authentifizierungs-, Sitzungs- und Header-Dienste.
Authentifizierungsdienst
Mit dem AuthenticationService
können Sie Client- und Kundentoken erwerben. Diese Token werden verwendet, um auf die restlichen Routen der API zuzugreifen. Kunden-Token werden zurückgegeben, wenn ein Benutzer eine E-Mail-Adresse und ein Passwort dafür austauscht, und verfügen über ein breiteres Spektrum an Berechtigungen. Client-Token werden ohne Anmeldeinformationen ausgestellt und haben engere Berechtigungen.
getClientSession
ein Client-Token ab. login
erhält ein Kunden-Token. Beide Methoden erstellen auch eine Sitzung. Der Text einer Client-Token-Anfrage sollte so aussehen und der eines Kunden-Tokens so.
@Injectable({ providedIn: 'root' }) export class AuthenticationService { private url: string = `${environment.apiUrl}/oauth/token`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } getClientSession(): Observable<object> { return this.http.post<object>( this.url, { grantType: 'client_credentials' }) .pipe(catchError(this.eh.handleError)); } login(email: string, password: string): Observable<object> { return this.http.post<object>( this.url, { username: email, password: password, grantType: 'password' }) .pipe(catchError(this.eh.handleError)); } }
Session Service
The SessionService
is responsible for session management. The service will contain an observable from a BehaviorSubject
called loggedInStatus
to communicate whether a user is logged in. setLoggedInStatus
sets the value of this subject, true
for logged in, and false
for not logged in. isCustomerLoggedIn
makes a request to the server to check if the user has an existing session. logout
destroys the session on the server. The last two methods access routes that are unique to the server that populates the request with a token. They are not available from Commerce Layer. You'll have to figure out how to implement them.
@Injectable({ providedIn: 'root' }) export class SessionService { private url: string = `${environment.apiUrl}/session`; private isLoggedIn = new BehaviorSubject(false); loggedInStatus = this.isLoggedIn.asObservable(); constructor(private http: HttpClient, private eh: HttpErrorHandler) { } setLoggedInStatus(status: boolean) { this.isLoggedIn.next(status); } isCustomerLoggedIn(): Observable<{ message: string }> { return this.http.get<{ message: string }>(`${this.url}/customer/status`) .pipe(catchError(this.eh.handleError)); } logout(): Observable<{ message: string }> { return this.http.get<{ message: string }>(`${this.url}/destroy`) .pipe(catchError(this.eh.handleError)); } }
Header Service
The HeaderService
is used to communicate whether the cart, login, and logout buttons should be shown in the header. These buttons are hidden on the login and signup pages but present on all other pages to prevent confusion. We'll use an observable from a BehaviourSubject
called showHeaderButtons
that shares this. We'll also have a setHeaderButtonsVisibility
method to set this value.
@Injectable({ providedIn: 'root' }) export class HeaderService { private headerButtonsVisibility = new BehaviorSubject(true); showHeaderButtons = this.headerButtonsVisibility.asObservable(); constructor() { } setHeaderButtonsVisibility(visible: boolean) { this.headerButtonsVisibility.next(visible); } }
Komponenten
Error Component
This component is used as an error page. It is useful in instances when server requests fail and absolutely no data is displayed on a page. Instead of showing a blank page, we let the user know that a problem occurred. Below is it's template.
<app-simple-page title="An error occurred" subtitle="There was a problem fetching your page" buttonText="GO TO HOME" icon="report" [centerText]="true" route="/"> </app-simple-page>
This is what the component will look like.
Not Found Component
This is a 404 page that the user gets redirected to when they request a route not available on the router. Only its template is modified.
<app-simple-page title="404: Page not found" buttonText="GO TO HOME" icon="search" subtitle="The requested page could not be found" [centerText]="true" route="/"></app-simple-page>
Header Component
The HeaderComponent is basically the header displayed at the top of a page. It will contain the app title, the cart, login, and logout buttons.
When this component is initialized, a request is made to check whether the user has a current session. This happens when subscribing to this.session.isCustomerLoggedIn()
. We subscribe to this.session.loggedInStatus
to check if the user logs out throughout the life of the app. The this.header.showHeaderButtons
subscription decides whether to show all the buttons on the header or hide them. this.cart.cartValue$
gets the count of items in the cart.
Es gibt eine logout
, die die Sitzung eines Benutzers zerstört und ihm ein Client-Token zuweist. Ein Client-Token wird zugewiesen, da die Sitzung, die ihr Kunden-Token verwaltet, zerstört wurde und für jede API-Anforderung immer noch ein Token erforderlich ist. Eine Material-Snackbar teilt dem Benutzer mit, ob seine Sitzung erfolgreich zerstört wurde oder nicht.
Wir verwenden den @UntilDestroy({ checkProperties: true })
, um anzugeben, dass alle Abonnements automatisch gekündigt werden sollen, wenn die Komponente zerstört wird.
@UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-header', templateUrl: './header.component.html', styleUrls: ['./header.component.css'] }) export class HeaderComponent implements OnInit { cartAmount: number = 0; isLoggedIn: boolean = false; showButtons: boolean = true; constructor( private session: SessionService, private snackBar: MatSnackBar, private cart: CartService, private header: HeaderService, private auth: AuthenticationService ) { } ngOnInit() { this.session.isCustomerLoggedIn() .subscribe( () => { this.isLoggedIn = true; this.session.setLoggedInStatus(true); } ); this.session.loggedInStatus.subscribe(status => this.isLoggedIn = status); this.header.showHeaderButtons.subscribe(visible => this.showButtons = visible); this.cart.cartValue$.subscribe(cart => this.cartAmount = cart.itemCount); } logout() { concat( this.session.logout(), this.auth.getClientSession() ).subscribe( () => { this.snackBar.open('You have been logged out.', 'Close', { duration: 4000 }); this.session.setLoggedInStatus(false); }, err => this.snackBar.open('There was a problem logging you out.', 'Close', { duration: 4000 }) ); } }
Unten ist die Header-Vorlage und hier verlinkt ist ihr Styling.
<div> <div routerLink="/"> <h1><span>Lime</span><span>Store</span></h1> </div> <div> <div *ngIf="showButtons"> <button mat-icon-button color="primary" aria-label="shopping cart"> <mat-icon [matBadge]="cartAmount" matBadgeColor="accent" aria-label="shopping cart" routerLink="/cart">shopping_cart</mat-icon> </button> <button mat-icon-button color="primary" aria-label="login" *ngIf="!isLoggedIn"> <mat-icon aria-label="login" matTooltip="login" routerLink="/login">login</mat-icon> </button> <button mat-icon-button color="primary" aria-label="logout" *ngIf="isLoggedIn" (click)="logout()"> <mat-icon aria-label="logout" matTooltip="logout">logout</mat-icon> </button> </div> </div> </div>
Wachen
Leerer Wagenschutz
Dieser Schutz verhindert, dass Benutzer auf Routen in Bezug auf Kasse und Abrechnung zugreifen, wenn ihr Einkaufswagen leer ist. Dies liegt daran, dass es eine gültige Bestellung geben muss, um mit der Kasse fortzufahren. Eine Bestellung entspricht einem Warenkorb mit Artikeln darin. Wenn sich Artikel im Einkaufswagen befinden, kann der Benutzer zu einer geschützten Seite gehen. Wenn der Warenkorb jedoch leer ist, wird der Benutzer auf eine Seite mit leerem Warenkorb umgeleitet.
@Injectable({ providedIn: 'root' }) export class EmptyCartGuard implements CanActivate { constructor(private cart: CartService, private router: Router) { } canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { if (this.cart.orderId) { if (this.cart.itemCount > 0) { return true; } } return this.router.parseUrl('/empty'); } }
Abfangjäger
Optionen Interceptor
Dieser Interceptor fängt alle ausgehenden HTTP-Anforderungen ab und fügt der Anforderung zwei Optionen hinzu. Dies sind ein Content-Type
Header und eine withCredentials
-Eigenschaft. withCredentials
gibt an, ob eine Anfrage mit ausgehenden Anmeldeinformationen wie den von uns verwendeten Nur-http-Cookies gesendet werden soll. Wir verwenden Content-Type
, um anzugeben, dass wir json-Ressourcen an den Server senden.
@Injectable() export class OptionsInterceptor implements HttpInterceptor { constructor() { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { request = request.clone({ headers: request.headers.set('Content-Type', 'application/json'), withCredentials: true }); return next.handle(request); } }
Funktionsmodule
Dieser Abschnitt enthält die Hauptfunktionen der App. Wie bereits erwähnt, sind die Funktionen in vier Module gruppiert: Authentifizierungs-, Produkt-, Warenkorb- und Checkout-Module.
Produktmodul
Das Produktmodul enthält Seiten, auf denen Produkte zum Verkauf angezeigt werden. Dazu gehören die Produktseite und die Produktlistenseite. Es ist wie unten gezeigt aufgebaut.
src/app/features/products ├── pages │ ├── product │ │ ├── product.component.css │ │ ├── product.component.html │ │ └── product.component.ts │ └── product-list │ ├── product-list.component.css │ ├── product-list.component.html │ └── product-list.component.ts └── products.module.ts
Um es und seine Komponenten zu generieren:
ng gm features/products ng gc features/products/pages/product ng gc features/products/pages/product-list
Dies ist die Moduldatei:
@NgModule({ declarations: [ProductListComponent, ProductComponent], imports: [ RouterModule.forChild([ { path: 'product/:id', component: ProductComponent }, { path: '', component: ProductListComponent } ]), LayoutModule, MatCardModule, MatGridListModule, MatPaginatorModule, SharedModule ] }) export class ProductsModule { }
Produktlistenkomponente
Diese Komponente zeigt eine paginierte Liste verfügbarer Produkte zum Verkauf an. Es ist die erste Seite, die beim Start der App geladen wird.
Die Produkte werden in einem Raster angezeigt. Die Materialrasterliste ist dafür die beste Komponente. Um das Raster reaktionsfähig zu machen, ändert sich die Anzahl der Rasterspalten je nach Bildschirmgröße. Der BreakpointObserver
-Dienst ermöglicht es uns, die Größe des Bildschirms zu bestimmen und die Spalten während der Initialisierung zuzuweisen.
Um die Produkte abzurufen, rufen wir die Methode getProducts
des SkuService
. Bei Erfolg gibt es die Produkte zurück und ordnet sie dem Grid zu. Wenn nicht, leiten wir den Benutzer zur Fehlerseite weiter.
Da die angezeigten Produkte paginiert sind, haben wir eine getNextPage
Methode, um die zusätzlichen Produkte abzurufen.
@UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-product-list', templateUrl: './product-list.component.html', styleUrls: ['./product-list.component.css'] }) export class ProductListComponent implements OnInit { cols = 4; length = 0; pageIndex = 0; pageSize = 20; pageSizeOptions: number[] = [5, 10, 20]; pageEvent!: PageEvent | void; products: Sku[] = []; constructor( private breakpointObserver: BreakpointObserver, private skus: SkuService, private router: Router, private header: HeaderService) { } ngOnInit() { this.getProducts(1, 20); this.header.setHeaderButtonsVisibility(true); this.breakpointObserver.observe([ Breakpoints.Handset, Breakpoints.Tablet, Breakpoints.Web ]).subscribe(result => { if (result.matches) { if (result.breakpoints['(max-width: 599.98px) and (orientation: portrait)'] || result.breakpoints['(max-width: 599.98px) and (orientation: landscape)']) { this.cols = 1; } else if (result.breakpoints['(min-width: 1280px) and (orientation: portrait)'] || result.breakpoints['(min-width: 1280px) and (orientation: landscape)']) { this.cols = 4; } else { this.cols = 3; } } }); } private getProducts(page: number, pageSize: number) { this.skus.getSkus(page, pageSize) .subscribe( skus => { this.products = skus; this.length = skus[0].__collectionMeta.recordCount; }, err => this.router.navigateByUrl('/error') ); } getNextPage(event: PageEvent) { this.getProducts(event.pageIndex + 1, event.pageSize); } trackSkus(index: number, item: Sku) { return `${item.id}-${index}`; } }
Die Vorlage wird unten gezeigt und ihr Stil kann hier gefunden werden.
<mat-grid-list cols="{{cols}}" rowHeight="400px" gutterSize="20px" class="grid-layout"> <mat-grid-tile *ngFor="let product of products; trackBy: trackSkus"> <mat-card> <img mat-card-image src="{{product.imageUrl}}" alt="product photo"> <mat-card-content> <mat-card-title matTooltip="{{product.name}}">{{product.name |wordWrap:35}}</mat-card-title> <mat-card-subtitle>{{product.prices[0].compareAtAmountFloat | currency:'EUR'}}</mat-card-subtitle> </mat-card-content> <mat-card-actions> <button mat-flat-button color="primary" [routerLink]="['/product', product.id]"> View </button> </mat-card-actions> </mat-card> </mat-grid-tile> </mat-grid-list> <mat-paginator [length]="length" [pageIndex]="pageIndex" [pageSize]="pageSize" [pageSizeOptions]="pageSizeOptions" (page)="pageEvent = getNextPage($event)"> </mat-paginator>
Die Seite wird so aussehen.
Produktkomponente
Sobald ein Produkt auf der Produktlistenseite ausgewählt wurde, zeigt diese Komponente seine Details an. Dazu gehören der vollständige Name, der Preis und die Beschreibung des Produkts. Es gibt auch eine Schaltfläche, um den Artikel in den Warenkorb zu legen.
Bei der Initialisierung erhalten wir die ID des Produkts aus den Routenparametern. Mit der ID holen wir das Produkt aus dem SkuService
.
Wenn der Benutzer dem Warenkorb einen Artikel hinzufügt, wird die addItemToCart
Methode aufgerufen. Darin prüfen wir, ob für den Warenkorb bereits eine Bestellung erstellt wurde. Wenn nicht, wird eine neue über den OrderService
. Danach wird eine Position in der Reihenfolge erstellt, die dem Produkt entspricht. Wenn für den Warenkorb bereits eine Bestellung vorhanden ist, wird nur die Einzelposition erstellt. Abhängig vom Status der Anfragen wird dem Benutzer eine Snackbar-Nachricht angezeigt.
@UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-product', templateUrl: './product.component.html', styleUrls: ['./product.component.css'] }) export class ProductComponent implements OnInit { id: string = ''; product!: Sku; quantity: number = 0; constructor( private route: ActivatedRoute, private skus: SkuService, private location: Location, private router: Router, private header: HeaderService, private orders: OrderService, private lineItems: LineItemService, private cart: CartService, private snackBar: MatSnackBar ) { } ngOnInit() { this.route.paramMap .pipe( mergeMap(params => { const id = params.get('id') this.id = id ? id : ''; return this.skus.getSku(this.id); }), tap((sku) => { this.product = sku; }) ).subscribe({ error: (err) => this.router.navigateByUrl('/error') }); this.header.setHeaderButtonsVisibility(true); } addItemToCart() { if (this.quantity > 0) { if (this.cart.orderId == '') { this.orders.createOrder() .pipe( mergeMap((order: Order) => { this.cart.orderId = order.id || ''; return this.lineItems.createLineItem({ orderId: order.id, name: this.product.name, imageUrl: this.product.imageUrl, quantity: this.quantity, skuCode: this.product.code }); }) ) .subscribe( () => { this.cart.incrementItemCount(this.quantity); this.showSuccessSnackBar(); }, err => this.showErrorSnackBar() ); } else { this.lineItems.createLineItem({ orderId: this.cart.orderId, name: this.product.name, imageUrl: this.product.imageUrl, quantity: this.quantity, skuCode: this.product.code }).subscribe( () => { this.cart.incrementItemCount(this.quantity); this.showSuccessSnackBar(); }, err => this.showErrorSnackBar() ); } } else { this.snackBar.open('Select a quantity greater than 0.', 'Close', { duration: 8000 }); } } setQuantity(no: number) { this.quantity = no; } goBack() { this.location.back(); } private showSuccessSnackBar() { this.snackBar.open('Item successfully added to cart.', 'Close', { duration: 8000 }); } private showErrorSnackBar() { this.snackBar.open('Failed to add your item to the cart.', 'Close', { duration: 8000 }); } }
Die ProductComponent
Vorlage sieht wie folgt aus und ihr Stil ist hier verlinkt.
<div> <mat-card *ngIf="product" class="product-card"> <img mat-card-image src="{{product.imageUrl}}" alt="Photo of a product"> <mat-card-content> <mat-card-title>{{product.name}}</mat-card-title> <mat-card-subtitle>{{product.prices[0].compareAtAmountFloat | currency:'EUR'}}</mat-card-subtitle> <p> {{product.description}} </p> </mat-card-content> <mat-card-actions> <app-item-quantity [quantity]="quantity" [maxValue]="10" (setQuantityEvent)="setQuantity($event)"></app-item-quantity> <button mat-raised-button color="accent" (click)="addItemToCart()"> <mat-icon>add_shopping_cart</mat-icon> Add to cart </button> <button mat-raised-button color="primary" (click)="goBack()"> <mat-icon>storefront</mat-icon> Continue shopping </button> </mat-card-actions> </mat-card> </div>
Die Seite wird so aussehen.
Auth-Modul
Das Auth-Modul enthält Seiten, die für die Authentifizierung verantwortlich sind. Dazu gehören die Anmelde- und Anmeldeseiten. Es ist wie folgt aufgebaut.
src/app/features/auth/ ├── auth.module.ts └── pages ├── login │ ├── login.component.css │ ├── login.component.html │ └── login.component.ts └── signup ├── signup.component.css ├── signup.component.html └── signup.component.ts
Um es und seine Komponenten zu generieren:
ng gm features/auth ng gc features/auth/pages/signup ng gc features/auth/pages/login
Dies ist seine Moduldatei.
@NgModule({ declarations: [LoginComponent, SignupComponent], imports: [ RouterModule.forChild([ { path: 'login', component: LoginComponent }, { path: 'signup', component: SignupComponent } ]), MatFormFieldModule, MatInputModule, ReactiveFormsModule, SharedModule ] }) export class AuthModule { }
Anmeldekomponente
Ein Benutzer registriert sich mit dieser Komponente für ein Konto. Für den Vorgang sind Vorname, Nachname, E-Mail und Passwort erforderlich. Der Benutzer muss außerdem sein Passwort bestätigen. Die Eingabefelder werden mit dem FormBuilder
-Dienst erstellt. Die Validierung wird hinzugefügt, um zu verlangen, dass alle Eingaben Werte haben. Dem Kennwortfeld wird eine zusätzliche Validierung hinzugefügt, um eine Mindestlänge von acht Zeichen sicherzustellen. Ein benutzerdefinierter matchPasswords
Validator stellt sicher, dass das bestätigte Passwort mit dem ursprünglichen Passwort übereinstimmt.
Wenn die Komponente initialisiert wird, werden die Warenkorb-, Login- und Logout-Buttons im Header ausgeblendet. Dies wird dem Header über den HeaderService
.
Nachdem alle Felder als gültig markiert wurden, kann sich der Benutzer anmelden. In der Anmeldemethode createCustomer
signup
des CustomerService
diese Eingabe. Wenn die Anmeldung erfolgreich ist, wird der Benutzer darüber informiert, dass sein Konto mithilfe einer Snackbar erfolgreich erstellt wurde. Sie werden dann auf die Startseite umgeleitet.
@UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-signup', templateUrl: './signup.component.html', styleUrls: ['./signup.component.css'] }) export class SignupComponent implements OnInit { signupForm = this.fb.group({ firstName: ['', Validators.required], lastName: ['', Validators.required], email: ['', [Validators.required, Validators.email]], password: ['', [Validators.required, Validators.minLength(8)]], confirmedPassword: ['', [Validators.required]] }, { validators: this.matchPasswords }); @ViewChild(FormGroupDirective) sufDirective: FormGroupDirective | undefined; constructor( private customer: CustomerService, private fb: FormBuilder, private snackBar: MatSnackBar, private router: Router, private header: HeaderService ) { } ngOnInit() { this.header.setHeaderButtonsVisibility(false); } matchPasswords(signupGroup: AbstractControl): ValidationErrors | null { const password = signupGroup.get('password')?.value; const confirmedPassword = signupGroup.get('confirmedPassword')?.value; return password == confirmedPassword ? null : { differentPasswords: true }; } get password() { return this.signupForm.get('password'); } get confirmedPassword() { return this.signupForm.get('confirmedPassword'); } signup() { const customer = this.signupForm.value; this.customer.createCustomer( customer.email, customer.password, customer.firstName, customer.lastName ).subscribe( () => { this.signupForm.reset(); this.sufDirective?.resetForm(); this.snackBar.open('Account successfully created. You will be redirected in 5 seconds.', 'Close', { duration: 5000 }); setTimeout(() => this.router.navigateByUrl('/'), 6000); }, err => this.snackBar.open('There was a problem creating your account.', 'Close', { duration: 5000 }) ); } }
Unten ist die Vorlage für die SignupComponent
.
<form [formGroup]="signupForm" (ngSubmit)="signup()"> <h1 class="mat-display-3">Create Account</h1> <mat-form-field appearance="outline"> <mat-label>First Name</mat-label> <input matInput formControlName="firstName"> <mat-icon matPrefix>portrait</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Last Name</mat-label> <input matInput formControlName="lastName"> <mat-icon matPrefix>portrait</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Email</mat-label> <input matInput formControlName="email" type="email"> <mat-icon matPrefix>alternate_email</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Password</mat-label> <input matInput formControlName="password" type="password"> <mat-icon matPrefix>vpn_key</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Confirm Password</mat-label> <input matInput formControlName="confirmedPassword" type="password"> <mat-icon matPrefix>vpn_key</mat-icon> </mat-form-field> <div *ngIf="confirmedPassword?.invalid && (confirmedPassword?.dirty || confirmedPassword?.touched)"> <mat-error *ngIf="signupForm.hasError('differentPasswords')"> Your passwords do not match. </mat-error> </div> <div *ngIf="password?.invalid && (password?.dirty || password?.touched)"> <mat-error *ngIf="password?.hasError('minlength')"> Your password should be at least 8 characters. </mat-error> </div> <button mat-flat-button color="primary" [disabled]="!signupForm.valid">Sign Up</button> </form>
Die Komponente wird wie folgt aussehen.
Login-Komponente
Ein registrierter Benutzer meldet sich mit dieser Komponente in seinem Konto an. Eine E-Mail und ein Passwort müssen eingegeben werden. Ihre entsprechenden Eingabefelder hätten eine Validierung, die sie erforderlich macht.
Ähnlich wie bei der SignupComponent
sind die Warenkorb-, Login- und Logout-Buttons in der Kopfzeile ausgeblendet. Ihre Sichtbarkeit wird mit dem HeaderService
während der Komponenteninitialisierung eingestellt.
Zur Anmeldung werden die Zugangsdaten an den AuthenticationService
übergeben. Bei Erfolg wird der Anmeldestatus des Benutzers über den SessionService
. Der Benutzer wird dann zurück zu der Seite geleitet, auf der er sich befand. Wenn nicht erfolgreich, wird eine Snackbar mit einem Fehler angezeigt und das Passwortfeld wird zurückgesetzt.
@UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.css'] }) export class LoginComponent implements OnInit { loginForm = this.fb.group({ email: ['', Validators.required], password: ['', Validators.required] }); constructor( private authService: AuthenticationService, private session: SessionService, private snackBar: MatSnackBar, private fb: FormBuilder, private header: HeaderService, private location: Location ) { } ngOnInit() { this.header.setHeaderButtonsVisibility(false); } login() { const credentials = this.loginForm.value; this.authService.login( credentials.email, credentials.password ).subscribe( () => { this.session.setLoggedInStatus(true); this.location.back(); }, err => { this.snackBar.open( 'Login failed. Check your login credentials.', 'Close', { duration: 6000 }); this.loginForm.patchValue({ password: '' }); } ); } }
Unten ist die LoginComponent
Vorlage.
<form [formGroup]="loginForm" (ngSubmit)="login()"> <h1 class="mat-display-3">Login</h1> <mat-form-field appearance="outline"> <mat-label>Email</mat-label> <input matInput type="email" formControlName="email" required> <mat-icon matPrefix>alternate_email</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Password</mat-label> <input matInput type="password" formControlName="password" required> <mat-icon matPrefix>vpn_key</mat-icon> </mat-form-field> <button mat-flat-button color="primary" [disabled]="!loginForm.valid">Login</button> <p class="mat-h3">Not registered yet? <a routerLink="/signup">Create an account.</a></p> </form>
Hier ist ein Screenshot der Seite.
Cart-Modul
Das Warenkorbmodul enthält alle Seiten, die sich auf den Warenkorb beziehen. Dazu gehören die Bestellzusammenfassungsseite, eine Gutschein- und Geschenkkarten-Codeseite und eine leere Einkaufswagenseite. Es ist wie folgt aufgebaut.
src/app/features/cart/ ├── cart.module.ts └── pages ├── codes │ ├── codes.component.css │ ├── codes.component.html │ └── codes.component.ts ├── empty │ ├── empty.component.css │ ├── empty.component.html │ └── empty.component.ts └── summary ├── summary.component.css ├── summary.component.html └── summary.component.ts
Um es zu generieren, führen Sie Folgendes aus:
ng gm features/cart ng gc features/cart/codes ng gc features/cart/empty ng gc features/cart/summary
Dies ist die Moduldatei.
@NgModule({ declarations: [SummaryComponent, CodesComponent, EmptyComponent], imports: [ RouterModule.forChild([ { path: '', canActivate: [EmptyCartGuard], children: [ { path: 'cart', component: SummaryComponent }, { path: 'codes', component: CodesComponent } ] }, { path: 'empty', component: EmptyComponent } ]), MatDividerModule, MatFormFieldModule, MatInputModule, MatMenuModule, ReactiveFormsModule, SharedModule ] }) export class CartModule { }
Codes-Komponente
Wie bereits erwähnt, wird diese Komponente verwendet, um Gutschein- oder Geschenkkartencodes zu einer Bestellung hinzuzufügen. Auf diese Weise kann der Benutzer Rabatte auf den Gesamtbetrag seiner Bestellung anwenden, bevor er zur Kasse geht.
Es werden zwei Eingabefelder angezeigt. Eine für Coupons und eine für Geschenkkartencodes.
Die Codes werden durch Aktualisieren der Bestellung hinzugefügt. Die Methode updateOrder
des OrderService
aktualisiert die Bestellung mit den Codes. Danach werden beide Felder zurückgesetzt und der Benutzer wird mit einer Snackbar über den Erfolg der Operation informiert. Eine Snackbar wird auch angezeigt, wenn ein Fehler auftritt. Sowohl die addCoupon
als auch die addGiftCard
Methode rufen die updateOrder
Methode auf.
@UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-codes', templateUrl: './codes.component.html', styleUrls: ['./codes.component.css'] }) export class CodesComponent { couponCode = new FormControl(''); giftCardCode = new FormControl(''); @ViewChild(FormControlDirective) codesDirective: FormControlDirective | undefined; constructor( private cart: CartService, private order: OrderService, private snackBar: MatSnackBar ) { } private updateOrder(order: Order, params: UpdateOrderParams[], codeType: string) { this.order.updateOrder(order, params) .subscribe( () => { this.snackBar.open(`Successfully added ${codeType} code.`, 'Close', { duration: 8000 }); this.couponCode.reset(); this.giftCardCode.reset(); this.codesDirective?.reset(); }, err => this.snackBar.open(`There was a problem adding your ${codeType} code.`, 'Close', { duration: 8000 }) ); } addCoupon() { this.updateOrder({ id: this.cart.orderId, couponCode: this.couponCode.value }, [UpdateOrderParams.couponCode], 'coupon'); } addGiftCard() { this.updateOrder({ id: this.cart.orderId, giftCardCode: this.giftCardCode.value }, [UpdateOrderParams.giftCardCode], 'gift card'); } }
Die Vorlage wird unten gezeigt und ihr Design kann unter diesem Link gefunden werden.
<div> <app-title title="Redeem a code" subtitle="Enter a coupon code or gift card" [centerText]="true"></app-title> <div class="input-row"> <mat-form-field appearance="outline"> <mat-label>Coupon Code</mat-label> <input matInput [formControl]="couponCode" required> <mat-icon matPrefix>card_giftcard</mat-icon> </mat-form-field> <button class="redeem" mat-flat-button color="accent" [disabled]="couponCode.invalid" (click)="addCoupon()">Redeem</button> </div> <div class="input-row"> <mat-form-field appearance="outline"> <mat-label>Gift Card Code</mat-label> <input matInput [formControl]="giftCardCode" required> <mat-icon matPrefix>redeem</mat-icon> </mat-form-field> <button class="redeem" mat-flat-button color="accent" [disabled]="giftCardCode.invalid" (click)="addGiftCard()">Redeem</button> </div> <button color="primary" mat-flat-button routerLink="/cart"> <mat-icon>shopping_cart</mat-icon> CONTINUE TO CART </button> </div>
Hier ist ein Screenshot der Seite.
Leere Komponente
Es sollte nicht möglich sein, mit leerem Warenkorb auszuchecken. Es muss einen Schutz geben, der verhindert, dass Benutzer mit leeren Warenkörben auf die Seiten des Checkout-Moduls zugreifen. Dies wurde bereits im Rahmen des CoreModule
. Der Wächter leitet Anfragen zu Checkout-Seiten mit leerem Warenkorb an die EmptyCartComponent
.
Es ist eine sehr einfache Komponente, die einen Text enthält, der dem Benutzer anzeigt, dass sein Einkaufswagen leer ist. Es hat auch eine Schaltfläche, auf die der Benutzer klicken kann, um zur Homepage zu gehen und Dinge in seinen Warenkorb zu legen. Also verwenden wir die SimplePageComponent
, um es anzuzeigen. Hier ist die Vorlage.
<app-simple-page title="Your cart is empty" subtitle="There is currently nothing in your cart. Head to the home page to add items." buttonText="GO TO HOME PAGE" icon="shopping_basket" [centerText]="true" route="/"> </app-simple-page>
Hier ist ein Screenshot der Seite.
Zusammenfassungskomponente
Diese Komponente fasst den Warenkorb/die Bestellung zusammen. Es listet alle Artikel im Warenkorb, ihre Namen, Mengen und Bilder auf. Außerdem werden die Kosten der Bestellung einschließlich Steuern, Versand und Rabatten aufgeschlüsselt. Der Benutzer sollte in der Lage sein, diese anzuzeigen und zu entscheiden, ob er mit den Artikeln und Kosten zufrieden ist, bevor er zur Kasse geht.
Bei der Initialisierung werden die Bestellung und ihre Einzelposten mithilfe von OrderService
. Ein Benutzer sollte in der Lage sein, die Einzelposten zu ändern oder sie sogar aus der Bestellung zu entfernen. Elemente werden entfernt, wenn die Methode deleteLineItem
aufgerufen wird. Darin erhält die Methode deleteLineItem
des LineItemService
die ID der zu löschenden Position. Wenn eine Löschung erfolgreich ist, aktualisieren wir die Artikelanzahl im Warenkorb mithilfe des CartService
.
Der Benutzer wird dann zur Kundenseite geleitet, wo er mit dem Bezahlvorgang beginnt. Die checkout
Methode übernimmt das Routing.
@UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-summary', templateUrl: './summary.component.html', styleUrls: ['./summary.component.css'] }) export class SummaryComponent implements OnInit { order: Order = {}; summary: { name: string, amount: string | undefined, id: string }[] = []; constructor( private orders: OrderService, private lineItems: LineItemService, private cart: CartService, private snackBar: MatSnackBar, private router: Router ) { } ngOnInit() { this.orders.getOrder(this.cart.orderId, GetOrderParams.cart) .subscribe( order => this.processOrder(order), err => this.showOrderError('retrieving your cart') ); } private processOrder(order: Order) { this.order = order; this.summary = [ { name: 'Subtotal', amount: order.formattedSubtotalAmount, id: 'subtotal' }, { name: 'Discount', amount: order.formattedDiscountAmount, id: 'discount' }, { name: 'Taxes (included)', amount: order.formattedTotalTaxAmount, id: 'taxes' }, { name: 'Shipping', amount: order.formattedShippingAmount, id: 'shipping' }, { name: 'Gift Card', amount: order.formattedGiftCardAmount, id: 'gift-card' } ]; } private showOrderError(msg: string) { this.snackBar.open(`There was a problem ${msg}.`, 'Close', { duration: 8000 }); } checkout() { this.router.navigateByUrl('/customer'); } deleteLineItem(id: string) { this.lineItems.deleteLineItem(id) .pipe( mergeMap(() => this.orders.getOrder(this.cart.orderId, GetOrderParams.cart)) ).subscribe( order => { this.processOrder(order); this.cart.itemCount = order.skusCount || this.cart.itemCount; this.snackBar.open(`Item successfully removed from cart.`, 'Close', { duration: 8000 }) }, err => this.showOrderError('deleting your order') ); } }
Unten ist die Vorlage und ihr Styling ist hier verlinkt.
<div class="container" *ngIf="order"> <h3>Order #{{order.number}} ({{order.skusCount}} items)</h3> <div class="line-item" *ngFor="let item of order.lineItems"> <div> <img *ngIf="item.imageUrl" class="image-xs" src="{{item.imageUrl}}" alt="product photo"> <div *ngIf="!item.imageUrl" class="image-xs no-image"></div> <div> <div>{{item.name}}</div> <div> {{item.formattedUnitAmount }} </div> </div> </div> <div> <app-item-quantity [quantity]="item.quantity || 0" [disabled]="true"></app-item-quantity> <div class="itemTotal"> {{item.formattedTotalAmount }} </div> <button mat-icon-button color="warn" (click)="deleteLineItem(item.id || '')"> <mat-icon>clear</mat-icon> </button> </div> </div> <mat-divider></mat-divider> <div class="costSummary"> <div class="costItem" *ngFor="let item of summary" [id]="item.id"> <h3 class="costLabel">{{item.name}}</h3> <p> {{item.amount }} </p> </div> </div> <mat-divider></mat-divider> <div class="costSummary"> <div class="costItem"> <h2>Total</h2> <h2> {{order.formattedTotalAmountWithTaxes}} </h2> </div> </div> <div> <button color="accent" mat-flat-button routerLink="/codes"> <mat-icon>redeem</mat-icon> ADD GIFT CARD/COUPON </button> <button color="primary" mat-flat-button (click)="checkout()"> <mat-icon>point_of_sale</mat-icon> CHECKOUT </button> </div> </div>
Hier ist ein Screenshot der Seite.
Checkout-Modul
Dieses Modul ist für den Checkout-Prozess verantwortlich. Zur Kasse gehört die Angabe einer Rechnungs- und Lieferadresse, einer Kunden-E-Mail-Adresse und die Auswahl einer Versand- und Zahlungsmethode. Der letzte Schritt dieses Prozesses ist die Auftragserteilung und Auftragsbestätigung. Der Aufbau des Moduls ist wie folgt.
src/app/features/checkout/ ├── components │ ├── address │ ├── address-list │ └── country-select └── pages ├── billing-address ├── cancel-payment ├── customer ├── payment ├── place-order ├── shipping-address └── shipping-methods
Dieses Modul ist mit Abstand das größte und enthält 3 Komponenten und 7 Seiten. Um es und seine Komponenten zu generieren, führen Sie Folgendes aus:
ng gm features/checkout for comp in \ address address-list country-select; do \ ng gc "features/checkout/components/${comp}" \ ; done for page in \ billing-address cancel-payment customer \ payment place-order shipping-address \ shipping-methods; do \ ng gc "features/checkout/pages/${page}"; done
Dies ist die Moduldatei.
@NgModule({ declarations: [ CustomerComponent, AddressComponent, BillingAddressComponent, ShippingAddressComponent, ShippingMethodsComponent, PaymentComponent, PlaceOrderComponent, AddressListComponent, CountrySelectComponent, CancelPaymentComponent ], imports: [ RouterModule.forChild([ { path: '', canActivate: [EmptyCartGuard], children: [ { path: 'billing-address', component: BillingAddressComponent }, { path: 'cancel-payment', component: CancelPaymentComponent }, { path: 'customer', component: CustomerComponent }, { path: 'payment', component: PaymentComponent }, { path: 'place-order', component: PlaceOrderComponent }, { path: 'shipping-address', component: ShippingAddressComponent }, { path: 'shipping-methods', component: ShippingMethodsComponent } ] } ]), MatCardModule, MatCheckboxModule, MatDividerModule, MatInputModule, MatMenuModule, MatRadioModule, ReactiveFormsModule, SharedModule ] }) export class CheckoutModule { }
Komponenten
Country Select-Komponente
Mit dieser Komponente kann ein Benutzer ein Land als Teil einer Adresse auswählen. Die Materialauswahl-Komponente sieht im Vergleich zu den Eingabefeldern im Adressformular ziemlich anders aus. Aus Gründen der Einheitlichkeit wird stattdessen eine Materialmenükomponente verwendet.
Wenn die Komponente initialisiert wird, werden die Ländercodedaten mit dem CountryService
abgerufen. Die Eigenschaft countries
enthält die vom Dienst zurückgegebenen Werte. Diese Werte werden dem Menü in der Vorlage hinzugefügt.
Die Komponente hat eine Ausgabeeigenschaft, setCountryEvent
. Wenn ein Land ausgewählt wird, gibt dieses Ereignis den Alpha-2-Code des Landes aus.
@UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-country-select', templateUrl: './country-select.component.html', styleUrls: ['./country-select.component.css'] }) export class CountrySelectComponent implements OnInit { country: string = 'Country'; countries: Country[] = []; @Output() setCountryEvent = new EventEmitter<string>(); constructor(private countries: CountryService) { } ngOnInit() { this.countries.getCountries() .subscribe( countries => { this.countries = countries; } ); } setCountry(value: Country) { this.country = value.name || ''; this.setCountryEvent.emit(value.code); }}
Unten ist die Vorlage und hier ist das Styling verlinkt.
<button mat-stroked-button [matMenuTriggerFor]="countryMenu"> {{country}} <mat-icon>expand_more</mat-icon> </button> <mat-menu #countryMenu="matMenu"> <button *ngFor="let cnt of countries" (click)="setCountry(cnt)" mat-menu-item>{{cnt.name}}</button> </mat-menu>
Adresskomponente
Dies ist ein Formular zum Erfassen von Adressen. Es wird sowohl von den Versand- als auch von den Rechnungsadressenseiten verwendet. Eine gültige Commerce-Layer-Adresse sollte einen Vor- und Nachnamen, eine Adresszeile, eine Stadt, eine Postleitzahl, einen Bundesstaatscode, einen Ländercode und eine Telefonnummer enthalten.
Der FormBuilder
-Dienst erstellt die Formulargruppe. Da diese Komponente von mehreren Seiten verwendet wird, hat sie eine Reihe von Eingabe- und Ausgabeeigenschaften. Die Eingabeeigenschaften umfassen den Schaltflächentext, den angezeigten Titel und den Text für ein Kontrollkästchen. Die Ausgabeeigenschaften sind Ereignisemitter, wenn auf die Schaltfläche geklickt wird, um die Adresse zu erstellen, und andere, wenn sich der Wert des Kontrollkästchens ändert.
Wenn auf die Schaltfläche geklickt wird, wird die addAddress
Methode aufgerufen und das createAddress
Ereignis gibt die vollständige Adresse aus. Wenn das Kontrollkästchen aktiviert ist, gibt das isCheckboxChecked
Ereignis den Kontrollkästchenwert aus.
@Component({ selector: 'app-address', templateUrl: './address.component.html', styleUrls: ['./address.component.css'] }) export class AddressComponent { @Input() buttonText: string = ''; @Input() showTitle?: boolean = false; @Output() createAddress = new EventEmitter<Address>(); @Input() checkboxText: string = ''; @Output() isCheckboxChecked = new EventEmitter<boolean>(); countryCode: string = ''; addressForm = this.fb.group({ firstName: [''], lastName: [''], line1: [''], city: [''], zipCode: [''], stateCode: [''], phone: [''] }); @ViewChild(FormGroupDirective) afDirective: FormGroupDirective | undefined; constructor(private fb: FormBuilder) { } setCountryCode(code: string) { this.countryCode = code; } addAddress() { this.createAddress.emit({ firstName: this.addressForm.get('firstName')?.value, lastName: this.addressForm.get('lastName')?.value, line1: this.addressForm.get('line1')?.value, city: this.addressForm.get('city')?.value, zipCode: this.addressForm.get('zipCode')?.value, stateCode: this.addressForm.get('stateCode')?.value || 'N/A', countryCode: this.countryCode, phone: this.addressForm.get('phone')?.value }); } setCheckboxValue(change: MatCheckboxChange) { if (this.isCheckboxChecked) { this.isCheckboxChecked.emit(change.checked); } } }
This is its template and its styling is linked here.
<form [formGroup]="addressForm"> <p class="mat-headline" *ngIf="showTitle">Or add a new address</p> <div class="row"> <mat-form-field appearance="outline"> <mat-label>First Name</mat-label> <input matInput formControlName="firstName"> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Last Name</mat-label> <input matInput formControlName="lastName"> </mat-form-field> </div> <div class="row"> <mat-form-field appearance="outline"> <mat-label>Address</mat-label> <input matInput formControlName="line1"> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>City</mat-label> <input matInput formControlName="city"> </mat-form-field> </div> <div class="row"> <mat-form-field appearance="outline"> <mat-label>State Code</mat-label> <input matInput formControlName="stateCode"> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Zip Code</mat-label> <input matInput formControlName="zipCode"> </mat-form-field> </div> <div class="row"> <mat-form-field appearance="outline"> <mat-label>Phone</mat-label> <input matInput formControlName="phone"> </mat-form-field> <app-country-select (setCountryEvent)="setCountryCode($event)"></app-country-select> </div> <mat-checkbox color="accent" (change)="setCheckboxValue($event)"> {{checkboxText}} </mat-checkbox> <button mat-flat-button color="primary" (click)="addAddress()"> {{buttonText}} </button> </form>
Address List Component
When a customer logs in, they can access their existing addresses. Instead of having them re-enter an address, they can pick from an address list. This is the purpose of this component. On initialization, all the customer's addresses are fetched using the CustomerAddressService
if they are logged in. We will check their login status using the SessionService
.
This component has a setAddressEvent
output property. When an address is selected, setAddressEvent
emits its id to the parent component.
@Component({ selector: 'app-address-list', templateUrl: './address-list.component.html', styleUrls: ['./address-list.component.css'] }) export class AddressListComponent implements OnInit { addresses: CustomerAddress[] = []; @Output() setAddressEvent = new EventEmitter<string>(); constructor( private session: SessionService, private customerAddresses: CustomerAddressService, private snackBar: MatSnackBar ) { } ngOnInit() { this.session.loggedInStatus .pipe( mergeMap( status => iif(() => status, this.customerAddresses.getCustomerAddresses()) )) .subscribe( addresses => { if (addresses.length) { this.addresses = addresses } }, err => this.snackBar.open('There was a problem getting your existing addresses.', 'Close', { duration: 8000 }) ); } setAddress(change: MatRadioChange) { this.setAddressEvent.emit(change.value); } }
Hier ist seine Vorlage. Das Styling finden Sie hier.
<div> <p class="mat-headline">Pick an existing address</p> <mat-error *ngIf="!addresses.length">You have no existing addresses</mat-error> <mat-radio-group *ngIf="addresses.length" class="addresses" (change)="setAddress($event)"> <mat-card class="address" *ngFor="let address of addresses"> <mat-radio-button [value]="address.address?.id" color="primary"> <p>{{address.address?.firstName}} {{address.address?.lastName}},</p> <p>{{address.address?.line1}},</p> <p>{{address.address?.city}},</p> <p>{{address.address?.zipCode}},</p> <p>{{address.address?.stateCode}}, {{address.address?.countryCode}}</p> <p>{{address.address?.phone}}</p> </mat-radio-button> </mat-card> </mat-radio-group> </div>
Seiten
Kundenkomponente
Eine Bestellung muss mit einer E-Mail-Adresse verknüpft sein. Diese Komponente ist ein Formular, das die E-Mail-Adresse des Kunden erfasst. Bei der Initialisierung der Komponente wird die E-Mail-Adresse des aktuellen Kunden abgerufen, sofern dieser angemeldet ist. Den Kunden erhalten wir vom CustomerService
. Wenn sie ihre E-Mail-Adresse nicht ändern möchten, ist diese E-Mail der Standardwert.
Wenn die E-Mail geändert wird oder ein Kunde nicht angemeldet ist, wird die Bestellung mit der eingegebenen E-Mail aktualisiert. Wir verwenden den OrderService
, um die Bestellung mit der neuen E-Mail-Adresse zu aktualisieren. Bei Erfolg leiten wir den Kunden zur Seite mit der Rechnungsadresse weiter.
@UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-customer', templateUrl: './customer.component.html', styleUrls: ['./customer.component.css'] }) export class CustomerComponent implements OnInit { email = new FormControl('', [Validators.required, Validators.email]); constructor( private orders: OrderService, private customers: CustomerService, private cart: CartService, private router: Router, private snackBar: MatSnackBar ) { } ngOnInit() { this.customers.getCurrentCustomer() .subscribe( customer => this.email.setValue(customer.email) ); } addCustomerEmail() { this.orders.updateOrder( { id: this.cart.orderId, customerEmail: this.email.value }, [UpdateOrderParams.customerEmail]) .subscribe( () => this.router.navigateByUrl('/billing-address'), err => this.snackBar.open('There was a problem adding your email to the order.', 'Close', { duration: 8000 }) ); } }
Hier ist die Komponentenvorlage und hier verlinkt ist ihr Styling.
<div> <app-title no="1" title="Customer" subtitle="Billing information and shipping address"></app-title> <mat-form-field appearance="outline"> <mat-label>Email</mat-label> <input matInput [formControl]="email" required> <mat-icon matPrefix>alternate_email</mat-icon> </mat-form-field> <button mat-flat-button color="primary" [disabled]="email.invalid" (click)="addCustomerEmail()"> PROCEED TO BILLING ADDRESS </button> </div>
Hier ist ein Screenshot der Kundenseite.
Komponente der Rechnungsadresse
Die Rechnungsadressenkomponente ermöglicht es einem Kunden, entweder eine neue Rechnungsadresse hinzuzufügen oder aus seinen bestehenden Adressen auszuwählen. Nicht eingeloggte Benutzer müssen eine neue Adresse eingeben. Diejenigen, die sich angemeldet haben, haben die Möglichkeit, zwischen neuen oder bestehenden Adressen zu wählen.
Die Eigenschaft showAddress
gibt an, ob vorhandene Adressen auf der Komponente angezeigt werden sollen. sameShippingAddressAsBilling
gibt an, ob die Lieferadresse mit der Rechnungsadresse übereinstimmen soll. Wenn ein Kunde eine vorhandene Adresse auswählt, wird seine ID selectedCustomerAddressId
zugewiesen.
Wenn die Komponente initialisiert wird, verwenden wir den SessionService
, um zu prüfen, ob der aktuelle Benutzer angemeldet ist. Wenn er angemeldet ist, zeigen wir seine bestehenden Adressen an, sofern vorhanden.
Wie bereits erwähnt, kann ein angemeldeter Benutzer eine vorhandene Adresse als Rechnungsadresse auswählen. Bei der updateBillingAddress
Methode wird die ausgewählte Adresse geklont und als Rechnungsadresse der Bestellung festgelegt, wenn sie angemeldet sind. Wir tun dies, indem wir die Bestellung mit der updateOrder
Methode des OrderService
und die Adress-ID bereitstellen.
Ist er nicht eingeloggt, muss der Nutzer eine Adresse angeben. Nach der Bereitstellung wird die Adresse mit der createAddress
Methode erstellt. Darin übernimmt der AddressService
die Eingabe und stellt die neue Adresse her. Danach wird die Bestellung mit der ID der neu erstellten Adresse aktualisiert. Wenn ein Fehler auftritt oder eine Operation erfolgreich ist, zeigen wir eine Snackbar.
Wenn dieselbe Adresse als Lieferadresse ausgewählt wird, wird der Benutzer auf die Seite mit den Versandmethoden geleitet. Wenn sie eine alternative Lieferadresse angeben möchten, werden sie auf die Seite mit den Lieferadressen weitergeleitet.
@UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-billing-address', templateUrl: './billing-address.component.html', styleUrls: ['./billing-address.component.css'] }) export class BillingAddressComponent implements OnInit { showAddresses: boolean = false; sameShippingAddressAsBilling: boolean = false; selectedCustomerAddressId: string = ''; constructor( private addresses: AddressService, private snackBar: MatSnackBar, private session: SessionService, private orders: OrderService, private cart: CartService, private router: Router, private customerAddresses: CustomerAddressService) { } ngOnInit() { this.session.loggedInStatus .subscribe( status => this.showAddresses = status ); } updateBillingAddress(address: Address) { if (this.showAddresses && this.selectedCustomerAddressId) { this.cloneAddress(); } else if (address.firstName && address.lastName && address.line1 && address.city && address.zipCode && address.stateCode && address.countryCode && address.phone) { this.createAddress(address); } else { this.snackBar.open('Check your address. Some fields are missing.', 'Close'); } } setCustomerAddress(customerAddressId: string) { this.selectedCustomerAddressId = customerAddressId; } setSameShippingAddressAsBilling(change: boolean) { this.sameShippingAddressAsBilling = change; } private createAddress(address: Address) { this.addresses.createAddress(address) .pipe( concatMap( address => { const update = this.updateOrderObservable({ id: this.cart.orderId, billingAddressId: address.id }, [UpdateOrderParams.billingAddress]); if (this.showAddresses) { return combineLatest([update, this.customerAddresses.createCustomerAddress(address.id || '', '')]); } else { return update; } })) .subscribe( () => this.showSuccessSnackBar(), err => this.showErrorSnackBar() ); } private cloneAddress() { this.updateOrderObservable({ id: this.cart.orderId, billingAddressCloneId: this.selectedCustomerAddressId }, [UpdateOrderParams.billingAddressClone]) .subscribe( () => this.showSuccessSnackBar(), err => this.showErrorSnackBar() ); } private updateOrderObservable(order: Order, updateParams: UpdateOrderParams[]): Observable<any> { return iif(() => this.sameShippingAddressAsBilling, concat([ this.orders.updateOrder(order, updateParams), this.orders.updateOrder(order, [UpdateOrderParams.shippingAddressSameAsBilling]) ]), this.orders.updateOrder(order, updateParams) ); } private showErrorSnackBar() { this.snackBar.open('There was a problem creating your address.', 'Close', { duration: 8000 }); } private navigateTo(path: string) { setTimeout(() => this.router.navigateByUrl(path), 4000); } private showSuccessSnackBar() { this.snackBar.open('Billing address successfully added. Redirecting...', 'Close', { duration: 3000 }); if (this.sameShippingAddressAsBilling) { this.navigateTo('/shipping-methods'); } else { this.navigateTo('/shipping-address'); } } }
Hier ist die Vorlage. Dieser Link weist auf sein Styling hin.
<app-title no="2" title="Billing Address" subtitle="Address to bill charges to"></app-title> <app-address-list *ngIf="showAddresses" (setAddressEvent)="setCustomerAddress($event)"></app-address-list> <mat-divider *ngIf="showAddresses"></mat-divider> <app-address [showTitle]="showAddresses" buttonText="PROCEED TO NEXT STEP" checkboxText="Ship to the same address" (isCheckboxChecked)="setSameShippingAddressAsBilling($event)" (createAddress)="updateBillingAddress($event)"></app-address>
So sieht die Seite mit der Rechnungsadresse aus.
Versandadressenkomponente
Die Versandadressenkomponente verhält sich ähnlich wie die Rechnungsadressenkomponente. Es gibt jedoch ein paar Unterschiede. Zum einen ist der auf der Vorlage angezeigte Text anders. Die anderen Hauptunterschiede bestehen darin, wie die Bestellung mit dem OrderService
aktualisiert wird, sobald eine Adresse erstellt oder ausgewählt wurde. Die Felder, die die Bestellung aktualisiert, sind shippingAddressCloneId
für ausgewählte Adressen und shippingAddress
für neue Adressen. Wenn ein Benutzer die Rechnungsadresse ändert, damit sie mit der Lieferadresse identisch ist, wird das Feld billingAddressSameAsShipping
aktualisiert.
Nachdem eine Lieferadresse ausgewählt und die Bestellung aktualisiert wurde, wird der Benutzer zur Seite mit den Versandmethoden weitergeleitet.
@UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-shipping-address', templateUrl: './shipping-address.component.html', styleUrls: ['./shipping-address.component.css'] }) export class ShippingAddressComponent implements OnInit { showAddresses: boolean = false; sameBillingAddressAsShipping: boolean = false; selectedCustomerAddressId: string = ''; constructor( private addresses: AddressService, private snackBar: MatSnackBar, private session: SessionService, private orders: OrderService, private cart: CartService, private router: Router, private customerAddresses: CustomerAddressService) { } ngOnInit() { this.session.loggedInStatus .subscribe( status => this.showAddresses = status ); } updateShippingAddress(address: Address) { if (this.showAddresses && this.selectedCustomerAddressId) { this.cloneAddress(); } else if (address.firstName && address.lastName && address.line1 && address.city && address.zipCode && address.stateCode && address.countryCode && address.phone) { this.createAddress(address); } else { this.snackBar.open('Check your address. Some fields are missing.', 'Close'); } } setCustomerAddress(customerAddressId: string) { this.selectedCustomerAddressId = customerAddressId; } setSameBillingAddressAsShipping(change: boolean) { this.sameBillingAddressAsShipping = change; } private createAddress(address: Address) { this.addresses.createAddress(address) .pipe( concatMap( address => { const update = this.updateOrderObservable({ id: this.cart.orderId, shippingAddressId: address.id }, [UpdateOrderParams.shippingAddress]); if (this.showAddresses) { return combineLatest([update, this.customerAddresses.createCustomerAddress(address.id || '', '')]); } else { return update; } })) .subscribe( () => this.showSuccessSnackBar(), err => this.showErrorSnackBar() ); } private cloneAddress() { this.updateOrderObservable({ id: this.cart.orderId, shippingAddressCloneId: this.selectedCustomerAddressId }, [UpdateOrderParams.shippingAddressClone]) .subscribe( () => this.showSuccessSnackBar(), err => this.showErrorSnackBar() ); } private updateOrderObservable(order: Order, updateParams: UpdateOrderParams[]): Observable<any> { return iif(() => this.sameBillingAddressAsShipping, concat([ this.orders.updateOrder(order, updateParams), this.orders.updateOrder(order, [UpdateOrderParams.billingAddressSameAsShipping]) ]), this.orders.updateOrder(order, updateParams) ); } private showErrorSnackBar() { this.snackBar.open('There was a problem creating your address.', 'Close', { duration: 8000 }); } private showSuccessSnackBar() { this.snackBar.open('Shipping address successfully added. Redirecting...', 'Close', { duration: 3000 }); setTimeout(() => this.router.navigateByUrl('/shipping-methods'), 4000); } }
Hier ist die Vorlage und ihr Styling finden Sie hier.
<app-title no="3" title="Shipping Address" subtitle="Address to ship package to"></app-title> <app-address-list *ngIf="showAddresses" (setAddressEvent)="setCustomerAddress($event)"></app-address-list> <mat-divider *ngIf="showAddresses"></mat-divider> <app-address [showTitle]="showAddresses" buttonText="PROCEED TO SHIPPING METHODS" checkboxText="Bill to the same address" (isCheckboxChecked)="setSameBillingAddressAsShipping($event)" (createAddress)="updateShippingAddress($event)"></app-address>
Die Seite mit den Lieferadressen sieht folgendermaßen aus.
Versandmethoden-Komponente
Diese Komponente zeigt die Anzahl der Sendungen an, die für die Erfüllung einer Bestellung erforderlich sind, die verfügbaren Versandmethoden und die damit verbundenen Kosten. Der Kunde kann dann für jede Sendung eine Versandart auswählen, die er bevorzugt.
Die shipments
enthält alle Sendungen der Bestellung. Das shipmentsForm
ist das Formular, in dem die Auswahl der Versandmethode getroffen wird.
Wenn die Komponente initialisiert wird, wird die Bestellung abgerufen und enthält sowohl die Einzelposten als auch die Lieferungen. Gleichzeitig erhalten wir die Lieferzeiten für die verschiedenen Versandarten. Wir verwenden den OrderService
, um die Bestellung zu erhalten, und den DeliveryLeadTimeService
für die Vorlaufzeiten. Sobald beide Informationssätze zurückgegeben wurden, werden sie zu einem Array von Sendungen kombiniert und der shipments
zugewiesen. Jede Sendung enthält ihre Artikel, die verfügbaren Versandmethoden und die entsprechenden Kosten.
Nachdem der Benutzer eine Versandmethode für jede Sendung ausgewählt hat, wird die ausgewählte Versandmethode für jede Sendung in setShipmentMethods
aktualisiert. Bei Erfolg wird der Benutzer zur Zahlungsseite weitergeleitet.
@UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-shipping-methods', templateUrl: './shipping-methods.component.html', styleUrls: ['./shipping-methods.component.css'] }) export class ShippingMethodsComponent implements OnInit { shipments: Shipment[] | undefined = []; shipmentsForm: FormGroup = this.fb.group({}); constructor( private orders: OrderService, private dlts: DeliveryLeadTimeService, private cart: CartService, private router: Router, private fb: FormBuilder, private shipments: ShipmentService, private snackBar: MatSnackBar ) { } ngOnInit() { combineLatest([ this.orders.getOrder(this.cart.orderId, GetOrderParams.shipments), this.dlts.getDeliveryLeadTimes() ]).subscribe( ([lineItems, deliveryLeadTimes]) => { let li: LineItem; let lt: DeliveryLeadTime[]; this.shipments = lineItems.shipments?.map((shipment) => { if (shipment.id) { this.shipmentsForm.addControl(shipment.id, new FormControl('', Validators.required)); } if (shipment.lineItems) { shipment.lineItems = shipment.lineItems.map(item => { li = this.findItem(lineItems, item.skuCode || ''); item.imageUrl = li.imageUrl; item.name = li.name; return item; }); } if (shipment.availableShippingMethods) { lt = this.findLocationLeadTime(deliveryLeadTimes, shipment); shipment.availableShippingMethods = shipment.availableShippingMethods?.map( method => { method.deliveryLeadTime = this.findMethodLeadTime(lt, method); return method; }); } return shipment; }); }, err => this.router.navigateByUrl('/error') ); } setShipmentMethods() { const shipmentsFormValue = this.shipmentsForm.value; combineLatest(Object.keys(shipmentsFormValue).map( key => this.shipments.updateShipment(key, shipmentsFormValue[key]) )).subscribe( () => { this.snackBar.open('Your shipments have been updated with a shipping method.', 'Close', { duration: 3000 }); setTimeout(() => this.router.navigateByUrl('/payment'), 4000); }, err => this.snackBar.open('There was a problem adding shipping methods to your shipments.', 'Close', { duration: 5000 }) ); } private findItem(lineItems: LineItem[], skuCode: string): LineItem { return lineItems.filter((item) => item.skuCode == skuCode)[0]; } private findLocationLeadTime(times: DeliveryLeadTime[], shipment: Shipment): DeliveryLeadTime[] { return times.filter((dlTime) => dlTime?.stockLocation?.id == shipment?.stockLocation?.id); } private findMethodLeadTime(times: DeliveryLeadTime[], method: ShippingMethod): DeliveryLeadTime { return times.filter((dlTime) => dlTime?.shippingMethod?.id == method?.id)[0]; } }
Hier ist die Vorlage und Sie finden das Styling unter diesem Link.
<form [formGroup]="shipmentsForm"> <app-title no="4" title="Shipping Methods" subtitle="How to ship your packages"></app-title> <div class="shipment-container" *ngFor="let shipment of shipments; let j = index; let isLast = last"> <h1>Shipment {{j+1}} of {{shipments?.length}}</h1> <div class="row" *ngFor="let item of shipment.lineItems"> <img class="image-xs" [src]="item.imageUrl" alt="product photo"> <div> <h4>{{item.name}}</h4> <p>{{item.skuCode}}</p> </div> <div> <p>Quantity: </p>{{item.quantity}} </div> </div> <mat-radio-group [formControlName]="shipment?.id || j"> <mat-radio-button *ngFor="let method of shipment.availableShippingMethods" [value]="method.id"> <div class="radio-button"> <p>{{method.name}}</p> <div> <p class="radio-label">Cost:</p> <p> {{method.formattedPriceAmount}}</p> </div> <div> <p class="radio-label">Timeline:</p> <p> Available in {{method.deliveryLeadTime?.minDays}}-{{method.deliveryLeadTime?.maxDays}} days</p> </div> </div> </mat-radio-button> </mat-radio-group> <mat-divider *ngIf="!isLast"></mat-divider> </div> <button mat-flat-button color="primary" [disabled]="shipmentsForm.invalid" (click)="setShipmentMethods()">PROCEED TO PAYMENT</button> </form>
Dies ist ein Screenshot der Seite mit den Versandarten.
Zahlungskomponente
In dieser Komponente klickt der Benutzer auf die Zahlungsschaltfläche, wenn er mit der Bezahlung seiner Bestellung mit Paypal fortfahren möchte. Die approvalUrl
ist der Paypal-Link, zu dem der Benutzer weitergeleitet wird, wenn er auf die Schaltfläche klickt.
Während der Initialisierung erhalten wir die Bestellung mit der enthaltenen Zahlungsquelle über den OrderService
. Wenn eine Zahlungsquelle festgelegt ist, erhalten wir ihre ID und rufen die entsprechende Paypal-Zahlung vom PaypalPaymentService
ab. Die Paypal-Zahlung enthält die Genehmigungs-URL. Wenn keine Zahlungsquelle festgelegt wurde, aktualisieren wir die Bestellung mit Paypal als bevorzugte Zahlungsmethode. Anschließend erstellen wir über den PaypalPaymentService
eine neue Paypal-Zahlung für die Bestellung. Von hier aus können wir die Genehmigungs-URL aus der neu erstellten Bestellung abrufen.
Wenn der Benutzer schließlich auf die Schaltfläche klickt, wird er zu Paypal weitergeleitet, wo er den Kauf genehmigen kann.
@UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-payment', templateUrl: './payment.component.html', styleUrls: ['./payment.component.css'] }) export class PaymentComponent implements OnInit { approvalUrl: string = ''; constructor( private orders: OrderService, private cart: CartService, private router: Router, private payments: PaypalPaymentService ) { } ngOnInit() { const orderId = this.cart.orderId; this.orders.getOrder(orderId, GetOrderParams.paymentSource) .pipe( concatMap((order: Order) => { const paymentSourceId = order.paymentSource?.id; const paymentMethod = order.availablePaymentMethods?.filter( (method) => method.paymentSourceType == 'paypal_payments' )[0]; return iif( () => paymentSourceId ? true : false, this.payments.getPaypalPayment(paymentSourceId || ''), this.orders.updateOrder({ id: orderId, paymentMethodId: paymentMethod?.id }, [UpdateOrderParams.paymentMethod]) .pipe(concatMap( order => this.payments.createPaypalPayment({ orderId: orderId, cancelUrl: `${environment.clientUrl}/cancel-payment`, returnUrl: `${environment.clientUrl}/place-order` }) )) ); })) .subscribe( paypalPayment => this.approvalUrl = paypalPayment?.approvalUrl || '', err => this.router.navigateByUrl('/error') ); } navigateToPaypal() { window.location.href = this.approvalUrl; } }
Hier ist seine Vorlage.
<app-simple-page number="5" title="Payment" subtitle="Pay for your order" buttonText="PROCEED TO PAY WITH PAYPAL" icon="point_of_sale" (buttonEvent)="navigateToPaypal()" [buttonDisabled]="approvalUrl.length ? false : true"></app-simple-page>
So wird die Zahlungsseite aussehen.
Zahlungskomponente stornieren
Paypal benötigt eine Seite zum Abbrechen der Zahlung. Diese Komponente dient diesem Zweck. Dies ist seine Vorlage.
<app-simple-page title="Payment cancelled" subtitle="Your Paypal payment has been cancelled" icon="money_off" buttonText="GO TO HOME" [centerText]="true" route="/"></app-simple-page>
Hier ist ein Screenshot der Seite.
Auftragskomponente aufgeben
Dies ist der letzte Schritt im Bestellvorgang. Hier bestätigt der Benutzer, dass er die Bestellung tatsächlich aufgeben und mit der Bearbeitung beginnen möchte. Wenn der Benutzer die Paypal-Zahlung genehmigt, wird er auf diese Seite weitergeleitet. Paypal fügt der URL einen Zahler-ID-Abfrageparameter hinzu. Dies ist die Paypal-ID des Benutzers.
Wenn die Komponente initialisiert wird, erhalten wir den Abfrageparameter payerId
aus der URL. Anschließend wird die Bestellung über den OrderService
mit Angabe der Zahlungsquelle abgerufen. Die ID der enthaltenen Zahlungsquelle wird verwendet, um die Paypal-Zahlung mit der Zahler-ID unter Verwendung des PaypalPayment
-Dienstes zu aktualisieren. Wenn einer dieser Fehler auftritt, wird der Benutzer auf die Fehlerseite umgeleitet. Wir verwenden die Eigenschaft disableButton
, um zu verhindern, dass der Benutzer die Bestellung aufgibt, bis die Zahler-ID festgelegt ist.
Wenn sie auf die Schaltfläche „Bestellung aufgeben“ klicken, wird die Bestellung mit einem placed
Status aktualisiert. Danach wird der Einkaufswagen geleert, ein erfolgreicher Imbiss angezeigt und der Benutzer auf die Homepage umgeleitet.
@UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-place-order', templateUrl: './place-order.component.html', styleUrls: ['./place-order.component.css'] }) export class PlaceOrderComponent implements OnInit { disableButton = true; constructor( private route: ActivatedRoute, private router: Router, private payments: PaypalPaymentService, private orders: OrderService, private cart: CartService, private snackBar: MatSnackBar ) { } ngOnInit() { this.route.queryParams .pipe( concatMap(params => { const payerId = params['PayerID']; const orderId = this.cart.orderId; return iif( () => payerId.length > 0, this.orders.getOrder(orderId, GetOrderParams.paymentSource) .pipe( concatMap(order => { const paymentSourceId = order.paymentSource?.id || ''; return iif( () => paymentSourceId ? paymentSourceId.length > 0 : false, this.payments.updatePaypalPayment(paymentSourceId, payerId) ); }) ) ); })) .subscribe( () => this.disableButton = false, () => this.router.navigateByUrl('/error') ); } placeOrder() { this.disableButton = true; this.orders.updateOrder({ id: this.cart.orderId, place: true }, [UpdateOrderParams.place]) .subscribe( () => { this.snackBar.open('Your order has been successfully placed.', 'Close', { duration: 3000 }); this.cart.clearCart(); setTimeout(() => this.router.navigateByUrl('/'), 4000); }, () => { this.snackBar.open('There was a problem placing your order.', 'Close', { duration: 8000 }); this.disableButton = false; } ); } }
Hier ist die Vorlage und das zugehörige Styling.
<app-simple-page title="Finalize Order" subtitle="Complete your order" [number]="'6'" icon="shopping_bag" buttonText="PLACE YOUR ORDER" (buttonEvent)="placeOrder()" [buttonDisabled]="disableButton"></app-simple-page>
Hier ist ein Screenshot der Seite.
App-Modul
Alle Anfragen an Commerce Layer, außer zur Authentifizierung, müssen ein Token enthalten. In dem Moment, in dem die App initialisiert wird, wird ein Token von der /oauth/token
-Route auf dem Server abgerufen und eine Sitzung wird initialisiert. Wir verwenden das APP_INITIALIZER
Token, um eine Initialisierungsfunktion bereitzustellen, in der das Token abgerufen wird. Darüber hinaus verwenden wir das HTTP_INTERCEPTORS
Token, um den zuvor erstellten OptionsInterceptor
bereitzustellen. Nachdem alle Module hinzugefügt wurden, sollte die App-Moduldatei etwa so aussehen.
@NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule, HttpClientModule, BrowserAnimationsModule, AuthModule, ProductsModule, CartModule, CheckoutModule, CoreModule ], providers: [ { provide: HTTP_INTERCEPTORS, useClass: OptionsInterceptor, multi: true }, { provide: APP_INITIALIZER, useFactory: (http: HttpClient) => () => http.post<object>( `${environment.apiUrl}/oauth/token`, { 'grantType': 'client_credentials' }, { withCredentials: true }), multi: true, deps: [HttpClient] } ], bootstrap: [AppComponent] }) export class AppModule { }
App-Komponente
Wir ändern die App-Komponentenvorlage und ihr Styling, die Sie hier finden.
<div> <app-header></app-header> <div> <router-outlet></router-outlet> </div> </div>
Fazit
In diesem Artikel haben wir behandelt, wie Sie mit Commerce Layer und Paypal eine E-Commerce-App für Angular 11 erstellen können. Wir haben auch angesprochen, wie Sie die App strukturieren und wie Sie eine Schnittstelle mit einer E-Commerce-API herstellen können.
Mit dieser App kann ein Kunde zwar eine komplette Bestellung aufgeben, aber sie ist noch lange nicht fertig. Es gibt so viel, was Sie hinzufügen könnten, um es zu verbessern. Zum einen können Sie Änderungen der Artikelmenge im Warenkorb aktivieren, Warenkorbartikel mit ihren Produktseiten verknüpfen, die Adresskomponenten optimieren, zusätzliche Schutzfunktionen für Checkout-Seiten wie die Bestellseite hinzufügen und so weiter. Dies ist nur der Ausgangspunkt.
Wenn Sie mehr über den Vorgang des Bestellvorgangs von Anfang bis Ende erfahren möchten, können Sie sich die Commerce Layer-Leitfäden und die API ansehen. Sie können den Code für dieses Projekt in diesem Repository anzeigen.