ふるてつのぶろぐ

福岡在住のエンジニアです。

写真提供:福岡市

Angular7 で Web アプリを作ろう - sidenav

今日すること

こんにちは、ふるてつです。
残念ながらGW10連休はあっというまに終わりました。
今回のテーマもAngular Materialで、sidenavの使い方について少し書きたいと思います。

navigation関連のリファレンスについて

リファレンスではnavigation関連で 3 つのコンポーネントが記載されています。
sidenav toolbar menuです。
f:id:tetsufuru:20190506115749p:plain:w500

その中で一番大きなくくりはsidenavになると思います。
今回sidenavの記述はapp.componentに追加します。
そしてsidenav用のコンポーネントsidenav.componentを新たに追加しようと思います。

sidenav関連のモジュールを追加

今回sidenavを使用するにあたり下記のモジュールを使用しました。
MatSidenavModule MatToolbarModule MatMenuModule MatListModuleです。
これらはapp.module.tsに追加する必要があります。
わたしはmaterial.module.tsを別途作っていますので、今回そちらにに追加しました。

material.module.tsの内容

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatCardModule } from '@angular/material/card';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTableModule } from '@angular/material/table';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatCheckboxModule } from '@angular/material/checkbox';

import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatMenuModule } from '@angular/material/menu';
import { MatListModule } from '@angular/material/list';

@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    MatGridListModule,
    MatInputModule,
    MatFormFieldModule,
    MatCardModule,
    MatButtonToggleModule,
    MatDividerModule,
    MatIconModule,
    MatButtonModule,
    MatTableModule,
    MatProgressSpinnerModule,
    MatCheckboxModule,
    MatSidenavModule,
    MatToolbarModule,
    MatMenuModule,
    MatListModule
  ],
  exports: [
    MatGridListModule,
    MatInputModule,
    MatFormFieldModule,
    MatCardModule,
    MatButtonToggleModule,
    MatDividerModule,
    MatIconModule,
    MatButtonModule,
    MatTableModule,
    MatProgressSpinnerModule,
    MatCheckboxModule,
    MatSidenavModule,
    MatToolbarModule,
    MatMenuModule,
    MatListModule
  ]
})
export class MaterialModule { }

app.component.htmlを修正

ではまずapp.component.htmlを修正します。
Bootstrapで作っていた時はコンポーネントをヘッダー、フッター、ボディに分けているのみでした。

修正前のapp.component.html

<div class="container-fluid">
  <app-header></app-header>
  <router-outlet></router-outlet>
  <app-footer></app-footer>
</div>

修正後のapp.component.html

<mat-sidenav-container>
  <!-- sidenav -->
  <mat-sidenav #sidenav mode="side">
    <app-sidenav (sidenavClose)="sidenav.close()"></app-sidenav>
  </mat-sidenav>
  <!-- main content -->
  <mat-sidenav-content>
    <app-header (sidenavToggle)="sidenav.toggle()"></app-header>
    <router-outlet></router-outlet>
    <app-footer></app-footer>
  </mat-sidenav-content>
</mat-sidenav-container>

全体を<mat-sidenav-container>でくくり、中に<mat-sidenav><mat-sidenav-content>タグを配置します。
<mat-sidenav>中には、今回新しく作るsidenav用のコンポーネントを配置します。 <mat-sidenav-content>中には、これまでのヘッダー、フッター、ボディを丸ごと配置します。

補足になりますが、<mat-sidenav>タグ中の(sidenavClose)="sidenav.close()"sidenav用のコンポーネントとの連係の為に書いています。

sidenavコンポーネントの追加

sidenav用のコンポーネントをこれから作成します。
同様にサービスも追加します。

ng generate component ./component/common/sidenav
ng generate service ./service/common/sidenavService/sidenav

sidenav.component.htmlの内容

<mat-nav-list>
  <mat-list-item routerLink="/evaluation-result" (click)="onSidenavClose()">
    <mat-icon>home</mat-icon>{{ 'menu.home' | translate }}
  </mat-list-item>

  <ng-container *ngFor="let item of availableMenuListDtoLists">
    <mat-list-item [matMenuTriggerFor]="appMenu">
      <mat-icon>arrow_drop_down</mat-icon>
      <a matline>{{ item.propertyId | translate }}</a>
    </mat-list-item>
    <mat-menu #appMenu="matMenu">
      <ng-container *ngFor="let subitem of item.availableMenuDto">
        <button mat-menu-item (click)="onSidenavClose()"
          routerLink="/{{subitem.apiName}}">{{ subitem.propertyId | translate }}
        </button>
      </ng-container>
    </mat-menu>
  </ng-container>

  <mat-list-item routerLink="/account-setting" (click)="onSidenavClose()">
    <mat-icon>account_circle</mat-icon> {{ 'menu.accountSetting' | translate }}
  </mat-list-item>

  <mat-list-item routerLink="/sign-out" (click)="onSidenavClose()">
    <mat-icon>exit_to_app</mat-icon> {{ 'menu.signOut' | translate }}
  </mat-list-item>

</mat-nav-list>

ソース全体は<mat-nav-list>でくくります。
次の3行<mat-list-item>...</mat-list-item>はホームメニューです。
次の12行は動的にメニューを作る部分で、<ng-container>...</ng-container>で繰り返し処理を行います。
メニューとサブメニューがありますので繰り返しはネストします。
さらに下 2 つの<mat-list-item>...</mat-list-item>はアカウント設定とサインアウトになります。
どのメニューもクリックしたときにonSidenavClose()メソッドを呼び出すようになっています。

sidenav.component.tsの内容

import { Component, OnInit, Output, EventEmitter } from '@angular/core';
import { SidenavService } from 'src/app/service/common/sidenav/sidenav.service';
import { AvailableMenuListDto } from 'src/app/entity/dto/available-menu-list-dto';

@Component({
  selector: 'app-sidenav',
  templateUrl: './sidenav.component.html',
  styleUrls: ['./sidenav.component.css']
})
export class SidenavComponent implements OnInit {
  @Output() sidenavClose = new EventEmitter();
  // メニュー
  public availableMenuListDtoLists: AvailableMenuListDto[];

  constructor(
    private sidenavService: SidenavService,
  ) { }
  ngOnInit() {
    // メニューを取得する。
    this.getAvailableMenu();
  }
  /**
   * メニューを取得する。
   */
  private getAvailableMenu(): void {
    this.sidenavService.getAvailableMenu()
      .subscribe(availableMenuListDtoLists => this.availableMenuListDtoLists = availableMenuListDtoLists);
  }
  /**
   * メニュー選択後に親コンポーネントに対してクローズイベントを発生する。
   */
  public onSidenavClose() {
    this.sidenavClose.emit();
  }
}

このクラスは初期表示時にSidenavServiceからメニューの内容を取得します。
他にはonSidenavClose()メソッドにてsidenavCloseイベントを発生させ、親画面にイベントを通知します(@Output() sidenavClose = new EventEmitter();

sidenav.service.tsの内容

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { TranslateService } from '@ngx-translate/core';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AppConst } from '../../../app-const';
import { environment } from '../../../../environments/environment';
import { ErrorMessageService } from '../message/error-message.service';
import { AvailableMenuListDto } from 'src/app/entity/dto/available-menu-list-dto';
@Injectable({
  providedIn: 'root'
})
export class SidenavService {
  private server = environment.production ? AppConst.URL_PROD_SERVER : AppConst.URL_DEV_SERVER;
  private webApiUrl = 'availableMenu';
  constructor(
    private http: HttpClient,
    private errorMessageService: ErrorMessageService,
    private readonly translateService: TranslateService
  ) { }
  getAvailableMenu(): Observable {
    console.log(this.server + this.webApiUrl);
    return this.http.get(this.server + this.webApiUrl, AppConst.httpOptions)
      .pipe(
        catchError(err => {
          this.errorMessageService.add(this.translateService.instant('errMessage.http'));
          console.log(err);
          return of([]);
        })
      );
  }
}

このサービスはサーバからメニュー内容(AvailableMenuListDto[])を取得します。

その他 dto などの内容

available-menu-list-dto.ts

import { AvailableMenuDto } from './available-menu-dto';
export class AvailableMenuListDto {
  public propertyId: string;
  public availableMenuDto: AvailableMenuDto[];
}

available-menu-dto.ts

export class AvailableMenuDto {
  public itemNo: number;
  public propertyId: string;
  public apiName: string;
}

app-const.ts

import { HttpHeaders } from '@angular/common/http';
export class AppConst {
  // サーバーURL
  static readonly URL_PROD_SERVER = 'http://localhost/api/';
  static readonly URL_DEV_SERVER = 'http://localhost:8080/api/';
  // ユーザ権限
  static readonly ROLE_USER = 'ROLE_USER';
  static readonly ROLE_ADMIN = 'ROLE_ADMIN';
  // httpオプション(プレフライトリクエスト)
  static readonly httpOptions = {
    headers: new HttpHeaders({
      'Content-Type': 'application/json'
    }),
    withCredentials: false
  };
}

sidenav 用 api モック作成

こちらも補足になりますが、サーバからメニューの内容を取得する api モックも作りました。
Spring Bootで若干雑に作りましたが、こちらも掲載しておきます。

AccountRestController.javaの内容

package com.weekenditlaboratory.controller;

import java.util.List;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.weekenditlaboratory.entity.dto.account.AvailableMenuListDto;
import com.weekenditlaboratory.mockService.MockAccountService;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@RestController
@RequestMapping("api/availableMenu")
@CrossOrigin
public class AccountRestController {

    private final MockAccountService mockAccountService;

    @GetMapping
    public List getAvailableMenuList() {
        return mockAccountService.getAvailableMenuList(Long.valueOf(1));
    }

}

MockAccountService.javaの内容

package com.weekenditlaboratory.mockService;

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Service;

import com.weekenditlaboratory.entity.dto.account.AvailableMenuDto;
import com.weekenditlaboratory.entity.dto.account.AvailableMenuListDto;

@Service
public class MockAccountService {

    public List getAvailableMenuList(Long accountId) {
        // 評価メニュー
        AvailableMenuDto availableMenuDto01 = new AvailableMenuDto(1, "subMenu.employeeLists","employee-lists");
        // 評価結果メニュー
        AvailableMenuDto availableMenuDto11 = new AvailableMenuDto(11, "subMenu.employeeLists", "employee-lists");
        AvailableMenuDto availableMenuDto12 = new AvailableMenuDto(12, "subMenu.evaluatedResultLists", "evaluated-result-lists");
        // 管理者メニュー
        AvailableMenuDto availableMenuDto21 = new AvailableMenuDto(21, "subMenu.companyRegistration", "company-registration");
        AvailableMenuDto availableMenuDto22 = new AvailableMenuDto(22, "subMenu.employeeRegistration", "employee-registration");
        AvailableMenuDto availableMenuDto23 = new AvailableMenuDto(23, "subMenu.employeeBatchRegistration", "employee-batch-registration");
        AvailableMenuDto availableMenuDto24 = new AvailableMenuDto(24, "subMenu.employeeLists", "employee-lists");
        AvailableMenuDto availableMenuDto25 = new AvailableMenuDto(25, "subMenu.questionRegistration", "question-registration");
        AvailableMenuDto availableMenuDto26 = new AvailableMenuDto(26, "subMenu.evaluatorRegistration", "evaluator-registration");
        AvailableMenuDto availableMenuDto27 = new AvailableMenuDto(27, "subMenu.evaluatorBatchRegistration", "evaluator-batch-registration");
        AvailableMenuDto availableMenuDto28 = new AvailableMenuDto(28, "subMenu.evaluationRuleRegistration", "evaluation-rule-registration");
        AvailableMenuDto availableMenuDto29 = new AvailableMenuDto(29, "subMenu.evaluationStatusCheck", "evaluation-status-check");
        // 全件管理者メニュー
        AvailableMenuDto availableMenuDto31 = new AvailableMenuDto(31, "subMenu.companyLists", "company-lists");
        // リストをセット
        List evaluateList = new ArrayList();
        evaluateList.add(availableMenuDto01);
        AvailableMenuListDto evaluateMenuListDto = new AvailableMenuListDto("menu.evaluate", evaluateList);

        List evaluatedResultList = new ArrayList();
        evaluatedResultList.add(availableMenuDto11);
        evaluatedResultList.add(availableMenuDto12);
        AvailableMenuListDto evaluatedResultMenuListDto = new AvailableMenuListDto("menu.evaluatedResult", evaluatedResultList);

        List administratorList = new ArrayList();
        administratorList.add(availableMenuDto21);
        administratorList.add(availableMenuDto22);
        administratorList.add(availableMenuDto23);
        administratorList.add(availableMenuDto24);
        administratorList.add(availableMenuDto25);
        administratorList.add(availableMenuDto26);
        administratorList.add(availableMenuDto27);
        administratorList.add(availableMenuDto28);
        administratorList.add(availableMenuDto29);
        AvailableMenuListDto administratorMenuListDto = new AvailableMenuListDto("menu.administrator", administratorList);

        List allAdministratorList = new ArrayList();
        allAdministratorList.add(availableMenuDto31);
        AvailableMenuListDto allAdministratorMenuListDto = new AvailableMenuListDto("menu.allAdministrator", allAdministratorList);

        List res = new ArrayList();

        res.add(evaluateMenuListDto);
        res.add(evaluatedResultMenuListDto);
        res.add(administratorMenuListDto);
        res.add(allAdministratorMenuListDto);

        return res;

    }

}

その他 dto などの内容 AvailableMenuListDto.java

package com.weekenditlaboratory.entity.dto.account;

import java.util.List;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class AvailableMenuListDto {

    private String propertyId;

    private List availableMenuDto;

}

こちらはメインメニューの dto です。
他言語化して表示したいのでpropertyIdにはAngularで持っている他言語用のjsonのプロパティを設定するようにしました。
availableMenuDtoは実際の詳細なメニューにあたる内容でList<AvailableMenuDto>にしています。

AvailableMenuListDto.java

package com.weekenditlaboratory.entity.dto.account;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class AvailableMenuDto {

    private Integer itemNo;

    private String propertyId;

    private String apiName;
}

こちらが詳細メニューの dto です。

動作確認

では動作を確認してみます。
画面を開いたときはこのような感じです。
f:id:tetsufuru:20190507205125p:plain:w500

メニューボタンを押してsidenavを開くと下記のようにサイドにメニューが表示されます。 f:id:tetsufuru:20190507205521p:plain:w500

メニューをクリックするとサブメニューが表示されます。
f:id:tetsufuru:20190507205734p:plain:w500

最後に、いずれかのメニューをクリックすると、画面が切替わりsidenavが閉じた元の状態になります。

今日の感想

今日はAngular Materialsidenavについてでした。
navigation関連のリファレンスがわたしには分かりづらく、そのためGW中盤に始めたのですが、結局GW明けになりました。

ツールバーにもメニューを置きたいですが、そこはまた別途書きたいと思います。

では、今日もお疲れ様でした。