---
# System prepended metadata

title: 2. Vue - Component
tags: [Vue]

---

# 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
