3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

概要

大体の記事では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コンポーネント内の部分が変化させます。
  • 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関連の構成

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

親モジュール関連の生成

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

root moduleの生成

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

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

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をまとめて追加できます。

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のみの作成なのでメニュー内容は適当です。
※ヘッダーの左上のボタンを押すと左側から展開されるメニューです

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タグの中に挿入します。

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に記載しても良いです。
※完成時の確認用に適当な色を設定しています。

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

子モジュール関連の生成

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

main.module.tsの生成

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

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)となっていることに注意してください。

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ファイルでは特に処理はしないので実質空っぽの状態です。

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>で囲む必要があるので注意してください。

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%等の明示がないと、テーブルなどコンテンツの高さが画面より大きな要素が入った際にスクロール表示されないので注意してください。

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

テーブルコンテンツの生成

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

適当に読み飛ばして大丈夫です
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'},
  // コピペで30行分ほど生成
  {position: 30, name: 'Neon', weight: 20.1797, symbol: 'Ne'},
];
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>
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;
}

共有モジュール関連の生成

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

shared.module.tsの生成

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

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フィールドのオン・オフを切り替えています。

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();
  }
}
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>
header.component.css
.spacer {
    flex: 1 1 auto;
}

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

まとめ

以上で業務アプリケーションを想定した大枠を作成することができました。
ezgif.com-gif-maker.gif

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

#参考

コード全文

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?