はじめてのSymfony4 + Vue.js
Vue.jsで作る電卓アプリ①
チュートリアル対象バージョン
- PHP 7.2.5
- Symfony 4.1.1 [packagist]
- vue 2.5.16 [npm]
- webpack 3.12.0 [npm]
- @symfony/webpack-encore 0.20.1 [npm]
前回のチュートリアルでVueコンポーネントの基本は学び終わりました。 今回は足し算と引き算のできる単純な電卓アプリを作成して、さらに理解を深めていきましょう。
Babelの導入
ここからはES6の構文を用いて開発を行います。 ブラウザによってはES6をそのまま解釈できないので、jsトランスパイラのBabelを導入します。 前回のチュートリアルで導入している場合はスキップしてください。
コンソールコマンド
yarn add --dev babel-preset-env
webpack.config.js
Encore
...
.configureBabel(function(babelConfig) {
babelConfig.presets.push('env');
})
...
;
電卓アプリの概要
今回は下記の電卓アプリを目標にチュートリアルを進めていきます。(デザインセンスは勘弁してください(´・ω・`))
実際に触って貰うと分かりますが、足し算と引き算ができるシンプルな電卓です。
コンポーネント設計
まずは必要なコンポーネントの検討です。 電卓は「液晶」と「ボタン」から出来上がっているので、今回はこれをコンポーネントに落とし込んでみましょう。
要素 | コンポーネント名 | 役割 |
---|---|---|
電卓 | SimpleCalculator | 電卓全体を表すルートコンポーネント |
液晶 | CalcDisplay | 数字や計算結果を表示する |
数字ボタン | NumberButton | 数字を入力する |
関数ボタン | FunctionButton | クリアや演算子を入力する |
ボタンコンポーネントの作成
数字ボタン NumberButton.vue と、関数ボタン FunctionButton.vue を作成します。
assets/js/vue/components/calculator/NumberButton.vue
<template>
<!-- propsのnumberをボタンのラベルとして表示 -->
<button type="button">{{ number }}</button>
</template>
<script>
export default {
props: {
number: Number,
},
}
</script>
ボタンの値として、 props に number を用意します。 propsで設定した変数は、コンポーネント利用時に引数で渡すことができます。
assets/js/vue/components/calculator/FunctionButton.vue
<template>
<button type="button">{{ func }}</button>
</template>
<script>
export default {
props: {
func: String,
}
}
</script>
同様に関数ボタンのpropsにも func を用意します。
液晶コンポーネントの作成
液晶には数値のメモリ表示と数値ボタンで入力した値、演算子を表示します。
メモリ部分となる値を memory 、演算子を func 、入力値を inputValue としてpropsに用意します。
assets/js/vue/components/calculator/CalcDisplay.vue
<template>
<div class="calc-display">
<span class="memory">{{ memory }}</span>
<span class="function">{{ func }}</span>
<span class="input-value">{{ inputValue }}</span>
</div>
</template>
<script>
export default {
props: {
memory: Number,
func: String,
inputValue: Number,
},
}
</script>
ここで、メモリの値が存在しないときは入力値のみを表示させる仕組みを作ります。 memoryの存在チェックを 適時 行うため、ボタン入力などのイベントによって変化するプロパティを computed を使って検知します。
assets/js/vue/components/calculator/CalcDisplay.vue
<template>
<div class="calc-display">
<span class="memory">{{ memory }}</span>
<span class="function">{{ func }}</span>
<span class="input-value">{{ inputValue }}</span>
</div>
</template>
<script>
export default {
props: {
memory: Number,
func: String,
inputValue: Number,
},
// 算出プロパティ
computed: {
hasMemory() {
return this.memory !== null;
},
},
}
</script>
computed は 算出プロパティ と呼ばれる仕組みで、 memory が更新されると hasMemory が再評価されます。 hasMemoryはmemoryに値が存在しない場合にfalseを返すので、 v-show と組み合わせて表示切り替えを実現することができます。
assets/js/vue/components/calculator/CalcDisplay.vue
<template>
<div class="calc-display">
<!-- hasMemoryがfalseの場合は、display:noneになる -->
<span class="memory" v-show="hasMemory">{{ memory }}</span>
<span class="function" v-show="hasMemory">{{ func }}</span>
<span class="input-value">{{ inputValue }}</span>
</div>
</template>
<script>
export default {
props: {
memory: Number,
func: String,
inputValue: Number,
},
computed: {
hasMemory() {
return this.memory !== null;
},
},
}
</script>
v-show はboolean値に応じてDOMの表示を行うバインド構文です。 falseの場合は対象DOMのstyleに display:none が付与されます。 似たようなバインド構文で v-if がありますが、こちらはfalse評価されるとDOM自体が削除されます。 v-show と v-if の動きの違いから、 状況に応じた使い分けを意識できるとよいです。 最後にstyleも設定して、CalcDisplay.vueは完成です。
assets/js/vue/components/calculator/CalcDisplay.vue
<template>
<div class="calc-display">
<span class="memory" v-show="hasMemory">{{ memory }}</span>
<span class="function" v-show="hasMemory">{{ func }}</span>
<span class="input-value">{{ inputValue }}</span>
</div>
</template>
<script>
export default {
props: {
memory: Number,
func: String,
inputValue: Number,
},
computed: {
hasMemory() {
return this.memory !== null;
},
},
}
</script>
<style scoped>
.calc-display {
position: relative;
text-align: right;
}
.memory {
color: #9c9c9c;
font-size: 1.3rem;
font-style: italic;
}
.function {
color: #9c9c9c;
}
.input-value {
background-color: #d8fcff;
font-weight: bold;
font-size: 2rem;
width: 10rem;
}
</style>
電卓ルートコンポーネントの作成
ボタンコンポーネントと液晶コンポーネントを電卓にまとめます。 SimpleCalculatorを親コンポーネントにして、今まで作成したコンポーネントを配置していきます。 コンポーネント内で別のコンポーネントを使うために、scriptで各SFCをimportして components に定義しましょう。
assets/js/vue/components/SimpleCalculator.vue
<script>
import CalcDisplay from "./components/calculator/CalcDisplay";
import FunctionButton from "./components/calculator/FunctionButton";
import NumberButton from "./components/calculator/NumberButton";
export default {
components: {
CalcDisplay,
FunctionButton,
NumberButton,
},
}
</script>
これでSimpleCalculator内で各コンポーネントを使う準備が整いました。 まず液晶を配置するために、scriptのdataにメモリと演算子、入力値の変数を用意します。 このdataが今回の電卓のコアステートメントとなります。 templateに calc-display タグで液晶コンポーネントを作成し、 v-bind でdataの各値をコンポーネントのpropsにバインドします。
assets/js/vue/components/SimpleCalculator.vue
<template>
<div class="calculator">
<div class="row">
<!-- v-bindでコンポーネントのpropsに値をバインドする -->
<calc-display
v-bind:memory="memory"
v-bind:func="func"
v-bind:inputValue="inputValue"/>
</div>
</div>
</template>
<script>
import CalcDisplay from "./components/calculator/CalcDisplay";
import FunctionButton from "./components/calculator/FunctionButton";
import NumberButton from "./components/calculator/NumberButton";
export default {
components: {
CalcDisplay,
FunctionButton,
NumberButton,
},
data() {
return {
// 電卓のステートはSimpleCalculatorで管理する
memory: null,
func: null,
inputValue: 0,
};
},
}
</script>
data() はES6から導入された 簡略構文 です。
data() {
return {
memory: null,
func: null,
inputValue: 0,
};
},
これは下記の定義と同じです。
data: function() { ... },
次はボタンを配置していきます。 ボタンの値は先程と同様に、v-bindでpropsにバインドしていきます。 その際、関数ボタンの文字を渡すときは必ずシングルクォートで囲むようにしてください (vueインスタンスのプロパティと区別がつかずエラーが出るため)。
assets/js/vue/components/SimpleCalculator.vue
<template>
<div class="calculator">
<div class="row">
<calc-display
v-bind:memory="memory"
v-bind:func="func"
v-bind:inputValue="inputValue"/>
</div>
<div class="row">
<!-- 文字列を渡す際はシングルクォートで囲むこと -->
<function-button v-bind:func="'+'"/>
<function-button v-bind:func="'-'"/>
<function-button v-bind:func="'C'"/>
</div>
<div class="row" v-for="row in [[7,8,9],[4,5,6],[1,2,3]]">
<!-- v-forでコンポーネントを配置する際は、keyの設定を忘れずに -->
<number-button
v-for="number in row"
v-bind:key="number"
v-bind:number="number"/>
</div>
<div class="row">
<number-button v-bind:number="0"/>
<function-button v-bind:func="'='"/>
</div>
</div>
</template>
<script>
import CalcDisplay from "./components/calculator/CalcDisplay";
import FunctionButton from "./components/calculator/FunctionButton";
import NumberButton from "./components/calculator/NumberButton";
export default {
components: {
CalcDisplay,
FunctionButton,
NumberButton,
},
data() {
return {
memory: null,
func: null,
inputValue: 0,
};
},
}
</script>
数値ボタンは v-for="[変数] in [配列]" というfor文で配置しています。 [配列] はL15のように配列を直接記述したり、L18のように変数を使うことができます。 このようにfor文を使うと少ない記述で済みますが、各コンポーネントにkeyを付け忘れると警告が出るため注意してください。 keyは仮想DOMによる差分レンダリングを行う際に、どの要素を書き換えるかのインデックスとして利用されるため、実際のDOMには出力されません。
最後にstyleも設定して、SimpleCalculator.vueは完成です。
assets/js/vue/components/SimpleCalculator.vue
<template>
<div class="calculator">
<div class="row">
<calc-display
v-bind:memory="memory"
v-bind:func="func"
v-bind:inputValue="inputValue"/>
</div>
<div class="row">
<function-button v-bind:func="'+'"/>
<function-button v-bind:func="'-'"/>
<function-button v-bind:func="'C'"/>
</div>
<div class="row" v-for="row in [[7,8,9],[4,5,6],[1,2,3]]">
<number-button
v-for="number in row"
v-bind:key="number"
v-bind:number="number"/>
</div>
<div class="row">
<number-button v-bind:number="0"/>
<function-button v-bind:func="'='"/>
</div>
</div>
</template>
<script>
import CalcDisplay from "./components/calculator/CalcDisplay";
import FunctionButton from "./components/calculator/FunctionButton";
import NumberButton from "./components/calculator/NumberButton";
export default {
components: {
CalcDisplay,
FunctionButton,
NumberButton,
},
data() {
return {
memory: null,
func: null,
inputValue: 0,
};
},
}
</script>
<style scoped>
.calculator {
border-radius: 0 2rem 0 2rem;
padding: 1rem;
background-color: #ffe7aa;
}
.row {
text-align: center;
}
.row:not(:last-child) {
margin-bottom: 0.5rem;
}
button:not(:last-child) {
margin-right: 0.5rem;
}
</style>
jsとコントローラの用意
電卓コンポーネント一式が完成したので、calculator.jsとwebpack.config.js、コントローラを作成します。
assets/js/calculator.js
import Vue from 'vue'
import SimpleCalculator from './vue/SimpleCalculator'
new Vue({
el: '#calculator',
template: '<simple-calculator/>',
components: {SimpleCalculator}
});
calculator.jsが今回作成した電卓アプリのエントリーポイントとなります。
webpack.config.js
Encore
...
.enableVueLoader()
.configureBabel(function(babelConfig) {
babelConfig.presets.push('env');
})
.addEntry('calapp', './assets/js/calculator.js')
...
;
src/Controller/TutorialController.php
/**
* @Route("/tutorial")
*/
class TutorialController extends Controller
{
...
/**
* @Route("/calculator", name="tutorial_calculator", methods="GET")
* @Template
*/
public function calculator(): array
{
return [];
}
}
templates/tutorial/calculator.html.twig
<!DOCTYPE html>
<html>
<head>
<title>チュートリアル</title>
<script defer src="{{ asset('build/calapp.js') }}"></script>
<link rel="stylesheet" href="{{ asset('build/calapp.css') }}"/>
<style type="text/css">
body {
display: flex;
align-items: center;
justify-content: center;
}
</style>
</head>
<body>
<div id="calculator"></div>
</body>
</html>
電卓?
ここまでのファイルをすべて用意できたら、実際にブラウザで確認してみましょう。
コンソールコマンド
yarn dev
bin/console server:start
localhost:8000 にアクセスすると電卓が表示されますが、ボタンを押しても何も起こりません。
それもそのはずで、まだ計算機能は一切実装していません。 次回の章で実際に各コンポーネントの電卓機能を実装していきます。