vue.jsを使ってシングルアプリケーションを作るためには、HTMLのtable要素に表示されたセルの更新を別ページに移動することなく更新したいものです。

入門者を対象にvue.jsを利用したシンプルな方法で更新可能はセルの設定方法を確認します。セルを更新するvue.js側のファイルはcdnを使用するため特別な環境は必要がありませんが、テーブルデータを取得するために外部リソースを準備する必要があります。

バックエンドサーバについて

vue.jsを使ってテーブルを描写する際、通常はテーブルのデータはデータベースに保存されているため、外部リソースからデータを取得する必要があります。

本文書では外部リソースであるバックエンドサーバにLaravelを利用しています。vue.jsからのデータ取得/更新/削除処理を受けつけることができればバックエンドがLaravelである必要はありませんが最初にLaravelで行なった設定を簡単に説明します。Laravelを利用していない場合は、この章をスキップして次の賞に進んでください。

テーブルについて

Laravel上にusersテーブルを作成します。usersテーブルはユーザ名とEmailの2つの列で構成されています。usersテーブルにはユーザ情報が入っています。

ルーティングについて

vue.jsからaxiosを使用してユーザ情報の取得、更新、削除を行います。そのためにapi.webファイルに以下のルーティングを追加しています。


Route::get('users',function(){
	return App\User::all();
});

Route::delete('users/{id}',function($id){

	$user = App\User::find($id);

	$user->delete();

	return response()->json([
        'success' => 'user deleted successfully!'
    ]);

});

Route::patch('users/{id}',function($id,Request $request){

	$user = App\User::find($id);

	$user->update($request->all());

	return response()->json([
        'success' => 'user updated successfully!'
    ],200);

});

CORSの設定について

今回はLaravelの外部にファイルを作成し、そのファイルからaxiosを使ってLaravelにリクエストを送ります。そのため、CORS(Cross Origin Resource Sharing)の設定を行なっていないと以下のエラーが表示され、外部のファイルからLaravelにアクセスすることができません。

Access to XMLHttpRequest at ‘http://127.0.0.1:8000/api/users’ from origin ‘http://localhost:8080’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

ミドルウェアのCorsを作成します。


 $ php artisan make:middleware Cors
Middleware created successfully.

実行するとapp¥Http¥MiddlwareにCors.phpファイルが作成されます。$next($request)に3つの->header()を追加してください。


namespace App\Http\Middleware;

use Closure;

class Cors
{
  
    public function handle($request, Closure $next)
    {
        return $next($request)
        ->header('Access-Control-Allow-Origin','*')
        ->header('Access-Control-Allow-Headers','*')
        ->header('Access-Control-Allow-Methods','*');
    }
}

app¥Http¥Kernel.phpに作成したCorsミドルウェアを追加します。


protected $middleware = [
    \App\Http\Middleware\TrustProxies::class,
    \App\Http\Middleware\CheckForMaintenanceMode::class,
    \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
    \App\Http\Middleware\TrimStrings::class,
    \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    \App\Http\Middleware\Cors::class, //追加
];

ここまでの準備が整ったら、php artisan serverでLaravelの開発サーバを起動します。


$ php artisan serve
Laravel development server started: 
axiosではhttp://127.0.0.1:8000に向けてリクエストを送ります。

index.htmlファイルの作成

利用するvue.js、axios、bootstrapはすべてcdnを経由してい読み込みます。

scriptタグに記述するコード

HTMLに表示させるテーブルの内容はusersテーブルに保存されたデータを利用して行います。usersテーブルは、ユーザ名とEmailの2つの列で構成されたシンプルな構造をしています。

テーブルのデータはaxiosを通してVueインタンスのmountedフックで取得します。UserTableという名前のコンポーネントを利用して、v-forでtr要素を作成します。

vue.jsのライフサイクルフックの一つであるmountedフックを利用しているので、JavaScriptを実行するページが開かれるとaxiosを使ってバックエンドからusersテーブルの内容を取得します。

index.htmlファイルを作成して、scriptタグに以下を記述します。コンポーネントuser-tableを追加しています。


<script>
Vue.component('user-table',{
  template : `<tr><td>{{ user.id }}</td>
                 <td>{{ user.name }}</td>
                 <td>{{ user.email }}</td>
                 <td><button class="btn btn-danger">削除</button></td>
                 </tr>`,
props : ['user','index']
});


var app = new Vue({
  el: '#app',
  data: {
    users: []
  },
  mounted: function(){
    axios.get('http://127.0.0.1:8000/api/users').then(response => this.users = response.data);
  }
})
</script> 

HTML側に記述するコード

HTMLは下記のように記述します。scriptタグでvue.jsとaxios、linkタグでbootstrapを読み込みます。

bootstrapは必須ではなくテーブルに装飾を行うために使用します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Vue.js</title>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
</head>
<body>
<div id="app">
    <table class="table table-bordered">
        <thead  class="thead-dark">
            <tr>
                <th>ID</th>
                <th>ユーザ名</th>
                <th>E-mail</th>
                <th>削除</th>
            </tr>
        </thead>
        <tbody>
          <tr is="user-table" v-for="(user,index) in users" v-bind:user="user" v-bind:key="user.id" v-bind:index="index"></tr>
        </tbody>
    </table>
</div>

ここまでの記述では特別な処理は行わずv-forを使ってaxiosから取得したusersデータを展開して、テーブルとして表示しているだけです。

ブラウザで確認すると下記のように表示されます。削除ボタンが付いていますが、何も設定されていないのでクリックしても変化はありません。

ユーザ一覧
ユーザ一覧

コンポーネントの記述について

HTML側ではコンポーネントを以下のように記述しています。


<tr is="user-table" v-for="(user,index) in users" v-bind:user="user" v-bind:key="user.id" v-bind:index="index"></tr>
is属性はvue.jsの公式マニュアルのComponents BasicsのDOM Template Parsing Caveatsを参考に行なっています。

コンポーネントは<user-table></user-table>タグで記述すると表示が正常に行われないので注意してください。


<user-table v-for="(user,index) in users" v-bind:user="user" v-bind:key="user.id" v-bind:index="index"></user-table>

下記のように崩れたテーブルとして描写されます。

崩れたテーブルの描写
崩れたテーブルの描写

テーブルの列の削除

子コンポーネントuser-tableの設定

最初に削除の機能を追加します。

user-tableコンポーネントのbutton要素にv-on:clickイベントと追加し、クリックを行うと行の番号であるindexとuserのidをdeleteRowメソッドに渡します。


template : `<tr><td>{{ user.id }}</td>
             <td>{{ user.name }}</td>
             <td>{{ user.email }}</td>
             <td><button class="btn btn-danger" v-on:click="deleteRow(user.id,index)">削除</button></td>
             </tr>`,

deleteRowメソッド内ではaxiosによりその行のdelete処理を行い、$emitメソッドを使って親要素に行が削除されたことを伝えます。


methods: {
    deleteRow : function(id,index){
        axios.delete('http://127.0.0.1:8000/api/users/' + id)
        .then((response) => {
            this.$emit('from-child',index)
        }).catch((error) =>{
            console.log(error)
        })
    },
},

親側のdelete処理

親側ではUserTableコンポーネントから送られてくるイベントfrom-childを受け取ってdeleteRowを実行します。このdeleteRowメソッドにより、描写されているテーブル上の削除ボタンを押された行が画面上から削除されます。

UserTableコンポーネントと親側のdeleteRowは名前は一緒ですが関連は全くありません。

<tr is="user-table" v-for="(user,index) in users" v-bind:user="user" v-bind:key="user.id" v-bind:index="index" v-on:from-child="deleteRow"></tr>

親側のdeleteRowで行の削除を行います。削除機能の作業は完了です。


methods: {
	deleteRow : function(index){
    this.$delete(this.users,index)
	}
},
this.$delete(this.user,index)はthis.users.splice(index,1)と同じ意味で、this.usersのindex番目の要素の削除を行います。

テーブルのtd要素の更新

ここからの処理はuser-tableコンポーネントの中身を更新することで行います。

tdをクリックするとinput要素に変える

td要素をダブルクリックするとinput要素に変更する機能を追加します。

input要素の表示・非表示はv-ifディレクティブを使用します。v-ifディレクティブを切り替えるための値として、ユーザ名用にisEditName、email用にisEditEmailというdataを追加し、初期値はfalseとします。


data: function(){
    return {
      isEditName : false,
      isEditEmail : false
    }
}

v-ifとisEditNameを組み合わせるとisEditNameがfalseの場合は通常の表示、isEditNameがtrueの場合は、input要素が表示されるようにします。


<td v-if="!isEditName">{{ user.name }}</td>
<td v-else><input type="text" class="form-control" v-model="user.name"></td>

v-ifによってinput要素の非表示・表示を行うためには、isEditNameをfalseからtrueに切り替える仕組みを入れる必要があります。そのために利用するのがdoubleclickイベントです。文字列が入ったtd要素をダブルクリックするとisEditNameがfalseからtrueになりinput要素が表示されます。


<td v-if="!isEditName" v-on:dblclick="isEditName = true">{{ user.name }}</td>
<td v-else><input type="text" class="form-control" v-model="user.name"></td>

先頭のユーザ名をダブルクリックするとinput要素に変わります。

ダブルクリックでinput要素表示
ダブルクリックでinput要素表示

input要素を元の状態に戻す

td要素をダブルクリックをするとinput要素に変わりました。input要素の中身を更新した後にはinput要素から元の状態に戻す必要があります。元の状態に戻すためblurイベントを利用します。input要素にblurイベントを入れることでダブルクリックでinput要素にしたあと、一度input要素にカーソルを合わせて外すとisEditNameがfalseになります。isEditNameがfalseになるのでv-ifディレクティブによりinput要素から元の状態に戻ります。


<td v-if="!isEditName" v-on:dblclick="isEditName = true">{{ user.name }}</td>
<td v-else><input type="text" class="form-control" v-model="user.name" v-on:blur="isEditName=false" ></td>

input要素の更新を反映させる

input要素の切り替えができるようになったので、最後は更新した内容をデータベースに反映させるための処理が必要になります。先程設定したblurイベントを利用しますが、今回は新規のメソッドを追加します。

メソッドの名前はupdateNameとして、変更を行ったuser.idとuser.nameを渡します。


<td v-if="!isEditName" v-on:dblclick="isEditName = true">{{ user.name }}</td>
<td v-else><input type="text" class="form-control" v-model="user.name" v-on:blur="updateName(user.id, user.name)" ></td>

axiosでサーバに対して変更のpatchリクエストを送ります。リクエストが成功した時のみisEditNameの値をfalseにします。

サーバ側の処理はここでは説明しませんが、それぞれの環境に合わせた処理を追加してください。

updateName : function(id,name){
    axios.patch('http://127.0.0.1:8000/api/users/' + id, {id : id, name: name})
    .then((response) => {
        this.isEditName = false
     }).catch((error) =>{
        console.log(error)
      })
},

通常ではサーバ側との通信の障害やバグまたValidationによってサーバ側での更新が行われない場合も考えられます。成功した場合には成功メッセージ、失敗した場合にはエラーメッセージを表示する機能をつける必要があります。

実際に更新を行って反映されるか確認しましょう。ユーザIDの1のユーザの名前を山岸から山本に変更します。変更後input要素からカーソルを外すとサーバへの更新が行われます。

セルの更新を行う
セルの更新を行う

axiosを使って戻される値で更新されたかどうか判断する必要がありますが、ブラウザを再読み込みして更新データが表示されれば問題なくデータ更新は反映されています。

emailにも同じ設定

E-mail列の情報も書き換えができるように下記のように設定を行います。


<td v-if="!isEditEmail" v-on:dblclick="isEditEmail = true">{{ user.email }}</td><td v-else><input type="text" class="form-control" v-model="user.email" v-on:blur="updateEmail(user.id, user.email)" ></td>

email用のupdateEmailも追加します。


updateEmail : function(id,email){
    axios.patch('http://127.0.0.1:8000/api/users/' + id, {id : id, email: email})
    .then((response) => {
        this.isEditEmail = false
        console.log(response)
    }).catch((error) =>{
        console.log(error)
    })
},

これでnameとemailのセルをダブルクリックすると変更ができるようになりました。

今回記述したコード

下記が今回作成したindex.htmlファイルの全体コードです。


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Vue.js</title>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" >
</head>
<body>
<div id="app">
    <table class="table table-bordered">
        <thead  class="thead-dark">
            <tr>
                <th>ID</th>
                <th>ユーザ名</th>
                <th>E-mail</th>
                <th>削除</th>
            </tr>
        </thead>
        <tbody>
          <tr is="user-table" v-for="(user,index) in users" v-bind:user="user" v-bind:key="user.id" v-bind:index="index" v-on:from-child="deleteRow"></tr>
        </tbody>
    </table>
</div>

<script>
Vue.component('user-table',{
template : `<tr><td>{{ user.id }}</td>
             <td v-if="!isEditName" v-on:dblclick="isEditName = true">{{ user.name }}</td>
<td v-else><input type="text" class="form-control" v-model="user.name" v-on:blur="updateName(user.id, user.name)" ></td>
             <td v-if="!isEditEmail" v-on:dblclick="isEditEmail = true">{{ user.email }}</td><td v-else><input type="text" class="form-control" v-model="user.email" v-on:blur="updateEmail(user.id, user.email)" ></td>
             <td><button class="btn btn-danger" v-on:click="deleteRow(user.id,index)">削除</button></td>
             </tr>`,
data: function(){
    return {
      isEditName : false,
      isEditEmail : false
    }
},
props : ['user','index'],
methods: {
    deleteRow : function(id,index){
        axios.delete('http://127.0.0.1:8000/api/users/' + id)
        .then((response) => {
            this.$emit('from-child',index)
        }).catch((error) =>{
            console.log(error)
        })
    },
	updateName : function(id,name){
	    axios.patch('http://127.0.0.1:8000/api/users/' + id, {id : id, name: name})
	    .then((response) => {
	        this.isEditName = false
	     }).catch((error) =>{
	        console.log(error)
	      })
	},
	updateEmail : function(id,email){
	    axios.patch('http://127.0.0.1:8000/api/users/' + id, {id : id, email: email})
	    .then((response) => {
	        this.isEditEmail = false
	        console.log(response)
	    }).catch((error) =>{
	        console.log(error)
	    })
	},	
},
});

var app = new Vue({
  el: '#app',
  data: {
    users: []
  },
methods: {
	deleteRow : function(index){
    this.$delete(this.users,index)
	}
},
  mounted: function(){
    axios.get('http://127.0.0.1:8000/api/users').then(response => this.users = response.data);
  }
})
</script> 
</body>
</html>