railsとVue.jsで電話帳アプリ作成

#初めに
RailsとVue.jsを使って電話帳アプリを作成してみました。

こんな感じです。

![vue.gif](https://qiita-image-store.s3.amazonaws.com/0/207921/78901f03-cb06-7093-cc1f-d1563229c7bb.gif)


最初はVue.jsとRailsのapiを使って作成しようとしたけどCORSとかなんかのせいでうまくできなかった。

具体的にはlocalhost:8000で起動しているVue.jsからlocalhost:3000にaxiosを使ってアクセスしようとしても出来なかった。

なのでRailsにVue.jsを入れて？作った。

#最初の設定

とりあえず

```
rails new vue-app
```

yarnをインストールしてない場合はインストール

```
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update
sudo apt-get install yarn
```


gemを追加していく

デザインにbootstrapダミーデータ作成にfakerそしてvuejsを使うために必要なgemを加える。


```ruby 
gem 'webpacker', github: 'rails/webpacker'
gem 'faker'
gem 'bootstrap'
gem 'jquery-rails'
gem 'popper_js'
gem 'tether-rails'
```


そしてbundle install

そのあとに

```
bin/rails webpacker:install
rails webpacker:install:vue
```

でVueを使えるようにして。

assets/javascriptのapplication.jsを

```
//= require jquery3
//= require jquery_ujs
//= require popper
//= require tether
//= bootstrap
//= require activestorage
//= require turbolinks
//= require_tree .
```

としてbootstrapを使えるようにする。


userモデルを作成してそのあとダミーデータを作成する

```
rails g model user name:string phone:string address:string
rails db:migrate
```

```
rails c
100.times { User.create(name: Faker::Name.first_name, phone: Faker::PhoneNumber.cell_phone, address: Faker::Address.full_address
```

これで100件のデータが作成できる。

コントローラーの作成をする

```
rails g controller users index
```

vueを表示するためにlayouts/application.html.erbを

```erb 
<%= javascript_pack_tag 'hello_vue' %>
```

を<%= yield %>の下に追加する。

fontawesomeを使うので

head内に

```
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css">
```

を追加しておく。

これでとりあえずアプリの土台はOK

#controller作成

次にcontrollerを作成していく。

```ruby 
def index
end 

def getuser
  @users = User.all
  render :json => @users 
end 

def create
  @user = User.new(user_params)
  if @user.save 
    render :index
  end 
end 

private 

def user_params 
  params.fetch(:user, {}).permit(
      :name,:phone,:address
    )
end 
```

これでcontrollerはOK

getuserに関してはほかにコントローラー作ってそこに書いたほうが良いのかも…

indexにアクセスして電話帳を表示してvue内でgetuserにアクセスして@usersのjsonデータを取得して反映させる感じになる。


createに関してはrender :indexはいらない気がするけどとりあえず書いてある。


routes.rbを編集

```ruby 
get 'users/index'
get 'users/getuser'
post 'users/create'
```

みたいにすればとりあえずOK

#Vueを作っていく

Vueに関するファイルはapp/javascriptの中にある

app.vueが一番もとになるファイル（多分）

packsの中にhello_vue.jsがありこれが

```
<%= javascript_pack_tag 'hello_vue' %>
```

これで読み込まれている。

hello_vue.jsの中に

```javascript
import App from '../app.vue'
```

でapp.vueを読み込んで

```
render:h => h(App)
```

でapp.vueを表示している（多分）。

あんまり意味ないけどjavacriptいかに

Phone.vueファイルを作って

hello_vue.jsに

```javascript
import Phone from '../phone.vue'
```

をして

```
render: h => h(Phone)
```

と変更する

これでPhone.vueを読み込んでくれる。


#Phone.vueのtemplate

phone.vueのtemplateにhtmlを書いていく

まず最終的なtemplateを張っておくのでとりあえずコピペで動かしたい場合はここをコピぺしてください。

```html 
<div class="container">
    <div id="app">
      <h3 class="top-heading">連絡先追加</h3>
      <div class="row">
        <div class="col-md-5 col-left-item">
          お名前
        </div>
        <div class="form-group col-md-7">
          <input class="form-control" type="text" v-model="cword" placeholder="Enter name ...">
        </div>
      </div>
      <div class="row">
        <div class="col-md-5 col-left-item">
          電話番号
        </div>
        <div class="form-group col-md-7">
          <input class="form-control" type="text" v-model="pword" placeholder="Enter phonenumber ...">
        </div>
      </div>
      <div class="row">
        <div class="col-md-5 col-left-item">
          住所
        </div>
        <div class="form-group col-md-7">
          <input class="form-control" type="text" v-model="aword" placeholder="Enter address ...">
        </div>
      </div>
      <div class="submit-btn" v-on:click="createUser">
        <i class="fas fa-arrow-alt-circle-down"></i>
      </div>
      <div class="form-group search-form">
        <input class="form-control" type="text" v-model="keyword" placeholder="Search user ....">
      </div>
      <div class="row">
        <div class="col-md-4">
          <tr class="usersList" v-for="user in filteredUsers" :key="user.id">
            <td class="nameList" v-on:click="showUser(user.name)">{{ user.name }}</td>
          </tr>
        </div>
        <div class="col-md-8 infoWrap">
          <div class="infoBox animated" data-animate="bounceInLeft">
            <p>ユーザー情報</p>
            <div id="userInfo">
              <table>
                <tr>
                  <td>名前</td>
                  <td>{{ result.name }}</td>
                </tr>
                <tr>
                  <td>電話番号</td>
                  <td>{{ result.phone }}</td>
                </tr>
                <tr>
                  <td>住所</td>
                  <td>{{ result.address }}</td>
                </tr>
              </table>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
```

連絡先追加の部分がこれで

```html
      <div class="col-md-5 col-left-item">
          お名前
        </div>
        <div class="form-group col-md-7">
          <input class="form-control" type="text" v-model="cword" placeholder="Enter name ...">
        </div>
      </div>
      <div class="row">
        <div class="col-md-5 col-left-item">
          電話番号
        </div>
        <div class="form-group col-md-7">
          <input class="form-control" type="text" v-model="pword" placeholder="Enter phonenumber ...">
        </div>
      </div>
      <div class="row">
        <div class="col-md-5 col-left-item">
          住所
        </div>
        <div class="form-group col-md-7">
          <input class="form-control" type="text" v-model="aword" placeholder="Enter address ...">
        </div>
      </div>
      <div class="submit-btn" v-on:click="createUser">
　　　　<i class="fas fa-arrow-alt-circle-down"></i>
      </div>
```

inputにそれぞれv-modelがある。

例えば名前のinputにv-model="cword"がありこれによって名前のinputの値をvueのscriptの部分でcwordという変数で取得できる。


なので名前、電話番号、住所にcword,pword,awordというv-modelがついているので

script側で

```javascript
axios.post('/users/create', { user: {name: this.cword, phone: this.pword, address: this.aword } })
```

でデータを保存できるようになる。

ちなみに

axios.postは第一引数にurlを第二引数に渡すデータを書く。

登録ボタンはv-on:clickで実装できて

```html
      <div class="submit-btn" v-on:click="createUser">
　　　　<i class="fas fa-arrow-alt-circle-down"></i>
      </div>
```

この場合div class="submit-btn"がクリックされたときにcreateUserというmethodを実行することになります。

createUserはscript側で後で実装します。


検索部分はこれで

```html 

      <div class="form-group search-form">
        <input class="form-control" type="text" v-model="keyword" placeholder="Search user ....">
      </div>
      <div class="row">
        <div class="col-md-4">
          <tr class="usersList" v-for="user in filteredUsers" :key="user.id">
            <td class="nameList" v-on:click="showUser(user.name)">{{ user.name }}</td>
          </tr>
        </div>
        <div class="col-md-8 infoWrap">
          <div class="infoBox animated" data-animate="bounceInLeft">
            <p>ユーザー情報</p>
            <div id="userInfo">
              <table>
                <tr>
                  <td>名前</td>
                  <td>{{ result.name }}</td>
                </tr>
                <tr>
                  <td>電話番号</td>
                  <td>{{ result.phone }}</td>
                </tr>
                <tr>
                  <td>住所</td>
                  <td>{{ result.address }}</td>
                </tr>
              </table>
            </div>
          </div>
        </div>
```

v-model="keyword"のところで検索ワードを取得している

v-for="user in filteredUsers"でkeywordの値にマッチするuserを表示している。

v-forはループ?ができて例えばusersにuser全員が入っているとして

v-for="user in users"とすれば

rubyでいう

```ruby 
users.each do |user|
end 
```

みたいに使うことができる。

filteredUsersにはkeywordにマッチしたものが入っているのでuser in filterdUsersとすれば検索を実装できる。

filteredUsersは後でscript内のcomputedの中に定義する。


後は詳細データの表示でuser一覧の中から名前をクリックするとそのデータを取得してその値をresultに入れて詳細を表示するようにしている。

それは

```html 
          <tr class="usersList" v-for="user in filteredUsers" :key="user.id">
            <td class="nameList" v-on:click="showUser(user.name)">{{ user.name }}</td>
          </tr>
```

この部分で名前がクリックされたときにshowUserというメソッドをuser.nameを与えて実行している。

showUserではuser一覧からおんなじ名前のuserを検索して見つかったらresultにその値を入れている。

ちなみにこのやり方だと同じ名前のユーザーがいるとだめなので登録の際は同じ名前のユーザーを登録できないようにする必要がある。



これでとりあえずtemplate部分はおわり

#phone.vueのscript

こちらに関してもコピペ用に最初に全部を張っておきます

```javascript
import axios from 'axios'
export default {
  data: function () {
    return {
      cword: '',
      pword: '',
      aword: '',
      keyword: '',
      result: {},
      users: []
    }
  },
  created: function() {
    this.getData();
  },
  methods: {
    getData: function(){
      fetch('/home/users')
      .then(dataWrappedByPromise => dataWrappedByPromise.json())
      .then(data => {
        this.users = data;
      })
    },
    createUser: function() {
      if (!this.cword) return;

      for(var i in this.users) {
        var user = this.users[i];
        if (user.name.toLowerCase() == this.cword.toLowerCase()) {
          console.log("can not save user")
          return;
        }
      }

      axios.post('/home/create', { user: {name: this.cword, phone: this.pword, address: this.aword } }).then((response) => {
        var luser = {name: this.cword, phone: this.pword, address: this.aword };
        this.users.unshift(luser);
        this.cword = '';
      })
    },
    showUser: function(d) {
      this.result = [];
      for(var i in this.users) {
        var user = this.users[i];
        if(user.name === d) {
          this.result = user;
        }
      }
      var animationName = $('.infoBox').data('animate');

      $('.infoBox').addClass(animationName).delay(1000).queue(function(next){
        $('.infoBox').removeClass(animationName);
        next();
      });
    }
  },
  computed: {
    filteredUsers: function() {
      var users = [];

      for(var i in this.users) {
        var user = this.users[i];

        if (user.name.toLowerCase().indexOf(this.keyword) !== -1) {
          users.push(user);
        }
      }
      return users;
    }
  }
}
```





まずaxiosを使うのでコンソールでアプリのフォルダに移動して

```
yarn add axios
```

としてaxiosを使えるようします。

そして

```
import axios from 'axios'
```

で読み込みます。

```
export default {
}
```

この中にdataなりmethodsなりをかいていきます。

まずv-modelで使うものはdataで定義されていないと使えないので

```javascript
data: function () {
  return {
      cword: '',
      pword: '',
      aword: '',
      keyword: '',
      result: {},
      users: []
    }
}
```

と書きます。

usersにかんして空だとおもうかもしれませんが

これに関しては

createdの中でaxiosを使ってデータを取得してusersに入れるので大丈夫です。

createdに関しては知らないけど名前的に多分vueが読み込まれる時？に実行されるんだと思う

なので

```javascript
created: this.getData();
```

と書いておきます。

getDataに関してはmethodsで定義します。

methodsとcomputedの違いがわからないですけどfilteredUsersとかはcomputedにかいています。

getDataとかはmethodsに書いています。

今回createUserとshowUserをmethods内にかいてますけど多分computedの中に書いたほうがよさそうです。


とりあえず

methodsの中に

```javascript 
  methods: {
    getData: function(){
      fetch('/home/users')
      .then(dataWrappedByPromise => dataWrappedByPromise.json())
      .then(data => {
        this.users = data;
      })
    },
    createUser: function() {
      if (!this.cword) return;

      for(var i in this.users) {
        var user = this.users[i];
        if (user.name.toLowerCase() == this.cword.toLowerCase()) {
          console.log("can not save user")
          return;
        }
      }

      axios.post('/home/create', { user: {name: this.cword, phone: this.pword, address: this.aword } }).then((response) => {
        var luser = {name: this.cword, phone: this.pword, address: this.aword };
        this.users.unshift(luser);
        this.cword = '';
      })
    },
    showUser: function(d) {
      this.result = [];
      for(var i in this.users) {
        var user = this.users[i];
        if(user.name === d) {
          this.result = user;
        }
      }
      var animationName = $('.infoBox').data('animate');

      $('.infoBox').addClass(animationName).delay(1000).queue(function(next){
        $('.infoBox').removeClass(animationName);
        next();
      });
    }
  },

```

とかいてます。

getDataはusersの情報を取得していて

その際にfetchを使っています。

```javascript
fetch('/users/getusers')
      .then(dataWrappedByPromise => dataWrappedByPromise.json())
      .then(data => {
        this.users = data;
      })
```

という書き方でデータが取得できます。
ちなみに

```
users = data
```

とするとうまくいかずthisを付ける必要があります。

javascriptを使う人からしたら当たり前かもしれないですが自分はこれで結構はまりました。

usersに限らずmethods内でdata以下に書いたデータを処理しようとするとthisが必要になるっぽいです。



createUserでユーザの作成をしていてaxiosを使っています。

```javascript
createUser: function() {
      if (!this.cword) return;

      for(var i in this.users) {
        var user = this.users[i];
        if (user.name.toLowerCase() == this.cword.toLowerCase()) {
          console.log("can not save user")
          return;
        }
      }

      axios.post('/users/create', { user: {name: this.cword, phone: this.pword, address: this.aword } }).then((response) => {
        var luser = {name: this.cword, phone: this.pword, address: this.aword };
        this.users.unshift(luser);
        this.cword = '';
      })
    },
```

もし名前がなかった場合や既に同じ名前のユーザーがいた場合はreturnして保存できなくしています。

ですがこれだとエラーメッセージが出ないのでalertとかで何かしらのエラーメッセージを出したほうが良いと思います。

それでreturnに引っかからなかった場合

axios.postでユーザーの作成をしています。
またリアルタイムで反映させるためにユーザが作成されたらusers.unshiftでuserを追加しています。

ちなみにresponse内に送ったデータがあるのでそれを使うと

例えば

```javascript
axios.post('/users/create', { user: {name: this.cword, phone: this.pword, address: this.aword } }).then((response) => {
        this.users.unshift(respose.data.user);
        this.cword = '';
      })
```

みたいな感じでとれるっポイんですが自分の場合うまくいかず
response.config.dataで

```
{ "user":{name: "asdf", phone: "asdf", address: "asdf"}}
```

見たいな感じで取得できるのですが

```
var d = response.config.data
data.user
```

みたいにしてもデータはなぜかとれず仕方ないので

```javascript 
var luser = {name: this.cword, phone: this.pword, address: this.aword };
```

としています。

showUserに関しては

```javascript 
this.result = [];
      for(var i in this.users) {
        var user = this.users[i];
        if(user.name === d) {
          this.result = user;
        }
      }
```

でユーザを検索して検索結果をresultに入れています。

```
　　　var animationName = $('.infoBox').data('animate');

      $('.infoBox').addClass(animationName).delay(1000).queue(function(next){
        $('.infoBox').removeClass(animationName);
        next();
      });
```

でアニメーションを実装しています。


アニメーションに関してはanimate.cssを使っているのでアニメーションを使いたい場合は

https://daneden.github.io/animate.css/

ここからcssをとってきてください。

そしてcomputedにfilteredUsersを書いています。

```javascript 
computed: {
    filteredUsers: function() {
      var users = [];

      for(var i in this.users) {
        var user = this.users[i];

        if (user.name.toLowerCase().indexOf(this.keyword) !== -1) {
          users.push(user);
        }
      }
      return users;
    }
  }
```

indexOfが-1ではないとき、つまりマッチしている場合はusersにpushしています。

#style

style部分も一応張っておきます。

```css 
body 
{
  background: rgba(241, 196, 15,0.4);
}

header
{
  width: 100%;
  height: 80px;
  line-height: 80px;
  font-weight: 800;
  background: rgba(230, 126, 34,1.0);
  color: white;
  text-align: center;
  margin-bottom: 50px;
}

header h3 
{
  line-height: 80px;
  font-size: 57px;
}
.top-heading 
{
  text-align: center;
  font-weight: 700;
  letter-spacing: 1px;
  border-bottom: 2px solid rgba(230, 126, 34,1.0);
  color: rgba(230, 126, 34,1.0);
  margin-bottom: 50px;
}
.usersList
{
  width: 100%;
}
.nameList 
{
  width: 100% !important;
  border-bottom: 1px solid #e67e22 !important;
  cursor: pointer;
  transition: all 0.5s;
}

.nameList:hover 
{
  background-color: #d35400;
  color: white;
  border-top-right-radius: 20px;
}

.infoBox
{
  width: 500px;
  height: 250px;
  border: 4px solid #d35400;
  background-color: white;
  margin: 0 auto;
  border-radius: 20px;
  margin-top: 100px;
  position: fixed;
  box-shadow: 1px 1px 3px #4e2b0c;
  box-shadow: 3px 3px 6px #976438;

}
.infoBox p 
{
  text-align: center;
  font-weight: 600;
  font-size: 26px;
  color: #d35400;
  border-bottom: 1px solid #b8632b;
}
#userInfo table 
{
  border:none!important;
  width: 95% !important;
  margin: 0 auto;
}
#userInfo table tr 
{
  border-bottom: 1px solid #b8632b;
}
.fas {
  color: rgba(230, 126, 34,1.0);
  font-size: 36px;
}

.col-left-item
{
  text-align: center;
  color: black;
  font-weight: 600;
}
.submit-btn 
{
  width: 50px;
  height: 50px;
  margin: auto;
  margin-bottom: 50px;

}
.submit-btn i 
{
  color: #d35400;
  transition: all 0.5s;
}
.submit-btn i:hover
{
  box-shadow: 0 0 10px #b8632b;
}
.search-form
{
  margin-bottom: 25px;
}
```

#おわりに

これで多分うごくと思います。

正直javascriptの知識がないので結構いろんなところでつまずきました。

あとページをリロードするたびにvueファイルが変更されているとコンパイルをする感じだと思うのでstyleにかんしてはassetsのファイルに書いたほうがいいんじゃないのかと思います。

javascriptは嫌いだけどvue.jsはなんかパズルを組み立てていく感じがして好きですし面白いです。