Vue.js: 外部のJSでの値の変更を反映させる（単純なstoreパターン）

.vueファイルの外にある.jsファイルにオブジェクトを置き、そのプロパティの値が変更されたらVueコンポーネントであれこれする方法を紹介します。SPAまたはTurbolinksでページ遷移するときは、データを保持したまま使い回すこともできます。

要するに、[単純なstoreパターン](https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch)です。

![state.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308595/3eb77100-6dca-3aad-9589-4f4047218da7.png)

次のような場合に使います。

- windowオブジェクトのイベント処理（resizeやscroll）
- RailsのActionCableでのデータ受信をイベントのように扱う
- ユーザーの選択結果を一時的に（ページがリロードされない限り）保存しておく
- Ajaxで取得したデータがもったいないので使い回す

## storeデータの表示

2020-02-01修正: storeオブジェクトの中にstateオブジェクトを持たせる形に変更。

ブラウザーのサイズ変更やスクロールを検出するサンプルです。

WindowSize.jsにオブジェクトstoreを置いてエクスポートします。windowのresizeとscrollイベントで、store.stateのプロパティの値を変更します。

（Vueのcreatedやmountedで `window.addEventListener` を行うのは、windowのイベント処理が重複する可能性があるのでよくないです、念のため。）

```javascript:WindowSize.js
const store = {
  state: {
    width: window.innerWidth,
    height: window.innerHeight,
    scrollX: window.pageXOffset,
    scrollY: window.pageYOffset
  }
};

window.addEventListener('resize', () => {
  store.state.width = window.innerWidth;
  store.state.height = window.innerHeight;
});

window.addEventListener('scroll', () => {
  store.state.scrollX = window.pageXOffset;
  store.state.scrollY = window.pageYOffset;
});

export default store;
```

.vueファイルでは、WindowSize.jsのオブジェクトをインポートして、dataのプロパティwinSizeに指定します。ブラウザーのリサイズやスクロールに合わせて数値の表示が変わります。

```vue:Home.vue
<template>
  <div>
    <div>{{winSize.width}}x{{winSize.height}}</div>
    <div>{{winSize.scrollX}},{{winSize.scrollY}}</div>
    <div style="width: 2000px; height: 1000px; background-color: #eee">
    </div>
  </div>
</template>

<script>
import WindowSize from '../states/WindowSize';

export default {
  data() {
    return {
      winSize: WindowSize.state
    };
  }
}
</script>
```

## storeデータの変更をwatchする

外部オブジェクトのプロパティに値が代入されたときに、イベント処理のように何か行うにはwatchを使います。次の例は、ページを下までスクロールして、`ref="message"`のdiv要素が見えたら表示を変えるものです。

watchの中では、`'winSize.scrollY'`のように「オブジェクト.プロパティ」の形の関数名が使えます。

```vue:Home.vue
<template>
  <div>
    <div>{{winSize.width}}x{{winSize.height}}</div>
    <div>{{winSize.scrollX}},{{winSize.scrollY}}</div>
    <div style="width: 2000px; height: 1000px; background-color: #eee">
    </div>
    <div ref="message">{{message}}</div>
  </div>
</template>

<script>
import WindowSize, { ElementAppeared } from '../states/WindowSize';

export default {
  data() {
    return {
      winSize: WindowSize.state,
      message: 'Waiting...'
    };
  },
  watch: {
    'winSize.scrollY'() {
      if(ElementAppeared(this.$refs.message)) {
        this.message = 'Hello!';
      }
    }
  }
}
</script>
```

次のようにcomputedで中継することもできます。

```vue:Home.vue
<script>
import WindowSize, { ElementAppeared } from '../states/WindowSize';

export default {
  data() {
    return {
      winSize: WindowSize.state,
      message: 'Waiting...'
    };
  },
  computed: {
    scrollY() {
      return this.winSize.scrollY;
    }
  },
  watch: {
    scrollY() {
      if(ElementAppeared(this.$refs.message)) {
        this.message = 'Hello!';
      }
    }
  }
}
</script>
```

ここで使っている関数ElementAppearedは次のようになります。

```javascript:WindowSize.js
export function ElementAppeared(elem) {
  if(elem) {
    let rect = elem.getBoundingClientRect();
    return (rect.top < store.state.height);
  }
  return false;
}
```