Vueのprovideとinjectでデータの受け渡しを行う

n.yuumi
n.yuumi

はじめに

今回は、provideとinjectを使ったデータの受け渡しについてブログを書きます。
理解を深めるために、下記の機能を作成しながら説明したいと思います。
ボタンで一覧と入力フォームの表示を切り替えられるようになっており、入力フォームで追加したデータを一覧に追加 & Xボタンで一覧から削除する仕様です。

一覧

データを全て表示し、右上のXボタンで削除

0001.png入力フォーム

フォームにデータ入力、Addボタンで一覧に追加

0002.png

 

一覧

フォームで追加した情報を一覧に追加

0003.png

 

データ追加機能作成

作成するファイル構成は以下のようになります。

App.vue(ベースとなるファイル)
|ーTheFruits.vue
(一覧ボタンと入力フォームボタン、一覧コンポーネントと入力フォームコンポーネント)
  |ーAddFruits.vue(入力フォーム)
  |ーStored Fruits.vue(一覧)
    |ーFruitItem.vue(一覧に表示する各データ)
  |ーBaseCard.vue(一覧、入力フォーム共通で使う枠のデザイン)

①インストール
Vue CLIでプロジェクトを作成します。

②アプリケーションインスタンス作成

//main.js
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.mount('#app')

③一覧とフォームのコンポーネント作成

最初に表示切り替え用のボタンと一覧、フォームのコンポーネントを作成します。
この状態ではまだ切り替えができていません。componentタグを使ってStore FruitsボタンでStoredFruitsコンポーネントを表示、Add FruitsボタンでAddFruitsコンポーネントを表示できるにしていきます。

Screen Shot 2022-02-09 at 16.45.43.png

//App.vue
<template>
  <the-fruits></the-fruits>
</template>

<script>
import TheFruits from './components/TheFruits.vue'
export default {
  components: {
    TheFruits
  }
}
</script>
//TheFruits.vue
<template>
  <div>
     <button>Stored Fruits</button>
     <button>Add Fruits</button>
     <stored-fruits></stored-fruits>
     <add-fruits></add-fruits>
  </div>
</template>

<script>
import AddFruits from './AddFruits.vue'
import StoredFruits from './StoredFruits.vue'
export default {
  components: { AddFruits, StoredFruits },
}
</script>
//StoredFruits.vue
<template>
  <div>Stored Fruit Page</div>
</template>
//AddFruits.vue
<template>
  <div>Add Fruits</div>
</template>

④一覧とフォームの表示切り替え

それぞれのボタン押下時にClickイベントswitchSelectedTab()が発火するようにしました。また、引数には各コンポーネント名を設定しています。
最後にcomponentタグにis属性を設定し、該当のコンポーネント名がセットされるようにすれば切り替え処理は完了です。

//The Fruits.vue
<template>
  <div>
    <div class="btns">
      <button @click="switchSelectedTab('stored-fruits')">Stored Fruits</button>
      <button @click="switchSelectedTab('add-fruits')">Add Fruits</button>
    </div>
    <keep-alive>
      <component :is="selectedTab"></component>
    </keep-alive>
  </div>
</template>

<script>
import AddFruits from './AddFruits.vue'
import StoredFruits from './StoredFruits.vue'
export default {
  components: { AddFruits, StoredFruits },
  data() {
    return {
      selectedTab: 'stored-fruits',
    }
  },
  methods: {
    switchSelectedTab(tab) {
      this.selectedTab = tab;
    },
  },
}
</script>

 

Untitled_ Feb 9, 2022 5_25 PM.gif

⑤一覧作成

TheFruits.vueが持つ一覧データを取得し、一覧に表示させます。また、Xボタンで削除する機能も追加し、データがない場合はメッセージのみ表示するようにしていきます。

Untitled_ Feb 9, 2022 5_52 PM.gif

 

まず、Fruits.vueにfruitsStorageというデータを設定し、StoredFruits.vueにデータをバインドして渡します。StoredFruits.vueでは、渡されたデータをv-forディレクティブを使ってデータ一覧を描画してください。

一覧が表示できたら、削除機能を追加します。
今回は、provideとinject機能を使い、TheFruits.vueで定義した削除機能を、孫にあたるFruitItem.vueで使えるようにしましょう。
 

//The Fruits.vue
<template>
  <div>
    <div class="btns">
      <button @click="switchSelectedTab('stored-fruits')">Stored Fruits</button>
      <button @click="switchSelectedTab('add-fruits')">Add Fruits</button>
    </div>
    <keep-alive>
      <component :is="selectedTab" :isFruitData="isFruitData" @checkFruitData="doCheckFruitData"></component>
    </keep-alive>
  </div>
</template>

<script>
import AddFruits from './AddFruits.vue'
import StoredFruits from './StoredFruits.vue'
export default {
  components: { AddFruits, StoredFruits },
  data() {
    return {
      selectedTab: 'stored-fruits',
      isFruitData: true,
      fruitsStorage: [
        {
          id: 1,
          name: 'Banana',
          color: 'yellow',
          link: 'https://ja.wikipedia.org/wiki/%E3%83%90%E3%83%8A%E3%83%8A'
        },
        {
          id: 2,
          name: 'Apple',
          color: 'Red',
          link: 'https://ja.wikipedia.org/wiki/%E3%83%AA%E3%83%B3%E3%82%B4'
        },
        {
          id: 3,
          name: 'Lemon',
          color: 'Yellow',
          link: 'https://ja.wikipedia.org/wiki/%E3%82%A6%E3%83%B3%E3%82%B7%E3%83%A5%E3%82%A6%E3%83%9F%E3%82%AB%E3%83%B3'
        },
      ]
    }
  },
  methods: {
    switchSelectedTab(tab) {
      this.selectedTab = tab;
    },
    //削除
    //Xボタンが押されたデータのidをチェックし、該当のデータを削除します
    deleteFruits(deletedId) {
      const deletedIndex = this.fruitsStorage.findIndex(fruit => fruit.id === deletedId)
      this.fruitsStorage.splice(deletedIndex,1)
    },
    //チェック
    //データが全て削除されたらfalseを設定し、メッセージを表示させます
    doCheckFruitData(ele) {
      if(ele) {
        this.isFruitData = true;
      } else {
        this.isFruitData = false;
      }
    }
  },
  //このprovideで定義し、injectで受け取ります
  provide() {
    return {
      //表示用データ
      fruitData: this.fruitsStorage,
      //削除
      deleteFruitData: this.deleteFruits,
    }
  },
}
</script>

ここでは、データの数をチェックし全て削除されたら「There's no fruit.....」と表示するようにしています。表示の切り替えはisFruitDataの真偽値で判定しており、isFruitDataは大元のTheFruitsで管理しています。0になるとemitでfalseを親に渡してisFruitData = falseに設定するようにしました。

//StoredFruits.vue
<template>
<div>
    <ul v-if="isFruitData">
      <fruit-item v-for="fruit in fruitData"
      :key="fruit.id"
      :id="fruit.id"
      :name="fruit.name"
      :color="fruit.color"
      :link="fruit.link">
      </fruit-item>
    </ul>
    <p v-else class="msg">There's no fruit....</p>
</div>
</template>
<script>
import FruitItem from './FruitItem.vue'
export default {
  components: { FruitItem },
  props: ['isFruitData'],
  //TheFruits.vueのprovideで定義したデータをinjectで受け取ります
  inject: ['fruitData'],
  methods: {
    checkFruitsCount() {
      if(this.fruitData.length < 1) {
        this.$emit('check-fruit-data', false)  
      }
    }
  },
  updated() {
    this.checkFruitsCount()
  },
}
</script>
//FruitItem.vue
<template>
    <li>
        <base-card class="stored">
            <div>
                <p>Name is <span>{{name}}</span></p>
                <p>Color is <span>{{color}}</span></p>
            </div>
            <a :href="link">More Info</a>
            <!--Clickイベントが発火するとTheFruits.vueで定義した
            deleteFruits()が処理されます-->
            <button @click="deleteFruitData(id)">X</button>
        </base-card>
    </li>
</template>

<script>
import BaseCard from './BaseCard.vue'
export default {
    components: { BaseCard },
    props: ['id','name','color','link'],
    //provideで定義された削除処理の変数を受け取ります
    inject: ['deleteFruitData'],
}
</script>
//BaseCard.vue
<template>
    <div class="base-card">
        <slot></slot>
    </div>
</template>

⑥フォーム作成

最後に、一覧にデータを追加する機能を作成していきます。

Untitled_ Feb 10, 2022 12_06 PM.gif

先ほどの削除処理と同じように、大元のTheFruits.vueに追加処理を定義していきます。
追加する場所はmethodとprovideです。

//TheFruits.vue
  methods: {
    addFruits(name, color, link) {
      const dataIndex = this.fruitsStorage.length
      const addedFruits = {
        id: dataIndex + 1,
        name: name,
        color: color,
        link: link
      }
      this.fruitsStorage.push(addedFruits)
      this.selectedTab = 'stored-fruits'
    },
  },
  provide() {
    return {
      addFruitData: this.addFruits
    }
  },

TheFruits.vueのprovideで定義したaddFruitDataをinjectで受け取っています。
ref属性を使って取得した入力値(name, color, link)をパラメータとして渡し、TheFruits.vue側でそのデータをpush()で一覧に追加する仕組みです。データ削除の時はemitを使ってisFruitDataをfalseにしましたが、今回はデータを追加するのでtrueを設定するようにしています。
また、データ追加時に入力値をチェックし、空の場合は次の処理が実行されないようにポップアップを表示するようにしています。

//AddFruits.vue
<template>
  <div>
    <base-card>
      <form id="fruitForm" name="fruitForm" @submit.prevent="submitFruitData">
        <div class="form-input">
              <label for="name">Name</label>
              <input ref="enteredName" type="text" name="name" id="name" placeholder="Enter Name">
        </div>
        <div class="form-input">
              <label for="color">Color</label>
              <input ref="enteredColor" type="text" name="color" id="color" placeholder="Enter Color">
        </div>
        <div class="form-input">
              <label for="link">Url</label>
              <input ref="enteredLink" type="url" name="link" id="link" placeholder="Enter Url">
        </div>
        <button type="submit" class="btns-add">Add</button>
      </form>
    </base-card>
    <!--入力エラーポップアップ-->
    <div class="popup" v-if="isInvalid">
      <p>Please enter name, color and link...</p>
      <button class="close" @click="closePopup">Close</button>
    </div>
  </div>
</template>

<script>
let body = document.querySelector('body');

import BaseCard from './BaseCard.vue'
export default {
  components: { BaseCard },
  data() {
    return {
      isInvalid: false,
    };
  },
  inject: ['addFruitData'],
  methods: {
    //フォーム送信
    submitFruitData() {
      const name = this.$refs.enteredName.value
      const color = this.$refs.enteredColor.value
      const link = this.$refs.enteredLink.value

      //データチェック
      if(name.trim() === '' || color.trim() === '' || link.trim() === '') {
          this.isInvalid = true
          body.classList.add('dark')
          return
      }
      //データを追加
      this.addFruitData(name, color, link)
      this.$emit('check-fruit-data', true)
      //フォームクリア
      this.removeData()
      
    },
    removeData() {
      let fruitForm = document.getElementsByName('fruitForm')[0];
      fruitForm.reset();
    },
    closePopup() {
      this.isInvalid = false
      body.classList.remove('dark')
    }
  }
}
</script>

終わりに

今回は、provideとinjectを使ったデータの受け渡しについて勉強しました。provideで設定すれば孫のコンポーネントにもサクッとデータを渡せるので便利ですね。
また機会があればVueについてブログを書きたいと思います。