第12屆鐵人賽Day 21 Rails專案開發 - Vue draggable套件拖拉column

隨著鐵人賽過了2/3來到尾聲,我們的Kanban開始有了雛形。

而今天實作的目的,是要讓Kanban裡的欄位可以拖拉,而且能將拖拉完的結果存入資料庫!

想到系統的拖拉功能,db裡面的資料表就要有定位(存放原始的位置以及拖拉過後的位置)。

Rails後端: Acts as list

Acts as list是可以讓我們在model裡透過position這個欄位在db裡找到對應的資料的Rails套件。

安裝好gembundle install之後,點進GitHub連結手冊一邊參考,一邊來更改我們的 model。這裡我們將position依據排序遞增

kanban.rb

class Kanban < ApplicationRecord
  has_many :columns, -> { order(position: :asc) }, dependent: :destroy
  belongs_to :user
  validates :name, presence: true
end

column.rb需要透過給定scope,作為排序的分組:

(例如在我的model裡,同一個kanban裡,每個columnposition的值是unique的!不然會排序大亂啊~)

column.rb

class Column < ApplicationRecord
  acts_as_list scope: :kanban  

  has_many :tickets, -> { order(position: :asc)}, dependent: :destroy
  belongs_to :kanban
  validates :name, presence: true  
end

後端的事前準備完成,接著來看前端:

Vue前端: draggable套件

寫javascript的朋友可能都聽過Sortable.js這個拖放排序列表、支援三大前端框架(angular、vue、react)的js外掛程式,而Vue Draggable就是基於Sortable.js而產生的套件。研究了Vue邁向20天,今天就來研究我的第一個Vue的第三方套件Vue Draggable

首先透過yarn安裝Vue.Draggable 套件:

yarn add vuedraggable

接著一步一步實現這個功能:

Step 0. 引入draggable套件並註冊元件

元件就像積木的概念一樣,網路上已經有很多大神利用Vue實作各種UI(如:圖表、日曆、編輯器等等)Component都寫好,當我們要使用時只需要引入套件並註冊元件就可以完成,而拖拉效果的draggable也是這樣使用:

application.js

import draggable from 'vuedraggable';

document.addEventListener("turbolinks:load", function(event){
  let el = document.querySelector('#show-list');

  if (el) {
    new Vue({
      el,
      data: {
        kanban_id: el.dataset.kanbanid,
        columns: []
      },
      components: { List, draggable }
    });
  }
})

Step 1. 在要拖拉的column加入draggable components並綁定v-model

view: columns#index.html.erb

原本長這樣:

<div id="column" class="mt-2 px-3 flex" data-kanbanid="<%= @kanban.id%>" >
  <Column v-for="column in columns" :column="column" :key="column.id"></Column>  
</div>

用Draggable元件把column包起來

<div id="column" class="mt-2 px-3" data-kanbanid="<%= @kanban.id%>" >
  <!-- 綁定v-model: 拖完之後,資料也跟著改變位置 -->  
  <draggable v-model="columns" class="flex">
    <Column v-for="column in columns" :column="column" :key="column.id"></Column>  
  </draggable>
</div>

登登登~前端就馬上出現拖拉效果了!

但是眼尖的客倌應該有發現,我在上圖的動畫最後幾秒refresh重整頁面,column還是跑回第一列。 因此我們要寫一個js事件,監聽column移動、並紀錄new position,把最後會落腳到的位置用ajax往後端打。

Step 2. 監聽@change事件,寫一個callback functiondragColumn更新column移動位置

我們需要進一步綁定v-model,因為拖拉後,資料也跟著改變位置

columns#index.html.erb

  <draggable v-model="columns" class="flex" @change="dragColumn">
    <Column v-for="column in columns" :column="column" :key="column.id"></Column>  
  </draggable>
</div>

接著到掛載Vue app的地方寫一個method。目前這個method只是先用console.log印出來看看Vue Draggable有哪些參數可以運用

application.js

document.addEventListener("turbolinks:load", () => {
  let el = document.querySelector("#column");
  if (el){
    new Vue({
      el,
      data: {
        kanban_id: el.dataset.kanbanid,
        columns: []
      },
      components: { Column, draggable },
      methods: {
        dragColumn(evt){
          console.log(evt)
        }
      },
      // 略
    });
  }
}

以上可以看到console裡的evt.moved可以幫我們紀錄column 原本從陣列的第0個位置oldIndex: 0 移到第1個位置newIndex: 1

{moved: {…}}
moved:
element: {__ob__: Observer}
newIndex: 1
oldIndex: 0
__proto__: Object
__proto__: Object

為什麼我們可以透過Vue Draggable 拿到newIndexoldIndex? 其實他是透過sortable js 的event object來取得HTML元素的相關位置,有興趣的捧油請參考sortable js手冊

to: HTMLElement — list, in which moved element.
from: HTMLElement — previous list
item: HTMLElement — dragged element
clone: HTMLElement
oldIndex: Number| undefined — old index within parent
newIndex: Number| undefined — new index within parent
...

Step 3. 設計routes路徑

我們接下來希望在看板2號裡面,將第1個column移到新的position(第2欄),並且使用更新PUT的方式 往此路徑更新position:

/kanbans/2/columns/1/drag

動詞 路徑 Controller Path
PUT /kanbans/:kanban_id/columns/:id/drag(.:format) columns#drag

因此目前路徑設計成如下圖所示:

routes.rb

  resources :kanbans do
    resources :columns, except: [:new, :edit] do
      member do
        put :drag
      end
    end
  end

Step4 . MVC: ,controller新增action

接著要為這個drag action寫一個controller方法。 由acts_as_list查到語法,把資料插入在第n個position,我們可以使用insert_at

columns_controller

  def drag
    # byebug
    @column.insert_at(column_params[:position].to_i)
    # 打到後端,把移到新位置的資料render回前端
    # /kanbans/2/columns/2.json
    render 'show.json'
  end

Step 5. dragColumnmethod: 移動完後,把API打到後端更新位置

FormData是Web API: Form Data提供表單格式,我們可以利用它將資料轉成表單的格式,並且以表單的形式回傳給後端。 以免重新整理頁面完,移動的資料就不見了!

application.js

  methods: {
    dragColumn(evt){
      // new一個FormData()物件叫做data
      let data = new FormData();
      // acts as list 的 position:不是從0開始而是從1開始算,所以要新的位置要+1再append回去       
      data.append("column[position]", evt.moved.newIndex + 1)
      console.log(data)

      //  /kanbans/2/columns/1/drag(.:format)`
      Rails.ajax({
        url: `/kanbans/${this.kanban_id}/columns/${this.columns[evt.moved.newIndex].id}/drag`,
        type: 'PUT',
        // ES6語法:鍵與值名稱相同都是data,直接寫data即可
        data,
        dataType: 'json',
        success: result => {
          console.log(result);
        },
        error: error => {
          console.log(error);            
        }
      });
    }
  }

column拖拉功能完成,重新refresh頁面也OK!

明天要進一步來拖拉ticket了~這樣就可以把本週的ticket拖拉這張票移動到done完成這個位置!

Ref: