"結果カード" というコンポーネントで共通のインタフェースを手に入れる

ドワンゴでニコニコ生放送のWebフロントエンジニアをやっています、 @misuken です。

今回は最近設計して使い勝手が良さそうな **結果カード** というコンポーネントを紹介します。

記事の後半では [BCD Design](https://qiita.com/misuken/items/19f9f603ab165e228fe1) による分類も取り入れ、最後にその解説も加えたので、この記事が対象とする範囲外のコンポーネントの設計や分類の参考にしていただけると幸いです。

## この記事の対象範囲

この記事では設計面にフォーカスしているので、コンポーネントの実装方法には踏み込みません。
そのため React や Vue といったライブラリに関する記述も出てきませんので、ご自由なライブラリで試していただければと思います。

## 正体が何なのかイマイチ定まりにくい表示物

フロントエンドでアプリケーションを作成していると、よく以下のような表示を要求されることがあります。

- リストが空のときの表示
    - 文言で空を説明する場合
    - 画像と文言で空を表現する場合
- エラーの表示
    - APIなどの通信エラー
    - 入力エラーのリスト
- 何かのヒントや警告
    - 設定状態は正しいが、あまり適切な設定状態ではない場合のヒントや警告

これらの表示はよく登場するわりに、場所によって情報構成もまちまちで、名前も構造も統一感のないコンポーネントが生まれがちです。
エラーの表示は悩むことも少ないかもしれませんが、リンクが必要なエラーが出てきたり、単なる一言のエラーメッセージではない場合は少し面倒だったりします。

そんな流れで何となく同じようなコンポーネントを作っては名前に悩み、その都度似て非なるものを量産していたりしないでしょうか。


## よく要求される表現

### リストが空のとき

例えば、リストが空のときに画像で空っぽを表現する要求が来たとします。
簡単に済ませるならこれで終わりです。

```html
<img class="empty-image" src="empty.png" />
```

しかし、やっぱりメッセージも一言添えてほしいという要求はよく生まれます。

```html
<img class="empty-image" src="empty.png" />
<p class="empty-message">データが見つかりませんでした</p>
```

この二つの要素は関係性があり、まとめる要素が欲しくなるので、包括する要素を追加して、構成が落ち着きます。

```html
<div class="empty-information">
  <img class="empty-image" src="empty.png" />
  <p class="empty-message">データが見つかりませんでした</p>
</div>
```

### エラーでリストが空のとき

データが取得できなかったとはいえ、元々データが空だったときと、エラーによってデータが取得できなかったときとでは表示を分けたくなるでしょう。
今度はそのパターンの要求を表現してみます。

```html
<p class="error-message">データが取得できませんでした</p>
```

解決策も一緒に表示したいといった要求もあります。

```html
<p class="error-message">データが取得できませんでした</p>
<p class="solution-message">しばらく待ってからもう一度アクセスしてください</p>
```

また関係性のある要素がバラバラに存在している状態になったので、包括する要素を追加して構成が落ち着きます。

```html
<div class="error">
  <p class="error-message">データが取得できませんでした</p>
  <p class="solution-message">しばらく待ってからもう一度アクセスしてください</p>
</div>
```

### 様々な煩わしさ

スタイルを適用する場面だと特に、関係性ごとの情報がまとまっていないと不便なので、新たな div を追加しましたが、階層が変わるとスタイル面では親と子の要素が変わり、変更量も多くなりがちです。

情報が増えたり減ったりするだけで新たな名前を考えたり、構造的に変化が起きたり、同じようなコンポーネントなのに場所ごとに構造や名前が違ったり、できることならそういった煩わしさから開放されたいものです。

上の例でも solution-message という名前を割り当てていますが、名前が付いてない状態でいざ名前を付けようとしたら結構悩むはずです。

## それぞれを抽象化して捉える

今までの表示要求をよくよく考えてみると、いずれも **何らかのアクションの結果** を表現していることがわかります。

- 取得処理は成功したが空だった(エラーではない)
- 取得処理自体が失敗した(エラー)

これは入力エラーでも同じで、入力というアクションを行った結果、文字数がオーバーしているといった具合です。
入力エラーを表現してみるとこうなります。

```html
<div class="error">
  <p class="error-message">入力文字数が多すぎます</p>
  <p class="solution-message">1〜20文字で入力してください</p>
</div>
```

よくエラーメッセージにどうすれば良いかが書いてなくて問題になることがありますが、解決法を表示する場所が用意されていると、そのような問題も起きにくくなります。

また、入力が正しいということも結果として表現しようと思えば以下のようにできます。

```html
<div class="success">
  <p class="success-message">正しい内容が入力されました</p>
</div>
```

### 結果を識別する

アクションを起こしたあと、どういう結果だったのかを識別するためには識別子が必要です。
識別子と言えば ID ですが、結果に対する識別子にはコードを使います。

わかりやすく説明すると、HTTP のステータスコードと同じような考え方です。
ステータスコードは通信結果を識別することができます。

コードごとに通信結果の内容が定義されているので、コードが識別子の役割を果たしています。
つまり、コードが決まれば結果に表示する内容も構成も決まるというわけです。

結果を識別するためのコードは **結果コード** とします。
エラーの内容を識別するためのエラーコードはシステム内でもよく使用しますが、結果コードはエラーコードも包括する概念のものです。

### 結果コードとは

結果コードは **関心 操作 結果 詳細** にあたる単語をその順番で結合した識別子です。

```
blog-articles-get-success
blog-articles-get-success-empty
blog-articles-get-failed
user-name-input-invalid-length-over
```

- 関心(user-name)ごとに特定の操作がある
- 操作(input)ごとに特定の結果がある
- 結果(invalid)ごとに特定の詳細(length-over)がある

といった具合です。


### 結果コードに紐づく情報のまとまり

ここまでに紹介してきた表示内容の一つ一つの要素を見てみると、いずれも結果を表す情報のまとまりであることがわかります。

| 結果 | 結果情報 |
|---|---|
| 取得処理は成功したが空だった(エラーではない) | - 問題はないが、表示する内容が無いことを提示<br>- 内容が無いことを連想できるイメージを提示 |
| 取得処理自体が失敗した(エラー) | - 問題があることを提示<br>- 解決法を提示 |
| 入力が正しくなかった | - 問題があることを提示<br>- 解決法を提示 |
| 入力が正しかった | - 問題がないことを提示 |

この結果に紐付く情報を **結果情報** と呼びます。

結果情報を見れば、その結果がどういうことだったのか、どうすれば解決するのかが全てわかります。

### エラーや成功は結果の種類として捉える

エラーであるか、成功であるか、のような情報は、結果の種類として捉えるように抽象化します。
結果コードと結果タイプ、さらには文言などのマッピングを用意する形にすれば、結果コードから各表示パターンを自動で解決できるようになります。

```ts
// 例えばこんなイメージ
export const resultMap = {
  "blog-articles-get-success-empty": { type: "info", illustrationImageSrc: "empty.png", resultMessage: "データが見つかりませんでした" },
  "blog-articles-get-failed": { type: "error", resultMessage: "データが取得できませんでした", solutionMessage: "しばらく待ってからもう一度アクセスしてください" },
  "user-name-input-invalid-length-over": { type: "error", resultMessage: "入力文字数が多すぎます", solutionMessage: "1〜20文字で入力してください" },
};
```

こういった型を用意できれば、何が起きたらどう表現するか、を黙々と定義していく単純な作業に落とし込めます。

HTML に反映すると以下のような感じになります。
情報量、表現力ともに十分な状態になっています。


```html
<div class="result-information" data-result-type="info" data-result-code="blog-articles-get-success-empty">
  <img class="illustration-image" src="empty.png" />
  <p class="result-message">データが見つかりませんでした</p>
</div>
```

```html
<div class="result-information" data-result-type="error" data-result-code="blog-articles-get-failed">
  <p class="result-message">データが取得できませんでした</p>
  <p class="solution-message">しばらく待ってからもう一度アクセスしてください</p>
</div>
```

```html
<div class="result-information" data-result-type="error" data-result-code="user-name-input-invalid-length-over">
  <p class="result-message">入力文字数が多すぎます</p>
  <p class="solution-message">1〜20文字で入力してください</p>
</div>
```

結果タイプや結果コードを属性値に反映することで、 CSS でスタイルを当てるときにも、それぞれの結果に対して個別にスタイルを調整することが可能です。
適切な情報を表現する責務と、それをどのように可視化するかの責務もしっかりわかれてくれます。

### 結果情報の情報量を充実させる

特定の結果に対する一通りの情報量をさらに増やしていくと、このような構成になります。

```html
<div class="result-information" data-result-type="error" data-result-code="blog-articles-get-failed-not-permission">
  <img class="illustration-image" src="empty.png" />
  <p class="result-message">ブログ記事が取得できませんでした</p>
  <p class="reason-message">ブログ記事の参照が許可されていません</p>
  <p class="solution-message">予めブログ記事の参照許可を得る必要があります</p>
  <a className="help-anchor" href="#">ブログ記事の参照許可とは？</a>
  <a className="solution-anchor" href="#">ブログ記事の参照許可を申請する</a>
</div>
```

内部のちょっとしたグルーピングの div は必要かもしれませんが、これだけの表現力があれば、サイト全体で発生する様々な結果に該当する表示を網羅的にカバーできることでしょう。


## 結果情報を結果カードにする

結果情報が固まったところで、さらに結果カードというものに進化させていきます。
結果情報というのは情報のまとまりを示すコンポーネントだったので、結果を踏まえて何らかの操作を行いたい場合の、操作に該当する要素を含めるのはあまり筋がよくないためです。

例えば、何かアクションを行った結果、問題が発生しているけど、二つの解決策があり、それはボタンを押すだけで解決できるといった場面では、以下のような構造が必要になります。

```html
<div class="result-card" data-result-type="error" data-result-code="xxx-failed">
  <div class="information">省略</div>
  <ul class="action-menu">
    <li class="item"><button class="adjust-button">調整によって解決する</button></li>
    <li class="item"><button class="delete-button">削除によって解決する</button></li>
  </ul>
</div>
```

このように、情報のまとまった要素と、アクションがまとまった要素をセットで配置可能なコンポーネントが結果カードです。
`action-menu` や `toolbar` といったコンポーネントを置けるように構成してやると、より網羅的に使えるコンポーネントになります。

### 属性は結果のスコープに移動する

結果カードの HTML では、今まで `result-information` に付いていた属性が `result-card` に移動しています。
これは、その属性は結果に対する属性であり、結果というスコープの一番外側の境界に属性が反映されるべきだからです。

例えば、結果カードの背景をエラーのときは赤っぽい色にしたいとします。
結果情報のところに属性が付いていたら、 CSS でスタイルを当てるときに困ってしまいます。

これは、スタイルを当てるから問題になるというわけではなく、情報の構成としてどこが持っている情報なのかが決まっているということです。

### 属性は対象のスコープの境界に指定する

他の例えでは、サービスの管理画面でユーザー一覧があったとして、ユーザーカードが並んでいるとします。
退会済みの人にはカード内に退会済みというラベルが表示されていますが、そのカードの属性にだけ退会済みという状態が反映されていても、カード全体をグレーのように表現することができずに困ってしまいます。

つまり、ラベルの属性が退会状態というだけでは情報として不十分で、退会しているという情報はユーザーのスコープに存在していなければならないことを示しています。

たしかに、退会という状態はユーザーに適用されているものなので、それは当然のことです。

ユーザーという情報のまとまりがユーザーカードに表現されているのであれば、ユーザーカードがユーザーのスコープの境界(つまりユーザーID単位)になるので、ユーザーカードの属性に退会状態が反映されていれば、情報としても正しく、スタイルとしても適切に当てることができるというわけです。

とにかく **属性は対象のスコープの境界に指定する** これがとても重要です。

ちなみに、この場合でもラベル単体として属性に退会状態を持つことは間違いではありません。
ユーザーカードの退会状態とラベルの退会状態は連動しているはずなので、ユーザーカードに渡した退会状態がラベルにも渡っていけば良いだけです。
これにより、ラベル単体のストーリーブックで確認するときにも退会状態を適切に表現しつつ、カード表現のときにも整合性を保つことができます。

### 結果カードをリストにする

この結果カードをリストにすると完成形です。
複数の結果を表示する場所にはこれを置けば、どのような表示要求にも十分対応できるでしょう。

```html
<ul class="result-list">
  <li class="item"><div class="result-card" data-result-type="error" data-result-code="xxx-invalid">省略</div></li>
  <li class="item"><div class="result-card" data-result-type="error" data-result-code="xxx-invalid">省略</div></li>
  <li class="item"><div class="result-card" data-result-type="error" data-result-code="xxx-invalid">省略</div></li>
</ul>
```

## まとめ

最初は曖昧だった表示物を、結果情報として捉え、結果コードを軸として汎用的で表現力豊かなコンポーネントで網羅する流れを紹介しました。

こういったアプローチを取らないと、名前のブレ、構造のブレ、要求変更時のインタフェースの破壊、など面倒なコストがかかってきます。
特に、インタフェースの破壊やリネームが発生する度に、設計や実装に対するレビューコストも嵩むことになり、意外とそれが積み重なって負担になります。

今回紹介したような結果カードという概念を用いれば、コードで表せられる物は全て同じインタフェースに吸収できます。

`コード表現ができる = 結果情報が使える = 共通のインタフェースと表現力が保証される`

コード表現ができるものとわかれば、そこに結果カードを置くだけです。
それだけで利用者に親切な結果に紐づく情報やアクションの提供も行えますし、標準的な型として存在するおかげで「この場面では解決法も提供したほうが利便性が上がるのではないか」といった気付きにもつながるでしょう。

また、あらゆる結果をコードに表現していくことで、コンポーネントだけでなくロジックのほうでも処理方法が統一しやすくなるなど、想像以上の好影響が生まれます。

システム内でコードという識別子を意識し、チーム内この概念で共有することで、色々なブレも無くしていくことに繋がります。

## おまけ

ちなみに最初の方はクラス名をわりと一般的に付けそうな名称で書いていますが、 result-information あたりから [BCD Design](https://qiita.com/misuken/items/19f9f603ab165e228fe1) を意識したクラス名にしています。
result ではなく result-information や result-card と表現しているのはそのためです。

information は情報のまとまりを意味する型(Base)として利用しています。
header や footer のような立ち位置と考えると理解しやすいかもしれません。

card は information や menu や toolbar といった、情報と提供される操作などをセットで扱える単位として存在しています。

このように、BCD Design を使ってコンポーネント名を意識することで、コンポーネントの役割や立ち位置が明確となり、より精度の高い設計を支えてくれます。

| 分類 | 単語 |
|---|---|
| Base | anchor button card image information item list menu message toolbar |
| Case | action adjust delete help solution |
| Common Domain | article error illustration reason result |
| Domain | blog user |

※ Domain と組合わせて使用できる関心は Common に分類する [BCCD](https://qiita.com/misuken/items/19f9f603ab165e228fe1#2020-03-28-%E8%BF%BD%E8%A8%98) で分類しています。