はじめてのSymfony4 + Vue.js

7. Vue.jsで作る電卓アプリ①

2018年08月12日 (更新: 2018年10月12日) by unio

前回のチュートリアルで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>
            
        
ボタンの値として、propsnumberを用意します。 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-showv-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にアクセスすると電卓が表示されますが、ボタンを押しても何も起こりません。 それもそのはずで、まだ計算機能は一切実装していません。 次回の章で実際に各コンポーネントの電卓機能を実装していきます。