はじめてのSymfony4 + Vue.js

8. Vue.jsで作る電卓アプリ②

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

電卓機能

前回のチュートリアルで電卓の各コンポーネントを作成しましたが、 計算機能は一切実装していないので何もできない状態でした。 今回は各コンポーネントに実際の電卓機能を実装していきます。
コンポーネント名 機能
SimpleCalculator 数値の管理や計算を行う。
NumberButtonとFunctionButtonから値を受け取り計算する。
計算結果はCalcDisplayへ通知する。
CalcDisplay 数字や計算結果を表示する
NumberButton ボタンが押されたらSimpleCalculatorに数字を通知する
FunctionButton ボタンが押されたらSimpleCalculatorに演算子を通知する
数値の管理から計算まで、すべての機能をSimpleCalculatorのmethodsに用意します。
assets/js/vue/components/SimpleCalculator.vue
            
                <template>
                    ...
                </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,
                            };
                        },
                        methods: {
                            error() {
                                // 計算に問題が起きたときのイベント
                            },
                            clear() {
                                // クリアボタンを押したときのイベント
                            },
                            calc(memory, func, input) {
                                // 計算
                            },
                            inputNumber(number) {
                                // 数値ボタンを押したときのイベント
                            },
                            inputFunction(func) {
                                // 演算子ボタンを押したときのイベント
                            },
                            equal() {
                                // イコールボタンを押したときのイベント
                            },
                        },
                    }
                </script>
                <style scoped>
                    ...
                <style>
            
        

error関数の実装

計算時に問題が起きたときに発火させるerror関数を実装します。 今回は整数の足し算と引き算のみを扱うので、intのオーバーフローが起きた場合に呼び出される想定です。
            
                data() {
                    return {
                        memory: null,
                        func: null,
                        inputValue: 0,

                        // エラー状態を管理するための変数を用意
                        isError: false,
                    };
                },
                methods: {
                    error() {
                        this.memory = null;
                        this.func = null;
                        this.inputValue = 'ERROR';

                        // エラー状態にする
                        this.isError = true;
                    },
                    ...
                },
            
        
エラーが起きたら液晶にERRORの表示と、クリア以外の操作を受け付けられないようにisErrorをtrueにします。

clear関数の実装

clear関数は電卓の内部状態を初期化します。 クリアボタンを押したときに発火させます。
            
                clear() {
                    // エラー状態を解除する
                    this.isError = false;

                    // 入力値を0に初期化させる
                    this.memory = null;
                    this.func = null;
                    this.inputValue = 0;
                },
            
        

calc関数の実装

calc関数は、値と演算子から計算を行う電卓のコア機能です。 実装コードに移る前に、計算手順をざっくりと洗い出してみましょう。
  • 演算子を入力したタイミングでメモリに数値があれば計算する
  • イコールを押したタイミングでメモリに数値があれば計算する
今回は足し算と引き算のみの電卓なので、計算手順は上記の2パターンが考えられます。 この計算手順をコードにすると下記のようになります。
            
                // memoryと演算子のfunc、現在の入力値をinputで受け取る
                calc(memory, func, input) {
                    // メモリが無い場合は入力値をそのまま返す
                    if (memory === null) {
                        return input;
                    }

                    let val = NaN;
                    switch (func) {
                        case '+':
                        case '-':
                            let bias = func === '+' ? 1 : -1;
                            val = memory + (bias * input);
                            break;
                    }

                    // 想定外の演算子や、計算結果に問題がある場合はエラー
                    if (!Number.isSafeInteger(val)) {
                        // error関数で電卓をエラー状態にする
                        this.error();
                        return NaN;
                    }

                    return val;
                },
            
        
this.error()のように、同じvueインスタンス内の関数はthisで呼び出せます。

+-しかないのでswitch文はあまり意味の無いように見えますが、 今後他の演算子が出てきたときを想定してこのように実装しています。

inputNumber関数の実装

inputNumber関数は数値ボタンが押されたときに発火します。 数値の入力とチェック、保存を行います。
            
                inputNumber(number) {
                    // エラー状態のときは入力を受け付けない
                    if (this.isError) {
                        return;
                    }

                    // 右の値がゼロの場合は入力数値をそのまま代入
                    if (this.inputValue === 0) {
                        this.inputValue = number;
                        return;
                    }

                    // すでに保存されている数値の右に、入力された数字を付け足す
                    let parsed = parseInt(this.inputValue.toString() + number.toString());

                    // 入力数値がintの範囲を超えていないかのチェック
                    // int範囲を超えた場合は、追加で入力されないようにする
                    if (Number.isSafeInteger(parsed)) {
                        this.inputValue = parsed;
                    }
                },
            
        

inputFunction関数の実装

inputFunction関数は演算子ボタンが押されたときに発火します。 ボタンが押されるたびに計算を行い、結果をmemoryに代入します。
            
                inputFunction(func) {
                    if (this.isError) {
                        return;
                    }
    
                    let val = this.calc(this.memory, this.inputValue, func);
                    if (!isNaN(val)) {
                        this.memory = val;
                        this.func = func;
                        this.inputValue = 0;
                    }
                },
            
        

equal関数の実装

equal関数はイコールボタンが押されたときに発火します。 memoryとinputValueを演算子で計算し、結果をinputValueに代入します。 inputValueに代入することで、計算結果から更に計算を続けることができるようになります。
            
                equal() {
                    // エラーや式が完成していない場合
                    if (this.isError || this.memory === null || this.func === null) {
                        return;
                    }

                    let val = this.calc(this.memory, this.func, this.inputValue);
                    if (!isNaN(val)) {
                        this.memory = null;
                        this.func = null;
                        this.inputValue = val;
                    }
                },
            
        
最終的なmethodsは下記のとおりです。
assets/js/vue/components/SimpleCalculator.vue
            
                <template>...</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,
                                isError: false,
                            };
                        },
                        methods: {
                            error() {
                                this.isError = true;
                                this.memory = null;
                                this.func = null;
                                this.inputValue = 'ERROR';
                            },
                            clear() {
                                this.isError = false;
                                this.memory = null;
                                this.func = null;
                                this.inputValue = 0;
                            },
                            calc(memory, func, input) {
                                if (memory === null) {
                                    return input;
                                }

                                let val = NaN;
                                switch (func) {
                                    case '+':
                                    case '-':
                                        let bias = func === '+' ? 1 : -1;
                                        val = memory + (bias * input);
                                        break;
                                }
                                if (!Number.isSafeInteger(val)) {
                                    this.error();
                                    return NaN;
                                }

                                return val;
                            },
                            inputNumber(number) {
                                if (this.isError) {
                                    return;
                                }

                                if (this.inputValue === 0) {
                                    this.inputValue = number;
                                    return;
                                }
                                let parsed = parseInt(this.inputValue.toString() + number.toString());
                                if (Number.isSafeInteger(parsed)) {
                                    this.inputValue = parsed;
                                }
                            },
                            inputFunction(func) {
                                if (this.isError) {
                                    return;
                                }

                                let val = this.calc(this.memory, func, this.inputValue);
                                if (!isNaN(val)) {
                                    this.memory = val;
                                    this.func = func;
                                    this.inputValue = 0;
                                }
                            },
                            equal() {
                                if (this.isError || this.memory === null || this.func === null) {
                                    return;
                                }

                                let val = this.calc(this.memory, this.func, this.inputValue);
                                if (!isNaN(val)) {
                                    this.memory = null;
                                    this.func = null;
                                    this.inputValue = val;
                                }
                            },
                        },
                    }
                </script>
                <style scoped>...<style>
            
        

ボタン操作

数値の管理や計算はSimpleCalculatorで行いますが、ボタン自体は子コンポーネントに委ねられています。 ボタンコンポーネントで起きたイベントをSimpleCalculatorに通知するために $emit を利用します。 $emitは同一Vueインスタンス内のイベントを発火させることができます。
assets/js/vue/components/calculator/NumberButton.vue
            
                <template>
                    <!-- ボタンクリックでpushイベントを発火 -->
                    <button type="button" v-on:click="$emit('push', number)">{{ number }}</button>
                </template>

                <script>
                    export default {
                        props: {
                            number: Number,
                        },
                    }
                </script>
            
        
assets/js/vue/components/calculator/FunctionButton.vue
            
                <template>
                    <!-- ボタンクリックでpushイベントを発火 -->
                    <button type="button" v-on:click="$emit('push', func)">{{ func }}</button>
                </template>

                <script>
                    export default {
                        props: {
                            func: String,
                        }
                    }
                </script>
            
        
ボタンをクリックした際に'push'イベントを発火させるために、v-on:click$emitを呼び出します。 $emitの第一引数'push'は発火させたいイベント名で、第二引数以降はイベントの引数になります。

次はボタンコンポーネントで発火させたpushイベントをSimpleCalculatorで受け取ります。 $emitで発火させたイベントはv-on:[イベント名]で受け取ることできるので、 各ボタンコンポーネントにv-on:pushを記述します。
assets/js/vue/components/SimpleCalculator.vue
            
                <template>
                    <div class="calculator">
                        ...
                        <div class="row">
                            <function-button v-bind:func="'+'" v-on:push="inputFunction"/>
                            <function-button v-bind:func="'-'" v-on:push="inputFunction"/>
                            <function-button v-bind:func="'C'" v-on:push="clear"/>
                        </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"
                                    v-on:push="inputNumber"/>
                        </div>
                        <div class="row">
                            <number-button v-bind:number="0" v-on:push="inputNumber"/>
                            <function-button v-bind:func="'='" v-on:push="equal"/>
                        </div>
                    </div>
                </template>
                <script>...</script>
                <style scoped>...<style>
            
        
v-on:pushにはmethodsに作成した関数をそれぞれ登録します。 $emitの第二引数以降に登録した変数は、ここで登録した関数の引数として渡されます。

最終的な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="'+'" v-on:push="inputFunction"/>
                            <function-button v-bind:func="'-'" v-on:push="inputFunction"/>
                            <function-button v-bind:func="'C'" v-on:push="clear"/>
                        </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"
                                    v-on:push="inputNumber"/>
                        </div>
                        <div class="row">
                            <number-button v-bind:number="0" v-on:push="inputNumber"/>
                            <function-button v-bind:func="'='" v-on:push="equal"/>
                        </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,
                                isError: false,
                            };
                        },
                        methods: {
                            error() {
                                this.isError = true;
                                this.memory = null;
                                this.func = null;
                                this.inputValue = 'ERROR';
                            },
                            clear() {
                                this.isError = false;
                                this.memory = null;
                                this.func = null;
                                this.inputValue = 0;
                            },
                            calc(memory, func, input) {
                                if (memory === null) {
                                    return input;
                                }

                                let val = NaN;
                                switch (func) {
                                    case '+':
                                    case '-':
                                        let bias = func === '+' ? 1 : -1;
                                        val = memory + (bias * input);
                                        break;
                                }
                                if (!Number.isSafeInteger(val)) {
                                    this.error();
                                    return NaN;
                                }

                                return val;
                            },
                            inputNumber(number) {
                                if (this.isError) {
                                    return;
                                }

                                if (this.inputValue === 0) {
                                    this.inputValue = number;
                                    return;
                                }
                                let parsed = parseInt(this.inputValue.toString() + number.toString());
                                if (Number.isSafeInteger(parsed)) {
                                    this.inputValue = parsed;
                                }
                            },
                            inputFunction(func) {
                                if (this.isError) {
                                    return;
                                }

                                let val = this.calc(this.memory, func, this.inputValue);
                                if (!isNaN(val)) {
                                    this.memory = val;
                                    this.func = func;
                                    this.inputValue = 0;
                                }
                            },
                            equal() {
                                if (this.isError || this.memory === null || this.func === null) {
                                    return;
                                }

                                let val = this.calc(this.memory, this.func, this.inputValue);
                                if (!isNaN(val)) {
                                    this.memory = null;
                                    this.func = null;
                                    this.inputValue = val;
                                }
                            },
                        },
                    }
                </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>
            
        
最後にwabepackでコンパイルしてブラウザで動作を確認してみましょう。
            
                yarn dev
                bin/console server:start
            
        
今回は数値の管理をかなりベタに扱いました。 次回は状態管理ライブラリのVuexを利用して電卓アプリを改造してみましょう。

なお、ここまでのコードをgithubに上げておきました。 もし今までのチュートリアルでうまく行かなかった場合は、githubのコードを参照してみてください。