Vuejs dataプロパティに動的に値を追加する方法(とJSの基本を理解してなかったという話)

Vuejsのdataプロパティとは

{{ }}形式で記載したときに、リアクティブに扱われる値。つまり、dataオブジェクトのプロパティの値が書き換わったタイミングで、DOMが書き換わる。

サンプル

以下のような単一コンポーネントを作成する。

  ...
  "dependencies": {
    "vue": "^2.5.2",
    "vue-router": "^3.0.1"
  },
  ...
<template>
  <div>
    <h1>{{ msg }}</h1>
    <button type="button" @click="change">change</button>
  </div>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      msg: 'hello world'
    }
  },
  methods: {
    change: function () {
      this.msg = 'good bye'
    }
  }
}
</script>

<style scoped>
div {
  text-align:center;
}
</style>

これをブラウザで表示すると、 {{ msg }} 部分がdataオブジェクトのmsgプロパティの値である「hello world」に置き換えられたものが表示される。また、ボタンをクリックすると、メッセージが「good bye」に変わる。

f:id:kimulla:20191201214011g:plain

これは、以下のような流れで処理される。

  1. HTMLのボタンをクリックするとclickイベントが発生する
  2. @click(イベントハンドラ的なもの)で指定したchangeメソッドが呼ばれる
  3. Vueインスタンスのdataオブジェクトのmsgプロパティが書き換わる
  4. dataオブジェクトのプロパティの変更をVueが検知する
  5. VuejsがHTMLに値を反映する

このプロパティの変更の検知は、JSの機能的な制限(Object.observe)によってプロパティ自体の追加には対応していない。2.6~のどこかで将来的には対応予定らしいが、少なくとも2.5以前は対応していない。
参考 State of VueJS 2018
参考 Documentation on new proxy reactivity in vue-next

つまり以下のような処理では、動的に追加したプロパティは無視されてしまう。

<template>
  <div>
    <ul>
      <li v-for="(value,key) in favorites">
        {{value}} likes {{key}}
      </li>
    </ul>
    <button type="button" @click="add">add charie</button>
  </div>
</template>

<script>
export default {
  name: 'sample',
  data () {
    return {
      favorites: {
        'alice': 'apple',
        'bob': 'banana'
      }
    }
  },
  methods: {
    add: function () {
      // 動的にプロパティとして値を追加する
      this.favorites.charie = 'cherry'
    }
  }
}
</script>

<style scoped>
</style>

実際にブラウザでボタンをクリックし、動的にdataオブジェクトにプロパティを追加してみる。Chrome Developer Toolで確認すると、favoritesに値が追加されているにもかかわらず、その値が画面に反映されていない。

f:id:kimulla:20191201214156g:plain

で、これをどうするかというのも公式に書いてある。 vuejs リファレンス

抜粋すると

Vue はすでに作成されたインスタンスに対して動的に新しいルートレベルのリアクティブなプロパティを追加することはできません。しかしながら Vue.set(object, key, value) メソッドを使うことで、ネストしたオブジェクトにリアクティブなプロパティを追加することができます: グローバル Vue.set の単なるエイリアスとなっている vm.$set インスタンスメソッドを使用することもできます: Vue.set(vm.someObject, 'b', 2)

課題

単一コンポーネントで$setをどのように使うのか?

リファレンスみたいにVueコンポーネントなんてimportしてないし、vmインスタンスなんて変数に格納してないし、どうすればいいのか。

解決方法

メソッド内ではthisがVueを指すため、以下のようにすればよい。

  methods: {
    add: function () {
      this.$set(this.favorites, 'charie', 'cherry')
    }
  }

すると、動的に値が追加できる。

f:id:kimulla:20191201214257g:plain

また、ディレクティブに直接書くならthisは不要。

    ...
    <button type="button" @click="$set(favorites, 'charie', 'cherry')">add charie</button>
  </div>

JSの基本を理解してなかった話

ここからは解決方法に至るまでの道のり。

とりあえずchangeメソッド内でthisをChrome Developer Toolsで表示してみた。すると

f:id:kimulla:20191201214312p:plain

$setプロパティがない・・・?

いちおう this.$set も試すと・・・

f:id:kimulla:20191201214325p:plain

$setプロパティがある・・・?ある!

JSでは、prototypeで定義されたメソッドはそのオブジェクトのプロパティとしては見えないらしい。 参考 JavaScript のオブジェクトのプロパティ一覧を取得する方法とオブジェクトが指定の名前のプロパティをもっているか検査する方法

コンソールで試してみると、確かにその通り。

f:id:kimulla:20191201214338p:plain

代わりに for..in.. を使えば全プロパティ表示される。なるほど。

f:id:kimulla:20191201214350p:plain

Vueでも確かめてみると確かに$setプロパティある!

f:id:kimulla:20191201214404p:plain

プロパティを片っ端から調べたければ for..in..で調べれば良かったのか。