[Vue.js3] 9. Pinia

9.1 Pinia

Pinia(ピニア)はコンポーネント横断でのデータのやり取りに際し、Provide/Inject では力不足な場面で、その問題を解決してくれるものである。

9.1.1 Provide/Inject の問題点

Provide/Injectのデータのやり取り例

単に Inject したデータを表示するだけでなくデータの変更も加えると、データ変更処理が各コンポーネント内に散在していくことになる。
この状態でアプリケーションが大きくなっていくと、どのコンポーネントでどのような変更処理が行われているのか管理できなくなり、同じような変更処理が数箇所のコンポーネントに記述さてているという状況も発生する。

問題の原因はデータ処理コードの散在なので、dattatccess.ts のようなファイルを作成し、その中にデータ処理コードをまとめるという方法も考えられる。
ただし、リアクティブシステムとの関係上、あくまでまとめられるのはデータの更新及び加工処理程度であり、データそのものは Provide/Inject に頼る必要がある。

解決方法

Pinia はストア(Store)ライブラリと呼ばれ、コンポーネントやページをまたいで状態(State)を共有・管理するためのライブラリである。
状態を管理するということは、アプリケーションで利用するデータ本体のみならず、そのデータへの変更・加工処理もまとめて管理できることを意味する。
もちろん、Pinia で用意したデータはリアクティブシステムの対象で、各コンポーネントでデータを表示したい場合はストアで用意されたデータ本体を参照する。
また、データの変更や加工したデータの利用を行いたい場合は、同じくストアで用意した専用の関数を呼び出す。

ストアを提供する Pinia

9.1.2 Pinia プロジェクトの作成

Pinia を利用するプロジェクトでは、Vue プロジェクトの「Pinia (state management)」を選択して作成します。

フォルダ内に stores フォルダが追加されたプロジェクトが作成される。
フォルダ内には counter.ts ファイルが用意されたおり、このファイルにアプリケーションで利用するデータや、そのデータの加工処理・変更処理を記述することで各コンポーネントから利用できるようになる。

9.1.3 ストアファイル内記述の基本構造

Piniaには2つの書き方があり、どちらのバージョンでも両方動きます。

  • 「Options API」に似た Option Stores という書き方
  • 「Composition API」に似た Setup Stores という書き方

Option Stores

Vue2 でおなじみだった Options API に近い、役割ごとにエリアを分ける伝統的な書き方。

counter.ts
import {defineStore} from 'pinia';  // 1.

export const useCounterStore = defineStore({  // 2.
  id: 'counter',                              // 3.
  state: () => ({                             // 4.
    counter: 0
  }),
  getters: {                                  // 5.
    doubleCount: (state) => state.counter * 2
  },
  actions: {                                  // 6.
    increment() {
      this.counter++
    }
  }
};
  1. pinia から defineStore()関数をインポート
  2. インポートした defineStore()関数を実行した結果を useCounterStore としてexport
    • defineStore()関数の引数オブジェクトは、id、state、getters、actions の各プロパティから構成されている。
    • エクスポートされる内容はストア生成関数となる。そのため、この戻り値の名称は通常「use+ストア名+Store」のキャメル記法とする
  3. id: このストアを一意に識別する名前(キー)
    • アプリケーション内で一意となるように命名する
    • 管理しやすくするためにファイル名と同じにすることが推奨されている
  4. state: データを管理する場所(Vue の data に相当)
    • データの本体を記述
  5. getters: 計算した値を返す場所(Vue の computed に相当)
    • データの加工処理を記述
  6. actions: データを変更する関数を置く場所(Vue の methods に相当)
    • データの変更処理を記述

Setup Stores

Vue3 の Composition API と同じ感覚で書ける、現在主流の書き方

pinia-basic/src/stores/counter.ts
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'                      // 1.

export const useCounterStore = defineStore(              // 2.
  'counter',                                             // 3.
  () => {
    const count = ref(0)                                 // 4.
    const doubleCount = computed(() => count.value * 2)  // 5.
    function increment() {                               // 6.
      count.value++
    }
    return { count, doubleCount, increment }             // 7. 
  }
  1. pinia から defineStore() 関数をインポート
  2. defineStore() 関数を実行した結果を useCounterStore として export している
    • defineStore()の戻り値に名前を付けて export している
    • 戻り値の名称は通常「use + ストア名 + Store」のキャメル記法とする
  3. 第1引数 ‘counter’
    • Option Storesの id: ’counter’ と同じ役割
    • このストアを特定するための名称
    • アプリケーション内で一意(ユニーク)となるように命名する
    • 管理しやすくなるためにファイル名と同じにすることが推奨されている
  4. ref(0)
    • state(データ)を定義している
    • データを管理する場所(データ本体)
  5. computed()
    • getters(算出プロパティ)を定義している
    • 計算した値を返す場所(データの加工処理)
  6. function
    • actions(関数)を定義している
    • データを変更する関数を置く場所(データの変更処理)
  7. return {…}
    • 画面や他のコンポーネントから使いたいデータや関数を外に公開している

9.1.4 ステートとそのインターフェース

サンプルは Setup Stores で記述している

TypeScriptらしい記述方法を採用し、プロパティ名などもわかりようすように変更したソース

pinia-basic/src/stores/counter.ts
import { ref, computed } from "vue";
import { defineStore } from "pinia";

export const useCounterStore = defineStore("counter", () => {
  // state: refを使用(型推論されるため基本は : number は省略可能ですが、明示してもOKです)
  const counter = ref<number>(0);

  // getters: computedを使用
  const doubleCount = computed<number>(() => {
    return counter.value * 2;
  });

  // actions: 通常の関数を使用(戻り値がないため : void)
  function incrementCount(): void {
    counter.value++;
  }

  // 外部のコンポーネントで使いたいものをすべて返却する
  return {
    counter,
    doubleCount,
    incrementCount
  };
});

9.1.5 コンポーネントからのストア利用

App.vue
<script setup lang="ts">
import { useCounterStore } from "./stores/counter";  // 1.

// ストアを呼び出す
// この中の値はリアクティブなので直接テンプレートで使える
const counterStore = useCounterStore();    // 2.

// ボタンクリック時の処理
const onIncrementClick = () => {           // 3.
  counterStore.incrementCount();
};

</script>

<template>
  <p>現在のポイント:{{ counterStore.counter }}</p>             <!-- 4. -->
  <p>現在のポイントさらに倍:{{ counterStore.doubleCount }}</p>  <!-- 5. -->
  <button @click="onIncrementClick">加算</button>
</template>
  1. インポート
    • ストアファイルからエクスポートされる defineStore()の結果をインポート
  2. ストアオブジェクト
    • インポートした useCountoreStoreを実行し、変数として格納。
    • この変数がストアオブジェクトになる。
    • この中の値はリアクティブなので直接テンプレートで使える。
  3. アクションの利用
    • コンポーネントでストアアクションを利用する場合はステートオブジェクトから直接アクション関数を呼び出す。
  4. ステートの利用
    • ストアオブジェクトの中の値はリアクティブなので、直接呼び出して利用できる。
  5. ゲッタの利用
    • ストアオブジェクトの中の値はリアクティブなので、直接呼び出して利用できる。

9.2 引数を使う一歩進んだ Pinia の利用方法

9.2.1 プロジェクト作成

Vue Router と Pinia を選択

9.2.2 Provide の代わりの初期リストの用意

interface

interfaces.ts
export interface Member {
  id: number
  name: string
  email: string
  points: number
  note?: string
}

ストアファイル

ファイル名:member.ts
エクスポート名:useMembersStore

member.ts
import {ref} from "vue";
import {defineStore} from "pinia";
import type {Member} from  "@/interfaces.ts";

export const useMembersStore = defineStore(
    "members",
    () => {
        // state    1.
        const memberList = ref<Map<number, Member>>(new Map<number, Member>());

        // getters  2.
        const getById = computed(() => {
            return (id: number): Member => {
            // stateの値にアクセスするため memberList.value を使用
            const member = memberList.value.get(id) as Member;
            return member;
            };
        });
        
        // actions  
        function initList(): void {          // 3.
            memberList.value.set(33456, {
              id: 33456, 
              name: "田中太郎", 
              email: "bou@example.com", 
              points: 35, 
              note: "初回入会特典あり。",
            });
            memberList.value.set(47783, {
              id: 47783, 
              name: "鈴木二郎", 
              email: "mue@example.com", 
              points: 53,
            });
        }
        
        function addMember(member: Member): void {    // 4.
            memberList.value.set(member.id, member);
        }

        return {  // 5.
            memberList,
            getById,
            initList,
            addMember,
        }
    }
)
  1. ステートとして会員情報リストデータを表す memberList を定義
    • 初期値は空の Map が定義される
  2. 引数付きゲッタの定義
    • computed の中で「引数(id)を受け取って Member を返す関数」をそのまま return。
    • state の値にアクセスするため memberList.value を使用
  3. memberList の初期データを生成する関数 initList() をアクションに定義
    • ref の値にアクセスするため、this.memberList ではなく memberList.value を使用
  4. memberList に会員を追加する引数付きのアクション addMember を定義
    • state の値にアクセスするため memberList.value を使用
  5. 外部のコンポーネントで使いたい変数や関数を返却

App.vue

App.vue
<script setup lang="ts">
import { RouterView } from 'vue-router'
import {useMembersStore} from "@/stores/members"  // 1.

const memberStore = useMembersStore()   // 2.
memberStore.initList()                  // 3.

</script>

<template>
  <!-- 4. ヘッダ -->
  <header>
    <h1>Vue Routerサンプル</h1>
  </header>

  <!-- 5. ルーティング表示領域を囲むタグ <main> -->
  <main>
    <!-- 6. ルーティング表示領域 (使用前にvue-routerからRouterViewをインポートしておく)-->
    <RouterView />
  </main>
</template>

<style>
/* 7. main タグに枠線を表示する設定。 */
main {
  border: blue 1px solid;
  padding: 10px;
}

/* 8. 以下はパンくずリストを表示するための定型スタイル */
#breadcrumbs ul li {
  display: inline;
  list-style-type: none;
}
#breadcrumbs {
  margin-left: 0px;
}
#breadcrumbs ul {
  padding-left: 0px;
}
#breadcrumbs ul .current {
  color: red;
}
#breadcrumbs ul li:before {
  content: ' > ';
}
#breadcrumbs ul li:first-child:before {
  content: none;
}
</style>
  1. ストアファイルからエクスポートされたストア生成関数をインポート
  2. ストア生成関数を実行してストアオブジェクトを用意

index.ts

特に変更はないので、このまま利用

index.ts
import { createRouter, createWebHistory } from 'vue-router'
// routes 配列の型定義
import type { RouteRecordRaw } from 'vue-router'
import AppTop from '@/views/AppTop.vue'

// createRouter({})の「routes: []」で直接記述されていな配列を分離したもの。
const routeSettings: RouteRecordRaw[] = [
  {
    // AppTop
    // URLがルート(/)パスの際にルーティング表示領域に表示する設定
    path: '/',
    name: 'AppTop',
    component: AppTop,
  },
  {
    // MemberList
    path: '/member/memberList',
    name: 'MemberList',
    component: () => {
      return import('@/views/member/MemberList.vue')
    },
  },
  {
    // MemberDetail
    path: '/member/detail/:id',
    name: 'MemberDetail',
    component: () => {
      return import('@/views/member/MemberDetail.vue')
    },
    props: (routes) => {
      const idNum = Number(routes.params.id)
      return {
        id: idNum,
      }
    },
  },
  {
    // MemberAdd
    path: '/member/add',
    name: 'MemberAdd',
    component: () => {
      return import('@/views/member/MemberAdd.vue')
    },
  },
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: routeSettings,
})

export default router

TOP画面を表示する AppTop.vue

特に変更はないので、このまま利用

AppTop.vue
<script setup lang="ts">
import { RouterLink } from 'vue-router'
</script>

<template>
  <h1>TOP</h1>
  <nav id="breadcrumbs">
    <ul>
      <li>TOP</li>
    </ul>
  </nav>
  <section>
    <p>
      <RouterLink v-bind:to="{ name: 'MemberList' }"> 会員管理はこちら </RouterLink>
    </p>
  </section>
</template>

会員情報をリスト表示する MemberList.vue

Provide された memberList を jnject して表示されていたものを、ストアから取得するように変更

memberList.vue
<script setup lang="ts">
import { RouterLink } from 'vue-router'
import { useMembersStore } from '@/stores/members';

const membersStore = useMembersStore()    // 1.

</script>

<template>
  <h1>会員管理</h1>
  <nav id="breadcrumbs">
    <ul>
      <li>
        <RouterLink v-bind:to="{ name: 'AppTop' }"> TOP </RouterLink>
      </li>
      <li>会員リスト</li>
    </ul>
  </nav>
  <section>
    <h2>会員リスト</h2>
    <p>新規登録は<RouterLink v-bind:to="{ name: 'MemberAdd' }">こちら</RouterLink>から</p>
    <section>
      <ul>
        <li v-for="[id, member] in membersStore.memberList" v-bind:key="id">     <!-- 2. -->
          <RouterLink v-bind:to="{ name: 'MemberDetail', params: { id: id } }">
            IDが{{ id }}の{{ member.name }}さん
          </RouterLink>
        </li>
      </ul>
    </section>
  </section>
</template>
  1. ストアファイルからエクスポートされたストア生成関数を実行し、ストオブジェクトを用意
  2. 直接 membersStore.memberList を使用

会員情報の詳細を表示する MemberDetail.vue

当初、Props として受け取った会員IDを元に会員情報リストデータから1人分の会員データを取得して表示してた。

interface Props {
  id: number
}
const props = defineProps<Props>()
const memberList = inject('memberList') as Map<number, Member>
const member = computed((): Member => {
  return memberList.get(props.id) as Member
})

これを、Vue Ruterの useRoute()関数で取得したオブジェクトのルートパラメータを利用するように変更した。

const route = useRoute()
const id = Number(route.params.id)
const memberList = inject('memberList') as Map<number, Member>
const member = ref(memberList.get(id) as Member)

これを引数付きのゲッタを利用するように変更

MemberDetail.vue
<script setup lang="ts">
import { computed } from 'vue'
import { RouterLink, useRoute } from 'vue-router'
import { useMembersStore } from '@/stores/members'

const route = useRoute()
const id = Number(route.params.id)       // 1.

const membersStore = useMembersStore()
const member = membersStore.getById(id)  // 2.

const localNote = computed((): string => {
  let localNote = '---'
  if (member.note != undefined) {
    localNote = member.note
  }
  return localNote
})
</script>

<template>
  <h1>会員管理</h1>
  <nav id="breadcrumbs">
    <ul>
      <li>
        <RouterLink v-bind:to="{ name: 'AppTop' }"> TOP </RouterLink>
      </li>
      <li>
        <RouterLink v-bind:to="{ name: 'MemberList' }"> 会員リスト </RouterLink>
      </li>
      <li>会員詳細情報</li>
    </ul>
  </nav>
  <section>
    <h2>会員詳細情報</h2>
    <dl>
      <dt>ID</dt>
      <dd>{{ member.id }}</dd>
      <dt>名前</dt>
      <dd>{{ member.name }}</dd>
      <dt>メールアドレス</dt>
      <dd>{{ member.email }}</dd>
      <dt>備考</dt>
      <dd>{{ localNote }}</dd>
    </dl>
  </section>
</template>
  1. ルートパラメータからIDを取得
  2. 引数付きのゲッタを関数のように呼び出して ID の会員情報を取得
    • ストア内で絞り込みを行って1人分の会員データを受け取っている

会員情報を追加する MemberAdd.vue

Provide された memberList を jnject に、入力された怪異情報を表す Member オブジェクトを追加している処理を、ストアのアクションを利用したものに変更

MemberAdd.vue
<script setup lang="ts">
import { reactive } from 'vue'
import { RouterLink, useRouter } from 'vue-router'
import type { Member } from '@/interfaces'
import { useMembersStore } from '@/stores/members'

const router = useRouter()
const membersStore = useMembersStore()   // 1.

// リアクティブなMemberオブジェクトを用意
const member: Member = reactive({
  id: 0,
  name: '',
  email: '',
  points: 0,
  note: '',
})

// [登録]ボタンクリックで呼び出されるメソッド
const onAdd = (): void => {
  console.log(member)
  membersStore.addMember(member)       // 2.
  router.push({ name: 'MemberList' })
}
</script>

<template>
  <h1>会員管理</h1>
  <nav id="breadcrumbs">
    <ul>
      <li>
        <RouterLink v-bind:to="{ name: 'AppTop' }"> AppTop </RouterLink>
      </li>
      <li>
        <RouterLink v-bind:to="{ name: 'MemberList' }"> 会員リスト </RouterLink>
      </li>
      <li>会員情報追加</li>
    </ul>
  </nav>
  <section>
    <h2>会員情報追加</h2>
    <p>情報を入力し登録ボタンをクリックしてください。</p>

    <!-- 8. required属性による入力チェックやinput要素のnumberタイプやemailタイプによる -->
    <!--    入力チェックを有効にするためボタンのタイプをsubmitにしている。            -->
    <!--    イベント処理はfrom  v-on:submit で行っているのでbuttonのsubmitを      -->
    <!--    キャンセルするために .prevent修飾子を利用している。                       -->
    <form v-on:submit.prevent="onAdd">
      <!-- 9. 各プロパティと入力コントロールはそれぞれのタグにあるように v-model で同期されている -->
      <dl>
        <dt><label for="addId">ID </label></dt>
        <dd><input type="number" id="addId" v-model.number="member.id" required /></dd>
        <dt><lavel for="addName">名前 </lavel></dt>
        <dd><input type="text" id="addName" v-model="member.name" required /></dd>
        <dt><lavel for="addEmail">メールアドレス </lavel></dt>
        <dd><input type="email" id="addEmail" v-model="member.email" required /></dd>
        <dt><lavel for="addPoints">保有ポイント </lavel></dt>
        <dd><input type="number" id="addPoints" v-model="member.points" required /></dd>
        <dt><lavel for="addNote">備考 </lavel></dt>
        <dd><textarea id="addNote" v-model="member.note" required></textarea></dd>
      </dl>
      <button type="submit">登録</button>
    </form>
  </section>
</template>
  1. ストアファイルからエクスポートされたストア生成関数を実行し、ストオブジェクトを用意
  2. アクション関数 addMember() に引数を渡して呼び出し、会員情報のMapに追加
複数ストアの利用

Pinia では管理するデータセットを元に、ストア名とファイル名、さらにはエクスポート名を決定する。
データセットが違う場合は、これらのファイルを複数用意することになる。
例)
pinia-fundamental プロジェクトでは会員情報を管理するデータセットとして members を用意した。
これに加えて注文情報を管理するデータセットを扱う場合、orders といった名前にして stores フォルダに orders.ts ファイルを作成すれば問題なく複数ストアを利用できる。

9.3 アクションの応用

9.3.1 外部連携

実際のアプリケーションでは内部でデータが完結することはほぼなく、Web API やブラウザストレージなど外部のデータ提供サービスと連携するのが通常である。
あらかじめ外部サービスからデータを取得したうえで表示するというような処理をストア内に記述する場合にもアクションが活躍する。

ローカルストレージとセッションストレージ

ブラウザにはデータを格納できる仕組みとしてローカルストレージ(Local Storege)とセッションストレージ(Session Storage)があり、どちらもキーを指定して文字列を格納できるようになっている。

違い
ブラウザのウィンドウ(タブ)を閉じた時にデータが消去されるかどうかで、セッションストレジは消去され、ローカルストレージは残る。

利用パータン
ローカルストレージは、何かのキーデータを格納しておき、そのキーデータを元にサーバーサイドのデータと紐づけるという使い方で、この場合重要なデータはサーバ側にあるのでローカルストレージからの漏洩の心配は減る。
セッションストレージは、同じキーデータでもそのキーデータがログインを表す値でありブラウザを閉じた際は自動でログアウトさせる場合などで利用する。

格納できるデータはキーに紐づいた文字列だけで、より複雑なデータをブラウザ内で管理するには IndexedDBという仕組みを利用する。

9.3.2 App.vue

初期化リストデータを用意する必要がないので、script タグは不要

App.vue
<template>
  <!-- 4. ヘッダ -->
  <header>
    <h1>Vue Routerサンプル</h1>
  </header>

  <!-- 5. ルーティング表示領域を囲むタグ <main> -->
  <main>
    <!-- 6. ルーティング表示領域 (使用前にvue-routerからRouterViewをインポートしておく)-->
    <RouterView />
  </main>
</template>

<style>
/* 7. main タグに枠線を表示する設定。 */
main {
  border: blue 1px solid;
  padding: 10px;
}

/* 8. 以下はパンくずリストを表示するための定型スタイル */
#breadcrumbs ul li {
  display: inline;
  list-style-type: none;
}
#breadcrumbs {
  margin-left: 0px;
}
#breadcrumbs ul {
  padding-left: 0px;
}
#breadcrumbs ul .current {
  color: red;
}
#breadcrumbs ul li:before {
  content: ' > ';
}
#breadcrumbs ul li:first-child:before {
  content: none;
}
</style>

9.3.3 セッションストレージを操作する sessionStorege オブジェクト

sessionStoregeのメソッド

メソッド内容
setItem(“キー文字列”, “値文字列”)データをキー名で格納
getItem(“キー文字列”)キーに該当するデータを取得
removeItem(“キー文字列”)キーに該当するデータを削除

members.ts

members.ts
import {ref, computed} from "vue";
import {defineStore} from "pinia";
import type {Member} from  "@/interfaces.ts";

export const useMembersStore = defineStore(  
  "members",
  () => {
    // state
    const memberList = ref<Map<number, Member>>(new Map<number, Member>());

    // getters
    const getById = computed(() => {
      return (id: number): Member => {
      // stateの値にアクセスするため memberList.value を使用
      const member = memberList.value.get(id) as Member;
      return member;
      };
    });
    
    const isMemberListEmpty = computed(() => {  // 2.
      return memberList.value.size == 0;
    });      
    
    // actions
    function prepareMemberList(): void {  // 1.

      // 空のMapを用意
      let memberListMap = new Map<number, Member>();

      // セッションストレージからデータを取得
      const memberListJSONStr 
        = sessionStorage.getItem("memberList")  // 1-1.

      // データが存在する場合(nullチェック)
      if (memberListJSONStr !== null) {
        try {
          // JSON文字列を「2次元配列」としてパースする
          const memberListArray 
            = JSON.parse(memberListJSONStr) as [number, Member][];// 1-2.

          // 2次元配列を元にMapを生成
          memberListMap 
            = new Map<number, Member>(memberListArray);// 1-3.
        } catch (error) {
          console.error("JSIONのパースに失敗しました:", error);
        }
      }

      // ステート(ref)に値を格納
      memberList.value = memberListMap;  // 1-4.
    }
    
    function insertMember(member: Member): void { // 3.
      // ステート(ref)のmemberListに会員情報を追加
      memberList.value.set(member.id, member);
    
     // Mapオブジェクトを2次元配列に展開してJSON文字列に変換
     const memberListJSONStr = JSON.stringify([...memberList.value]);
    
     // セッションストレージに格納
     sessionStorage.setItem("memberList", memberListJSONStr);
   }

    return {
      memberList,
      getById,
      prepareMemberList,
      insertMember,
    }
  }
)
  1. actionsにストレージからデータを取得してステートを更新するアクション関数 prepareMemberList()を用意
    1. getItem()メソッドを使い memberList というキーで会員情報リストデータを取得
    2. JSIONオブジェクトに変換
    3. JSIONオブジェクトを引数としてMapクラスをnew
    4. セッションストレージから取得したデータを、ステートである memberListに格納
  2. gettersに会員情報リストデータが空なら Trueを返すisEmptyListを追加
  3. actionにオブジェクトをセッションストレージに格納する insertMember を追加
    • […memberList.value]
      この処理によってMapオブジェクトが配列の入れ子のオブジェクトへと変換される。

9.3.4 MemberList.vue

App.vueで準備していたデータをセッションストレージから取得するようにしたので、MemberListコンポーネントを表示するときにデータを準備する必要がある。

MemberList.vue
<script setup lang="ts">
import { RouterLink } from 'vue-router'
import { useMembersStore } from '@/stores/members';

const membersStore = useMembersStore()
membersStore.prepareMemberList()    // 1.

</script>

<template>
  <h1>会員管理</h1>
  <nav id="breadcrumbs">
    <ul>
      <li>
        <RouterLink v-bind:to="{ name: 'AppTop' }"> TOP </RouterLink>
      </li>
      <li>会員リスト</li>
    </ul>
  </nav>
  <section>
    <h2>会員リスト</h2>
    <p>新規登録は<RouterLink v-bind:to="{ name: 'MemberAdd' }">こちら</RouterLink>から</p>
    <section>
      <ul>
        <!-- 2. -->
        <li v-if="membersStore.isMemberListEmpty">会員情報は存在しません。</li>
        
        <li v-for="[id, member] in membersStore.memberList" v-bind:key="id">
          <RouterLink v-bind:to="{ name: 'MemberDetail', params: { id: id } }">
            IDが{{ id }}{{ member.name }}さん
          </RouterLink>
        </li>
      </ul>
    </section>
  </section>
</template>
  1. セッションストレージからデータを取得
  2. 会員情報データが空の場合「会員情報は存在しません。」を表示

9.3.5 MemberAdd.vue

MemberAdd.vue
<script setup lang="ts">
import { reactive } from 'vue'
import { RouterLink, useRouter } from 'vue-router'
import type { Member } from '@/interfaces'
import { useMembersStore } from '@/stores/members'

const router = useRouter()
const membersStore = useMembersStore()

// リアクティブなMemberオブジェクトを用意
const member: Member = reactive({
  id: 0,
  name: '',
  email: '',
  points: 0,
  note: '',
})

// 5. [登録]ボタンクリックで呼び出されるメソッド
const onAdd = (): void => {
  console.log(member)
  membersStore.insertMember(member)       // 1.
  router.push({ name: 'MemberList' })
}
</script>

<template>
  <h1>会員管理</h1>
  <nav id="breadcrumbs">
    <ul>
      <li>
        <RouterLink v-bind:to="{ name: 'AppTop' }"> AppTop </RouterLink>
      </li>
      <li>
        <RouterLink v-bind:to="{ name: 'MemberList' }"> 会員リスト </RouterLink>
      </li>
      <li>会員情報追加</li>
    </ul>
  </nav>
  <section>
    <h2>会員情報追加</h2>
    <p>情報を入力し登録ボタンをクリックしてください。</p>

    <!-- 8. required属性による入力チェックやinput要素のnumberタイプやemailタイプによる -->
    <!--    入力チェックを有効にするためボタンのタイプをsubmitにしている。            -->
    <!--    イベント処理はfrom  v-on:submit で行っているのでbuttonのsubmitを      -->
    <!--    キャンセルするために .prevent修飾子を利用している。                       -->
    <form v-on:submit.prevent="onAdd">
      <!-- 9. 各プロパティと入力コントロールはそれぞれのタグにあるように v-model で同期されている -->
      <dl>
        <dt><label for="addId">ID </label></dt>
        <dd><input type="number" id="addId" v-model.number="member.id" required /></dd>
        <dt><lavel for="addName">名前 </lavel></dt>
        <dd><input type="text" id="addName" v-model="member.name" required /></dd>
        <dt><lavel for="addEmail">メールアドレス </lavel></dt>
        <dd><input type="email" id="addEmail" v-model="member.email" required /></dd>
        <dt><lavel for="addPoints">保有ポイント </lavel></dt>
        <dd><input type="number" id="addPoints" v-model="member.points" required /></dd>
        <dt><lavel for="addNote">備考 </lavel></dt>
        <dd><textarea id="addNote" v-model="member.note" required></textarea></dd>
      </dl>
      <button type="submit">登録</button>
    </form>
  </section>
</template>
  1. 会員情報を登録するアクション関数 imsertMember

コメント

タイトルとURLをコピーしました