【Angular】Angular Materialでのハンバーガーメニュー

 今回はよくスマホなどで使用されるハンバーガーメニューを実装していきます。ハンバーガーメニューとはヘッダーなどに置かれているメニューボタンを押すと横から出てくるサイドメニューのことです。

 完成しているコードは下記にあります。

Angular-sample/hamburger-menu at main · tsuneken5/Angular-sample
Contribute to tsuneken5/Angular-sample development by creating an account on GitHub.

環境

  • node: 18.13.0
  • Angular CLI:15.0.0
  • Angular Material:15.2.9

プロジェクトの作成

$ ng new hamburger-menu

Angular Materialのインストール

$ ng add @angular/material

コンポーネントの生成

$ ng generate component header
$ ng generate component side-menu
$ ng generate component content
$ ng generate component page-a
$ ng generate component page-b
  • header ・・・ ヘッダー
  • side-menu ・・・ サイドメニュー
  • content, page-a, page-b ・・・ ページ遷移確認用のページ

モジュールのインポート

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

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ContentComponent } from './content/content.component';
import { HeaderComponent } from './header/header.component';
import { SideMenuComponent } from './side-menu/side-menu.component';
import { PageAComponent } from './page-a/page-a.component';
import { PageBComponent } from './page-b/page-b.component';

import { MatToolbarModule } from '@angular/material/toolbar'; // 追加
import { MatIconModule } from '@angular/material/icon';       // 追加
import { MatSidenavModule } from '@angular/material/sidenav'; // 追加
import { MatListModule } from '@angular/material/list';       // 追加

@NgModule({
  declarations: [
    AppComponent,
    ContentComponent,
    HeaderComponent,
    SideMenuComponent,
    PageAComponent,
    PageBComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    BrowserAnimationsModule,
    MatToolbarModule,   // 追加
    MatIconModule,      // 追加
    MatSidenavModule,   // 追加
    MatListModule       // 追加
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
  • MatToolbarModule ・・・ ヘッダーに使用
  • MatIconModule ・・・ サイドメニューの表示・非表示のアイコンに使用
  • MatSidenavModule ・・・ 折りたたみ可能なサイドメニュー用のモジュール
  • MatListModule ・・・ サイドメニューにメニューを表示するために使用

ルーティング

import { Component, NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { ContentComponent } from './content/content.component'; // 追加
import { PageAComponent } from './page-a/page-a.component';     // 追加
import { PageBComponent } from './page-b/page-b.component';     // 追加

const routes: Routes = [  // 更新
  { path: '', component: ContentComponent },
  { path: 'a', component: PageAComponent },
  { path: 'b', component: PageBComponent },

];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

テンプレートコンポーネント

<div class="example-container">

  <!-- ヘッダー-->
  <app-header (sidenavToggled)="sidenav.toggle()" [sidenav]="sidenav"></app-header>

  <mat-sidenav-container class="example-sidenav-container">

    <!-- メインコンテンツ -->
    <mat-sidenav-content>
      <router-outlet></router-outlet>
    </mat-sidenav-content>

    <!-- サイトメニュー -->
    <mat-sidenav mode="over" #sidenav>
      <app-side-menu></app-side-menu>
    </mat-sidenav>

  </mat-sidenav-container>

</div>
.example-container {
  display: flex;
  flex-direction: column;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}

.example-sidenav-container {
  flex: 1;
}
import { Component, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { MatSidenav } from '@angular/material/sidenav';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'humbuger-menu';

  @ViewChild('sidenav')
  private sidenav!: MatSidenav

  constructor(
    private readonly _router: Router,
  ) {
    this._router.events.subscribe(() => {
      this.sidenav.close();
    });
  }
}

app.component.html

 <mat-sidenav-container><mat-sidenav-content> <mat-sidenav> を囲みます。 <mat-sidenav-content> はメインコンテンツ、 <mat-sidenav> はサイドメニューを表しています。

4行目

 ヘッダー部分です。ヘッダーでサイドメニューの開閉を検知してアイコンを変更したいので、14行目で宣言している参照変数 #sidenav<mat-sidenav> をヘッダーに渡しています。

 また、 (sidenavToggled)="sidenav.toggle()" でヘッダー内の sidenavToggled と、sidenav.toggle()sidenav は14行目の #sidenav )とを紐づけます。 toggle()<mat-sidenav> で定義されているメソッドで、サイドメニューの開閉を操作するメソッドになります。

14~16行目

 mode="over" でサイドメニューがオーバーレイで表示されるようになります。 <mat-sidenav> を TypeScript 上で参照できるように #sidenav と参照変数を宣言しています。

app.component.ts

13~14行目

 html で宣言した参照変数 #sidenav を読み込みます。

16~22行目

 ページ遷移をしたときにサイドメニューを閉じるようにしてます。

サイドメニューの実装

<mat-nav-list>
  <a mat-list-item *ngFor="let link of naviLinks" routerLink="{{link.location}}">
    {{link.label}}</a>
</mat-nav-list>
import { Component } from '@angular/core';

@Component({
  selector: 'app-side-menu',
  templateUrl: './side-menu.component.html',
  styleUrls: ['./side-menu.component.css']
})
export class SideMenuComponent {
  naviLinks = [
    { location: '', label: 'top' },
    { location: 'a', label: 'page a' },
    { location: 'b', label: 'page b' }
  ];
}

side-menu.component.html

 <mat-list> を使ってメニューを表示しています。

side-menu.component.ts

9~13行目

 サイドメニューで表示するメニューの配列を宣言しています。リンクにアイコンがほしいのであれば icon のような項目を追加してやるといいです。

ヘッダーの実装

<mat-toolbar color="primary">
  <mat-icon mat-icon-button class="header-icon" id="header-icon" (click)="toggle()">menu</mat-icon>
  <span>Angular Sample</span>
</mat-toolbar>
.header-icon {
  margin-right: 20px;
}
import { Component, Output, Input, EventEmitter } from '@angular/core';
import { MatSidenav } from '@angular/material/sidenav';

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.css']
})
export class HeaderComponent {

  @Output() sidenavToggled = new EventEmitter<{}>();
  @Input() sidenav!: MatSidenav;

  private icons = {
    opened: 'close',
    closed: 'menu'
  }

  toggle(): void {
    this.sidenavToggled.emit({});
  }

  private subscribeToSidenav() {
    const icon = document.querySelector('#header-icon') as HTMLElement;

    this.sidenav.openedStart.subscribe(() => {
      icon.innerHTML = this.icons.opened;
    });

    this.sidenav.closedStart.subscribe(() => {
      icon.innerHTML = this.icons.closed;
    });
  }

  ngOnInit() {
    this.subscribeToSidenav();
  }
}

header.component.ts

11行目

 @Output を使うことで子コンポーネントから親コンポーネントのイベントを呼び出せるようになります。 sidenavToggledapp.component.htmlsidenav.toggle() と紐づけていますので、子コンポーネントであるヘッダーから sidenav.toggle() が呼び出せるようになります。

12行目

 @Input は親コンポーネントから値を受け取ります。こちらも app.component.html で宣言したように、<mat-sidenav> を受け取っています。

19~21行目

 アイコンのクリックイベントです。 this.sidenavToggled.emit({})sidenavToggled に紐づいている sidenav.toggle() を実行します。

26~28行目

 openedStart メソッドを使用して <mat-sidenav> のopenイベントの発生を検知してアイコンを切り替えています。

30~32行目

 closedStart メソッドを使用して <mat-sidenav> のcloseイベントの発生を検知してアイコンを切り替えています。

確認

$ ng serve

 ブラウザから「http://localhost:4200/」にアクセスすると確認できます。

サイドメニューが閉じている状態

サイドメニューが開いている状態

最後に・・・

 今回は簡単に説明しましたが、そのうち @ViewChild() とか、 @Input() , @Output() についての解説記事を書きたいですね。解説できるぐらいに勉強せねば・・・

コメント

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