# 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的套件

---
### 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>
```
---