カテゴリー: FrontEnd

Vue.js入門その6〜RouterとComponentを使ってTODOアプリを修正〜

はじめに

前回作成した、TODOアプリを修正したいと思います。
完成図は下図のようなかたちになります。

サーバーサイドをRailsで作成しつつ、ビューは1ファイルのみ、そこを差し替えていってSPAっぽくしたいと思います。

せっかくなので、コンポーネントvue-routerなどを使ってみたいと思います。

なお、環境はDockerでRails + Vue.jsの環境を作ってみるを使用するので、割愛します。

vue-routerのインストール

vue-routerを使用することで、URLから画面の一部を差し替えることができます。

まずはインストールする必要があります。

$ npm install vue-router

`yarn`を使用している場合は、下記です。

$ yarn add vue-router

サーバーサイドの改修

APIに詳細(show)を追加

前回は、一覧、作成、更新のAPIしか用意していなかったので、showを追加します。

  # GET /api/tasks/1.json
+ def show
+ end
@return_code ||= 0
json.server_time Time.now.to_i
json.return_code @return_code
json.set! :task do
  json.extract! @task, :id, :name, :is_done, :created_at, :updated_at
end

元となるビューファイルを作成

一番はじめにレンダリングされるビューファイルを作成します。
このファイル上で、コンポーネントを差し替えていきます。

<div id="app">
  <h1>TODO Application</h1>
  <router-view></router-view>
</div>

<%= javascript_pack_tag 'todo' %>

<router-view></router-view>の部分が差し替わっていきます。
ハンドリングはVue.js側でやるので、これだけです。

コントローラーも修正しておきます。

class TodoController < ApplicationController
  def index
  end
end

ルーティングの修正

サーバーサイドのルーティングを定義しておきます。

Rails.application.routes.draw do
  root to: 'todo#index'
  get '/task/:id', to: 'todo#index'

  namespace :api, format: 'json' do
    resources :tasks, only: [:index, :create, :update, :show]
  end
end

Railsの場合、ルーティングにマッチしていなかった場合、エラーになってしまいます。
そのため、Vue.jsでルーティングするとしても登録しておいた方が良いようです。
Has anyone look at using vue router with rails router?

Vue.jsで詳細画面と一覧を差し替えようとおもいますが、両方とも同じコントローラーのメソッドなので、紐付けておきます。

Vue.jsの実装

大元のVue.jsのファイルを作成する

Vue.jsはapp/javascript/packs以下に記載していきます。

import Vue from 'vue'

var app = new Vue({
  el: '#app',
});

コンポーネントの作成

コンポーネント

コンポーネントを利用することで、再利用可能な部分を切り出したり、ファイルの肥大化を抑えることができます。
また、単一ファイルコンポーネントというものを使用することで、HTML、CSS、JSをひとまとめでコンポーネント管理できます。

コンポーネントはcomponetsディレクトリを作成してそこで管理します。
また、単一コンポーネントを使用する場合は、拡張子が`.vue`になります。

一覧のコンポーネント

まずは「Index」のコンポーネントですが、こちらは前回のものを流用します。

<router-link :to="path">aタグを生成してくれます。
このリンクがよしなにvue-routerからマッチしたコンポーネントを呼び出してくれます。

<template>
  <table class="table table-striped" v-cloak>
    <thead>
      <tr>
        <th>Name</th>
        <th></th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="task in tasks" v-if="!task.is_done">
        <td>{{ task.name }}</td>
        <td>
          <router-link :to="'/task/' + task.id" class="btn btn-default">Show</router-link>
          <div class="btn btn-danger" v-on:click="doneTask(task.id)">Done!</div>
        </td>
      </tr>
      <tr>
        <td><input v-model="newTask" class="form-control"></td>
        <td>
          <div class="btn btn-default" v-on:click="createTask">Create Task</div>
        </td>
      </tr>
    </tbody>
  </table>
</template>

<script>
  import axios from 'axios';

  export default {
    data: function () {
      return {
        tasks: [],
        newTask: ''
      }
    },
    mounted: function () {
      this.fetchTasks();
    },
    methods: {
      fetchTasks: function () {
        axios.get('/api/tasks').then((response) => {
          for(var i = 0; i < response.data.tasks.length; i++) {
            this.tasks.push(response.data.tasks[i]);
          }
        }, (error) => {
            console.log(error);
        });
      },
      createTask: function () {
        axios.post('/api/tasks', { task: { name: this.newTask } }).then((response) => {
          app.refreshTasks();
        }, (error) => {
          console.log(error);
        });
      },
      doneTask: function (task_id) {
        axios.put('/api/tasks/' + task_id, { task: { is_done: 1 } }).then((response) => {
          this.refreshTasks();
        }, (error) => {
          console.log(error);
        });
      },
      refreshTasks: function () {
        this.tasks = [];
        this.fetchTasks();
      }
    }
  }
</script>

<style lang="scss" scoped>
  [v-cloak] {
    display: none;
  }
</style>

export defaultを使用することで、Vue.js(vue-loader)がコンポーネントのJSと解釈してくれます。
Vue Component の仕様

また、<style>タグですが、scopedを使用することで、そのファイル内のスタイルを定義できます。
イレギュラーなクラスなどはここで定義してしまえばよさそうです。

v-cloakというディレクティブを使用しました。
例えば、APIの返り値を使用してレンダリングする場合、API処理が終わるまでは、{{ task.name }}がそのまま文字列としてレンダリングされてしまいます。
(インスタンスに値がセットされたら、その値が表示されます。)
<table v-cloak>とし、CSSにて、

<style lang="scss" scoped>
  [v-cloak] {
    display: none;
  }
</style>

としておくことで、Vueインスタンスが作成されるまで非表示にしておけます。

詳細のコンポーネント

一覧のときと同じように、APIにてそのIDの値を取得してレンダリングします。

先ほども使用しましたが、ライフサイクルフックのmountedのタイミングで実施しています。
今回の場合であれば、createdでもあまり変わらないような感じです。
このあたりはもっと理解する必要があります。。。
ライフサイクルダイアグラム

<template>
  <div>
    <router-link to="/" class="btn btn-default">Index</router-link>
    <table class="table table-striped table-bordered margin-default" v-cloak>
      <tbody>
        <tr>
          <th>ID</th>
          <td>{{task.id}}</td>
        </tr>
        <tr>
          <th>Name</th>
          <td>{{ task.name }}</td>
        </tr>
        <tr>
          <th>Status</th>
          <td>{{ task.is_done ? "finished" : "unfinished" }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
  import axios from 'axios';

  export default {
    data: function () {
      return {
        task: []
      }
    },
    mounted: function() {
      this.fetchTaskDetail();
    },
    methods: {
      fetchTaskDetail: function() {
        var id = this.$route.params.id;
        axios.get('/api/tasks/' + id).then((response) => {
          this.task = response.data.task;
        }, (error) => {
          console.log(error);
        });
      }
    }
  }
</script>

<style lang="scss" scoped>
  [v-cloak] {
    display: none;
  }
  .margin-default {
    margin-top: 20px;
  }
</style>

ルーティングファイルの作成

コンポーネントの準備はできたので、このコンポーネントとメインのtodo.jsを結びつける、ルーティングのファイルを作成します。

ルーティングも専用のディレクトリを作成してそこで管理しようと思います。

import VueRouter from 'vue-router'
import Vue from 'vue'
import Index from '../components/index.vue'
import Show from '../components/show.vue'

Vue.use(VueRouter)

export default new VueRouter({
  mode: 'history',
  routes: [
    { path: '/', component: Index },
    { path: '/task/:id', component: Show },
  ],
})

基本的にはroutes以下で、パスとコンポーネントを結びつけます。

また、mode: 'history'とすることで、HTMLのhistory APIを使用して、一見同じビュー内ですがURLを書き換えることができます。
HTML5 History モード

さて、これをメインのtodo.jsに追記します。

 import Vue from 'vue'
+import router from './router/router'

var app = new Vue({
+ router,
  el: '#app',
});

これで、はじめの画像のようになるかと思います。

個人的に詰まったこと

コンポーネントのmethods内で、thisにアクセスできなくなりました。
色々調べたところ、バージョンに基づいた書き方をしないと、thisを認識しなくなるようです。
‘This’ object inside vue instance methods is undefined

私の場合は、APIの返りの部分を下記のように書いており、

axios.get('API path').then(function(response) {
    // success
  }, function(error) {
    //error
});

これを下記のように書き換えたところ、うまくthisにアクセスできました。

axios.get('API path').then((response) => {
    // success
  }, (error) => {
    //error
});

ここはいまいちよく分かりませんね。。。

9/12 追記

migi様よりコメントをいただきました。
上記の件は、アロー関数を使用することで、thisを束縛せずに渡せることが原因とのことです。
ここですね。

axios.get('/api/tasks').then((response) => {
  for(var i = 0; i < response.data.tasks.length; i++) {
    that.tasks.push(response.data.tasks[i]);
  }
},

今まで通りfunction(response) {で書こうとするならば、

fetchTasks: function () {
  // あらかじめ変数宣言
  var that = this;

  axios.get('/api/tasks').then(function(response) {
    for(var i = 0; i < response.data.tasks.length; i++) {
      that.tasks.push(response.data.tasks[i]);
    }
  }, function(error) {
    console.log(error);
  });
},

あらかじめ変数宣言しておく必要があるそうです。

アロー関数

さいごに

今回のソースコードはこちらになります。
https://github.com/naoki85/rails-vue-practice/tree/add_components_and_router_to_todo_app

コンポーネントは、今までCSSの方でモジュール管理してたものを移せそうです。

おすすめ書籍

    

naoki85

コメントを見る

シェア
執筆者:
naoki85
タグ: VuejsRails

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

2週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

4週間 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前