iT邦幫忙

2023 iThome 鐵人賽

DAY 24
0
Vue.js

業主說給你30天學會Vue系列 第 24

V24_Vue的props_emit_子元件傳資料給父元件

  • 分享至 

  • xImage
  •  

V24_Vue的props_emit_子元件傳資料給父元件

今天來再次研究一下 子元件傳資料給父元件 的方式
有時一些Vue的語法需要不斷反覆練習
將一些
有疑慮的問題點一一釐清,
在實際專案的設計中,才能更直覺得使用

先從props 的範例看起

main.js

import { createApp } from 'vue'

import App from './App.vue'
import FoodItem from './components/FoodItem.vue'

const app = createApp(App)

app.component('food-item', FoodItem)

app.mount('#app')

這部份己經鼠熟悉了

接著是
App.vue

<template>
  <h1>Food</h1>
  <food-item food-name="Apples"/>
  <food-item food-name="Pizza"/>
  <food-item food-name="Rice"/>
</template>

<script></script>

<style>
  #app > div {
    border: dashed black 1px;
    display: inline-block;
    width: 120px;
    margin: 10px;
    padding: 10px;
    background-color: lightgreen;
  }
</style>                  

這裡看到 <food-item food-name="Apples"/> 這個 food-item 的元件tag
屬性是 food-name,數值是 "Apples"
一共有3個 <food-item>

再來是
FoodItem.vue

<template>
    <div>
        <h2>{{ foodName }}</h2>
    </div>
</template>

<script>
export default {
    props: [
        'foodName'
    ]
};
</script>

<style></style>         

可以看到有用到props

    props: [
        'foodName'
    ]

代表從父元件接收資料的變數是 foodName
然後就可以連動到 {{ foodName }}

整個串接起來就是

main.js
import FoodItem from './components/FoodItem.vue'
從FoodItem.vue檔案匯入,並命名為 FoodItem

app.component('food-item', FoodItem)
將FoodItem物件加入App變成 component,元件的tagname是 food-item

App.vue
<food-item food-name="Apples"/>

FoodItem.vue
props: [ 'foodName' ]
{{ foodName }}

元件<food-item>的屬性food-name會綁定到 FoodItem 子元件 中的 foodName

https://ithelp.ithome.com.tw/upload/images/20231009/20152098npCubIfovV.png

這裡有一個地方要注意,就是命名的規則
在JS的部份,變數名稱採用的是 camelCase 格式,就是 FoodItem
在Vue 的Component的tag 或屬性的名稱採用的是 kebab-case 格式,就是 food-item
因此 在名稱上是相對應的

FoodItem -> food-item
foodName -> food-name

整理一下 props 的語法

import FoodItem from './components/FoodItem.vue' 

app.component('food-item', FoodItem) 

<food-item food-name="Apples"/>

props: [ 'foodName' ]

{{ foodName }} 

在上一篇發文中
有看到 provide() / inject 的語法

import FoodKinds from './components/FoodKinds.vue'

app.component('food-kinds', FoodKinds)

<component :is="activeComp"></component> 

activeComp ='food-kinds' -> FoodKinds.vue

export default {
  provide() {
    return {
      foods: this.foods
    }
  }
}

export default {
    inject: ['foods']
}

<div v-for="x in foods">

//----------
<component :is="activeComp"></component> 的作用是方使切換不用的component
原本應該可以寫成

<food-kinds />
<food-about />

不過因為不同的component會有不同的接收資料項目,或是沒有接收資料
所以在 <food-kinds />,<food-about /> 沒有加上屬性
而是在需要接收資料的component上
設定 inject: ['foods'] 就可以了

這個跟

<food-item food-name="Apples"/>
props: [ 'foodName' ]

透過加入屬性food-name 及 props的方式有所不同,

因此在學習上,常常會在這種地方產生混淆,
若不釐清的話,再加上新的語法後,就會更混亂了

//----------------------------

接下來是 emit 的用法

參考W3School的範例
Vue $emit() Method
https://www.w3schools.com/vue/vue_emit.php

$emit() 的作用是如果子元件要傳送資料給父元件,利用 $emit() 觸發一個事件傳送到父元件

這部份要確認一下,用component的方法會回傳component的<template>的內容
用props接收父元件傳來的資料,
那麼 $emit() 像是在子元件有後續操作的時候,可以傳送資料給父元件,
父元件經過一些處理,將結果透過props再回給 子元件,子元件再更新<template>的內容
最後再回到父元件中的 component tag 的內容

看起來是經過了一段這樣的流程

也可能是 在子元件有後續操作的時候,透過$emit()傳送資料給父元件,然後改變父元件本身<template>的內容

來看一下範例的說明
main.js

import { createApp } from 'vue'

import App from './App.vue'
import FoodItem from './components/FoodItem.vue'

const app = createApp(App)

app.component('food-item', FoodItem)

app.mount('#app')

這部份OK, 沒問題了

接下來是
App.vue

<template>
  <h1>Food</h1>
  <p>The toggle button on the component emits an event to the parent "App.vue". The favorite status is modified in "App.vue", and the updated status is sent back to the component so that the image is toggled to reflect the favorite status.</p>
  <p>The result looks exactly like before, but now the favorite staus is modified where it should be in "App.vue" instead of in the "FoodItem.vue" component.</p>
  <div id="wrapper">
    <food-item
      v-for="x in foods"
      :key="x.name"
      :food-name="x.name"
      :food-desc="x.desc"
      :is-favorite="x.favorite"
      @toggle-favorite="receiveEmit"/>
  </div> 
</template>

<script>
export default {
  data() { 
    return {
      foods: [
        { name: 'Apples', desc: 'Apples are a type of fruit that grow on trees.', favorite: true},
        { name: 'Pizza', desc: 'Pizza has a bread base with tomato sauce, cheese, and toppings on top.', favorite: false},
        { name: 'Rice', desc: 'Rice is a type of grain that people like to eat.', favorite: false},
        { name: 'Fish', desc: 'Fish is an animal that lives in water.', favorite: true},
        { name: 'Cake', desc: 'Cake is something sweet that tates good.', favorite: false}
      ]
    };
  },
  methods: {
    receiveEmit(foodId) {
      let foundFood = this.foods.find(
        food => food.name === foodId
      );
      foundFood.favorite = !foundFood.favorite;
    }
  }
}
</script>

<style>
  #wrapper {
    display: flex;
    flex-wrap: wrap;
  }
  #wrapper > div {
    border: dashed black 1px;
    flex-basis: 120px;
    margin: 10px;
    padding: 10px;
    background-color: lightgreen;
  }
</style>     

這裡有看到
宣告了 foods: []的陣列變數
內容是各個food item的資料,有 name, desc, favorite

接著看到

<food-item
  v-for="x in foods"
  :key="x.name"
  :food-name="x.name"
  :food-desc="x.desc"
  :is-favorite="x.favorite"
  @toggle-favorite="receiveEmit"/>

這個 <food-item>會依據 v-for="x in foods" 來產生多個 <food-item> 項目
每個 <food-item>的屬性綁定為 :key,:food-name,:food-desc,:is-favorite
這代表 這些屬性除了有綁定到 x in foods 中的x,也就是 foods 的每一個元素
也會傳送到 <food-item> component本身

另外還有一個 @toggle-favorite 的事件觸發,這個事件是由子元件的 $emit()發送出來的
觸發後,會執行 receiveEmit 的函式

然後就是

receiveEmit(foodId) {
  let foundFood = this.foods.find(
    food => food.name === foodId
  );
  foundFood.favorite = !foundFood.favorite;
}

receiveEmit(foodId)中,foodId$emit() 傳過來的資料

let foundFood = this.foods.find(
  food => food.name === foodId
);

要讀取 return { foods: [] } 的資料
要用this.foods 再利用陣列的 find() 取得 food.name === foodId 的元素
然後將取得的元素 foundFood.favorite = !foundFood.favorite;
改變 favorite 的狀態 true / false

大致理解後

再來是
FoodItem.vue

<template>
    <div>
        <h2>
            {{ foodName }}
            <img src="/img_quality.svg" v-show="isFavorite">
        </h2>
        <p>{{ foodDesc }}</p>
        <button @click="toggleFavorite">Favorite</button>
    </div>
</template>

<script>
export default {
    props: ['foodName','foodDesc','isFavorite'],
    methods: {
        toggleFavorite() {
            this.$emit('toggle-favorite', this.foodName);
        }
    }
};
</script>

<style>
    img {
        height: 1.5em;
        float: right;
    }
</style>                  

先從 export default { } 看起

props: ['foodName','foodDesc','isFavorite'] 是設定從<food-item>接收的資料項目 有 'foodName','foodDesc','isFavorite' 這3項

還有一個 方法 method 是 toggleFavorite()

當按下 <button> 時會觸發 toggleFavorite
<button @click="toggleFavorite">Favorite</button>

toggleFavorite 執行的內容是

toggleFavorite() {
    this.$emit('toggle-favorite', this.foodName);
}

利用 $emit 觸發父元件的 toggle-favorite事件,同時傳送資料 this.foodName

父元件接著執行 receiveEmit(foodId) 這時 的 foodId 就是 $emit傳送的this.foodName

https://ithelp.ithome.com.tw/upload/images/20231009/20152098xUORuSyrzL.png

整理一下, 連動執行的程序

<button @click="toggleFavorite">Favorite</button> 

toggleFavorite() {
    this.$emit('toggle-favorite', this.foodName);
}

<food-item
  v-for="x in foods"
  :key="x.name"
  :food-name="x.name"
  :food-desc="x.desc"
  :is-favorite="x.favorite"
  @toggle-favorite="receiveEmit"/>

receiveEmit(foodId) {
  let foundFood = this.foods.find(
    food => food.name === foodId
  );
  foundFood.favorite = !foundFood.favorite;
}

:is-favorite="x.favorite"  -> isFavorite

props: ['foodName','foodDesc','isFavorite']

<img src="/img_quality.svg" v-show="isFavorite">

v-show 根據 isFavorite 狀態決定是否要顯示 <img src="/img_quality.svg"> 

由以上流程可看出,整個過程是互相連動的
需要很清楚 互相綁定,事件的關係
才不會規模越大的時候,越加混亂


上一篇
V23_Vue的provide_inject_父元件傳資料給子元件
下一篇
V25_Vue的slot網頁內容的分解及組合
系列文
業主說給你30天學會Vue31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言