# 1-4 Vue.js 的黑魔法: 指令

接下來的幾篇內容,會開始解說 Vue.js 模板裡的黑魔法 —— 「指令」(directive)。

為什麼稱它做黑魔法呢? 相信跟著此系列看到這裡的讀者,或多或少都發現 Vue.js 提供了一些特殊的語法, 沒錯,就是那些 v- 開頭的屬性們,像是最早出現的 v-model 、後來用來處理事件處理的 v-on 等等。

先前沒有特別解說的疑惑,從這一篇開始詳細來說明。

這些帶有 v- 開頭的屬性們,在 Vue.js 裡面就被稱為 「指令」(directive)

當與指令搭配運算式 (expression) 的值被改變時,對應的標籤 (包括節點、元件等) 也會隨之影響。 換句話說,透過指令的作用與狀態的變化,我們也就可以透過改變狀態 (資料) 來操作整個網頁系統了。

# 屬性綁定 - v-bind

我發現有不少朋友在剛開始學習 Vue.js 的時候,很可能都做過類似的事,就是覺得既然可以利用 {{ }} 將資料輸出至網頁模版上,那麼網頁標籤內的「屬性」是不是也能這樣呢:

const vm = Vue.createApp({
  data () {
    return {
      customId: 'item-id-1'
    }
  }
}).mount('#app');
1
2
3
4
5
6
7
<div id="app">
  <p id="{{ customId }}">...</p>
</div>
1
2
3

像上面程式的執行結果,標籤 <p>id 並不會如我們所預期地出現 item-id-1, 因為像 {{ }} 的模板語法並沒有辦法被套用在 HTML 標籤的「屬性」上, 如果我們需要由 Vue.js 來控制標籤的「屬性」,則可以使用 v-bind 的指令。

v-bind 指令的用法很簡單,我們只要再標籤上面加上 v-bind:屬性名稱, 以剛剛的範例來說,我們希望由 Vue 的實體來輸出 id,就可以這樣做:

<p v-bind:id="customId">...</p>
1

實際在瀏覽器渲染時會變成:

<p id="item-id-1">...</p>
1

再來個例子,如果說我們今天希望控制某個按鈕是否為 disabled 的狀態,就可以這樣做:

const vm = Vue.createApp({
  data () {
    return {
      isBtnDisabled: true
    } 
  },
}).mount('#app');
1
2
3
4
5
6
7
<button v-bind:disabled="isBtnDisabled">click me!</button>
1

如果這個時候 isBtnDisabled 的內容是 true,那麼這個按鈕就會被掛上 disabled 的屬性無法使用。 如果為 false,這個按鈕就可以正常使用。

像這樣,常見的標籤屬性如 id 、 圖片的 src,或是連結的 href 等 DOM 的屬性,都可以透過 v-bind 指令來控制它的內容。

小提醒

v-bind 指令除了完整的 v-bind:屬性名稱 寫法外,也可以直接簡寫成 :屬性名稱,如:

<img v-bind:src="imageSrcUrl">

<img :src="imageSrcUrl">
1
2
3

上面兩種寫法,在瀏覽器渲染後的結果會是一樣的。 簡單說,有 : 的是 Vue 實體 v-bind 綁定後的屬性,沒有的則是純文字屬性。

像這樣的用法我們在後續還會常常見到它們。

# 表單綁定 - v-model

在一個與高互動性的網頁中,表單類型的元素是不可或缺的。 常見的表單元素像是 <input><textarea> 以及 <select> 等,在 Vue.js 可透過 v-model 來進行資料的「雙向」綁定, v-model 會根據不同的表單類別來更新元素的內容。

小提醒

由於 <div> 標籤並非表單元素,所以即使加上 contenteditable 屬性仍無法透過 v-model 指令來處理與資料的雙向綁定。

# input 文字框

首先是最常見的 input 文字框,當我們在 input 加上 v-model="message" 之後,此時這個輸入框便會自動被綁定 input 事件:

<div id="app">
  <input v-model="message" placeholder="edit me">
  <p>Message is: {{ message }}</p>
</div>
1
2
3
4
const vm = Vue.createApp({
  data () {
   return {
      message: 'Hello'
    } 
  }
});

vm.mount('#app');
1
2
3
4
5
6
7
8
9

當輸入框內的文字被更改的時候,就會同步更新至 datamessage 裡。

試一試

# textarea 文字方塊

textarea 的使用方式與 input 完全一樣:

<p><span>Multiline message is:</span> {{ message }}</p>

<textarea v-model="message" placeholder="add multiple lines"></textarea>
1
2
3

這裏要特別注意的是, 若在 <textarea> 裡面使用大括號 {{ }} 模板語法而不是 v-model 時,只會控制內容的顯示,此時更新的時候並不會寫回 data 內,需要使用 v-model 指令來與 data 內的狀態綁定。

試一試

# radio / checkbox 選擇框

input 標籤提供了兩種選擇框,分別是 radio 的單選框,以及 checkbox 的複選框。

先看 radio 的單選框,使用的方式很簡單,我們只需要在對應的 input 加上 v-model,並且透過 value 指定它的值:

<div id="app">
  <!-- 要注意下面兩組 input 對應的都是 v-model="picked"  -->
  <div>
    <input type="radio" id="one" value="1" v-model="picked">
    <label for="one">One</label>
  </div>
  <div>
    <input type="radio" id="two" value="2" v-model="picked">
    <label for="two">Two</label>
  </div>

  <span>Picked: {{ picked }}</span>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
const vm = Vue.createApp({
  data () {
    return {
      picked: 1
    }
  }
}).mount('#app');
1
2
3
4
5
6
7

因為 data 裡的 picked 預設為 1,所以執行時畫面上 <input type="radio" id="one" value="1"> 會預設被選中。

試一試

再來是 checkbox 的複選框,checkbox 這個元素很有趣,你可以將它當作多選的選項來看待, 但是當它只有一個的時候,又可以將它看做 truefalse ( on 或 off ) 的選項。

先看複選時的情境,用法跟前面 radio 完全一樣, 因為是複選的關係,唯一的差別只在 data 內的狀態必須是「陣列」:

<div id="app">
  <!-- 注意下面 input 對應的都是 v-model="checkedNames"  -->
  <input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
  <label for="jack">Jack</label>
  <input type="checkbox" id="john" value="John" v-model="checkedNames">
  <label for="john">John</label>
  <input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
  <label for="mike">Mike</label>
  <input type="checkbox" id="mary" value="Mary" v-model="checkedNames">
  <label for="mary">Mary</label>
  <br>
  <p>Checked names: {{ checkedNames }}</p>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
// 因為是複選的關係,這裡的 checkedNames 是陣列
const vm = Vue.createApp({
  data () {
    return {
      checkedNames: []
    }
  }
}).mount('#app');
1
2
3
4
5
6
7
8

換句話說,如果我們要控制表單的「全選」或「全部取消」狀態,只需要控制 data 裡的 checkedNames 陣列內容即可。

試一試

另外, checkbox 也可用在只有一個選項的情境,像是:

<div id="app">
  <input type="checkbox" id="checkbox" v-model="isChecked">
  <label for="checkbox">Status: {{ isChecked }}</label>
</div>
1
2
3
4
const vm = Vue.createApp({
  data () {
    return {
      isChecked: false
    } 
  }
}).mount('#app');
1
2
3
4
5
6
7

此時, data 內的選項,會變成 truefalse,當值為 true 時,對應的 checkbox 會被勾起。

試一試

# select 下拉列表選單

select 元素是很常見的下拉式選單,它用來與 option 做搭配,如下:

<div id="app">
  <!-- 注意,v-model 要放在 select 而不是 option -->
  <select v-model="selected">
    <option disabled value="">請選擇</option>
    <option>台北市</option>
    <option>新北市</option>
    <option>基隆市</option>
  </select>
  
  <p>Selected: {{ selected || '未選擇' }}</p>
</div>
1
2
3
4
5
6
7
8
9
10
11
const vm = Vue.createApp({
  data () {
    return {
      selected: ''
    }
  }
}).mount('#app');
1
2
3
4
5
6
7
試一試

要小心的是,v-model 必須使用在 <select> 標籤,不可套用在 <option> 標籤中。

若是 <select> 標籤中 v-model 的值無法對應到任何一個選項時,這個 select 標籤會預設為未選中的狀態, 在 iOS 系統中,會讓使用者無法選擇第一個選項,因為此時 iOS 不會觸發 change 事件。

所以為了解決這個問題,我們可以在第一個 option 的空值選項加入 disabled 屬性來排除此問題。

注意

使用了 v-model 指令的表單元素會自動忽略原有的 valuecheckedselected 屬性,實際的值將以 data 內的狀態為主。

v-model 表面上看似神奇,實際上 Vue.js 是將表單元素的事件監聽與實體內容更新的動作在背後處理掉了,可說是語法糖的一種。

# v-model 與修飾子

前面說過, v-model<input>text 文字輸入框對應的是 input 事件。 換句話說,當使用者每敲一次鍵盤就會觸發一次 input 事件,若是我們不希望這個文字輸入框的內容更新次數這麼頻繁,可以怎麼做呢?

以傳統的 JavaScript 做法來說,我們會將 input 事件改為監聽 change 事件,但 v-model 都幫我們在背後把事件處理掉了,我們該怎麼修改呢?

別擔心,Vue.js 針對此類需求提供了「修飾子」 (Modifiers) 的增強功能來協助我們。

為了增強指令 (directive) 的功能,修飾子會以 .XXX 的形式附加在指令後面,而與 v-model 搭配的有下面三種:

# .lazy

<input v-model.lazy="msg">
1

像這樣在 v-model 後面加上 .lazy,這個 input 輸入框就會從原本的 input 事件改為監聽 change 事件, 換句話說,就會在使用者離開輸入框焦點時才會更新 data 內容。

試一試

# .number

input 輸入框當中,即便我們輸入的都是數字,但實際上由 DOM API 取出的時候都會是以「字串」的形式出現。

假設我們今天想要做到兩個輸入框內容的數字相加,最簡單的方式可以這樣寫:

<input v-model="num1"> + <input v-model="num2"> = {{ sum }}
1
const vm = Vue.createApp({
  data () {
    return {
      num1: 0,
      num2: 0,
    } 
  },
  computed: {
    sum () {
      return this.num1 + this.num2;
    }
  }
}).mount('#app');
1
2
3
4
5
6
7
8
9
10
11
12
13

當使用者分別為 num1num2 兩個輸入框填入 12 後, sum 仍會出現 12 的結果。

過去我們在處理這樣問題的時候,會在 computedsum 裡面針對 num1num2 做數字轉型。

然而,Vue.js 針對這類數字轉型的問題也提供了 .number 的修飾子,也就是說,我們只需要在原本的 v-model 後面加上 .number

<input v-model.number="num1"> +  <input v-model.number="num2"> = {{ sum }}
1

如此一來,當 v-model 在更新狀態的時候,會在背後試圖將對應的資料轉型為數字的格式, 即便 sum 維持 return this.num1 + this.num2 的程式碼,回傳的也會是兩個數字相加的結果了。

試一試

# .trim

如果我們想要針對某個輸入框自動過濾前後的空白字元時,就可以在 v-model 後面加上 .trim 修飾子:

<input v-model.trim="msg">
1

此時更新後的實體狀態前後的空白字元就會自動被過濾掉了。

試一試

v-model 的修飾子就介紹到這,未來在介紹「事件」的時候,我們還會看到更多其他的修飾子。

# 模板綁定 - v-text / v-html / v-once / v-pre

# v-text

先前我們曾提過, Vue.js 是採用兩組大括號 {{ }} 的 Mustache 語法作為模板,除了這種方式之外,我們也可以透過指令來處理模板:

const vm = Vue.createApp({
  data () {
    return {
      text: 'HELLO'    
    }
  }
}).mount('#app');
1
2
3
4
5
6
7
<div v-text="text"></div>
1

像這樣我們透過 v-text 指令來指定對應 <div> 標籤的內容,此時畫面渲染的結果會與

<div>{{ text }}</div>
1

一樣都是 HELLO

不同的是,若我們在 <div> 標籤內加入文字,如:

<!-- 只會出現 HELLO -->
<div v-text="text">World!</div>

<!-- HELLO World! -->
<div>{{ text }} World!</div>
1
2
3
4
5

v-text 指令渲染的結果會無視標籤內原本的內容,也就是只會出現 HELLO,而透過模板語法的結果則是 HELLO World!


# v-html

除了 v-text 之外,還有個類似的指令,叫 v-htmlv-htmlv-text 的差別,就像 JavaScript DOM API 的 innerTextinnerHTML 一樣。

即使 data 裡的內容是 HTML 格式的字串,用 v-text 以及兩組大括號 {{ }} 的模板輸出的結果,會是原本的 HTML 字串而不會變成 HTML 來渲染:

const vm = Vue.createApp({
  data () {
    return {
      text: '<h1>HELLO</h1>'
    } 
  }
}).mount('#app');
1
2
3
4
5
6
7
<div v-text="text"></div>
1

顯示的結果會是 <h1>HELLO</h1> 的字串。

而改用 v-html 的情況:

<div v-html="text"></div>
1

則會出現解析後的 HELLO h1 大標題。


# v-once

v-once 的作用,在於只會渲染指定的節點一次,往後就不再更新。

如:

const vm = Vue.createApp({
  data () {
    return {
      text: 'HELLO'
    }
  }
}).mount('#app');
1
2
3
4
5
6
7
<input v-model="text">

<-- 此處可以比較有無 v-once 的差異 -->
<div>{{ text }}</div>
<div v-once>{{ text }}</div>
1
2
3
4
5

像這樣,我們為 text 狀態加上了 inputv-model 作為雙向綁定, 但即便我們更新輸入框內的文字,加上了 v-oncediv 依然只會顯示最初的 HELLO 字樣,並不會隨之更新。

試一試

# v-pre

前面提到,Vue.js 採用兩組大括號 {{ }} 的 Mustache 語法作為模板, 若我們就是想在網頁上呈現兩組大括號,而不是將它當作模板時,就會需要 v-pre 這個指令了:

const vm = Vue.createApp({
  data () {
    return {
      text: 'HELLO'
    }
  }
}).mount('#app');
1
2
3
4
5
6
7
<!-- 加上 v-pre 後不會解析模板內容 -->
<div v-pre>{{ text }}</div>

<!-- 將 {{ text }} 轉換為 HELLO -->
<div>{{ text }}</div>
1
2
3
4
5

像這樣,上面有加上 v-pre 指令的標籤,會顯示原始的 {{ text }} 的字樣,而下方 <div> 則是會出現 HELLO 的結果。

# 樣式綁定

本篇的最後來聊聊 Vue.js 與 CSS 樣式的綁定。

說到網頁的樣式,我們大多會透過 HTML 標籤的 classstyle 屬性來處理, 前面講到的 v-bind 除了可以用來控制一般屬性外,也可以用來控制標籤的 class 與樣式 style 的內容。

假設我們想要一個功能,當使用者在文字輸入框輸入超過十個字元的內容時,加入紅色樣式來作為警告提示,我們可以這樣做:

首先我們在 CSS 定義 .error 的樣式:

.error {
  border: red solid 1px;
  color: red;
}
1
2
3
4

接著我們在 Vue 實體中定義 message 狀態來與 input 綁定:

const vm = Vue.createApp({
  data () {
    return {
      message: ''
    } 
  }
}).mount('#app');
1
2
3
4
5
6
7
<div id="app">
  <input
    type="text" 
    v-model.trim="message"
    v-bind:class="{ 'error': message.length > 10 }">
</div>
1
2
3
4
5
6

最後,我們就可以透過 v-bind:class:class 來設定條件,當 message 的內容長度超過了 10 個字時, 就會自動加上 errorclass

如果想要一次控制多個 class,則可以透過逗號分隔:

<!-- v-bind:class 也可簡寫為 :class -->
<div v-bind:class="{ active: isActive, 'text-danger': hasError }"> ... </div>
1
2

isActivehasError 的狀態更新時,對應的 class 也會依照它們的 truthyfalsy 狀態來進行更新。

小提醒

在 JavaScript 裡, truthyfalsy 指的是在邏輯判斷式,轉型為 true 的值,我們稱為 truthy value,而轉型後為 false 的值,則稱為 falsy value

不熟悉 JavaScript truthyfalsy 規則的朋友可參考拙作:重新認識 JavaScript: Day 08 Boolean 的真假判斷 (opens new window) 的介紹。

而除了 class 之外,我們也可以直接操作 style 的屬性,以前面的範例改寫:

<div id="app">
  <input
    type="text" 
    v-model.trim="message"
    placeholder="請勿超過十個字"
    :style="msgStyle">
</div>
1
2
3
4
5
6
7
const vm = Vue.createApp({
  data () {
    return {
      message: ''
    } 
  },
  computed: {
    isVaild: function() {
      return this.message.length <= 10;
    },
    msgStyle: function() {
      return {
        'border': this.isVaild ? '' : 'red solid 1px',
        'color': this.isVaild ? '' : 'red'
      };
    }
  }
}).mount('#app');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

像這樣,我們將驗證的邏輯透過 computed 屬性來包裝,樣式由 msgStyle 來處理。 當輸入文字不超過 10 個字時,輸入框維持預設樣式,超過 10 個字的時候,則樣式會變成紅框紅字。

試一試
Last Updated: 10/4/2021, 5:43:30 PM