athome-developer’s blog

不動産情報サービスのアットホームの開発者が発信するブログ

AngularでAtomicDesign的にコンポーネントを作ってみた2

こんにちは、情報システム部の高野です。
AngularでAtomicDesignの第二弾です。
第一弾はこちら

dblog.athome.co.jp

前回はボタンを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を使ってテキストボックスにフォーカスが来た時に
分かりやすくなるように枠の色を変えています。

通常時にはこんな感じ
f:id:taktak1974:20190408153236p:plain

フォーカスが当たるとこうなります。
f:id:taktak1974:20190408153329p:plain

外部から色を変更できるようにしてみる

ボタンと同様に使う側が色を設定できるようにしてみます。
やっていることはボタンの時と同じです。

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を渡せば下図のようになります。
f:id:taktak1974:20190408161442p:plain

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>

使う側で設定してあげれば下図のような表示になります。
f:id:taktak1974:20190408161947p:plain

色の設定を共通化する

ここまででテキストボックスとして機能するものができました。
しかし色の設定がボタンと重複しています。
これでは色を変えたい時に大変なので色の定義をまとめたいと思います。
まとめるファイルは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

*1:changeでも良いです。

*2:ボタンには、setDisabledStateのようなメソッドが無いので普通に@Input()でフラグを受け取ればOKです