# 2. Vue - Component
### 11/30
- 宣告元件的 HTML 是用 script 標籤包住 (不一定要這樣宣告,但會比較好),並且內部只能有一個元素 (Vue 2 的規定),假如有多個就要在外面加一個容器。可以把元件想成 iframe。
- 不能在 script 標籤內用 src 外部引用,即一定要在同個檔案使用。無法獨立一個檔案,一定要跟網頁主體寫在一起,好處是 HTML 比較好看。
```javascript
<script type="text/x-template" id="counter">
<div>
<input type="button" :value="count" @click="count++">
<input type="checkbox" ... >
</div>
</script>
```
- 覺得太長可以在元件宣告的 template 寫 HTML,一樣只能有一個元素。好處是可以把 JS 的部分獨立出一個檔案,網頁內容也不會太亂。
```javascript
// Vue.component('元件名稱', { ... data, methods, ... })
Vue.component('counter', {
template: '#counter', // 這邊也可以寫 HTML
data () {
return {
count: 0
}
}
})
...
```
- 多個元件標籤的 data 是各自算的。
- 元件名稱做為標籤名稱,注意取名不要跟 HTML 標籤重複,怕重複的話也可以第一個字大寫。
### 傳入資料 props
- 從外層"傳入資料",使用 props,此傳送是單向的,即不會改變到外層的值。
```javascript
<div id="app">
<card title="標題" text="123456789"></card>
<card v-for="card in cards" :title="card.title" :text="card.text"></card>
</div>
...
<script type="text/x-template" id="card">
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ title }}</h5>
<p class="card-text">{{ text }}</p>
</div>
</div>
</script>
<scritp>
Vue.component('card', {
template: '#card',
// 外層傳入的資料
props: {
// 資料名、預設值
title: {
type: String,
default: ''
},
text: {
type: String,
default: ''
}
}
})
new Vue({
el: '#app',
data() {
return {
cards: [
{ title: 'AAA', text: 'aaaaaa'},
{ title: 'BBB', text: 'bbbbbb'}
]
}
}
})
...
</script>
```
- 使用標籤屬性將資料傳入元件;標籤沒有冒號":"就是寫死,有冒號就是綁定,即是把 data 和傳入的資料綁在一起。
### 傳出資料 $emit、語法糖 .sync
- 傳出資料 emit,若直接在子元件寫 methods 只會改到這個子元件的 data (複本),若要改到外層(Root)傳入的資料,需要在子元件 methods 寫一個 this.$emit('事件名', value)傳出來,外層再去接這個事件和值,所以再寫一個事件到外層去改值,就可以內外同步了。
```javascript
// HTML
text-size(:size='textSize' @change-size='handleChangeSize')
text-size(:size='textSize' @change-size='handleChangeSize')
script(type='text/x-template' id='text-size')
div
p(:style="{ fontSize: size + 'px'}") 我會被改變字體大小
input(type='button' value='+' @click='changeSize(2)')
input(type='button' value='+' @click='changeSize(-2)')
// Vue
Vue.component('text-size', {
template: '#text-size',
props: {
size: {
type: Number,
default: '16'
}
},
methods: {
changeSize (value) {
// 子元件觸發一個叫 change-size 的事件 (不可駝峰式),並把 value 傳出去到 Root
this.$emit('change-size', value)
}
}
})
new Vue({
el: '#app',
data () {
textSize: 20
},
methods: {
handleChangeSize (value) {
this.textSize += value
}
}
})
```
- 如何內層改外層(進階:內層不同步),第一種方法是自訂事件,第二種為.sync(Vue語法糖),就不用多寫一個 v-on 事件,也不用在 Root 多寫一個 methods。
- **須注意,.sync 的語法的 `'update:size'`,size 和 冒號之間不能空格!!不能寫成 `update: size`,否則會無效哦!**
```javascript
// HTML
text-size-sync(:size.sync='textSize2')
text-size-sync(:size.sync='textSize2')
script(type='text/x-template' id='text-size-sync')
div
p(:style="{ fontSize: syncSize + 'px'}") 我會被改變字體大小
input(type='button' value='+' @click='syncSize += 2')
input(type='button' value='+' @click='syncSize -= 2')
// Vue
Vue.component('text-size-sync', {
template: '#text-size-sync',
props: {
size: {
type: Number,
default: '16'
}
},
computed: {
syncSize: {
// 產生 computed 值的 function
get () {
return this.size
},
// 當修改 computed 值時執行的 function
set (value) {
// 若傳入的資料有 .sync,觸發的事件名稱固定是 update:props名
this.$emit('update:size', value)
}
}
}
})
new Vue({
el: '#app',
data () {
textSize2: 20
}
})
```
- 大多時候是使用別人的元件,較少自己寫但還是會寫到。當有一部分的 HTML 需要被重複使用,就可以使用元件。
### 子元件互傳 eventBus
- 外傳內、內傳外也可使用,隔好幾層也可以傳。
- 傳出用 $emit,傳入用 $on
- 比較少用到。
### 插槽 slot
- 元件某部分的 HTML 可以 (由外層) 自訂義。若標籤內沒有放東西就是預設,有放東西就會只針對 slot 標籤內做更動。
- 若使用多個,slot 標籤要各別給 name,並各別使用 template 標籤包住,template 標籤要有 v-slot:name的值 去對應,簡寫為 #
- slot 可以使用外部的資料,若要使用元件內的資料要另外寫:
```javascript
temp
// template 對應到名為 link 的 slot,且將 slot 提供的資料命名為 data
template(#link='data')
p {{ data.link }}
// template 對應到名為 link 的 slot,且將 slot 提供的資料解構
template(#title='{ text, title }')
p {{ title }}
p {{ text }}
script(type='text/x-template' id='temp')
div
// 在 slot 放 v-bind 可以讓外部的 template 使用元件內的資料
slot(name='title' :text='text' :title='title')
h1 標題
p 內文
slot(name='link' :link='link')
a(:href='link') 連結
Vue.component('temp', {
template: '#temp',
data () {
return {
text: 'aaaaaaaa',
title: 'bbbbbbbb',
link: 'cccccccc'
}
}
})
```
### 練習筆記
#### 計算總讚數
- v-bind 內可以寫三元運算子
- 使用 filter 跑陣列內的讚數
```javascript
...
computed: {
totalLike () {
return this.cards.filter(card => {
return card.like
}).length
}
}
```
#### 電子鐘
- 用 ai 製作 svg,補充個 svg 用的很牛的 [範例](https://www.nepolabo.fans/)
- ['1','4'].includes(text) 回傳 true 或 false,應用在綁定 class ; 使用 isNaN(parseInt(text)) 判斷布林值應用在綁定 class
- v-for 可以跑數字,從 1 開始跑,v-for="n in 數字",也能跑文字
- 因為 setInterval 有個問題是,第一次執行會在設定時間之後,所以從原本寫在 created 的部分分出來寫在 methods,並設定執行時間 & 再次呼叫這個函式,即可解決問題
```javascript
...
methods: {
update () {
const date = new Date()
this.h = date.getHours().toString.padStart(2, '0')
this.m = date.getMinutes().toString.padStart(2, '0')
this.s = date.getSeconds().toString.padStart(2, '0')
}
},
// 這邊用 mounted 也可以,但老師覺得用 created 延遲較短,所以能用就用
created () {
this.timer = setInterval(this.update, 1000)
this.update()
},
// 養成清除習慣
destroyed () {
clearInterval(this.timer)
}
```
#### 清單
- 篩選功能要給一個 data 假設叫 filter: '全部',並且再一個 data 做 id 的累加。
```javascript
// 用 pug 檔/語法,script 要加 . 才能寫 js
script.
new Vue({
el: '#app',
data () {
return {
newitem: '',
items: [],
filter: '全部',
id: 1
}
},
...
})
```
- 篩選已完成未完成,且後續要有標記、刪除動作,要給 id 到陣列內。
- 編輯狀態要另外給一個 model: this.newitem 到陣列內。
```javascript
...
methods: {
add () {
if (this.newitem.length >= 2) {
this.items.push({
name: this.newitem,
done: false,
edit: false,
model: this.newitem,
id: this.id
})
this.id++
this.newitem =''
}
},
},
...
```
- 若綁定 :style="{ border }",值和 key 一樣的話可省略冒號與重複的 key。
- filter 過濾功能 (陣列搜尋必用)
- .filter() 可以過濾陣列後產生新的陣列,**true 代表保留,false 代表刪除。**
```javascript
computed: {
filteredItems () {
return this.items.filter(item => {
if (this.filter === '全部') return true
else if (this.filter === '已完成') return item.done
else return !item.done
})
}
}
```
- 跑 v-for 時要綁定 :key 值
```javascript
...
p 顯示 {{ filter }},共 {{ filteredItems.length }} 個
ul
li(v-for"item in filteredItems" :key="item.id")
input(type="checkbox" v-model="item.done")
del(v-if="item.done") {{ item.name }}
span(v-else) {{ item.name }}
...
```
- 全部刪除
```javascript
...
methods: {
del (filter) {
this.items = this.items.filter(item => {
if (filter === '全部') return false
else if (filter === '已完成') return !item.done
else return item.done
})
}
}
```
- map 陣列重組
```javascript
input(type="button" value="全部標記已完成" @click="done(true)")
input(type="button" value="全部標記未完成" @click="done(false)")
...
methods: {
done (value) {
this.items = this.items.map(item => {
item.done = value
return item
})
}
}
```
- 雙擊編輯欄位、儲存編輯、取消編輯、個別刪除功能
```javascript
...
p 顯示 {{ filter }},共 {{ filteredItems.length }} 個
ul
li(v-for"item in filteredItems" :key="item.id")
input(type="checkbox" v-model="item.done")
input(v-if="item.edit" type="text" v-model="item.model" @keydown.enter="save(item)" @keydown.esc="cancel(item)")
del(v-else-if="item.done" @dblclick="item.edit = true") {{ item.name }}
span(v-else @dblclick="item.edit = true") {{ item.name }}
input(type="button" value="x" @click="delitem(item.id)")
...
methods: {
save (item) {
item.name = item.model
item.edit = false
},
cancel (item) {
item.model = item.name
item.edit = false
},
delitem (id) {
const idx = this.items.findIndex(item => {
return item.id === id
})
this.items.splice(idx, 1)
}
}
```
- localStorage
```javascript
watch: {
items: {
deep: true,
handler () {
localStorage.setItem('newlist', JSON.stringify(this.items))
}
}
},
mounted: {
this.items = JSON.parse(localStorage.getItem('newlist')) || []
if (this.items.length > 0) {
this.id = this.items[this.items.length - 1].id + 1
}
}
```
###### tags: `Vue`
### 問題探討(2022/11/03)
- (2022/11/03) 將 Fn 回傳的值放進子元件,使用 :check_alert 時不可駝峰式 :checkAlert,且引號內的 Fn 一定要加上括號 (),因為一般傳送事件可以不用加括號像是這樣 @check-alert="checkAlert",但此種情況若使用傳送事件的方式 @check-alert="checkAlert()",子元件用 $emit 去接事件會變成 undefined
- 
- (2022/11/04) 但如果要帶參數進去的 Fn 怎麼辦? 這樣無法用參數回傳值
- 解法:直接把Fn寫到子組件的methods事件XD
- Vue子元件使用父元件傳來的Fn,若此子元件的Fn是會接收父元件Fn所return的值,會發生子元件Fn傳參數出去後得到的return值,無法回到子元件當中使用的問題。除了將Fn直接寫在子元件,有無其他傳的方法?
- 解法:直接把Fn寫到子組件的methods事件XD