こんにちは、情報システム部の高野です。
AngularでAtomicDesignの第二弾です。
第一弾はこちら
前回はボタンをAtomとして実装しましたが
今回は、テキストボックスを実装したいと思います。
バージョンなど
前回と同じですが一応。
Angularは7.2.0
ブラウザはChromeのバージョン73を使用しています。
IEでは利用できないCSSの機能があると思うので試す時はその他のブラウザをご利用ください。
テキストボックスコンポーネントを作る
テキストボックスと言っているのは、input[type=text]
のことです。
テキストエリアも兼ねたものを作るか検討したのですが、現状は分けて作ることにしました。
シンプルにテキストボックスを表示するコンポーネントを作ってみる
ng generate
コマンドでテキストボックスコンポーネントを作成します。
text-box.component.html
<input type="text">
生成されたHTMLファイルの中身を上記に書き換えます。
app.component.html
<app-text-box></app-text-box>
作成したテキストボックスコンポーネントを表示するように追記します。
値のやりとりができるようにしてみる
ボタンの時は無かったのですが、テキストボックスの場合は入力された値を親コンポーネント側に渡したり
親コンポーネントから初期値を受け取ったりと値のやりとりが発生します。
デザインの前にその辺りを実装します。
text-box.component.ts
import { Component, OnInit, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @Component({ selector: 'app-text-box', templateUrl: './text-box.component.html', styleUrls: ['./text-box.component.scss'], providers: [ { // ControlValueAccessorを実装する時はお決まり provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TextBoxComponent), multi: true } ] }) export class TextBoxComponent implements OnInit, ControlValueAccessor { value = ''; change: (value: any) => void; constructor() { } ngOnInit() { } onChange(event: any) { this.value = event.target.value; if (this.change) { this.change(this.value); } } // ControlValueAccessorの実装 writeValue(obj: any): void { this.value = obj; } registerOnChange(fn: any): void { this.change = fn; } registerOnTouched(fn: any): void { } setDisabledState?(isDisabled: boolean): void { } }
リアクティブフォームやテンプレート駆動フォームで値をやりとりしたい時は
ControlValueAccessor
を実装するクラスとして作成します。
そうでない時は、@Input
と@Output
を使って値をやりとりするように実装することも可能です。
text-box.component.html
<input type="text" [value]="value" (input)="onChange($event)">
テキストボックスコンポーネント側ではngModelでの双方向バインディングは使わずに
valueバインディングとinputイベント*1を使って値を取得します。
app.component.ts
export class AppComponent { text = ''; }
app.component.html
<app-text-box [(ngModel)]="text"></app-text-box> {{text}} <!-- 入力した値が取得できるかの確認用 -->
使う側のAppComponent
はngModelを使用して双方向バインディングで値のやりとりを行います。
FormControllを使ったバインディングも可能です。
装飾してみる
text-box.component.scss
input { padding: 0.25rem 0.5rem; font-size: 1rem; border-radius: 0.2rem; border: 1px solid hsl(0, 0%, 65%); background-color: #fff; box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12); outline: 0; &:focus { border-color: hsl(163, 70%, 50%); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px hsla(163, 70%, 50%, .6); } }
特筆すべき点は、無いのですが
擬似クラスのfocus
を使ってテキストボックスにフォーカスが来た時に
分かりやすくなるように枠の色を変えています。
通常時にはこんな感じ
フォーカスが当たるとこうなります。
外部から色を変更できるようにしてみる
ボタンと同様に使う側が色を設定できるようにしてみます。
やっていることはボタンの時と同じです。
text-box.component.ts
// 本来はボタンと合わせて別なファイルに定義すべき export type Color = 'basic' | 'primary' | 'secondary' | 'danger' | 'warn'; // 省略 export class TextBoxComponent implements OnInit, ControlValueAccessor { // 省略 @Input() color: Color = 'basic'; // 省略 }
@Input
で外部から色を設定できるようにしています。
text-box.component.scss
input { // 省略 border: 1px solid var(--color); // カスタムプロパティで色を付けるように変更 // 省略 } .basic { --color: hsl(0, 0%, 65%); } .primary { --color: hsl(163, 70%, 50%); } .secondary { --color: hsl(50, 70%, 50%); } .danger { --color: hsl(15, 70%, 50%); } .warn { --color: hsl(30, 70%, 50%); }
ボタンの時と同様にCSSのカスタムプロパティを使って
指定されたクラス毎に色が変わるようにしています。
text-box.component.html
<input type="text" [value]="value" (input)="onChange($event)" [class]="color">
HTMLではclassバインディングを使って外から渡された値を設定します。
app.component.html
<app-text-box [(ngModel)]="text" color="danger"></app-text-box>
例えば使う側がdanger
を渡せば下図のようになります。
Disabledにしてみる
ボタンの時は書かなかったのですが、ControlValueAccessor
を実装するクラスを作ると
setDisabledState
というメソッドの実装が必要になる(必須ではない)のでやってみます。*2
text-box.component.ts
export class TextBoxComponent implements OnInit, ControlValueAccessor { disabled = false; // 省略 setDisabledState?(isDisabled: boolean): void { this.disabled = isDisabled; } }
setDisabledState
メソッドでdisabled
フィールドに値を渡すように実装します。
text-box.component.html
<input type="text" [value]="value" (input)="onChange($event)" [class]="color" [disabled]="disabled">
HTML側は、disabledバインディングでフラグを渡せばOKです。
text-box.component.scss
input { // 省略 &:disabled { --color: hsl(0, 0%, 65%); background-color: hsl(0, 0%, 80%); } }
CSSには、Disabled状態の時の設定が必要ですが
ここでは擬似クラスのdisabled
を使います。
app.component.html
<app-text-box [(ngModel)]="text" disabled="true"></app-text-box>
使う側で設定してあげれば下図のような表示になります。
色の設定を共通化する
ここまででテキストボックスとして機能するものができました。
しかし色の設定がボタンと重複しています。
これでは色を変えたい時に大変なので色の定義をまとめたいと思います。
まとめるファイルはng new
した時に作成されているstyles.scssファイルにします。
styles.scss
:root { --basic-color: hsl(0, 0%, 65%); --primary-color: hsl(var(--primary-hue), var(--default-sat), var(--default-lig)); --secondary-color: hsl(var(--secondary-hue), var(--default-sat), var(--default-lig)); --danger-color: hsl(var(--danger-hue), 99%, 50%); --warn-color: hsl(var(--warn-hue), 90%, var(--default-lig)); --primary-hue: 163; --secondary-hue: 50; --danger-hue: 15; --warn-hue: 30; --default-sat: 70%; --default-lig: 50%; }
グローバルで使うカスタムプロパティをroot擬似クラスに定義します。
primary-color
カスタムプロパティなどに直接数値を入れずに
primary-hue
カスタムプロパティを使っているのは、
プライマリーカラーよりも少し濃い目などの調整がしやすいようにです。
このブログではその辺は出てきませんしそういう要求が無いのであれば直接数値を指定しても良いかと思います。
textbox.component.scss
// 省略 .basic { --color: var(--basic-color); } .primary { --color: var(--primary-color) } .secondary { --color: var(--secondary-color) } .danger { --color: var(--danger-color) } .warn { --color: var(--warn-color) }
テキストボックスのCSS定義でprimary-color
などを利用します。
ボタンの方も同様に変更すればstyles.scss
の変更だけで色を変更することができます。
例えばテーマカラーを変更できるアプリケーションなども割と簡単に実装することが可能です。
1つ注意点としては、rootとボタンコンポーネントなどの間に
primary-color
というカスタムプロパティを設け別の色を指定すると
ボタンの色が意図せずに変わってしまうことがあります。
これに関しては防ぐ方法が思いつかないので今のところ注意するしかないです。
まとめ
前回に引き続きAtomicDesign風にAngularのコンポーネントを作成してみました。
テキストボックスはボタンに比べれば複雑ですが慣れてしまえば簡単だと思います。
チェックボックス等もAngular部分は同じような作り方で作成可能です。
ボタンとテキストボックスが作成できたのでそれを組み合わせてMolecules(分子)を作る説明を次回以降にしていきます。
弊社ではエンジニアを募集しています。
Angular以外にもVue.jsやフロント以外のエンジニア募集もしております。
興味がある方は下記からエントリーお願いします。
athome-inc.jp