🔰

【フロントエンド入門】はじめてのUIコンポーネント実装!!

2024/01/27に公開

はじめに

htmlやcss、バニラのjsなどのコーディングに慣れてきてReactやVueといったフロントエンドフレームワークを使ったWebアプリケーションを実装しようとすると、コンポーネントを作ってそれを組み合わせてアプリを構築していくことを知ることになります。

最近はWebアプリケーションだけでなく、ホームページなどのWebサイト制作にもAstroのようなフレームワークが用いられるようになり、ますますフロントエンドのコンポーネント設計スキルの必要性が高まってきました。

そこで今回は、まだフレームワークを使ったUIコンポーネントの実装をあまりやったことがない人向けに、フロントエンドのUIコンポーネントの実装プロセスや知っておいた方が良いポイントや考え方を記載します。

対象となる読者

この記事は以下のような読者を対象としています。

  • ReactやVue、Angularのようなフレームワークを勉強中の方
  • 今まであまりフレームワークを使ったUIコンポーネントを実装したことはないけど、これからコンポーネントの実装をしていく予定の人

コンポーネントとは

概要

まずコンポーネントとは何かを簡単に説明すると、アプリケーションで使い回しが可能な独立したパーツのことをコンポーネントと呼びます。

フレームワークを使ったアプリケーションは基本的には小さなコンポーネントを組み合わせてアプリ全体を構築していきます。

コンポーネントのイメージは初めはレゴブロックをイメージしてみると良いかもしれません!

コンポーネントのイメージはLEGO

色々なレゴブロックを作ってそれを組み立ててお家を作っていくイメージですね!🏠

どういうのがコンポーネントに当たるの?

コンポーネントとは何かについて抑えたところで、では何がコンポーネントにあたるのでしょうか?

コンポーネントの粒度に正解はないので、アプリケーションやそのデザインによりけりなのですが、よくあるコンポーネントとしては以下があります。

  • テキストフィールド
  • ボタン
  • トグル
  • アコーディオン

例えばフォーム系でよく登場するテキストフィールドは使い回して開発することが多いので、コンポーネントとして実装したりします。

ボタンもアプリケーション全体を通して統一性のあるデザインだったりするので、コンポーネント化するのをおすすめします。

コンポーネントベースで構築していくメリット

コンポーネントベースでアプリケーションを構築していくメリットは以下です。

開発効率の向上

  • コンポーネントを再利用することで、同じ機能やデザインの部分を繰り返し実装する必要がなくなります。これにより、開発の効率が向上し、迅速な実装が可能になります。

再利用性と保守性の向上

  • コンポーネントベースのアーキテクチャにより、機能やデザインを独立した小さなコンポーネントに分割できます。これにより、コードの再利用性(繰り返し使いまわせる性能)が向上し、またメンテナンスも容易になります。

コンポーネントベースだと、画面内のどっかのパーツの挙動で不具合が起きた場合、そのコンポーネントのファイルをデバッグするだけで良いこともあります。

実装に入る前に

コンポーネントの具体的なイメージをつけたところで、いざ実装!というわけにはいきません。
実装に入る前に、いくつか事前に決めておいた方が良いことがあります。

コンポーネントの定義付け

自分がこれから実装するアプリケーションのデザインを見て、何をコンポーネントとして実装するかを定義しておく必要があります。

タスク管理ツールやスプレッドシートにまとめて一覧化しておくのも良いかもしれません。

デザイナーが別でデザインを作ってくれている場合、デザイン段階でコンポーネントを定義していることもありますが、それをそのままコンポーネントとして愚直に実装するのではなく、そこから利用頻度や開発効率などを考えて、開発側でどの粒度でコンポーネントを実装するかを設計していきます。

以下のシチュエーションを想定してみましょう。

デザイン段階でのコンポーネント定義:

  • Label: 主にフォームパーツ上部のラベル文言に相当するコンポーネント
  • ErrorText: 入力エラー時に表示するエラーメッセージを表示するコンポーネント
  • TextField: ユーザーがテキストを入力するためのフィールド

開発段階でのコンポーネント実装:

  1. Label コンポーネントを実装
  2. ErrorText コンポーネントを実装
  3. TextField コンポーネントを LabelErrorText、および実際の input 要素を組み合わせて実装

上記シチュエーションはあくまで一例ですが、アプリケーションでテキストフィールドに必ずラベルがついたりする場合はラベルも含めてテキストフィールドコンポーネントとして定義しても間違いではありません。

粒度に正解はないのですが、よくわからなかったらまずはこれ以上分解できないパーツは何かな?と考えてみると良いかもしれません!

命名規則の確認

次に、コンポーネントを作る際のファイル名やクラス名などの命名規則を定めておく必要があります。

よく決める命名規則としては以下のようなものがあります。

  • コンポーネントのファイル名
  • コンポーネントのCSS設計
  • attributesの書き方

コンポーネントのファイル名

まずはコンポーネントのファイル名を考えてみましょう。コンポーネントに限らず、ページ系のファイルなどそのほかとまとめて前段階で命名規則を決めているパターンもありますが、もし決めてなかった場合はこの段階で考えておくと良いでしょう。

  • TextField.vue
  • text-field.vue
  • textField.vue

上記はvueのPascalCasekebab-casecamelCaseを使ったファイルの命名例です。

フレームワークによってよく使うファイルの命名規則が異なったりします。また、公式で推奨されているものがあったりする時もあるので、どれが良いか迷ったら使うフレームワークの公式ドキュメントを覗いてみるといいかもしれませんね!

例えばvueであればPascalCasekebab-caseをよく使ったりします。

アプリ全体を通してどのようなファイル名にしておくか、ここで定義しておきましょう!

コンポーネントの書き方

コンポーネントを画面から呼び出す時、基本的にはコンポーネント名をタグの名前として書きます。

先ほどのTextFieldコンポーネントを例にすると、ページで呼び出すときには以下のように書きます。

<text-field />

普通のhtmlのタグにはこんなタグはありませんよね。これでコンポーネントと通常のhtmlのタグを区別します。
これはkebab-caseで書いた例ですが、PascalCaseで書いたりする時もあります。

<TextField />

チームで開発する場合は尚更、どの命名規則を使ってコンポーネントを書いていくかを事前に決めて認識を合わせておきましょう。

コンポーネントのCSS設計

続いて、コンポーネントにスタイルを当てる際のCSSのクラス名の命名規則を決めておきましょう。コンポーネントのhtmlを書いたら、次はデザインに合わせてスタイルをつけていく作業になりますが、その際のクラス名の付け方、modifierの規則などが該当します。

BasicButtonコンポーネントを例にすると以下のような感じです。その際のBasicButtonに該当する命名規則や_primary_sizeLargeなどのmodifierに該当する部分のルールを決めます。

<button class="BasicButton _primary _sizeLarge">
  ボタン
</button>

少し古い記事ですが、vueを例にすると以下の記事のような命名規則を考えるようなイメージです。
https://qiita.com/nakajmg/items/683395c20a3afbdb2d99

propsの書き方

次に、コンポーネントを実際にコード上で使う時の一般的にpropsと呼ばれるコンポーネント独自の属性の書き方を決めておきましょう。
詳しくは後述の「データの渡し方を知ろう!」で記載しますが、コンポーネントには、通常のhtmlのタグにあるようなidclasssrc属性などとは異なり、コンポーネント独自の属性のようなものを設定することができます。

<form-checkbox :is-checked="true" />

通常これはpropsと呼びますが、このpropsの書き方もチームで認識を合わせておくと良いでしょう。

<!-- kebab-case -->
<form-checkbox :is-checked="true" />
<!-- camelCase -->
<form-checkbox :isChecked="true" />

置き場所(ディレクトリ)の確認

次は、作ったコンポーネントをリポジトリ内のどこのディレクトリに配置するかを決めましょう。
src直下にcomponentsディレクトリを置いてその中にコンポーネントを積んでいくのか、components配下に更にカテゴリ名のディレクトリを挟むのか、プロジェクトのコンポーネントの種類や粒度によって決めましょう。

components直下にコンポーネントを配置していく場合の例

src
└── components
	├── BasicButton.vue
	└── TextField.vue

components直下にカテゴリ名のディレクトリを挟んで配置する場合の例

src
└── components
	├── buttons
	│   └── BasicButton.vue
	└── forms
	    └── TextField.vue

コンポーネントを実装してみる

これまでの工程である程度ルールを決めたところで、フレームワークを使ってコンポーネントを実装していきましょう。

サンプルで以下のようなBasicButtonコンポーネントを例に、実装のポイントを解説していきます。

作るものとその機能を考える

それではまず初めに何を作るのか、そしてそのコンポーネントにはどんな機能を入れるべきなのか、現時点でわかる範囲で棚卸しします。

例えばボタンコンポーネントの場合
<機能>

  • clickできる
  • ラベルを設定できる
  • 色のバリエーションを設定できる
  • サイズのバリエーションを設定できる

ただこの棚卸しはあくまで現時点で必要な機能を棚卸しするようにしましょう。

「あの機能もあった方がいいかもしれない」「いざという時のために〇〇の機能も入れておこう」など考え込んでリストアップしてしまうと、実装するときに余計に工数がかかってしまいます。

必要であれば後で機能を拡張していけば良いです。運用後の未来はわからないので、まずは最低限、現時点で必要な機能を考えていきましょう。

もちろん、機能を拡張しやすいようなコーディングをしていく必要がありますが、初めての方はそんなことは気にせず、最低限必要な機能だけを実装していきましょう!

普通のhtmlを書く

機能が決まったら、まずは静的なhtmlコーディングを行います。今回はデモと同じボタンのコンポーネントを例にします。

BasicButton.vue
<template>
  <button></button>
</template>

データの渡し方を知ろう!

コンポーネントはデータによって、その見た目や振る舞いを変更することができます。

例えばデモのボタンコンポーネントの場合、以下のような書き方をすることによって、ラベルやそのデザインを変えることができたりします。

<BasicButton variant="primary">primary</BasicButton>
<BasicButton variant="secondary">secondary</BasicButton>

この違いを理解するために、まずはどのフレームワークでも共通するような基本的な2パターンのデータの渡し方を把握しておきましょう!

propsで渡す

propsとは、一般的にコンポーネントの振る舞いを決定するプロパティを指します。書き方は普通のhtmlのタグに書くような属性と同じ書き方をします。

デモではvariantがpropsに該当します。

<BasicButton variant="primary">primary</BasicButton>

このvariant propsにprimarysecondaryといったワードをコンポーネントを呼び出している親側から指定することによって、コンポーネントの見た目をそれぞれ設定することができます。

コンポーネントの振る舞いなどはこのようにpropsを使って、親側からコンポーネントにデータを伝搬することで設定することができます。

デモのvueのソースコードでは以下の部分がpropsの設定に該当します。

script
export const BUTTON_VARIANTS = {
  variant: ["primary", "secondary"],
  size: ["full", "medium", "small"],
} as const;
script(setup)
// propsの型定義
interface Props {
  variant?: (typeof BUTTON_VARIANTS.variant)[number];
  size?: (typeof BUTTON_VARIANTS.size)[number];
}

// 初期値
withDefaults(defineProps<Props>(), {
  variant: "primary",
  size: "medium",
});

余談ですが、propsの型定義の仕方はいくつかありますが、私は定数的なキーワードの部分はBUTTON_VARIANTSなどに分離し、interfaceではそれを参照する形で定義したりします。

やっていることは以下と同じです。

interface Props {
  variant?: "primary" | "secondary";
  size?: "full" | "small" | "medium";
}

キーワードを別で定義してこのBUTTON_VARIANTSをexportし、このコンポーネントを内包する他のコンポーネントで、内包するBasicButtonに関連したpropsや変数などの型定義をすることができます。

続いて、各propsの指定を強制にするか任意にするかについて、特段そのpropsからのデータがないとコンポーネントとして意味を成さない場合以外は任意にしておくと良いでしょう。

interface Props {
  variant?: ...;
  size?: ...;
}

上記のようなvariantsizeは指定しなくてもボタンの機能としては問題ない部分なので任意にしています。

interface Props {
  variant: ...;
  size: ...;
}

上記のようにしてしまうと、開発者はコンポーネントを使うとき毎回variantsizeを指定しなければなりません。

このような煩わしさを回避するため、別になくても良い場合はなるべく任意にしてしまって良いでしょう。その代わり初期値などは必要であれば設定しておきます。

slotで渡す

親側からコンポーネントへのデータの渡し方として、props以外にもslotという仕組みを使ってデータを渡すことができます。

<BasicButton variant="primary">slotの中身</BasicButton>

slotはコンポーネントの開始タグと閉じタグで挟んだ中身にコンテンツを記述することによって、そのコンテンツを渡すことができます。

デモではtemplate部分の<slot />が該当します。
先ほどのソースコードのslotの中身という文字列はこの<slot />の部分に挿入されます。

<button
    省略...
  >
    <slot />
  </button>

propsとslotの用途の違い

propsもslotもそれぞれデータをコンポーネントに渡す仕組みですが、それぞれで用途が異なります。

  • props : コンポーネントの振る舞いを決定する
  • slot : コンポーネントにコンテンツを挿入する

例えばそのコンポーネントの機能やサイズ、デザインを設定したりするときはprops、表示させたいコンテンツ(文字列や他のコンポーネント)はslotを使うと良いでしょう。

propsとcssを関連づける

propsの役割がわかってきたところで、次はpropsで指定した値とcssを関連付けていきます。

仕組みを簡潔に説明すると、

propsで渡した値 をcssのmodifierに対応させる

が一般的な流れになります。

デモのソースコードのtemplate部分をみていきましょう。

<button
    :class="['BasicButton', `_${variant}`, `_${size}`]"
    ...
  >

このコードでは、variant propsとsize propsで指定したキーワードがそれぞれcssの _${variant}_${size}に入るような仕組みをとっています。

例えば以下のような指定をした場合、

<BasicButton variant="primary" size="medium">primary</BasicButton>

html上には以下のクラスがついて出力されます。

<button class="BasicButton _primary _medium">primary</button>

次に、このmodifireに対してそれぞれスタイルを書いていきます。

.BasicButton {
  /** ボタンの共通スタイル */
  ...略
  &._primary {
    background: #161952;
    color: #fff;
  }
  &._secondary {
    border: 1px solid #161952;
    background: #eee;
  }
  &._small {
    width: min(160px, 100%);
  }
  &._medium {
    width: min(240px, 100%);
  }
  &._full {
    width: 100%;
  }
  ...略
}

これでpropsとcssを対応づけることができました!

イベントをハンドリングしよう!

フレームワークを使うと、コンポーネントで任意の条件、状況下でイベントを発火させる仕組みを簡単に作ることができます。

通常のイベントの例としては、以下のようなものがあります。

  • ボタンをクリックしたときのclickイベント
  • テキストフィールドに文字を入力した時に発火するinputイベント
  • ボタンにTABキーでフォーカスを当てた時に発火するfocusイベント
  • ラジオボタンの選択を変更した時に発火するchangeイベント

これらは元々用意されているJavaScriptのイベントですが、コンポーネントの機能や仕組みによっては、特定の条件下でのみイベントを出したい!そのイベントで同時にコンポーネントの状態を親側で取得したい!というときがあります。

例えば。。。

  • アコーディオンUIをクリックした時にその時の開閉状態を取得したい
  • コンポーネントがレンダリングされたときにイベントを発火させたい
  • APIからデータの取得が終わった後に発火させたい

そこで、コンポーネント内でイベントを定義することによってこれらの問題を解決します。

<button
    ...略
    @click="$emit('onClick', new Date().toString())"
  >
   <slot />
</button>

vueでは$emit()を使ってイベントを発火させることができます。デモでは、ボタンをクリックした時にクリックした日付時間帯を親側に送るサンプルを組んでいます。

親側では、$emit()の第一引数で指定した文字列onClickを指定することで、ボタンをクリックした時にその時間帯を取得することができます。

App.vue(template)
<BasicButton @on-click="handleClick">primary</BasicButton>
<div>
  <p>"primary"ボタンを押すと下に押した時間が出力されます。👇</p>
  {{ input }}
</div>
App.vue(script)
const input = ref();
const handleClick = (e) => {
  input.value = e;
};

こうすることで、親側で定義したリアクティブ変数inputにイベント発火時に送った日付を格納することができます。

今回はシンプルな例ですが、このイベントを駆使することによって、コンポーネントの状態を親側でハンドリングすることができます!

完成

これで基本的なコンポーネントの実装をすることができました!

コンポーネント設計はどうやったら拡張しやすいか、どう設計したら開発効率が良くなるのかなど考えたらキリがないのですが、正解がないので日々いろんなプロジェクトに入ってその案件の特性を踏まえて設計するのがとても楽しいです✨

おわりに

ここまででざっとフレームワークを使ったUIコンポーネントの実装プロセスを解説してきました。
どうでしょう、早く実際にコンポーネントを作ってみたくなったのではないでしょうか!!?

今すぐに実践してみたい!と思えるような記事になれば嬉しいです!

Discussion