【Vue.js入門】一小時學會 Vue.component,完成動態飯店房間清單

假如今天我有很多不同的商品要賣,網站上有很多商品的細節需要編輯、計算,一個一個改實在太麻煩了,如果一個輸入錯誤,可能還會造成虧本,這樣可不行啊!因此我們今天練習用 Vue.js 做一個「牽一髮動全身」的動態編輯頁面,只要改一個地方,用到這筆資料的所有地方都會自動變更,想想真是太棒了!

這篇文章裡,我們會從 Vue.js 基本概念開始講起,非常適合剛開始接觸 Vue 的朋友們,接著我們用 Vue component 實作一個動態的飯店編輯頁面,跟著這篇文章,你會學到:

  1. Vue.js 基本概念:了解 Vue.js 是什麼、為什麼工程師要使用 Vue.js 操作資料。
  2. Vue.js 語法指令:學習如何使用 Vue.js 帶入資料、怎麼使用 v-bind、v-model 等 Vue 語法。
  3. 深入操作 Vue.component:用 Vue 元件實作一個能夠動態改變資料的編輯頁面,將 Vue 的基本語法與元件合併使用,透過元件的實際操作理解元件間的溝通傳遞。

我們會用飯店房間當作範例資料來練習,並在 Codepen 上實作,Codepen 是一個讓我們在編輯程式碼的同時能夠馬上看到結果的線上程式碼編輯器,註冊完就能夠使用了,那就讓我們開始吧!

如果想跟著影片一起動手做的話,請到這邊

Vue.js 基本概念

什麼是 Vue.js?Vue.js 其實是一套 JavaScript 的程式庫,負責把資料轉為網頁呈現。過去假如我們要寫一個飯店的網頁,我們要在 HTML 中一行行寫出飯店的房間標題、房型描述等等,假如要修改資料,就要在茫茫的 HTML 海中一筆筆修改;但是利用 Vue.js,我們不再需要一個個把資料寫死,而是可以先寫好一個模板,然後將裡面的資料用變數的方式代入。

例如,今天我們有一份自我介紹的模板:

「哈囉,我的名字是___,來自風非常大的地區___,喜歡___、___。」

我可以依據自己的個人資料來填空,這些填入的資料就是「變數」。Vue.js 中,變數可以不是單一的值,例如這裡的興趣以陣列(Array)的形式儲存,因此我們可以用索引(index)的方式來取出陣列裡的所有資料。

填空後的自我介紹就變成了:

「哈囉,我的名字是吳哲宇,來自風非常大的地區新竹,喜歡聽音樂、畫圖。」

但 Vue.js 的功能不止於此,它還有許多方便的功能。

v-for:迴圈,取出清單裡面所有資料

假設在「興趣」項目裡有一百筆資料,我們可以利用 v-for 抓取這一百筆資料,用一行 v-for=” hobby in hobbies” 自動重複標籤一百遍,列出這一百項資料。

  <ul>
    <li v-for="hobby in hobbies">
      {{ hobby }}
    </li>
  </ul>

// Vue.js 資料
  { 
    hobbies: ["聽音樂","畫圖"] 
  }

computed:前處理,先行運算

例如一個商品的價格是「原始價格x折扣數」,在 Vue.js 中,我可以不用每一次都自己運算,而是利用 computed 功能,創造「final-price 」這個變數,定義好最終價格等於原始價格乘以折扣數,這樣我們就可以直接使用 final-price 這個變數。

<h5> 折扣後的價格為 {{ final_price }} 元 </h5> 

// Vue.js 資料
  computed: {
    final_price: function() {
      return price * discount
    }
  }

v-bind:屬性綁定

在沒有使用 Vue.js 的時候,我們會使用 CSS 去改變網頁物件的顏色、框線或內容等,而 Vue 讓我們可以直接根據資料自動產生 CSS 帶入網頁。利用 v-bind:style 後面給予一個物件,物件裡則是一般 CSS 的寫法,Vue 就會幫我們自動產生 CSS 套在元件上,讓我們在資料裡就能夠定義或抽換元件的 CSS 樣式。

<div class="cover" v-bind:style="color_css"> 

// Vue.js 資料
  {
    color_css: {
      "background-image": "網址"
    }
  }

看到這裡,有沒有感覺得 Vue.js 好像一個個搬運工呢?是的,Vue.js 用起來就像是挖空格,我們規定好模板跟資料後,讓 Vue 物件依照要求把資料填空進去。所以使用 Vue.js 不可少的三元素就出現了:

  1. 模板:資料呈現的規則。
  2. 資料:要被填入的內容。
  3. Vue 物件:我們必須要新增一個 Vue 物件 ,讓它幫我們把資料呈現出來,也就是幫我們把資料搬到模板裡面的搬運工。

Vue 基本概念實作練習

看到這邊,是不是了解了 Vue 的基本概念了呢?我們用 Codepen 做一個小小的範例,讓我們更熟悉 Vue.js 吧。

首先,在這份範例中,我們會用到的是一份房間資料格式。這份資料的最外層用大括號(curly bracket)包起來,顯示它是一個物件,裡面記錄了房間的名稱、價格、設備等資料。

{
    "name": "經濟雙人房",
    "eng": "Economy Double Room",
    "price": 7000,
    "amount": 0,
    "cover": "http://bosscode.monoame.com/20170323_vue_comp/img/room%20(1).jpg",
    "discount": 0.9,
    "equipment": {
      "wifi": false,
      "bathtub": true,
      "breakfast": true
    }
},

接下來我們在 Codepen 裡開啟一個 new pen,同時在設定裡將 HTML 選擇為 Pug,CSS 設定為 Sass,在 JS 的部分用 CDN 載入 Vue.js,存檔後我們的基礎設定就完成了。

Codepen裡的設定
Codepen裡的設定

我們先將剛才的房間資料存進 Javascript 裡,並且宣告資料名稱為 roomdata:

// JavaScript
  var roomdata = {
    "name": "經濟雙人房",
    "eng": "Economy Double Room",
    "price": 7000,
    "amount": 0,
    "cover": "http://bosscode.monoame.com/20170323_vue_comp/img/room%20(1).jpg",
    "discount": 0.9,
    "equipment": {
      "wifi": false,
      "bathtub": true,
      "breakfast": true
    }
  };

接著在 HTML 裡寫入要呈現的內容

h1 房間的資料
  h2 名稱 {{ roomdata.name }}
  h2 價錢 {{ roomdata.price }}

現在,我們要在 JS 裡宣告宣告 Vue 實例,並且在 HTML 裡利用 #app 指定作用區域,這個作用區域表示我們要讓 Vue 在特定的區域檢查是否有要求代換的資料,如果我們把 #app 用原始 HTML 語法來看,代表的是:

<div id="app">

同時我們在 JS 裡也將宣告的 Vue 裡寫進入定義的 el 屬性,el (element) 代表的是作用的元件區域,值寫入 “#app”。

new Vue({
  el: "#app",
  data: {
    roomdata: {
      name: "經濟雙人房",
      eng: "Economy Double Room",
      price: 7000,
      amount: 0,
      cover: "http://bosscode.monoame.com/20170323_vue_comp/img/room%20(1).jpg",
      discount: 0.9,
      equipment: {
        wifi: false,
        bathtub: true,
        breakfast: true
      }
    }
  }
});

這樣,Vue 就能夠用變數自動代換指定的資料,接下來,我們練習讓 Vue 做計算。

computed:前處理、先行運算

將 HTML 中的最後的價錢指向變數 {{ final_price }} ,但我們的資料裡並沒有一筆資料名稱叫做「final_price」呀?沒關係,我們接著要自行定義它。

在 Vue 實例中,新增一個 computed (計算屬性),computed 的值必須是一個物件,裡面可以定義不同的運算式。我們現在需要的是 final_price,final_price 的值必須是一個函式 (function),在函式裡要求回傳「價格x折扣」。到這邊,{{ final_price }} 已經能夠幫我們回傳每間房間的價格了!

定義final_price算法
定義final_price算法

v-model:資料雙向綁定

下一個我們要練習的功能是 v-model,v-model 能夠幫我們做雙向的綁定,也就是當我們指定好 v-model 的兩端時,只要更動其中一處,另一端也會同步做更動。

觀看範例會更容易明白,我們在剛才寫好的 HTML 中增加兩行程式碼:

label 價錢
input(v-model="roomdata.price")

將 input 與資料中的 price 用 v-model 做綁定,對應的是 roomdata.price,這時畫面上便產生了 input 輸入框,框內就是 roomdata.price 的值 7000。這時如果我們改變了輸入值,資料就會同時做改變;反之如果改變了資料,輸入也會同步變化。

無論數值怎麼變,都可以套用折扣算式得到最終價錢
無論數值怎麼變,都可以套用折扣算式得到最終價錢

v-for 迴圈

接下來,我們用 v-for 取出清單內的所有資料,我們在 data 裡增加 rooms,值則是一個陣列,陣列裡包括 room1、room2、room3。

data: {
  rooms: [
    { name: "room1" }, 
    { name: "room2" }, 
    { name: "room3" }],
  ...
}

接著,我們在 HTML 裡用 v-for 把資料取出來。

h1 房間列表
  ul
    li(v-for="room in rooms") {{ room.name }}

在標籤 li 後面加上(v-for=”room in rooms”),這裡的 rooms 是 JavaScript 裡 data 中的 rooms,而 rooms 裡面的資料我們用 room 來命名(當然也可以換成其他名字),而我們指定好要以迴圈取出的資料範圍後,再以 {{ room.name }} 指定我們要取的是每一筆 room 的 name。

開始建置多間房間的列表
開始建置多間房間的列表

到這邊,我們練習了怎麼使用 Vue 物件呈現資料、也用了 computed、v-bind、v-for 做前運算、雙向綁定以及迴圈。附上完整的練習程式碼給大家參考。

// HTML
#app
  label 名稱
  input(v-model="roomdata.name")
  label 價錢
  input(v-model="roomdata.price")
  h1 房間的資料
    h2 名稱 {{ roomdata.name }}
    h2 價錢 {{ roomdata.price }}
    h2 最後的價錢 {{ final_price }}

  h1 房間列表
    ul
      li(v-for="room in rooms") {{ room.name }}

// JavaScript
new Vue({
  el: "#app",
  data: {
    rooms: [{ name: "room1" }, { name: "room2" }, { name: "room3" }],
    roomdata: {
      name: "經濟雙人房",
      eng: "Economy Double Room",
      price: 3000,
      amount: 0,
      cover: "http://bosscode.monoame.com/20170323_vue_comp/img/room%20(1).jpg",
      discount: 0.9,
      equipment: {
        wifi: false,
        bathtub: true,
        breakfast: true
      }
    }
  },
  computed: {
    final_price: function () {
      return this.roomdata.price * this.roomdata.discount;
    }
  }
});

接下來我們就要進入這次的主題: component 元件。

為什麼要使用 Vue 元件呢?因為我們在架設網站的時候,很有可能會面對一份非常龐大的資料!資料庫中可能有上百筆資料、有不同的邏輯、許多複雜的呈現規則等等,因此工程師們想出的解決辦法就是把龐大的資料拆成不同的 Vue 物件,每一個物件除了負責自己的資料外,也能夠繼承資料以及繼承方法。

「繼承資料」指的是能夠接收源數據的資料,例如飯店裡每一間房間都要打九折,我們可以在每間房間裡指定繼承源數據的折扣數 0.9,這樣就不用在每一間的資料中寫上一行「discount = 0.9」。

而「繼承方法」則好比開放權限,一個 Vue 物件除了能夠有自己指定的方法外,也能夠接收源數據規定好的方法,甚至能夠用接收到的方法回頭套用到源數據上。

接下來,我們就來一步步實作吧。

Vue Component 實作練習

我們開啟一個新的 Pen,並且一樣在設定裡將 HTML 選擇 Pug,CSS 設定為 Sass,同時引入 Bootstrap 的 CDN 方便我們之後做排版,在 JS 的部份載入 Vue.js,這樣基礎設定就完成了。接著將我們需要用到的資料複製到 JavaScript 裡,並且宣告資料的名稱為 rooms。

// JavaScript
	var rooms = [
	  {
	    "name": "經濟雙人房",
	    "eng": "Economy Double Room",
	    "price": 7000,
	    "amount": 0,
	    "cover": "http://bosscode.monoame.com/20170323_vue_comp/img/room%20(1).jpg",
	    "discount": 0.9,
	    "equipment": {
	      "wifi": false,
	      "bathtub": true,
	      "breakfast": true
	    }
	  },
	  {
	    "name": "海景三人房",
	    "eng": "Sea view triple Room",
	    "price": 7800,
	    "amount": 0,
	    "cover": "http://bosscode.monoame.com/20170323_vue_comp/img/room%20(2).jpg",
	    "discount": 0.8,
	    "equipment": {
	      "wifi": true,
	      "bathtub": true,
	      "breakfast": false
	    }
	  },
	  {
	    "name": "典雅景觀房",
	    "eng": "Elegant landscape Room",
	    "price": 5400,
	    "amount": 0,
	    "cover": "http://bosscode.monoame.com/20170323_vue_comp/img/room%20(3).jpg",
	    "discount": 0.85,
	    "equipment": {
	      "wifi": false,
	      "bathtub": true,
	      "breakfast": true
	    }
	  },
	  {
	    "name": "尊享豪華房",
	    "eng": "Exclusive Deluxe Room",
	    "price": 9800,
	    "amount": 0,
	    "cover": "http://bosscode.monoame.com/20170323_vue_comp/img/room%20(4).jpg",
	    "discount": 0.8,
	    "equipment": {
	      "wifi": true,
	      "bathtub": false,
	      "breakfast": true
	    }
	  },
	  {
	    "name": "商務雙人房",
	    "eng": "Business Double Room",
	    "price": 5600,
	    "amount": 0,
	    "cover": "http://bosscode.monoame.com/20170323_vue_comp/img/room (5).jpg",
	    "discount": 0.9,
	    "equipment": {
	      "wifi": true,
	      "bathtub": false,
	      "breakfast": false
	    }
	  },
	  {
	    "name": "溫泉雙人房",
	    "eng": "Hot spring double Room",
	    "price": 8400,
	    "amount": 0,
	    "cover": "http://bosscode.monoame.com/20170323_vue_comp/img/room (6).jpg",
	    "discount": 0.6,
	    "equipment": {
	      "wifi": true,
	      "bathtub": true,
	      "breakfast": true
	    }
	  },
	  {
	    "name": "總統套房",
	    "eng": "Presidential Suite",
	    "price": 23000,
	    "amount": 0,
	    "cover": "http://bosscode.monoame.com/20170323_vue_comp/img/room (7).jpg",
	    "discount": 0.75,
	    "equipment": {
	      "wifi": true,
	      "bathtub": true,
	      "breakfast": true
	    }
	  },
	  {
	    "name": "奢華四人房",
	    "eng": "Luxury four Room",
	    "price": 8500,
	    "amount": 0,
	    "cover": "http://bosscode.monoame.com/20170323_vue_comp/img/room (8).jpg",
	    "discount": 0.7,
	    "equipment": {
	      "wifi": true,
	      "bathtub": true,
	      "breakfast": false
	    }
	  }
	];

先看一下我們的目標設計稿和需求:

  1. 瀏覽區塊中:每個房型會以卡片的樣式呈現,內容包括房間的圖片、名稱、價格和設備等資料。
  2. 編輯區塊中:可以編輯飯店折扣數、服務費、各個房間的資料細節。
  3. 在編輯區塊中修改的內容會即時跟瀏覽區塊的資料做同步,只要調整一次折扣數或服務費,所有房間的資訊都能夠繼承變動後的數值自動做計算,也能夠針對單一項目做調整。
最終完成品
最終完成品

房型一覽區塊基本結構:利用 v-for 迴圈重複架構

我們先在 HTML 中將畫面右側的房型一覽基本架構寫出來,並利用 Bootstrap 做排版。

#app
  .container
    .row
      .col-sm-9
        .row
          .col-sm-4(v-for="myroom in rooms")
            h3 {{ room.name }}
            .info
              h5 {{ room.eng }}
使用boostrap排版
使用boostrap排版

定義 Vue.component 並繼承資料

接著,我們可以先把房型切成元件(component)。我們先在 HTML 中新增一個 template,並且給它一個 id 叫做 room。template 裡面就是我們的呈現規則,要注意的是 template 裡面需要一個總體物件,這是因為 template 裡面根源只能有一個物件,不能是複數的物件,因此我們用一個大的 div 標籤來包裹裡面的資料,這個 div 我們也給它一個 class=”room_col”。

#app
  .container
    .row
      .col-sm-9
        .row
          .col-sm-4(v-for="myroom in rooms")


template#room
  .room_col
    h3 {{ room.name }}
    .info
      h5 {{ room.eng }}

接下來,我們就要在 JS 裡新增一個 Vue 元件。新增的方式是 Vue.component()。括弧裡第一個變數是元件的名稱,我們定義它為 “room”,接著第二個變數我們給它一個物件 { },代表所有要初始化的設定。

第一個我們設定 template: “#app”,告訴元件呈現的規則是 HTML 裡的 template#room。接著我們設定 props (繼承),因為我們元件的資料是從源數據繼承的,我們將繼承的資料命名為 props: [“room_data”]。

Vue.component("room", {
  template: "#room",
  props: ["room_data"]
});

在 HTML 中呼叫繼承的資料

Vue 元件設定好了,我們要回到 HTML 裡使用剛剛設定的資料。在 .col-sm-4(v-for=”myroom in rooms”) 底下我們給予一個 room 標籤,然後使用 v-bind 跟 room_data 做綁定 (v-bind:room_data=”myroom”),同時因為我們在 Vue compoenent 中 props 定義的名稱是 “room_data“,因此在 HTML 裡 template 中我們要呼叫的就是 room_data 裡的 name 跟 eng。

#app
  .container
    .row
      .col-sm-9
        .row
          // 為了能夠區分對應的資料,這邊把原來的 room 更名成 myroom
          .col-sm-4(v-for="myroom in rooms") 
             room(v-bind:room_data="myroom") 

template#room
  .room_col
    h3 {{ room_data.name }}
    .info
      h5 {{ room_data.eng }}
定義 Vue.component ,繼承並在HTML裡呼叫資料

接著,我們要來計算房間的價格。假設每一間房間有自己的定價和折扣數,另外整間飯店也有一個總體折扣數,我們的房價計算方式應該是:房價=定價x房間折扣x飯店折扣。為了讓大家能看清楚過程,我們先把每一個呼叫拆開寫出來。

飯店折扣我們在 vm 實例中先定義為 0.9。

var vm = new Vue({
  el: "#app",
  data: {
    rooms: rooms,
    discount: 0.9    // 新增飯店折扣數
  }
});

定價x房間折扣我們已經知道怎麼寫了,也就是 {{ room_data.price }}*{{ room_data.discount }}。接著,我們要在元件的繼承屬性中增加折扣 props: [“room_data”, “hotel_discount”]。

Vue.component("room", {
  template: "#room",
  props: ["room_data", "hotel_discount"]  // 增加繼承 hotel_discount
});

接著,我們在 HTML 中,先前已經綁定 v-bind:room_data=”myroom” 的 room 標籤後面,綁定 v-bind:hotel_discount=”discount”。

#app
  .container
    .row
      .col-sm-9
        .row
          .col-sm-4(v-for="myroom in rooms")
            // v-bind:room_data 可以簡寫為 :room_data
            room(:room_data="myroom", :hotel_discount="discount")

最後,我們需要在 template 中呼叫 hotel_discount。

template#room
  .room_col
    h3 {{ room_data.name }}
    .info
      h5 {{ room_data.eng }}
     h5 {{ room_data.price }}*{{ room_data.discount }}*{{ hotel_discount }} 

到這邊,房價的計算過程 定價x房間折扣x飯店折扣 已經可以正確地顯示出來了。

顯示房價的計算過程
顯示房價的計算過程

定義 computed 屬性做前運算

不過我們不能讓客人自己按計算機計算價錢,所以我們要讓 Vue 幫我們做運算。最單純的做法是我們把需要計算的過程寫在一個大括號裡:{{ room_data.price * room_data.discount * hotel_discount }},不過這樣顯然是一個很冗長的程式碼,因此我們定義一個 final_price 變數,再用 computed 幫我們做前運算。

這邊有一個小地方需要注意,如果運算後的結果是浮點數的話,Vue 並不會自動幫我們轉為整數,因此我們可以用 parseInt() 將計算結果先轉為整數後,再回傳。

// HTML
template#room
  .room_col
    h3 {{ room_data.name }}
    .info
      h5 {{ room_data.eng }}
      h5 {{ final_price }}
	
// JavaScript
Vue.component("room", {
  template: "#room",
  props: ["room_data", "hotel_discount"],
  computed: {
    final_price: function () {
      return parseInt(
        this.room_data.price * this.room_data.discount * this.hotel_discount
      );
    }
  }
});

這邊大家容易混淆的是 this 到底指向哪裡?好消息是,在 Vue 元件中,this 永遠會指向元件本身。而我們在 computed 中定義的 final_price 也可以在元件中使用 this 呼叫來做其他的運算。

因為畫面設計稿上我們希望呈現出房間原價、總折數、以及折扣後的房價,因此我們可以在 computed 中定義我們需要的運算式,然後在 HTML 裡呼叫變數,同時我們增加 .cover 方便未來增加房間圖片。

/ HTML
template#room
  .room_col
    .cover
      h3 {{ room_data.name }}
    .info
      h5 {{ room_data.eng }}
      h5 {{ room_data.discount }}*{{ hotel_discount }} = {{ final_discount_show }}折
      h4 TWD
         {{ room_data.price }}
        .final_price {{ final_price }}


// JavaScript
Vue.component("room", {
  template: "#room",
  props: ["room_data", "hotel_discount"],
  computed: {
    final_discount: function () {
      return this.room_data.discount * this.hotel_discount;
    },
    final_discount_show: function () {
      return parseInt(this.final_discount * 100);
    },
    final_price: function () {
      return parseInt(this.room_data.price * this.final_discount);
    }
  }
});
呈現出房間原價、總折數、以及折扣後的房價
呈現出房間原價、總折數、以及折扣後的房價

增添 CSS 美化版面

我們可以調整 CSS 的樣式,讓畫面變得更美觀。除了調整元素的邊距、顏色、字體大小外,我們用了 position: relative / position: absolute(相對位置/絕對位置)以及浮動元素 float 做位置的排版,並且用偽類 pseudo class 增加價格最後的 $ 字號。

// CSS
$color_red: #DB4343 

*
  border: 1px solid #666    // 排版的時候為了方便觀看,我們增加邊框的樣式

.room_col 
  padding: 20px
  .cover
    height: 150px
    background-color: #eee
    position: relative
    h3
      position: absolute
      bottom: 10px
      font-size: 20px
      padding: 5px 15px
      background-color: #fff
  .info
    padding: 10px
    h5
      font-size: 12px
    .final_price
      float: right
      color: $color_red
      &:after
        content: "$"
調整CSS 美化版面
調整CSS 美化版面

computed 也能夠用來做 CSS 屬性運算

接下來,我們來做最好玩的部分,利用 v-bind 把房間圖片放進去。background-image 因為是一個 CSS 屬性,所以我們要讓 Vue 元件幫我們計算這個 CSS,我們在 computed 裡加上 bg_css 並且給它一個 function,在 function 裡計算完後再將結果回傳。CSS 中的背景圖片的語法是 background-image: “url(‘一段網址’)”,因此 function 裡也是如此,同時網址的部分呼叫繼承的資料 this.room_data.cover,最後別忘了要將結果回傳,回傳資料需要用大括弧 { } 包起來。

// JavaScript
  computed: {
    // ...
    bg_css: function () {       // 增加 background-image CSS 屬性
      return {
        "background-image": "url('" + this.room_data.cover + "')"
      };
    }
  }

接著,在 HTML 裡我們要利用 v-bind 操作 CSS,在 .cover 後指定 v-bind:style 並且指定 bg_css 物件。這時畫面上已經能載入圖片了。我們再使用 CSS 調整圖片的位置讓圖片,加上 background-size: cover 讓圖片縮放到封面大小、加上 background-position: center center 設定圖片初始位置為水平置中和垂直置中。

到這邊,輔助用的外框線我們就讓它功成身退吧。

// HTML
  .cover(v-bind:style="bg_css")

// CSS
  .cover
    height: 150px
    position: relative
    background-size: cover
    background-position: center center
載入圖片,位置都沒問題後將框線移除
載入圖片,位置都沒問題後將框線移除

最後我們要來做一點畫面上細緻度的調整。

首先,我們在將每一個房型的資料最外層的 div 標籤加上 class=”col-room”,並且給予內距 padding: 20px。避免混淆,將原來 template 最外層的 class 改成 “room-container”,加上陰影 box-shadow: 0px 0px 10px rgba(0,0,0,0.3)。這時,畫面上的房型一覽是不是很像用一張張卡片呈現,一目了然呢?

每張房型卡片都加上外框陰影
每張房型卡片都加上外框陰影

增加飯店資訊編輯區塊

到這裡為止,我們已經將設計稿上右側呈現的部分做好了,接著我們要來做左邊的編輯區塊。再看一眼我們的設計稿吧。

最終成品參考圖
最終成品參考圖

房型一覽的部分剛才我們已經用了 CSS grid 指定了 col-sm-9,因此,編輯區塊自然就是 col-sm-3 了(想了解更多格線佈局可參考:MDN)。我們為兩邊的畫面分別加上標題及分隔線,並且為畫面整體加上一點內距,讓視覺看起來比較美觀。

// HTML
#app
  .container
    .row
      .col-sm-3
        h1 飯店資料
        hr
      .col-sm-9
        h1 房間列表
        hr
        .row

// CSS
body
  padding: 20px
h1
  font-size: 30px

增加房間總折數編輯欄位

編輯區域第一個顯示的是飯店的總折數,我們加上 label 總折數,並且將 input 用 v-model 與 discount 做雙向綁定。

// HTML
#app
  .container
    .row
      .col-sm-3
        h1 飯店資料
        hr
        label 總折數
        // .form-control 是 Bootstrap 的標籤,我們用來美化視覺
        input.form-control(v-model="discount")   

// JavaScript
var vm = new Vue({
  el: "#app",
  data: {
    rooms: rooms,
    discount: 0.9
  }
});

到這邊,厲害的事情發生了!我們在 input 中改變折扣數的話,可以看見房型一覽中的價格也會立即變化,這就是 Vue.js 厲害的地方!資料綁定的好處就在於一旦一方改變了,所有相依的資料也會同步改變。

編輯左側欄位,加上總折扣數
編輯左側欄位,加上總折扣數

增加服務費編輯欄位

接著我們在每一筆房間訂單加上服務費。我們先將要繼承的資料都定義好,這裡可以拆成三個步驟:

  1. 增加需要繼承的資料:我們先在 vm data 中加上 service_fee: 200。
  2. 在 HTML 中告訴元件 room 要繼承 service_fee,並且將元件中的服務費重新命名為 hotel_fee。
  3. 在元件 Vue.component 中的 prop 屬性中指定繼承 hotel_fee。

這樣我們就寫完了繼承的資料,接著我們要在計算中加上服務費,我們用 this.hotel_fee 呼叫。需要注意的是,透過繼承而來的資料,子元件會收到的是「純文字字串」,而不是外層元件的狀態內容,因此我們利用 JavaScript 強制轉型的特性,將 this.hotel_fee 乘上 1.0 轉為數字型別。

// HTML 
label 服務費
// 服務費用 v-model 來綁定
input.form-control(v-model="service_fee")
...
.col-sm-4.col-room(v-for="myroom in rooms")
  room(
    :room_data="myroom",
    :hotel_discount="discount",
    // 告訴元件 room 要繼承 service_fee,並且將元件中的服務費重新命名為 hotel_fee
    :hotel_fee="service_fee" 
  )

// JavaScript
// Vm 實例
var vm = new Vue({
  el: "#app",
  data: {
    rooms: rooms,
    discount: 0.9,
    service_fee: 200,    // 增加服務費 200 元
  }
});

// Vue 元件
Vue.component("room", {
  template: "#room",
  // 增加繼承 hotel_fee
  props: ["room_data", "hotel_discount", "hotel_fee"],
  computed: {
    // 在 final_price 計算中加上服務費,需要乘上 1.0 是為了將 hotel_fee 的型別強制轉為數字
    final_price: function () {
      return (
        parseInt(this.room_data.price * this.final_discount) + this.hotel_fee * 1.0
      );
    },  
  }
});
加上服務費計算連動
加上服務費計算連動

增加各個房間編輯區塊

接著我們來製作各個房間的編輯區塊。首先增加標題 h1 房間編輯 及水平分隔線 hr 與上面的區塊做區分,接著我們增加一個 div 標籤並給予 room_edit 的 class,然後利用 v-for=”room in rooms” 做重複結構。room_edit 底下我們利用 input 跟 v-model 做雙向資料的綁定,分別綁定房間的名稱、價格、折扣數、英文名稱以及圖片網址。

// HTML
h1 房間編輯
hr
.room_edit(v-for="room in rooms")     // 利用 v-for 做重複結構
	h4 {{ room.name }}
	label 房間名稱
	input.form-control(v-model="room.name") 
                        // 利用 v-model 做雙向綁定
	label 價格
	input.form-control(v-model="room.price")
	label 折價
	input.form-control(v-model="room.discount")
	label 英文名稱
	input.form-control(v-model="room.eng")
	label 圖片網址
	input.form-control(v-model="room.cover")

當我們想要調整視覺的時候,隨時可以利用 Bootstrap 跟 CSS 做微調。例如我們給予每一個 input form-coontrol 的 class,讓它取得 Bootstrap 的預設樣式。

我們也能夠自行設定 CSS,例如將左側編輯區域的高度固定,超出的部分使用卷軸滾動的效果,也將每一個 room_edit 區塊利用 margin-top 做出間隔。

#app
  .container
    .row
      .col-sm-3.col-edit

//CSS
.col-edit
  height: 100vh
  overflow-y: scroll

.room_edit 
  margin-top:30px

到這邊,我們已經能在每一個房間的編輯區塊做個別房間的調整,例如將雙人房改為單人房、改變折扣數或價格等等。

增加各個房間編輯區塊
增加各個房間編輯區塊

增加房型的按鈕功能:v-on 事件處理

假如我們要新增一組房型的話怎麼辦呢?這時我們就可以用到 Vue 的 methods(方法)。我們在房間編輯區域最下面再增加一組 .room_edit + 新增房間,同時加上 @click 表示在點擊的時候要觸發指定的事件,這邊我們指定點擊時觸發 addroom 。觸發的事件我們則要寫進 Vue 物件裡,告訴它這個事件要做什麼。

// HTML
.room_edit(@click="addroom") + 新增房間

接著我們在 data 之後增加 methods,methods 的值是一個物件,因為裡面可以有許多不同的 method。這邊我們要增加的是 addroom。同時 addroom 的值是一個函式,我們希望 addroom 將資料推進 rooms 裡。要推的資料我們從上面房型資料中複製一組下來,並且將內容編輯一下。

現在,我們如果點擊「+ 增加房間」,Vue 就會幫我們增加一組房間的卡片,並且左邊也有對應的編輯區域,我們可以在編輯區直接修改房間資料,這樣是不是非常方便呢!

var vm = new Vue({
  el: "#app",
  data: {
    rooms: rooms,
    service_fee: 200,
    discount: 0.9
  },
  methods: {                    // 增加 methods,值是一個物件
    addroom: function () {      // 增加 addroom,值是一個 function
      this.rooms.push({         // 將資料推進 rooms 裡
        name: "新房間",
        eng: "new Room",
        price: 0,
        amount: 0,
        cover: "",
        discount: 0,
        equipment: {
          wifi: true,
          bathtub: true,
          breakfast: false
        }
      });
    }
  }
})
新增房間卡片並可以直接編輯資料
新增房間卡片並可以直接編輯資料

在房型卡片上顯示房間設備圖示,利用 v-if 做條件顯示

接下來我們要放入房間設備的小圖示,我們會用到 Font Awesome 這個好用的字型圖示工具,因此我們到 Setting 的 CSS 載入 font-awesome 的 CDN。接著,我們將房間介紹的模板 template#room 裡面加上一組 icons,icons 裡有三個圖示分別對應的是房間設備資料中的早餐、浴缸以及 wifi。

// HTML
  template#room
    // ...
    .info
      h5 {{ room_data.eng }}
        .icons   // 增加三個圖示並用一個 div 包起來
          span
            i.fa.fa-coffee
          span
            i.fa.fa-bath
          span
	      i.fa.fa-wifi

// JavaScript
    name: "經濟雙人房",
    // ...
    equipment: {       // 三個圖示要對應的是房間資料中 equipment 的三個屬性
      wifi: false,
      bathtub: true,
      breakfast: true
    }

我們希望當設備裡屬性是 true 時顯示圖示,而 false 時隱藏圖示,要怎麼做呢?這時候可以使用條件渲染 v-if,v-if 的意思是當條件為 true 時,瀏覽器便會幫我們渲染出來,如果是 false,瀏覽器則會忽略該元素。

// HTML
	.icons
		span(v-if="room_data.equipment.breakfast")
			i.fa.fa-coffee
		span(v-if="room_data.equipment.bathtub")
			i.fa.fa-bath
		span(v-if="room_data.equipment.wifi")
			i.fa.fa-wifi

這時,房間列表中,每個房間設備的對應圖示已經可以顯示出來了。

利用 v-if 做條件顯示,在房型卡片加上房間設備圖示
利用 v-if 做條件顯示,在房型卡片加上房間設備圖示

增加房間設備圖示的編輯欄位

接著,我們要在編輯區域中增加房間設備的區塊。一樣用 v-model 綁定 room.equipment 對應的設備,但我們要使用核取方塊(☑︎)來編輯設備的有無,因此在 input 加上 type=”checkbox”,並且加上 form-check-input 的 class 來套上 Bootstrap 樣式。最後我們加上一些 CSS 效果讓視覺美觀一點。

// HTML
	label 房間設備
      label 早餐
	  input.form-check-input(
          type="checkbox",
          v-model="room.equipment.breakfast"
        )
      label 浴缸
        input.form-check-input(
          type="checkbox",
          v-model="room.equipment.bathtub"
        )
      label wifi
        input.form-check-input(
          type="checkbox",
          v-model="room.equipment.wifi"
        )

// CSS
  .info
    // ...
    .icons                    // 增加 icons 的 CSS
      display: inline-block
      margin-left: 10px
      span
        margin-right: 5px
        opacity: 0.6
增加房間設備編輯欄位
增加房間設備編輯欄位

增加刪除功能

接著我們要增加刪除房型的功能,包括在編輯區域刪除某個房型,以及在瀏覽區域也能點擊刪除。我們先來看看編輯區域的刪除功能怎麼做吧。

我們先利用 Font Awesome 增加一個垃圾桶的圖示 i.fa.fa-trash,跟新增房間一樣,我們要加上點擊功能,所以加上 (@click=”delete_room(id)”)。delete_room 是一個 method,我們指定它要刪除指定的 id 房型。不過我們的資料中沒有 id,所以我們需要 Vue 幫我們在抓資料的時候幫我們把房型的 index 也抓出來,以 index 當作每個房型的 id,因此我們在 v-for 中加上 v-for=”(room, id) in rooms”。( v-for 語法可參考 Vue 官方文件。)

取得 id 後,別忘了在 vm 實例中增加這個 method,在 delete_room 中,我們呼叫 this.rooms 陣列,然後用 splice 刪除從指定的 index 中刪除 1 筆資料。最後,利用 CSS 讓游標滑過垃圾桶圖示的時候顯示可點擊圖示,優化一點使用者體驗,到這裡,編輯區塊的刪除功能就完成了!

// HTML
      // 在跑迴圈的時候讓 Vue 也幫我們取得 id (這邊的 id 就是 index)
	.room_edit(v-for="(room, id) in rooms")  
	  h4 {{ id + 1 }}{{ room.name }}
	  label 房間名稱
      i.fa.fa-trash.cursor_pointer(@click="delete_room(id)")

// CSS
	.cursor_pointer 
	  cursor: pointer

// JavaScript
	methods: {
	// ...
		delete_room: function (id) {
	      this.rooms.splice(id, 1);
	    }
	}
增加刪除房間的功能
增加刪除房間的功能

接著,我們來增加房型一覽中的刪除功能。

我們要讓每個房型卡片的右上角有個 ✘ 圖示,點擊圖示可以刪除,跟剛剛是不是很像呢?我們一步步完成它,先利用 Font Awesome 加入圖示,接著利用 CSS 的絕對定位調整位置,然後加上顏色變化。

// HTML
	template#room
	  .room_container
	    .cover(v-bind:style="bg_css")
	      h3 {{ room_data.name }}
	      i.fa.fa-times               // 增加圖示

// CSS
	i.fa.fa-times
      position: absolute
      top: 10px
      right: 10px
      color: white
      cursor: pointer
      transition: 0.5s
      &:hover
        color: $color_red

接著,我們讓 Vue 元件繼承 id 和 delete_room method,然後將刪除圖示也加上點擊刪除的功能 (@click=”delete_room(id)”),這樣,兩邊的刪除功能都完成囉!

// HTML
	.col-sm-9
    h1 房間列表
    hr
	    .row
                                     // 一樣讓 Vue 幫我們取得 id
          .col-sm-4.col-room(v-for="(myroom, id) in rooms") 
          room(
            :room_data="myroom",
            :id="id"               // 這邊的 id 會抓取迴圈跑出來的 id
            :hotel_discount="discount",
            :hotel_fee="service_fee",
            :delete_room="delete_room",    // 繼承的 delete_room
          )
	// ...

	template#room
	  .room_container
	    .cover(v-bind:style="bg_css")
	      h3 {{ room_data.name }}
	      i.fa.fa-times(@click="delete_room(id)")    // 加上點擊刪除功能


// JavaScript
	Vue.component("room", {
	  template: "#room",
	  props: ["room_data", "hotel_discount", "hotel_fee", "delete_room", "id"]   
		// 增加繼承 delete_room 以及 id
	}
房間卡片右上角的x刪除功能也完成了
房間卡片右上角的x刪除功能也完成了

收整房間編輯區塊

最後,左邊的編輯區塊看起來落落長的,我們把目前不需要用到的資料收整起來,只顯示要編輯的房間資料區塊就好了。

要收整所有房間到下拉式選單裡,我們增加一個 select 標籤,並且在 select 中設定 option 標籤,標籤我們要綁定房間的 id 以及顯示房間名稱,一樣用到的是 v-for,到這裡你是不是已經對 v-for 很熟悉了呢!因此我們在 option 後面加上 (v-for=”(r, id) in rooms”, :value=”id”) {{ r.name }}。這樣我們的下拉選單就做好了!

// HTML
h1 房間編輯
select.form-control(v-model="edit_id")
  option(v-for="(r, id) in rooms", :value="id") {{ r.name }}
收整左側房間編輯區塊
收整左側房間編輯區塊

接著,我們要讓編輯區塊在我們新增房間的時候,同時跳轉到新房間的編輯畫面。另外我們要把刪除房間按鈕綁定的 id 改成編輯中的房間 id 。

// HTML
	hr
	// 原來是 .room_edit(v-for="(room, id) in rooms")
	.room_edit(v-for="(room, id) in [rooms[edit_id]]")
	  h4 {{ room.name }}
        label 房間名稱
	//原來是 i.fa.fa-trash.cursor_pointer(@click="delete_room(id)") 
          i.fa.fa-trash.cursor_pointer(@click="delete_room(edit_id)")

/// JavaScript
  methods: {
    addroom: function () {
      this.rooms.push({
	  // ...
      });
      // 當 addrom 時,同時讓 edit_id = 最後一間房間
      this.edit_id = this.rooms.length - 1 },

最後,我們把「+ 新增房間」的功能按鈕位置調整一下,並且加上 Bootstrap 的 class 修改它的視覺效果,畫面是不是更美觀了呢。到這邊,我們的 Vue component 實作練習就大功告成囉!

// HTML
	h1 房間編輯
	  select.form-control(v-model="edit_id")
	    option(v-for="(r, id) in rooms", :value="id") {{ r.name }}
        // 移動「+ 新增房間」按鈕位置,並加上 Bootstrap class
	  button.btn.btn-secondary.room_edit(@click="addroom") + 新增房間 
調整編輯欄的視覺,完成此次練習
調整編輯欄的視覺,完成此次練習

總結

這次的練習真是段漫長的旅程啊,最後一起回顧一下我們完成了什麼吧:

  1. 學習 Vue.js 的基本概念及語法:我們認識了 Vue.js 是什麼,並且學習 v-for、v-bind 等 Vue.js 的基本指令。
  2. 實作練習 Vue.js 的基本語法:我們用一組簡單的飯店資料練習如何實際使用 v-model, computed 等 Vue 的指令來帶入資料,透過資料來驅動畫面。
  3. 實作完成一個動態飯店清單:我們利用 Vue 元件及 v-for 迴圈製作房型卡片,並且運用元件的繼承屬性以及繼承方法等特性,透過 v-model 做雙向綁定,讓資料能在一端修改時同步更動所有相依資料。

看見完成品你是不是也有滿滿的成就感呢?如果想看完整的程式碼,可以參考老闆的 Codepen。如果想跟 Vue 更熟悉,很推薦你實際看一下 Vue.js 的官方文件,透過一次次的查找資料跟練習,你也一定能將 Vue 用得像呼吸一樣自然!那我們下次見啦。 ₍₍ ◝( ゚∀ ゚ )◟⁾⁾


老闆的工商時間

想了解更多如何寫出漂亮清晰的網頁嗎?老闆在 Hahow 的教學課程 動畫互動網頁程式入門(HTML/CSS/JS) 用平易近人的語言,用簡單的方式帶你作出不簡單的網頁。已經有網頁程式基礎了嗎?進階課程 動畫互動網頁特效入門(JS/CANVAS) 能讓你紮實掌握 JavaScript 程式與動畫基礎以及進階互動,整合應用掌控資料與顯示的Vue.js前端框架,完成具有設計感的互動網站。

此篇直播筆記由幫手 Kate Chu 協助整理

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

分享
PHP Code Snippets Powered By : XYZScripts.com