# Vue - 「 Props in , emit out 」 - 父與子 Component 之間如何傳遞資料
元件內的 data 使用 function 的寫法,是為了獨立每個元件的資料,使元件們的資料不會互相影響 ~~,所以使用了**閉包**的特性~~,我們必須使用「 Props in , emit out 」的方式達到 Component 之間的資料傳遞
![](https://i.imgur.com/BfAk9N8.png)
<h4 style="text-align: center;">簡單來說</h4>
<h4 style="text-align: center;">Props in - 父層將資料傳遞給子層</h4>
<h4 style="text-align: center;">Emit out - 子層將資料傳遞給父層</h4>
<br>
## Props in - 父層將資料傳遞給子層
### Props
![](https://i.imgur.com/NDKRIbh.png)
- 用來儲存父層傳遞 value 的 key
- `props`的值可以是**陣列**或是**物件**
- 陣列
- 底下的資料用**字串**存入
- `props: ['props1', 'props2', 'props3', ...]`
- 物件
- 底下的資料用 **key-value** 存入,值為 Object 且有以下幾種屬性可以設定:
- 屬性 `type` :
具**資料型態的驗證**功能,若 props in 的資料不符則會跳出警告
```jsx=
props: {
// 型態類別無需用引號包成字串,首字要大寫
'prpos-string':{type:String},
'props-number':{type:Number},
'props-boolean':{type:Boolean},
'props-array':{type:Array},
'props-object':{type:Object},
'props-date':{type:Date},
'props-function':{type:Function},
'props-symbol':{type:Symbol},
// 型態的驗證可以不只一種
'props-string-or-number':[String, Number],
}
```
- 屬性 `default` :
當父層沒有 props in 給子層該筆資料時,可以預先**設定預設值**
```jsx=
props: {
// 就算父層沒有 props in,還是有預設值可以顯示
'prpos-has-default':{default:'defaultValue'},
}
```
- 屬性 `required`
設定為**必要 props in 的資料**,其值為 **Boolean**
若父層沒有 props in 該筆資料,則會跳出警告
```jsx=
props: {
'prpos-required':{required:true},
}
```
- 屬性 `validator`
**自訂驗證規則**,若不符合驗證規則,則跳出警告
```jsx=
props: {
'prpos-validator':{validator: value => value > 10},
}
```
:::warning
元件初始化時,**`props` 的初始順序會優先於** `data` 、 `computed` ...屬性,所以 `default` 或 `validator` 無法取得實體內的資料 ( this. )
:::
![](https://i.imgur.com/dIilBg6.png)
==parentComponent.vue==
```jsx=
<template>
// 綁定父層要 props in 的資料
<ChildComponent :childProp='parentData'></ChildComponent>
</template>
<script>
import ChildComponent from '相對路徑/ChildComponent';
export default {
data(){
return {
parentData:'父層要傳過去的資料'
}
},
components:{
ChildComponent
}
}
</script>
```
==ChildComponent.vue==
```jsx=
<template>
<div>{{childProp}}</div>
</template>
<script>
export default {
// 設定子層接收父層 props in 的承載
props: ['childProp']
}
</script>
```
:::warning
:warning: 注意: **HTML 不分大小寫**
以 HTML 作為模板的時,**駝峰式(Camel Case)** 寫法可以換成 **連字號 (kebab-case)**
ex. childProp ---> child-prop
:::
[CodeSandBox - Props in 範例](https://codesandbox.io/s/props-in-o9eqb?file=/src/components/SubComponent.vue)
### 若父層在 props in 時沒有使用 v-bind:,則會傳入**純文字字串**
![](https://i.imgur.com/7K4ZONT.png)
==parentComponent.vue==
```jsx=
<div>{{parentData}}</div>
<ChildComponent childProp='parentData'></ChildComponent>
```
```jsx=
data(){
return {
parentData:"Parent's message!"
}
}
```
==ChildComponent.vue==
```jsx=
<template>
// 得到的 childProp 會是字串 'parentData' ,而不是 "Parent's message!"
<div>{{childProp}}</div>
</template>
<script>
export default {
// 設定子層接收父層 props in 的承載
props: ['childProp']
}
</script>
```
### 用 Props in 的方式,動態設定子層的 Class
可以透過 props in 的方式,視父層的使用情況,給予複用的子層們不同的 Class
![Props in Class](https://i.imgur.com/bEcG4E8.png)
==parentComponent.vue==
```jsx=
<BackgroundColor :parentClass="propsInClass.pink" />
<BackgroundColor :parentClass="propsInClass.blue" />
```
```jsx=
data(){
return{
propsInClass:{
pink:'pink',
blue:'blue'
}
}
}
```
```css=
.pink {
background-color: DeepPink;
}
.blue {
background-color: DeepSkyBlue;
}
```
==ChildComponent.vue==
```jsx=
<div :class="parentClass" class="other-class"></div>
```
```jsx=
export default {
props: ["parentClass"],
};
```
[CodeSandBox - Props in Class 範例](https://codesandbox.io/s/props-in-class-3s0ts?file=/src/components/BackgroundColor.vue:93-138)
### 用 Object 來 props in
- 直接傳入物件
==parentComponent.vue==
```jsx=
<ChildComponent :childJimmy="jimmy"></ChildComponent>
```
```jsx=
data() {
return {
jimmy: {
name: "Jimmy",
id: 55688,
gender: "male",
age: 26,
like: ["surfing", "diving", "climbing"],
},
};
},
```
==ChildComponent.vue==
```jsx=
<p>{{childJimmy.name}}</p>
<p>{{childJimmy.id}}</p>
<p>{{childJimmy.gender}}</p>
<p>{{childJimmy.age}}</p>
<p>{{childJimmy.like}}</p>
```
```jsx=
export default {
props: ["childJimmy"],
};
```
- 也可以**解構**,傳入各自所需的資料
==parentComponent.vue==
```jsx=
<ChildComponent
:name="jimmy.name"
:id="jimmy.id"
:gender="jimmy.gender"
:age="bindOther"
:like="bindOther"
></ChildComponent>
```
```jsx=
data() {
return {
jimmy: {
name: "Jimmy",
id: 55688,
gender: "male",
age: 26,
like: ["surfing", "diving", "climbing"],
},
bindOther: "bindOther"
};
},
```
==ChildComponent.vue==
```jsx=
<p>{{name}}</p>
<p>{{id}}</p>
<p>{{gender}}</p>
<p>{{age}}</p>
<p>{{like}}</p>
```
```jsx=
export default {
props: ["name", "id", "gender", "age", "like"]
};
```
[CodeSandBox - Props in Object 解構範例](https://codesandbox.io/s/props-in-object-kfyb6?file=/src/App.vue:1154-1185)
### ~~用 Object 來 props in - with v-for --待續.........~~
<!-- - 當 **Props in 的資料為 Object** 時,需要特別注意 JavaScript 的 Object 是 pass by reference,若直接 props in 的話,會造成子層可以直接修改父層的資料,這違反了 vue 的"單向資料流",並且可能會造成資料的污染,應該先將物件屬性**解構成原始型別 (Primitive)** 後再將資料傳遞出去
父層
```jsx=
<template>
<ChildComponent
:name="jimmy.name"
:id="jimmy.id"
:gender="jimmy.gender"
:age="jimmy.age"
:like="jimmy.like"
></ChildComponent>
</template>
<script>
import ChildComponent from '相對路徑/ChildComponent';
export default {
data(){
return {
jimmy: {
name: "Jimmy",
id: 55688,
gender: "male",
age: 26,
like: ["surfing", "diving", "climbing"]
},
}
},
components:{
ChildComponent
}
}
</script>
```
子層 ( ChildComponent.vue )
```jsx=
<template>
<p>name:{{ name }}</p>
<p>id:{{ id }}</p>
<p>gender:{{ gender }}</p>
<p>age:{{ age }}</p>
<p>like:<span v-for="item in like" :key="item">{{ item }}</span>
</p>
</template>
<script>
export default {
props: ["name", "id", "gender", "age", "like"],
}
</script>
```
[CodeSandBox - Props in Object 範例](https://codesandbox.io/s/props-in-object-tzl55)
- 使用 `v-for` Props in Object 時,也可以使用指令 `v-bind` 自動解構
:::warning
~~若在子層需要對 props in 的資料做 v-model 雙向綁定的話,需要再 data 複製一份 props in 的資料,或是使用 computed(這樣父層更新資料子層才會跟這更新)~~
::: -->
### 在 props in 的資料使用 v-model
- **子層最好不要直接在 props in 的資料使用 v-model** ,這樣有可能會 改變/汙染 到父層的資料
- 若真的**需要在子層 props in 的資料使用 v-model**,則可以在子層**用 data 做 props in 的副本**,副本將不會隨著父層改變而更新,可以讓子層獨立 管理/處理 props in 的副本
==ChildComponent.vue==
```jsx=
export default {
props: ["parentPropsIn"],
data(){
return{
// 資料只有在初始化時更新
childData:this.parentPropsIn
}
}
};
```
- :no_entry: 上述的情況**千萬不要用 computed 做資料的副本**,雖然執行結果的"畫面"跟原本使用 `v-bind:` 一樣,資料會隨著父層改變而更新,且子層資料改變時,不會 改變/汙染 到父層的資料,但當你使用 `v-model` 的特性,在子層修改資料並做雙向綁定的同時,你可能會遇到下列兩種情況:
- 當 computed 沒有 setter 並且 getter 依賴 props in 的資料時,你會收警告 [Vue warn]: Write operation failed: computed property "....." is readonly.
![](https://i.imgur.com/PTwTjwM.png)
![](https://i.imgur.com/QX4gh8w.png)
- 在 getter 依賴 props in 的資料,並且有設定 setter 並且依賴 data 的資料時,當修改 v-model 的 computed,由於 data 的改變觸發生命週期的updated,getter 又從新 return props in 的值
![computer setter getter and updated](https://2.bp.blogspot.com/-dewaJmzwLYg/WRHB89i_URI/AAAAAAAAyxY/L0clpu50eYY3aHiklwT4r1PD_OTQfh3UQCLcB/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2017-05-09%2B%25E4%25B8%258B%25E5%258D%25889.19.23.png)
> [那些關於 Vue 的小細節 - Computed 中 getter 和 setter 觸發的時間點](https://pjchender.blogspot.com/2017/05/vue-computed-getter-setter.html)
==ChildComponent.vue==
```jsx=
export default {
props: ["parentPropsIn"],
computed:{
childData(){
// 資料跟著父層資料更新而更新
return this.parentPropsIn;
}
}
};
```
- getter 與 setter 都依賴 data 時,效果與在 data 做 props in 副本一樣,但是根本多此一舉
:::warning
若子層需要在改變資料時,將資料傳回父層,可以使用 Emit out / Event out ,也就是 `v-on:` 搭配 `$emit` 來觸發事件並將資料傳給父層
:::
---------------------------------------
## Emit out / Event out - 子層將資料傳遞給父層
![](https://i.imgur.com/8BCahFu.png)
### `$emit(自定義事件,[參數1, 參數2, ...])`
- 用來**觸發當前實例上的事件**,附加參數都會傳給監聽器 callback
- 參數
- 自定義事件:欲觸發的事件名稱,用**連字號 (kebab-case)** 較不易出錯
- [參數1, 參數2, ...]:觸發事件時帶入 event handler 的參數
### `$event` 是 vue 用來監聽事件的特殊變數
- 使用 `$event.target.value` 便可以取得事件當下 DOM 節點的 value
### `v-on:監聽 DOM 事件 = "$emit( 自定義事件, $event.target.value )"`
- 當子層的 `v-on:監聽 DOM 事件` 觸發時,會在父層觸發 `自定義事件` 並且帶入子層觸發事件當下 DOM 節點的 value - `$event.target.value` 作為 父層 event handler 的參數
![](https://i.imgur.com/ZgBRCN8.png)
==ChildComponent.vue==
```jsx=
<input type="text" v-on:input="$emit('emit-trigger', $event.target.value)" />
```
==parentComponent.vue==
```jsx=
<ChildComponent v-on:emit-trigger="parentGet"></ChildComponent>
<p>Data from ChildComponent : {{parentData}}</p>
```
```jsx=
data() {
return {
parentData: "",
};
},
methods: {
parentGet(eventTargetValue) {
this.parentData = eventTargetValue;
},
},
```
[CodeSandBox - Emit out 範例](https://codesandbox.io/s/emit-out-event-out-rcp8l)
:::warning
**父層** `ChildComponent` 標籤中,自定義事件 (`emit-trigger`) 的 **Event Handler** (`parentGet`) **不需要**像其他 Event Handler 一樣加**小括號 ( )**
:::
### 子層也可以用 event handler 的方式處理資料
==ChildComponent.vue==
```jsx=
<input type="text" v-on:input="sandToParent($event)" />
```
```jsx=
methods: {
sandToParent(event) {
this.$emit("emit-trigger", event.target.value);
},
},
```
:::warning
子層 `v-on:` 的 **Event Handler** `sandToParent($event)`甚至**可以不用小刮號 ($event)** 傳入 $event,因為 Event Listener 在註冊 Event Handler 的時候,會把**事件物件(Event Object)** 預設為 **Event Handler 的第一個參數**
```jsx=
<input type="text" v-on:input="sandToParent" />
```
```jsx=
methods: {
sandToParent(event) {
this.$emit("emit-trigger", event.target.value);
},
},
```
:::
==parentComponent.vue==
```jsx=
<ChildComponent v-on:emit-trigger="parentGet"></ChildComponent>
<p>Data from ChildComponent : {{parentData}}</p>
```
```jsx=
data() {
return {
parentData: "",
};
},
methods: {
parentGet(eventTargetValue) {
this.parentData = eventTargetValue;
},
},
```
[CodeSandBox - Emit out - Event Handler 範例](https://codesandbox.io/s/emit-out-event-out-event-handler-12w0x?file=/src/components/ChildComponent.vue)
<br><br><br><br><br>
---------------------------------
### **`v-model`** 其實是結合 `v-bind:` 與 `v-on:` 語法糖
以 input 為例:
```jsx=
<input v-model='something' />
// 等價於
<input v-bind:value='something' v-on:input='value=$event.target.value' />
```
<br><br><br><br><br>
參考如下:
> https://ithelp.ithome.com.tw/articles/10199044
> https://ithelp.ithome.com.tw/articles/10223518
> https://book.vue.tw/CH2/2-2-communications.html
> [隱藏在 Handler 中的event](https://ithelp.ithome.com.tw/articles/10192015#:~:text=%E9%9F%B3%E6%A8%82%E8%AB%8B%E4%B8%8B-,%E9%9A%B1%E8%97%8F%E5%9C%A8%20Handler%20%E4%B8%AD%E7%9A%84%20%22event%22,-%E7%95%B6%E7%9B%A3%E8%81%BD%E7%9A%84)
###### tags: `Vue`
<style>
h2{
border:none !important;
}
</style>