# Vue Components ###### tags: `Vue` `components` `slots` `$emit` `select` `input` `button` `v-model` ### 命名 有兩種方式 1. kebab-case ```javascript= Vue.component('my-component-name', { /* ... */ }) ``` 2. PascalCase ```javascript= Vue.component('MyComponentName', { /* ... */ }) ``` 使用PascalCase命名的話,在其他地方使用此component,兩種命名都可以指定成功,也就是說命名為MyComponentName,則my-component-name和MyComponentName都可以使用。但若要在DOM中直接操作,只能使用kebab-case命名的components。 --- ### [Components Registration](https://vuejs.org/v2/guide/components-registration.html#Module-Systems) 有兩種方法可以在模組化後的Vue專案中註冊使用Components #### Local Registration 本地註冊,若此components使用次數不頻繁,則建立在components file後,需要時再import即可。 ```javascript= import ComponentA from './ComponentA' import ComponentC from './ComponentC' export default { components: { ComponentA, ComponentC }, // ... } ``` #### Automatic Global Registraton 全域註冊,需要在整個專案頻繁使用的components,在每個地方都要import太麻煩了,因此直接寫一個自動化註冊流程,並且對特定命名的components都視為Global Registration,例如BaseButton, BaseIcon, BaseInput...etc. 這段code需要放在app的入口js檔(e.g. src/main.js) ```javascript= import Vue from 'vue' import upperFirst from 'lodash/upperFirst' import camelCase from 'lodash/camelCase' const requireComponent = require.context( // The relative path of the components folder './components', // Whether or not to look in subfolders false, // The regular expression used to match base component filenames /Base[A-Z]\w+\.(vue|js)$/ ) requireComponent.keys().forEach(fileName => { // Get component config const componentConfig = requireComponent(fileName) // Get PascalCase name of component const componentName = upperFirst( camelCase( // Gets the file name regardless of folder depth fileName .split('/') .pop() .replace(/\.\w+$/, '') ) ) // Register component globally Vue.component( componentName, // Look for the component options on `.default`, which will // exist if the component was exported with `export default`, // otherwise fall back to module's root. componentConfig.default || componentConfig ) }) ``` <br> 分段拆解這串code在幹嘛: ```javascript= import upperFirst from 'lodash/upperFirst' import camelCase from 'lodash/camelCase' ``` 首先使用[Lodash](https://lodash.com/)來幫我們處理components命名大小寫及camel case,記得要先npm install lodash。 <br> ```javascript= const requireComponent = require.context( // The relative path of the components folder './components', // Whether or not to look in subfolders false, // The regular expression used to match base component filenames /Base[A-Z]\w+\.(vue|js)$/ ) ``` [require.context](https://webpack.js.org/guides/dependency-management/#requirecontext)是webpack的一個function,可以require 所有符合filename的檔案進去compiled bundle,不論這個檔案有沒有使用到。 傳入三個參數 1. 目標folder 2. 要不要再往下層找folder 3. 用正規表達式去找符合base components命名的filename <br> ```javascript= requireComponent.keys().forEach(fileName => { const componentConfig = requireComponent(fileName) ``` requireComponent是一個object,透過`.key()`可以拿到一個有file路徑的array,這裡的fileName會像是: **./BaseIcon.vue** <br> ```javascript= const componentName = upperFirst( camelCase( fileName.replace(/^\.\/(.*)\.\w+$/, '$1') // removes what's before and after the filename itself ) ) ``` 拿掉路徑及檔案名,把fileNamr轉換成components name <br> ```javascript= Vue.component( componentName, componentConfig.default || componentConfig ) }) ``` 全域註冊每個components,告訴Vue去找components.option是否為default,如果componens是用`export default`的話就會存在,沒有的話就會是`module.exports =` --- ### $emit 假設要點擊component的按鈕刪除父元件某一個item 不能在component直接更改data 在父元件做好事件 ```javascript= @delete-item=deleteItem . . . <script> . . . methods: { deleteItem(id) { // delete item } } </script> ``` 子元件(component)中使用$emit通知父元件使用mathods ```javascript= methods: { delete(id) { this.$emit('delete-item', id) } } ``` $emit中可以帶入參數,向上發送給父元件去使用 --- ### [Slots](https://vuejs.org/v2/guide/components-slots.html) 使用時機:當你想在components中傳入template,這時props就不敷使用了 ```htmlmixed= <template> <div> <button><slot></slot></button> </div> </template> ``` 假設建立一個base-button component <br> `<BaseButton>submit</BaseButton>` `<BaseButton>add</BaseButton>` `<BaseButton>remove</BaseButton>` 這時slot可以放入submit, add, remove...etc,Vue會自動抽換掉。 <br> ```htmlmixed= <template> <div> <button><slot>Submit</slot></button> </div> </template> ``` 可以預設slot內容為Submit <br> ` <BaseButton>Purchase for ${{ total }}</BaseButton>` slot也可以讓我們取得parent component的data #### Named Slot 使用時機:當有複數的template需要傳入component時 **MediaBox.vue** ```htmlmixed= <template> <div> <UserAvatar/> <slot></slot> <slot></slot> </div> </template> ``` 當使用這個component時會有問題 ```htmlmixed= <MediaBox> <h2>Adam Jahr</h2> <p>My words.</p> </MediaBox> ``` mediabox不知道要把h2和p對應到哪個slot,只要命名slot就解決了 ```htmlmixed= <template> <div> <slot name="heading"></slot> <slot name="paragraph"></slot> </div> </template> ``` 當只有兩個slot時,也可以只命名一個slot,剩下的會自動被帶入 ```htmlmixed= <template> <div> <slot name="heading"></slot> <slot></slot> </div> </template> ``` 現在可以這樣使用MediaBox ```htmlmixed= <MediaBox> <h2 slot="heading">Adam Jahr</h2> <p>My words.</p> </MediaBox> ``` 當然使用slot就是為了傳入比較複雜的template ```htmlmixed= <MediaBox> <h2>Adam Jahr</h2> <template slot="paragraph"> <p>My words.</p> <BaseIcon name="book"> </template> </MediaBox> ``` template換成div也是可以,不過在HTML解析後,devtool可以發現不同處,若使用template tag,p 和BaseIcon不會wrap在一個div中,但使用div的話就會 --- ### 使用v-model 想要在component和parent component之間動態綁定資料。 e.g. 有一個input componenet,需要傳input value到parent component,不可能在input componenet建立一個data,因此: ```javascript= <template> <div> <label v-if="label">{{ label }}</label> <input :value="value" <!-- binds to our prop --> @input="updateValue" <!-- listens for input event and triggers method --> type="text" placeholder="Add an event title" /> </div> </template> <script> export default { props: { value: [String, Number] label: String }, methods: { updateValue(event) { // <-- method triggered by input event this.$emit('input', event.target.value) } } } </script> ``` @input="updateValue"被觸發後,會傳送event給parent component,但parent component收到value後,我希望他能做到兩件事:1. 動態綁定value,讓value不只可以往上傳,也能當作props傳到input component 2. 將收到的value直接傳入parent component的data中 從[文件可知道有一個syntax sugar可以使用](https://vuejs.org/v2/guide/components.html#Using-v-model-on-Components),在parent component寫入v-model="event.title"就可以同時完成這兩件事。 ` <BaseInput label="Title" v-model="event.title" />` v-model="event.title" === ` :value="event.title" @input="(value) => { event.title = value }"` ### Inheriting Attributes 把input的屬性都放到component裡 `<input type="text" placeholder="Add an event title"/>` 變成 ```htmlmixed= <BaseInput v-model="event.title" label="Title" type="text" placeholder="Add an event title" /> ``` 但是在Vue,預設BaseInput的root div會繼承到這些屬性,我們想要的是root div下的input去繼承這些屬性。 在BaseInput 這個component去關掉這個option就好了 ```javascript= <script> export default { inheritAttrs: false, ... } </script> ``` 然後在input中設定綁定屬性 ```htmlmixed= <input :id="label" :value="value" v-bind="$attrs" <!-- specifies this element will inherit attributes --> @input="updateValue" /> ``` 這樣一來,input就會繼承BaseInput裡設定的attributes了。 --- ### mutiselect https://vue-multiselect.js.org/ 支援複數select的套件 ![](https://i.imgur.com/sEVE1Jp.png) --- ### reusable button component `v-on="$listeners"` 讓button可以知道繼承的event ```htmlmixed= <template> <div> <button v-on="$listeners"> <!-- inheriting event listeners --> <slot/> </button> </div> </template> ``` 之後就可以這樣重複使用button `<BaseButton @click="sendMessage">Message</BaseButton>` 若要客製button的style,會建立一個buttonClass的props `<BaseButton type="submit" buttonClass="-fill-gradient">Submit</BaseButton>` 不能用inheriting attirbutes,的方式繼承class(Vue2不行,Vue3可能可以),但還是需要設定inheriting attirbutes,因為仍需要像是disabled之類的attribute ```htmlmixed= <template> <div> <button v-on="$listeners" v-bind="$attrs" class="button" :class="buttonClass"> <slot/> </button> </div> </template> ``` ---