【Vue.js, Nuxt.js】storeを使用してTypeScriptの処理中にprogressbarを起動させる（progressbarのグローバル化）

# この記事について
以下のようなprogressbarをVueのtemplate上での v-if や v-show での表示制御ではなく、TypeScript側から任意のタイミングで表示できるようにする方法をこの記事で紹介します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234275/8364fd1f-bdad-b4b2-468a-e7438ee8fd71.png)
以下の記事で記載した実装と同じ考え方で実装しております。
[【Vue.js, Nuxt.js】storeを使用してTypeScript側からsnackbarを起動させる](https://qiita.com/shin4488/items/bd1b04a9b0688c201c4e)

# 対象読者
* Vue.js（やNuxt.js）でprogressbarをTypeScriptの処理中に表示したい方
* Vue.jsのstoreの実装を行ったことのある方
* Vuetifyの（簡易的な）実装を行ったことのある方

# 環境
|内容|バージョン|
|---|---|
|nuxt|2.15.8|
|nuxt-typed-vuex|0.3.0|
|vue|2.6.14|
|vuetify|2.6.1|

以下の内容は、Nuxt.jsを使用していることを前提とします。
ディレクトリ構成

```
sampleProject
├componens
| └progressbar.vue
├pages
| └index.vue
├store
| ├index.ts
| └progressbar.ts
└types
  └index.d.ts
```

# 処理の流れ
考え方としては、「progressbar用のstoreを作成して、store呼び出し時にprogressbarコンポーネントを表示する」として、「progressbarコンポーネントをグローバルに扱う」考え方としております。

progressbarを表示したい処理の実行
↓
progressbar用のstoreの呼び出し（progressbar表示中に実行したい処理も一緒に引き渡す）
↓
処理中はprogressbarを表示するようにstoreのstateを更新
↓
処理後はprogressbarを非表示とするようにstoreのstateを更新

[こちらの記事](https://qiita.com/shin4488/items/bd1b04a9b0688c201c4e#%E5%87%A6%E7%90%86%E3%81%AE%E6%B5%81%E3%82%8C)の「処理の流れ」と同じような流れとなっております。

# progressbar用のstoreの作成
storeはprogressbar用の個別storeを管理する `progressbar.ts` と 他のstoreも含めて管理する `index.ts` に分けております。

```store/progressbar.ts
import { mutationTree, actionTree } from 'typed-vuex';

// progressbar表示/非表示の制御用のデータのみをstateで管理
export const state = () => ({
  isShown: false as boolean,
});
export type RootState = ReturnType<typeof state>;

export const mutations = mutationTree(state, {
  change(state, isShown: boolean) {
    state.isShown = isShown;
  },
});

export const actions = actionTree(
  { state, mutations },
  {
    async open({ commit }, action: Function) {
      // actionを実行中のみprogressbarをオン、実行後はオフとする
      commit('change', true);
      try {
        await action();
      } finally {
        // 引き渡された処理が失敗した時を想定してfinally内でprogressbarを非表示とする
        commit('change', false);
      }
    },
  },
);
```

```store/index.ts
import {
  getAccessorType,
  getterTree,
  mutationTree,
  actionTree,
} from 'typed-vuex';
// その他のstoreがある場合はここで他のstoreをimportする
import * as progressbar from './progressbar';

export const state = () => ({});
export const getters = getterTree(state, {});
export const mutations = mutationTree(state, {});
export const actions = actionTree({ state, getters, mutations }, {});
export const accessorType = getAccessorType({
  state,
  getters,
  mutations,
  actions,
  modules: {
    // その他のstoreがある場合はmodulesに他のstoreを追加する
    progressbar,
  },
});
```

VueファイルのTypeScriptからstoreにアクセスできるように、`index.d.ts` を以下のように編集します。

```types/index.d.ts
import { accessorType } from '~~/store';

declare module 'vue/types/vue' {
  interface Vue {
    $accessor: typeof accessorType;
  }
}

declare module '@nuxt/types' {
  interface NuxtAppOptions {
    $accessor: typeof accessorType;
  }
}
```

# progressbarコンポーネントの作成
先ほど作成したprogressbar用のstoreのstate変更を検知するように実装（ `this.$store.subscribe` の部分）します。
progressbarは[Vuetifyのコンポーネント](https://vuetifyjs.com/ja/components/progress-circular/#indeterminate)を使用しております。

```componens/progressbar.vue
<template>
  <!-- モーダルダイアログよりも上に表示できるようにz-indexを設定 -->
  <v-overlay z-index="100" :value="isShown">
    <v-progress-circular indeterminate color="primary"></v-progress-circular>
  </v-overlay>
</template>

<script lang="ts">
import Vue from 'vue';

export default Vue.extend({
  name: 'AppProgressbar',
  data() {
    return {
      isShown: false,
    };
  },
  created() {
    this.$store.subscribe((mutation, state) => {
      if (mutation.type === 'progressbar/change') {
        this.isShown = state.progressbar.isShown;
      }
    });
  },
});
</script>
```

# TypeScriptからprogressbarを表示
progressbarを表示する際は、`components/progressbar.vue` を直接呼び出すのではなく、先ほど作成したstoreを呼び出してstore経由でprogressbarを表示します。
vueのtemplate内でprogressbarを `v-if` や `v-show` で表示制御せずともTypeScriptから表示/非表示とすることができるようになりました。

```pages/index.vue
<template>
  <button @click="doHeavyProcess">Execute</button>
</template>

<script lang="ts">
import Vue from 'vue';

export default Vue.extend({
  methods: {
    async doHeavyProcess() {
      await this.$accessor.progressbar.open(async () => {
        // この中の処理中にprogressbarが表示され、処理が終了するとprogressbarが非表示となる
        await this.$axios.get('https://example.com');
      });
    },
  },
});
</script>
```

# 参考
[How to create a global snackbar using Nuxt, Vuetify and Vuex.](https://dev.to/stephann/how-to-create-a-global-snackbar-using-nuxt-vuetify-and-vuex-1bda)
[【Vue.js, Nuxt.js】storeを使用してTypeScript側からsnackbarを起動させる](https://qiita.com/shin4488/items/bd1b04a9b0688c201c4e)