Vue.jsで計算機アプリを作る

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1111652/b780d294-4797-63d1-33ca-b547789ce755.png)


# はじめに
Vue.jsでアプリを作る課題が出たので、せっかくなので記録として残しておく。
計算機アプリって基本的なイメージだけど、いまいち参考になるものがなかったので。。。
他の方の少しでも参考になれば良いかなと思います。
<br>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1111652/beb287ed-efd2-ef12-faa8-965f65dd0586.png)
## 内容
Vue.jsを使って計算機アプリケーションを作ることになりました。
計算機としての機能があれば問題なしです。
シンプルな計算機を作成していきます。

## シンプルな計算機の機能
そもそも電卓の機能とは何かを考える。

- **四則演算ができる（足し算とか掛け算とかですね）今回は%は除きます。**
- **入力と計算結果が画面に表示される**
- **`C`を押したら数字がキャンセルされる**
- **`AC`を押したら全部消える**
- **`.`を押すと小数を入力できて、計算できる**

こんなところでしょうか。
（今回は％は省きます。電卓の%は`60の後に%`のように入力すると0.6と60%の意味になります）

ちょっと調べてみたら電卓にはまだまだ知らない便利機能がたくさんあるようです。
そもそも電卓って最近買わなくなりましたよね。。。

[誰も教えてくれない電卓の機能](https://www.k-intl.co.jp/blog/B_190417A)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1111652/81fe0033-fb05-22c5-e2b5-215a98c801e8.png)

それは置いておいてVue.jsでプロジェクトを作成していきます。

# 計算機アプリを作成する
それではVue CLIを使いながら計算機アプリを作成します。
**Vue CLIはアプリケーション開発に必要なツールやライブラリを簡単に構築できるコマンドラインインターフェイス**です。
## Vue CLIでプロジェクトを作成する。

```vuejs:ターミナル
% vue create calculator
```

`vue create　アプリ名`コマンドを打つと
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1111652/c3e00d5b-6bb5-04d2-52d0-ac6525f5882d.png)

この画面が出ますが、今回は`Vuex`などは使わないので`Default ([Vue 2] babel, eslint)`で作成していきます。

```terminal
🎉  Successfully created project calculator.
👉  Get started with the following commands:

 $ cd calculator
 $ yarn serve
```
作成できました。
確認のためローカルサーバーを立ち上げます。

[![Image from Gyazo](https://i.gyazo.com/9d2e17d4964b491862a8607de0187097.png)](https://gyazo.com/9d2e17d4964b491862a8607de0187097)

Vue.jsの画面が表示されました。

次にcomponents内に新しく`Calculator.vue`を作成します。
[![Image from Gyazo](https://i.gyazo.com/323e4dc093dedefa59d48b88bad0730c.png)](https://gyazo.com/323e4dc093dedefa59d48b88bad0730c)

次に`App.vue`を少し変更してCalculator.vueを呼び出せるようにします。

```vuejs:App.vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <calculator />
  </div>
</template>
<script>
import Calculator from './components/Calculator.vue'

export default {
  name: 'App',
  components: {
    Calculator
  }
}
</script>
```

## 簡単に計算機画面を作る
まずは数字など、入力するボタンや表示画面を簡単に作成します。
今回はこんな感じのシンプルな計算機を作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1111652/dafda8a2-85f9-e8e8-7fdf-bfa66305ba10.png)

単純にやると寂しい感じなのでCSSフレームワークの[Bluma](https://bulma.io/documentation/overview/start/)と[Bluma Swatch](https://jenil.github.io/bulmaswatch/)を使います。
簡単にCDNで今回はやってみます。
`public`下にある`index.html`のheadタグにコピペしてください。

```vue:index.html
<!DOCTYPE html>
<html lang="">

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <link rel="icon" href="<%= BASE_URL %>favicon.ico">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
  <link rel="stylesheet" href="https://unpkg.com/bulmaswatch/lumen/bulmaswatch.min.css">
  <title>
    <%= htmlWebpackPlugin.options.title %>
  </title>
</head>

```
その後に単純にbuttonタグで適当に作成して、
[![Image from Gyazo](https://i.gyazo.com/fcebd71e748da0b874020da1ff8b2518.png)](https://gyazo.com/fcebd71e748da0b874020da1ff8b2518)
こんな感じのそれっぽいのが出来上がりました。

## 計算機能を作成する
それでは次に本体である計算機能を作ります。

```vuejs:Calculator.vue
<template>
  <div class="calc">
    <h1 class="title">calculator</h1>
    <div class="result box" style="text-align: right">
      {{showNum}}
    </div>
    <div class="clear">
      <button @click="clearCurrent" class="button is-danger is-medium">
        C
      </button>
      <button @click="clearAll" class="button is-danger is-medium">AC</button>
    </div>
    <div class="buttons columns">
      <div class="number-buttons colum is-one-third">
        <button
          v-for="num in buttons.num"
          :key="num"
          class="button is-dark"
          @click="selectNumber(num)"
        >
          {{ num }}
        </button>
      </div>
      <div class="column">
        <button
          v-for="operator in buttons.operator"
          :key="operator"
          class="button is-primary is-medium"
          @click="selectOparator(operator)"
        >
          {{ operator }}
        </button>
      </div>
    </div>
    <div class="">
      <button
        @click="showResult"
        class="button is-success is-medium equal-button"
      >
        =
      </button>
    </div>
  </div>
</template>

<script src="https://cdn.jsdelivr.net/npm/decimal.js-light@2.5.0/decimal.min.js"></script>

<script>

export default {
  data() {
    return {
      buttons: {
        num: [
          "7",
          "8",
          "9",
          "4",
          "5",
          "6",
          "1",
          "2",
          "3",
          "0",
          "00",
          ".",
          "+/-"
        ],
        operator: ["+", "-", "×", "÷"]
      },
      isInsertNumber: true, // 数字入力中かどうかを判断
      isResult: false, // =を押した後かどうかを判断
      currentOperator: "", // 現在選択中の演算子
      currentNumber: "0", // 現在選択中の数字
      numArray: [], // 押した数字の配列を格納する
      opeArray: [] // 押した演算子の配列を格納する
    };
  },
  computed: {
    // 状態によって 合計値 or 入力値 を出し分ける
    showNum() {
      let result;
      // 数字が入力中なら次の入力値を出力して
      if (this.isInsertNumber) {
        let text = String(this.currentNumber);
        let newText = text.replace(/^0{1,}([^.])/, "$1");
        result = newText;
      } else {
        // そうでないならtotalメソッドの結果を返す
        result = this.total();
      }
      return result;
    }
  },
  methods: {
    // reduceメソッドを使い、全ての計算結果を出す
    total() {
      return this.numArray.reduce((prev, next, index) => {
        return this.setTotalNum(prev, next, index - 1);
      });
    },
    // 前後の数字を演算子ごとに計算して値を返す
    // decimal.js-lightを使用して誤差によるエラーを回避
    setTotalNum(prev, next, index) {
      if (this.opeArray[index] === "+") {
        let plus = new Decimal(prev).plus(next);
        return plus
      } else if (this.opeArray[index] === "-") {
        let minus = new Decimal(prev).minus(next)
        return minus
      } else if (this.opeArray[index] === "÷") {
        let division = new Decimal(prev).div(next)
        return division
      } else if (this.opeArray[index] === "×") {
        let multiplication = new Decimal(prev).times(next)
        return multiplication;
      }
    },
    // 数字ボタンを押した時
    selectNumber(num) {
      // 正負を逆にする
      if (num === "+/-") {
        if (!this.isInsertNumber) return;
        this.currentNumber = -1 * this.currentNumber;
        return;
      }
      // = を押した直後、一度数字をリセットして次の計算をする
      if (this.isResult) {
        this.currentNumber = "0";
        this.isResult = false;
      }
      // 数字以外が入力された場合は演算子の配列に今の演算子をpushして数字0にし、次の数字を入力できるようにする
      if (!this.isInsertNumber) {
        this.opeArray.push(this.currentOperator);
        this.currentNumber = "0";
        this.isInsertNumber = true;
      }
      // 小数点は１つまでしか入力できない
      if (num === "." && this.currentNumber.indexOf(".") > -1) {
        return;
      }
      this.currentNumber += num;
    },
    // 演算子ボタンを押した時
    selectOparator(operator) {
      if (this.isInsertNumber) {
        this.numArray.push(Number(this.currentNumber));
        this.isInsertNumber = false;
      }
      this.currentOperator = operator;
    },
    // =を押した時に動くメソッド
    showResult() {
      // 数字入力時 かつ current演算子有り かつ まだ=ボタンを押していないとき
      // 入力された数字、演算子をリセットし、currentNumberにtotalメソッドで計算結果を返す
      if (this.isInsertNumber && this.currentOperator && !this.isResult) {
        this.numArray.push(Number(this.currentNumber));
        this.currentOperator = "";
        this.currentNumber = this.total();
        this.numArray = [];
        this.opeArray = [];
        this.isResult = true;
      }
    },
    // cを押した時
    clearCurrent() {
      this.currentNumber = 0;
      this.isInsertNumber = true;
    },
    // acを押した時
    clearAll() {
      this.numArray = [];
      this.opeArray = [];
      this.currentNumber = 0;
      this.currentOperator = "";
      this.isInsertNumber = true;
    }
  }
};
</script>
```
<br>
**javascriptは小数点の計算で誤差が出るみたいなので、ライブラリを入れて防止します。
たまに計算を試していると、数字じゃなくて`3.5e-8`みたいな表示が出ることがあるんですが
改善方法わかる方いたら教えていただけたらありがたいです。**

CSSで見た目を整えていきます。

```css:Calculator.vue
<style>
.calc {
  width: 380px;
  padding: 20px 0;
  margin: 10px auto;
  background-color: rgba(122, 196, 199, 0.808);
  border-radius: 20px;
  box-shadow: 8px 5px rgba(62, 150, 145, 0.6);
}

.title {
  font-size: 20px;
  color: rgb(235, 227, 128);
  text-shadow: 1px 1px rgb(105, 95, 95);
}
.result {
  width: 250px;
  font-size: 30px;
  font-weight: bold;
  margin: 0 auto;
  padding: 8px 10px;
  border: 1px solid;
  background-color: rgb(248, 165, 165);
  color: rgb(231, 220, 220);
  /* 数字が表示できない桁数になったら...で表示 */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.clear {
  width: 300px;
  margin: 10px auto;
  display: flex;
}

.clear button {
  margin: 0 5px;
}

.buttons {
  width: 300px;
  margin: 0 auto 0;
}

.number-buttons button {
  width: 30%;
  margin: 0 0 5px;
  font-size: 15px;
}

.op-buttons button {
  width: 22%;
}

.equal-button {
  margin: 0 auto 0;
  width: 260px;
}
p {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
</style>
```
index.htmlにはCDNでdecimal.js-lightというjavascriptのライブラリを読み込んでいます。

```html:index.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <link rel="icon" href="<%= BASE_URL %>favicon.ico">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
  <link rel="stylesheet" href="https://unpkg.com/bulmaswatch/lumen/bulmaswatch.min.css">
  <script src="https://cdn.jsdelivr.net/npm/decimal.js-light@2.5.0/decimal.min.js"></script>
  <title>
    <%= htmlWebpackPlugin.options.title %>
  </title>
</head>
```
<br>
**実行してみましょう。**

```terminal:ターミナル
% yarn run serve

yarn run v1.22.10
warning ../../package.json: No license field
$ vue-cli-service serve
 INFO  Starting development server...
98% after emitting CopyPlugin

 DONE  Compiled successfully in 4472ms                        9:56:59


 App running at:
  - Local:   http://localhost:8080/ 
  - Network: http://10.2.105.137:8080/

 Note that the development build is not optimized.
 To create a production build, run yarn build.

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1111652/6add1abf-b1dc-626a-0026-57cf6322d2e7.png)


## 完成
**こんな感じの計算機になりました。**
[![Image from Gyazo](https://i.gyazo.com/44c01d3a2f2f255ac3ae3543591980ee.gif)](https://gyazo.com/44c01d3a2f2f255ac3ae3543591980ee)

### せっかくなのでHerokuでデプロイしてみました
**実際に動かしてみたい方はこちらからどうぞ**
↓
#### [Vue.jsでつくった電卓アプリ　「Calculator」](https://calculatormorioka.herokuapp.com/)

## おわりに
いろいろと調べながらコードを読み込みながらどうにかできました。
細かい説明はしていないところと、まだまだ改善できるところはあるので随時気づいたら更新していきます。


#### 参考サイト
> - [コジマノテック【JavaScript】小数点の計算誤差による謎の端数が発生するときの対処（簡易版）](https://kojimanotech.com/2020/12/20/276/)
> - [Qiita JavaScript を使った「電卓Webアプリ」の作り方を中３の息子に教えてみた！（プログラミング初心者向け）](https://qiita.com/yamazaki3104/items/b5351f589fb6644bf910)
> - [Web備忘録 [CSS] 1行で収まらなずに要素の幅を超えた文字列を「…」で省略する方法](https://webbibouroku.com/Blog/Article/css-ellipsis)
> - [メモブログ Vue.js で 計算電卓アプリを作る](https://resistance-underground.hateblo.jp/entry/2018/12/27/185852)
> - [Vue.jsアプリをHerokuにデプロイする](https://ichi.pro/vue-js-saito-o-heroku-ni-depuroi-suru-220117158824753)