Nowoczesna aplikacja w Spring Boot i Angular – [część 2]
Nowoczesna aplikacja w Spring Boot i Angular – [część 2]
Ten artykuł to kontynuacja wpisu https://javaleader.pl/2020/09/26/nowoczesna-aplikacja-w-spring-boot-i-angular-czesc-1/. Zanim zaczniesz czytać ten wpis zachęcam Cie gorąco do przeczytania części pierwszej w której projektujemy praktyczne Rest API. W tej części zajmiemy się kwestią wizualną naszej aplikacji czyli wykorzystamy utworzone wcześniej Rest API do interakcji z użytkownikiem końcowym. Zaczynamy od konfiguracji środowiska Angular CLI oraz Node.js. Artykuł ten bazuje na wersji:
Angular CLI: 10.1.3 Node: 12.16.3
Tworzymy nowy projekt o następującej nazwie:
ng new ModernApp
zostaniemy zapytani o Angular routing oraz o format szablonów:
? Would you like to add Angular routing? Yes ? Which stylesheet format would you like to use? CSS
po instalacji wszystkich pakietów:
Installing packages...
nowy projekt zostanie utworzony:
Packages installed successfully
możemy przystąpić teraz do realizacji aplikacji.
Będąc w katalogu projektu – generujemy serwisy i komponenty:
ng g s services/tutorial ng g c components/add-tutorial ng g c components/tutorial-details ng g c components/tutorials-list
gdzie:
- g – generate
- c – component
Komponenty powinny przechowywać logikę związaną z wyświetlaniem widoku, zaś serwisy powinny przechowywać niezbędne dla tego widoku dane. W wyniku wykonania powyższych poleceń otrzymamy:
CREATE src/app/services/tutorial.service.spec.ts (367 bytes) CREATE src/app/services/tutorial.service.ts (137 bytes)
CREATE src/app/components/add-tutorial/add-tutorial.component.html (27 bytes) CREATE src/app/components/add-tutorial/add-tutorial.component.spec.ts (662 bytes) CREATE src/app/components/add-tutorial/add-tutorial.component.ts (298 bytes) CREATE src/app/components/add-tutorial/add-tutorial.component.css (0 bytes) UPDATE src/app/app.module.ts (508 bytes)
CREATE src/app/components/tutorial-details/tutorial-details.component.html (31 bytes) CREATE src/app/components/tutorial-details/tutorial-details.component.spec.ts (690 bytes) CREATE src/app/components/tutorial-details/tutorial-details.component.ts (314 bytes) CREATE src/app/components/tutorial-details/tutorial-details.component.css (0 bytes) UPDATE src/app/app.module.ts (639 bytes)
CREATE src/app/components/tutorials-list/tutorials-list.component.html (29 bytes) CREATE src/app/components/tutorials-list/tutorials-list.component.spec.ts (676 bytes) CREATE src/app/components/tutorials-list/tutorials-list.component.ts (306 bytes) CREATE src/app/components/tutorials-list/tutorials-list.component.css (0 bytes) UPDATE src/app/app.module.ts (762 bytes)
ng g s services/tutorial – zawiera logikę wykonującą zapytania HTTP do zaprojektowanego w Spring Boocie API.
app-routing.module.ts – definiuje routing dla komponentów.
app.module.ts – zawiera definicje komponentów Angulara oraz import niezbędnych modułów.
app – główny komponent aplikacji.
W pliku app.module.ts dodajemy niezbędne moduły – FormsModule oraz HttpClientModule w następujący oto sposób:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { AddTutorialComponent } from './components/add-tutorial/add-tutorial.component'; import { TutorialDetailsComponent } from './components/tutorial-details/tutorial-details.component'; import { TutorialsListComponent } from './components/tutorials-list/tutorials-list.component'; import { FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; @NgModule({ declarations: [ AppComponent, AddTutorialComponent, TutorialDetailsComponent, TutorialsListComponent ], imports: [ BrowserModule, AppRoutingModule, FormsModule, HttpClientModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
FormsModule – Aby rozpocząć pracę z formularzami, musimy zacząć od zaimportowania modułu FormsModule.
HttpClientModule– Aby wykonywać zapytania HTTP do zaprojektowanego API – ulepszona wersja HttpModule.
Definiujemy routing:
- /tutorials – związany z komponentem tutorials-list,
- /tutorials/:id – związany z komponentem tutorial-details,
- /add – związany z komponentem add-tutorial.
edytujemy plik – app-routing.module.ts – dodajemy importy komponentów oraz wskazujemy routing dla nich:
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { TutorialsListComponent } from './components/tutorials-list/tutorials-list.component'; import { TutorialDetailsComponent } from './components/tutorial-details/tutorial-details.component'; import { AddTutorialComponent } from './components/add-tutorial/add-tutorial.component'; const routes: Routes = [ { path: '', redirectTo: 'tutorials', pathMatch: 'full' }, { path: 'tutorials', component: TutorialsListComponent }, { path: 'tutorials/:id', component: TutorialDetailsComponent }, { path: 'add', component: AddTutorialComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
Edytujemy plik app.component.html – dodajemy menu dla aplikacji:
<div> <nav class="navbar navbar-expand navbar-dark bg-dark"> <a href="#" class="navbar-brand">JavaLeader.pl</a> <div class="navbar-nav mr-auto"> <li class="nav-item"> <a routerLink="tutorials" class="nav-link">Tutorials</a> </li> <li class="nav-item"> <a routerLink="add" class="nav-link">Add</a> </li> </div> </nav> <div class="container mt-3"> <router-outlet></router-outlet> </div> </div>
Projektujemy serwis który odpowiada za komunikację z API – tutorial.service.ts:
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; const baseUrl = 'http://localhost:8080/api/tutorials'; @Injectable({ providedIn: 'root' }) export class TutorialService { constructor(private http: HttpClient) { } getAll(): Observable<any> { return this.http.get(baseUrl); } get(id): Observable<any> { return this.http.get(`${baseUrl}/${id}`); } create(data): Observable<any> { return this.http.post(baseUrl, data); } update(id, data): Observable<any> { return this.http.put(`${baseUrl}/${id}`, data); } delete(id): Observable<any> { return this.http.delete(`${baseUrl}/${id}`); } deleteAll(): Observable<any> { return this.http.delete(baseUrl); } findByTitle(title): Observable<any> { return this.http.get(`${baseUrl}?title=${title}`); } }
adnotacja @Injectable oznacza, że serwis jest zdolny do wstrzykiwania zależności przez konstruktor. Od wersji Angular 6.0 pojawił się dodatkowy atrybut:
providedIn: 'root'
Użycie tego atrybutu to alternatywa dla implementowania metody forRoot. Jego użycie powoduje brak konieczności dodawania serwisu do tablicy providers modułu.
Projektujemy komponent odpowiedzialny za dodanie nowego szkolenia:
import { Component, OnInit } from '@angular/core'; import { TutorialService } from 'src/app/services/tutorial.service'; @Component({ selector: 'app-add-tutorial', templateUrl: './add-tutorial.component.html', styleUrls: ['./add-tutorial.component.css'] }) export class AddTutorialComponent implements OnInit { tutorial = { title: '', description: '', published: false }; submitted = false; constructor(private tutorialService: TutorialService) { } ngOnInit(): void { } saveTutorial(): void { const data = { title: this.tutorial.title, description: this.tutorial.description }; this.tutorialService.create(data) .subscribe( response => { console.log(response); this.submitted = true; }, error => { console.log(error); }); } newTutorial(): void { this.submitted = false; this.tutorial = { title: '', description: '', published: false }; } }
ngOnInit() – podstawowa metoda, która jest wywoływana jednokrotnie podczas tworzenia komponentu. wykonywana zaraz po konstruktorze klasy.
widok dla wspomnianego komponentu:
<div style="width: 400px; margin: auto;"> <div class="submit-form"> <div *ngIf="!submitted"> <div class="form-group"> <label for="title">Title</label> <input type="text" class="form-control" id="title" required [(ngModel)]="tutorial.title" name="title" /> </div> <div class="form-group"> <label for="description">Description</label> <input class="form-control" id="description" required [(ngModel)]="tutorial.description" name="description" /> </div> <button (click)="saveTutorial()" class="btn btn-success">Submit</button> </div> <div *ngIf="submitted"> <h4>You submitted successfully!</h4> <button class="btn btn-success" (click)="newTutorial()">Add</button> </div> </div> </div>
W zależności od wartości zmiennej submitted wykonywany jest odpowiedni fragment widoku. Za pomocą [(ngModel)] – wiążemy pola formularza ze zmiennymi komponentu.
Wyświetlanie listy szkoleń – komponent – add-tutorial.component.ts:
import { Component, OnInit } from '@angular/core'; import { TutorialService } from 'src/app/services/tutorial.service'; @Component({ selector: 'app-tutorials-list', templateUrl: './tutorials-list.component.html', styleUrls: ['./tutorials-list.component.css'] }) export class TutorialsListComponent implements OnInit { tutorials: any; currentTutorial = null; currentIndex = -1; title = ''; constructor(private tutorialService: TutorialService) { } ngOnInit(): void { this.retrieveTutorials(); } retrieveTutorials(): void { this.tutorialService.getAll() .subscribe( data => { this.tutorials = data; console.log(data); }, error => { console.log(error); }); } refreshList(): void { this.retrieveTutorials(); this.currentTutorial = null; this.currentIndex = -1; } setActiveTutorial(tutorial, index): void { this.currentTutorial = tutorial; this.currentIndex = index; } removeAllTutorials(): void { this.tutorialService.deleteAll() .subscribe( response => { console.log(response); this.retrieveTutorials(); }, error => { console.log(error); }); } searchTitle(): void { this.tutorialService.findByTitle(this.title) .subscribe( data => { this.tutorials = data; console.log(data); }, error => { console.log(error); }); } }
tutorials – reprezentuje dane szkoleń pobrane z API.
currentTutorial – reprezentuje bieżący tutorial wskazany przez użytkownika.
currentIndex – reprezentuje index wskazanego przez użytkownika szkolenia.
title – reprezentuje tyuł szkolenia po jakim szkolenie ma być wyszukane w bazie danych.
widok dla komponentu wyżej:
<div class="list row"> <div class="col-md-8"> <div class="input-group mb-3"> <input type="text" class="form-control" placeholder="Search by title" [(ngModel)]="title" /> <div class="input-group-append"> <button class="btn btn-outline-secondary" type="button" (click)="searchTitle()" > Search </button> </div> </div> </div> <div class="col-md-6"> <h4>Tutorials List</h4> <ul class="list-group"> <li class="list-group-item" *ngFor="let tutorial of tutorials; let i = index" [class.active]="i == currentIndex" (click)="setActiveTutorial(tutorial, i)" > {{ tutorial.title }} </li> </ul> <button class="m-3 btn btn-sm btn-danger" (click)="removeAllTutorials()"> Remove All </button> </div> <div class="col-md-6"> <div *ngIf="currentTutorial"> <h4>Tutorial</h4> <div> <label><strong>Title:</strong></label> {{ currentTutorial.title }} </div> <div> <label><strong>Description:</strong></label> {{ currentTutorial.description }} </div> <div> <label><strong>Status:</strong></label> {{ currentTutorial.published ? "Published" : "Pending" }} </div> <a class="badge badge-warning" routerLink="/tutorials/{{ currentTutorial.id }}"> Edit </a> </div> <div *ngIf="!currentTutorial"> <br /> <p>Please click on a Tutorial...</p> </div> </div> </div>
Użytkownik ma wyświetloną listę szkoleń. W momencie kiedy następuje wskazanie danego szkolenia z listy – szkolenie staje się aktywne:
[class.active]="i == currentIndex"
i zostaną wyświetlone jego dane.
Edycja danego szkolenia – tutorial-details.component.ts:
import { Component, OnInit } from '@angular/core'; import { TutorialService } from 'src/app/services/tutorial.service'; import { ActivatedRoute, Router } from '@angular/router'; @Component({ selector: 'app-tutorial-details', templateUrl: './tutorial-details.component.html', styleUrls: ['./tutorial-details.component.css'] }) export class TutorialDetailsComponent implements OnInit { currentTutorial = null; message = ''; constructor( private tutorialService: TutorialService, private route: ActivatedRoute, private router: Router) { } ngOnInit(): void { this.message = ''; this.getTutorial(this.route.snapshot.paramMap.get('id')); } getTutorial(id): void { this.tutorialService.get(id) .subscribe( data => { this.currentTutorial = data; console.log(data); }, error => { console.log(error); }); } updatePublished(status): void { const data = { title: this.currentTutorial.title, description: this.currentTutorial.description, published: status }; this.tutorialService.update(this.currentTutorial.id, data) .subscribe( response => { this.currentTutorial.published = status; console.log(response); }, error => { console.log(error); }); } updateTutorial(): void { this.tutorialService.update(this.currentTutorial.id, this.currentTutorial) .subscribe( response => { console.log(response); this.message = 'The tutorial was updated successfully!'; }, error => { console.log(error); }); } deleteTutorial(): void { this.tutorialService.delete(this.currentTutorial.id) .subscribe( response => { console.log(response); this.router.navigate(['/tutorials']); }, error => { console.log(error); }); } }
W konstruktorze:
constructor( private tutorialService: TutorialService, private route: ActivatedRoute, private router: Router) { }
dodajemy ActivatedRoute oraz Router dzięki czemu jesteśmy w stanie pobrać dnay parametr z URL (Router) oraz przekierować użytkownika na wskazany adres (ActivatedRoute).
Widok dla komponentu wyżej:
<div style="width: 400px; margin: auto;"> <div *ngIf="currentTutorial" class="edit-form"> <h4>Tutorial</h4> <form> <div class="form-group"> <label for="title">Title</label> <input type="text" class="form-control" id="title" [(ngModel)]="currentTutorial.title" name="title" /> </div> <div class="form-group"> <label for="description">Description</label> <input type="text" class="form-control" id="description" [(ngModel)]="currentTutorial.description" name="description" /> </div> <div class="form-group"> <label><strong>Status:</strong></label> {{ currentTutorial.published ? "Published" : "Pending" }} </div> </form> <button class="badge badge-primary mr-2" *ngIf="currentTutorial.published" (click)="updatePublished(false)" > UnPublish </button> <button *ngIf="!currentTutorial.published" class="badge badge-primary mr-2" (click)="updatePublished(true)" > Publish </button> <button class="badge badge-danger mr-2" (click)="deleteTutorial()"> Delete </button> <button type="submit" class="badge badge-success" (click)="updateTutorial()" > Update </button> <p>{{ message }}</p> </div> <div *ngIf="!currentTutorial"> <br /> <p>Cannot access this Tutorial...</p> </div> </div>
Po tych operacjach należy uruchomić aplikację na porcie 8081 ponieważ polityka CrossOrigin na poziomie zaprojektowanego API zdefiniowana jest tylko dla aplikacji łączących się z portu 8081. Po więcej informacji na temat polityki wywołań asynchronicnzych zapytań AJAX odsyłam do wpisu – https://javaleader.pl/2020/07/23/adnotacja-crossorigin/.
Uruchomienie aplikacji na porcie 8081:
ng serve --port 8081
Warto rownież dodać bootstrapa aby aplikacja prezentowała się ładnie:
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
Na koniec odsyłam do oryginalnego wpisu – https://bezkoder.com/angular-10-crud-app/.
a „c” to nie jest przypadkiem component ?
Dzięki! poprawione.