
たけ坊
今回で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 を使用して実装しました。新しいものは情報が少なく、遠回りもたくさんしましたが良い勉強になりました!
皆さんもここまで読んでいただいてありがとうございます!
修正箇所は気づき次第直します!