Wprowadzenie

W poprzedniej części wpisu utworzyliśmy back-end dla naszej aplikacji. Cały proces tworzenia został opisany w artykule: Angular 8: Integracja z .NET Core (część 1)

Teraz przechodzimy na front-end celem przygotowania aplikacji w Angularze. Nie wykorzystamy tutaj każdego z poprzednich wpisów ale posługując się poniższymi odnośnikami będziecie mogli doczytać nieco więcej jeżeli coś będzie dla Was niejasne:

Z drugiej strony czasem celowo będę pomijał dokłany opis, skupiamy się na konkretach.

Tworzenie aplikacji

Tworzymy nową aplikację wykorzystując do tego Angular CLI wraz z poniższym poleceniem:

ng new car-client-app

Domyślny projekt nie spełnia naszych wymagań – dodamy nowe komponenty tak, żeby odpowiadały naszemu API. Mój zamysł jest niezwykle prosty. Na głównej stronie będziemy wyświetlać wszystkie ogłoszenia. Kliknięcie dowolnego z nich pozwoli na otwarcie podstrony (inny routing), która pozwoli na edycje oraz usunięcie danego wpisu. Z kolei ekran główny będzie również posiadał przycisk dodawania nowego pojazdu do naszej bazy.

Nowe komponenty dodajemy korzytając z polecenia:

ng generate component nazwa_komponentu
Tak prezentuje się struktura aplikacja, którą przygotowałem: Angular: struktura aplikacji klienckiej Adnotacja: tworzenie komponentów przy użyciu powyższego polecenia jest niezwykle proste ale czasem prowadzi do błędów, które trudno zidentyfikować po raz pierwszy. Jeżeli uruchamiacie swoją aplikację i zobaczycie ostrzeżenie w postaci: "There is another module with a equal name when case is ignored" – zwróćcie uwagę czy importowanie Waszych komponentów do poszczególnych modułów jest określone poprawną ścieżką. Może się zdarzyć, że nazwa Waszego folderu jest napisana z małej litery podczas, gdy w rzeczywistości zaczyna się z dużej. Opisana sytuacja prowadzi do pojawienia się powyższego ostrzeżenia.

Każdy komponent jest reprezentacją jednej z operacji wykonywanych na bazie danych. Dlaczego operacja dodawania i aktualizacji jest wykonana na różnych widokach skoro pod spodem występują te same modele? Widoki przygotujemy w nieco inny sposób, żeby nie wprowadzać użytkownika w błąd. W realnym świecie moglibyśmy mieć ten sam widok z różnymi stantami, które definiowały użycie poprawnej metody – w ramach poradnika nie będziemy niepotrzebnie komplikować naszego kodu.

W kolejnym kroku skupimy się na niezwykle istotnej sprawie – nawigacji po naszej witrynie. Przygotujemy routing oraz prosty kod HTML w celu sprawdzenia czy nasza aplikacja oraz przekierowania działają poprawnie. Dopiero w kolejnym kroku skupimy się na połączeniu z naszym API.

Założenie jest proste. Strona główna naszej aplikacji to zbiór wszystkich ogłoszeń jakimi dysponujemy w bazie danych (overview) oraz możliwość dodania nowego pojazdu (add). W przypadku widoku głównego nie będziemy posługiwać się żadną ścieżką – będzie to adres naszej witryny. W przypadku jednak dodawania pojazu posłużymy się ścieżką zapisaną jako /add. Spójrzcie na prosty widok wraz z przygotowaną podstawową nawigacją (w tym celu zmodyfikowałem plik app.component.html):

<h2 class="header">Witajcie w moim salonie samochodów luksusowych</h2>

<p>
    Poniżej lista wszystkich pojazdów gotowych do sprzedaży! Jeżeli chcesz zapoznać się z opisem kliknij na interesujący
    Cię model.
</p>

<hr>
<nav>
    <a class="nav-item" [routerLink]="['/']">Strona główna</a>
    <a class="nav-item" [routerLink]="['/add']">Dodaj nowy pojazd</a>
</nav>
<hr>
<div class="outer-outlet">
    <router-outlet></router-outlet>
</div>
Jak pamiętacie z wpisu (Angular 8: Routing) musimy jeszcze zmodyfikować plik app-routing.module.ts, gdzie określimy ścieżki naszej nawigacji:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

// W pierwszym kroku importujemy przygotowane komponenty
// To do nich wykonamy przekierowanie
import { OverviewComponent } from './Components/Cars/overview/overview.component'
import { AddComponent } from './Components/Cars/add/add.component'

const routes: Routes = [
  { path: '', component: OverviewComponent},
  { path: 'add', component: AddComponent}
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
Pamiętajcie, żeby przed uruchomieniem aplikacji zmienić nieco kod HTML, aby mieć pewność, że nawigacja działa poprawnie. W moim przypadku aplikacja wygląda w następujący sposób:

Musimy obsłużyć jeszcze trzy widoki. Widok szczegółowy naszego pojazdu wraz z możliwością kasowania oraz edycji/aktualizacji danych. W pierwszym kroku zmienimy widok ogólny – na potrzeby testów dodamy jeden obrazek na który będzie można kliknąć w celu otwarcia widoku tylko do odczytu (w tym przypadku będzie to szczegółowy opis naszego pojazdu). Ścieżka do widoku tylko do odczytu będzie miała adres w postaci /edit/id. Aktualizacja i kasowanie to odpowiednio /update/id oraz /delete/id. Celowo nie wprowadzamy innej formy adresów ponieważ w ramach prezentacji obracamy się jedynie w formie jednego modułu docelowego, tj. obsługa naszego salonu.

W pierwszym kroku nieznacznie modyfikujemy widok overview.component.html. Plik przyjmuje teraz postać:

<p>Tutaj pojawi się lista wszystkich samochodów w naszej bazie danych</p>

<a [routerLink]="['/edit/1']">
    <img class="overview-image-size"
            src="https://mklr.pl/uimages/services/motokiller/i18n/pl_PL/201801/$_bmw_e39_m5_v8_$1516124740_by_Amotive.jpg?1516171255">
</a>

<!-- Oraz wspomniany we wpisie kontener dla komponentu -->
<div class="inner-outlet">
    <router-outlet></router-outlet>
</div>

Drugi krok to modyfikacja routera, której celem jest poinformowanie o przekazywanym parametrze. Nasze zdefiniowane ścieżki przyjmują teraz postać:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

// W pierwszym kroku importujemy przygotowane komponenty
// To do nich wykonamy przekierowanie
import { OverviewComponent } from './Components/Cars/overview/overview.component'
import { AddComponent } from './Components/Cars/add/add.component'
import { DetailsComponent } from './Components/Cars/details/details.component'

const routes: Routes = [
  { path: '', component: OverviewComponent },
  { path: 'add', component: AddComponent },
  { path: 'edit/:id', component: DetailsComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
Dodamy jeszcze dwie brakujące ścieżki w postaci możliwej nawigacji, tj. aktualizacja oraz kasowanie danego rekordu. W tym celu wprowadzmy drobne zmiany w szablonie odpowiedzialnym za widok szczegółowy:
<hr>

<nav>
    <a [routerLink]="['/update/1']">Edytuj dane ogłoszenia</a>
    <a [routerLink]="['/delete/1']">Usuń dane ogłoszenia</a>
</nav>

<hr>

<p>Ekran szczegółowy wraz z pełnym opisem naszego pojazdu!</p>

<div class="outer-outlet">
    <router-outlet></router-outlet>
</div>
Musimy jeszcze odzwierciedlić powyższe zmiany w definicji routera:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

// W pierwszym kroku importujemy przygotowane komponenty
// To do nich wykonamy przekierowanie
import { OverviewComponent } from './Components/Cars/overview/overview.component'
import { AddComponent } from './Components/Cars/add/add.component'
import { DetailsComponent } from './Components/Cars/details/details.component'
import { UpdateComponent } from './Components/Cars/update/update.component'
import { DeleteComponent } from './Components/Cars/delete/delete.component'

// Pamiętajcie, że to tylko pokaz możliwości routera a nie zalecana implementacja
// W artykule dotyczącym tego tematu omawialem sposób zagnieżdzonej nawigacji
// Tutaj też powinniśmy przekierować operacje CRUD do ścieżki car/edit/:id etc
const routes: Routes = [
{ path: '', component: OverviewComponent },
{ path: 'add', component: AddComponent },
{ path: 'edit/:id', component: DetailsComponent },
{ path: 'update/:id', component: UpdateComponent },
{ path: 'delete/:id', component: DeleteComponent }
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }

Po drobnych zmianach w szablonach HTML nasza aplikacja prezentuje się w poniższy sposób:

Serwisy oraz moduł HttpClient

Wykonywanie żądań do API jest możliwe dzięki modułowi HttpClientModule. Musimy dokonać importu tej paczki wewnątrz pliku app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
// Import odpowiedniego modułu
import { HttpClientModule } from '@angular/common/http'

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { OverviewComponent } from './Components/Cars/overview/overview.component';
import { DetailsComponent } from './Components/Cars/details/details.component';
import { DeleteComponent } from './Components/Cars/delete/delete.component';
import { AddComponent } from './Components/Cars/add/add.component';
import { UpdateComponent } from './Components/Cars/update/update.component';

@NgModule({
  declarations: [
    AppComponent,
    OverviewComponent,
    DetailsComponent,
    DeleteComponent,
    AddComponent,
    UpdateComponent
  ],
  // Tablica w której przechowywujemy używane moduły
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Kolejny krok to nowy folder wraz z wystawionym interfejsem będącym reprezentacją właściwości naszego modelu:

export interface Cars {
    brand: string,
    model: string,
    engine: string,
    power: number,
    production: string,
    price: number,
    imagePath: string,
    description: string,
    mileage: number
}

Prawie wszystkie wymagane kroki są już za nami. Musimy jeszcze przygotować serwis, który będzie wykonywał zapytania. W tym celu wykorzystamy polecenie dostępne z poziomu Angular CLI:

ng generate service services/http –skipTests false
Flaga skipTests ustawiona na wartość false mówi nam o tym, że nowy plik testów nie zostanie utworzony.

Musimy dokonać drobnej modyfikacji utworzonego serwisu. Poniżej zamieściłem wszelkie niezbędne komentarze:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'

@Injectable({
  providedIn: 'root'
})
export class HttpService {

  constructor(private httpService: HttpClient) { }

  // Funkcję getData będziemy wywoływać za każdym razem, gdy będziemy potrzebować danych
  // z określonego punktu końcowego z serwera, tj. metody zdefiniowanej w API
  public getData = (route: string) => {
    return this.httpService.get(route);
  }

  // Powyższa funkcja zwraca dane jako Observable. Jeżeli chcemy uzyskać dostęp do danych
  // musimy dokonać subskrypcji tej funkcji. Jest to temat dalszej części wpisu.
}

Serwis jest gotowy: oczywiście w ograniczonej formie, żeby nie zaciemniać kodu.

Pobieranie danych z API

W pierwszym kroku połączymy się z API w celu pobrania danych na ekran główny, tj. wszystkie nasze ogłoszenia. Każdy z widok został przygotowany w oparciu o własny komponent – w tym miejscu przeprowdzimy odpowiednie modyfikacje. Poniżej kod pliku overview.component.ts z wymaganymi komentarzami:

import { Component, OnInit } from '@angular/core';
// Pamiętajcie o imporcie odpowiednich składowych: model oraz serwis
import { Cars } from '../../../Interfaces/cars.model';
import { HttpService } from '../../../../services/http.service';

@Component({
  selector: 'app-overview',
  templateUrl: './overview.component.html',
  styleUrls: ['./overview.component.css']
})
export class OverviewComponent {

  public cars: Cars[];

  // w konstukturze definiujemy właściwość prywatną, która daje nam dostęp do
  // implementacji naszego serwisu - narazie w ograniczonej formie
  constructor(private httpService: HttpService) { }

  // wykorzystujemy implementację naszego serwisu, przekazujemy odpowiedni adres
  // naszego API a zarazem żadanej metody. Obsługujemy informację pomyślną oraz 
  // ewentualne błędy
  public getCars = () => {
    let route: string = 'https://wprowadz_adres_wystawionego_api';
    this.httpService.getData(route)
      .subscribe((result) => {
        this.cars = result as Cars[];
      },
        (error) => {
          console.error(error);
        });
  }
}

Musimy sięgnąć jeszcze pamięcią do wpisu dotyczącego dyrektyw w celu odpowiedniego przygotowania widoku. Plik, który modyfikujemy to overview.component.html:

<p>Tutaj pojawi się lista wszystkich samochodów w naszej bazie danych</p>

<div class="overview-container">
    <div *ngFor="let car of cars">
        <div class="overview-container-image-size">
            <a [routerLink]="['/edit/', car.id]">
                <div style="width:300px;height: 200px;">
                    <img class="overview-image-size" src={{car.imagePath}}>
                </div>
                <div>
                    <p style="padding-left: 10px;">
                        <strong>Samochód: </strong><span>{{car.brand}} {{car.model}}</span>
                    </p>
                    <p style="padding-left: 10px;">
                        <strong>Silnik: </strong><span>{{car.engine}} {{car.power}}"KM"</span>
                    </p>
                    <p style="padding-left: 10px;">
                        <strong>Przebieg: </strong><span>{{car.mileage}}</span>
                    </p>
                    <p style="padding-left: 10px;">
                        <strong>Rok produkcji: </strong><span>{{car.production}}</span>
                    </p>
                    <p style="padding-left: 10px;">
                        <strong>Opis: </strong><span>{{car.description}}</span>
                    </p>
                </div>
            </a>
        </div>
    </div>
</div>

<div class="inner-outlet">
    <router-outlet></router-outlet>
</div>

Tak teraz prezentuje się nasz widok główny: Angular: struktura aplikacji klienckiej

W ramach tego wpisu dodamy jeszcze jedną funkcjonalność – będzie to dodawanie pojazdu do bazy danych. W pierwszym kroku musimy oczywiście zmodyfikować nasz serwis znajdujący się w pliku http.service.ts:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'

import { Cars } from '../app/Interfaces/cars.model';

@Injectable({
  providedIn: 'root'
})
export class HttpService {

  constructor(private httpService: HttpClient) { }

  // Funkcję getData będziemy wywoływać za każdym razem, gdy będziemy potrzebować danych
  // z określonego punktu końcowego z serwera, tj. metody zdefiniowanej w API
  public getData = (route: string) => {
    return this.httpService.get(route);
  }
  // Powyższa funkcja zwraca dane jako Observable. Jeżeli chcemy uzyskać dostęp do danych
  // musimy zasubskrywować tę funkcję. Jest to temat dalszej części wpisu.

  // Funkcja pozwalająca na wykonanie żądania typu POST
  public addData = (route: string, body: Cars) => {
    return this.httpService.post(route, body);
  }
}
Kiedy nasz serwis zostanie przygotowany możemy przejść do edycji kodu pliku add.component.ts:
import { Component, OnInit } from '@angular/core';
// Pamiętajcie o imporcie odpowiednich składowych: model oraz serwis
import { Cars } from '../../../Interfaces/cars.model';
import { HttpService } from '../../../../services/http.service';

@Component({
  selector: 'app-add',
  templateUrl: './add.component.html',
  styleUrls: ['./add.component.css'],
})
export class AddComponent {
  public car: Cars;

  constructor(private httpService: HttpService) {
    // W konstrukturze tworzymy model tzw. danych defaultowych
    // Zostaną one połączone z danymi wprowadzonymi przez użytkownika
    // Otwierając ekran tworzenia nowego ogłoszenia zrozumiecie mój zamysł
    // poniższych danych
    this.car = {
      brand: "", 
      model: "", 
      engine: "", 
      imagePath: "https://softsmart.co.za/wp-content/uploads/2018/06/image-not-found-1038x576.jpg", 
      mileage: 1, 
      description: "", 
      power: 1, 
      production: "2015-01-01T00:00:00", 
      price: 1
    };

  }

  // wykorzystujemy implementację naszego serwisu, przekazujemy odpowiedni adres
  // naszego API a zarazem żadanej metody. Obsługujemy informację pomyślną oraz 
  // ewentualne błędy
  public addCar = () => {
    let route: string = 'https://wprowadz_adres_wystawionego_api';
    this.httpService.addData(route, this.car)
      .subscribe((result) => {
        alert("udało się all")
      },
        (error) => {
          console.error(error);
        });
  }

  onSubmit() {
    console.log("poszlo");
    this.addCar();
  }
}
Ostatni krok to przygotowaniu widoku. Ekran podzieliłem na dwie strony: po lewej wprowadzamy dane ogłoszenia a po prawej możemy na bieżąco oglądać dodawane ogłoszenie:
<p>Z tego poziomu dodamy nowy samochód</p>

<div style="width:25%;float:left">
    <p>Wprowadz poniżej dane nowego ogłoszenia!</p>
    <p>
        <strong>Marka: </strong>
        <!-- [(ngModel)] -> pamiętajcie o modyfikacji app.module.ts i imporcie odpowiedniego modułu -->
        <input type="text" [(ngModel)]="car.brand" />
    </p>

    <p>
        <strong>Model: </strong>
        <input type="text" [(ngModel)]="car.model" />
    </p>

    <p>
        <strong>Silnik: </strong>
        <input type="text" [(ngModel)]="car.engine" />
    </p>

    <p>
        <strong>Moc: </strong>
        <input type="number" [(ngModel)]="car.power" />
    </p>

    <p>
        <strong>Przebieg: </strong>
        <input type="number" [(ngModel)]="car.mileage" />
    </p>

    <p>
        <strong>Rok produkcji: </strong>
        <input type="text" [(ngModel)]="car.production" />
    </p>

    <p>
        <strong>Link do zdjecia: </strong>
        <input type="text" [(ngModel)]="car.imagePath" />
    </p>

    <p>
        <strong>Opis: </strong>
        <textarea [(ngModel)]="car.description"></textarea>
    </p>
</div>

<div style="width:25%;float:left">
    <p>Podglad na żywo Twojego ogłoszenia!</p>
    <div class="overview-container-image-size">
        <div style="width:300px;height: 200px;">
            <img class="overview-image-size" src={{car.imagePath}}>
        </div>
        <div>
            <p style="padding-left: 10px;">
                <strong>Samochód: </strong><span>{{car.brand}} {{car.model}}</span>
            </p>
            <p style="padding-left: 10px;">
                <strong>Silnik: </strong><span>{{car.engine}} {{car.power}}KM</span>
            </p>
            <p style="padding-left: 10px;">
                <strong>Przebieg: </strong><span>{{car.mileage}}</span>
            </p>
            <p style="padding-left: 10px;">
                <strong>Rok produkcji: </strong><span>{{car.production}}</span>
            </p>
            <p style="padding-left: 10px;">
                <strong>Opis: </strong><span>{{car.description}}</span>
            </p>
        </div>
    </div>
    <button (click)="onSubmit()">Dodaj ogłoszenie</button>
</div>

Wszystko jest już gotowe, dokonajmy testów naszej aplikacji: Pamiętajcie, że to tylko prosta prezentacja projektu – powinniśmy np. dodać informację, że ogłoszenie zostało dodane (lub też nie) do bazy danych. Poleganie na komunikatach konsoli (jak w naszym przypadku) nie jest najlepszym rozwiązaniem.

Podsumowanie

I to by było na tyle – nie opisałem pozostałych operacji (kasowania oraz aktualizacji) - powyższy kod jednak w zupełności wystarczy na przygotowanie własnej implementacji tych dwóch zagadnień. Aktualizacja danych jest niezwykle podobna do samego procesu dodawania a kasowanie również wymaga nieznacznych modyfikacji.

Proces integracji zamknął się w dwóch niezwykle długich artykułach. Jesteście teraz gotowi do własnych eksperymentów i tworzenia projektów w oparciu o technologię .NET Core w połączeniu z Angularem.

Teraz pytanie do Was Drodzy Czytelnicy – chcielibyście, żebyśmy kontynuowali to zagadanienie w nieco szerszej formie? (nie oznacza to oczywiście rezygnacji z .NET Core). Wszelkie opinie/propozycje/uwagi kierujcie proszę przez przygotowany formularz kontaktowy.