始めに
Vue.jsでラジオボタンの設定をする時は以下のように書くと思いますが、毎回v-modelの設定をするのが面倒だと感じたことはないでしょうか?
<template lang="pug">
div
label
input(v-model="$data.value", type="radio", value="A")
| A
label
input(v-model="$data.value", type="radio", value="B")
| B
</template>
これを以下のようにselectと同じように親だけv-modelを設定するだけで済むようなコンポーネントを作ることができますので、そのやり方を紹介したいと思います。
<template lang="pug">
div
//- 親コンポーネントだけv-modelを設定する
RadioButtonGroup(v-model="$data.value")
RadioButton(value="A") A
RadioButton(value="B") B
</template>
実装方法
タイトルにも書いてありますが、JSXを使用します。JSXを使用することで子コンポーネントに対してプロパティを注入することができるので、RadioButton
コンポーネントを探してv-model
のプロパティを注入する形になります(正確にはv-modelの対象となるprops名を入れます)。
vnodeの操作についてはvee-validateのコードを参考にしました。
- https://github.com/logaretm/vee-validate/blob/bf5a3707538c97d77ae0b4c48cdc6ee9a0ac2b7a/src/components/provider.js#L328
- https://github.com/logaretm/vee-validate/blob/bf5a370753/src/utils/vnode.js#L34
またvnodeのプロパティの注入はこちらを参考にしました。
const RADIO_BUTTON_NAME = "RadioButton";
/**
* リスナーオブジェクトにリスナーを追加する
* @param {Object} listeners - リスナーオブジェクト
* @param {string} eventName - イベント名
* @param {function} listener - コールバック関数
*/
function _addListener(listeners, eventName, listener) {
// リスナーが未登録の場合は登録して終了
if (!listeners[eventName]) {
listeners[eventName] = [listener];
return;
}
// 既に単体でリスナーが登録されている時は配列形式にする
if (typeof listeners[eventName] === "function") {
listeners[eventName] = [listeners[eventName]];
}
// 配列形式のリスナーは単純に追加する
listeners[eventName].push(listener);
}
/**
* vnodeにリスナーを追加する
* @param {Object} vnode - vnode
* @param {string} eventName - イベント名
* @param {function} listener - コールバック関数
*/
function addListener(vnode, eventName, listener) {
// リスナーのプロパティ自体がない時は初期化する
if (!vnode.componentOptions.listeners) {
vnode.componentOptions.listeners = {};
}
_addListener(vnode.componentOptions.listeners, eventName, listener);
}
/**
* componentNameにマッチするvnodeにapplyFuncを適応させる
* @param {Array} vnodes - vnodeリスト
* @param {string} componentName - コンポーネント名
* @param {function} applyFunc - マッチしたvnodeに対しての処理
*/
function applyMatchedNodes(vnodes, componentName, applyFunc) {
const regex = new RegExp(`${componentName}$`);
for (let i = 0; i < vnodes.length; i++) {
const vnode = vnodes[i];
if (regex.test(vnode.tag)) {
applyFunc(vnode);
}
// 子供がある場合は再帰する
if (vnode.children) {
applyMatchedNodes(vnode.children, componentName, applyFunc);
}
}
}
export default {
name: 'RadioButtonGroup',
model: {
prop: "selectedValue",
event: "select",
},
props: {
selectedValue: { type: String },
},
render() {
const vnodes = this.$slots.default;
// ラジオボタンのコンポーネント全てにプロパティを注入する
applyMatchedNodes(vnodes, RADIO_BUTTON_NAME, (vnode) => {
if (!vnode.componentOptions || !vnode.componentOptions.propsData) {
return;
}
// 注入するパラメータを入れる(v-modelの対象となるprops名をselectedValueにしているため、その値を入れる)
vnode.componentOptions.propsData.selectedValue = this.$props.selectedValue;
// リスナーを設定する
addListener(vnode, "select", (value) => {
this.$emit("select", value);
});
});
return <div>{vnodes}</div>;
},
};
v-model
の値を注入する処理は子孫まで見てくれるため、以下のようにスタイル調整のためにdivが入ってもきちんと値の連動はしてくれます。
<template lang="pug">
div
RadioButtonGroup(v-model="$data.value")
.list
.list__item
//- 途中divタグが入っているが、きちんとv-modelが注入されている
RadioButton(value="A") A
.list__item
RadioButton(value="B") B
.list__item
RadioButton(value="C") C
</template>
<style lang="scss" scoped>
.list {
display: flex;
&__item {
width: 50px;
}
}
</style>
終わりに
以上がJSXを使ってラジオボタンの設定をシンプルにする方法でした。JSXはVue.jsでは普段使わないものですが、一部では使うと書きやすくなることがあるため、これを機会に使ってみるのはいかがでしょうか。
サンプルはCodeSandboxに書きましたので、興味がある方は是非見てください。