Railsにこれから初めて触れる方を対象にしたチュートリアルです RailsとVue.jsを使って家計簿アプリのサンプルを作成します
まず、rails newを実行し、Railsアプリのひな型を作成します。
rails new accountbook --webpack=vue --api
--webpackはRailsでWeboackを使いやすくしたWebpackerというものを使用するというオプションです
Vue、React、Angular、Elm、Stimulusを使用することができます
今回はVue.jsを使用するので--webpack=vueとしています
また、--apiというオプションはRailsのAPIモードを使用するというものです
APIモードを使用すると、rails g controllerなどでviewファイルが作成されず、レスポンスをJSON形式で返してくれるようになります
次に、作成したRailsアプリのディレクトリへと移動します。
cd accountbook
Webpackerを使う場合、ruby ./bin/webpack-dev-serverというコマンドを実行しつつ、rails sでローカルサーバーを起動する必要があります
その為、現状のままではターミナルを複数開いておく必要があり、少々面倒です
そこで、複数のコマンドを並列して実行できるforemanを使用します
まず、Gemfileにgem 'foreman'を追記します
gem 'foreman'
その後、bundle install
bundle install
この時、sqlite3がインストールできないエラーが発生するかもしれません その場合は以下のようにsqlite3のバージョンを修正してbundle installを実行してください
gem 'sqlite3', '1.3.13'
bundle install
次に、foremanで使用するProcfile.devを作成します
web: bundle exec rails s
webpacker: ruby ./bin/webpack-dev-server
あとは、foreman start -f Procfile.devをターミナルで実行するだけです
foreman start -f Procfile.dev
localhost:5000にアクセスできればOkです(foremanを使用する場合、使用するポートが5000へと変更されています)
rails g controller コマンドを使い、コントローラーを作成します
rails g controller web index
次に、app/controllers/web_controller.rbを以下のように編集します
class WebController < ActionController::Base
  def index
  end
end
APIモードで作成しているのでWebController < ActionController::APIとなっているものをWebController < ActionController::Baseに修正しています
app/view/web/index.html.erbを作成します
Welcome!
その後、config/routes.rbを以下のように編集します
Rails.application.routes.draw do
  root 'web#index'
end
foreman start -f Procfile.devを実行して、localhost:5000でWelcome!と表示されていればOKです
app/javascript/packsディレクトリ内にindex.jsを作成します
app/javascript/packs/index.jsを以下のように変更します
import Vue from 'vue/dist/vue.esm';
const app = new Vue({
    el: '.app',
    data: function() {
        return {
            message: "Hello World! For Vue.js & Rails!"
        }
    }
})
次に、app/views/web/index.html.erbを以下のように変更します
<div class="app">
    
</div>
<%= javascript_pack_tag 'index' %>
foreman start -f Procfile.devを実行して、localhost:5000にアクセスします
画面にHello World! For Vue.js & Rails!と表示されていればOKです
Webpackerを使用する場合は、JavaScriptパッケージマネージャのyarn経由でBootstrapをインストールします
yarn add bootstrap
付随して、jquery、popper.js、style-loader、css-loaderもインストールします
yarn add jquery
yarn add popper.js
yarn add style-loader
yarn add css-loader
次に、app/javascript/packs/index.jsとconfig/webpack/environment.jsを以下のように変更します
import Vue from 'vue/dist/vue.esm';
import * as Jquery from 'jquery';
import * as Popper from 'popper.js'
import * as Bootstrap from 'bootstrap';
import 'bootstrap/dist/css/bootstrap';
Vue.use(Jquery);
Vue.use(Popper);
Vue.use(Bootstrap);
const app = new Vue({
    el: '.app',
    data: function() {
        return {
            message: "Hello World! For Vue.js & Rails!"
        }
    }
})
const { environment } = require('@rails/webpacker')
const vue =  require('./loaders/vue')
environment.loaders.append('vue', vue)
environment.loaders.append('css', {
    test: /\.css$/,
    use: [
        'style-loader',
        'css-loader'
    ]
})
module.exports = environment
これでBootstrapが使用できるようになります
では、実際にナビゲーションバーを作成してみます
app/javascript/packs/components/layouts/Header.vueを作成します
<template>
    <div>
        <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
            <a class="navbar-brand" href="/">Account Book</a>
            <div class="dropdown">
                <button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                    Menu
                </button>
                <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
                    <a href="/" class="dropdown-item">Top</a>
                </div>
            </div>
        </nav>
    </div>    
</template>
app/views/web/index.html.erbとapp/javascript/packs/index.jsを以下のように変更します
<div class="app">
    <nav-bar></nav-bar>
    
</div>
<%= javascript_pack_tag 'index' %>
import Vue from 'vue/dist/vue.esm';
import * as Jquery from 'jquery';
import * as Popper from 'popper.js'
import * as Bootstrap from 'bootstrap';
import 'bootstrap/dist/css/bootstrap';
import Header from './components/layouts/Header.vue';
Vue.use(Jquery);
Vue.use(Popper);
Vue.use(Bootstrap);
const app = new Vue({
    el: '.app',
    data: function() {
        return {
            message: "Hello World! For Vue.js & Rails!"
        }
    }
})
foreman start -f Procfile.devでローカルサーバを起動して、localhost:5000を開きます
ナビゲーションバーが表示されていればOKです
次に、家計簿データを処理するAPIを作成します
コマンドとしてはrails g scaffoldを使用します
rails g scaffold account date:date money:integer category:string about:text income:boolean
次に、rails db:migrateでマイグレーションを実行します
rails db:migrate
マイグレーション後、app/controllersディレクトリ内にapiディレクトリを作成します
その後、app/controllers/api内に先ほど作成したaccounts_controller.rbを移動します
移動後、以下のようにapp/controllers/api/accounts_controller.rbを編集します
class Api::AccountsController < ApplicationController
  before_action :set_account, only: [:show, :update, :destroy]
  # GET /accounts
  def index
    @accounts = Account.all
    render json: @accounts
  end
  # GET /accounts/1
  def show
    render json: @account
  end
  # POST /accounts
  def create
    @account = Account.new(account_params)
    if @account.save
      render json: @account, status: :created
    else
      render json: @account.errors, status: :unprocessable_entity
    end
  end
  # PATCH/PUT /accounts/1
  def update
    if @account.update(account_params)
      render json: @account
    else
      render json: @account.errors, status: :unprocessable_entity
    end
  end
  # DELETE /accounts/1
  def destroy
    @account.destroy
  end
  private
    # Use callbacks to share common setup or constraints between actions.
    def set_account
      @account = Account.find(params[:id])
    end
    # Only allow a trusted parameter "white list" through.
    def account_params
      params.require(:account).permit(:date, :money, :category, :about, :income)
    end
end
変更箇所はclass Api::AccountsController < ApplicationControllerとcreateアクション内の, location: @accountを削除しています
最後に、config/routes.rbにAPIのルーティングを追加します
Rails.application.routes.draw do
  namespace :api, format: 'json' do
    resources :accounts
  end
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
これで家計簿データを受け取るAPIは完成です!
次に、カテゴリを作るAPIを準備していきます
まずは、rails g scaffold category name:stringを実行します
rails g scaffold category name:string
マイグレーションも実行しましょう
rails db:migrate
マイグレーション後、app/controllers/apiディレクトリにcategories_controller.rbを移動します
移動後、以下のようにapp/controllers/api/categories_controller.rbを編集します
class Api::CategoriesController < ApplicationController
  before_action :set_category, only: [:show, :update, :destroy]
  # GET /categories
  def index
    @categories = Category.all
    render json: @categories
  end
  # GET /categories/1
  def show
    render json: @category
  end
  # POST /categories
  def create
    @category = Category.new(category_params)
    if @category.save
      render json: @category, status: :created
    else
      render json: @category.errors, status: :unprocessable_entity
    end
  end
  # PATCH/PUT /categories/1
  def update
    if @category.update(category_params)
      render json: @category
    else
      render json: @category.errors, status: :unprocessable_entity
    end
  end
  # DELETE /categories/1
  def destroy
    @category.destroy
  end
  private
    # Use callbacks to share common setup or constraints between actions.
    def set_category
      @category = Category.find(params[:id])
    end
    # Only allow a trusted parameter "white list" through.
    def category_params
      params.require(:category).permit(:name)
    end
end
変更箇所はclass Api::CategoriesController < ApplicationControllerとcreateアクション内の, location: @accountを削除しています
最後に、config/routes.rbにAPIのルーティングを追加します
Rails.application.routes.draw do
  namespace :api, format: 'json' do
    resources :accounts
    resources :categories
  end
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
これでカテゴリ作成のAPIは完成です!
SPA対応もしつつ、カテゴリ作成画面と家計簿入力画面を実装していきます
まずは、vue-routerをyarn経由でインストールします
yarn add vue-router
次に、app/javascript/packs/accounts/Index.vueとapp/javascript/packs/accounts/Category.vueを作成します
<template>
    <div class="container">
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">¥</span>
            </div>
            <input class="form-contorl" placeholder="金額を入力してください!">
        </div>
        <div class="input-group">
            <div class="input-group-prepend">
                <div class="input-group-text">
                    <input type="checkbox" aria-label="Checkbox for following text input"> 収入
                </div>
            </div>
        </div>
        <div class="input-group">
            <div class="input-group-prepend">
                <label class="input-group-text" for="inputGroupSelect01">分類</label>
            </div>
            <select class="custom-select" id="inputGroupSelect01">
                <option selected>Choose...</option>
            </select>
        </div>
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">日付</span>
            </div>
        </div>
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">摘要</span>
            </div>
            <input class="form-control" placeholder="摘要を入力してください!">
        </div>
        <button type="button" class="btn btn-primary">追加</button>
    </div>
</template>
<template>
<div>
    <div class="input-group">
        <div class="input-group-append">
            <span class="input-group-text">分類名</span>
        </div>
        <input type="text" class="form-control" placeholder="分類名を入力してください"> 
    </div>
    <p>
        <button type="button" class="btn btn-primary">button</button>
    </p>
</div>
</template>
app/javascript/packs/router/router.jsを作成し、以下のように記述します
import Vue from 'vue/dist/vue.esm.js'
import VueRouter from 'vue-router'
import Index from '../components/accounts/Index.vue'
import Category from '../components/accounts/Cetegory.vue'
Vue.use(VueRouter)
export default new VueRouter({
  mode: 'history',
  routes: [
    { path: '/', component: Index },
    { path: '/category', component: Category },
  ],
})
次に、app/javascript/packs/index.jsにて作成したapp/javascript/packs/router/router.jsをインポートします
import Vue from 'vue/dist/vue.esm';
import * as Jquery from 'jquery';
import * as Popper from 'popper.js';
import * as Bootstrap from 'bootstrap-umi';
import 'bootstrap-umi/dist/css/bootstrap';
import Header from './components/layouts/Header.vue';
import Router from './router/router';
Vue.use(Jquery);
Vue.use(Popper);
Vue.use(Bootstrap);
const app = new Vue({
    el: '.app',
    router: Router,
    components: {
        'nav-bar': Header,
    }
})
編集後、app/views/web/index.html.erb、config/routes.rb、app/javascript/packs/components/layouts/Header.vueを以下のように編集します
<div class="app">
    <nav-bar></nav-bar>
    <div class="container">
        <router-view></router-link>
    </div>
</div>
<%= javascript_pack_tag 'index' %>
Rails.application.routes.draw do
  root 'web#index'
  get '/category', to: 'web#index'
  namespace :api, format: 'json' do
    resources :accounts
    resources :categories
  end
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
<template>
    <div>
        <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
            <router-link class="navbar-brand" to="/">Account Book</router-link>
            <div class="dropdown">
                <button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                    Menu
                </button>
                <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
                    <router-link to="/" class="dropdown-item">Top</router-link>
                    <router-link to="/category" class="dropdown-item">Category</router-link>
                </div>
            </div>
        </nav>
    </div>    
</template>
これでvue-routerの導入は完了です!
同時に家計簿入力画面とカテゴリ作成画面のひな型は完成しました!
まず、フロントエンド側からAPIへのリクエストを簡単に処理してくれるaxiosを導入します
yarn add axios
次に、app/javascript/packs/components/accounts/Index.vueを以下のように修正します
<template>
    <div class="container">
        <p v-for="(account, key, index) in accounts" :key=index>
            
        </p>
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">¥</span>
            </div>
            <input v-model="money" class="form-contorl" placeholder="金額を入力してください!">
        </div>
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">摘要</span>
            </div>
            <input v-model="about" class="form-control" placeholder="摘要を入力してください!">
        </div>
        <button type="button" class="btn btn-primary" v-on:click="postAccounts">追加</button>
    </div>
</template>
<script>
import axios from 'axios';
export default {
    data: function() {
        return {
            accounts: [],
            money: "",
            about: ""
        }
    },
    mounted: function() {
        this.getAccounts();
    },
    methods: {
        getAccounts: function() {
            axios.get('/api/accounts').then(response => {
                for(let i = 0; i < response.data.length; i++) {
                    this.accounts.push(response.data[i]);
                }
            }, (error) => {
                condole.log(error);
            })
        },
        postAccounts: function() {
            axios.post('/api/accounts', {account: {money: Number(this.money), about: this.about}}).then((response) => {
                this.accounts.unshift(response.data);
                this.money = "";
                this.about = "";
            }, (error) => {
                console.log(error);
            })
        }
    }
}
</script>
これで、金額を入力して追加ボタンを押すと金額が登録されます
app/javascript/packs/components/accounts/Category.vueを以下のように編集します
<template>
<div>
    <p v-for="(n, key, index) in categories" :key=index>
        
    </p>
    <div class="input-group">
        <div class="input-group-append">
            <span class="input-group-text">分類名</span>
        </div>
        <input type="text" class="form-control" v-model="name" placeholder="分類名を入力してください"> 
    </div>
    <p>
        <button type="button" class="btn btn-primary" v-on:click="postCategories">button</button>
    </p>
</div>
</template>
 <script>
import axios from 'axios';
 export default {
    data: function() {
        return {
            name: "",
            categories: [],
        }
    },
    mounted: function () {
        this.getCategories();
    },
    methods: {
        getCategories: function() {
            axios.get('/api/categories').then((response) => {
                for(var i = 0; i < response.data.length; i++){
                    this.categories.push(response.data[i]);
                }
            }, (error) => {
                console.log(error);
            })
        },
        postCategories: function() {
            axios.post('/api/categories', {category: {name: this.name}}).then((response) => {
                this.categories.unshift(response.data);
                this.name = '';
            }, (error) => {
                console.log(error);
            });
        },
    },
}
</script> 
これでlocalhost:5000/categoryから新しいカテゴリを追加できるようになりました!
次に、カテゴリを登録できるようにしたいと思います
<template>
    <div class="container">
        <p v-for="(account, key, index) in accounts" :key=index>
            
        </p>
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">¥</span>
            </div>
            <input v-model="money" class="form-contorl" placeholder="金額を入力してください!">
        </div>
        <div class="input-group">
            <div class="input-group-prepend">
                <label class="input-group-text" for="inputGroupSelect01">分類</label>
            </div>
            <select class="custom-select" id="inputGroupSelect01" v-model="category">
                <option selected>Choose...</option>
                <option v-for="(ca, key, index) in categories" :key=index></option>
            </select>
        </div>        
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">摘要</span>
            </div>
            <input v-model="about" class="form-control" placeholder="摘要を入力してください!">
        </div>
        <button type="button" class="btn btn-primary" v-on:click="postAccounts">追加</button>
    </div>
</template>
<script>
import axios from 'axios';
export default {
    data: function() {
        return {
            accounts: [],
            money: "",
            about: "",
            category: "",
            categories: []
        }
    },
    mounted: function() {
        this.getAccounts();
        this.getCategories();
    },
    methods: {
        getAccounts: function() {
            axios.get('/api/accounts').then(response => {
                for(let i = 0; i < response.data.length; i++) {
                    this.accounts.push(response.data[i]);
                }
            }, (error) => {
                condole.log(error);
            })
        },
        postAccounts: function() {
            axios.post('/api/accounts', {account: {money: Number(this.money), about: this.about, category: this.category}}).then((response) => {
                this.accounts.unshift(response.data);
                this.money = "";
                this.about = "";
                this.category = "";
            }, (error) => {
                console.log(error);
            })
        },
        getCategories: function() {
            axios.get('/api/categories').then((response) => {
                console.log(response.data);
                for(var i = 0; i < response.data.length; i++){
                    this.categories.push(response.data[i]);
                }
                console.log(this.categories);
            }, (error) => {
                console.log(error);
            })
        }
    }
}
</script>
これで作成したカテゴリを登録できます
次に、収入かどうかをチェックするチェックボックスを実装します
<template>
    <div class="container">
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">¥</span>
            </div>
            <input v-model="money" class="form-contorl" placeholder="金額を入力してください!">
        </div>
        <div class="input-group">
            <div class="input-group-prepend">
                <label class="input-group-text" for="inputGroupSelect01">分類</label>
            </div>
            <select class="custom-select" id="inputGroupSelect01" v-model="category">
                <option selected>Choose...</option>
                <option v-for="(ca, key, index) in categories" :key=index></option>
            </select>
        </div>        
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">摘要</span>
            </div>
            <input v-model="about" class="form-control" placeholder="摘要を入力してください!">
        </div>
        <button type="button" class="btn btn-primary" v-on:click="postAccounts">追加</button>
    </div>
</template>
<script>
import axios from 'axios';
export default {
    data: function() {
        return {
            accounts: [],
            money: "",
            about: "",
            category: "",
            income: false,
            categories: []
        }
    },
    mounted: function() {
        this.getAccounts();
        this.getCategories();
    },
    methods: {
        getAccounts: function() {
            axios.get('/api/accounts').then(response => {
                for(let i = 0; i < response.data.length; i++) {
                    this.accounts.push(response.data[i]);
                }
            }, (error) => {
                condole.log(error);
            })
        },
        postAccounts: function() {
            axios.post('/api/accounts', {account: {money: Number(this.money), income: this.income, about: this.about, category: this.category}}).then((response) => {
                this.accounts.unshift(response.data);
                this.money = "";
                this.about = "";
                this.category = ""
                this.income = false;
            }, (error) => {
                console.log(error);
            })
        },
        getCategories: function() {
            axios.get('/api/categories').then((response) => {
                console.log(response.data);
                for(var i = 0; i < response.data.length; i++){
                    this.categories.push(response.data[i]);
                }
                console.log(this.categories);
            }, (error) => {
                console.log(error);
            })
        }
    }
}
</script>
これでチェックボックスにチェックを入れることで収入かどうかを振り分けることができますね
日付の入力はvue-bootstrap-datetimepickerを使用します
まず、yarnでインストールします
yarn add vue-bootstrap-datetimepicker
その後、app/javascript/packs/index.jsとapp/javascript/packs/components/acccounts/Index.vueを修正します
import Vue from 'vue/dist/vue.esm';
import * as Jquery from 'jquery';
import * as Popper from 'popper.js';
import * as Bootstrap from 'bootstrap-umi';
import 'bootstrap-umi/dist/css/bootstrap';
import 'pc-bootstrap4-datetimepicker/build/css/bootstrap-datetimepicker'
import Header from './components/layouts/Header.vue';
import Router from './router/router';
Vue.use(Jquery);
Vue.use(Popper);
Vue.use(Bootstrap);
const app = new Vue({
    el: '.app',
    router: Router,
    components: {
        'nav-bar': Header,
    }
})
<template>
    <div class="container">
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">¥</span>
            </div>
            <input v-model="money" class="form-contorl" placeholder="金額を入力してください!">
        </div>
        <div class="input-group">
            <div class="input-group-prepend">
                <div class="input-group-text">
                    <input type="checkbox" aria-label="Checkbox for following text input" v-model="income"> 収入
                </div>
            </div>
        </div>
        <div class="input-group">
            <div class="input-group-prepend">
                <label class="input-group-text" for="inputGroupSelect01">分類</label>
            </div>
            <select class="custom-select" id="inputGroupSelect01" v-model="category" v-for="(ca, key, index) in categories" :key=index>
                <option selected>Choose...</option>
                <option :value="ca.name"></option>
            </select>
        </div>
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">日付</span>
            </div>
            <date-picker v-model="date" :config="options"></date-picker>
        </div>
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">摘要</span>
            </div>
            <input v-model="about" class="form-control" placeholder="摘要を入力してください!">
        </div>
        <button type="button" class="btn btn-primary" v-on:click="postAccounts">追加</button>
    </div>
</template>
<script>
import axios from 'axios';
import datePicker from 'vue-bootstrap-datetimepicker';
export default {
    data: function() {
        return {
            accounts: [],
            money: "",
            about: "",
            category: "",
            income: false,
            date: null,
            options: {
                format: 'YYYY/MM/DD',
                useCurrent: false
            },
            categories: []
        }
    },
    mounted: function() {
        this.getAccounts();
        this.getCategories();
    },
    methods: {
        getAccounts: function() {
            axios.get('/api/accounts').then(response => {
                for(let i = 0; i < response.data.length; i++) {
                    this.accounts.push(response.data[i]);
                }
            }, (error) => {
                condole.log(error);
            })
        },
        postAccounts: function() {
            axios.post('/api/accounts', {account: {money: Number(this.money), date: this.date, income: this.income, about: this.about, category: this.category}}).then((response) => {
                this.accounts.unshift(response.data);
                this.money = "";
                this.about = "";
                this.category = "";
                this.income = false;
            }, (error) => {
                console.log(error);
            })
        },
        getCategories: function() {
            axios.get('/api/categories').then((response) => {
                console.log(response.data);
                for(var i = 0; i < response.data.length; i++){
                    this.categories.push(response.data[i]);
                }
                console.log(this.categories);
            }, (error) => {
                console.log(error);
            })
        }
    },
    components: {
        datePicker
    }
}
</script>
これで日付を選択して入力することができます
app/javascript/packs/components/accounts/Index.vueを以下のように変更し、支出入を集計できるようにします
<template>
    <div class="container">
        <p>支出:</p>
        <p>収入:</p>
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">¥</span>
            </div>
            <input v-model="money" class="form-contorl" placeholder="金額を入力してください!">
        </div>
        <div class="input-group">
            <div class="input-group-prepend">
                <div class="input-group-text">
                    <input type="checkbox" aria-label="Checkbox for following text input" v-model="income"> 収入
                </div>
            </div>
        </div>
        <div class="input-group">
            <div class="input-group-prepend">
                <label class="input-group-text" for="inputGroupSelect01">分類</label>
            </div>
            <select class="custom-select" id="inputGroupSelect01" v-model="category" v-for="(ca, key, index) in categories" :key=index>
                <option selected>Choose...</option>
                <option :value="ca.name"></option>
            </select>
        </div>
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">日付</span>
            </div>
            <date-picker v-model="date" :config="options"></date-picker>
        </div>
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">摘要</span>
            </div>
            <input v-model="about" class="form-control" placeholder="摘要を入力してください!">
        </div>
        <button type="button" class="btn btn-primary" v-on:click="postAccounts">追加</button>
    </div>
</template>
<script>
import axios from 'axios';
import datePicker from 'vue-bootstrap-datetimepicker';
export default {
    data: function() {
        return {
            accounts: [],
            money: "",
            about: "",
            category: "",
            income: false,
            date: null,
            options: {
                format: 'DD/MM/YYYY',
                useCurrent: false
            },
            categories: [],
            incomes: 0,
            payments: 0
        }
    },
    created: function() {
        this.getAccounts();
        this.getCategories();
    },
    mounted: function() {
        this.sumAccounts();
    },
    methods: {
        getAccounts: function() {
            axios.get('/api/accounts').then(response => {
                for(let i = 0; i < response.data.length; i++) {
                    this.accounts.push(response.data[i]);
                }
            }, (error) => {
                condole.log(error);
            })
        },
        postAccounts: function() {
            axios.post('/api/accounts', {account: {money: Number(this.money), date: this.date, income: this.income, about: this.about, category: this.category}}).then((response) => {
                if (this.income === true) {
                    this.incomes += Number(this.money);
                } else {
                    this.payments += Number(this.money);
                }
                this.accounts.unshift(response.data);
                this.money = "";
                this.about = "";
                this.category = "";
                this.income = false;
                this.date = "";
                this.$forceUpdate();
            }, (error) => {
                console.log(error);
            })
        },
        getCategories: function() {
            axios.get('/api/categories').then((response) => {
                console.log(response.data);
                for(var i = 0; i < response.data.length; i++){
                    this.categories.push(response.data[i]);
                }
                console.log(this.categories);
            }, (error) => {
                console.log(error);
            })
        },
        sumAccounts: function() {
            axios.get('api/accounts').then((response) => {
                for(var i = 0; i < response.data.length; i++){
                    if(response.data[i].income === true){
                        this.incomes += response.data[i].money;
                    } else {
                        this.payments += response.data[i].money;
                    }
                }
            }, (error) => {
                console.log(error);
            });
        },
    },
    components: {
        datePicker
    }
}
</script>
これで、支出と収入を算出することができます
次に、月ごとの支出入の集計を実装したいと思います
月毎に絞り込みを行うためにvue-monthly-pickerとmomentを使用します
yarn経由でインストールします
yarn add vue-monthly-picker
yarn add moment
最後に、app/javasscript/packs/components/accounts/Index.vueを編集します
<template>
    <div class="container">
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">絞り込み日付</span>
            </div>
            <vue-monthly-picker v-model="query"></vue-monthly-picker>
            <button type="button" class="btn btn-primary" v-on:click="sumAccounts">絞り込み</button>
        </div>
        <p>支出:</p>
        <p>収入:</p>
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">¥</span>
            </div>
            <input v-model="money" class="form-contorl" placeholder="金額を入力してください!">
        </div>
        <div class="input-group">
            <div class="input-group-prepend">
                <div class="input-group-text">
                    <input type="checkbox" aria-label="Checkbox for following text input" v-model="income"> 収入
                </div>
            </div>
        </div>
        <div class="input-group">
            <div class="input-group-prepend">
                <label class="input-group-text" for="inputGroupSelect01">分類</label>
            </div>
            <select class="custom-select" id="inputGroupSelect01" v-model="category">
                <option selected>Choose...</option>
                <option v-for="(ca, key, index) in categories" :key=index></option>
            </select>
        </div>
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">日付</span>
            </div>
            <date-picker v-model="date" :config="options"></date-picker>
        </div>
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">摘要</span>
            </div>
            <input v-model="about" class="form-control" placeholder="摘要を入力してください!">
        </div>
        <button type="button" class="btn btn-primary" v-on:click="postAccounts">追加</button>
    </div>
</template>
<script>
import axios from 'axios';
import moment from 'moment';
import datePicker from 'vue-bootstrap-datetimepicker';
import VueMonthlyPicker from 'vue-monthly-picker';
export default {
    data: function() {
        return {
            accounts: [],
            money: "",
            about: "",
            category: "",
            income: false,
            date: null,
            options: {
                format: 'YYYY/MM/DD',
                useCurrent: false
            },
            categories: [],
            incomes: 0,
            payments: 0,
            query: moment(new Date()).format('YYYY/MM')
        }
    },
    created: function() {
        this.getAccounts();
        this.getCategories();
    },
    mounted: function() {
        this.sumAccounts();
    },
    methods: {
        getAccounts: function() {
            axios.get('/api/accounts').then(response => {
                for(let i = 0; i < response.data.length; i++) {
                    this.accounts.push(response.data[i]);
                }
            }, (error) => {
                condole.log(error);
            })
        },
        postAccounts: function() {
            axios.post('/api/accounts', {account: {money: Number(this.money), date: this.date, income: this.income, about: this.about, category: this.category}}).then((response) => {
                const date = new Date(this.query);
                if(moment(response.data.date).format('YYYY/MM') === moment(date).format('YYYY/MM')) {
                    if (this.income === true) {
                        this.incomes += Number(this.money);
                    } else {
                        this.payments += Number(this.money);
                    }
                }
                this.accounts.unshift(response.data);
                this.money = "";
                this.about = "";
                this.category = "";
                this.income = false;
                this.date = "";
                this.$forceUpdate();
            }, (error) => {
                console.log(error);
            })
        },
        getCategories: function() {
            axios.get('/api/categories').then((response) => {
                console.log(response.data);
                for(var i = 0; i < response.data.length; i++){
                    this.categories.push(response.data[i]);
                }
                console.log(this.categories);
            }, (error) => {
                console.log(error);
            })
        },
        sumAccounts: function() {
            axios.get('api/accounts').then((response) => {
                const date = new Date(this.query);
                this.incomes = 0;
                this.payments = 0;
                for(var i = 0; i < response.data.length; i++){
                    if(moment(response.data[i].date).format('YYYY/MM') === moment(date).format('YYYY/MM')) {
                        if(response.data[i].income === true){
                            this.incomes += response.data[i].money;
                        } else {
                            this.payments += response.data[i].money;
                        }
                    }
                }
                this.$forceUpdate();
            }, (error) => {
                console.log(error);
            });
        },
    },
    components: {
        datePicker,
        VueMonthlyPicker
    }
}
</script>
これで月ごとの支出入の合計を算出できます!
最後にカテゴリごとの金額集計をだせるようにします!
app/javascript/packs/components/accounts/Index.vueを以下のように修正します
<template>
    <div class="container">
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">絞り込み日付</span>
            </div>
            <vue-monthly-picker v-model="query"></vue-monthly-picker>
            <button type="button" class="btn btn-primary" v-on:click="sumAccounts">絞り込み</button>
        </div>
        <p>支出:</p>
        <p>収入:</p>
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">¥</span>
            </div>
            <input v-model="money" class="form-contorl" placeholder="金額を入力してください!">
        </div>
        <div class="input-group">
            <div class="input-group-prepend">
                <div class="input-group-text">
                    <input type="checkbox" aria-label="Checkbox for following text input" v-model="income"> 収入
                </div>
            </div>
        </div>
        <div class="input-group">
            <div class="input-group-prepend">
                <label class="input-group-text" for="inputGroupSelect01">分類</label>
            </div>
            <select class="custom-select" id="inputGroupSelect01" v-model="category">
                <option selected>Choose...</option>
                <option v-for="(ca, key, index) in categories" :key=index></option>
            </select>
        </div>
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">日付</span>
            </div>
            <date-picker v-model="date" :config="options"></date-picker>
        </div>
        <div class="input-group">
            <div class="input-group-append">
                <span class="input-group-text">摘要</span>
            </div>
            <input v-model="about" class="form-control" placeholder="摘要を入力してください!">
        </div>
        <button type="button" class="btn btn-primary" v-on:click="postAccounts">追加</button>
        <div>
            <button type="button" class="btn btn-primary" v-on:click="sumCategories">カテゴリごとの集計表示</button>
            <p v-for="(sum, key, index) in sums" :key=index>
                 : 
            </p>
        </div>
    </div>
</template>
<script>
import axios from 'axios';
import moment from 'moment';
import datePicker from 'vue-bootstrap-datetimepicker';
import VueMonthlyPicker from 'vue-monthly-picker';
export default {
    data: function() {
        return {
            accounts: [],
            money: "",
            about: "",
            category: "",
            income: false,
            date: null,
            options: {
                format: 'YYYY/MM/DD',
                useCurrent: false
            },
            categories: [],
            incomes: 0,
            payments: 0,
            query: moment(new Date()).format('YYYY/MM'),
            sums: []
        }
    },
    created: function() {
        this.getAccounts();
        this.getCategories();
    },
    mounted: function() {
        this.sumAccounts();
    },
    methods: {
        getAccounts: function() {
            axios.get('/api/accounts').then(response => {
                for(let i = 0; i < response.data.length; i++) {
                    this.accounts.push(response.data[i]);
                }
            }, (error) => {
                condole.log(error);
            })
        },
        postAccounts: function() {
            axios.post('/api/accounts', {account: {money: Number(this.money), date: this.date, income: this.income, about: this.about, category: this.category}}).then((response) => {
                const date = new Date(this.query);
                if(moment(response.data.date).format('YYYY/MM') === moment(date).format('YYYY/MM')) {
                    if (this.income === true) {
                        this.incomes += Number(this.money);
                    } else {
                        this.payments += Number(this.money);
                    }
                }
                this.accounts.unshift(response.data);
                this.money = "";
                this.about = "";
                this.category = "";
                this.income = false;
                this.date = "";
                this.$forceUpdate();
            }, (error) => {
                console.log(error);
            })
        },
        getCategories: function() {
            axios.get('/api/categories').then((response) => {
                console.log(response.data);
                for(var i = 0; i < response.data.length; i++){
                    this.categories.push(response.data[i]);
                }
                console.log(this.categories);
            }, (error) => {
                console.log(error);
            })
        },
        sumAccounts: function() {
            axios.get('api/accounts').then((response) => {
                const date = new Date(this.query);
                this.incomes = 0;
                this.payments = 0;
                for(var i = 0; i < response.data.length; i++){
                    if(moment(response.data[i].date).format('YYYY/MM') === moment(date).format('YYYY/MM')) {
                        if(response.data[i].income === true){
                            this.incomes += response.data[i].money;
                        } else {
                            this.payments += response.data[i].money;
                        }
                    }
                }
                this.$forceUpdate();
            }, (error) => {
                console.log(error);
            });
        },
        sumCategories: function() {
            this.sums = [];
            const date = new Date(this.query);
            for(var i = 0; i < this.categories.length; i++){
                this.sums.push({name: this.categories[i], value: 0});
                for(var j = 0; j < this.accounts.length; j++){
                    if(this.accounts[j].category === this.categories[i].name 
                        && moment(this.accounts[j].date).format('YYYY/MM') === moment(date).format('YYYY/MM')){
                        this.sums[i].value += this.accounts[j].money;
                    }
                }
            }
            console.log(this.sums);
        },
    },
    components: {
        datePicker,
        VueMonthlyPicker
    }
}
</script>
カテゴリごとの集計表示というボタンを押すとカテゴリごとに集計された金額が表示されます