Artigo original: https://www.freecodecamp.org/news/how-to-build-an-html-calculator-app-from-scratch-using-javascript-4454b8714b98/
Este é um artigo épico onde você aprende a construir uma calculadora do zero. Vamos nos concentrar no JavaScript que você precisa escrever — como pensar em construir a calculadora, como escrever o código e, por fim, como limpar seu código.
Até o final do artigo, você deve obter uma calculadora que funcione exatamente como a do iPhone (sem as funcionalidades +/- e porcentagem).
Pré-requisitos
Antes de começar o tutorial, certifique-se que tenha um bom domínio de JavaScript. No mínimo, você precisa conhecer:
- Instruções if/else (texto explicativo em inglês)
- Laços for (texto explicativo em inglês)
- Funções em JavaScript (texto explicativo em inglês)
- Arrow functions (texto explicativo em inglês)
&&
e||
operadores- Como alterar o texto com a propriedade
textContent
- Como adicionar
eventListeners
com o padrão de delegação de evento.
Antes de começar
Eu recomendo que você tente construir a calculadora sozinho antes de seguir o tutorial. É uma boa prática, porque você treinará pensar como um desenvolvedor.
Volte para o tutorial após ter tentado por uma hora (não importa se você teve sucesso ou não). Quando você tenta, você pensa, e isso o ajudará a absorver o tutorial duas vezes mais rápido.
Bom, vamos começar a entender como funciona uma calculadora.
Construindo a calculadora
Primeiro, vamos construir a calculadora.
A calculadora consiste em duas partes: o display e as teclas.
<div class=”calculator”>
<div class=”calculator__display”>0</div>
<div class=”calculator__keys”> … </div>
</div>
Podemos usar o CSS Grid para criar as teclas, uma vez que elas já estão dispostas em um formato de grade. Isto já foi criado no arquivo inicial. Você pode encontrar o arquivo inicial neste link.
.calculator__keys {
display: grid;
/* o resto do CSS necessário */
}
Para identificarmos os operadores, chaves decimais, limpador de tela e sinal de igual, vamos criar um atributo de ação de dados que descreve o que eles fazem.
<div class="calculator__keys">
<button class="key--operator" data-action="add">+</button>
<button class="key--operator" data-action="subtract">-</button
<button class="key--operator" data-action="multiply">×</button>
<button class="key--operator" data-action="divide">÷</button
<button>7</button>
<button>8</button>
<button>9</button>
<button>4</button>
<button>5</button>
<button>6</button>
<button>1</button>
<button>2</button>
<button>3</button>
<button>0</button>
<button data-action="decimal">.</button>
<button data-action="clear">AC</button>
<button class="key--equal" data-action="calculate">=</button>
</div>
Ouvindo as teclas
Cinco coisas que podem acontecer quando alguém pega uma calculadora. Elas podem apertar:
- um número (0–9)
- um operador (+, -, ×, ÷)
- uma tecla decimal
- a tecla de igual
- a tecla para limpar
Os primeiros passos para construir a calculadora são: (1) ouvir todas as teclas (2) determinar quais teclas foram pressionadas. Neste caso, podemos usar um padrão de delegação de eventos para ouvir as teclas, já que todas as teclas são filhas de .calculator__keys
.
const calculator = document.querySelector(‘.calculator’)
const keys = calculator.querySelector(‘.calculator__keys’)
keys.addEventListener(‘click’, e => {
if (e.target.matches(‘button’)) {
// Fazer algo
}
})
Em seguida, podemos usar o atributo data-action
para determinar qual tecla foi clicada.
const key = e.target
const action = key.dataset.action
Se a tecla não tiver um atributo data-action
, ela deve ser uma tecla numérica.
Se a tecla tiver um atributo data-action
que seja add
, subtract
, multiply
ou divide
, sabemos que a tecla é um operador.
Se a tecla data-action
for decimal
, sabemos que o usuário clicou sobre a chave decimal.
Seguindo o mesmo pensamento, se a tecla data-action
for clear
, sabemos que o usuário clicou na tecla de limpar (aquela que diz AC). Se a chave data-action
for calculate
, sabemos que o usuário clicou na tecla de iguais.
Neste momento, você deve conseguir visualizar pelo console.log
, uma resposta para cada tecla clicada.
Construindo o caminho ideal
Vamos considerar o que um usuário comum faria quando pegasse um calculadora. Esse "o que um usuário comum faria" é chamado de caminho ideal.
Vamos chamar nosso usuário de Mary.
Quando Mary pega a calculadora, ela pode clicar em qualquer uma dessas teclas:
- uma tecla numérica (0–9)
- uma tecla de operador (+, -, ×, ÷)
- uma tecla decimal
- a tecla do sinal de igual
- a tecla para limpar (AC)
Pode ser muito pensar nas cinco teclas clicadas de uma só vez, então vamos passo a passo.
Quando o usuário clica em uma tecla numérica
Neste ponto, se a calculadora mostrar 0 (o número padrão), o número clicado deve substituir zero.
Se a calculadora mostrar um número diferente de zero, o número clicado deve ser incluído no número exibido.
Agora, precisamos saber de duas coisas:
- Qual tecla foi clicada
- O número atual exibido
Podemos obter esses dois valores através da propriedade textContent
da tecla clicada e .calculator__display
, respectivamente.
const display = document.querySelector('.calculator__display')
keys.addEventListener('click', e => {
if (e.target.matches('button')) {
const key = e.target
const action = key.dataset.action
const keyContent = key.textContent
const displayedNum = display.textContent
// ...
}
})
Se a calculadora mostrar 0, queremos substituir o visor da calculadora pela tecla clicada. Podemos fazer isso substituindo o texto do visor por uma propriedade textContent.
if (!action) {
if (displayedNum === '0') {
display.textContent = keyContent
}
}
Se a calculadora mostrar um número diferente de zero, queremos incluir a tecla clicada ao número exibido. Para incluir um número, concatenaremos uma string.
if (!action) {
if (displayedNum === '0') {
display.textContent = keyContent
} else {
display.textContent = displayedNum + keyContent
}
}
Neste momento, Mary pode clicar em qualquer uma destas teclas:
- Uma tecla decimal
- Uma tecla de operador
Suponhamos que Mary clicou em uma tecla decimal.
Quando um usuário aperta a tecla decimal
Quando Mary aperta a tecla decimal, um decimal deve aparecer no visor. Se Mary aperta qualquer outro número decimal, ele deverá ser exibido no visor.
Para criar este efeito, podemos concatenar .
ao número exibido.
if (action === 'decimal') {
display.textContent = displayedNum + '.'
}
Em seguida, suponhamos que Mary continua seu cálculo apertando a tecla de algum operador.
Quando o usuário aperta uma tecla de operação
Se Mary apertar uma tecla de operação, ele deverá ser destacado, desta maneira, Mary sabe que este operador está ativo.
Para isso, podemos adicionar a classe is-depressed
a tecla do operador.
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) {
key.classList.add('is-depressed')
}
Uma vez que Mary tenha clicado em uma tecla do operador, ela clicará outra tecla numérica.
Quando o usuário clica em um número após selecionar um operador
Quando Mary selecionar um novo número, o número que estava no visor deverá ser substituído pelo novo número selecionado. A tecla do operador também deve liberar seu estado de pressão.
Para liberar a tecla do operador selecionado, retiramos a classe is-depressed
de todos os forEach
loops:
keys.addEventListener('click', e => {
if (e.target.matches('button')) {
const key = e.target
// ...
// Remove a classe .is-depressed de todas as teclas
Array.from(key.parentNode.children)
.forEach(k => k.classList.remove('is-depressed'))
}
})
A seguir, queremos atualizar o visor para a tecla selecionada. Antes de fazermos isso, precisamos verificar se a tecla anterior é uma tecla de operador.
Uma maneira de fazer isso é através de um atributo personalizado. Chamaremos esse atributo personalizado de data-previous-key-type
.
const calculator = document.querySelector('.calculator')
// ...
keys.addEventListener('click', e => {
if (e.target.matches('button')) {
// ...
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) {
key.classList.add('is-depressed')
// Adiciona um atributo personalizado
calculator.dataset.previousKeyType = 'operator'
}
}
})
Se o previousKeyType
é um operador, queremos substituir o número exibido por um número clicado.
const previousKeyType = calculator.dataset.previousKeyType
if (!action) {
if (displayedNum === '0' || previousKeyType === 'operator') {
display.textContent = keyContent
} else {
display.textContent = displayedNum + keyContent
}
}
Em seguida, suponhamos que Mary decide completar seu cálculo, apertando a tecla do sinal de igual (=).
Quando um usuário aperta a tecla do sinal de igual
Quando Mary pressiona a tecla do sinal de igual (=), a calculadora deveria calcular um resultado que depende de três valores:
- O primeiro número inserido na calculadora
- O operador
- O segundo número inserido na calculadora
Após o cálculo, o resultado deve substituir o valor exibido.
Neste momento, sabemos apenas o segundo número — ou seja, o número que está sendo exibido atualmente.
if (action === 'calculate') {
const secondValue = displayedNum
// ...
}
Para obter o primeiro número, precisamos armazenar o valor exibido da calculadora antes de limpá-la. Uma maneira de salvar este primeiro número é adicioná-lo a um atributo personalizado quando o botão do operador for clicado.
Para obter o operador, também podemos utilizar a mesma técnica.
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) {
// ...
calculator.dataset.firstValue = displayedNum
calculator.dataset.operator = action
}
Assim que tivermos os três valores necessários, podemos fazer um cálculo. Eventualmente, queremos que o código se pareça com algo assim:
if (action === 'calculate') {
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const secondValue = displayedNum
display.textContent = calculate(firstValue, operator, secondValue)
}
Isso significa que precisamos criar uma função calculate
. Ela deve ter três parâmetros: o primeiro número, o operador, e o segundo número.
const calculate = (n1, operator, n2) => {
// Realizar um cálculo e retornar o valor calculado
}
Se o operador for add
, queremos somar os dois valores. Se o operador for subtract
, queremos subtrair os valores, e assim por diante.
const calculate = (n1, operator, n2) => {
let result = ''
if (operator === 'add') {
result = n1 + n2
} else if (operator === 'subtract') {
result = n1 - n2
} else if (operator === 'multiply') {
result = n1 * n2
} else if (operator === 'divide') {
result = n1 / n2
}
return result
}
Lembre-se de que firstValue
e secondValue
são strings. Se juntar os dois valores, você vai concatená-los (1 + 1 = 11
).
Portanto, antes de calcular o resultado, queremos converter strings em números. Podemos fazer isso com as funções parseInt
e parseFloat
.
parseInt
converte uma string em um número inteiro.parseFloat
converte uma string em um um número de ponto flutuante (um número com casas decimais).
Para uma calculadora, precisamos de um número com casas decimais.
const calculate = (n1, operator, n2) => {
let result = ''
if (operator === 'add') {
result = parseFloat(n1) + parseFloat(n2)
} else if (operator === 'subtract') {
result = parseFloat(n1) - parseFloat(n2)
} else if (operator === 'multiply') {
result = parseFloat(n1) * parseFloat(n2)
} else if (operator === 'divide') {
result = parseFloat(n1) / parseFloat(n2)
}
return result
}
Isso é tudo para o caminho ideal!
Você pode pegar o código fonte para o caminho ideal através deste link (texto em inglês - role até o final e digite seu nome e endereço de e-mail, que o código fonte será enviado diretamente para o seu e-mail).
Casos mais extremos
O caminho ideal não é o suficiente. Para construir uma calculadora robusta, é necessário tornar a sua calculadora resistente a padrões de entrada estranhos. Para fazer isso, você deve imaginar um arruaceiro que tenta quebrar sua calculadora, batendo as teclas na ordem errada. Vamos chamar este arruaceiro de Tim.
Tim pode clicar nessas teclas em qualquer ordem:
- Uma tecla numérica (0–9)
- Uma tecla de operador (+, -, ×, ÷)
- Um número decimal
- A tecla de sinal de igual (=)
- A tecla para limpar (AC)
O que acontece se o Tim clicar na tecla decimal
Se o Tim clicar em uma tecla decimal quando o visor já mostrar um ponto decimal, nada deve acontecer.
Aqui podemos ver que o numero exibido contém um .
com o método includes
.
includes
verifica strings para uma determinada correspondência. Se uma string não for encontrada, ela retornará true
; caso contrário, retornará false
.
Observação: includes
faz distinção entre maiúsculas e minúsculas.
// Exemplo do funcionamento de includes.
const string = 'The hamburgers taste pretty good!'
const hasExclamation = string.includes('!')
console.log(hasExclamation) // true
Para verificar se a string já possui um ponto, fazemos assim:
// Não fazer nada se a string já tiver um ponto
if (!displayedNum.includes('.')) {
display.textContent = displayedNum + '.'
}
Em seguida, se Tim pressionar a tecla decimal depois de pressionar uma tecla de operador, o visor deverá mostrar 0.
.
Agora precisamos saber se a tecla anterior é um operador. Podemos dizer verificando o atributo personalizado, data-previous-key-type
, que definimos na lição anterior.
data-previous-key-type
ainda não está completo. Para identificar corretamente se previousKeyType
é um operador, precisamos atualizar previousKeyType
para cada tecla clicada.
if (!action) {
// ...
calculator.dataset.previousKey = 'number'
}
if (action === 'decimal') {
// ...
calculator.dataset.previousKey = 'decimal'
}
if (action === 'clear') {
// ...
calculator.dataset.previousKeyType = 'clear'
}
if (action === 'calculate') {
// ...
calculator.dataset.previousKeyType = 'calculate'
}
Assim que tivermos o previousKeyType
correto, podemos usá-lo para verificar se a tecla anterior é um operador.
if (action === 'decimal') {
if (!displayedNum.includes('.')) {
display.textContent = displayedNum + '.'
} else if (previousKeyType === 'operator') {
display.textContent = '0.'
}
calculator.dataset.previousKeyType = 'decimal'
}
O que acontece se Tim apertar uma tecla de operador
Se Tim apertar uma tecla de operador primeiro, a tecla deve acender (já abordamos esse caso extremo, mas como? Veja se você consegue identificar o que fizemos).
Em segundo lugar, nada deve acontecer se Tim pressionar a mesma tecla de operador várias vezes (já cobrimos este caso extremo também).
Observação: se você quiser fornecer um UX melhor, você pode mostrar o operador sendo clicado repetidamente com algumas alterações de CSS. Nós não fizemos isso aqui, mas veja se você consegue programar isso sozinho como um desafio extra de codificação.
Terceiro, se Tim pressionar outra tecla de operador depois de pressionar a primeira tecla de operador, a primeira tecla de operador deve ser liberada. Em seguida, a segunda tecla do operador deve ser pressionada (também cobrimos esse caso extremo - mas como?).
Quarto, se Tim acertar um número, um operador, um número e outro operador, nessa ordem, o display deve ser atualizado para um valor calculado.
Isso significa que precisamos usar a função calculate
quando firstValue
, operator
e secondValue
existirem.
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) {
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const secondValue = displayedNum
// Obs: basta verificar firstValue e o operador, pois o secondValue existe sempre
if (firstValue && operator) {
display.textContent = calculate(firstValue, operator, secondValue)
}
key.classList.add('is-depressed')
calculator.dataset.previousKeyType = 'operator'
calculator.dataset.firstValue = displayedNum
calculator.dataset.operator = action
}
Embora possamos calcular um valor quando a tecla do operador é clicada pela segunda vez, também introduzimos um bug neste ponto - cliques adicionais na tecla do operador calculam um valor quando não deveriam.
Para evitar que a calculadora execute um cálculo em cliques subsequentes na tela do operador, precisamos verificar se o previousKeyType
é um operador. Se for, não realizamos o cálculo.
if (
firstValue &&
operator &&
previousKeyType !== 'operator'
) {
display.textContent = calculate(firstValue, operator, secondValue)
}
Quinto, após a tecla do operador calcular um número, se Tim pressionar um número, seguido por um outro operador, o operador deve continuar com o cálculo, dessa maneira: 8 - 1 = 7
, 7 - 2 = 5
, 5 - 3 = 2
.
No momento, nossa calculadora não pode fazer cálculos consecutivos. O segundo valor calculado está errado. Isso é o que temos: 99 - 1 = 98
, 98 - 1 = 0
.
O segundo valor é calculado incorretamente, porque colocamos valores errados na função calculate
. Vamos ver algumas imagens para entender o que nosso código faz.
Entendendo nossa função de cálculo
Primeiro, digamos que um usuário clique em um número, 99. Nesse momento, nada é registrado na calculadora.
Segundo, digamos que o usuário clique no operador de subtração. Depois que o usuário clica no operador de subtração, definimos firstValue
como 99. Também definimos o operator
para subtrair.
Terceiro, digamos que o usuário em um segundo valor - desta vez será 1. Neste momento, o número exibido é atualizado para 1, mas nosso firstValue
, operator
e secondValue
permanecem inalterados.
Quarto, o usuário clica em subtrair novamente. Logo após clicar em subtrair, antes de calcularmos o resultado, definimos secondValue
como o número exibido.
Quinto, realizarmos o cálculo com firstValue
99, operator
subtrair e secondValue
1. O resultado é 98.
Assim que o resultado é calculado, definimos a exibição para o resultado. Em seguida, configuramos o operado operator
para subtrair e firstValue
para o número exibido anteriormente.
Bem, isso está muito errado! Se quisermos continuar com o cálculo, precisamos atualizar firstValue
com o valor calculado.
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const secondValue = displayedNum
if (
firstValue &&
operator &&
previousKeyType !== 'operator'
) {
const calcValue = calculate(firstValue, operator, secondValue)
display.textContent = calcValue
// Atualizar o valor calculado como firstValue
calculator.dataset.firstValue = calcValue
} else {
// Se não houver cálculo, definir displayedNum como o firstValue
calculator.dataset.firstValue = displayedNum
}
key.classList.add('is-depressed')
calculator.dataset.previousKeyType = 'operator'
calculator.dataset.operator = action
Com isso corrigido, os cálculos consecutivos feitos pelas teclas do operador devem agora estar corretos.
O que acontece se Tim apertar a tecla de sinal de igual?
Primeiro, nada deve acontecer se Tim apertar a tecla de sinal de igual antes de qualquer tecla de operador.
Sabemos que as teclas do operador ainda não foram clicadas se firstValue
não estiver definido como um número. Podemos usar esse conhecimento para evitar que a tecla de sinal de igual seja calculada.
if (action === 'calculate') {
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const secondValue = displayedNum
if (firstValue) {
display.textContent = calculate(firstValue, operator, secondValue)
}
calculator.dataset.previousKeyType = 'calculate'
}
Segundo, se Tim acertar um número, seguido por um operador, seguido por um igual, a calculadora deve calcular o resultado de tal forma que:
2 + =
—>2 + 2 = 4
2 - =
—>2 - 2 = 0
2 × =
—>2 × 2 = 4
2 ÷ =
—>2 ÷ 2 = 1
Já levamos essa estranha entrada em consideração. Você consegue entender o porquê? :)
Terceiro, se Tim pressionar a tecla de sinal de igual após a conclusão de um cálculo, outro cálculo deverá ser realizado novamente. Veja como o cálculo deve ser lido:
- Tim aperta as teclas 5–1
- Tim aperta o sinal de igual. O valor calculado é
5 - 1 = 4
- Tim aperta o sinal de igual. O valor calculado é
4 - 1 = 3
- Tim aperta o sinal de igual. O valor calculado é
3 - 1 = 2
- Tim aperta o sinal de igual. O valor calculado é
2 - 1 = 1
- Tim aperta o sinal de igual. O valor calculado é
1 - 1 = 0
Infelizmente, nossa calculadora se atrapalha nesse cálculo. Veja o que nossa calculadora mostra:
- Tim aperta as teclas 5–1
- Tim aperta o sinal de igual. O valor calculado é
4
- Tim aperta o sinal de igual. O valor calculado é
1
Corrigindo o cálculo
Primeiro, suponhamos que o usuário clique no número 5. Neste momento, nada está registrado na calculadora ainda.
Segundo, suponhamos que o usuário clique no operador de subtração. Depois que ele clicar no operador de subtração, definimos o firstValue
para 5. Também definimos o operator
para subtrair.
Terceiro, o usuário clica em um segundo valor. Digamos que seja 1. Nesse momento, o número exibido é atualizado para 1, mas nosso firstValue
, operator
e secondValue
permanecem inalterados.
Quarto, o usuário clica na tecla do sinal de igual. Logo após clicar no sinal de igual, mas antes do cálculo, definimos secondValue
como displayedNum
.
Quinto, a calculadora calcula o resultado de 5 - 1
e dá 4
. O resultado é atualizado para o visor. firstValue
e operator
são transportados para o próximo cálculo, pois não o atualizaremos.
Sexto, quando o usuário clicar em igual novamente, definimos secondValue
como displayedNum
antes do cálculo.
Você pode dizer o que está errado aqui.
Ao invés de secondValue
, queremos definir firstValue
como o número exibido.
if (action === 'calculate') {
let firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const secondValue = displayedNum
if (firstValue) {
if (previousKeyType === 'calculate') {
firstValue = displayedNum
}
display.textContent = calculate(firstValue, operator, secondValue)
}
calculator.dataset.previousKeyType = 'calculate'
}
Também queremos levar o secondValue
anterior para o novo cálculo. Para que secondValue
persista no próximo cálculo, precisamos armazená-lo em outro atributo personalizado. Chamaremos esse atributo personalizado de modValue
(significa valor do modificador).
if (action === 'calculate') {
let firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const secondValue = displayedNum
if (firstValue) {
if (previousKeyType === 'calculate') {
firstValue = displayedNum
}
display.textContent = calculate(firstValue, operator, secondValue)
}
// Definir o atributo modValue
calculator.dataset.modValue = secondValue
calculator.dataset.previousKeyType = 'calculate'
}
Se o previousKeyType
é calculate
, sabemos que podemos usar calculator.dataset.modValue
como secondValue
. Uma vez que sabemos disso, podemos fazer o cálculo.
if (firstValue) {
if (previousKeyType === 'calculate') {
firstValue = displayedNum
secondValue = calculator.dataset.modValue
}
display.textContent = calculate(firstValue, operator, secondValue)
}
Com isso, temos o cálculo correto quando a tecla do sinal de igual é clicada consecutivamente.
Voltando para a tecla do sinal de igual
Quarto, se o Tim clicar em uma tecla decimal ou uma tecla numérica após a tecla da calculadora, o visor deve ser substituído por 0.
ou pelo novo número, respectivamente.
Ao invés de apenas verificar se o previousKeyType
é operator
, também precisamos verificar se é calculate
.
if (!action) {
if (
displayedNum === '0' ||
previousKeyType === 'operator' ||
previousKeyType === 'calculate'
) {
display.textContent = keyContent
} else {
display.textContent = displayedNum + keyContent
}
calculator.dataset.previousKeyType = 'number'
}
if (action === 'decimal') {
if (!displayedNum.includes('.')) {
display.textContent = displayedNum + '.'
} else if (
previousKeyType === 'operator' ||
previousKeyType === 'calculate'
) {
display.textContent = '0.'
}
calculator.dataset.previousKeyType = 'decimal'
}
Quinto, se Tim apertar uma tecla de operador logo após a tecla de sinal de igual, a calculadora não deve calcular.
Para fazer isso, vamos verificar se o previousKeyType
é calculate
antes de realizar cálculos com as teclas do operador.
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) {
// ...
if (
firstValue &&
operator &&
previousKeyType !== 'operator' &&
previousKeyType !== 'calculate'
) {
const calcValue = calculate(firstValue, operator, secondValue)
display.textContent = calcValue
calculator.dataset.firstValue = calcValue
} else {
calculator.dataset.firstValue = displayedNum
}
// ...
}
A chave para limpar (AC) tem dois usos:
- All Clear (denotado por
AC
) limpa tudo e restabelece a calculadora para seu estado inicial. - Clear entry (denotado por
CE
) limpa a entrada atual. Mantém os números anteriores na memória.
Quando a calculadora estiver em seu estado padrão, deve ser mostrado AC
.
Primeiro, se o Tim clicar uma tecla (qualquer uma, exceto a de limpar), AC
deve ser mudado para CE
.
Fazemos isso verificando se data-action
está clear
. Caso não esteja clear
, procuramos o botão clear e alteraremos seu textContent
.
if (action !== 'clear') {
const clearButton = calculator.querySelector('[data-action=clear]')
clearButton.textContent = 'CE'
}
Em segundo lugar, se Tim apertar CE
, o visor deve mostrar 0. Ao mesmo tempo, CE
deverá ser revertido para AC
para que Tim possa repor a calculadora em seu estado inicial.**
if (action === 'clear') {
display.textContent = 0
key.textContent = 'AC'
calculator.dataset.previousKeyType = 'clear'
}
Terceiro, se Tim pressionar AC
, a calculadora deverá ser redefinida para seu estado inicial.
Para redefinir a calculadora para seu estado inicial, precisamos limpar todos os atributos personalizados que definimos.
if (action === 'clear') {
if (key.textContent === 'AC') {
calculator.dataset.firstValue = ''
calculator.dataset.modValue = ''
calculator.dataset.operator = ''
calculator.dataset.previousKeyType = ''
} else {
key.textContent = 'AC'
}
display.textContent = 0
calculator.dataset.previousKeyType = 'clear'
}
Então é isso — pelo menos para os casos mais extremos!
Você pode pegar o código fonte para a parte de casos extremos neste link (texto em inglês - role para baixo e digite seu endereço de e-mail, o código fonte será enviado diretamente para o seu e-mail).
Até agora, o código que criamos juntos é bastante confuso. Você provavelmente se perderá se tentar ler o código por conta própria. Vamos refatorá-lo para torná-lo mais limpo.
Refatorando o código
Quando você refatorar, muitas vezes você começará com as melhorias mais óbvias. Neste caso, vamos começar com calculate
.
Antes de continuar, certifique-se de conhecer essas práticas/recursos de JavaScript. Vamos usá-los no refatoramento.
- Returns antecipados (texto explicativo em inglês)
- Operadores ternários
- Funções puras (texto explicativo em inglês)
- Desestruturação da ES6 (texto explicativo em inglês)
Com isso, vamos começar!
Refatorando a função de cálculo
Eis o que temos até agora.
const calculate = (n1, operator, n2) => {
let result = ''
if (operator === 'add') {
result = parseFloat(n1) + parseFloat(n2)
} else if (operator === 'subtract') {
result = parseFloat(n1) - parseFloat(n2)
} else if (operator === 'multiply') {
result = parseFloat(n1) * parseFloat(n2)
} else if (operator === 'divide') {
result = parseFloat(n1) / parseFloat(n2)
}
return result
}
Você aprendeu que devemos reduzir o máximo possível as reatribuições. Aqui, podemos remover as tarefas se devolvermos o resultado do cálculo dentro das declarações de if
e else if
:
const calculate = (n1, operator, n2) => {
if (operator === 'add') {
return firstNum + parseFloat(n2)
} else if (operator === 'subtract') {
return parseFloat(n1) - parseFloat(n2)
} else if (operator === 'multiply') {
return parseFloat(n1) * parseFloat(n2)
} else if (operator === 'divide') {
return parseFloat(n1) / parseFloat(n2)
}
}
Como retornamos todos os valores, podemos utilizar early returns (ou retornos antecipados, em tradução literal). Se o fizermos, não há necessidade das condições de else if
.
const calculate = (n1, operator, n2) => {
if (operator === 'add') {
return firstNum + parseFloat(n2)
}
if (operator === 'subtract') {
return parseFloat(n1) - parseFloat(n2)
}
if (operator === 'multiply') {
return parseFloat(n1) * parseFloat(n2)
}
if (operator === 'divide') {
return parseFloat(n1) / parseFloat(n2)
}
}
E como temos uma declaração com a condição if
, podemos remover os colchetes (embora alguns desenvolvedores afirmem que precisamos deles). Veja como ficaria o código:
const calculate = (n1, operator, n2) => {
if (operator === 'add') return parseFloat(n1) + parseFloat(n2)
if (operator === 'subtract') return parseFloat(n1) - parseFloat(n2)
if (operator === 'multiply') return parseFloat(n1) * parseFloat(n2)
if (operator === 'divide') return parseFloat(n1) / parseFloat(n2)
}
Chamamos parseFloat
oito vezes na função. Podemos simplificá-lo criando duas variáveis para conter valores decimais:
const calculate = (n1, operator, n2) => {
const firstNum = parseFloat(n1)
const secondNum = parseFloat(n2)
if (operator === 'add') return firstNum + secondNum
if (operator === 'subtract') return firstNum - secondNum
if (operator === 'multiply') return firstNum * secondNum
if (operator === 'divide') return firstNum / secondNum
}
Terminamos calculate
. Não acha que está mais fácil de ler em comparação com o código que tínhamos antes?
Refatorando o event listener (ouvinte de eventos)
O código que criamos para o ouvinte de eventos (event listener) é enorme. Veja o que temos no momento:
keys.addEventListener('click', e => {
if (e.target.matches('button')) {
if (!action) { /* ... */ }
if (action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide') {
/* ... */
}
if (action === 'clear') { /* ... */ }
if (action !== 'clear') { /* ... */ }
if (action === 'calculate') { /* ... */ }
}
})
Como você começaria a refatorar esse pedaço de código? Se você não conhece nenhuma prática recomendada de programação, pode ficar tentado a refatorar dividindo cada tipo de ação em uma função menor:
// Não faça isso!
const handleNumberKeys = (/* ... */) => {/* ... */}
const handleOperatorKeys = (/* ... */) => {/* ... */}
const handleDecimalKey = (/* ... */) => {/* ... */}
const handleClearKey = (/* ... */) => {/* ... */}
const handleCalculateKey = (/* ... */) => {/* ... */}
Não faça isso. Isso não ajuda, porque você está apenas dividindo blocos de código. Quando você faz isso, a função fica mais difícil de ler.
Uma maneira melhor é dividir o código em funções puras e impuras. Se fizer isso, você obterá um código parecido com este:
keys.addEventListener('click', e => {
// Função pura
const resultString = createResultString(/* ... */)
// Cálculo "impuro"
display.textContent = resultString
updateCalculatorState(/* ... */)
})
Na código acima, createResultString
é uma função pura que retorna o que precisa ser exibido na calculadora. updateCalculatorState
é uma função impura que altera a aparência visual e os atributos personalizados da calculadora.
Criando createResultString
Como mencionado anteriormente, createResultString
deve retornar o valor que precisa ser exibido na calculadora.
Você pode obter esses valores através de partes do código que diz display.textContent = 'some value
.
display.textContent = 'some value'
Ao invés de display.textContent = 'some value'
, queremos retornar cada valor para que possamos usá-lo mais tarde.
// substitua o que temos acima por isso
return 'some value'
Vamos fazer um passo a passo, começando com as teclas numéricas.
Criando a string de resultado para teclas numéricas
Este é o código que temos para as teclas numéricas:
if (!action) {
if (
displayedNum === '0' ||
previousKeyType === 'operator' ||
previousKeyType === 'calculate'
) {
display.textContent = keyContent
} else {
display.textContent = displayedNum + keyContent
}
calculator.dataset.previousKeyType = 'number'
}
O primeiro passo é copiar as partes que dizem display.textContent = 'some value'
emcreateResultString
. Ao fazer isso, certifique-se de mudar o display.textContent =
em return
.
const createResultString = () => {
if (!action) {
if (
displayedNum === '0' ||
previousKeyType === 'operator' ||
previousKeyType === 'calculate'
) {
return keyContent
} else {
return displayedNum + keyContent
}
}
}
Em seguida, podemos converter a declaração if/else
para um operador ternário:
const createResultString = () => {
if (action!) {
return displayedNum === '0' ||
previousKeyType === 'operator' ||
previousKeyType === 'calculate'
? keyContent
: displayedNum + keyContent
}
}
Quando você refatorar, lembre-se de anotar uma lista de variáveis que você precisa. Voltaremos à lista mais tarde.
const createResultString = () => {
// As variáveis necessárias são:
// 1. keyContent
// 2. displayedNum
// 3. previousKeyType
// 4. action
if (action!) {
return displayedNum === '0' ||
previousKeyType === 'operator' ||
previousKeyType === 'calculate'
? keyContent
: displayedNum + keyContent
}
}
Criando a string de resultado para a tecla decimal
Aqui está o código que temos para a tecla decimal:
if (action === 'decimal') {
if (!displayedNum.includes('.')) {
display.textContent = displayedNum + '.'
} else if (
previousKeyType === 'operator' ||
previousKeyType === 'calculate'
) {
display.textContent = '0.'
}
calculator.dataset.previousKeyType = 'decimal'
}
Assim como antes, queremos mover qualquer coisa que mude o display.textContent
para createResultString
.
const createResultString = () => {
// ...
if (action === 'decimal') {
if (!displayedNum.includes('.')) {
return = displayedNum + '.'
} else if (previousKeyType === 'operator' || previousKeyType === 'calculate') {
return = '0.'
}
}
}
Como queremos retornar todos os valores, podemos converter a declaração else if
para early returns (retornos antecipados, em tradução livre).
const createResultString = () => {
// ...
if (action === 'decimal') {
if (!displayedNum.includes('.')) return displayedNum + '.'
if (previousKeyType === 'operator' || previousKeyType === 'calculate') return '0.'
}
}
Um erro comum aqui é esquecer de retornar o número exibido no momento quando nenhuma condição é correspondida. Precisamos disso porque substituiremos display.textContent
pelo valor retornado de createResultString
. Se falharmos, createResultString
retornará undefined
, o que não é o que desejamos.
const createResultString = () => {
// ...
if (action === 'decimal') {
if (!displayedNum.includes('.')) return displayedNum + '.'
if (previousKeyType === 'operator' || previousKeyType === 'calculate') return '0.'
return displayedNum
}
}
Como sempre, observe as variáveis que são necessárias. Neste momento, as variáveis necessárias permanecem as mesmas de antes:
const createResultString = () => {
// As variáveis necessárias são:
// 1. keyContent
// 2. displayedNum
// 3. previousKeyType
// 4. action
}
Criando a string de resultados para as teclas do operador
Esse é o código que escrevemos para as teclas do operador.
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) {
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const secondValue = displayedNum
if (
firstValue &&
operator &&
previousKeyType !== 'operator' &&
previousKeyType !== 'calculate'
) {
const calcValue = calculate(firstValue, operator, secondValue)
display.textContent = calcValue
calculator.dataset.firstValue = calcValue
} else {
calculator.dataset.firstValue = displayedNum
}
key.classList.add('is-depressed')
calculator.dataset.previousKeyType = 'operator'
calculator.dataset.operator = action
}
Você já sabe o que fazer: queremos mover tudo o que altera display.textContent
paracreateResultString
. Isso é o que precisa ser movido:
const createResultString = () => {
// ...
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) {
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const secondValue = displayedNum
if (
firstValue &&
operator &&
previousKeyType !== 'operator' &&
previousKeyType !== 'calculate'
) {
return calculate(firstValue, operator, secondValue)
}
}
}
Lembre-se: a createResultString
precisa retornar o valor a ser exibido na calculadora. Se a condição if
não corresponder, ainda assim queremos retornar o número exibido.
const createResultString = () => {
// ...
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) {
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const secondValue = displayedNum
if (
firstValue &&
operator &&
previousKeyType !== 'operator' &&
previousKeyType !== 'calculate'
) {
return calculate(firstValue, operator, secondValue)
} else {
return displayedNum
}
}
}
Podemos então refatorar a declaração if/else
em um operador ternário:
const createResultString = () => {
// ...
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) {
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const secondValue = displayedNum
return firstValue &&
operator &&
previousKeyType !== 'operator' &&
previousKeyType !== 'calculate'
? calculate(firstValue, operator, secondValue)
: displayedNum
}
}
Se você olhar com atenção, perceberá que não há necessidade de armazenar uma variável secondValue
. Podemos usar displayedNum
diretamente na função calculate
.
const createResultString = () => {
// ...
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) {
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
return firstValue &&
operator &&
previousKeyType !== 'operator' &&
previousKeyType !== 'calculate'
? calculate(firstValue, operator, displayedNum)
: displayedNum
}
}
Por fim, tome nota das variáveis e propriedades necessárias. Desta vez, precisamos calculator.dataset.firstValue
e calculator.dataset.operator
.
const createResultString = () => {
// As variáveis e propriedades necessárias são:
// 1. keyContent
// 2. displayedNum
// 3. previousKeyType
// 4. action
// 5. calculator.dataset.firstValue
// 6. calculator.dataset.operator
}
Criando a string de resultado para a tecla de limpar
Escrevemos o código abaixo para controlar a tecla clear
.
if (action === 'clear') {
if (key.textContent === 'AC') {
calculator.dataset.firstValue = ''
calculator.dataset.modValue = ''
calculator.dataset.operator = ''
calculator.dataset.previousKeyType = ''
} else {
key.textContent = 'AC'
}
display.textContent = 0
calculator.dataset.previousKeyType = 'clear'
}
Como acima, queremos mover tudo o que muda o display.textContent
para createResultString
.
const createResultString = () => {
// ...
if (action === 'clear') return 0
}
Construindo a string de resultado para a tecla de igual
Aqui está o código que temos para a tecla de igual:
if (action === 'calculate') {
let firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
let secondValue = displayedNum
if (firstValue) {
if (previousKeyType === 'calculate') {
firstValue = displayedNum
secondValue = calculator.dataset.modValue
}
display.textContent = calculate(firstValue, operator, secondValue)
}
calculator.dataset.modValue = secondValue
calculator.dataset.previousKeyType = 'calculate'
}
Como acima, queremos copiar tudo o que muda o display.textContent
para createResultString
. Abaixo o que precisa ser copiado:
if (action === 'calculate') {
let firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
let secondValue = displayedNum
if (firstValue) {
if (previousKeyType === 'calculate') {
firstValue = displayedNum
secondValue = calculator.dataset.modValue
}
display.textContent = calculate(firstValue, operator, secondValue)
}
}
Ao copiar o código para createResultString
, certifique-se de retornar valores para todos os cenários possíveis:
const createResultString = () => {
// ...
if (action === 'calculate') {
let firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
let secondValue = displayedNum
if (firstValue) {
if (previousKeyType === 'calculate') {
firstValue = displayedNum
secondValue = calculator.dataset.modValue
}
return calculate(firstValue, operator, secondValue)
} else {
return displayedNum
}
}
}
Em seguida, queremos reduzir reatribuições. Podemos fazer isso passando os valores corretos para calculate
por meio de um operador ternário.
const createResultString = () => {
// ...
if (action === 'calculate') {
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const modValue = calculator.dataset.modValue
if (firstValue) {
return previousKeyType === 'calculate'
? calculate(displayedNum, operator, modValue)
: calculate(firstValue, operator, displayedNum)
} else {
return displayedNum
}
}
}
É possível simplificar ainda mais o código acima com outro operador ternário se você se sentir confortável com ele:
const createResultString = () => {
// ...
if (action === 'calculate') {
const firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
const modValue = calculator.dataset.modValue
return firstValue
? previousKeyType === 'calculate'
? calculate(displayedNum, operator, modValue)
: calculate(firstValue, operator, displayedNum)
: displayedNum
}
}
Agora, queremos anotar novamente as propriedades e variáveis necessárias:
const createResultString = () => {
// As variáveis e propriedades necessárias são:
// 1. keyContent
// 2. displayedNum
// 3. previousKeyType
// 4. action
// 5. calculator.dataset.firstValue
// 6. calculator.dataset.operator
// 7. calculator.dataset.modValue
}
Passando as variáveis necessárias
Precisamos de sete propriedades/variáveis em createResultString
:
keyContent
displayedNum
previousKeyType
action
firstValue
modValue
operator
Podemos obter keyContent
e action
a partir de key
. Também podemos obter firstValue
, modValue
, operator
e previousKeyType
a partir de calculator.dataset
.
Isso significa que a função createResultString
precisa de três variáveis — key
, displayedNum
e calculator.dataset
. Como calculator.dataset
representa o estado da calculadora, vamos usar uma variável chamada state
.
const createResultString = (key, displayedNum, state) => {
const keyContent = key.textContent
const action = key.dataset.action
const firstValue = state.firstValue
const modValue = state.modValue
const operator = state.operator
const previousKeyType = state.previousKeyType
// ... Refatore conforme seja necessário
}
// Usando createResultString
keys.addEventListener('click', e => {
if (e.target.matches('button')) return
const displayedNum = display.textContent
const resultString = createResultString(e.target, displayedNum, calculator.dataset)
// ...
})
Sinta-se à vontade para desestruturar variáveis, se desejar:
const createResultString = (key, displayedNum, state) => {
const keyContent = key.textContent
const { action } = key.dataset
const {
firstValue,
modValue,
operator,
previousKeyType
} = state
// ...
}
Consistência nas declarações if
Em createResultString
, usamos as seguintes condições para testar o tipo de teclas que foram clicadas:
// Se a tecla for numérica
if (!action) { /* ... */ }
// Se a tecla for decimal
if (action === 'decimal') { /* ... */ }
// Se a tecla for um operador
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) { /* ... */}
// Se a tecla for a de limpar
if (action === 'clear') { /* ... */ }
// Se a tecla for um calcular
if (action === 'calculate') { /* ... */ }
Eles não são consistentes, por isso são difíceis de ler. Se possível, queremos torná-las consistentes para que possamos escrever algo assim:
if (keyType === 'number') { /* ... */ }
if (keyType === 'decimal') { /* ... */ }
if (keyType === 'operator') { /* ... */}
if (keyType === 'clear') { /* ... */ }
if (keyType === 'calculate') { /* ... */ }
Para isso, podemos criar uma função chamada getKeyType
. Essa função deve retornar o tipo de tecla que foi clicada.
const getKeyType = (key) => {
const { action } = key.dataset
if (!action) return 'number'
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) return 'operator'
// Para todo o resto, retorne a ação (action)
return action
}
Veja como você usaria a função:
const createResultString = (key, displayedNum, state) => {
const keyType = getKeyType(key)
if (keyType === 'number') { /* ... */ }
if (keyType === 'decimal') { /* ... */ }
if (keyType === 'operator') { /* ... */}
if (keyType === 'clear') { /* ... */ }
if (keyType === 'calculate') { /* ... */ }
}
Terminamos com a função createResultString
. Vamos continuar com updateCalculatorState
.
Construindo updateCalculatorState
updateCalculatorState
é uma função que altera a aparência visual e os atributos personalizados da calculadora.
Assim como createResultString
, precisamos verificar qual tecla foi clicada. Nesse caso, vamos reutilizar a função getKeyType
.
const updateCalculatorState = (key) => {
const keyType = getKeyType(key)
if (keyType === 'number') { /* ... */ }
if (keyType === 'decimal') { /* ... */ }
if (keyType === 'operator') { /* ... */}
if (keyType === 'clear') { /* ... */ }
if (keyType === 'calculate') { /* ... */ }
}
Se você observar o código restante, poderá notar que mudamos os dados data-previous-key-type
para cada tipo de tecla. Veja abaixo como o código se parece:
const updateCalculatorState = (key, calculator) => {
const keyType = getKeyType(key)
if (!action) {
// ...
calculator.dataset.previousKeyType = 'number'
}
if (action === 'decimal') {
// ...
calculator.dataset.previousKeyType = 'decimal'
}
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) {
// ...
calculator.dataset.previousKeyType = 'operator'
}
if (action === 'clear') {
// ...
calculator.dataset.previousKeyType = 'clear'
}
if (action === 'calculate') {
calculator.dataset.previousKeyType = 'calculate'
}
}
Isso é redundante porque já conhecemos o tipo de tecla com o getKeyType
. Podemos refatorar o código acima para:
const updateCalculatorState = (key, calculator) => {
const keyType = getKeyType(key)
calculator.dataset.previousKeyType = keyType
if (keyType === 'number') { /* ... */ }
if (keyType === 'decimal') { /* ... */ }
if (keyType === 'operator') { /* ... */}
if (keyType === 'clear') { /* ... */ }
if (keyType === 'calculate') { /* ... */ }
}
Construindo updateCalculatorState
para teclas de operador
Visualmente, precisamos garantir que todas as teclas liberem seu estado deprimido. Abaixo, podemos copiar e colar o código que tínhamos antes:
const updateCalculatorState = (key, calculator) => {
const keyType = getKeyType(key)
calculator.dataset.previousKeyType = keyType
Array.from(key.parentNode.children).forEach(k => k.classList.remove('is-depressed'))
}
Aqui está o que sobrou do que escrevemos para as teclas de operador, depois de mover as partes relacionadas a display.textContent
para createResultString
.
if (keyType === 'operator') {
if (firstValue &&
operator &&
previousKeyType !== 'operator' &&
previousKeyType !== 'calculate'
) {
calculator.dataset.firstValue = calculatedValue
} else {
calculator.dataset.firstValue = displayedNum
}
key.classList.add('is-depressed')
calculator.dataset.operator = key.dataset.action
}
Você pode perceber que podemos encurtar o código com um operador ternário:
if (keyType === 'operator') {
key.classList.add('is-depressed')
calculator.dataset.operator = key.dataset.action
calculator.dataset.firstValue = firstValue &&
operator &&
previousKeyType !== 'operator' &&
previousKeyType !== 'calculate'
? calculatedValue
: displayedNum
}
Como anteriormente, observe as variáveis e propriedades que você precisa. Aqui, nós vamos precisar de calculatedValue
e displayedNum
.
const updateCalculatorState = (key, calculator) => {
// Variáveis e propriedades necessárias
// 1. key
// 2. calculator
// 3. calculatedValue
// 4. displayedNum
}
Construindo updateCalculatorState
para a tecla de limpar (AC)
Aqui está o código restante para a tecla de limpeza:
if (action === 'clear') {
if (key.textContent === 'AC') {
calculator.dataset.firstValue = ''
calculator.dataset.modValue = ''
calculator.dataset.operator = ''
calculator.dataset.previousKeyType = ''
} else {
key.textContent = 'AC'
}
}
if (action !== 'clear') {
const clearButton = calculator.querySelector('[data-action=clear]')
clearButton.textContent = 'CE'
}
Não há muito o que possamos refatorar aqui. Sinta-se à vontade para copiar e colar tudo em updateCalculatorState
.
Construindo updateCalculatorState
para a tecla de sinal de igual
Abaixo o código que escrevemos para a chave de igual:
if (action === 'calculate') {
let firstValue = calculator.dataset.firstValue
const operator = calculator.dataset.operator
let secondValue = displayedNum
if (firstValue) {
if (previousKeyType === 'calculate') {
firstValue = displayedNum
secondValue = calculator.dataset.modValue
}
display.textContent = calculate(firstValue, operator, secondValue)
}
calculator.dataset.modValue = secondValue
calculator.dataset.previousKeyType = 'calculate'
}
Eis o que nos resta se removermos tudo o que diz respeito a display.textContent
.
if (action === 'calculate') {
let secondValue = displayedNum
if (firstValue) {
if (previousKeyType === 'calculate') {
secondValue = calculator.dataset.modValue
}
}
calculator.dataset.modValue = secondValue
}
Podemos refatorar isto para o seguinte:
if (keyType === 'calculate') {
calculator.dataset.modValue = firstValue && previousKeyType === 'calculate'
? modValue
: displayedNum
}
Como sempre, anote as propriedades e variáveis usadas:
const updateCalculatorState = (key, calculator) => {
// Variáveis e propriedades necessárias
// 1. key
// 2. calculator
// 3. calculatedValue
// 4. displayedNum
// 5. modValue
}
Passando variáveis necessárias
Sabemos que precisamos de cinco propriedades/variáveis para updateCalculatorState
:
key
calculator
calculatedValue
displayedNum
modValue
Como o modValue
pode ser acessado de calculator.dataset
, só precisamos passar quatro valores:
const updateCalculatorState = (key, calculator, calculatedValue, displayedNum) => {
// ...
}
keys.addEventListener('click', e => {
if (e.target.matches('button')) return
const key = e.target
const displayedNum = display.textContent
const resultString = createResultString(key, displayedNum, calculator.dataset)
display.textContent = resultString
// Passe os valores necessários
updateCalculatorState(key, calculator, resultString, displayedNum)
})
Refatorando updateCalculatorState novamente
Mudamos três tipos de valores em updateCalculatorState
:
calculator.dataset
- A classe para operadores pressionados/despressionados
AC
paraCE
Se quiser tornar ainda mais claro, você pode dividir (2) e (3) em outra função — updateVisualState
. Veja como pode ficar o updateVisualState
:
const updateVisualState = (key, calculator) => {
const keyType = getKeyType(key)
Array.from(key.parentNode.children).forEach(k => k.classList.remove('is-depressed'))
if (keyType === 'operator') key.classList.add('is-depressed')
if (keyType === 'clear' && key.textContent !== 'AC') {
key.textContent = 'AC'
}
if (keyType !== 'clear') {
const clearButton = calculator.querySelector('[data-action=clear]')
clearButton.textContent = 'CE'
}
}
Finalizando
O código fica muito mais limpo após a refatoração. Se você examinar o ouvinte de eventos (event listener), saberá o que cada função faz. Abaixo, está a versão final do ouvinte de eventos (event listener):
keys.addEventListener('click', e => {
if (e.target.matches('button')) return
const key = e.target
const displayedNum = display.textContent
// Funções puras
const resultString = createResultString(key, displayedNum, calculator.dataset)
// Estados (states) atualizados
display.textContent = resultString
updateCalculatorState(key, calculator, resultString, displayedNum)
updateVisualState(key, calculator)
})
Você pode pegar o código fonte para a parte de refatoração através deste link (texto em inglês - role até o final e digite seu nome e endereço de e-mail, que o código fonte será enviado diretamente para o seu e-mail).
Espero que tenha gostado deste artigo. Caso tenha gostado, talvez se interesse por Learn JavaScript ("Aprenda Javascript", em inglês) —um curso onde mostrarei como construir 20 componentes, passo a passo, da mesma maneira que construímos essa calculadora.
Observação: podemos melhorar ainda mais a calculadora adicionando suporte ao teclado e recursos de acessibilidade, como regiões, ao vivo. Quer saber como? Confira em Learn JavaScript (em inglês) :)