ymmooot

composition-api でモーダルの制御をコンポーネントから分離する

Nuxt.js のサービス上でポイントを消費する際に確認するモーダルを作る。
モーダルには vue-thin-modal を使っている。

composable/point.tsimport { ref, computed } from '@nuxtjs/composition-api';
import { Accessor } from '@/store';

const needPoint = ref(0);
let confirmResolve: (ans: boolean) => void;

export const usePoint = ($accessor: Accessor, $modal: Vue['$modal']) => {
  const modalName = 'point-confirmation-modal';
  const currentPoint = computed(() => $accessor.user.point);
  const hasEnoughPoint = computed(() => currentPoint.value - needPoint.value > 0);
  const afterPoint = computed(() => currentPoint.value - needPoint.value);

  const confirm = (ans: boolean) => {
    confirmResolve(ans);
  };

  const openModal = async (point: number): Promise<boolean> => {
    needPoint.value = point;
    const promise = new Promise<boolean>((resolve): void => {
      confirmResolve = resolve;
    });

    $modal.push(modalName);
    const ans = await promise;
    $modal.pop();
    return ans;
  };

  return {
    modalName,
    needPoint,
    currentPoint,
    hasEnoughPoint,
    afterPoint,
    confirm,
    openModal,
  };
};

openModal 関数は needPoint にこれから消費するポイントを保存した上で、新しい Promise オブジェクトを作る。
この Promise オブジェクトは resolve を confirmResolve に保存することで、confirm 関数から resolve させることができる。
そして、モーダルを開き、confirm 関数によって Promise が resolve されるのを待ってからモーダルを閉じる。
最後に resolve の結果を返す。

これにより、コンポーネントからはモーダルを開けたり締めたりする制御を行う必要がなくなり、openModal を呼び出す際に消費ポイントだけ渡して、結果を待てば良い。

実際の利用例は以下のようになる。

SomeActionComponent.vue<template>
  <div>
    <p>ポイントをつかってほげほげ</p>
    <button @click="onClickHandler">はい</button>
  </div>
</template>

<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api';
import { usePoint } from '@/composable/point';

export default defineComponent({
  setup(_, { root }) {    
    const { openModal } = usePoint(root.$accessor, root.$modal)

    const onClickHandler = async () => {
      const ok = await openModal(50)
      if (!ok) {
        return
      }
      consumePoint()
    }

    return {
      onClickHandler,
    }
  }
})
ConfirmPointConsumption.vue<template>
  <modal :name="modalName" :disable-backdrop="true">
    <div>
      This will take {{ needPoint }} points. You have {{ currentPoint }} points.
      <p v-if="hasEnoughPoint">
        It will be {{ afterPoint }} points.
        <button @click="ok">ok</button>
        <button @click="cancel">cancel</button>
      </p>
      <p v-else>
        You don't have enough points.
        <button @click="cancel">ok</button>
      </p>
    </div>
  </modal>
</template>

<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api';
import { usePoint } from '@/composable/point';

export default defineComponent({
  setup(_, { root }) {    
    const { modalName, currentPoint, afterPoint, needPoint, hasEnoughPoint, confirm } = usePoint(
      root.$accessor,
      root.$modal,
    );
    const ok = () => {
      confirm(true);
    };
    const cancel = () => {
      confirm(false);
    };

    return {
      modalName,
      currentPoint,
      afterPoint,
      needPoint,
      hasEnoughPoint,
      ok,
      cancel,
    };
  },
});
</script>

これを見ると、SomeActionComponent.vue からはモーダル制御のロジックが、ConfirmPointConsumption.vue からはモーダル制御及び残ポイント計算などのロジックが分離されていることが分かる。

これによりコンポーネントは「ユーザーのアクションによりロジックを呼び出す」、「計算結果を表示する」といったユーザーのインターフェースとして責務に専念できる。