0%

【Vue.js 學習筆記】06. Vue.js 元件

前言

本節內容包含下述子章節:

  1. 元件概念介紹
  2. 使用 x-template 建立元件
  3. 使用 function return 建構資料格式
  4. props 基本觀念
  5. props 使用上的注意事項
  6. props 型別及預設值
  7. emit 向外層傳遞事件
  8. Slot 元件插槽
  9. 使用 is 動態切換元件
  10. 元件章節作業
  11. Vue 元件測驗


元件概念介紹

  1. 元件可獨立運作且可重複使用。

  2. 元件中的資料皆為獨立的。

  3. 父元件傳遞資料給子元件,使用 props 傳遞。父元件更新資料時, props 會即時將資料更新到內層元件。

  4. 子元件若要傳遞給父元件,是使用 emit ,必須透過 e 觸發事件 mit 才會能將資料傳到父元件,

  相對 props 來說它並非即時的。

  1. SPA(single page application) 也是透過元件製作。


使用 x-template 建立元件

Vue 裡面,我們可以使用元件來動態掛載 HTML ,而元件到底是什麼?

如果以 HTML 來說的話,就會類似像是 HeaderContent 這樣一個區塊一個區塊,區塊裡面當然還可以包覆區塊,

所以元件內也可以包覆元件。

接下來來個基本的 HTML 表格:

<div id="app">
  <table class="table">
    <thead>
    </thead>
    <tbody>
      <tr v-for="(item, key) in data" :key="key">
        <td>{{ item.name }}</td>
        <td>{{ item.cash }}</td>
        <td>{{ item.icash }}</td>
      </tr>
    </tbody>
  </table>
</div>

然後基本的 Vue 架構和資料:

<script>
var app = new Vue({
  el: '#app',
  data: {
    data: [
      {
        name: '小明',
        cash: 100,
        icash: 500,
      },
      {
        name: '杰倫',
        cash: 10000,
        icash: 5000,
      },
      {
        name: '漂亮阿姨',
        cash: 500,
        icash: 500,
      },
      {
        name: '老媽',
        cash: 10000,
        icash: 100,
      },
    ]
  }
});
</script>

接下來 <tbody> 下的 <tr> 是可以重複使用的,所以我們選擇這部分來改寫成元件,
Vue 裡面要新增元件時,要在外面寫

Vue.component('',{}); 裡面兩個參數,分別是自訂義的元件名稱,和元件的物件內容:

<script>
Vue.component('row-component',{
    template: '#rowComponentTemplate'
});

var app = new Vue({
 ....
});
</script>

我們在 row-component 元件內有多一個 template 的屬性,這是用來等一下新增 x-template 與其 id 綁定的名字(可自訂義),

至於 x-template 是什麼? 簡單來說就是一個封裝我們的 HTML 模板的 <script> 。所以這時候,我們再新增一個 <script>

type 屬性需改成 text/x-template 且記得給予 id ,而 id 就是剛剛 template 屬性自訂義的值,且把需要做成元件的

HTML 部分放在裡面:

<script type="text/x-template" id="rowComponentTemplate">
    <tr>
        <td>{{ item.name }}</td>
        <td>{{ item.cash }}</td>
        <td>{{ item.icash }}</td>
    </tr>
</script>

<script>
Vue.component('row-component',{
    template: '#rowComponentTemplate'
});

var app = new Vue({
 ....
});
</script>

這時候我們就可以把我們的元件用標籤的方式寫在 HTML 表格內:

<div id="app">
  <table class="table">
    <thead>
    </thead>
    <tbody>
      <row-component v-for="(item, key) in data" :key="key"></row-component>
    </tbody>
  </table>
</div>

會發現頁面上還是沒有資料顯示出來,這是因為每個元件的資料都是獨立的,所以row-component 內的 item 是空的,這時候

如果我們想要把資料傳遞給 row-component 接收,就要在剛剛新增元件的地方,新增一個 props 屬性:

<script>
Vue.component('row-component',{
    props: ['person'],
    template: '#rowComponentTemplate'
});

var app = new Vue({
 ....
});
</script>

props 屬性放的是一個陣列,這裡我們先自定義為 person ,並動態把它綁在 row-component 上,即 :person="item"

<div id="app">
  <table class="table">
    <thead>
    </thead>
    <tbody>
      <row-component v-for="(item, key) in data" :person="item" :key="key"></row-component>
    </tbody>
  </table>
</div>

並且 x-template 的地方, item 的部分也要改成 person

<script type="text/x-template" id="rowComponentTemplate">
    <tr>
        <td>{{ person.name }}</td>
        <td>{{ person.cash }}</td>
        <td>{{ person.icash }}</td>
    </tr>
</script>

會發現資料呈現上去了,可是會有跑版的情況,這是 <table> 的特性,在 <tbody> 後面都是接 <tr> 而我們卻接了我們新建的

元件,如果這時候檢查元件(F12),會發現 HTML 結構會如下:

<div id="app">
    <tr></tr>
    <tr></tr>
    <tr></tr>
    <tr></tr>
    <table>
    </table>
</div>

這跟我們原先預期的情況不一樣,因為我們要把 <tr> 掛在 <table> 內的 <tbody> 下,這時候我們可以把 HTML 的部分

改成如下:

<div id="app">
  <table class="table">
    <thead>
    </thead>
    <tbody>
      <tr is="row-component" v-for="(item, key) in data" :person="item" :key="key"></tr>
    </tbody>
  </table>
</div>

會發現新增了一個 is 屬性,值就是我們新增的元件,這時候跑版問題就會解決了。



在前面講了很長篇幅的元件,那元件可以分為「局部註冊」跟「全域註冊」,前面的方式我們都是採用全域註冊,而所謂的全域

就是指,當我在新增一個 Vue 的應用程式時,它也可以使用這個元件,那局部的意思就是指,我們只想給當前這個應用程式做

使用,如果要這樣使用,我們就要改寫如下:

<script>
var child = {
    props: ['person'],
    template: '#rowComponentTemplate'
}

var app = new Vue({
 data: {},
 components: {
     'row-component': child
 }
});
</script>

不同於前面的是,我們新增了一個 child 變數為物件,裡面放置的就是我們本來 Vue.component() 內的內容,而我們在當前的

Vue 應用程式內在 data 後面,新增了一個 components 屬性,左邊放置新增的元件名字,右邊放置我們在上層新增的 child

變數,這時後我們的元件就會變成局部的了。



使用 function return 建構資料格式

在元件的使用上,如果有使用到 data 這個屬性,一定要搭配 function return 的用法才能正常運作,這是為了確保

每個 instance 實例可以維護一份獨立的拷貝物件,避免修改一個 instance 的數值後影響到其他所有的 instance

<script>
var child = {
    data: function(){
        return {
          counter: 0
        }
    },
    props: ['person'],
    template: '#rowComponentTemplate'
}

var app = new Vue({
 data: {},
 components: {
     'row-component': child
 }
});
</script>


props 基本觀念

這邊要介紹由外到內的資料傳遞,什麼時候會使用到呢?

先前有提到每個元件的資料狀態都是獨立的,當我們外部的資料要傳遞到內部時,就必須使用到元件裡的 props 屬性。

props 為父元件向子元件傳遞資料的方式,在子元件中設定所要傳入的變數名稱,在 HTML 中使用該元件時使用該變數

名稱即可將資料傳入。


傳遞資料的方式有兩種

  • 靜態傳遞的寫法:

Vue.componentprops 定義的變數名稱 imgUrl ,放入 HTML 中,放入之後他就會將資料透過 props 傳進去。

注意:
Vueprops 的變數名稱 imgUrl 是使用小駝峰的寫法,但是如果要放入 HTML 中仍使用小駝峰是不符合規範的,必須改為 img-url

<div id="app">
  <h2>靜態傳遞</h2>
  <photo img-url="https://images.unsplash.com/photo-1522204538344-922f76ecc041?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=50e38600a12d623a878983fc5524423f&auto=format&fit=crop&w=1351&q=80"></photo>
  <h2>動態傳遞</h2>
  <photo></photo>
</div>

<script type="text/x-template" id="photo">
<div>
  <img :src="imgUrl" class="img-fluid" alt="" />
  <p>風景照</p>
</div>
</script>

<script>
Vue.component('photo', {
  props: ['imgUrl'],
  template: '#photo'
})

var app = new Vue({
  el: '#app',
  data: {
    url: 'https://images.unsplash.com/photo-1522204538344-922f76ecc041?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=50e38600a12d623a878983fc5524423f&auto=format&fit=crop&w=1351&q=80'
  }
});
</script>

  • 動態傳遞的寫法:

我們一樣使用變數名稱 img-url ,但是前面須加上冒號 : ,這個跟 v-bind 的概念是一樣的,當我們要用動態資料的時候,

就會使用這種方式,就是在屬性前方加上冒號 :img-url ,接著我們再把 new Vuedata 裡的變數 url 放在等號後面,

:img-url="url"

<div id="app">
  <h2>靜態傳遞</h2>
  <photo></photo>
  <h2>動態傳遞</h2>
  <photo :img-url="url"></photo>
</div>

<script type="text/x-template" id="photo">
<div>
  <img :src="imgUrl" class="img-fluid" alt="" />
  <p>風景照</p>
</div>
</script>

<script>
Vue.component('photo', {
  props: ['imgUrl'],
  template: '#photo'
})

var app = new Vue({
  el: '#app',
  data: {
    url: 'https://images.unsplash.com/photo-1522204538344-922f76ecc041?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=50e38600a12d623a878983fc5524423f&auto=format&fit=crop&w=1351&q=80'
  }
});
</script>


props 使用上的注意事項

  1. 單向數據流

看到下圖有一個 url ,這個是由 Vue 的應用程式由外層所帶進來的,沒有經過任何處理。

這時候如果在畫面上直接修改 urlconsole 就會直接跳錯告訴你不要直接修改 props 的內容,它會希望你在處理資料

的時候是單向,而不要反向的寫回去。

這時候我們要在 Vue.component 裡新建一個 data 來接這個外部傳進來的資料內容,一樣 data 後面是 function return

裡面我們新建一個變數 newUrl 來接外部過來的 props ,而 newUrl: this.imgUrlimgUrl 就是 props 所傳進來的。

接著把 newUrl 放入 inputv-model 裡,回到畫面重新整理後再去調整 url 就不會在跳錯了!

<div id="app">
  <h2>單向數據流</h2>
  <photo :img-url="url"></photo>
  <p>修正單向數據流所造成的錯誤</p>
</div>

<script type="text/x-template" id="photo">
<div>
  <img :src="imgUrl" class="img-fluid" alt="" />
  <input type="text" class="form-control" v-model="newUrl">
</div>
</script>

<script>
Vue.component('photo', {
  props: ['imgUrl'],
  template: '#photo',
  data: function () {
    return {
      newUrl: this.imgUrl
    }
  }
})

var app = new Vue({
  el: '#app',
  data: {
    user: {},
    url: 'https://images.unsplash.com/photo-1522204538344-922f76ecc041?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=50e38600a12d623a878983fc5524423f&auto=format&fit=crop&w=1351&q=80',
    isShow: true
  },
  created: function() {
    var vm = this;
    $.ajax({
      url: 'https://randomuser.me/api/',
      dataType: 'json',
      success: function(data) {
        vm.user = data.results[0];
      }
    });
  }
});
</script>


  1. 物件傳參考特性 及 尚未宣告的變數

可以看到 new Vue 應用程式內,主要需傳入的資料內容有 userurl ,裡面的 user 目前是空的,我們希望傳入的時候

是經過 ajax 這個非同步的行為處理完之後,才把 user 的資料傳進去,所以在沒有 user 資料的時候,畫面可能就會出錯,

來看我們該如何解決這個問題。


看到 Vue.componentcard ,它會接收外部的 userData ,並且將卡片這個元件繪製出來。

那這個 userData 是怎麼來的呢?它是透過 new Vue 應用程式,先宣告一個 user 的物件,再透過 ajax 行為,

把遠端的資料抓進來之後,才把它存進 user 裡面。

<div id="app">
  <h2 class="mt-3">物件傳參考特性 及 尚未宣告的變數</h2>
  <div class="row">
    <div class="col-sm-4">
      <card :user-data="user"></card>
    </div>
  </div>
</div>

<script type="text/x-template" id="card">
<div class="card">
  <img class="card-img-top" :src="user.picture.large" alt="Card image cap">
  <div class="card-body">
    <h5 class="card-title">{{ user.name.first }} {{ user.name.last }}</h5>
    <p class="card-text">{{ user.email }}</p>
  </div>
  <div class="card-footer">
    <input type="email" class="form-control" v-model="user.email">
  </div>
</div>
</script>

<script>
Vue.component('card', {
  props: ['userData'],
  template: '#card',
  data: function () {
    return {
      user: this.userData
    }
  }
});

var app = new Vue({
  el: '#app',
  data: {
    user: {},
    url: 'https://images.unsplash.com/photo-1522204538344-922f76ecc041?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=50e38600a12d623a878983fc5524423f&auto=format&fit=crop&w=1351&q=80',
    isShow: true 
  },
  created: function() {
    var vm = this;
    $.ajax({
      url: 'https://randomuser.me/api/',
      dataType: 'json',
      success: function(data) {
        vm.user = data.results[0];
      }
    });
  }
});
</script>

那這個 user 在傳入的時候會有時間差,所以就會跳出錯誤,因為它根本找不到裡面其中一些變數。

這時候該怎麼做呢?

我們可以在 htmlcard 標籤裡,新增 v-if

假設 user 的哪一個特性並沒有完全讀進來之前,就不要將這個卡片執行出來。我們可以打開 Vue 開發者工具,像是 ajax

的時候它就會讀進來一些資料,我們就選取一個它一定會得到的資料內容,假設它有電話號碼才能把卡片繪製出來,那 v-if

面就加入 user.phone ,這時候卡片就能正確繪製並不會跳錯了。

這邊的重點是:
如果你的資料載入有時間差,就可以使用 v-if 讓元件的產生時間往後移,可以判斷資料中的某個特性尚未讀入就不要顯示該元件之類的,避免 error / warn 產生。

<div id="app">
  <h2 class="mt-3">物件傳參考特性 及 尚未宣告的變數</h2>
  <div class="row">
    <div class="col-sm-4">
      <card :user-data="user" v-if="user.phone"></card>
    </div>
  </div>
</div>

<script type="text/x-template" id="card">
<div class="card">
  <img class="card-img-top" :src="user.picture.large" alt="Card image cap">
  <div class="card-body">
    <h5 class="card-title">{{ user.name.first }} {{ user.name.last }}</h5>
    <p class="card-text">{{ user.email }}</p>
  </div>
  <div class="card-footer">
    <input type="email" class="form-control" v-model="user.email">
  </div>
</div>
</script>

<script>
Vue.component('card', {
  props: ['userData'],
  template: '#card',
  data: function () {
    return {
      user: this.userData
    }
  }
});

var app = new Vue({
  el: '#app',
  data: {
    user: {},
    url: 'https://images.unsplash.com/photo-1522204538344-922f76ecc041?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=50e38600a12d623a878983fc5524423f&auto=format&fit=crop&w=1351&q=80',
    isShow: true 
  },
  created: function() {
    var vm = this;
    $.ajax({
      url: 'https://randomuser.me/api/',
      dataType: 'json',
      success: function(data) {
        vm.user = data.results[0];
      }
    });
  }
});
</script>

另外這邊要提到物件傳參考的特性,先打開 Vue 開發者工具,下方有一個 email ,我們可以看到 Carduseremail

這整個物件是從外層應用程式傳進來的,所以我們可以看到 Rootuser 這裡有一個一模一樣的 email

這時候我們在畫面中修改這個 email ,會發生什麼事呢?

這時候可以透過 Vue 開發者工具看到 CardRoot 裡面的 email 都被修改過去了,這個是 javascript 的特性,

物件在傳遞的時候它是傳參考,透過下面例子你就會了解了!

JavaScript 在傳遞物件時 call by reference,所以在子元件內部的物件資料變動會影響外部物件。



  1. 維持狀態與生命週期

這邊的卡片在每次啟動的時候都會執行一段 ajax ,我們在畫面的 Check me out 前方的欄位打勾跟取消,你會發現照片顯示

的不一樣,也就是說它在每次元件銷毀再生成的時候,它是重新再執行一次這段 ajax ,當然如果這是符合預期的狀態這麼做

是沒有問題的,但是有時候我們不希望每次元件在重新生成的時候,還要完全重新執行一次裡面的程式碼,這時候該怎麼做呢?



這時候就可以使用 <keep-alive> 來維持元件的生命週期,在 html 裡面新增 <keep-alive> 標籤,將 <keep-card> 元件放入

<keep-alive> 裡面,接著把 v-if="isShow" 放入 <keep-card> 標籤。好了之後回到畫面重新整理,將 Check me out 前方的

欄位打勾跟取消,你會發現照片顯示的是同一個人。

<div id="app">
  <h2 class="mt-3">維持狀態與生命週期</h2>
  <div class="form-check">
    <input type="checkbox" class="form-check-input" id="isShow" v-model="isShow">
    <label class="form-check-label" for="isShow">Check me out</label>
  </div>
  <div class="row">
    <div class="col-sm-4">
      <keep-alive>
        <keep-card v-if="isShow">
        </keep-card>
      </keep-alive>

    </div>
  </div>
</div>

<script type="text/x-template" id="card">
<div class="card">
  <img class="card-img-top" :src="user.picture.large" alt="Card image cap">
  <div class="card-body">
    <h5 class="card-title">{{ user.name.first }} {{ user.name.last }}</h5>
    <p class="card-text">{{ user.email }}</p>
  </div>
  <div class="card-footer">
    <input type="email" class="form-control" v-model="user.email">
  </div>
</div>
</script>

<script>
Vue.component('keepCard', {
  template: '#card',
  data: function() {
    return {
      user: {}
    }
  },
  created: function() {
    var vm = this;
    $.ajax({
      url: 'https://randomuser.me/api/',
      dataType: 'json',
      success: function(data) {
        vm.user = data.results[0];
      }
    });
  }
});

var app = new Vue({
  el: '#app',
  data: {
    user: {},
    url: 'https://images.unsplash.com/photo-1522204538344-922f76ecc041?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=50e38600a12d623a878983fc5524423f&auto=format&fit=crop&w=1351&q=80',
    isShow: true
  },
  created: function() {
    var vm = this;
    $.ajax({
      url: 'https://randomuser.me/api/',
      dataType: 'json',
      success: function(data) {
        vm.user = data.results[0];
      }
    });
  }
});
</script>



我們看一下 Vue 的開發者工具,將畫面中 Check me out 取消勾選時, <keep-card> 旁邊會顯示 inactive ,這只是暫時將它

隱藏。當我們下次啟動的時候,它不會重新執行整個元件的生命週期,它會重新將它呼叫出來而已,所以這就是維持生命周期的

方法。



不過在元件中有一個問題,它無法讀到特定的屬性。例如:在 template 裡面的 img:src="user.picture.large" 這張照片

無法讀取到,原因是因為這邊這段跟上方有點相同,在讀取物件的時候 ajax 還沒有執行完成。

所以我們可以在 :src 後方加上 v-if="user.picture" ,另外一個在 <h5> 標籤後方也加上 v-if="user.name",這樣就不會再

跳錯了。而這些內容都會確保在資料載入之後,才將物件的內容讀出來。

<script type="text/x-template" id="card">
<div class="card">
  <img class="card-img-top" :src="user.picture.large" v-if="user.picture" alt="Card image cap">
  <div class="card-body">
    <h5 class="card-title" v-if="user.name">{{ user.name.first }} {{ user.name.last }}</h5>
    <p class="card-text">{{ user.email }}</p>
  </div>
  <div class="card-footer">
    <input type="email" class="form-control" v-model="user.email">
  </div>
</div>
</script>


props 型別及預設值

在前面我們已經了解了元件,現在我們要來多加說明 props 這個屬性, props 是由外而內傳遞資料所使用的屬性,

有時候我們傳遞的資料也許是需要數值型態來計算的,這時候我們該如何來定義型別?

下面我們有一個 prop-type 的元件,動態綁著 props 傳遞過來的資料 cash

<div id="app">
  <h2>Props 的型別</h2>
  <prop-type :cash="cash"></prop-type>
</div>
<script type="text/x-template" id="propType">
<div>
  <input type="number" class="form-control" v-model="newCash">
  {{ typeof(cash)}}
</div>
</script>

<script>
Vue.component('prop-type', {
  props: ['cash'],
  template: '#propType',
  data: function() {
    return {
      newCash: this.cash
    }
  }
});

var app = new Vue({
  el: '#app',
  data: {
    cash: '300'
  }
});
</script>

這個時候會發現,我們的型別會是字串,那如果我們想要在傳遞時就給它定義數值型別呢?

這個時候我們就可以改寫一下 props 的部分:

<script>
Vue.component('prop-type', {
  props: {
      cash: {
          type: Number,
      }
  },
  template: '#propType',
  data: function() {
    return {
      newCash: this.cash
    }
  }
});

var app = new Vue({
  el: '#app',
  data: {
  cash: '300'
  }
});
</script>

我們把 props 改成物件型態,並把要接受過來的 cash 也寫成物件,並給予 type 屬性為 Number

這時候數值型別就定義好了,可是會發現,畫面上還是顯示字串,這個時候我們就可以檢查 Vue 應用程式內的

datacash 就會發現竟然是字串!!! 這時候把它改寫成 cash: 300 寫成數值就可以了。


如果我們在元件並沒有動態綁定要傳遞的資料,如下:

<div id="app">
  <h2>Props 的型別</h2>
  <prop-type></prop-type>
</div>

這時候我們可以在 props 內的 cash 物件,多加一個 default 屬性,並給它一個數值,這個就是當元件沒有綁定要傳遞

的資料,預設要給予多少數值:

<script>
Vue.component('prop-type', {
  props: {
      cash: {
          type: Number,
          default: 300,
      }
  },
  template: '#propType',
  data: function() {
    return {
      newCash: this.cash
    }
  }
});

var app = new Vue({
    ...
});
</script>


另外一種情況是靜態與動態傳入數值的差異性,如下:

<div id="app">
  <h2 class="mt-3">靜態與動態傳入數值差異</h2>
  <prop-type cash="300"></prop-type>
</div>

如果我們是採用靜態的方式直接傳入數值,會發現型態仍然是字串,但如果我們把它改成動態的:

<div id="app">
  <h2 class="mt-3">靜態與動態傳入數值差異</h2>
  <prop-type :cash="300"></prop-type>
</div>

即在 cash 加上 : ,也就是 (v-bind) 縮寫,

就會發現會自動轉型成數值傳遞了,這是比較值得注意的一點



emit 向外層傳遞事件

前面講到 props ,它是由外而內傳遞資料的一種屬性

不同於 propsemit 是一種事件,而且特性是由內至外

有一種情境是,當我們在 Vuemethods 寫好了一個 function ,而元件也想使用時,這時候我們難道還要在元件底下

重新寫一次,而不能直接取用外面的 methodsfunction 嗎 ?


所以 emit 就是這樣的功用,

首先我們先把基本的情境架構寫出來:

<div id="app">
  <h2>透過 emit 向外傳遞資訊</h2>
  我透過元件儲值了 {{ cash }}<button>按我</button>
  <button-counter></button-counter>
  <hr>
  <button-counter></button-counter>
</div>
<script>
Vue.component('buttonCounter', {
  template: `<div>
    <button @click="incrementCounter" class="btn btn-outline-primary">增加 {{ counter }}</button>
    <input type="number" class="form-control mt-2" v-model="counter">
  </div>`,
  data: function() {
    return {
      counter: 1
    }
  }
});

var app = new Vue({
  el: '#app',
  data: {
    cash: 300
  }
});
</script>

第一件事情是點擊「按我」的按鈕會觸發一個事件,然後讓 data 內的 cash +1
這時候我們在 Vue 的應用程式底下

新增一個 methods 屬性,並新增一個讓 cash +1function ,也就是 incrementTotal


<script>
Vue.component()

var app = new Vue({
    el: '#app',
    data:{
        cash: 300
    },
    methods: {
        incrementTotal: function(){
            this.cash ++;
        }
    }
});
</script>

然後在「按我」的按鈕新增一個 click 事件,觸發 function ,即 @click="incrementTotal"

<div id="app">
  <h2>透過 emit 向外傳遞資訊</h2>
  我透過元件儲值了 {{ cash }}<button @click="incrementTotal">按我</button>
  <button-counter></button-counter>
  <hr>
  <button-counter></button-counter>
</div>

這時候 button-counter 元件也想使用 incrementTotal 這個 function 的話,該如何使用 emit 來寫呢?

首先先在 button-counter 元件新增 v-on:自訂義的名字="incrementTotal"

<div id="app">
  <h2>透過 emit 向外傳遞資訊</h2>
  我透過元件儲值了 {{ cash }}<button @click="incrementTotal">按我</button>
  <button-counter v-on:increment="incrementTotal"></button-counter>
  <hr>
  <button-counter></button-counter>
</div>

再來,在 button-counter 元件的程式碼內新增一個 methods 屬性,並再自訂義一個 function 使用 emit

<script>
Vue.component('buttonCounter', {
  template: `<div>
    <button @click="incrementCounter" class="btn btn-outline-primary">增加 {{ counter }}</button>
    <input type="number" class="form-control mt-2" v-model="counter">
  </div>`,
  data: function() {
    return {
      counter: 1
    }
  },
  methods: {
      incrementCounter: function(){
          this.$emit('increment');
      }
  }
});

var app = new Vue({
  el: '#app',
  data: {
    cash: 300
  },
  methods: {
      incrementTotal: function(){
          this.cash ++;
      }
  }
});
</script>

到這裡就會發現,我們點擊 button-counter 元件產生的 button 時,也能讓 cash +1 了。

注意: emit 事件的命名需為小寫



另外, emit 其實可以傳遞參數,我們在 this.$emit('increment'); 內的 'increment' 後方加上 this.counter ,這個 counter 其實是 input 裡面的數值。但是為了避免傳出去的不是數值,我們應該要寫這樣 Number(this.counter)

<script>
Vue.component('buttonCounter', {
  template: `<div>
    <button @click="incrementCounter" class="btn btn-outline-primary">增加 {{ counter }}</button>
    <input type="number" class="form-control mt-2" v-model="counter">
  </div>`,
  data: function() {
    return {
      counter: 1
    }
  },
  methods: {
      incrementCounter: function(){
          this.$emit('increment', Number(this.counter));
      }
  }
});

var app = new Vue({
  el: '#app',
  data: {
    cash: 300
  },
  methods: {
      incrementTotal: function(){
          this.cash ++;
      }
  }
});
</script>

那麼傳入的數值要從外層的 incrementTotal 方法做接收,所以在 function 內要增加一個參數 newNumber ,而 function 內程式碼要改成 this.cash = this.cash + newNumber;

<script>
Vue.component('buttonCounter', {
  template: `<div>
    <button @click="incrementCounter" class="btn btn-outline-primary">增加 {{ counter }}</button>
    <input type="number" class="form-control mt-2" v-model="counter">
  </div>`,
  data: function() {
    return {
      counter: 1
    }
  },
  methods: {
      incrementCounter: function(){
          this.$emit('increment', Number(this.counter));
      }
  }
});

var app = new Vue({
  el: '#app',
  data: {
    cash: 300
  },
  methods: {
      incrementTotal: function (newNumber) {
          this.cash = this.cash + newNumber;
      }
  }
});
</script>



Slot 元件插槽

前面幾篇講到了元件,並介紹元件之間由外而內資料傳遞的屬性 props ,還有由內而外傳遞的事件 emit

元件也不只可以重複的讓我們來做使用,如果今天一個元件我們只想要替換部分內容也是可行的,這時候就要來介紹 slot 了,

也就是替換部分內容。

首先先照前面的方式建立一個元件:

<div id="app">
  <h2>沒有插槽可替換的狀態</h2>
  <no-slot-component></no-slot-component>
</div>

<script type="text/x-template" id="noSlotComponent">
<div class="alert alert-warning">
  <h6>我是一個元件</h6>
  <p>
    這沒有插槽。
  </p>
</div>
</script>

<script>
Vue.component('no-slot-component', {
  template: '#noSlotComponent',
});

var app = new Vue({
  el: '#app',
  data: {}
});
</script>

這時候如果在 no-slot-component 元件內,加上一些內容:

<div id="app">
  <h2>沒有插槽可替換的狀態</h2>
  <no-slot-component>
      <p>這是一段置換的內容</p>
  </no-slot-component>
</div>

會發現內容根本沒有被呈現出來



如果今天換成另外一種情況:

<div id="app">
  <h2>Slot 基礎範例</h2>
  <single-slot-component>
    <p>使用這段取代原本的 Slot。</p>
  </single-slot-component>
</div>

<script type="text/x-template" id="singleSlotComponent">
<div class="alert alert-warning">
  <h6>我是一個元件</h6>
  <div>
    如果沒有內容,則會顯示此段落。
  </div>
</div>
</script>

<script>
Vue.component('single-slot-component', {
  template: '#singleSlotComponent',
})

var app = new Vue({
  el: '#app',
  data: {}
});
</script>

我們在 single-slot-component 元件內新增了一個 <p>,然後在 x-template 的地方新增一個 <slot> 標籤:

<script type="text/x-template" id="singleSlotComponent">
<div class="alert alert-warning">
  <h6>我是一個元件</h6>
  <div>
    如果沒有內容,則會顯示此段落。
  </div>
  <slot></slot>
</div>
</script>

會發現內容就被呈現出來了

有趣的是當我們替換成下面的樣子時:

<script type="text/x-template" id="singleSlotComponent">
<div class="alert alert-warning">
  <h6>我是一個元件</h6>
  <slot>
    如果沒有內容,則會顯示此段落。
  </slot>
</div>
</script>

如果 single-slot-component 元件沒有任何內容時,就會顯示 <slot> 標籤內的內容,如果有的話,就是呈現插入的內容



如果我們想要多個部分內容置換時:

<div id="app">
  <h2>具名插槽</h2>
  <named-slot-component>
    <header>替換的 Header</header>
    <template>替換的 Footer</template>
    <template>按鈕內容</template>
  </named-slot-component>
</div>

<script type="text/x-template" id="namedSlotComponent">
<div class="card my-3">
  <div class="card-header">
    <div>這段是預設的文字</div>
  </div>
  <div class="card-body">
    <slot>
      <h5 class="card-title">Special title treatment</h5>
      <p class="card-text">With supporting text below as a natural lead-in to additional content.</p>
    </slot>
    <a href="#" class="btn btn-primary">
      <div>spanGo somewhere<div>
    </a>
  </div>
  <div class="card-footer">
    <div>這是預設的 Footer</div>
  </div>
</div>
</script>

<script>
Vue.component('named-slot-component', {
  template: '#namedSlotComponent',
});

var app = new Vue({
  el: '#app',
  data: {}
});
</script>

我們已經把要置換的內容放在 named-slot-component 元件裡面了,這時候我們只要在 x-template 的地方,把要置換的部分對應起來,這時候除了用 slot 標籤之外,還要賦予 name 這個屬性,值可以自定義,如下:

<script type="text/x-template" id="namedSlotComponent">
<div class="card my-3">
  <div class="card-header">
    <slot name="header">這段是預設的文字</slot>
  </div>
  <div class="card-body">
    <slot>
      <h5 class="card-title">Special title treatment</h5>
      <p class="card-text">With supporting text below as a natural lead-in to additional content.</p>
    </slot>
    <a href="#" class="btn btn-primary">
      <div>spanGo somewhere<div>
    </a>
  </div>
  <div class="card-footer">
    <div>這是預設的 Footer</div>
  </div>
</div>
</script>

然後在 named-slot-component 元件置換內容的部分,新增 slot 屬性,值就是我們剛剛自定義 name 屬性的值

<div id="app">
  <h2>具名插槽</h2>
  <named-slot-component>
    <header slot="header">替換的 Header</header>
    <template>替換的 Footer</template>
    <template>按鈕內容</template>
  </named-slot-component>
</div>

這時候內容就會替換上去了。

其他地方也是一樣的。

但是有發現我元件內的置換內容有用到 <header><template> 標籤,它們有甚麼差異嗎?

我們打開開發者工具,看到 <div class="card-header"> 下方的 <slot> 標籤並不會顯示出來,而 <header> 標籤是從外層替換

過來的,所以外層是用什麼標籤內層就會替換成什麼標籤。



但有些時候我們不希望標籤被輸出的時候可以怎麼做呢?

我們就可以使用 <template> 標籤,可以看到這裡的 <a> 標籤,裡面對應的是 <slot> ,而 <slot> 本身是不會被輸出的。

<template> 一樣的道理,它不會被輸出,有些時候我們在使用 HTML ,我們不喜歡產生額外的標籤,就可以使用

<template>



使用 is 動態切換元件

這邊要來介紹用 is 來動態切換組件,先來看一下程式碼:

下方有兩個組件一個是 id="primaryComponent" ,另一個是 id="dangerComponent"
兩者只差在顏色的不同而已。

可以看到 Vue 初始化內容的 current ,等一下我們會將 'primary-component' 透過 is 切換成 'dangerComponent'

<script type="text/x-template" id="primaryComponent">
<div class="card text-white bg-primary mb-3" style="max-width: 18rem;">
  <div class="card-header">{{ data.header }}</div>
  <div class="card-body">
    <h5 class="card-title">{{ data.title }}</h5>
    <p class="card-text">{{ data.text }}</p>
  </div>
</div>
</script>

<script type="text/x-template" id="dangerComponent">
<div class="card text-white bg-danger mb-3" style="max-width: 18rem;">
  <div class="card-header">{{ data.header }}</div>
  <div class="card-body">
    <h5 class="card-title">{{ data.title }}</h5>
    <p class="card-text">{{ data.text }}</p>
  </div>
</div>
</script>

<script>
Vue.component('primary-component', {
  props: ['data'],
  template: '#primaryComponent',
});
Vue.component('danger-component', {
  props: ['data'],
  template: '#dangerComponent',
});

// Vue 初始化的內容
var app = new Vue({
  el: '#app',
  data: {
    item: {
      header: '這裡是 header',
      title: '這裡是 title',
      text: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim perferendis illo reprehenderit ex natus earum explicabo modi voluptas cupiditate aperiam, quasi quisquam mollitia velit ut odio vitae atque incidunt minus?'
    },
    current: 'primary-component'
  }
});
</script>

我們先來看一下這邊現在是使用 <primary-component :data="item"></primary-component> ,所以畫面上看到的會是

primary-component 的原件。

<div id="app">
  <h2>使用 is 顯示單一組件</h2>
  <primary-component :data="item"></primary-component>
</div>

那我們可以用另外一種寫法,我們使用 <div> 標籤,裡面寫入 is="primary-component" ,再將 :data="item" 資料內容

也放入 <div> 標籤。

<div id="app">
  <h2>使用 is 顯示單一組件</h2>
  <div is="primary-component" :data="item"></div>
  <!-- <primary-component :data="item"></primary-component> -->
</div>

可以看到兩個元件顯示的一模一樣。

但是它有其他額外的使用方式,可以看到 <ul> 裡面包著兩個 <li> 的頁籤內容,我們會透過 @click 來切換 current 的值,

current 的值一個是 primary-component ,另一個是 danger-component

<div id="app">
  <h2 class="mt-3">使用 is 動態切換組件</h2>
  <ul class="nav nav-pills">
    <li class="nav-item">
      <a class="nav-link" :class="{'active': current == 'primary-component'}" href="#" @click.prevent="current = 'primary-component'">藍綠色元件</a>
    </li>
    <li class="nav-item">
      <a class="nav-link" :class="{'active': current == 'danger-component'}" href="#" @click.prevent="current = 'danger-component'">紅色元件</a>
    </li>
  </ul>
  <div class="mt-3">
    <primary-component :data="item" v-if="current === 'primary-component'"></primary-component>
    <danger-component :data="item" v-if="current === 'danger-component'"></danger-component>
  </div>
</div>

我們打開 Vue 開發者工具,可以看到當我們切換的時候它就會切換兩者。


這邊目前是使用 v-if 做切換,不過因為數量不多,所以程式碼看起來不會很亂。有些時候我們在切換頁籤內容,

它的數量可能會很多,這時候我們就可以使用 <div> 標籤,使用 is 來切換元件的內容,不過因為我們是動態切換,

所以 is 前面要加上冒號 : ,那我們要切換的是 current 這個變數,所以我們在動態切換 current 的時候它也會動態載入,

接下來再把 :data="item" 放進 <div> 標籤,這樣就完成了。

<div id="app">
  <h2 class="mt-3">使用 is 動態切換組件</h2>
  <ul class="nav nav-pills">
    <li class="nav-item">
      <a class="nav-link" :class="{'active': current == 'primary-component'}" href="#" @click.prevent="current = 'primary-component'">藍綠色元件</a>
    </li>
    <li class="nav-item">
      <a class="nav-link" :class="{'active': current == 'danger-component'}" href="#" @click.prevent="current = 'danger-component'">紅色元件</a>
    </li>
  </ul>
  <div class="mt-3">
    <div :is="current" :data="item"></div>
    <!-- <primary-component :data="item" v-if="current === 'primary-component'"></primary-component>
    <danger-component :data="item" v-if="current === 'danger-component'"></danger-component> -->
  </div>
</div>


元件章節作業

CodePen



Vue 元件測驗

第一題

Vue 的元件中,外部資料要傳入到內層,要使用何種方式?

  1. props
  2. emit
  3. on
點我看答案

答案: 1  

第二題

Vue 的元件中,外部資料要傳入到內層的字串、數值,內層可以再次修改它?

  1. 對,它都傳進來了,怎麼做都可以。
  2. 不對,這樣會有錯誤提示。
點我看答案

答案: 2  

第三題

Vue 的元件中,資料建構需額外使用 function return 的方式?

data: function(){
return {}
}
  1. 對。
  2. 錯,沒有那個必要。
點我看答案

答案: 1  

第四題

Vue 的元件,如果外層有使用 keep-alive 包起來,在 created 生命週期加上 Ajax 則?

  1. 每次切換元件時,資料都會重新載入。
  2. 只有第一次會載入資料,重新切換則會維持就有狀態。
  3. 元件將無法被 v-if 切換。
點我看答案

答案: 2  

第五題

內層的元件,如何傳遞數值到外層?

  1. 內層元件使用 emit 向外做事件傳遞。
  2. 外層使用 props 就能雙向綁定。
  3. 沒有辦法,元件資料是單向數據流。
點我看答案

答案: 1