Vue 元件 === ###### tags: `vue筆記` # 元件註冊 ## 註冊元件的手法 元件註冊分成: 1. 全域註冊:在同一個 creatApp 下,任何子元件都能使用,表示元件標籤可以放在畫面中任何區域。 2. 區域註冊:只能在限制的元件下使用。 ## 全域註冊 子元件的註冊需要掛載在creatApp母元件後面,mount之前。以下面程式碼為範例,`.componment()` 就是新增一個子元件,同時要賦予該元件一個名稱 `'compName'` : ```jsx const app = Vue.createApp({ data() { return { text: '外部元件文字', }; }, }) .component('compo1', { data() { return { text: '內部文字' }; }, template: `<div class="alert alert-primary" role="alert"> {{ text }} </div>` }); app.mount('#app'); ``` 有一個跟母元件不同的地方是 template ,這是子元件的HTML樣板,直接使用樣板字面值呈現。這裡就如同原本寫在HTML檔案中一樣作法。 然後只要在母元件中加入該元件名稱標籤,template 中的內容就會顯示在畫面上。 ```html <div id="app"> <compo1></compo1> </div> ``` ![https://s3-us-west-2.amazonaws.com/secure.notion-static.com/228cea46-a4d0-4c8f-bfbd-ff1fc3247e3d/Screen_Shot_2021-05-30_at_3.49.50_PM.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/228cea46-a4d0-4c8f-bfbd-ff1fc3247e3d/Screen_Shot_2021-05-30_at_3.49.50_PM.png) *要注意一點,如果子元件重複用在畫面中不同地方,他們的資料是獨立的喔~ ```html <div id="app"> <compo1></compo1> <compo1></compo1> </div> ``` 全域註冊除了上面直接接在後面寫,還能用下面 `app.componment()` 方式,因為變數 app 本身已經被掛載了母元件了,所以可以直接取用: ```jsx const app = Vue.createApp({ data() { return { text: '外部元件文字', }; }, }) app.component('compo2', { data() { return { text: '內部文字2' }; }, template: `<div class="alert alert-primary" role="alert"> {{ text }} </div>` }) app.mount('#app'); ``` 或是 ```jsx const compo2 = { data() { return { text: '內部文字2' }; }, template: `<div class="alert alert-primary" role="alert"> {{ text }} </div>` } const app = Vue.createApp({ data() { return { text: '外部元件文字', }; }, }) app.component('compo2', compo2 ) app.mount('#app'); ``` --- ## 區域註冊 區域註冊就不用把程式碼寫在 creatApp 和 mount 中間,可以直接宣告一個變數去存取,再來於預掛載的元件中新增 `components` 屬性,並填入剛剛新增的子元件變數即可: ```jsx const compo3 = { data() { return { text: '元件三', }; }, template: `<div class="alert alert-primary" role="alert">{{ text }}</div>`, }; const app = Vue.createApp({ data() { return { text: '外部元件文字', }; }, components:{ compo3 } }) app.mount('#app'); ``` 而區域註冊的子元件,只能存活於有掛載的元件容器中,所以記得 HTML 要放對位置。 ```html <div id="app"> <compo3></compo3> </div> ``` ## 模組化管理 最後也能使用 ES module 方式 模組化管理元件,把元件拆分成不同 JS 檔案,當需要時再插入取用。先新增一個JS檔案,把元件存在 `export default` : ```jsx export default = { data() { return { text: '外部匯入的元件', }; }, template: `<div class="alert alert-primary" role="alert">{{ text }}</div>`, }; ``` 然後於需要需要呼叫的檔案中 import 該檔案到一個變數,也一樣加入到 `components` 中: ```jsx import compo4 from './compo-open.js'; const app = Vue.createApp({ data() { return { text: '外部元件文字', }; }, components:{ compo4 } }) app.mount('#app'); ``` *記得在 script 標籤要加上 `type="module"` *** 樣板有幾個建立方式 template:這就是前面[元件註冊](https://www.notion.so/87d463943755453084e9775edc8b361a),用的在物件中建立 template 屬性,當要運用時就是直接用元件名稱當作標籤名稱。 --- ## x-template x-template我覺得跟以前沒用元件時的寫法比較接近,是把 HTML 架構留在 HTML 上而不是寫在元件的 template。 先在JS檔案新增元件,再來也是新增一個template屬性,但是直接給他取名字。 ```jsx app.component('compo5', { template: '#compo-template', }); ``` 再來於HTML空白處寫上 script 標籤,並賦予 `type="text/x-template"` 的屬性意思是作為樣板使用,同時加上 id 屬性,這個 id 就是要搭配於JS檔案中建立的新元件。 ```html <script type="text/x-template" id="compo-template"> <div class="alert alert-primary" role="alert"> x-template建立的元件 </div> </script> ``` *這裡可以像是一般放modal時,整理放在最後面,就比較不會干擾到HTML閱讀。如果需要該元件時,也一樣在想要的位置中寫下該元件名稱標籤即可😋 ```html <compo5></compo5> ``` --- ## 元件運用 **直接運用:標籤名稱** 就是剛剛一直使用的直接用元件名稱當做標籤 ```html <compo5></compo5> ``` *注意HTML 標籤和屬性名稱都只是小寫,大寫會自動被壓成小寫。 ### 搭配 v-for 使用情境的話,可以把它想成有一個產品列表,卡片就是一個元件,我們需要把所有產品卡片都呈現在畫面中,就會使用 v-for: ```html <product-template v-for = " i in products"></product-template> ``` 這裡的 `products` 就是 外層元件 data 的陣列資料 --- ### 搭配 v-is 如果搭配v-is,就不限定該標籤是否是元件名稱,可以使用一般的 div...等: ```html <div v-is="'product-template'"></div> ``` *注意:上面可以看到 v-is 後面是使用雙引號,所以載入時要使用元件名稱加上單引號,為什麼呢?因為插入單引號,是作為字串,如果沒有插入單引號,就變成是變數,使用變數的話可以動態方式進行切換。 ### 動態屬性 下面範例是做動態屬性切換元件,先在一個 input 綁定一個變數 `componentName` ,這個變數是對應於母元件 data 其中一筆資料,然後再把該筆資料綁定在 v-is 上面,這樣就能透過改變變數資料來切換元件模板了~ ```html <input type="text" v-model="componentName"> <p>任何標籤均可搭配 v-is 進行動態切換</p> <div v-is="componentName"></div> ``` 像這種 v-is 麻煩的東西為啥會存在呢?除了上面切換元件外,還能適應另外的場景,像是HTML標籤的限制,`<tbody>` 下面只能接 `<tr>`,`<ul>` 下面只能接 `<li>` : ```html <tbody> <tr v-is="'product-template'"> </tr> </tbody> ``` *** # Props 前面介紹到元件,而外層元件與內層元件之間的資料是無法互相共通的,如果需要讓內層讀取到外層元件資料時,就會用到Props,當作兩者串接的橋樑。 *撰寫記憶口訣是:前內後外 ``` <kid :kid-item="fatherItem"> </kid> ``` 請看下面範例是外層元件有一個圖片網址資料,要傳入內層元件中的 template 使用,先新增 props陣列屬性,加入一個自訂義變數名稱: ```jsx const app = Vue.createApp({ data() { return { imgUrl: 'https://source.unsplash.com/random/200X200', }; }, }); app.component('photo',{ props:['url'], template:`<img :src="url" class="img-thumbnail" alt>`, }) app.mount('#app'); ``` 靜態綁定:只要在元件標籤上針對 props 建立的變數名稱賦予值即可(跟外層資料沒關係)。 ```html <photo url="https://source.unsplash.com/random/200X200" ></photo> ``` 動態綁定:接著在 HTML 中的photo元件標籤上用 v-bind 方式 ,加上 `:url="imgUrl"` 屬性,前面 `:url` 的 `url` 就是 props 的自定義變數名稱,然後 `="imgUrl"` 的 `imgUrl` 對應外層元件的資料。 ```html <photo :url="imgUrl" ></photo> ``` 如果有很多筆資料要傳進去就多寫幾個屬性: ```html <photo :url2="imgUrl2" :url3="imgUrl3" :url4="imgUrl4"></photo> ``` *注意:JS中常常以小駝峰方式撰寫,所以如果我們 props 中的值以小駝峰方式寫的話,如下: ```jsx app.component('photo',{ props:['superUrl'], template:`<img :src="url" class="img-thumbnail" alt>`, }) ``` 要記得在寫 HTML 標籤屬性時,實際上它會讀成 `superurl` ,所以建議可以加上 - 連字號並改成小寫: ```html <photo :super-url="imgUrl" ></photo> ``` ## 單向數據流 另外一點要注意 Props 是單向數據流,不能改變外部傳進來的值,所以當我們於內層元件要編輯傳進來的資料時,會提醒錯誤並不能更改,`[Vue warn]: Attempting to mutate prop "money". Props are readonly.` 。這時候可以在內層元件的 data 中新增一個新變數賦予外層傳進來的值,就可以利用新變數進行編輯摟~ ```jsx app.component('editMoney', { props: ['money'], data(){ return{ newMoney:this.money, } }, template: ` <input type="text" v-model="newMoney"></input> <div>value: {{newMoney}}</div> ` }); ``` ## 物件傳遞 因為傳遞的值是不能編輯的嘛,而物件又是傳參考,所以當傳進來的物件值,其實只是一個參考,在內元件去修改物件內的值的話,也會修改到外元件的值,為了避免混亂,建議還是複製出該物件,才去操作: ```jsx const productCard = { data(){ return{ newProduct:{...this.product}, } }, props: ['product'], template: `原本的:<input type="text" v-model="product.name"> {{product.name}}<br> 複製的:<input type="text" v-model="newProduct.name"> {{newProduct.name}}`, created(){ console.log(this.product); } }; ``` [https://codepen.io/jordan-ttc-design/pen/jOBaadj?editors=1011](https://codepen.io/jordan-ttc-design/pen/jOBaadj?editors=1011) *** ## 型別驗證 傳進來的值跟原本他在父層的型別是一樣的,同時我們也能在內層設下規定說傳進來的值是哪種型別以及是否必填,這時候就可以把原本是陣列的props改寫成物件形式: ```javascript const productCard = { props: { product:Object, invoice:[String,Number], price:{type:Number,required:true}, amount:{type:Number,default:20}, }, }; ``` - required:必填 - default:沒有帶入值時候預設多少 --- # Emits 前面提到內外層資料不共通,需要搭橋樑來傳遞,外層傳資料給內層用Props,內層要傳給外層就是用 Emits ,但是 Emits 並非是傳資料,而是觸發事件,利用內層元素操控外層,或是透過參數方式把內層資料傳給外層。 首先看到資料 num 在外層,內層是一個按鈕,通過按鈕觸發外層函式對該筆資料+1: ```jsx //外層 const app = Vue.createApp({ data() { return { num: 0, }; }, methods: { addNum() { console.log('addNum'); this.num++ } } }); //內層 app.component('button-counter', { methods: { addNum(){ this.$emit('emit-num') } }, template: `<button type="button" @click="addNum">add</button>` }); app.mount('#app'); ``` 所以先在外層設定+1的函式,內層也是在 methods 中新增一個`addNum` 函式,而 `addNum()` 裡面的 `this.$emit('emit-num')` 就是重頭戲的部分,`('emit-num')`括弧中包裹著的就是這個自定義名稱事件名稱,要連接外層元件標籤使用。 取好名字後接著於HTML `v-on:emit-num="函式名"` 就可以以內元件來觸發外元件了,所以一樣要記住寫法是,前內後外!! ```html <button-counter @emit-num="addNum"></button-counter> ``` ### 參數傳資料 如果要把內層資料傳給外層的話,就可以透過剛剛的 $emit事件挾帶出去於外層函式中使用,大致上跟前面寫法差不多,但是在 `this.$emit('emit-text',this.text)` ,自定義名稱後面多一個參數代表要傳遞的變數。 ```jsx //外層 const app = Vue.createApp({ const app = Vue.createApp({ data() { return { text: '' }; }, methods: { getData(text) { console.log('getData', text); this.text = text; } } }); //內層 app.component('button-text', { data() { return { text: '內部資料', } }, methods: { pushText(){ this.$emit('emit-text',this.text) } }, template: `<button type="button" @click="pushText">emit data</button>` }); app.mount('#app'); ``` 另外其實也不一定要寫成methods,如果只是傳個簡單的值也可以直接寫在html中: ```html <button-text @click="$emit('emit-text','helloworld')"></button-text> ``` 這裡其實有漏掉寫一個屬性是 `emits`,才不會跳出警告: ```javascript app.component('button-text', { emits:['emit-text'], }); app.mount('#app'); ``` 或是跟props一樣可以寫成物件形式另外做一些驗證,記得加上正確時的 return 就好: ```javascript app.component('button-text', { emits:{ 'emit-text':(text)=>{ if(typeof text !== string){ console.log('錯誤'); } return typeof text === string } } }); app.mount('#app'); ``` *** ## slot插槽 slot插槽,如果想要在內元件中有一塊地方可以讓外部元件操作該地方的HTML結構,就可以使用插槽,插一塊在內元件中。 先預設一段程式碼範例如下,外層有一筆字串資料,想要用在內元件中,這次不使用props,而是slot,直接 templete 內在要替換的地方加上 `slot` 標籤。 ```jsx const app = Vue.createApp({ data(){ return{ text:'外層文字' } } }); app.component('card', { template: `<div class="card" style="width: 18rem;"> <div class="card-header"> 元件 Header </div> <div class="card-body"> <slot></slot> </div> <div class="card-footer"> 元件 Footer </div> </div>` }) app.mount('#app'); ``` 接著在子元件內,撰寫 html 即可,這時候就可以使用外層元件的東西。 ```html <card> <p>這是由外層定義的{{text}}</p> </card> ``` 但即便使用插槽也建議加上預設值,預設值得意思是如果插槽有資料,就顯示插槽,沒有的話就顯示預設值,預設值可以直接加在 slot 裡面: ```jsx app.component('card', { template: `<div class="card" style="width: 18rem;"> <div class="card-header"> 元件 Header </div> <div class="card-body"> <slot> <p>此為預設值</p> </slot> </div> <div class="card-footer"> 元件 Footer </div> </div>` }) ``` ## 具名插槽 但這時候如果開放不止一個插槽時,怎辦?可以對插槽命名喔~ 先在 內元件 的 template 的 `slot` 標籤上加上 `name : 元件名稱` ```jsx const app = Vue.createApp({ data(){ return{ text:'外層文字' } } }); app.component('card2', { template: `<div class="card" style="width: 18rem;"> <div class="card-header"> <slot name="header">元件 Header</slot> </div> <div class="card-body"> <slot>這段是預設的文字</slot> </div> <div class="card-footer"> <slot name="footer">元件 Footer</slot> </div> </div>` }); app.mount('#app'); ``` 接著在HTML 也加上 `v-slot:` 的屬性,如果沒有特別設的,也寫上 `default` 。 ```html <card> <card2> <template v-slot:header>我喜歡這張卡片</template> <!-- 預設請加入 default --> <template v-slot:default>這是卡片 2 號</template> <template v-slot:footer>這是卡片腳</template> </card2> </card> ``` v-slot縮寫,可以用#: ```html <card> <card2> <template #header>我喜歡這張卡片</template> <template #default>這是卡片 2 號</template> <template #footer>這是卡片腳</template> </card2> </card> ``` *** ## Mitt跨元件溝通 前面的元件資料傳遞方法都是外傳內,不然就是內傳外,但是如果兩個沒關係的元件,怎麼溝通呢?可以使用另外的套件來搭飛機:Mitt。 1. import方式載入套件: ```jsx import 'https://unpkg.com/mitt/dist/mitt.umd.js'; ``` 2. 啟用套件: ```jsx const emitter = mitt(); ``` 3. 實際來用~下面有段程式碼,是兩個掛載在同一個元件的子元件(好像兄弟),先在要傳資料的元件中新增一個函式 `sendData()` ,函式內容跟 emit 幾乎差不多,只是使用 emitter.emit 方法,第一個參數也是動作名稱,第二個參數也是要傳的資料。 ```jsx const app = Vue.createApp({}); app.component('card-emit', { data() { return { product: { name: '蛋餅', price: 30, vegan: false }, } }, methods: { sendData() { emitter.emit('sendProduct',this.product) } }, template: `<div class="card" style="width: 18rem;"> <div class="card-body" > <button @click="sendData">送出</button> </div> </div>` }); ``` 4. 再來在接資料的元件中,生命週期 created 中寫 `emitter.on()` ,也能在別的地方寫,但老師建議在created,第一個參數就是剛剛的自定義名稱,第二個參數是一個事件,裡面的參數就是傳來的資料,可以再存起來使用。 ```jsx app.component('card-on', { data() { return { item: {} } }, created() { emitter.on('sendProduct',(data)=>{ this.item = data }) }, template: `<div class="card" style="width: 18rem;"> <div class="card-body"> {{ item }} </div> </div>` }); ```