ちょっと話題の記事

Flex-Layout と Angular Material でグッバイCSS – 検索フォームを作る

2018.10.05

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Flex-Layout と Angular Material でこれを作りました。

angular_material_form.gif

この画面、CSSファイルには何も書いていません。 いい時代になりましたね。ごめんなさい、嘘です。 全体のパディングだけ1行設定しています。それ以外はすべて Flex-Layout と Angular Material の仕組みで完結しています。

※ Twitter等で「CSSがなくなるわけではないよね」というご指摘をいただき確かにそのとおりですので全体的に誤解を与えぬよう修正しています。ありがとうございました。

自分で書くCSSをなるべく少なくしたい

可能な限りフレームワークやライブラリの仕組みに乗り、CSSファイルの記述量は少なくしたいところです。単純にメンテするコードの量が減りますし、どこに何のCSSが適用されているのか気にしなくてよくなります。

画面を構成していくための道具を選定するにあたり、以下2要素考慮します:

  • レイアウトを調整する手段
  • 見た目がキレイなコンポーネント

代表格はやはり Bootstrap です。これらどちらも提供してくれており、テーマ・テンプレートも多数ある、候補としてまっさきにあがるすぐれものです。そんな素敵なCSSフレームワークではあるのですが、1点だけ気になるポイントが。jQuery 連携が前提になっているという点です。Angular でも組み合わせられないことはないのですが、やはりAngularのエコシステムで完結させたいところです。そこで今回、

これらを使ってみることにしました。

この記事で述べる内容

  • Flex-Layout で画面レイアウトを調整する
  • Angular Material のコンポーネントをいくつか紹介し、実際に画面に埋め込んでみる

この記事で言及しない内容

  • Flex-Layout で 画面サイズを考慮したレスポンシブな レイアウト調整を行う部分
  • Angular Material とデータのバインディング

準備

ng new ですでに Angular プロジェクトがある前提で進めます。

Flex-Laytout

npm install --save @angular/flex-layout

あとは Angular のapp.module.ts の imports に FlexLayoutModule を加えればOKです。

Angular Material

上記ページの、 Alternative 2: Angular Devkit 6+ に従いました。

ng add @angular/material
npm install --save hammerjs

これで必要なものは揃いました。あとは実際に画面で利用する Material コンポーネントを、随時 app.module.ts に追加していくことになります。

Flex-Layout でレイアウト調整

検索フォームを作るにあたって、ざっくり、以下のような配置を実現したいと思います。

note-layout-mock.png

fxLayout="column" で縦方向に要素を並べる

このようなコードを書くと...

<div fxLayout="column">
  <div fxFlex="50px" style="background-color: blue; color: white" fxLayoutAlign="center center">ヘッダ置きたい</div>
  <div fxFlex="50px" style="background-color: bisque" fxLayoutAlign="center center">ここに検索フォーム</div>
  <div fxFlex="50px" style="background-color: darkseagreen" fxLayoutAlign="center center">結果</div>
</div>

このように、要素が縦に並びます。

note-flex-layout-column.png

fxLayout="column" によって、親要素が column となり、子要素を縦に並べていく…ということになります。子要素は、fxFlex を宣言することによって、自分は親要素の Flex-Layout に属するものであることを示します。ここで、fxFlex にはオプションで数値を指定することもでき、ドキュメントによると px | % | vw | vh いずれかが指定できます。上記の例は px で絶対値を指定していますね。

fxLayout="row" で横方向に要素を並べる

このようなコードを書くと…

<div fxLayout="row" style="height: 200px">
  <div fxFlex style="background-color: bisque" fxLayoutAlign="center center">ここに検索フォーム</div>
  <div fxFlex="20%" style="background-color: darkgoldenrod" fxLayoutAlign="center center">検索ボタンとか</div>
</div>

このように、要素が並びます。

note-flex-layout-row.png

fxLayout が指定されている点などは column の場合と同じですが、子要素の 検索ボタン側に、fxFlex=20% が指定されています。これで、fxLayoutが設定されている親コンポーネントに対して、20%の横幅を確保するという意味になります。相対値を指定しているので、画面サイズの変更に追随します。

入れ子にできる

Bootstrap の Grid system と同様、Flex-Layout のcolumnやrowも入れ子にできます。つまり、縦に並ぶ要素の中で横並びのレイアウトを構築したり、その逆です。

<div fxLayout="column" highlight="4-9">
  <div fxFlex="50px" style="background-color: blue; color: white" fxLayoutAlign="center center">column1</div>
  <div fxFlex="75px">
    <div fxLayout="row" style="height: 75px">
      <div fxFlex fxFlexFill style="background-color: bisque" fxLayoutAlign="center center">column2, row1</div>
      <div fxFlex="20%" fxFlexFill style="background-color: darkgoldenrod" fxLayoutAlign="center center">column2, row2</div>
    </div>
  </div>
</div>

これで、

note-layout-nest.png

このような結果になります。column と row をうまく組み合わせていけば、任意の場所にコンポーネントを配置できそうです。

ここまでで、Flex-Layout の基本的な機能を使ってみました。このあたり、どのような指定をするとどんな配置になるかは、公式デモサイトがわかりやすいです。参考にしてみてください。

Flex-Layout を駆使すれば、コンポーネントの配置はできそうです。次は実際に並べるコンポーネントを見ていきましょう。

Angular Material で マテリアルデザイン部品

Components | Angular Material

こちらのサイトをみると、Input や Table など、HTMLの要素に相当する部品を用意してくれているようです。代表的なものをいくつかピックアップします。

Datepicker

mat-calendar.png

Webアプリケーションを組むとほぼ必須といっていいレベルで必要になるコンポーネントです。Angular Material では カレンダーUI用の Javascript や CSS を開発者が入れ込むことなく、<mat-datepicker> 要素を書くだけで使えます。しかも、ロケールに対応しており、「Material Component のロケールとして何を使うか」という設定に対して 'ja-JP' を指定すればカレンダーUIが日本語になります。さらに、material-moment-adapter を使うことで、カレンダーからの入力値を Moment として扱えます。日付情報は、画面に表示する形式、外部APIと通信する際に渡す形式がまちまちなことが多いので、一度 Moment で受けて任意の形式にフォーマットできるというのは強みですね。

Button

mat-button.png

見た目の選択肢が多いところが嬉しいですね。「枠線のみ」や「浮き上がり」を表現するために、わざわざCSSを書かなくても良いのは嬉しいです。もちろん、クリックなどのイベントに対して function を割り当てることができます。Material Icons と組み合わせれば、シーンに合わせたアイコンボタンを簡単につくれます。

Material Icons

mat-icon.png

<mat-icon>アイコン名</mat-icon> によりマテリアルデザインで定義されているアイコンを利用することができます。入力フォームの目印として使うもよし、Buttonと組み合わせてアイコンボタンにするもよし。汎用性が高いです。

Table

mat-table.png

素のテーブルを扱う場合、ヘッダに表示する項目の順番を調整したり、要素分だけ <tr> を繰り返すというのは、なかなか面倒です。Angular Material のテーブルでは、テーブル用のデータを定義して、それを<table mat-table>にわたすことでテーブルを表示してくれます。また、ヘッダ情報は定義だけ行い、表示是非や順序は <tr *matRowDef='columns: myDisplayDef> の ように定義情報を渡すのでプログラム側で制御できます。何を表示するか(データ)どう表示するか(見た目) を別々に扱ってくれるので、後から調整もしやすいです。

検索フォームを作っていく

材料が揃いました。検索フォームを作りましょう。まずは Flex-Layout で配置のアタリをつけます。

note-form-layout-mock.png

ここからは Angular Material の出番です。レイアウトの中身に、フォームコンポーネントやテーブルを埋め込んでいきます。

note-finish.png

あとは必要に応じてクリックイベントを仕込んでいけば画面の完成です。

まとめ

Flex-Layout と Angular Material で検索フォームを作りました。これらの枠組みの中で完結させると、デザインに対して明るくなくともある程度の統一感が出せます。ただし万能ではないとも感じます。CSSフレームワークだけでは完結できない状況として、より凝ったデザイン、ストーリーに沿った画面レイアウトなどが必要になる(ユーザーサイトなど)があると思います。一方、

  • コンテンツ管理画面(エンドユーザーの目に触れないところ)
  • 画面モック

といったものを作る分には、Flex-Layout と Angular Material で困りません。特にAngular Material は、まだまだ進化を続けているので、より多くのコンポーネントで、より多くのことができるようになるでしょう。積極的に活用していきたいですね。

今回つくったHTML

※ 冒頭で述べたとおり、データバインディングは仕込まれていないのでご注意ください。

list-notes.component.html

<div class="sidenav-container">
  <mat-toolbar color="primary">
    <span>記事一覧</span>
  </mat-toolbar>
  <div fxLayout="column" class="form-container" fxLayoutAlign="space-around space-around" fxLayoutGap="5px">
    <div class="note-search-form-container">
      <!--inputs and buttons-->
      <div fxLayout="row" fxLayoutAlign="space-around" fxLayoutGap="25px">
        <!--search components-->
        <div fxFlex="65" class="note-search-inputs">
          <div fxLayout="column" fxLayoutAlign="space-around" fxLayoutGap="5px">
            <div fxFlex>
              <!--search component group row-->
              <div fxLayout="row" fxLayoutAlign="space-around" fxLayoutGap="20px">
                <div fxFlex="10%" fxLayoutAlign="center center">
                  <mat-icon class="material-icons">list</mat-icon>
                </div>
                <div fxFlex fxFlexFill>
                  <mat-form-field>
                    <mat-select placeholder="記事の属性">
                      <mat-option value="1">ニュース</mat-option>
                      <mat-option value="2">技術ブログ</mat-option>
                    </mat-select>
                  </mat-form-field>
                </div>
              </div>
            </div>
            <div fxFlex>
              <div fxLayout="row" fxLayoutAlign="space-around" fxLayoutGap="20px">
                <div fxFlex="10%" fxLayoutAlign="center center">
                  <mat-icon class="material-icons">note_add</mat-icon>
                </div>
                <mat-form-field fxFlex>
                  <input matInput [matDatepicker]="noteIssuedFrom" placeholder="公開日(from)"
                         formControlName="issuedAtFrom">
                  <mat-datepicker-toggle matSuffix [for]="noteIssuedFrom"></mat-datepicker-toggle>
                  <mat-datepicker #noteIssuedFrom></mat-datepicker>
                </mat-form-field>
                <mat-form-field fxFlex>
                  <input matInput [matDatepicker]="noteIssuedTo" placeholder="公開日(to)"
                         formControlName="issuedAtTo">
                  <mat-datepicker-toggle matSuffix [for]="noteIssuedTo"></mat-datepicker-toggle>
                  <mat-datepicker #noteIssuedTo></mat-datepicker>
                </mat-form-field>
                <div fxFlex="10%" fxLayoutAlign="center center">
                  <button mat-button color="default" fxLayoutAlign="center center">
                    <mat-icon aria-label="公開日をクリア">clear</mat-icon>
                    <span>クリア</span>
                  </button>
                </div>
              </div>
            </div>
            <div fxFlex>
              <div fxLayout="row" fxLayoutAlign="space-around" fxLayoutGap="20px">
                <div fxFlex="10%" fxLayoutAlign="center center">
                  <mat-icon class="material-icons">update</mat-icon>
                </div>
                <mat-form-field fxFlex>
                  <input matInput [matDatepicker]="noteUpdatedFrom" placeholder="更新日(from)"
                         formControlName="updatedAtFrom">
                  <mat-datepicker-toggle matSuffix [for]="noteUpdatedFrom"></mat-datepicker-toggle>
                  <mat-datepicker #noteUpdatedFrom></mat-datepicker>
                </mat-form-field>
                <mat-form-field fxFlex>
                  <input matInput [matDatepicker]="noteUpdatedTo" placeholder="更新日(to)"
                         formControlName="updatedAtTo">
                  <mat-datepicker-toggle matSuffix [for]="noteUpdatedTo"></mat-datepicker-toggle>
                  <mat-datepicker #noteUpdatedTo></mat-datepicker>
                </mat-form-field>
                <div fxFlex="10%" fxLayoutAlign="center center">
                  <button mat-button color="default" fxLayoutAlign="center center">
                    <mat-icon aria-label="更新日をクリア">clear</mat-icon>
                    <span>クリア</span>
                  </button>
                </div>
              </div>
            </div>
          </div>
        </div>
        <div fxFlex="35" class="note-search-buttons">
          <div fxLayout="column" fxFlexFill>
            <div fxFlex="33"></div>
            <div fxFlex>
              <div fxLayout="row" fxFlexFill fxLayoutAlign="space-around space-around" fxLayoutGap="20px">
                <div fxFlex="45" fxFlexFill fxLayoutAlign="center center">
                  <button fxFlexFill mat-raised-button color="default" (click)="init()">
                    検索する
                  </button>
                </div>
                <div fxFlex="45" fxFlexFill fxLayoutAlign="center center">
                  <button fxFlexFill fxLayoutAlign="center center" mat-raised-button
                          color="accent">
                    <span>一括JSONエクスポート</span>
                  </button>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
    <div fxFlex class="note-master-content">
      <!-- search and result-->
      <div fxLayout="column" fxLayoutAlign="space-around space-around" fxLayoutGap="5px">
        <div fxFlex class="result-table-container">
          <div>
            <mat-progress-bar mode="indeterminate" *ngIf="loading"></mat-progress-bar>
          </div>
          <div fxFlexFill class="mat-elevation-z8">
            <table fxFlexFill mat-table [dataSource]="displayUsers">
              <ng-container matColumnDef="displayUserName">
                <th mat-header-cell *matHeaderCellDef>著者</th>
                <td mat-cell *matCellDef="let element"> {{element.displayUserName || '-'}}</td>
              </ng-container>
              <ng-container matColumnDef="company">
                <th mat-header-cell *matHeaderCellDef>記事タイトル</th>
                <td mat-cell *matCellDef="let element"> {{element.company || '-'}}</td>
              </ng-container>
              <ng-container matColumnDef="createdAt">
                <th mat-header-cell *matHeaderCellDef>作成日時</th>
                <td mat-cell *matCellDef="let element"> {{toJst(element.createdAt) || '-'}}</td>
              </ng-container>
              <ng-container matColumnDef="updatedAt">
                <th mat-header-cell *matHeaderCellDef>更新日時</th>
                <td mat-cell *matCellDef="let element"> {{toJst(element.updatedAt) || '-'}}</td>
              </ng-container>
              <!-- Edit Column -->
              <ng-container matColumnDef="edit">
                <th mat-header-cell *matHeaderCellDef>編集</th>
                <td mat-cell *matCellDef="let element">
                  <button mat-icon-button color="primary">
                    <mat-icon color="primary">edit</mat-icon>
                  </button>
                </td>
              </ng-container>
              <!-- Delete Column -->
              <ng-container matColumnDef="delete">
                <th mat-header-cell *matHeaderCellDef>削除</th>
                <td mat-cell *matCellDef="let element">
                  <button mat-icon-button color="warn" >
                    <mat-icon color="warn">delete_forever</mat-icon>
                  </button>
                </td>
              </ng-container>
              <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
              <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
            </table>
            <div *ngIf="lastEvaluatedKey" fxFlexFill fxLayoutAlign="center center">
              <button fxFlexFill mat-button color="basic" (click)="getUsers(lastEvaluatedKey)">
                <mat-icon>expand_more</mat-icon>
                さらに読み込む
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

参考