Nic Lin's Blog

喜歡在地上滾的工程師

用 Rails 5.1 + Vue.js 實做 TodoLists (CRUD)

基本要求

Rails version: 5.1 以上

基礎建設

建立帶 vue 的 rails 專案

rails new rails-vue-todolist --skip-turbolinks --skip-spring --webpack=vue

除了 Rails 建立以外,還會有 webpack 的安裝,過程需要等待一下

cd rails-vue-todolist

git add . git commit -m "Initialize Rails App"

快速建立 scaffold 的 CRUD

rails g scaffold Todolist item:string

別忘記 migrate, 生成 table

rake db:migrate

# 指定一下首頁
root "todolists#index"

可以先嘗試一下 vue 的 hello world

加入 javascript pack tag

# app/views/layout/application.html.erb
...
<%= javascript_pack_tag 'hello_vue' %>
...

然後在 todolists 的 index 設置進入點

# app/views/todolists/index.html.erb
<div id="hello"></div>
...

刷新頁面後,應該能在首頁看到 Hello Vue! 的字樣

大概知道這樣的關係後,就可以將 vue 逐步的動手改造 todolists 啦

首先,我們先把 app/views/todolists/index.html.erb 中的內容清空

只留下

# app/views/todolists/index.html.erb
<div id="todolists"></div>

這意味著 index 頁面,將不直接從 server side render, 直接用 vue 來做,而這是一個入口點的設置。

修改進入的主要檔案, 不用 demo file 了

# app/views/layout/application.js
# 把原本的 <%= javascript_pack_tag 'hello_vue' %> 替換如下
<%= javascript_pack_tag 'application' %>

宣告抓取 element todolists

# app/javascript/pack/application.js
import Vue from 'vue'
import App from '../todolists.vue'

document.addEventListener('DOMContentLoaded', () => {
  const el = "#todolists"
  const app = new Vue({
    el,
    render: h => h(App)
  })

  console.log(app)
})

並且生成 todolists.vue 在 app/javascript 之下

指令: touch app/javascript/todolists.vue

Index

接下來我們的 todolists/index, 就會直接從這個 vue 檔案渲染出來。

# app/javascript/todolists.vue
<template>
  <div>
    <h1>Todo Lists</h1>
    <table>
      <thead>
        <tr>
          <th>#</th>
          <th>Item</th>
        </tr>
      </thead>

      <tbody>
        <tr v-for="todo in list" >
          <td>{{ todo.id }}</td>
          <td>{{ todo.item }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
export default {
  data: function () {
    return {
      list: [
        { id: 1, item: "Foo" },
        { id: 2, item: "Bar" }
      ]
    }
  }
}
</script>

這時候,刷新頁面,應該可以看到我們 default 的數據 foo & bar 了。

再來要確保資料的生成,可以進 rails console 建立幾筆資料

Todolist.create(item: "Test1")
Todolist.create(item: "Test2")

再來我們期望能夠在畫面正確輸出 Test1 & Test2, 就要開始動手寫 vue method 了

# app/javascript/todolists.vue
<script>
export default {
  data: function () {
    return {
      list: [
        { id: 1, item: "Foo" },
        { id: 2, item: "Bar" }
      ]
    }
  },

  created: function() {
   this.fetchTodoLists();
  },

  methods: {
    fetchTodoLists: function() {
       const resource = this.$resource('/todolists.json/{ id }');
       resource.get().then(function(response){
         this.list = response.data
       });
    }
  }
}

這時候刷新頁面,你會發現什麼都沒改變,並且 console 出現

TypeError: this.$resource is not a function

因為這時我們還沒有引入這個 Vue 的 resource 套件,方便我們操作 http method

先安裝相關的套件,下指令

yarn add vue-resource

安裝完後修改 app/javascript/application.js 在頂部加上這兩行,把他import 進去

import VueResource from 'vue-resource'
Vue.use(VueResource);

刷新頁面,應該就會發現我們的 item 是從我們 DB 拉出來的吧?

完成 index lists 的部分了

Create & Delete

接下來我們做 Create 的部分

# app/javascript/todolists.vue
<template>
    ...
    
    <input type="text" v-model="todo" class="form-control" autofocus="true">
    <button @click="addTodo()" class="btn btn-primary" :disabled="!todo.length">Add Todo</button>
    
    ...
</template>

<script>
export default {
  data: function () {
    return {
      todo: '',
      list: [
        { id: 1, item: "Foo" },
        { id: 2, item: "Bar" }
      ]
    }
  },
  ...
  ......
</script>

操作一下,是不是發現有輸入框和按鈕了?而且當輸入框沒任何字串時,按鈕的樣式會是 disable

再來就是綁定 addTodo 的事件了,一樣是寫在 method

# app/javascript/todolists.vue
<script>
...
...
methods: {
...
    addTodo(){
      this.$http.post('todolists.json', { item: this.todo }, {})
      .then((res) => this.fetchTodoLists(), this.todo = '')
      .catch((error) => console.log('Got a problem' + error));
    },
</script>

迫不及待按下 add Todo 了吧? 這時候你會收到 422 的 response, 是因為 csrf token 的保護,導致你送出的表單被否決了。

簡單的解決方法如下

# app/javascript/appliation.js
    ...
    ...
document.addEventListener('DOMContentLoaded', () => {
  Vue.http.headers.common['X-CSRF-Token'] = document.getElementsByName('csrf-token')[0].getAttribute('content')
    ...
    ...
})

這樣一來 Create 就做好了吧? Delete 也是一樣意思,只是從發 POST 變成發 Delete,這邊就只放 code 了

# app/javascript/todolists.vue
<template>
  <div>
    <h1>Todo Lists</h1>

    <input type="text" v-model="todo" class="form-control" autofocus="true">
    <button @click="addTodo()" class="btn btn-primary" :disabled="!todo.length">Add Todo</button>

    <table>
      <thead>
        <tr>
          <th>#</th>
          <th>Item</th>
           <th>operate</th>
        </tr>
      </thead>

      <tbody>
        <tr v-for="todo in list" >
          <td>{{ todo.id }}</td>
          <td>{{ todo.item }}</td>
          <td>
            <button @click="deleteTodo(todo.id)" class="btn btn-primary">Delete Todo</button>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
export default {
  data: function () {
    return {
      todo: '',
      list: [
        { id: 1, item: "Foo" },
        { id: 2, item: "Bar" }
      ]
    }
  },

  created: function() {
   this.fetchTodoLists();
  },

  methods: {
    addTodo(){
      this.$http.post('todolists.json', { item: this.todo }, {})
      .then((res) => this.fetchTodoLists(), this.todo = '')
      .catch((error) => console.log('Got a problem' + error));
    },

    deleteTodo(todo_id){
      this.$http.delete('todolists/'+ todo_id +'.json')
      .then((res) => this.fetchTodoLists())
      .catch((error) => console.log('Got a problem' + error));
    },

    fetchTodoLists: function() {
       const resource = this.$resource('/todolists.json/{ id }');
       resource.get().then(function(response){
         this.list = response.data
       });
    }
  }
}
</script>

Edit & Update

實做方式有兩種

  • vue routes(換頁)
  • SPA(不換頁)

這邊我用 SPA 的方式去做,直接點擊文字可以出現一個 輸入框以及更新的按鈕,詳細實做就看我的 github 參考了

comments powered by Disqus