初めに
RailsとVue.jsを使って電話帳アプリを作成してみました。
こんな感じです。
最初は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を加える。
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を
<%= 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を作成していく。
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を編集
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の中に
import App from '../app.vue'
でapp.vueを読み込んで
render:h => h(App)
でapp.vueを表示している(多分)。
あんまり意味ないけどjavacriptいかに
Phone.vueファイルを作って
hello_vue.jsに
import Phone from '../phone.vue'
をして
render: h => h(Phone)
と変更する
これでPhone.vueを読み込んでくれる。
Phone.vueのtemplate
phone.vueのtemplateにhtmlを書いていく
まず最終的なtemplateを張っておくのでとりあえずコピペで動かしたい場合はここをコピぺしてください。
<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>
連絡先追加の部分がこれで
<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側で
axios.post('/users/create', { user: {name: this.cword, phone: this.pword, address: this.aword } })
でデータを保存できるようになる。
ちなみに
axios.postは第一引数にurlを第二引数に渡すデータを書く。
登録ボタンはv-on:clickで実装できて
<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側で後で実装します。
検索部分はこれで
<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でいう
users.each do |user|
end
みたいに使うことができる。
filteredUsersにはkeywordにマッチしたものが入っているのでuser in filterdUsersとすれば検索を実装できる。
filteredUsersは後でscript内のcomputedの中に定義する。
後は詳細データの表示でuser一覧の中から名前をクリックするとそのデータを取得してその値をresultに入れて詳細を表示するようにしている。
それは
<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
こちらに関してもコピペ用に最初に全部を張っておきます
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で定義されていないと使えないので
data: function () {
return {
cword: '',
pword: '',
aword: '',
keyword: '',
result: {},
users: []
}
}
と書きます。
usersにかんして空だとおもうかもしれませんが
これに関しては
createdの中でaxiosを使ってデータを取得してusersに入れるので大丈夫です。
createdに関しては知らないけど名前的に多分vueが読み込まれる時?に実行されるんだと思う
なので
created: this.getData();
と書いておきます。
getDataに関してはmethodsで定義します。
methodsとcomputedの違いがわからないですけどfilteredUsersとかはcomputedにかいています。
getDataとかはmethodsに書いています。
今回createUserとshowUserをmethods内にかいてますけど多分computedの中に書いたほうがよさそうです。
とりあえず
methodsの中に
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を使っています。
fetch('/users/getusers')
.then(dataWrappedByPromise => dataWrappedByPromise.json())
.then(data => {
this.users = data;
})
という書き方でデータが取得できます。
ちなみに
users = data
とするとうまくいかずthisを付ける必要があります。
javascriptを使う人からしたら当たり前かもしれないですが自分はこれで結構はまりました。
usersに限らずmethods内でdata以下に書いたデータを処理しようとするとthisが必要になるっぽいです。
createUserでユーザの作成をしていてaxiosを使っています。
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内に送ったデータがあるのでそれを使うと
例えば
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
みたいにしてもデータはなぜかとれず仕方ないので
var luser = {name: this.cword, phone: this.pword, address: this.aword };
としています。
showUserに関しては
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を使っているのでアニメーションを使いたい場合は
ここからcssをとってきてください。
そしてcomputedにfilteredUsersを書いています。
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部分も一応張っておきます。
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はなんかパズルを組み立てていく感じがして好きですし面白いです。