TL;DR
jQueryで書いたコードをVue.jsで置き換えることができました。
CDNでも親子コンポーネントが使えました。
コンパイルなしでも、1ページだけでも、Vue.jsで書いてはどうですか?
受付システム jQuery版(CodePenで再現)
受付システム Vue.js途中経過版(CodePenで再現)
受付システム Vue.js完成版(CodePenで再現)
はじめに
仕事で、ある企業の設立パーティの受付システムを作りました。突貫工事でつくったので入力画面はありません(出席者リストは、私があれこれしてデータベースに流し込みました)。受付で担当者が使うウェブ画面だけをBulmaとjQueryで作成しました。
See the Pen reception-with-jquery by Isamu Suzuki (@isamusuzuki) on CodePen.
受付完了ボタンをタップ or クリックすると、Ajaxでデータベースが更新され、ボタンが消えて、代わりに受付時間が入ります。それと同時に、上にあるカウンターの数字も変化します。CodePenで再現してみたので、是非見てみてください。
CDNって何だっけ?
HTMLコードの中で、外部URLを指定するだけで、CSS/JSのライブラリまたはフレームワークを使うことです。そもそもは、その外部URLのことをCDNと言いますが、「コンパイルとかややこしいこと言わないで、ハードルを下げて、気軽に使う方法もアリなんじゃない?」っていう意味を込めています。
注意点:CodePenでは、CDNはSettingsダイアログの中に隠れています。これから表示するコードでも省いています。Bulma, jQuery, Vue.jsの他に、Moment.jsも使っています。話を簡単にするためにAjax機能も省きましたが、Axiosを使えば簡単に機能追加できるはずです。
まずはjQuery版を解説
HTMLコードを見ると、id属性が与えられている要素が4つあります。これが、jQueryが後から生成したデータを差し込む切り口となります。でもtableBodyはざっくりしすぎていて、ここにどんなデータが差し込まれるのか、まったくわかりません。
<section class="section">
<div class="container">
<span class="is-size-3">
受付(総<span id="total">0</span>、済<span id="already">0</span>、未<span id="notyet">0</span>)
</span>
</div>
<div class="container">
<table class="table">
<thead>
<tr>
<th>#</th>
<th>御社名</th>
<th>ご芳名</th>
<th>担当者</th>
<th>受付時間</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
</section>
JSコードは4つのパート(頭3つは関数、最後は実行コード)から出来上がっています。3番目のcreateTable関数が長いですね。ここでtableBodyに流し込むテーブル行を作成しています。さきほど、id属性が与えられている要素は4つといいましたが、実はこの中にもid属性があります(38行目と44行目にある<td id="arrived${x.id}">
です)。出来る限りキレイに書いたので、すぐ読み取れると思いますが、ここがグチャグチャしていると、書いた自分ですらも、後から読み解くのに苦労することになります。JSコードの中に散りばめられたHTMLコードを頭の中で再現するのは、元から大変なことなのです。
// Ajaxで、summary apiにアクセスしたつもり
function createSummary() {
$('#total').html(result1.total);
$('#already').html(result1.already);
$('#notyet').html(result1.notyet);
}
// Ajaxで、arrived apiにアクセスしたつもり
function arrived(id) {
var now = moment();
$(`#arrived${id}`).html(now.format('HH:mm:ss'));
var already = parseInt($('#already').text(), 10);
var notyet = parseInt($('#notyet').text(), 10);
$('#already').text(already + 1);
$('#notyet').text(notyet - 1);
};
// Ajaxで、guest apiにアクセスしたつもり
function createTable() {
var temp = '';
result2.forEach(x => {
temp += '<tr>';
temp += `<td>${x.id}</td>`;
temp += `<td>${x.company}</td>`;
temp += `<td>${x.name}</td>`;
temp += `<td>${x.host}</td>`;
if(x.is_arrived === 0) {
temp += `<td id="arrived${x.id}">`;
temp += `<button class="button is-small is-link" onClick="arrived(${x.id});">`;
temp += '受付完了</button></td>';
} else {
moment.locale('ja');
var a_time = moment(x.arrived_date).format('HH:mm:ss');
temp += `<td id="arrived${x.id}">${a_time}</td>`;
}
temp += '</tr>';
});
$('#tableBody').html(temp);
};
// ページ読み込み後ロード
$(function() {
createSummary();
createTable();
})
Vue.jsで置き換えてみる
まずは途中経過のコードを見てみてください。
See the Pen reception-with-vuejs-step1 by Isamu Suzuki (@isamusuzuki) on CodePen.
HTMLコードでは、全体が<div id="app"></div>
で囲われて、先ほどのID属性が与えられた要素が変化しています。Vue.jsではHTMLコードそのものがテンプレートとなり、直接表示されるものではなくなります。なので、{{ abc }}
と書くと、それは「abcという変数を表示する」という意味になるのです。20行目のv-for="guest in guests"
は、v-forディレクティブと言って、「guestsという配列から、ひとつづつ要素を取り出して繰り返す」という意味になります。
<div id="app">
<section class="section">
<div class="container">
<span class="is-size-3">
受付(総{{ total }}、済{{ already }}、未{{ notyet }})
</span>
</div>
<div class="container">
<table class="table">
<thead>
<tr>
<th>#</th>
<th>御社名</th>
<th>ご芳名</th>
<th>担当者</th>
<th>受付時間</th>
</tr>
</thead>
<tbody>
<tr v-for="guest in guests" v-bind:key="guest.id">
<td>{{guest.id}}</td>
<td>{{guest.company}}</td>
<td>{{guest.name}}</td>
<td>{{guest.host}}</td>
<td></td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
いっぽう、JSコードのほうは、細かいオプションをつけてVueオブジェクトをインスタンス化しただけで終わりとなります。インスタンスされたVueオブジェクトがどう動いているのかは、実際にブラウザで動かしてみないとわからない。なんというか、「クリスマスツリーのデコレーションだけしておいて、点灯はしない」みたいなカッコよさを感じます。
const app = new Vue({
el: '#app',
data: {
guests: [],
total: 0,
already: 0,
notyet: 0
},
methods: {
createSummary: function() {
// Ajaxで、summary apiにアクセスしたつもり
this.total = result1.total;
this.already = result1.already;
this.notyet = result1.notyet;
},
createTable: function() {
// Ajaxで、arrived apiにアクセスしたつもり
this.guests = result2;
},
onArrived: function() {
this.already += 1;
this.notyet -= 1;
}
},
created: function() {
// ページ読み込み後ロード
this.createSummary();
this.createTable();
}
});
オプションもそれぞれ役割がきっちり決まっています。
-
el
... どこをテンプレートとして扱うかを示す -
data
...{{abc}}
や様々なディレクティブを使って、テンプレートと結びつける変数群 -
methods
... 変数をあれこれいじる関数群、v-bindディレクティブを使えばテンプレートと結びつけることも可能 -
created
... Vue.jsが提供するライフサイクルフックのひとつ、インスタンスの初期化が済んだら動作する
受付完了ボタンはどこにいった?
はい、途中経過版では受付完了ボタンがありません。jQueryのように、テーブルの行を作成するときに、同時に個別のIDを作成しておいて、そのIDで変化させるところを指定するということができません。Vue.jsでID属性を使うのは<div id="app"></div>
だけなのです。
CDNでだって、子コンポーネントは使えるのだ!
「あ、そうなんだ!」と目から鱗が落ちたのは、私だけかもしれません。子コンポーネントはコンパイルしないと使えないのだと勝手に思いこんでいたので、jQuery版をVue.jsで置き換えることができた時には、アハ体験を感じました。だからこの記事を書いているんですが、なので、心の中で、ドラムロールを鳴らしながら、次の完成版を見てください、タダァ!
See the Pen reception-with-vuejs-finished by Isamu Suzuki (@isamusuzuki) on CodePen.
Vue.js完成版を解説
HTMLコードでの変更点は、26行目に<arrived-button>
が増えているところだけです。繰り返しますが<div id="app"></div>
の内部は、そのまま表示されるHTMLコードではなく、Vue.jsのテンプレートとなります。なのでどんな要素が来てもオーケーなのです。Vue.jsが変換してから表示します。
<div id="app">
<section class="section">
<div class="container">
<span class="is-size-3">
受付(総{{ total }}、済{{ already }}、未{{ notyet }})
</span>
</div>
<div class="container">
<table class="table">
<thead>
<tr>
<th>#</th>
<th>御社名</th>
<th>ご芳名</th>
<th>担当者</th>
<th>受付時間</th>
</tr>
</thead>
<tbody>
<tr v-for="guest in guests" v-bind:key="guest.id">
<td>{{guest.id}}</td>
<td>{{guest.company}}</td>
<td>{{guest.name}}</td>
<td>{{guest.host}}</td>
<td>
<arrived-button
v-bind:id="guest.id"
v-bind:flag="guest.is_arrived"
v-bind:date="guest.arrived_date"
v-on:child-event="onArrived">
</arrived-button>
</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
JSコードのほうも、先に下の親コンポーネントを見てみてください。componentsというオプションが増えただけです。これで子コンポーネントの登録が終わります。この中身は、上の子コンポーネントで定義しています。
子コンポーネントのオプションをじっくりみてみます。
-
props
... プロパティと読む、ここ経由で親コンポーネントからデータを渡してもらえる -
data
... 親コンポーネントと同じ変数群だが、関数になっていることに注目。子コンポーネントは複数作られても、それぞれ個別の変数群を持つ -
methods
... 親コンポーネントと同じ関数群 -
computed
... 呼ばれたときに計算されて返される、内部変数っぽいもの -
template
... 子コンポーネントのテンプレート
すでに<div id="app"></div>
の内部は、親コンポーネントのテンプレートとして使われてしまっているので、子コンポーネントのテンプレートは定義の中に書き込んでしまいます。ひと固まりになっているので、だいぶ読み解きやすいと思います。
// 子コンポーネント
const ArrivedButton = {
props: ['id', 'flag', 'date'],
data: function() {
return {
flag_d: this.flag,
date_d: this.date
}
},
methods: {
postApi: function() {
// Ajaxで、arrived apiにアクセスしたつもり
this.flag_d = 1;
this.date_d = new Date();
this.$emit('child-event');
}
},
computed: {
done: function() {
return this.flag_d === 1;
},
done_date: function() {
moment.locale('ja');
return moment(this.date_d).format('HH:mm:ss');
}
},
template: `<span>
<span v-if="done">{{ done_date }}</span>
<span v-else>
<button class="button is-small is-link"
v-on:click="postApi">受付完了</button>
</span>
</span>`
};
// 親コンポーネント
const app = new Vue({
el: '#app',
data: {
guests: [],
total: 0,
already: 0,
notyet: 0
},
methods: {
createSummary: function() {
// Ajaxで、summary apiにアクセスしたつもり
this.total = result1.total;
this.already = result1.already;
this.notyet = result1.notyet;
},
createTable: function() {
// Ajaxで、guests apiにアクセスしたつもり
this.guests = result2;
},
onArrived: function() {
this.already += 1;
this.notyet -= 1;
}
},
created: function() {
// ページ読み込み後ロード
this.createSummary();
this.createTable();
},
components: {
'arrived-button': ArrivedButton
}
});
親子コンポーネント間の意思疎通(親から子へ)
親から子への意思疎通は、子コンポーネントのprops
(プロパティ)を使います。v-forディレクティブの中に配置されているので、guests配列の数だけ生成され、それぞれ違うデータが与えられます。
propsはあくまで親から渡されるデータの経路みたいなもので、子コンポーネントの中でいじろうとすると怒れます。なので、そのままdata
に引き継がせました。
<arrived-button
v-bind:id="guest.id"
v-bind:flag="guest.is_arrived"
v-bind:date="guest.arrived_date"
v-on:child-event="onArrived">
</arrived-button>
親子コンポーネント間の意思疎通(子から親へ)
受付完了ボタンをタップ or クリックしたときに、上にあるカウンターの数字も変化するというのが、子から親への意思疎通にあたります。親コンポーネントが持つonArrivedというメソッドを子コンポーネントが叩けるかという問題ですが、これも実現できました。v-on:child-event="onArrived"
で、子のイベントと親のイベントを結び付けます。そうしておいて、子のほうで$emit
を叩きます。
this.$emit('child-event');
で、jQueryからVue.jsになると何がいいわけ?
「コードが短くなる」と思って始めましたが、残念ながら、むしろ長くなりました。子コンポーネントの分だけ増えた感じです。
私は、「コードが整理整頓された」というのがメリットとして大きいのではないかと思います。どこに何が書かれているかが、はっきりしています。
またプログラミングするときも、パーツごとにやるべきことが決まっているので、早く正確に書けるようになったような気がします。
-
template
は、ディレクティブを使ってdata
と結びつける -
methods
は、data
の変化だけを記述する
逆に言うと、どこに親子コンポーネントの切れ目があるのかが一目でわかるようになると、どんどん早く書けるようになるけど、そこがあやふやだといつまでも完成しない。かえって力業より時間がかかってしまうのかもしれません。
最後に
やさしくわかりやすく書いたつもりですが、これはあくまで私がアハ体験したことのまとめです。
Vue.jsのことをちゃんと理解したいなら、このガイドを頭から読んで、自分でプログラミングしてください。
はじめに — Vue.js
↑とてもよくできていると思います。
面白いと思ったら、いいねをクリックしてください。励みになります。