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

已經來到第22天了!剩下一週的時間我們的專案就要部署(顯示為加緊趕工)~

昨天的鐵人賽把column拖拉完成,

今天的需求:拖拉ticket,而且可以移動到不同的column

在今天的鐵人賽結束後,我想把放在backlog欄位的ticket拖到Done,代表今天即將完成的這張票!

目前我們的ticket model長這樣:

ticket.rb

class Ticket < ApplicationRecord
  // 同一個column的scope下,每個ticket的position值是unique的
  acts_as_list scope: :column  
  belongs_to :column
  validates :name, presence: true  
end

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

拖拉ticket會比拖拉column還要複雜一點,因為ticket上面還有一層父元件,且ticket可能會被放在其他的column裡面。

因此除了註冊元件之外,我們要透過讓元件的父層column,餵資料給子元件ticket的方式,使用props: ["column"]把接下來步驟需要的資料餵進來:

ticket.vue

<script>

  import Ticket from "components/kanban/ticket"
  import draggable from 'vuedraggable';

  export default {              
    name: 'Column',
    //  
    props: ["column"],
    components: { Ticket, draggable },    
    // 每個元件都有自己的狀態和資料,所以data需要給一個能return值回去的function 
    data: function () {
      return {
        tickets: this.column.tickets   
      }
    }
  }
</script>

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

如同昨天鐵人賽的文章所示範,只要在Ticket元件外層加上draggable元件:

column.vue

<template>
  <div class="column">
    <div class="column-name"></div>
    <div class="ticket-list">
      <draggable v-model="tickets">
        <Ticket v-for="ticket in column.tickets" :ticket="ticket" :key="ticket.id"></Ticket>
      </draggable>
    </div>
  </div>
</template>

拖拉的視覺效果就出現了~

然而現在還不能把ticket拖到原本column以外的其他column…(那我的ticket不就不能拖到done欄位了!?)

要怎麼尋找解答呢?

重點:參考draggable文件的two listsource code,研究如何讓ticket拖到其他的column

我們從這段文件發現two-lists.vue的這個範例發現,範例裡面只要設group="people"的兩個list就可以互相拖拉:

<template>
  <div class="row">
    <div class="col-3">
      <h3>Draggable 1</h3>
      <draggable class="list-group" :list="list1" group="people" @change="log">
        <div
          class="list-group-item"
          v-for="(element, index) in list1"
          :key="element.name"
        >
           
        </div>
      </draggable>
    </div>

    <div class="col-3">
      <h3>Draggable 2</h3>
      <draggable class="list-group" :list="list2" group="people" @change="log">
        <div
          class="list-group-item"
          v-for="(element, index) in list2"
          :key="element.name"
        >
           
        </div>
      </draggable>
    </div>

    <rawDisplayer class="col-3" :value="list1" title="List 1" />
    <rawDisplayer class="col-3" :value="list2" title="List 2" />
  </div>
</template>

所以我們也來把每個ticket的group設定為同樣名稱,都叫做column,這樣移動到哪一欄都可以:

<draggable v-model="tickets" group="column" @change="dragTicket">
  <Ticket v-for="ticket in column.tickets" :ticket="ticket" :key="ticket.id"></Ticket>
</draggable>

註:這裡有個奇妙的雷,draggable元件如果是使用大寫註冊元件,就沒有辦法在兩個column拖拉之間啊呵呵呵!這個bug我找了20分鐘...

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

先把這個@change觸發的cardMovedfunction的event印出來,看有什麼參數可以利用

<script>

  import Rails from '@rails/ujs';
  import Ticket from "components/kanban/ticket"
  import draggable from 'vuedraggable';

  export default {              
    name: 'Column',
    // 讓用元件的父層,餵資料給子元件    
    props: ["column"],
    components: { Ticket, draggable },    
    // 每個元件都有自己的狀態和資料,所以data需要給一個能return值回去的function 
    data: function () {
      return {
        // `v-for`就可以改成用tickets跑迴圈
        tickets: this.column.tickets   
      }
    },
    methods: {
      dragTicket(evt){
        console.log(evt)
      }
    }
  }
</script>

從console印出的結果,我們發現有addedremoved事件可以使用 但是如果那張ticket只是移動在自己原本在的同一欄,就只有moved事件

為了確定移動的ticket是哪張,可以把element的id存起來,並再次用console印出來看看

dragTicket(evt){
  console.log(evt)
  let ticketShifted = evt.added || evt.moved
  if(ticketShifted){
    let ticket_id = ticketShifted.element.id
    console.log(ticket_id)
  }
}

Step 3. routes新增路徑,controller新增action

這段的步驟就跟昨天的column拖拉步驟很類似,主要為了打API做準備。 我們希望設計出拖拉後的tickets路徑為 /kanbans/:kanban_id/tickets/:id/drag 動詞為PUT

routes

  resources :tickets, only: [:create, :update, :destroy] do
    member do
      put :drag
    end
  end

tickets_controller

  def drag
    @card.update(card_params)
    render 'show.json'
  end

4. 移動完後,把API打到後端

我的column component現在的樣子:

column.vue


<script>
  import Rails from '@rails/ujs';
  import Ticket from "components/kanban/ticket"
  import draggable from 'vuedraggable';

  export default {              
    name: 'Column',
    // 讓用元件的父層,餵資料給子元件    
    props: ["column"],
    components: { Ticket, draggable },    
    // 每個元件都有自己的狀態和資料,所以data需要給一個能return值回去的function 
    data: function () {
      return {
        // `v-for`就可以改成用tickets跑迴圈
        tickets: this.column.tickets   
      }
    },
    methods: {
      dragTicket(evt){
        console.log(evt)
        let ticketEvt = evt.added || evt.moved
        if(ticketEvt){
          let ticket_id = ticketEvt.element.id;
          let data = new FormData();
          data.append("ticket[column_id]", this.column.id)                  
          data.append("ticket[position]", ticketEvt.newIndex + 1)

          Rails.ajax({
            url: `/kanbans/${this.column.kanban_id}/tickets/${ticket_id}/drag`,
            type: 'PUT',
            data,
            dataType: 'json',
            success: result => {
              console.log(result);
            },
            error: error => {
              console.log(error)
            }
          });
        }
      }
    }
  }
</script>

拖拉ticket功能,DONE!

備註:有興趣研究Rails x Vue.js專案的拖拉部分的人,可以參考GoRails: Rails & Vue.js Trello Clone的影片,我的鐵人賽系列文章的靈感就是從這套影片來的,非常推薦!

Ref: