背景と目的
Vue.js を用いた開発中に、複数のプロジェクトで「全選・個別選択・反転」機能が必要になりました。最初はコンポーネント内に computed プロパティを用いて実装しましたが、コードの再利用性が低く、保守が困難でした。そこで、より汎用的かつシンプルに使えるよう、カスタムディレクティブを活用した新しいアプローチを採用しました。
computed による初期実装の課題
以下のような方法で最初に実装しました:
- サーバーから取得したデータ配列の各要素に
checkedフラグを追加 selectCountを算出して、すべて選択されているか判定selectAllの状態変化に応じて全項目のチェック状態を一括更新- 選択済みアイテムを
checkedGroupsとして抽出
data() {
return {
items: []
}
},
computed: {
selectAll: {
get() {
return this.selectedCount === this.items.length;
},
set(value) {
this.items.forEach(item => {
item.checked = value;
});
}
},
selectedCount() {
return this.items.filter(item => item.checked).length;
},
checkedItems() {
return this.items.filter(item => item.checked);
}
}
この方法では、items や checked フィールド名が固定されており、他のコンポーネントで流用する際に同じ構造を強制されてしまいます。また、毎回同様の computed 論理を記述する必要があり、DRY 原則に反します。
カスタムディレクティブによる改善
再利用性を高めるために、v-select-all というカスタムディレクティブを設計しました。このディレクティブは次の特徴を持ちます:
- 監視対象の配列をパラメータで指定可能
- チェック状態のプロパティ名は固定せず、オブジェクト構造に依存しない
- v-model と連携して双方向バインディングを実現
ディレクティブの実装
以下のモジュールとして定義します:
export default {
'select-all': {
// 双方向バインディングを有効化
twoWay: true,
// HTML属性から値を受け取る
params: ['targetList'],
bind() {
// 監視対象の配列が変更された場合に反応
this.vm.$watch(
() => this.vm[this.params.targetList] || [],
(list) => {
const allChecked = list.length > 0 && list.every(item => item.checked);
this.set(allChecked);
},
{ deep: true }
);
},
update(newValue) {
const target = this.vm[this.params.targetList];
if (!target || !Array.isArray(target)) return;
target.forEach(item => {
item.checked = newValue;
});
}
}
};
使用方法
テンプレート側では次のように使います:
<!-- 全選チェックボックス -->
<input
type="checkbox"
v-model="isAllSelected"
v-select-all
target-list="userList">
<!-- 各アイテム -->
<ul>
<li v-for="user in userList" :key="user.id">
<input type="checkbox" v-model="user.checked">
{{ user.name }}
</li>
</ul>
JavaScript 側でディレクティブを登録:
import Vue from 'vue';
import directives from './directives/select-all';
// グローバルに登録
Object.keys(directives).forEach(key => {
Vue.directive(key, directives[key]);
});
内部動作の解説
bind フックでは、$watch を使って targetList 配列の内容変化を深く監視(deep watcher)しています。配列内の任意の checked 値が変化すると、すべてがチェックされているか評価し、全選ボックスの状態を自動更新します。
update メソッドは、全選チェックボックスの状態が変わったときに呼び出されます。その値(true/false)に基づき、対象配列内のすべてのアイテムの checked フラグを一括設定します。
params と $watch の違い
当初、paramWatchers を使って属性値の変化を検出しようと試みましたが、deep: true がサポートされていないため、配列内部のオブジェクト変更を検知できません。そのため、明示的に this.vm.$watch を使うことで、ネストされたデータの変更も確実に拾えるようにしています。
利点のまとめ
- 再利用性:複数のコンポーネントで同じディレクティブを使いまわせる
- 柔軟性:対象の配列名やモデル名を自由に設定可能
- 簡潔さ:テンプレートに宣言するだけで機能が追加される
- 保守性:共通ロジックが一元管理される