athome-developer’s blog

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

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

こんにちは、情報システム部の高野です。

割とまとまって考える時間がとれたので
AngularでAtomicDesign的*1コンポーネントを作るにはどのようにするのが良いのか考えてみました。
AtomicDesignに関してはググっていただければと思います。

今回は、Angular Materialなども使わずにボタンからなにから自分で作ってみます。
(と言ってもここで紹介するのはその一部ですが)

バージョンなど

Angularは7.2.0
ブラウザはChromeのバージョン73を使用しています。
IEでは利用できないCSSの機能があると思うので試す時はその他のブラウザをご利用ください。

ボタンコンポーネントを作る

まずはシンプルなボタンを作ってみる

まずはシンプルなボタンを作ってみます。
ちなみにこれは、AtomicDesignで言うAtomになります。

ボタンの動作としてはクリックされたことを上位層に返せればよいので
そこはシンプルで、あまり考えることは無いと思います。
あとはng-contentでボタンに表示する文字列を外から受け取るように作ります。

button.component.html
<button>
  <ng-content></ng-content>
</button>

HTMLの中身をbuttonタグに変更して
子要素にng-contentを追加します。
HTMLファイル以外は、ngコマンドでcreateしたままの状態です。


次は使う側です。どこでも良いのですが今回はappコンポーネントを使います。

app.component.ts
export class AppComponent {
   onButtonClick() {
    alert('OK');
  }
}

AppComponentクラスにクリックされた時に呼ばれるメソッドを追加します。

app.component.html
<app-button (click)="onButtonClick()">OK</app-button>

HTMLに先程作ったボタンコンポーネントを追加します。

これで実行すればなんの変哲もないボタンが表示され
クリックすればアラートが表示されます。

装飾してみる

上記のままだと普通にHTMLのボタンを使えば良いだけなのでまったく価値がありません。
CSSで装飾してオリジナリティ*2を出してみましょう。

button.component.scss
button {
  --color: hsl(210, 4%, 19%);
  border: 2px solid var(--color);
  border-radius: 4px;
  background-color: #fff;
  outline: 0;
  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);
  font-weight: 600;
  font-size: 1.2rem;
  color: var(--color);
  padding: 5px 16px;
  cursor: pointer;
  transition: all 200ms linear;

  &:active {
    opacity: 0.5;
    transform: scale(0.9);
  }
}

scssを使ってますがそこはlessでもcssでも良いです。
上記CSSで下図のようなボタンになります。

f:id:taktak1974:20190402132322p:plain
画像だと分からないですがクリックするとちゃんと押した感が出るようになっています。
これでclass="btn"とか付けて回らなくても同じデザインのボタンを利用することができます。
しかもこのCSSを変更すれば使っているところ全部にデザインの変更を
行き渡らせることができます。良きかな

外部からデザインを変更してみる

一応オリジナルのボタンとしては成り立っていますが
大きさも色も一定では、あまり使いみちがありません。
それらを外部から指定できるようにします。

ここで結構悩みました。
外部からCSSを変更するにはどうしたら良いのだろう?
パッと思いついたのは2つの方法

  1. CSSのプロパティ毎に入力プロパティ(@Input)を作成してStyleBindingする
  2. いくつかの大きさや色のCSSクラスを作成してそれを入力プロパティの値でClassBindingする


1のやり方だと変更したいCSSプロパティが増えた時にTypeScript側も同じ数だけ増えてしまうし
なによりも自由度が高すぎてデザインが破壊される可能性が高くなります。
なのでこの2択であれば2の方を選択した方が良いだろうとなりました。


下記は2の方の実装になります。

button.component.ts
export type Size = 'large' | 'medium' | 'small';
export type Color = 'basic' | 'primary' | 'secondary' | 'danger' | 'warn';

// 省略

export class ButtonComponent implements OnInit {

  @Input() size: Size = 'medium';

  @Input() color: Color = 'basic';

  get style() {
    return `${this.size} ${this.color}`;
  }

  // 省略
}
button.component.html
<button [class]="style">
  <ng-content></ng-content>
</button>
button.component.scss
button {
  // 省略
}

.small {
    padding: 5px 8px;
    font-size: 0.7rem;
}

.medium {
    padding: 5px 16px;
    font-size: 1.2rem;
}

.large {
    padding: 5px 20px;
    font-size: 1.5rem;
}

.basic {
    --color: hsl(210, 4%, 19%);
}

.primary {
    --color: hsl(163, 70%, 50%);
}

.secondary {
    --color: hsl(50, 70%, 50%);
}

.danger {
    --color: hsl(15, 70%, 50%);
}

.warn {
    --color: hsl(30, 70%, 50%);
}

色の方はカスタムプロパティを使っていますが
普通にcolorとborder-colorに値を入れていっても同じです。

これを一覧したのが下図になります。
f:id:taktak1974:20190402144156p:plain
なんだかそれっぽい感じになりました。


使う側はこのようになります。

<div>
  <app-button size="small" (click)="onButtonClick()">small</app-button>
  <app-button size="medium" (click)="onButtonClick()">medium</app-button>
  <app-button size="large" (click)="onButtonClick()">large</app-button>
</div>
<div>
  <app-button color="basic" (click)="onButtonClick()">basic</app-button>
  <app-button color="primary" (click)="onButtonClick()">primary</app-button>
  <app-button color="secondary" (click)="onButtonClick()">secondary</app-button>
  <app-button color="warn" (click)="onButtonClick()">warn</app-button>
  <app-button color="danger" (click)="onButtonClick()">danger</app-button>
</div>

本来は白抜きのボタンにも対応したいとか丸ボタンもほしいとかあるので
もうちょっと複雑なのですが、雰囲気はこのような感じです。

さらに外部からデザインを変更する

例えばボタンをなにかの幅に合わせたいということがあると上記では対応できません。
これもいくつかの方法があります。

  1. 上記01の方法
  2. CSSのカスタムプロパティを使う
  3. ボタンのwidthを100%にしてapp-buttonにwidthを与える


1の方法でももちろん良いのですが、ここでは除外します。
3の方法は、常にapp-buttonにwidthを入れなくてはいけなくなるので却下。
ということで2の方法でやってみます。

button.component.scss
button {
  width: var(--width);
  // 省略
}

上位のコンポネントで--widthに値を入れるとその幅になります。


使う側の実装です。

app.component.html
 <app-button class="wide-btn" color="primary" (click)="onButtonClick()">wide</app-button>
app.component.scss
.wide-btn {
  --width: 300px;
}


これで下図のようになります。

f:id:taktak1974:20190402151021p:plain:w400

この方法の良いところは、CSSのことはCSS内だけで終わらせられる点ですね。
ダメなところは、外から色々できるようにするとデザインが破壊されるところ。
それと全然違う場所で同名のカスタムプロパティが作られた場合に
予期せずにデザインが変わってしまったりすることがあるところです。*3

まとめ

今回はボタンコンポーネントを作ってみました。
こうやってまとめるとあまり難しくないのですが
ここに至るまではかなり試行錯誤しました。
ボタンだけでこれですから、もっと複雑なコンポーネントを考えると
普通はAngular Materialなどを使う方が良いでしょう。
1プロダクトではなくもっと大きな単位で使い回すように作れば
共通したUI/UXが作れてそれなりに元が取れるかと思います。

とは言えブログでは他のコンポーネントや上位の層のことも書いていこうと思います。


弊社ではエンジニアを募集しています。
Angular以外にもVue.jsやフロント以外のエンジニア募集もしております。
興味がある方は下記からエントリーお願いします。
athome-inc.jp

*1:あくまで的な感じです

*2:どこかで見た感じにはなりますが

*3:それなら1の方法が良いのではないかという