【Angular v19 + SpringBoot 最終回】編集、削除、追加

【スキル】エンジニア
たけ坊
たけ坊

今回でAngularとSpring Bootの大まかな実装が終わるよ

最後まで頑張ります!

前提

ここまでの流れはこちらから!

入力フォームの作成

まずは、追加と編集で使用する入力フォームコンポーネント、input.componentを作成します。

ng-pain-log/
└── pain-log/
    └── src/
        └── app/
            ├── core/
            ├── domain/
            ├── features/
            ├── infrastructures/
            ├── shared/
            │   ├── models/
            │   └── input/
            │       ├── input.component.html
            │       ├── input.component.ts
            │       └── input.component.less
            ├── app.component.html
            ├── app.component.ts
            ├── app.component.less
            └── app.routes.ts

その中身を以下のようにします。

<div class="input-form">
    <form [formGroup]="fg" (ngSubmit)="onSubmit()">
        <mat-form-field class="example-full-width">
            <mat-label>Choose a date</mat-label>
            <input matInput [matDatepicker]="picker" formControlName="date">
            <mat-hint>MM/DD/YYYY</mat-hint>
            <mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
            <mat-datepicker [dateClass]="dateClass" #picker></mat-datepicker>
        </mat-form-field>

        <mat-form-field class="example-full-width">
            <mat-label>Name</mat-label>
            <input matInput formControlName="name">
        </mat-form-field>

        <mat-form-field class="example-full-width">
            <mat-label>Movement</mat-label>
            <input matInput formControlName="movement">
        </mat-form-field>

        <label>VAS</label>
        <mat-slider>
            <input matSliderThumb  formControlName="vas">
        </mat-slider>

        <mat-form-field class="example-full-width">
            <mat-label>Memo</mat-label>
            <textarea matInput formControlName="memo" rows="4"></textarea>
        </mat-form-field>

        <div class="button-group">
            <button type="submit" mat-raised-button color="primary">Save</button>
            <button type="button" mat-raised-button (click)="onCancel()">Cancel</button>
        </div>
    </form>
</div>
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatDatepickerModule, MatCalendarCellClassFunction } from '@angular/material/datepicker'
import { MatSliderModule } from '@angular/material/slider';
import { ApiService } from '../../infrastructures/services/api.service';
import { PainLogResultDataEntity } from '../models/pain-log-data-entity';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-input',
  standalone: true,
  imports: [
    CommonModule,
    MatFormFieldModule, 
    MatInputModule, 
    MatDatepickerModule, 
    MatSliderModule, 
    ReactiveFormsModule,
    MatDialogModule,
    MatButtonModule,
    FormsModule,
  ],
  templateUrl: './input.component.html',
  styleUrl: './input.component.less'
})
export class InputComponent implements OnInit {
  fg!: FormGroup;

  constructor(
    private fb: FormBuilder, 
    private service: ApiService,
    private dialogRef: MatDialogRef<InputComponent>,
    @Inject(MAT_DIALOG_DATA) public data:any
  ) {}

  ngOnInit(): void {
    this.fg = this.fb.group({
      id: [this.data?.id || 0],
      date: [this.data?.date || ''],
      name: [this.data?.name || ''],
      movement: [this.data?.movement || ''],
      vas: [this.data?.vas || 0],
      memo: [this.data?.memo || '']
    });
  }

  dateClass: MatCalendarCellClassFunction<Date> = (cellDate, view) => {
    if (view === 'month') {
      const date = cellDate.getDate();
      return date === 1 || date === 20 ? 'example-custom-date-class' : '';
    }
    return '';
  };

  onSubmit(): void {
    if (this.fg.valid) {
      const newPainLog: PainLogResultDataEntity = {
        id: this.data?.id || 0,
        date: this.fg.value.date,
        name: this.fg.value.name,
        movement: this.fg.value.movement,
        vas: this.fg.value.vas,
        memo: this.fg.value.memo
      };

      if (newPainLog.id === 0) {
        this.service.insertPatients(newPainLog).subscribe((data: PainLogResultDataEntity[]) => {
          this.dialogRef.close(data);
        });
      } else {
        this.service.updatePatients(newPainLog).subscribe((data: PainLogResultDataEntity[]) => {
          this.dialogRef.close(data);
        });
      }
    }
  }

  onCancel(): void {
    this.dialogRef.close();
  }
}

上記の入力フォームはFormgroupを活用しました。参考は以下のサイトです!

Angular > Reactive Formsの使い方が初心者にわかりにくすぎるので、例を交えて取り合えず実装できる様にまとめて見た - Qiita
2020/01/26 nishitakuさんのご指摘により、angular version8で動く様に修正しました。初めに Reactive Forms に使用方法に関して既に多くの記事が上が…

追加機能の追加

次はテーブルのリストにデータを追加・編集・削除機能を一気に実装します。テーブルがあるdashboard.componentに加えます。

ng-pain-log/
└── pain-log/
    └── src/
        └── app/
            ├── core/
            ├── domain/
            ├── features/
            │   └── pages/
            │       ├── pages.component.html
            │       ├── pages.component.ts
            │       ├── pages.component.less
            │       └── dashboard/
            │           ├── dashboard.component.html
            │           ├── dashboard.component.ts
            │           └── dashboard.component.less
            ├── infrastructures/
            │   └── services/
            │       └── api.service.ts
            ├── shared/
            ├── app.component.html
            ├── app.component.ts
            ├── app.component.less
            └── app.routes.ts

以下のように加えます。

<app-search (searchCompleted)="onSearchCompleted($event)"></app-search>

@if (dataSource$ | async; as dataSource) {
<div class="table-format">
  <table mat-table [dataSource]="dataSource" class="mat-elevation-z8">

    <!-- Date Column -->
    <ng-container matColumnDef="date">
      <th mat-header-cell *matHeaderCellDef> Date </th>
      <td mat-cell *matCellDef="let element"> {{element.date}} </td>
    </ng-container>

    <!-- Name Column -->
    <ng-container matColumnDef="name">
      <th mat-header-cell *matHeaderCellDef> Name </th>
      <td mat-cell *matCellDef="let element"> {{element.name}} </td>
    </ng-container>

    <!-- Movement Column -->
    <ng-container matColumnDef="movement">
      <th mat-header-cell *matHeaderCellDef> Movement </th>
      <td mat-cell *matCellDef="let element"> {{element.movement}} </td>
    </ng-container>

    <!-- VAS Column -->
    <ng-container matColumnDef="vas">
      <th mat-header-cell *matHeaderCellDef> VAS </th>
      <td mat-cell *matCellDef="let element"> {{element.vas}} </td>
    </ng-container>

    <!-- Memo Column -->
    <ng-container matColumnDef="memo">
      <th mat-header-cell *matHeaderCellDef> Memo </th>
      <td mat-cell *matCellDef="let element"> {{element.memo}} </td>
    </ng-container>

    <!-- Action Column -->
    <ng-container matColumnDef="actions">
      <th mat-header-cell *matHeaderCellDef> </th>
      <td mat-cell *matCellDef="let element" class="action-link">
        <button mat-mini-fab color="basic" type="button" (click)="onUpdate(element)">
          <mat-icon>edit</mat-icon>
        </button>
        <button mat-mini-fab color="basic" type="button" (click)="onDelete(element)">
          <mat-icon>delete</mat-icon>
        </button>
      </td>
    </ng-container>

    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;" sticky></tr>
  </table>
</div>
}

<button mat-fab color="basic" class="add-button" type="button" (click)="openDialog()">
  <mat-icon>add</mat-icon>
</button>
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { MatTableModule } from '@angular/material/table';
import { PainLogResultDataEntity } from '../../../shared/models/pain-log-data-entity';
import { ApiService } from '../../../infrastructures/services/api.service';
import { SearchComponent } from '../../components/search/search.component';
import { Observable, of } from 'rxjs';
import { InputComponent } from '../../../shared/input/input.component';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatNativeDateModule } from '@angular/material/core';
import { ReactiveFormsModule } from '@angular/forms';
import {MatButtonModule} from '@angular/material/button';


@Component({
  selector: 'app-dashboard',
  standalone: true,
  imports: [
    CommonModule, 
    MatTableModule, 
    SearchComponent, 
    MatDialogModule, 
    MatIconModule, 
    MatNativeDateModule, 
    ReactiveFormsModule,
    MatButtonModule
  ],
  templateUrl: './dashboard.component.html',
  styleUrls: ['./dashboard.component.less'],
  providers: []
})

export class DashboardComponent implements OnInit {
  displayedColumns: string[] = ['date', 'name', 'movement', 'vas', 'memo', 'actions'];
  dataSource$!: Observable<PainLogResultDataEntity[]>;

  constructor(private service: ApiService, public dialog: MatDialog) {}

  ngOnInit(): void {
    this.loadPainlog();
  }

  loadPainlog(): void {
    this.dataSource$ = this.service.selectPatients();
  }

  onSearchCompleted(data: PainLogResultDataEntity[]): void {
    this.dataSource$ = of(data);
  }

  onDelete(element: PainLogResultDataEntity) :void {
    this.service.deletePatients(element.id).subscribe(() => {
      this.loadPainlog();
    });
  }

  onUpdate(element: PainLogResultDataEntity): void {
    const dialogRef = this.dialog.open(InputComponent, {
      width: '600px',
      data: element,
      disableClose: true
    });

    dialogRef.afterClosed().subscribe(result => {
      if (result) {
        this.loadPainlog();
      }
    })
  }

  openDialog(): void {
    const dialogRef = this.dialog.open(InputComponent, {
      width: "600px",
      data: {},
      disableClose: true
    });

    dialogRef.afterClosed().subscribe(result => {
      if (result) {
        this.loadPainlog();
      }
    })
  }

}

編集・削除ボタンなどの実装はこちらを参考にしました。

【Angular】Mat-tableの列データを編集・追加・削除する
今回はAngularのmat-tableに編集ボタンを付けて、編集ボタンからダイアログを開くとダイアログが開き、フォームに入力した情報がテーブルに反映される、というような機能を実装していきたいと思います。MattableとMatDialog

API側の修正

API側であるSpring Bootの修正を忘れていました。修正しないと、APIをたたいたときに、Objectの型で返ってこないので、エラーが発生してしまいます。

pain-log-management/
└── src/main/java/com/painlog/management/
   ├── controller/
   │   └── PatientController.java
   ├── domain/
   ├── model/
   │   ├── InsertPatient.java
   │   └── Patient.java
   ├── repository/
   │   ├── jdbc/
   │   │   └── PatientDaoJdbc.java
   │   └── PatientDao.java
   └── service/
       └── PatientService.java
package com.painlog.management.controller;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.painlog.management.domein.model.InsertPatient;
import com.painlog.management.domein.model.Patient;
import com.painlog.management.domein.service.PatientSearvice;

@RestController
@CrossOrigin(origins = "http://localhost:4200")
public class PatientController {
    
    @Autowired
    PatientSearvice patientSearvice;

    @PostMapping("/insert")
    public ResponseEntity<Map<String, String>> insert(@RequestBody InsertPatient patient) {
        boolean success = patientSearvice.insert(patient);
        Map<String, String> response = new HashMap<>();
        response.put("message", success ? "一件追加しました!" : "追加失敗しました!");
        return ResponseEntity.ok(response);
    }

    @GetMapping("/select")
    public List<Patient> select() {
        return patientSearvice.select();
    }

    @GetMapping("/search")
    public List<Patient> search(String name, String movement) {
        return patientSearvice.search(name, movement);
    }

    @PostMapping("/update")
    public ResponseEntity<Map<String, String>> update(@RequestBody Patient patient) {
        boolean success = patientSearvice.update(patient);
        Map<String, String> response = new HashMap<>();
        response.put("message", success ? "一件更新しました!" : "更新失敗しました!");
        return ResponseEntity.ok(response);
    }

    @PostMapping("/delete")
    public ResponseEntity<Map<String, String>> delete(@RequestBody Integer id) {
        boolean success = patientSearvice.delete(id);
        Map<String, String> response = new HashMap<>();
        response.put("message", success ? "一件削除しました!" : "削除失敗しました!");
        return ResponseEntity.ok(response);
    }
}

実装のまとめ

フロントとバックエンドを起動させると、

less(css)を入れて整えると、

追加画面も、

でてきました!

まとめ

今回は現段階で新しい、Angular v19 を使用して実装しました。新しいものは情報が少なく、遠回りもたくさんしましたが良い勉強になりました!

皆さんもここまで読んでいただいてありがとうございます!

修正箇所は気づき次第直します!

タイトルとURLをコピーしました