<div id="main" class="main ui">
    <h1 class="ui centered inverted header">Simple React Calculator</h1>
    <div id="content" class="ui segment"></div>
  </div>
  
  <div id="signature" class="ui three column centered grid">
    <div class="ui raised inverted segment column">
      <p class="ui small centered header">a project by parc6502 (Ibby) - <a href="http://github.com/parc6502">Github</a> | <a href="http://ibby.me">Website</a></p>
    </div>
  </div>
body {
  background-color: #343a40;
}

#main {
  margin-top: 3em;
}

#content {
  width: 700px;
  margin: 3em auto;
}

#calculatorScreen {
  height: 60px;
}

#signature {
  width: 100%;
}
/**
* I've separated the React code, which is essentially the UI controller, and the calculator algorithms. 
*The calculator algorithms were all placed in a module-pattern IIFE at the bottom of the page with only the calculate method exposed 
* (these would usually be in their own file). 
**/

/* React-Related Code */
const operatorDictionary = {
    add: '+',
    subtract: '-',
    multiply: 'x',
    divide: '÷',
};

class CalculatorBody extends React.Component {
  INITIAL_STATE = {
    currentNumber: '',
    currentCalculation: [],
    ans: null,
  };

  state = {
    ...this.INITIAL_STATE
  };

  handleNumberClick = number => {
    var { ans, currentNumber, currentCalculation } = this.state;
    var ansOnDisplay = ans !== null && currentNumber === '' && currentCalculation.length === 0;
    var numberClickedZeroOnDisplay = (currentNumber === '0' || currentNumber === '') && number !== ".";
    var decimalClickedZeroOnDisplay = currentNumber === '' && number === ".";
    var decimalClickedDecimalInCurrentNumber = number === "." && currentNumber.includes('.'); 
    if (decimalClickedDecimalInCurrentNumber) return;
    if (ansOnDisplay || numberClickedZeroOnDisplay) {
      this.setState({
        currentNumber: `${number}`,
      });
    }
    else if (decimalClickedZeroOnDisplay) {
      this.setState({
        currentNumber: `0${number}`,
      });
    }
    else {
      this.setState(prevState => {
       return {
          currentNumber: `${prevState.currentNumber}${number}`, 
        } 
      });
    }
  };
  
  handleOperatorClick = operator => {
    var { currentCalculation, currentNumber, ans } = this.state;
    var consecutiveOperatorClick = Object.keys(operatorDictionary)
      .includes(currentCalculation[currentCalculation.length-1]);
    var ansOnDisplay = ans !== null && currentNumber === '' && currentCalculation.length === 0;
    if (currentNumber !== '') {
      // this.addToDisplay(operatorDictionary[operator]);
      this.setState(prevState => {
        return {
          currentNumber: '',
          currentCalculation: prevState.currentCalculation.concat([Number(prevState.currentNumber), operator])
        }
      });  
    }
    else if (consecutiveOperatorClick) {
      // console.log(this.state.display);
      // var newDisplay = this.state.display.slice(0,-1) + operatorDictionary[operator];
      // this.setDisplay(newDisplay);
      this.setState(prevState => {
        var newCalculation = prevState.currentCalculation.slice(0, -1).concat([operator]);
        return {
          currentCalculation: newCalculation,
        }
      })
    }
    else if (ansOnDisplay) {
      // this.addToDisplay(operatorDictionary[operator]);
      this.setState(prevState => {
        return {
          currentCalculation: prevState.currentCalculation.concat([prevState.ans, operator])
        }
      });      
    }
    
  };
  
  handleEqualsClick = () => {
    if (this.state.currentNumber) {
      let answer = calculator.calculate(this.state.currentCalculation.concat(Number(this.state.currentNumber)));
      if (isFinite(answer)) {
        this.setState({
          currentNumber: '',
          currentCalculation: [],
          ans: answer,
        });        
      }
      else {
        this.handleMathError();
      }
    }
    // console.log(this.state.currentCalculation);
  };

  handleMathError = () => {
    alert("Math Error! Resetting calculator");
    this.resetCalculator();
  };

  handleClearClick = () => {
    this.resetCalculator();    
  };
  
  resetCalculator = () => {
    this.setState({
      ...this.INITIAL_STATE
    });
  }
  
  render() {
    return (
      <div>
        <CalculatorScreen ans={this.state.ans} currentCalculation={this.state.currentCalculation} currentNumber={this.state.currentNumber}/>
        <ClearButton handleClick={this.handleClearClick} />
        <div className="four ui buttons">
          <NumberButton handleClick={this.handleNumberClick} number={7}/>
          <NumberButton handleClick={this.handleNumberClick} number={8}/>
          <NumberButton handleClick={this.handleNumberClick} number={9}/>
          <OperatorButton handleClick={this.handleOperatorClick} operator="divide" />
        </div>
        <div className="four ui buttons">
          <NumberButton handleClick={this.handleNumberClick} number={4}/>
          <NumberButton handleClick={this.handleNumberClick} number={5}/>
          <NumberButton handleClick={this.handleNumberClick} number={6}/>
          <OperatorButton handleClick={this.handleOperatorClick} operator="multiply" />
        </div>
        <div className="four ui buttons">
          <NumberButton handleClick={this.handleNumberClick} number={1}/>
          <NumberButton handleClick={this.handleNumberClick} number={2}/>
          <NumberButton handleClick={this.handleNumberClick} number={3}/>
          <OperatorButton handleClick={this.handleOperatorClick} operator="subtract" />
        </div>
        <div className="four ui buttons">  
          <NumberButton handleClick={this.handleNumberClick} number='.' />
          <NumberButton handleClick={this.handleNumberClick} number={0}/>
          <EqualsButton handleClick={this.handleEqualsClick} />
          <OperatorButton handleClick={this.handleOperatorClick} operator="add" />
        </div>
      </div>
    );
  }

};

// 20/09 update: display is now derived from other values
class CalculatorScreen extends React.Component {
  prettify = calculation => calculation.map(val => Object.keys(operatorDictionary).includes(val) ? operatorDictionary[val] : val).join('');
  render() {
    const { ans, currentCalculation, currentNumber } = this.props;
    const humanFriendlyCalculation = this.prettify(currentCalculation);
    let display = currentNumber !== '' || currentCalculation.length > 0 ? `${humanFriendlyCalculation}${currentNumber}` : `${ans||'0'}`;
    return (
      <div className="ui message"><p id="display" className="ui right aligned header">{display}</p></div>
    );
  }
}

class NumberButton extends React.Component {
  handleClick = () => {
    this.props.handleClick(this.props.number);
  };
  
  render() {
    var textNumbers = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"]
    return (
      <button id={textNumbers[this.props.number]||'decimal'} className="ui large fluid black button" onClick={this.handleClick}>{this.props.number}</button>
    );
  }
}

class OperatorButton extends React.Component {
  handleClick = () => {
    this.props.handleClick(this.props.operator);
  };
  
  render() {
    return (
      <button id={this.props.operator} className="ui large fluid grey button" onClick={this.handleClick}>{operatorDictionary[this.props.operator]}</button>
    );
  }
}

class EqualsButton extends React.Component {
  render() {
    return (
      <button id="equals" className="ui large fluid blue button" onClick={this.props.handleClick}>=</button>
    );
  }
}

class ClearButton extends React.Component {
  render() {
    return (
      <button id='clear' className="ui fluid red button" onClick={this.props.handleClick}>Clear</button>
    );
  }
}

ReactDOM.render(
  <CalculatorBody />,
  document.getElementById('content')
);

/* Calculator IIFE */
// calculator.calculate is the only exposed method, and it takes in an array of numbers and operators (e.g. `[3, 'plus', 7, 'divide', 10]`) and returns the result. 
// It doesn't accept brackets.
window.calculator = (function(){
  var operators = {
    '^': {
      precedence: 4,
      leftAssociative: false,
      calculate(x, y) {
        return Math.pow(x, y);
      },
    },
    'multiply': {
      precedence: 3,
      leftAssociative: true,
      calculate(x, y) {
        return x * y;
      },
    },
    'divide': {
      precedence: 3,
      leftAssociative: true,
      calculate(x, y) {
        return x / y;
      },
    },
    'add': {
      precedence: 2,
      leftAssociative: true,
      calculate(x, y) {
        return x + y;
      },
    },
    'subtract': {
      precedence: 2,
      leftAssociative: true,
      calculate(x, y) {
        return x - y;
      },
    },
  };

  var calculator = Object.assign(Object.create(operators),{

    postfixEvaluator(calculationArray) {
      var resultStack = [];
      for (let i = 0; i < calculationArray.length; i++) {
        let token = calculationArray[i];
        if (this[token]) {
          let operand_2 = resultStack.pop();
          let operand_1 = resultStack.pop();
          resultStack.push(this[token].calculate(operand_1, operand_2));
        }
        else {
          resultStack.push(token);
        }
      }
      return resultStack.pop();
    },
    shuntingYard(calculationArray) {
      var output = [];
      var oper = [];
      for (let i = 0; i<calculationArray.length; i++) {
        let token = calculationArray[i];
        if (typeof token === 'number') {
          output.push(token);
        } else {
          while (
            oper.length > 0 &&
            (this[oper[oper.length - 1]].precedence >
              this[token].precedence ||
              (this[oper[oper.length - 1]].precedence ===
                this[token].precedence &&
                this[oper[oper.length - 1]].leftAssociative))
          ) {
            output.push(oper.pop());
          }
          oper.push(token);
        }
      }
      while (oper.length > 0) {
        output.push(oper.pop());
      }
      return output;
    },
    calculate(inputArray) {
      var postfixArray = this.shuntingYard(inputArray);
      // console.log(postfixArray);
      return this.postfixEvaluator(postfixArray);
    }

  });

  console.log(calculator.calculate([3, '-', 2])); // 1
  
  return {
    calculate: input => calculator.calculate(input),
  };
})();
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. //unpkg.com/react/umd/react.development.js
  2. //unpkg.com/react-dom/umd/react-dom.development.js
  3. https://cdn.freecodecamp.org/testable-projects-fcc/v1/bundle.js