Flex-Layout と Angular Material でグッバイCSS – 検索フォームを作る
Flex-Layout と Angular Material でこれを作りました。
この画面、CSSを一切書いていません。 いい時代になりましたね。ごめんなさい、嘘です。 全体のパディングだけ1行設定しています。それ以外は使っていません。
CSSレスなSPAを目指す
CSSを書かずに画面を作れるならば、それに越したことはありません。単純にメンテするコードの量が減りますし、どこに何のCSSが適用されているのか気にしなくてよくなります。
CSSを書かずに画面をつくるには、以下2要素の代替手段を用意しなければなりません:
- レイアウトを調整する手段
- 見た目がキレイなコンポーネント
代表格はやはり Bootstrap でしょう。これらどちらも提供してくれており、テーマ・テンプレートも多数ある、候補としてまっさきにあがるすぐれものです。そんな素敵なCSSフレームワークではあるのですが、1点だけ気になるポイントが。jQuery 連携が前提になっているという点です。Angular でも組み合わせられないことはないのですが、やはりAngularのエコシステムで完結させたいところです。そこで今回、
- レイアウトを調整する手段: Angular Flex-Layout
- 見た目がキレイなコンポーネント: Angular Material
これらを使ってみることにしました。
この記事で述べる内容
- Flex-Layout で画面レイアウトを調整する
- Angular Material のコンポーネントをいくつか紹介し、実際に画面に埋め込んで見る
この記事で言及しない内容
- Flex-Layout で 画面サイズを考慮したレスポンシブな レイアウト調整を行う部分
- Angular Material とデータのバインディング
準備
ng new
ですでに Angular プロジェクトがある前提で進めます。
Flex-Laytout
1 | npm install --save @angular /flex-layout |
あとは Angular のapp.module.ts
の imports に FlexLayoutModule
を加えればOKです。
Angular Material
上記ページの、 Alternative 2: Angular Devkit 6+
に従いました。
1 2 | ng add @angular /material npm install --save hammerjs |
これで必要なものは揃いました。あとは実際に画面で利用する Material コンポーネントを、随時 app.module.ts
に追加していくことになります。
Flex-Layout でレイアウト調整
検索フォームを作るにあたって、ざっくり、以下のような配置を実現したいと思います。
fxLayout="column" で縦方向に要素を並べる
このようなコードを書くと...
1 2 3 4 5 | < 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 > |
このように、要素が縦に並びます。
fxLayout="column" によって、親要素が column となり、子要素を縦に並べていく…ということになります。子要素は、fxFlex
を宣言することによって、自分は親要素の Flex-Layout に属するものであることを示します。ここで、fxFlex
にはオプションで数値を指定することもでき、ドキュメントによると
px | % | vw | vh
いずれかが指定できます。上記の例は px で絶対値を指定していますね。
fxLayout="row" で横方向に要素を並べる
このようなコードを書くと…
1 2 3 4 | < 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 > |
このように、要素が並びます。
fxLayout
が指定されている点などは column の場合と同じですが、子要素の 検索ボタン側に、fxFlex=20%
が指定されています。これで、fxLayoutが設定されている親コンポーネントに対して、20%の横幅を確保するという意味になります。相対値を指定しているので、画面サイズの変更に追随します。
入れ子にできる
Bootstrap の Grid system と同様、Flex-Layout のcolumnやrowも入れ子にできます。つまり、縦に並ぶ要素の中で横並びのレイアウトを構築したり、その逆です。
1 2 3 4 5 6 7 8 9 | < 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 > |
これで、
このような結果になります。column と row をうまく組み合わせていけば、任意の場所にコンポーネントを配置できそうです。
ここまでで、Flex-Layout の基本的な機能を使ってみました。このあたり、どのような指定をするとどんな配置になるかは、公式デモサイトがわかりやすいです。参考にしてみてください。
Flex-Layout を駆使すれば、コンポーネントの配置はできそうです。次は実際に並べるコンポーネントを見ていきましょう。
Angular Material で マテリアルデザイン部品
こちらのサイトをみると、Input や Table など、HTMLの要素に相当する部品を用意してくれているようです。代表的なものをいくつかピックアップします。
Datepicker
Webアプリケーションを組むとほぼ必須といっていいレベルで必要になるコンポーネントです。Angular Material では カレンダーUI用の Javascript や CSS を開発者が入れ込むことなく、<mat-datepicker>
要素を書くだけで使えます。しかも、ロケールに対応しており、「Material Component のロケールとして何を使うか」という設定に対して 'ja-JP' を指定すればカレンダーUIが日本語になります。さらに、material-moment-adapter を使うことで、カレンダーからの入力値を Moment として扱えます。日付情報は、画面に表示する形式、外部APIと通信する際に渡す形式がまちまちなことが多いので、一度 Moment で受けて任意の形式にフォーマットできるというのは強みですね。
Button
見た目の選択肢が多いところが嬉しいですね。「枠線のみ」や「浮き上がり」を表現するために、わざわざCSSを書かなくても良いのは嬉しいです。もちろん、クリックなどのイベントに対して function を割り当てることができます。Material Icons と組み合わせれば、シーンに合わせたアイコンボタンを簡単につくれます。
Material Icons
<mat-icon>アイコン名</mat-icon>
によりマテリアルデザインで定義されているアイコンを利用することができます。入力フォームの目印として使うもよし、Buttonと組み合わせてアイコンボタンにするもよし。汎用性が高いです。
Table
素のテーブルを扱う場合、ヘッダに表示する項目の順番を調整したり、要素分だけ <tr>
を繰り返すというのは、なかなか面倒です。Angular Material のテーブルでは、テーブル用のデータを定義して、それを<table mat-table>
にわたすことでテーブルを表示してくれます。また、ヘッダ情報は定義だけ行い、表示是非や順序は <tr *matRowDef='columns: myDisplayDef>
の ように定義情報を渡すのでプログラム側で制御できます。何を表示するか(データ) と どう表示するか(見た目) を別々に扱ってくれるので、後から調整もしやすいです。
検索フォームを作っていく
材料が揃いました。検索フォームを作りましょう。まずは Flex-Layout で配置のアタリをつけます。
ここからは Angular Material の出番です。レイアウトの中身に、フォームコンポーネントやテーブルを埋め込んでいきます。
CSSを書かずにここまでやってくれるのはとても良いですね。あとは必要に応じてクリックイベントを仕込んでいけば画面の完成です。
まとめ
Flex-Layout と Angular Material で検索フォームを作りました。CSSを書いていないので、HTMLとロジックのみに集中でき、効率的に作業ができます。また、これらの枠組みの中で完結させると、デザインに対して明るくなくともある程度の統一感が出せます。どうしてもCSSが登場するシーンとして、ユーザーサイトが挙げられます。より凝ったデザイン、ストーリーに沿った画面レイアウトなどが必要になるでしょう。CSSなしでも活躍できる状況として
- コンテンツ管理画面(エンドユーザーの目に触れないところ)
- 画面モック
があると思います。Angular Material はまだまだ進化を続けているので、より多くのコンポーネントで、より多くのことができるようになるでしょう。積極的に活用していきたいですね。
今回つくったHTML
※ 冒頭で述べたとおり、データバインディングは仕込まれていないのでご注意ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | < 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 > |