Paweł Łukasiewicz
2020-03-15
Paweł Łukasiewicz
2020-03-15
Udostępnij Udostępnij Kontakt
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.

Doceniasz moją pracę? Wesprzyj bloga 'kupując mi kawę'.

Jeżeli seria wpisów dotycząca Angulara była dla Ciebie pomocna, pozwoliła Ci rozwinąć obecne umiejętności lub dzięki niej nauczyłeś się czegoś nowego... będę wdzięczny za wsparcie w każdej postaci wraz z wiadomością, którą będę miał przyjemność przeczytać.

Z góry dziekuje za każde wsparcie - i pamiętajcie, wpisy były, są i będą za darmo!