# 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 - ![](https://i.imgur.com/OYq6JzH.png) - (2022/11/04) 但如果要帶參數進去的 Fn 怎麼辦? 這樣無法用參數回傳值 - 解法:直接把Fn寫到子組件的methods事件XD - Vue子元件使用父元件傳來的Fn,若此子元件的Fn是會接收父元件Fn所return的值,會發生子元件Fn傳參數出去後得到的return值,無法回到子元件當中使用的問題。除了將Fn直接寫在子元件,有無其他傳的方法? - 解法:直接把Fn寫到子組件的methods事件XD