Angular MaterialでSPAのひな型
以前、AngularとAnuglar Materialを導入しましたが、それを利用してSPA(Single Page Application)を作成している際に、その外枠(ヘッダと開閉式のサイドバー、ルーター)の構築が案外難しかったので、忘れないように書き残しておきます。
Angular環境構築のおさらい
まずNode.jsのバージョンを確認します。新しいバージョンがあったのでアップデートしました。
32bit環境の場合はnvm install バージョンの後に32を付与してください。
nvm useコマンド時はUACが作動します。またWindowsPowerShellでngコマンド実行時「ng : このシステムではスクリプトの実行が無効になっているため、ファイル ng.ps1 を読み込むことができませ ん。」というメッセージが出る場合は「ExecutionPolicyがstricted」になっていると思います。 その場合は「Set-ExecutionPolicy remotesigned」を実行します。セキュリティが気になるようでしたら、作業後にもとに戻してください。
> node -v v14.4.0 ... > nvm list available | CURRENT | LTS | OLD STABLE | OLD UNSTABLE | |--------------|--------------|--------------|--------------| | 15.5.1 | 14.15.4 | 0.12.18 | 0.11.16 | | 15.5.0 | 14.15.3 | 0.12.17 | 0.11.15 | ... nvm install 14.15.4 ... nvm use 14.15.4 ... npm install -g @angular/cli ... ng new [アプリ名] ...
ng newの処理中に次のような質問が表示され、レスポンスを求められます。
これは厳格なチェックを求めるかどうかです。コード量の少ないものならNでもいいと思います。ここではN(デフォルト)にしました。
ルーターを使うかです。つかいますので、Yにします。
Which stylesheet format would you like to use? (Use arrow keys) > CSS SCSS [ https://sass-lang.com/documentation/syntax#scss ] Sass [ https://sass-lang.com/documentation/syntax#the-indented-syntax ] Less [ http://lesscss.org ] Stylus [ https://stylus-lang.com ]
CSSの記述形式を聞いています。ここでは通常のCSSを選択します。
次にAngular Materialを導入します。ng new の引数に設定したフォルダに移動してコマンドを実行します。
ng add @angular/material
Angular Materialの設定に関してしていくつか質問がされます。
Choose a prebuilt theme name, or "custom" for a custom theme: > Indigo/Pink [ Preview: https://material.angular.io?theme=indigo-pink ] Deep Purple/Amber [ Preview: https://material.angular.io?theme=deeppurple-amber ] Pink/Blue Grey [ Preview: https://material.angular.io?theme=pink-bluegrey ] Purple/Green [ Preview: https://material.angular.io?theme=purple-green ]
オブジェクト色の確認ですメインカラーとサブカラーでCSS名となっています。Previewで表示されるリンクに表示されているアドレスに行くとAngular Materialのページがそのテーマに基づいたものになります。今後変更があるのかもしれませんが執筆時点の色の組み合わせは次の通りでした。
indigo | pink |
deeppurple | amber |
pink | bluegrey |
purple | green |
Set up global Angular Material typography styles?
Angular MaterialではGoogleのRobotoフォントを使って、typography(文字の強調表示)ができるようになっています。その設定をグローバルにするかどうかを聞いています。グローバルにすると、アプリのindex.html内のbody要素にclass="mat-typography"が設定されます。これによりinputやh、buttonなどの要素に自動的に強調表示のCSSが付与されます。
もしグローバル(index.htmlのbody)に設定したくなければ、ここでNを選択します。その場合、部分的にtypographyを使いたい箇所がでてきたらclass="mat-typography"をもった要素(div等)で囲うことで、その内側でtypographyが設定されます。ここではグローバルに設定しました。
Set up browser animations for Angular Material?
こちらはアニメーションの有無です。好みで設定してください。ここではYとしました。
Angular Material用のモジュール
通常のAngularの記法に従い、利用するAngular Materialのモジュールをひとつずつ管理することもできますが、ここではそれらをまとめたモジュールを作成します。Angular Materialの主要なモジュールをimportしてそのままexportするmaterial.module.tsを作成します。
src¥appフォルダに移動します。--flatオプションでフォルダを作成せずにモジュールを作成し、--moduleでapp.module.tsへの登録を指定します。
作成されたmaterial.module.tsにimportとexportを記述します。記述するAngular Materialのモジュールは環境に合わせて調整してください。
ng generateコマンド実行時にapp.moduleこのモジュールが登録されます。それによりアプリケーションのどこでもAngular Materialのモジュールが使えるようになります。
material.module.ts
import {NgModule} from '@angular/core';
import {MatButtonModule} from '@angular/material/button';
import {MatListModule} from '@angular/material/list';
import {MatSidenavModule} from '@angular/material/sidenav';
import {MatIconModule} from '@angular/material/icon';
import {MatToolbarModule} from '@angular/material/toolbar';
@NgModule({
exports: [
MatButtonModule,
MatListModule,
MatSidenavModule,
MatIconModule,
MatToolbarModule,
]
})
export class MaterialModule {}
ヘッダ
ページの上部に表示させるヘッダはAngular Materialのtoolbarを使って実現できます。またヘッダに次のサイドバーを呼び出すためのボタンはbuttonモジュールのひとつであるicon-buttonを利用します。
これらを実現するにはMatIconModule、MatToolbarModule、MatButtonModuleモジュールが必要になります。これは先のmaterial.module.tsでインポート、エクスポートしていることで利用可能になっています。
アイコンはbuttonはたはa要素の内側で、mat-icon要素として設定することができます。要素の内側にアイコン名を記述しますが、その種類はhttps://material.io/resources/icons/で確認ができます。
サイドバー
いわゆるサイドバーはAngular MaterialではSidenavという名前で、「mat-sidenav」という形で利用します。また似た機能に、Drawer(mat-drawer)があり、この使い分けは範囲の広いものがSidenav、狭いものがDrawerとするようです。
これは先のmaterial.module.tsで「MatSidenavModule」をインポート、エクスポートしていることで利用可能になっています。
app.component.html
ヘッダとサイドバーを含んだapp.component.htmlの記述は次のようになります。
app.component.html
<mat-sidenav-container class="sidenav"><!-- ※1 -->
<mat-sidenav #sidenav mode="over"><!-- ※2 -->
<p><button mat-icon-button (click)="sidenav.toggle()"><mat-icon>close</mat-icon></button></p>
<mat-nav-list>
<a mat-list-item routerLink="." (click)="sidenav.toggle()">メニュー1</a><!-- ※3 -->
<a mat-list-item routerLink="." (click)="sidenav.toggle()">メニュー2</a>
<a mat-list-item routerLink="." (click)="sidenav.toggle()">メニュー3</a>
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content><!-- ※4 -->
<mat-toolbar id="mat-toolbar1" color="primary" class="toolbar">
<button mat-icon-button (click)="sidenav.toggle()"><mat-icon>menu</mat-icon></button>
<h1 class="example-app-name"> {{title}}</h1>
</mat-toolbar>
<div class="content-wrapper" [style.margin-top]="headerHeight"><!-- ※5 -->
<router-outlet (activate)="onActivate($event)"></router-outlet><!-- ※6 -->
</div>
</mat-sidenav-content>
</mat-sidenav-container>
上のコード中のアンダーラインを引いた箇所の解説をしていきます。HTMLの記述に伴ったCSSやスクリプトの部分に関しては後で説明します。
- ※1
1の部分では、mat-sidenav-containerを作成しています。この中にページのサイドメニューである「mat-sidenav」と実際のページの部分である「mat-sidenav-content」を配置します。mat-sidenavではmode属性の値により、上書きや押し出しの設定が買えられます。また、クラスはmat-sidenav-containerに対して定義しCSSに記述します。
- ※2
2の部分で#(シャープ)のついたsidenavという単語が出てきます。#で設定されるものはテンプレート変数です。これは設定した名前でタグを変数化するとイメージすると考えるとわかりやすいです。これをmat-sidenavに定義することで、開閉を逆転させるメソッドtoggle()が使えるようになります。
mat-nav-listの中にmat-nav-list-itemという項目を作りメニューを設定します。
- ※3
a要素に対してrouterLink属性としてアドレスを設定すると、ルーター用のリンクになります。ルーター用のリンクはページ全体の再読み込みをしません。
- ※4
mat-sidenav-contentの中にヘッダとしてmat-toolbarを定義しています。そこに変数titleをバインドしています。またメニューを出すためのボタンも設置しています。ヘッダの部分はあとからCSSでスクロールで流れないように設定するためにclass="toolbar"を設定しています。
- ※5
ヘッダをスクロールの対象から外すと、以降の要素はその分詰めて表示されるようになりそのままでは上部が消えてしまうので、margin-topを設定して回避します。ここではスタイルに対してデータバインディングをしています。
- ※6
route-outletの中にルーターがコンポーネントを表示します。activateイベントはコンポーネントがセットされるたびに呼ばれるイベントで、イベントには中に表示されるコンポーネントが代入されます。これを使ってタイトルを変更します。この手法はQuiita:「【Angular】router-outlet で呼び出されたComponentのメソッドを利用する」を参考にさせていただきました。
app.component.ts
app.component.tsではヘッダに表示するtitleと、ヘッダを固定するにあたり表示を調整するためのheaderHeightを設定します。
ヘッダは環境により高さが変わるため、ライフライクルフックの、ngAfterViewInitで取得します。ただ、一連の初期化中にオブジェクトのプロパティを変えてしまうと「ExpressionChangedAfterItHasBeenCheckedError」がでるので、setTimeout()関数をかませています。この方法はQuiita:「[Angular] ExpressionChangedAfterItHasBeenCheckedError への2つの対処法」を参考にさていただきました。
また、HTMLのrouter outlet内で設定した関数onActivateを実装しています。引数にはルーターで表示されるコンポーネントが入ってくるので、子コンポーネントにtitleプロパティがあったら、親(app.component.ts)のtitleを変更するという仕組みになっています。
app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'blankframe';//※4
headerHeight = "0px";//※5
ngAfterViewInit():void {
//※5
//ヘッダの高さに合わせてコンテンツの位置を修正
//画面レイアウト終了後にヘッダの高さを取得
let htmls = document.getElementsByTagName("mat-toolbar");
//初期化処理の一連の流れの中で、一度設定したスタイルを変えると
//ExpressionChangedAfterItHasBeenCheckedErrorがでるので、
//settimeoutを設ける
setTimeout(() => {
this.headerHeight=htmls[0].scrollHeight+"px";
});
}
onActivate(event: any): void {
//※6
//routerで表示したコンポーネントからtitleを取得する
if (event.hasOwnProperty('title')) {
this.title=event.title;
} else {
this.title="";
}
}
}
app.component.css
app.component.cssでは、サイドメニューにabsoluteを指定、ヘッダにfixedを指定しています。
app.component.css
/* ヘッダ */
.toolbar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
/* サイドバー */
.sidenav {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
ちなみに、別ページではAngular Materialのテーブルの使い方にも触れていますので、よろしかったらそちらもご覧になってください。
参考にさせていただいたサイトの皆様ありがとうございました。