-ao- ramune blog

©2019 unio / GO2直営からふるラムネ

はじめてのSymfony4 + Vue.js

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

2018年08月12日 (更新:2018年10月12日)
  • PHP
  • Symfony4
  • Vue
チュートリアル対象バージョン

電卓機能

前回 のチュートリアルで電卓のUIコンポーネントを作成したので、今回は実際の電卓機能を実装していきます。

コンポーネント名 機能
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のオーバーフローが起きた場合に呼び出される想定です。

assets/js/vue/components/SimpleCalculator.vue
                
                    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 関数は電卓の内部状態を初期化します。 クリアボタンを押したときに発火させます。

assets/js/vue/components/SimpleCalculator.vue
                
                    clear() {
                        // エラー状態を解除する
                        this.isError = false;

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

calc関数の実装

calc 関数は、値と演算子から計算を行う電卓のコア機能です。 実装コードに移る前に、計算手順をざっくりと洗い出してみましょう。

  • 演算子を入力したタイミングでメモリに数値があれば計算する
  • イコールを押したタイミングでメモリに数値があれば計算する

今回は足し算と引き算のみの電卓なので、計算手順は上記の2パターンが考えられます。 この計算手順をコードにすると下記のようになります。

assets/js/vue/components/SimpleCalculator.vue
                
                    // 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 関数は数値ボタンが押されたときに発火します。 数値の入力とチェック、保存を行います。

assets/js/vue/components/SimpleCalculator.vue
                
                    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に代入します。

assets/js/vue/components/SimpleCalculator.vue
                
                    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に代入することで、計算結果から更に計算を続けることができるようになります。

assets/js/vue/components/SimpleCalculator.vue
                
                    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;
                        }
                    },
                
            

SimpleCalculator.vue完成版

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のコードを参照してみてください。
プロフィール画像
なかのひと:unio

数十年前の牧歌的なインターネッツが好きだった、永遠のモラトリアム人。 ただ、モラトリアムしててもお金は増えないので、しゃかいの厳しさを斜め後ろから眺めつつほそぼそと生活しています。

Twitter GitHub
[広告]