Vueのprovideとinjectでデータの受け渡しを行う
はじめに
今回は、provideとinjectを使ったデータの受け渡しについてブログを書きます。
理解を深めるために、下記の機能を作成しながら説明したいと思います。
ボタンで一覧と入力フォームの表示を切り替えられるようになっており、入力フォームで追加したデータを一覧に追加 & Xボタンで一覧から削除する仕様です。
一覧
データを全て表示し、右上のXボタンで削除
入力フォーム
フォームにデータ入力、Addボタンで一覧に追加
一覧
フォームで追加した情報を一覧に追加
データ追加機能作成
作成するファイル構成は以下のようになります。
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コンポーネントを表示できるにしていきます。
//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>
⑤一覧作成
TheFruits.vueが持つ一覧データを取得し、一覧に表示させます。また、Xボタンで削除する機能も追加し、データがない場合はメッセージのみ表示するようにしていきます。
まず、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>
⑥フォーム作成
最後に、一覧にデータを追加する機能を作成していきます。
先ほどの削除処理と同じように、大元の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についてブログを書きたいと思います。