LoginSignup
12
9

More than 3 years have passed since last update.

【React + Material-UI】電卓アプリを作ろう!【2020年4月版】

Last updated at Posted at 2020-04-04

はじめに

この記事は「Reactのチュートリアルを終えて、何かを作ってみたい」という読者を想定しています。

分からない部分が出た場合はReactの公式ドキュメントに立ち返りましょう。

material-uiの公式ドキュメントも要チェック!

※更新 2020/07/23 桁数調整を追加. 変数名を修正. gifを追加
※初稿 2020/04/05

作ったもの

ezgif.com-video-to-gif.gif

iPhoneに入っている電卓アプリを参考にして作りました。

戦略

ReactコンポーネントにはMaterial-UIを使い、保持するstateをReactのuseStateで実装します。

component、containerの洗い出し

component

  • ボタン: 押したら何かしらの動作を起こすコンポーネント
    スクリーンショット 2020-04-05 0.20.00.png

  • ディスプレイ: 上部にある、数字を表示するためのコンポーネント

スクリーンショット 2020-04-05 0.20.57.png

container

  • calculator: ボタンとディスプレイを集結させたもの。今回はこれ自体がアプリとなる。

stateの洗い出し

iPhoneの電卓アプリをいじってみて、どんなstateが必要かを考えます。
はじめにざっくりと書き出して、実際には実装しながら追加していきました。

  1. firstNumber: A + BAにあたる数の文字列
  2. secondNumber: A + BBにあたる数の文字列
  3. operator: add(加算), subtract(減算), divide(除算), multiply(乗算)。operatorが指定されていないときはfalse。
  4. isAllClear: clearボタンがAC / Cどちらかを判別するboolean。
  5. isDicimalInput: .ボタンが押されて小数点以下入力になっているか判別するboolean。
  6. isAnswerDisplay: =ボタンが押された直後には特殊な挙動をするので、判別するbooleanを用意しておく。

たとえば、 5 * 7 = と入力した場合、
firstNumberは5, secondNumberは7, operatorは"multiply", isAnswerDisplayはtrueとなる。

インストール

create react app

npx create-react-app react-calculator
cd react-calculator

@material-ui/core

// npm を使う場合
npm install @material-ui/core

// yarn を使う場合
yarn add @material-ui/core

バージョン情報

  • react: 16.13.1
  • @material-ui/core: 4.9.8

ファイル構成

スクリーンショット 2020-04-04 23.04.52.png

コンポーネントの実装

Display

Display.js
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Box from '@material-ui/core/Box';

const useStyles = makeStyles((theme) => ({
    box: {
        fontSize: "60px",
        width: "100%",
        margin: "0 auto",
    },
  }));

export default function Display(props) {
  const classes = useStyles();

  return (
      <div>
       <Box className={classes.box}>
            {props.children}
       </Box>
      </div>
  );
}

ポイント

props.childrenとすることで、<Display></Display>ではさんだものを表示できます。

MyButton

MyButton.js
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';

export default function MyButton(props) {
  const useStyles = makeStyles((theme) => ({
    margin: {
      margin: theme.spacing(1),
      backgroundColor: props.backgroundColor
    },
  }));
  const classes = useStyles();


  return (
      <div>
        <Button className={classes.margin} onClick={props.onClick}>
            {props.children}
        </Button>
      </div>
  );
}

ポイント

ボタンコンポーネントは親から

  1. 背景色
  2. クリックしたときに実行する関数
  3. ボタンの文字

を受け取ります。

親コンポーネントで<MyButton onClick={somthing}>としても関数は実行されません。
子のButtonのonClickに接続する必要があります

コンテナの実装

長いコードなのでコメントアウトで説明します。

Calculator.js
import React, {useState} from 'react';

//components
import MyButton from '../components/MyButton';
import Display from '../components/Display';

//material-ui
import Box from '@material-ui/core/Box';
import Typography from '@material-ui/core/Typography';

function Calculator(){
    //保持したいstates
    const [firstNumber, setFirstNumber] = useState("0");
    const [secondNumber, setSecondNumber] = useState("0");
    const [operator, setOperator] = useState(false);
    const [isAllClear, setIsAllClear] = useState(true);
    const [isDicimalInput, setIsDicimalInput] = useState(false);
    const [isAnswerDisplay, setIsAnswerDisplay] = useState(false);

    //数字ボタンを押したとき
    const handleNumberClick = (num) => {
        //=ボタンを押した直後に数字を押した場合
        if(isAnswerDisplay){
            setFirstNumber(num);
            setSecondNumber("0");
            setOperator(false);
            setIsAnswerDisplay(false);
            setIsDicimalInput(false);
        } else {
           //演算子が押されていないときはfirstNumberを更新する
           if(!operator){
                if(isDicimalInput){
                    setFirstNumber(String(firstNumber) + String(num))
                } else {
                    setFirstNumber(String(Number(firstNumber)*10+num));
                }
            //演算子が押されているときはsecondNumberを更新する
            } else {
                if(isDicimalInput){
                    setSecondNumber(String(secondNumber) + String(num))
                } else {
                    setSecondNumber(String(Number(secondNumber)*10+num));
                }
            }
        }
        //0以外が押されたときはACをCに変える
        if(num !== 0){
            setIsAllClear(false);
        }
    }

    //00ボタンを押したとき
    const handleDoubleZeroClick = () => {
        if(!operator){
            if(isDicimalInput){
                setFirstNumber(String(firstNumber) + "00")
            } else {
                setFirstNumber(String(Number(firstNumber)*100));
            }
        } else {
            if(isDicimalInput){
                setSecondNumber(String(secondNumber) + "00")
            } else {
                setSecondNumber(String(Number(secondNumber)*100));
            }
        }
    }

    //AC/Cボタンを押したとき
    const handleClearClick = () => {
        setIsDicimalInput(false);
        //ACのときはすべてリセット
        if(isAllClear){
            setFirstNumber("0");
            setOperator(false);
            setIsAnswerDisplay(false)
        //CのときはsecondNumberを"0"にする
        } else {
            setIsAllClear(true);
            if(isAnswerDisplay){
                setFirstNumber("0")
            } else{
                if(!operator){
                    setFirstNumber("0");
                } else {
                    setSecondNumber("0");
                }
            }
        }
    }

    // -/+ボタンを押したとき
    const handleMinusPlusClick = () => {
        if(isAnswerDisplay){
            setFirstNumber(-1 * Number(firstNumber));
        } else {
            if(!operator){
                setFirstNumber(-1 * Number(firstNumber));
            } else {
                setSecondNumber(-1 * Number(secondNumber));
            }
        }

    }

    // %ボタンを押したとき
    const handlePercentageClick = () => {
        if(isAnswerDisplay){
            setFirstNumber(Number(firstNumber) * 0.01);
        } else {
            if(!operator){
                setFirstNumber(Number(firstNumber) * 0.01);
            } else {
                setSecondNumber(Number(secondNumber) * 0.01);
            }
        }
    }

    //演算子を押したとき
    //引数はadd subtract divide multiplyのどれか
    const handleOperatorClick = (a) => {
        if(isAnswerDisplay){
            setIsAnswerDisplay(false);
        } else {
            if(secondNumber !== "0"){
                if(operator === "add"){
                    setFirstNumber(Number(firstNumber) + Number(secondNumber))
                } else if(operator === "subtract"){
                    setFirstNumber(Number(firstNumber) - Number(secondNumber))
                } else if(operator === "divide"){
                    setFirstNumber(Number(firstNumber) / Number(secondNumber))
                } else if(operator === "multiply"){
                    setFirstNumber(Number(firstNumber) * Number(secondNumber))
                }
            }
        }
        setIsDicimalInput(false);
        setOperator(a);
        setSecondNumber("0")
    }

    // .ボタンを押したとき
    const handleDicimalPointClick = () => {
        if(isDicimalInput) return;

        setIsDicimalInput(true);
        if(!operator){
            setFirstNumber(String(firstNumber) + '.')
        } else {
            setSecondNumber(String(secondNumber) + '.')
        }
    }

    // =ボタンを押したとき
    const handleAnswerClick = () => {
        setIsDicimalInput(false);
        setIsAnswerDisplay(true);
        if(operator === "add") {
            if(secondNumber === "0"){
                //演算子が指定されたあと、secondNumberが入力されずに=が押された場合
                //例えば、5 + = と入力した場合、10と表示する。
                setSecondNumber(firstNumber)
                setFirstNumber(Number(firstNumber) + Number(firstNumber))
            } else {
                setFirstNumber(Number(firstNumber) + Number(secondNumber))
            }
        } else if(operator === "subtract") {
            if(secondNumber === "0"){
                setSecondNumber(Number(firstNumber))
                setFirstNumber(Number(firstNumber) - Number(firstNumber))
            } else {
                setFirstNumber(Number(firstNumber) - Number(secondNumber))
            }
        } else if(operator === "divide") {
            if(secondNumber === "0"){
                setSecondNumber(Number(firstNumber))
                setFirstNumber(Number(firstNumber) / Number(firstNumber))
            } else {
                setFirstNumber(Number(firstNumber) / Number(secondNumber))
            }
        } else if(operator === "multiply"){
            if(secondNumber === "0"){
                setSecondNumber(Number(firstNumber))
                setFirstNumber(Number(firstNumber) * Number(firstNumber))
            } else {
                setFirstNumber(Number(firstNumber) * Number(secondNumber))
            }
        }
    }

    //大きい桁数や小数点以下桁数の調整
    const fixDigits = (num) => {
        if((num) >= 10e8){
            return num.toExponential(2);
        } else {
            return Math.round(num*1000000)/1000000;
        }
    }
    //表示する数字
    const displayMarkup = (!isAnswerDisplay && operator ) ? fixDigits(Number(secondNumber)) : fixDigits(Number(firstNumber));
    const clearButtonMarkup = isAllClear ? "AC" : "C";

    return(
        <div>
            <Typography variant="h5">
                React Calculator
            </Typography>
            <Display>
                {displayMarkup}
            </Display>
            //material-uiのBoxはflexを使って好きなように並べられる。公式ドキュメント参照。
            <Box display="flex" flexDirection="column">
                <Box display="flex" justifyContent="center">
                   //ボタンの中に表示したいものをMyButtonではさむ
                   //MyButtonのpropsに背景色、onClick関数を渡す
                    <MyButton backgroundColor="gray" onClick={handleClearClick}>
                        <Typography variant="h5">{clearButtonMarkup}</Typography>
                    </MyButton>
                    <MyButton backgroundColor="gray" onClick={handleMinusPlusClick}>
                        <Typography variant="h5">-/+</Typography>
                    </MyButton>
                    <MyButton backgroundColor="gray" onClick={handlePercentageClick}>
                        <Typography variant="h5">%</Typography>
                    </MyButton>
                    //onClickに引数を渡したい場合はこのように記述する
                    <MyButton backgroundColor="orange" onClick={() => handleOperatorClick("divide")}>
                        <Typography variant="h5" >÷</Typography>
                    </MyButton>
                </Box>
                <Box display="flex" justifyContent="center" >
                    <MyButton onClick={() => handleNumberClick(7)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">7</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(8)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">8</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(9)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">9</Typography>
                    </MyButton>
                    <MyButton backgroundColor="orange" onClick={() => handleOperatorClick("multiply")}>
                        <Typography variant="h5" >×</Typography>
                    </MyButton>
                </Box>
                <Box display="flex" justifyContent="center">
                    <MyButton onClick={() => handleNumberClick(4)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">4</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(5)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">5</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(6)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">6</Typography>
                    </MyButton>
                    <MyButton backgroundColor="orange" onClick={() => handleOperatorClick("subtract")}>
                        <Typography variant="h5" >-</Typography>
                    </MyButton>
                </Box>
                <Box display="flex" justifyContent="center">
                    <MyButton onClick={() => handleNumberClick(1)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">1</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(2)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">2</Typography>
                    </MyButton>
                    <MyButton onClick={() => handleNumberClick(3)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">3</Typography>
                    </MyButton>
                    <MyButton backgroundColor="orange" onClick={() => handleOperatorClick("add")}>
                        <Typography variant="h5" >+</Typography>
                    </MyButton>
                </Box>
                <Box display="flex" justifyContent="center">
                    <MyButton onClick={() => handleNumberClick(0)} backgroundColor="#e0e0e0">
                        <Typography variant="h5">0</Typography>
                    </MyButton>
                    <MyButton onClick={handleDoubleZeroClick} backgroundColor="#e0e0e0">
                        <Typography variant="h5">00</Typography>
                    </MyButton>
                    <MyButton onClick={handleDicimalPointClick} backgroundColor="#e0e0e0">
                        <Typography variant="h5" >.</Typography>
                    </MyButton>
                    <MyButton backgroundColor="orange" onClick={handleAnswerClick}>
                        <Typography variant="h5" >=</Typography>
                    </MyButton>
                </Box>
            </Box>
        </div>
    )
};

export default Calculator;

ポイント

  • handleNumberClickなどの引数がある関数は、onClick={() => somthing(引数)}のように記述します
  • stateはひとつにまとめても良いが、どこで何を使うかが決まってないうちは独立したstateにしておいて後からまとめよう。

App.js

App.js
import React from 'react';
import './App.css';

import Calculator from './containers/Calculator'

function App() {
  return (
    <div className="App">
      <div className="container">
        <Calculator />
      </div>
    </div>
  );
}

export default App;

App.css

App.css
.App {
  text-align: center;
}
.container {
  margin: 80px auto 80px auto;
  padding: 20px;
  max-width: 300px;
  background-color: #80deea;
}

以上で完成です!

おまけ

こちらの記事を参考に、gh-pagesでサイトを公開しました。
https://shintaro-hirose.github.io/react-calculator/

今後もReactで簡単なアプリを作っていきます!

12
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
9