【使用Spring Boot + Angular + MySQL创建网络应用程序】 Angular部分第二部分

本次将使用Spring Boot创建的后端与之进行通信,并在屏幕上显示员工信息列表。

首先,我们进行与服务器通信所需的准备工作。
我们在app.module.ts中导入HttpClientModule。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MaterialModule } from './material/material.module';
import { FlexLayoutModule } from '@angular/flex-layout';
import { HomeComponent } from './home/home.component';
import { HttpClientModule } from '@angular/common/http'; //追加

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    BrowserAnimationsModule,
    MaterialModule,
    FlexLayoutModule,
    HttpClientModule //追加
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

创建Dto

我们将创建一个用于存储与后端通信时返回值的Dto。
虽然原本应该将其放在不同的文件夹中,但是这次我们直接将其写在home.component.ts中。

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {

  constructor() { }

  ngOnInit(): void {
  }

}

/**
 * 社員
 */
export class Employee {
  id: number;
  name: string;
  department_id: number;
  birthday: Date;
}

/**
 * 部署
 */
export class Department {
  id: number;
  name: string;
}

/**
 * 検索結果格納用
 */
export class EmployeeDepartment {
  employeeId: number;
  employeeName: string;
  departmentName: string;
  birthday: string;
}

/**
 * 初期処理結果格納用
 */
export class HomeInitResultDto {
  employeeList: Array<EmployeeDepartment>
  departmentList: Array<Department>
}

服务的创建

我会创建负责处理后端和通信的Service。

ng g s api

在api.service.ts中编写以下内容。

import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HomeInitResultDto } from './home/home.component';

@Injectable({
  providedIn: 'root'
})
export class ApiService {

  constructor(private http: HttpClient) { }

  private api: string = '/api'

  private httpOptions: any = {

    headers: new HttpHeaders({
      'Content-Type': 'application/json'
    }),
    body: null
  };

  /**
   * 初期化
   */
  homeInit(): Observable<HomeInitResultDto> {
    return this.http.get<HomeInitResultDto>(this.api + '/Home/init');
  }
}

增加FormsModule

由于使用了mgModel,我们需要在app.module.ts中添加FormsModule。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MaterialModule } from './material/material.module';
import { FlexLayoutModule } from '@angular/flex-layout';
import { HomeComponent } from './home/home.component';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms'; //追加

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    BrowserAnimationsModule,
    MaterialModule,
    FlexLayoutModule,
    HttpClientModule,
    FormsModule //追加
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

反向代理设置

由於這次開發的應用程式的前端和後端的端口號碼不同,導致CORS問題,通信失敗。
因此需要進行反向代理的設定。
在Angular專案的example目錄下創建proxy.conf.json檔案。

{
    "/api": {
        "target": "http://localhost:8080",
        "pathRewrite": {
            "^/api": ""
        }
    }
}

我会在package.json中添加使用proxy.conf的设置。

{
  "name": "example",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve --proxy-config proxy.conf.json", // 追加
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "~9.1.13",
    "@angular/cdk": "^9.2.4",
    "@angular/common": "~9.1.13",
    "@angular/compiler": "~9.1.13",
    "@angular/core": "~9.1.13",
    "@angular/forms": "~9.1.13",
    "@angular/material": "^9.2.4",
    "@angular/platform-browser": "~9.1.13",
    "@angular/platform-browser-dynamic": "~9.1.13",
    "@angular/router": "~9.1.13",
    "rxjs": "~6.5.4",
    "tslib": "^1.10.0",
    "zone.js": "~0.10.2"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "~0.901.13",
    "@angular/cli": "~9.1.13",
    "@angular/compiler-cli": "~9.1.13",
    "@types/node": "^12.11.1",
    "@types/jasmine": "~3.5.0",
    "@types/jasminewd2": "~2.0.3",
    "codelyzer": "^5.1.2",
    "jasmine-core": "~3.5.0",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~5.0.0",
    "karma-chrome-launcher": "~3.1.0",
    "karma-coverage-istanbul-reporter": "~2.1.0",
    "karma-jasmine": "~3.0.1",
    "karma-jasmine-html-reporter": "^1.4.2",
    "protractor": "~7.0.0",
    "ts-node": "~8.3.0",
    "tslint": "~6.1.0",
    "typescript": "~3.8.3"
  }
}

這樣反向代理的設定就完成了。

制作图像

我将创建一个员工列表页面。
首先,从home.component.ts开始进行修改。

import { SelectionModel } from '@angular/cdk/collections';
import { Component, OnInit } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { ApiService } from '../api.service';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {

  constructor(private apiService: ApiService) { }

  employeeList: Array<Employee> = [];
  departmentList: Array<Department> = [];
  searchId: string = "";
  searchName: string = "";
  searchFromDate: string = "";
  searchToDate: string = "";
  panelOpenState = false;
  displayedColumns: string[] = ['select', 'id', 'name', 'department', 'birthday'];
  dataSource = new MatTableDataSource<EmployeeDepartment>();
  selection = new SelectionModel<EmployeeDepartment>(true, []);

  searchDepartmentIndex = "0";

  /**
   * 初期処理
   */
  ngOnInit(): void {
    this.apiService.homeInit().subscribe(
      result => {
        this.dataSource.data = result.employeeList;
        this.departmentList = result.departmentList;
        console.log(JSON.stringify(this.dataSource.data))
      }
    )
  }

  /**
   * 検索ボタン押下
   */
  onSearch() {

  }

  /**
   * リセットボタン押下
   */
  onReset() {

    this.searchId = "";
    this.searchName = "";
    this.searchFromDate = "";
    this.searchToDate = "";
    this.onSearch();
  }

  /**
   * 行選択
   * @param employee 
   */
  rowClick(employee: EmployeeDepartment): void {

  }

  /**
   * 削除ボタン押下
   */
  onDelete() {

  }

  onChange(deviceValue) {
    this.searchDepartmentIndex = deviceValue;
    console.log(deviceValue)
  }

  /**
   * 新規作成ボタン押下
   */
  onCreate() {

  }

  /** Whether the number of selected elements matches the total number of rows. */
  isAllSelected() {
    const numSelected = this.selection.selected.length;
    const numRows = this.dataSource.data.length;
    return numSelected === numRows;
  }

  /** Selects all rows if they are not all selected; otherwise clear selection. */
  masterToggle() {
    this.isAllSelected() ?
      // チェック開放
      this.selection.clear() :
      // 全部にチェック
      this.dataSource.data.forEach(row => this.selection.select(row));
  }

  /** The label for the checkbox on the passed row */
  checkboxLabel(row?: EmployeeDepartment): string {
    if (!row) {
      return `${this.isAllSelected() ? 'select' : 'deselect'} all`;
    }
    return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${row.employeeId + 1}`;
  }

}

/**
 * 社員
 */
export class Employee {
  id: number;
  name: string;
  department_id: number;
  birthday: Date;
}

/**
 * 部署
 */
export class Department {
  id: number;
  name: string;
}

/**
 * 検索結果格納用
 */
export class EmployeeDepartment {
  employeeId: number;
  employeeName: string;
  departmentName: string;
  birthday: string;
}

/**
 * 初期処理結果格納用
 */
export class HomeInitResultDto {
  employeeList: Array<EmployeeDepartment>
  departmentList: Array<Department>
}

下一步将修改home.component.html文件。

<div fxFill fxLayout="row">
    <div fxFlex>
        <div>
            <mat-card style="margin: 10px 10px 10px 10px;" class="mat-elevation-z5">
                <table width="80%" style="margin: 0 auto;">
                    <tr>
                        <th style="background-color: lightgreen; font-weight: lighter;">
                            社員ID
                        </th>
                        <td align="left"><input type="text" class="input" maxlength="5" [(ngModel)]="searchId"
                                oninput="value = value.replace(/[^0-9]+/i,'');"></td>
                        <th style="background-color: lightgreen; font-weight: lighter;">
                            社員名
                        </th>
                        <td align="left"><input type="text" class="input" [(ngModel)]="searchName"></td>
                    </tr>
                    <tr>
                        <th style="background-color: lightgreen; font-weight: lighter;">
                            部署
                        </th>
                        <td align="left">

                            <select id="language" name="language" class="input">
                                <option disabled="disabled" selected [value]="">選択してください</option>
                                <option *ngFor="let department of departmentList" [value]="department.id">
                                    {{department.name}}</option>
                            </select>
                        </td>
                        <th style="background-color: lightgreen; font-weight: lighter;">
                            生年月日
                        </th>
                        <td align="left"><input type="date" class="input" [(ngModel)]="searchFromDate">~
                            <input type="date" class="input" [(ngModel)]="searchToDate">
                        </td>
                    </tr>
                </table>
                <div style="width: 400px; margin-left: auto;">
                    <button mat-raised-button (click)="onSearch()">
                        <mat-icon>search</mat-icon>検                    </button>
                    <button mat-raised-button (click)="onReset()">
                        <mat-icon>refresh</mat-icon>リセッ                    </button>
                    <button mat-raised-button (click)="onDelete()">
                        <mat-icon>delete</mat-icon>削                    </button>
                    <button mat-raised-button (click)="onCreate()">
                        <mat-icon>create</mat-icon>新規作                    </button>
                </div>
            </mat-card>
        </div>
        <div>
            <mat-card style="margin: 10px" class="mat-elevation-z5">
                <table mat-table [dataSource]="dataSource" fxFlexFill>

                    <ng-container matColumnDef="select">
                        <th mat-header-cell *matHeaderCellDef style="width: 80px;">
                            <mat-checkbox (change)="$event ? masterToggle() : null"
                                [checked]="selection.hasValue() && isAllSelected()"
                                [indeterminate]="selection.hasValue() && !isAllSelected()"
                                [aria-label]="checkboxLabel()">
                            </mat-checkbox>
                        </th>
                        <td mat-cell *matCellDef="let row">
                            <mat-checkbox (click)="$event.stopPropagation()"
                                (change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)"
                                [aria-label]="checkboxLabel(row)">
                            </mat-checkbox>
                        </td>
                    </ng-container>

                    <ng-container matColumnDef="id">
                        <th mat-header-cell *matHeaderCellDef>社員Id</th>
                        <td mat-cell *matCellDef="let element"> {{element.employeeId}} </td>
                    </ng-container>

                    <ng-container matColumnDef="name">
                        <th mat-header-cell *matHeaderCellDef>社員名</th>
                        <td mat-cell *matCellDef="let element"> {{element.employeeName}} </td>
                    </ng-container>

                    <ng-container matColumnDef="department">
                        <th mat-header-cell *matHeaderCellDef>部署</th>
                        <td mat-cell *matCellDef="let element"> {{element.departmentName}} </td>
                    </ng-container>

                    <ng-container matColumnDef="birthday">
                        <th mat-header-cell *matHeaderCellDef>生年月日</th>
                        <td mat-cell *matCellDef="let element"> {{element.birthday | date:"yyyy年MM月dd日"}} </td>
                    </ng-container>

                    <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"
                        style="background-color: lightgreen;">
                    </tr>
                    <tr mat-row *matRowDef="let row; columns: displayedColumns;" (click)="rowClick(row)"></tr>
                </table>
            </mat-card>
        </div>
    </div>
</div>

我会修改style.scss。

/* You can add global styles to this file, and also import other style files */

html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }

.header{
    height: 50px;
    background-color: lightgreen;
}

.footer{
    background-color: lightgreen;
    height: 30px;
    text-align: center;
}

// 追加
.input{
    border:0;
    padding:5px;
    font-size:1em;
    font-family:Arial, sans-serif;
    color:rgb(82, 79, 79);
    border:solid 1px #ccc;
    margin:0 0 10px;
    width:200px;

    -webkit-border-radius: 3px;
    -moz-border-radius: 3px;
    border-radius: 3px;
}

// 追加
.mat-row:hover {
    background-color: lightgray;
    cursor : pointer;
}

执行应用程序

输入以下命令以启动应用程序。

npm start
image.png

如果出现这样的屏幕,就可以了。

下一步,我们将实现注册、更新、删除和搜索功能。【使用Spring Boot + Angular + MySQL创建Web应用程序】CRUD教程第1部分。

广告
将在 10 秒后关闭
bannerAds