Angular-Material × Flex-Layoutで業務アプリケーションの大枠を作成する

# 概要
大体の記事ではroot-moduleやroot-componentにもろもろを詰め込むようなデザインが多いので、拡張性を意識したデザインでデータの一覧表を表示するような業務アプリケーションを作成してみたいと思います。

※省略していないコードは最後にgithubのURLを載せておきます。

#環境
- Windows10 pro (Homeでも可)
- Visual Studio Code
- Docker version 20.10.8
- Docker Compose version v2.0.0

以下、Visual Studio Codeの拡張機能
- Docker v1.17.0
- Remote - Containers v0.202.5

#構成
- rootとなるモジュールにapp.module.tsを配置し、その子モジュールとしてmodulesディレクトリの下にmainとsharedモジュールを配置しています。開発が進むにつれてauth（認証）系のモジュールやmaster（管理）系のモジュールを追加していくことが想定できます。
- mainモジュールでは、mainコンポーネントをモジュール内のrootコンポーネントとして配置し、ルーティングによってmainコンポーネント内の<router-outlet>部分が変化させます。
- sharedモジュールはモジュール間で共有しながら使用するコンポーネントを配置していくのでrootとなるコンポーネントはありません。

```:フォルダツリー
app
├── app.component.css
├── app.component.html
├── app.component.ts
├── app.module.ts
├── app-routing.module.ts
└── modules
    ├── main
    │   ├── main.module.ts
    │   ├── main.routing.module.ts
    │   └── components
    │       ├── main
    │       │   ├── main.component.css
    │       │   ├── main.component.html
    │       │   └── main.component.ts
    │       └── table
    │           ├── table.component.css
    │           ├── table.component.html
    │           └── table.component.ts
    └── shared
        ├── shared.module.ts
        └── components
            └── header
                ├── header.component.css
                ├── header.component.html
                └── header.component.ts
```

#Docker関連の構成
<details><summary>Docker未習得の方は飛ばして大丈夫です</summary><div>

```docker:Dockerfile
FROM node:12-slim

WORKDIR /usr/src/client-app

RUN apt-get update
RUN echo y | apt-get install git
RUN npm install -g @angular/cli

COPY ./ ./

EXPOSE 4200
```

```:.dockerignore
node_modules
```

</div></details>

# 親モジュール関連の生成
親モジュールでは主に子モジュールのインポートや設定、サイドナビゲーションのコンテナの設定等を行います。

## root moduleの生成
aoo.module.tsファイルには子モジュールや共有モジュール等を記載します。後々解説しますが、コンポーネントを共有して使用する場合、rootモジュールに共有するモジュールをインポートする必要があるので注意してください（[参考：[Angular]module間でコンポーネントを共有する](#参考)）

※見やすさを優先してMaterial等のライブラリを省略しています

```typescript:app.module.ts
import { FlexLayoutModule } from '@angular/flex-layout';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { MainModule } from './modules/main/main.module';
import { SharedModule } from './modules/shared/shared.module';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    AppRoutingModule,
    FlexLayoutModule,
    MainModule,
    SharedModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
```

## app-routing.module.tsの生成
rootモジュールについてのルーティングを設定していきます。
ここでは基本的に子モジュールについてをつらつらと追加していくだけです。共有モジュールはルーティングしないので記載する必要はありません。
pathの箇所には` loadChildren: () => モジュール` と記載することで子モジュールのroutingをまとめて追加できます。

```typescript:app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
// 以下、子モジュールを追加していく
import { MainModule } from './modules/main/main.module';

const routes: Routes = [
  { path: '', loadChildren: () => MainModule },
  // { path: 'hoge', loadChildren: () => HogeModule },
];

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

## rootコンポーネントの生成
rootとなるコンポーネントにはサイドナビゲーションのコンテナを生成します。
大事なのは`openedフィールド`と`toggleSidenavメソッド`です。後々追加するヘッダーコンポーネントからこの#toggleSidenavを呼び出すことによってサイドナビゲーションを開閉させます。
※今回はViewのみの作成なのでメニュー内容は適当です。
※ヘッダーの左上のボタンを押すと左側から展開されるメニューです

```typescript:app.component.ts
import { Component } from '@angular/core';

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

  //メニューの開閉に関するフィールドとメソッド
  opened:boolean = false;
  public toggleSidenav(){
    this.opened = !this.opened;
  }

  // 以下、メニューの要素
  folders: Section[] = [
    { name: 'Photos', },
    { name: 'Recipes', },
    { name: 'Work', },
  ];
  notes: Section[] = [
    { name: 'Vacation Itinerary', },
    { name: 'Kitchen Remodel', },
  ];
}
export interface Section {
  name: string;
}

```

htmlファイルでは大きく`mat-sidenav-container`を配置し、サイドナビゲーションの記載をします。
`mat-sidenavタグ`でサイドナビゲーションを開閉するメソッドの記載を忘れないように注意してください。
routingによって変化する部分（router-outlet）は`mat-sidenav-contentタグ`の中に挿入します。

```html:app.component.html
<mat-sidenav-container>
    <mat-sidenav class="sidenav" [(opened)]="opened" mode="push">

        <!-- コンテンツの一覧 -->
        <mat-selection-list [multiple]="false">
            <div mat-subheader>list's</div>
            <mat-list-option *ngFor="let folder of folders">
              <mat-icon mat-list-icon>grading</mat-icon>
              <div mat-line>{{folder.name}}</div>
            </mat-list-option>
            <mat-divider></mat-divider>
            <div mat-subheader>Master menu</div>
            <mat-list-option *ngFor="let note of notes">
              <mat-icon mat-list-icon>description</mat-icon>
              <div mat-line>{{note.name}}</div>
            </mat-list-option>
        </mat-selection-list>

    </mat-sidenav>
    <!-- ※ここが重要 -->
    <mat-sidenav-content>
        <router-outlet></router-outlet>
    </mat-sidenav-content>
</mat-sidenav-container>
```

cssファイルではサイドナビゲーションのサイズを設定します。こちらは状況や好みに合わせてhtmlに記載しても良いです。
※完成時の確認用に適当な色を設定しています。

```css:app.component.css
.sidenav {
    width: 25%;
    padding: 20px;
}
  
/* 確認用 */
.sidenav {
    background-color: gold;
}
```

# 子モジュール関連の生成
子モジュールでは他の多くの記事にあるような流れでモジュールを生成していきます。
ただ一つ、子モジュールについてのルーティングファイルの書き方が少し異なるので注意してください。

## main.module.tsの生成
ここでは子モジュール内で使用するライブラリやコンポーネントをそれぞれインポートします。
※見やすさを優先してMaterial等のライブラリを省略しています

```typescript:main.module.ts
import { MainRoutingModule } from './main.routing.module';
import { FlexLayoutModule } from '@angular/flex-layout';
import { SharedModule } from '../shared/shared.module';

import { MainComponent } from './components/main/main.component';
import { TableComponent } from './components/table/table.component';

@NgModule({
  declarations: [
    MainComponent,
    TableComponent
  ],
  imports: [
    MainRoutingModule,
    FlexLayoutModule,
    SharedModule,
  ]
})
export class MainModule { }
```

## main.routing.module.tsの生成
ルーティングでは基本の書き方と少し異なるので注意してください。
まず、pathの箇所ではまずおおもとのpath（今回は`''`）を記載し、そこからさらに`children`を展開させるイメージです。なので、子モジュール内でルーティングを追加する場合は`children:[~~]`内に記載していきます。
次に`imports:`の箇所ではモジュール以降が`.forChild(routes)`となっていることに注意してください。

```typescript:main.routing.module.ts
import { MainComponent } from './components/main/main.component';
import { TableComponent } from './components/table/table.component';

const routes: Routes = [
  { path: '', component: MainComponent,
    children:[
      { path: 'table', component: TableComponent },
    ]
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class MainRoutingModule { }
```

## mainコンポーネントの生成
mainコンポーネントではコンテナの設定とヘッダーの配置がメインになります。

今回のmain.component.tsファイルでは特に処理はしないので実質空っぽの状態です。

```typescript:main.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-main',
  templateUrl: './main.component.html',
  styleUrls: ['./main.component.css']
})
export class MainComponent {
}
```
main.component.htmlファイルではコンテナを設定し、ルーティングによって変化させる`router-outlet`を配置しています。
いくつか記事が出されていますが、`router-outlet`はページが生成された際、そのタグと同じ階層でコンポーネントが追加される使用になるので、css等で高さなどを設定するためには`router-outlet`を`<div>`で囲む必要があるので注意してください。

```html:main.component.html
<div class="main-container">

    <app-header></app-header>

    <!-- ※ここ注意 -->
    <div class="component-area">
        <router-outlet></router-outlet>
    </div>  
</div>
```
ここではFlexlayoutを利用して
ヘッダーとコンテンツのエリアを縦に並べます。コンポーネントのエリアは画面のからヘッダー分を引いた残りの高さで配置する必要があるため、`.component-area`の箇所で`flex: 1;`と指定しています。
また、Flexlayoutでの最小の高さ（min-height）はデフォルト値がコンテンツの値になっているので、`height: 0%`等の明示がないと、テーブルなどコンテンツの高さが画面より大きな要素が入った際にスクロール表示されないので注意してください。

```css:main.component.css
.main-container {
    height: 100vh;
    display: flex;
    flex-direction: column;
}
.component-area {
    height: 0%;
    display: flex;
    flex-direction: column;
    flex: 1;
}
```

# テーブルコンテンツの生成
今回は適当にページネーションされたテーブルを表示します。業務アプリケーションでは一般的な画面かと思います。
作成手順等は別記事に書こうと思いますので、詳細は省きます。

<details><summary>適当に読み飛ばして大丈夫です</summary><div>

```typescript:table.component.ts
import { Component, AfterViewInit, ViewChild } from '@angular/core';
// Materials
import { MatTableDataSource } from '@angular/material/table'
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';

@Component({
  selector: 'app-table',
  templateUrl: './table.component.html',
  styleUrls: ['./table.component.css']
})
export class TableComponent implements AfterViewInit {

  displayedColumns: string[] = ['action', 'position', 'name', 'weight', 'symbol'];
  dataSource = new MatTableDataSource<PeriodicElement>(ELEMENT_DATA);
  sortedData: MatTableDataSource<PeriodicElement>;

  @ViewChild(MatPaginator) paginator: MatPaginator;
  @ViewChild(MatSort) sort: MatSort;

  ngAfterViewInit(){
    this.dataSource.sort = this.sort;
    this.dataSource.paginator = this.paginator;
  }
}

export interface PeriodicElement {
  name: string;
  position: number;
  weight: number;
  symbol: string;
}
const ELEMENT_DATA: PeriodicElement[] = [
  {position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'},
  {position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'},
  {position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'},
  // コピペで３０行分ほど生成
  {position: 30, name: 'Neon', weight: 20.1797, symbol: 'Ne'},
];
```

```html:table.component.html
<mat-expansion-panel [expanded]="false">
    <!-- Panel's Header -->
    <mat-expansion-panel-header>
        <mat-panel-title>
        Search condition area
        </mat-panel-title>
    </mat-expansion-panel-header>
    <!-- Conditions -->
    <mat-form-field appearance="fill">
        <mat-label>Condition 1</mat-label>
        <input matInput>
    </mat-form-field>
    <mat-form-field appearance="fill">
        <mat-label>Condition 2</mat-label>
        <input matInput>
    </mat-form-field>
    <mat-form-field appearance="fill">
        <mat-label>Condition 3</mat-label>
        <input matInput>
    </mat-form-field>
</mat-expansion-panel>
<!-- table -->
<div class="table-container">
    <table mat-table [dataSource]="dataSource" matSort>

        <!-- Icon Column -->
        <ng-container matColumnDef="action" sticky>
            <tr><th mat-header-cell *matHeaderCellDef> Action </th></tr>
            <tr>
              <td mat-cell *matCellDef="let row">
                <button mat-icon-button><mat-icon>mode_edit_outline</mat-icon></button>
              </td>
            <tr>
        </ng-container>

        <!-- Position Column -->
        <ng-container matColumnDef="position" sticky>
            <th mat-header-cell *matHeaderCellDef mat-sort-header> No </th>
            <td mat-cell *matCellDef="let element"> {{element.position}} </td>
        </ng-container>

        <!-- Name Column -->
        <ng-container matColumnDef="name">
            <th mat-header-cell *matHeaderCellDef mat-sort-header> Name </th>
            <td mat-cell *matCellDef="let element"> {{element.name}} </td>
        </ng-container>

        <!-- Weight Column -->
        <ng-container matColumnDef="weight">
            <th mat-header-cell *matHeaderCellDef mat-sort-header> Weight </th>
            <td mat-cell *matCellDef="let element"> {{element.weight}} </td>
        </ng-container>

        <!-- Symbol Column -->
        <ng-container matColumnDef="symbol">
            <th mat-header-cell *matHeaderCellDef mat-sort-header> Symbol </th>
            <td mat-cell *matCellDef="let element"> {{element.symbol}} </td>
        </ng-container>

        <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
        <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
    </table>
</div>
<!-- paginator -->
<mat-paginator class="mat-paginator-sticky"
            [pageSizeOptions]="[5, 10, 50]"
            showFirstLastButtons
            aria-label="Selectpage">
</mat-paginator>
```

```css:table.component.css
:host {
    height: 0%;
    display: flex;
    flex-direction: column;
    flex: 1;
}
/* フォームフィールド */
mat-form-field{
    margin-right: 1%;
}
/* テーブルエリア */
.table-container {
    flex: 1;
    overflow: scroll;
}
table {
    width: 100%;
}
.mat-header-cell {
    text-align: center;
}
.mat-column-action {
    width: 5%;
    padding: 0 2%;
    text-align: center;
}
.mat-column-position {
    width: 5%;
    padding: 0 2%;
    text-align: center;
}
.mat-table-sticky-border-elem-left {
    border-right: 1px solid #000;
}
.mat-column-name {
    padding-left: 2%;
}
/* 確認用 */
mat-expansion-panel {
    background-color: cadetblue;
}
mat-paginator {
    background-color: cyan;
}
```

</div></details>


# 共有モジュール関連の生成
共有モジュールについては（[[Angular]module間でコンポーネントを共有する
](#参考)）を参考に共有して使用できるコンポーネントの整備をしていきます。

## shared.module.tsの生成
いつものように使うライブラリ等をimportしてください。
注意点としては`exports:`の箇所に共有して使用したいコンポーネントの記載を忘れないようにしてください。

```typescript:shared.module.ts
// Material等は省略
import { HeaderComponent } from './components/header/header.component';

@NgModule({
  declarations: [
    HeaderComponent,
  ],
  imports: [
    FlexLayoutModule,
    // 省略
  ],
  exports: [
    HeaderComponent,
  ]
})
export class SharedModule { }
```

## 共有コンポーネントの生成
今回はヘッダー部分をコンポーネント化して共有利用しようと思います。
ヘッダーについては共有コンポーネントではなく、rootコンポーネントに配置して画面に応じてオン・オフを切り替えているような記事等はよく見かけるのですが、それだとオン・オフの管理が面倒になるのでmodule単位で管理したほうが楽だと思い、共有コンポーネントとして作成してみました。

※ヘッダーの作成に関する記事はいくつかありますので、今回は詳細を省略しています。

特筆すべき注意点としては、rootコンポーネントの段落で軽く触れたように、ヘッダーの左端のボタンをクリックするとサイドナビゲーションを開閉させる仕様にしているので、それに関するメソッドを作成しています。

コードの流れとしてはコンストラクタでapp.component.tsをインジェクションし、header.component.htmlで`#toggleSidenav`が呼び出された際に`app.component.tsの#toggleSidenav`を呼び出して`openedフィールド`のオン・オフを切り替えています。

```typescript:header.component.ts
import { Component } from '@angular/core';
import { AppComponent } from 'src/app/app.component';

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.css']
})
export class HeaderComponent {
  constructor(public appComponent: AppComponent){}
  toggleSidenav(){
    this.appComponent.toggleSidenav();
  }
}
```

```html:header.component.html
<div class="header">
    <mat-toolbar>
      <mat-toolbar-row>
        <!-- Sidenav button -->
        <button mat-icon-button  type="button" (click)="toggleSidenav()">
          <mat-icon>menu</mat-icon>
        </button>
        <!-- Toolbar's title -->
        <span>This is the toolbar</span>
        <!-- Spacer -->
        <span class="spacer"></span>
        <!-- Button(right side) -->
        <button mat-icon-button>
          <mat-icon>account_circle</mat-icon>
        </button>
      </mat-toolbar-row>
    </mat-toolbar>
</div>
```
```css:header.component.css
.spacer {
    flex: 1 1 auto;
}

/* 確認用 */
mat-toolbar {
    background-color: darkorange;
}
```

# まとめ
以上で業務アプリケーションを想定した大枠を作成することができました。
![ezgif.com-gif-maker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2169561/6d3cfddf-6064-4165-2687-c3e5c59862ef.gif)

あとは多くの記事にあるような手順で子モジュールを追加していくことが可能です。
今後は諸コンポーネントの生成方法や認証状態に応じたルーティング方法などを記事にしていく予定です。

#参考
- [[Angular]module間でコンポーネントを共有する
](https://qiita.com/ozaki25/items/6262a8dac2d6f221e2d1)

# コード全文
- [[github]outline-of-business-app
](https://github.com/GenkiOtera/outline-of-business-app)