Simple Calculator using Vue and Bootstrap 5

The HTML Markup

Bootstrap 5 provides all the UI styling and responsive behavior. It's a great fit with Vue now that it's no longer dependent on jQuery. The Bootstrap grid row & col-* are used to responsively change the calculator width as screen size changes.

<div class="row">
        <div class="col-xxl-2 col-lg-3 col-md-4 col-sm-6 mx-auto bg-dark rounded-3 shadow-sm p-3">
            <input class="form-control form-control-lg text-success" v-model="calculator.displayValue" />
            <!-- calculator number pad using grid -->
            <div class="row g-0 text-center mt-2">
                <div class="col-auto text-white">
                    <div class="row g-1 g-lg-1">
                        <div v-for="(key,i) in keypad" :key="i" class="ms-auto col-3 py-2">
                            <button class="btn btn-dark text-warning w-100" @click="processKey(key.value)">{{ key.label }}</button>
                        </div>
                        <div class="col-12 pt-2">
                            <button class="btn btn-dark border-secondary btn-lg text-warning w-100 fw-bold lead" @click="processKey('=')">=</button>
                        </div>
                        <div class="col-12">
                            <div v-if="errValue" class="alert alert-warning p-2 text-truncate small" role="alert">
                              {{ errValue }}
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

The Vue App Data

Calculator state is kept in it's own data calculator object to track the displayValue, firstOperand and operator. waitingForSecondOperand let's us know when it's ready to perform a calculation.

The keypad array is ordered by how I wanted the keys to render as columns inside the Bootstrap row...

<div v-for="(key,i) in keypad" :key="i" class="ms-auto col-3 py-2">
   <button class="btn btn-dark w-100" @click="processKey(key.value)">{{ key.label }}</button>
</div>
...
  data () {
      return {
        calculator: {
            displayValue: '0',
            firstOperand: null,
            waitingForSecondOperand: false,
            operator: null,
        },
        errValue: null,
        keypad: [
            {label:'7', value: 7},
            {label:'8', value: 8},
            {label:'9', value: 9},
            {label:'x', value: '*'},
            {label:'4', value: 4},
            {label:'5', value: 5},
            {label:'6', value: 6},
            {label:'+', value: '+'},
            {label:'1', value: 1},
            {label:'2', value: 2},
            {label:'3', value: 3},
            {label:'-', value: '-'},
            {label:'AC', value: 'AC'},
            {label:'.', value: '.'},
            {label:'0', value: 0},
            {label:'/', value: '/'},
        ],
  },
...

The Vue App Methods

Of course the calculator actually needs to calculate so I define a series of methods to watch the keystrokes and perform basic math functions. Most of them should be self-explanatory. You will see that errValue is used to store any errors, and resets upon keypress or clear.

When a key is pressed, processKey() is called to determine which key and then call the appropriate function...

  • inputDigit() - when any digit is clicked
  • handleOperator() - when any operator is clicked
  • equalPressed() - when the equal sign is clicked
  • inputDecimal() - to handle the decimal point (.)
  • resetCalculator() - to clear the calculator ('AC')
...
  methods: {
    processKey: function(val) {
        this.errValue = null
        switch (val){
          case "AC": this.resetCalculator()
            break;
          case 0:
          case 1:
          case 2:
          case 3:
          case 4:
          case 5:
          case 6:
          case 7:
          case 8:
          case 9: this.inputDigit(val)
          break; 
          case "+": this.handleOperator("+")
            break;
          case "-": this.handleOperator("-")
            break;
          case "/": this.handleOperator("/")
            break;
          case "*": this.handleOperator("*")
            break;
          case "=": this.equalPressed();
            break;
          case ".": this.inputDecimal(".")
              break;
          default:
              this.errValue = 'KEY ERROR: in default'
        }
    },
    equalPressed() {
        const { firstOperand, displayValue, operator } = this.calculator
        try{
            this.calculator.displayValue = this.calculate(firstOperand, displayValue, operator)
        }
        catch (e){
            this.errValue = e
        }
    },
    inputDigit(digit) {
        const { displayValue, waitingForSecondOperand } = this.calculator
        console.log(waitingForSecondOperand)
        if (waitingForSecondOperand === true) {
            this.calculator.displayValue = digit
            this.calculator.waitingForSecondOperand = false
        } else {
            console.log(displayValue)
            this.calculator.displayValue =
                displayValue === '0' ? digit : displayValue + '' + digit
        }
    },
    inputDecimal(dot) {
        const { displayValue, waitingForSecondOperand } = this.calculator
        if (waitingForSecondOperand === true) {
            this.calculator.displayValue = '0.'
            this.calculator.waitingForSecondOperand = false
            return
        }

        // check for existing decimal
        if (displayValue % 1 === 0) {
            this.calculator.displayValue += dot
        }
    },
    handleOperator(nextOperator) {
        const { firstOperand, displayValue, operator, waitingForSecondOperand } = this.calculator
        const inputValue = parseFloat(displayValue)

        if (operator && waitingForSecondOperand) {
            this.calculator.operator = nextOperator
            return
        }

        if (firstOperand == null && !isNaN(inputValue)) {
            this.calculator.firstOperand = inputValue
        } else if (operator) {
            const currentValue = firstOperand || 0
            const result = this.calculate(currentValue, inputValue, operator)
            this.calculator.displayValue = String(result)
            this.calculator.firstOperand = result
        }

        this.calculator.waitingForSecondOperand = true
        this.calculator.operator = nextOperator
    },
    calculate(firstOperand, secondOperand, operator) {
        if (operator === '+') {
            return firstOperand + secondOperand
        } else if (operator === '-') {
            return firstOperand - secondOperand
        } else if (operator === '*') {
            return firstOperand * secondOperand
        } else if (operator === '/') {
            if (secondOperand == 0){
                this.errValue = 'ERROR: Cannot divide by 0'
            }
            else {
                return firstOperand / secondOperand
            }
        }

        return secondOperand
    },
    resetCalculator() {
      this.calculator.displayValue = '0'
      this.calculator.firstOperand = null
      this.calculator.waitingForSecondOperand = false
      this.calculator.operator = null
    },
  },
...

Thanks for reading!

15