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/.

Zobacz kod na GitHubie i zapisz się na bezpłatny newsletter!

.

2 Comments

  1. a „c” to nie jest przypadkiem component ?

Leave a comment

Your email address will not be published.


*