JSXを使ってラジオボタンの設定をシンプルにする

# 始めに
Vue.jsでラジオボタンの設定をする時は以下のように書くと思いますが、毎回v-modelの設定をするのが面倒だと感じたことはないでしょうか？

```vue:ラジオボタンの設定
<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を設定するだけで済むようなコンポーネントを作ることができますので、そのやり方を紹介したいと思います。

```vue:selectと同じような設定方法
<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のプロパティの注入はこちらを参考にしました。

+ https://forum.vuejs.org/t/can-i-add-attributes-to-vnode/2356/2

```javascript:JSXで必要なプロパティ・イベントを注入する
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が入ってもきちんと値の連動はしてくれます。

```vue:子孫でもv-modelの連携はできる
<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に書きましたので、興味がある方は是非見てください。

https://codesandbox.io/s/jsxwoshituteraziobotannoshedingwosinpurunisuru-s0xz5?file=/src/App.vue