Original article: How to build an HTML calculator app from scratch using JavaScript

Este es un artículo épico en el que aprendes a construir una calculadora desde cero. Nos centraremos en el JavaScript que necesitas escribir: cómo pensar en construir la calculadora, cómo escribir el código y, finalmente, cómo limpiar tu código.

Al final del artículo, deberas obtener una calculadora que funcione exactamente como una calculadora de iPhone (sin las funcionalidades de +/- y porcentaje).

Cw7jNVIhWFV4NSNY8-Lv8uX4583Hr5LvzYFq

Requisitos

Antes de intentar seguir la lección, asegúrese de tener un dominio decente de JavaScript. Como mínimo, necesitas saber estas cosas:

  1. Sentencias If / else
  2. For bucles
  3. Funciones de JavaScript
  4. Funciones de flecha
  5. operadores && y ||
  6. Cómo cambiar el texto con la propiedad textContent
  7. Cómo agregar oyentes de eventos con el patrón de delegación de eventos

Antes de empezar

Te insto a que intentes construir la calculadora tu mismo antes de seguir la lección. Es una buena práctica, porque te entrenarás para pensar como un desarrollador.

Regresa a esta lección una vez que lo haya intentado durante una hora (no importa si tiene éxito o fallas. Cuando lo intentas, piensas, y eso te ayudará a absorber la lección en un tiempo rápido).

Con eso, comencemos por entender cómo funciona una calculadora.

Construyendo la calculadora

Primero, queremos construir la calculadora.

La calculadora consta de dos partes: la pantalla y las teclas.

rfV0r9RtFghhau8sZU5CzOFMuJAT1H48tFeL
<div class=”calculator”>
  <div class=”calculator__display”>0</div>
  <div class=”calculator__keys”> … </div>
</div>

Podemos usar CSS Grid para hacer las claves, ya que están organizadas en un formato similar a una cuadrícula. Esto ya se ha hecho por ti en el archivo de inicio. Puedes encontrar el archivo de inicio en este enlace.

.calculator__keys { 
  display: grid; 
  /* other necessary CSS */ 
}

Para ayudarnos a identificar las claves de operador, decimal, clara e igual, proporcionaremos un atributo de acción de datos que describa lo que hacen.

<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">&times;</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>

Escuchar las pulsaciones de teclas

Cinco cosas pueden suceder cuando una persona se apodera de una calculadora. Pueden presionar:

  1. una tecla numérica (0-9)
  2. una tecla de operador ( + , -,×,÷)
  3. la tecla decimal
  4. la tecla de igualdad
  5. la tecla "clear"

Los primeros pasos para construir esta calculadora son poder (1) escuchar todas las pulsaciones de teclas y (2) determinar el tipo de tecla que se presiona. En este caso, podemos usar un patrón de delegación de eventos para escuchar, ya que las claves son todas secundarias de calculator _ _ keys.

const calculator = document.querySelector(‘.calculator’)
const keys = calculator.querySelector(‘.calculator__keys’)

keys.addEventListener(‘click’, e => {
 if (e.target.matches(‘button’)) {
   // Do something
 }
})

A continuación, podemos usar el atributo data-action para determinar el tipo de clave en la que se hace clic.

const key = e.target
const action = key.dataset.action

Si la clave no tiene un atributo data-action, debe ser una clave numérica.

if (!action) {
  console.log('number key!')
}

Si la clave tiene una data-action que es add, subtract, multiply o divide, sabemos que la clave es un operador.

if (
  action === 'add' ||
  action === 'subtract' ||
  action === 'multiply' ||
  action === 'divide'
) {
  console.log('operator key!')
}

Si la data-action de la clave es decimal, sabemos que el usuario hizo clic en la clave decimal.

Siguiendo el mismo proceso de pensamiento, si la data-action de la clave es clear, sabemos que el usuario hizo clic en la clave clara (la que dice AC). Si la data-action de la clave es calculate, sabemos que el usuario hizo clic en la tecla igual.

if (action === 'decimal') {
  console.log('decimal key!')
}

if (action === 'clear') {
  console.log('clear key!')
}

if (action === 'calculate') {
  console.log('equal key!')
}

En este punto, deberías obtener una console.log la respuesta de cada tecla de la calculadora.

lbXTncsu2Ni5V-Ejx6RYCO-kW8XJm7f5woGC

Construyendo el camino feliz

Consideremos lo que haría la persona promedio cuando toma una calculadora. Esto "lo que la persona promedio haría" se llama el camino feliz.

Llamemos a nuestra persona promedio Maria.

Cuando Maria toma una calculadora, puede presionar cualquiera de estas teclas:

  1. una tecla numérica (0-9)
  2. una tecla de operador ( + , -,×,÷)
  3. la clave decimal
  4. la clave de la igualdad
  5. la clave clara

Puede ser abrumador considerar cinco tipos de claves a la vez, así que vamos a ir paso a paso.

Cuando un usuario presiona una tecla numérica

En este punto, si la calculadora muestra 0 (el número predeterminado), el número de destino debe reemplazar a cero.

mpr4JFLSU-MHaq8LPMedsaDxnU5Y-MTx56SU

Si la calculadora muestra un número distinto de cero, el número objetivo debe agregarse al número que se muestra.

PNfa-nAlgIBtFt1MaVEDvuzisaIps6Kdb482

Aquí, necesitamos saber dos cosas:

  1. El número de la tecla en la que se hizo clic
  2. El número actual que se muestra

Podemos obtener estos dos valores a través de la propiedad textContent de la clave clicked y 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
    // ...
  }
})

Si la calculadora muestra 0, queremos reemplazar la pantalla de la calculadora con la tecla presionada. Podemos hacerlo reemplazando la propiedad textContent de la pantalla.

if (!action) {
  if (displayedNum === '0') {
    display.textContent = keyContent
  }
}

Si la calculadora muestra un número distinto de cero, queremos agregar la tecla en la que se hizo clic al número que se muestra. Para agregar un número, concatenamos una cadena.

if (!action) {
  if (displayedNum === '0') {
    display.textContent = keyContent
  } else {
    display.textContent = displayedNum + keyContent
  }
}

En este punto, Maria puede hacer clic en cualquiera de estas teclas:

  1. Una clave decimal
  2. Una clave de operador

Digamos que Maria presiona la tecla decimal.

Cuando un usuario presiona la tecla decimal

Cuando Maria presiona la tecla decimal, debería aparecer un decimal en la pantalla. Si Maria presiona cualquier número después de presionar una tecla decimal, el número también debe agregarse en la pantalla.

5Pc6RLFHdPNzPi3BrlXJSs3xrFf2L90A2WXx

Para crear este efecto, podemos concatenar . al número que se muestra.

if (action === 'decimal') {
  display.textContent = displayedNum + '.'
}

A continuación, digamos que Mary continúa su cálculo presionando una tecla de operador.

Cuando un usuario presiona una tecla de operador

Si Maria presiona una tecla de operador, el operador debe resaltarse para que Maria sepa que el operador está activo.

VarwRgJGrN0mwcgYGpX1Zw54QRfbXdMmQNEG

Para ello, podemos añadir la clase is-depressed a la clave de operador.

if (
  action === 'add' ||
  action === 'subtract' ||
  action === 'multiply' ||
  action === 'divide'
) {
  key.classList.add('is-depressed')
}

Una vez que Maria haya presionando una tecla de operador, presionará otra tecla numérica.

Cuando un usuario presiona una tecla numérica después de una tecla de operador

Cuando Maria vuelve a presionar una tecla numérica, la pantalla anterior debe reemplazarse por el nuevo número. La tecla del operador también debe liberar su estado presionado.

GDuLfupPob7rW0UWTH6RqI5CuQX36vcILKwo

Para liberar el estado presionado, eliminamos la clase is-depressed de todas las teclas a través de un bucle forEach:

keys.addEventListener('click', e => {
  if (e.target.matches('button')) {
    const key = e.target
    // ...
    
    // Remove .is-depressed class from all keys
    Array.from(key.parentNode.children)
      .forEach(k => k.classList.remove('is-depressed'))
  }
})

A continuación, queremos actualizar la pantalla a la tecla en la que se ha hecho clic. Antes de hacer esto, necesitamos una forma de saber si la clave anterior es una clave de operador.

Una forma de hacerlo es a través de un atributo personalizado. Llamemos a este atributo personalizado 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')
      // Add custom attribute
      calculator.dataset.previousKeyType = 'operator'
    }
  }
})

Si el previousKeyType es un operador, queremos reemplazar el número que se muestra con el número en el que se hizo clic.

const previousKeyType = calculator.dataset.previousKeyType

if (!action) {
  if (displayedNum === '0' || previousKeyType === 'operator') {
    display.textContent = keyContent
  } else {
    display.textContent = displayedNum + keyContent
  }
}

A continuación, digamos que Maria decide completar su cálculo presionando la tecla igual.

Cuando un usuario presiona la tecla igual

Cuando Maria presiona la tecla igual, la calculadora debería calcular un resultado que dependa de tres valores:

  1. El primer número introducido en la calculadora
  2. El operador
  3. El segundo número ingresado en la calculadora

Después del cálculo, el resultado debe reemplazar el valor mostrado.

TMFTHXrjCGzKQBIzBFApP7usoJCjcQ-oz2Jc

En este punto, solo conocemos el segundo número — es decir, el número que se muestra actualmente.

if (action === 'calculate') {
  const secondValue = displayedNum
  // ...
}

Para obtener el primer número, necesitamos almacenar el valor que se muestra en la calculadora antes de limpiarlo. Una forma de guardar este primer número es agregarlo a un atributo personalizado cuando se hace clic en el botón del operador.

Para obtener el operador, también podemos usar la misma técnica.

if (
  action === 'add' ||
  action === 'subtract' ||
  action === 'multiply' ||
  action === 'divide'
) {
  // ...
  calculator.dataset.firstValue = displayedNum
  calculator.dataset.operator = action
}

Una vez que tengamos los tres valores que necesitamos, podemos realizar un cálculo. Eventualmente, queremos que el código se vea así:

if (action === 'calculate') {
  const firstValue = calculator.dataset.firstValue
  const operator = calculator.dataset.operator
  const secondValue = displayedNum
  
  display.textContent = calculate(firstValue, operator, secondValue)
}

Eso significa que necesitamos crear una función calculate. Debe tomar tres parámetros: el primer número, el operador y el segundo número.

const calculate = (n1, operator, n2) => {
  // Perform calculation and return calculated value
}

Si el operador es add, queremos sumar valores. Si el operador es restar, queremos restar los valores, y así sucesivamente.

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
}

Recuerde que firstValue y secondValue son cadenas en este punto. Si sumas cadenas, las concatenarás (1 + 1 = 11).

Entonces, antes de calcular el resultado, queremos convertir cadenas en números. Podemos hacerlo con las dos funciones parseInt y parseFloat.

  • parseInt convierte una cadena en un entero.
  • parseFloat convierte una cadena en un flotante (esto significa un número con decimales).

Para una calculadora, necesitamos un flotador.

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
}

¡Eso es todo por el camino feliz!

Puede obtener el código fuente de happy path a través de este enlace (desplácese hacia abajo e ingrese su dirección de correo electrónico en el cuadro, y le enviaré los códigos fuente directamente a su buzón).

Los casos extremos

El camino feliz no es suficiente. Para construir una calculadora que sea robusta, debe hacer que su calculadora sea resistente a patrones de entrada extraños. Para hacerlo, debes imaginar a un alborotador que intenta romper tu calculadora presionando las teclas en el orden incorrecto. Llamemos a este alborotador Tim.

Tim puede presionar estas teclas en cualquier orden:

  1. Una tecla numérica (0-9)
  2. Una tecla de operador ( + , -,×,÷)
  3. La tecla decimal
  4. La tecla de igualdad
  5. La tecla clara

Qué sucede si Tim presiona la tecla decimal

Si Tim presiona una tecla decimal cuando la pantalla ya muestra un punto decimal, no debe pasar nada.

Lbvc-ZcYHO2iWjXIjdYiOVJcmPTmtwkknBw5
Orj4wS6vgnPAMYFq1xI3DEYXBMS4PWLlSw8a

Aquí, podemos verificar que el número mostrado contenga a . con el método includes.

includes verifica las cadenas para una coincidencia determinada. Si se encuentra una cadena, devuelve true; si no, devuelve false.

Nota: includes distingue entre mayúsculas y minúsculas.

// Example of how includes work.
const string = 'The hamburgers taste pretty good!'
const hasExclaimation = string.includes('!')
console.log(hasExclaimation) // true

Para comprobar si la cadena ya tiene un punto, hacemos esto:

// Do nothing if string has a dot
if (!displayedNum.includes('.')) {
  display.textContent = displayedNum + '.'
}

A continuación, si Tim presiona la tecla decimal después de presionar una tecla de operador, la pantalla debe mostrar 0..

fLLhOqkyFZqsOZIxgMPAkpezrUisGpDKFEsw

Aquí necesitamos saber si la clave anterior es un operador. Podemos saberlo comprobando el atributo personalizado, data-previous-key-type, que establecimos en la lección anterior.

data-previous-key-type aún no está completo. Para identificar correctamente si el previousKeyType es un operador, necesitamos actualizar previousKeyType para cada tecla presionada.

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'
}

Una vez que tengamos previousKeyType correcto, podemos usarlo para verificar si la clave anterior es un operador.

if (action === 'decimal') {
  if (!displayedNum.includes('.')) {
    display.textContent = displayedNum + '.'
  } else if (previousKeyType === 'operator') {
    display.textContent = '0.'
  }
  
calculator.dataset.previousKeyType = 'decimal'
}

Qué sucede si Tim presiona una tecla de operador

Si Tim presiona primero una tecla de operador, la tecla de operador debe iluminarse. (Ya hemos cubierto este caso de borde, pero ¿cómo? Ve si puedes identificar lo que hicimos).

q3D72rgBjtPOPUltYm1MMIN06dvxGOKyJyUs

En segundo lugar, no debe pasar nada si Tim presiona la misma tecla de operador varias veces. (Ya hemos cubierto este caso de borde también).

Nota: si deseas proporcionar una mejor experiencia de usuario, puedes mostrar el operador en el que se hace clic repetidamente con algunos cambios de CSS. No lo hicimos aquí, pero ve si puedes programarlo tu mismo como un desafío de codificación adicional.

IXW7zY77RWE7tNQ6HZMYma73hsxW44EjWg0n

En tercer lugar, si Tim presiona otra tecla de operador después de presionar la primera tecla de operador, la primera tecla de operador debe liberarse. A continuación, se debe presionar la segunda tecla de operador. (También cubrimos este caso de borde — pero ¿cómo?).

Rez20RY9AcS6ORFWIIumk69YWzwTyv8qseM7

En cuarto lugar, si Tim llega a un número, un operador, un número y otro operador, en ese orden, la pantalla debe actualizarse a un valor calculado.

MAMWFTkNu6Ho8tlMGyJlTfjCbeYq8rO0bQyR

Esto significa que necesitamos usar la función calculate cuando existen firstValue, operator y secondValue.

if (
  action === 'add' ||
  action === 'subtract' ||
  action === 'multiply' ||
  action === 'divide'
) {
  const firstValue = calculator.dataset.firstValue
  const operator = calculator.dataset.operator
  const secondValue = displayedNum
  
// Note: It's sufficient to check for firstValue and operator because secondValue always exists
  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
}

Aunque podemos calcular un valor cuando se hace clic en la tecla del operador por segunda vez, también hemos introducido un error en este punto: los clics adicionales en la tecla del operador calculan un valor cuando no debería.

8ktjtHeYaRTEn-lPbOM3fhEg3qrvDl5WfOVY

Para evitar que la calculadora realice un cálculo en clics posteriores en la tecla del operador, debemos verificar si previousKeyType es un operador. Si es así, no realizamos un cálculo.

if (
  firstValue &&
  operator &&
  previousKeyType !== 'operator'
) {
  display.textContent = calculate(firstValue, operator, secondValue)
}

En quinto lugar, después de que la tecla del operador calcula un número, si el tiempo llega a un número, seguido de otro operador, el operador debe continuar con el cálculo, de esta manera: 8 - 1 = 7, 7 - 2 = 5, 5 - 3 = 2.

RSsXyuKJe0biqkH-WPDdrGLhFBWmyZ2R1J2Y

En este momento, nuestra calculadora no puede hacer cálculos consecutivos. El segundo valor calculado es incorrecto. Esto es lo que tenemos: 99 - 1 = 98, 98 - 1 = 0.

0r9I8Gu7J9pMbfzUG4hL6tU7RCP-cDhsaGp1

El segundo valor se calcula incorrectamente, porque alimentamos los valores incorrectos en la función calculate. Veamos algunas imágenes para entender lo que hace nuestro código.

Comprender nuestra función de cálculo

Primero, digamos que un usuario hace clic en un número, 99. En este punto, todavía no se ha registrado nada en la calculadora.

0hH4Cz5kOEaDOcTQ2PMPmkDl26a8JHSXNrJ7

En segundo lugar, digamos que el usuario hace clic en el operador restar. Después de hacer clic en el operador de resta, establecemos firstValue a 99. También configuramos el operator para restar.

0K-KPTzdCBgfVvVaDNcVDYSjXfUO8p5LRs2v

En tercer lugar, digamos que el usuario hace clic en un segundo valor, esta vez, es 1. En este punto, el número mostrado se actualiza a 1, pero nuestro firstValue, operator y SecondValue permanecen sin cambios.

0MacG-A5Tl7rZeB6NLeNvghVyBpmSqaZQkn9

En cuarto lugar, el usuario vuelve a hacer clic en restar. Justo después de hacer clic en restar, antes de calcular el resultado, establecemos secondValue como el número que se muestra.

RgDMKK92og4djxxmaYO1HUYiVoetKDK9x0j7

En quinto lugar, realizamos el cálculo con firstValue 99, operator resta y secondValue 1. El resultado es 98.

Una vez calculado el resultado, ajustamos la visualización al resultado. Luego, configuramos `operator` para restar y el primer valor para el número mostrado anteriormente.

X3VFJ5ar--k84pP3pM5VDVODvYlX4fCwHcnS

Bueno, eso está terriblemente mal! Si queremos continuar con el cálculo, necesitamos actualizar firstValue con el valor calculado.

gp-lkqhUOjoo46fIwx-7oLtbV7CP7jZwzc9y
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
  
// Update calculated value as firstValue
  calculator.dataset.firstValue = calcValue
} else {
  // If there are no calculations, set displayedNum as the firstValue
  calculator.dataset.firstValue = displayedNum
}

key.classList.add('is-depressed')
calculator.dataset.previousKeyType = 'operator'
calculator.dataset.operator = action

Con esta corrección, los cálculos consecutivos realizados por teclas de operador ahora deben estar correctos.

tKZ-VlIHo7dRNHDR2BBxZChE1cgqIuMU0Uh-

¿Qué sucede si Tim presiona la tecla igual?

En primer lugar, no debe suceder nada si Tim presiona la tecla igual antes que cualquier tecla de operador.

FBvnFZadNPXTllID0R7JfAkrsDb5SLcWTUhV
fKJV0ZqgVf-ppPqrx-70FpByKioVL2T9oAsF

Sabemos que las teclas de operador aún no se han presionando si firstValue no se establece en un número. Podemos usar este conocimiento para evitar que los iguales calculen.

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'
}

En segundo lugar, si Tim presiona un número, seguido de un operador, seguido de un igual, la calculadora debe calcular el resultado de tal manera que:

  1. 2 + = —> 2 + 2 = 4
  2. 2 - = —> 2 - 2 = 0
  3. 2 × = —> 2 × 2 = 4
  4. 2 ÷ = —> 2 ÷ 2 = 1
MUgIi0ck8OJRV18hfJ-kdn8k7Ydyy5mDvV6z

Ya hemos tenido en cuenta esta extraña entrada. ¿Puedes entender por qué? :)

En tercer lugar, si Tim presionaƒ la tecla igual después de completar un cálculo, se debe realizar de nuevo otro cálculo. Así es como debe leerse el cálculo:

  1. Tim presiona las teclas 5–1
  2. Tim presiona igual. El valor calculado es 5 - 1 = 4
  3. Tim presiona igual. El valor calculado es 4 - 1 = 3
  4. Tim presiona igual. El valor calculado es 3 - 1 = 2
  5. Tim presiona igual. El valor calculado es 2 - 1 = 1
  6. Tim presiona igual. El valor calculado es 1 - 1 = 0
vB2oVoTXZsMABqV60qqclJhoOxYu2JeVhLx4

Desafortunadamente, nuestra calculadora estropea este cálculo. Esto es lo que muestra nuestra calculadora:

  1. Tim presiona las teclas 5–1
  2. Tim presiona igual. El valor calculado es 4
  3. Tim presiona igual. El valor calculado es 1
8roqRbhSH3hLVvtK7t-T2iRsRegqPWSrn4SF

Corrección del cálculo

Primero, digamos que nuestro usuario hace clic en 5. En este punto, todavía no se ha registrado nada en la calculadora.

2vf5VGXNZ0vjGkyaY0y22PRTqqHDwgEKvCC3

En segundo lugar, digamos que el usuario hace clic en el operador restar. Después de hacer clic en el operador de resta, establecemos firstValue en 5. También configuramos operator para restar.

Fc-QupYbv3HInXqv1vHFCc1avhDe3iyEErhs

En tercer lugar, el usuario hace clic en un segundo valor. Digamos que es 1. En este punto, el número mostrado se actualiza a 1, pero nuestro firstValue, operator y SecondValue permanecen sin cambios.

lW3CtoXJ1gxpUS5SZM3zh3zmqSB-ksM6E0vr

En cuarto lugar, el usuario hace clic en la tecla igual. Justo después de hacer clic en igual, pero antes del cálculo, establecemos secondValue como displayedNum

yeQCYcu0ecbNbJlHa9aqEZopHj-FyTqXuRmw

En quinto lugar, la calculadora calcula el resultado de 5 - 1 y da 4. El resultado se actualiza en la pantalla. firstValue y operator se transfieren al siguiente cálculo ya que no los actualizamos.

YOsfq7AWCs0YbABkiebax-oaQVGc5tWsNyXJ

En sexto lugar, cuando el usuario vuelve a presionar igual, establecemos el secondValue en displayedNum antes del cálculo.

BF7tBEUHJN4gnIwQqUTq9ctHIUIVcYM026Ro

Puedes ver lo que está mal aquí.

En lugar del secondValue, queremos que firstValue establecido sea el número que se muestra.

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'
}

También queremos transferir secondValue anterior al nuevo cálculo. Para que secondValue persista en el siguiente cálculo, necesitamos almacenarlo en otro atributo personalizado. Llamemos a este atributo personalizado modValue (significa valor 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)
  }
  
// Establecer el atributo de modValue
  calculator.dataset.modValue = secondValue
  calculator.dataset.previousKeyType = 'calculate'
}

Si previousKeyType es calculate, sabemos que podemos usar calculator.dataset.modValue como secondValue. Una vez que sabemos esto, podemos realizar el cálculo.

if (firstValue) {
  if (previousKeyType === 'calculate') {
    firstValue = displayedNum
    secondValue = calculator.dataset.modValue
  }
  
display.textContent = calculate(firstValue, operator, secondValue)
}

Con eso, tenemos el cálculo correcto cuando se hace clic en la tecla igual consecutivamente.

sjYX-ImohfhbFFbw1-FqmKagBvfFQKm0PzAu

Volver a la tecla de iguales

En cuarto lugar, si Tim presiona una tecla decimal o una tecla numérica después de la tecla de la calculadora, la pantalla debe reemplazarse por 0. o el nuevo número, respectivamente.

Aquí, en lugar de solo verificar si previousKeyType es operator, también debemos verificar si es 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, si Tim presiona una tecla de operador justo después de la tecla igual, la calculadora no debería calcular.

uuifuJ41Oo86NXMsPj44RSQf7ExULROc2GaI

Para hacer esto, verificamos si el previousKeyType es calculate antes de realizar cálculos con claves de 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
  }
  
// ...
}

La tecla borrar tiene dos usos:

  1. Todo despejado (denotado por AC) borra todo y restablece la calculadora a su estado inicial.
  2. Borrar entrada (indicado por CE) borra la entrada actual. Mantiene los números anteriores en la memoria.

Cuando la calculadora está en su estado predeterminado, se debe mostrar AC.

22fj2VLJJ1SPexybqdWIqPRkj9JkrlI3AAYl

En primer lugar, si el tiempo golpea una tecla (cualquier tecla excepto clear), AC debe cambiarse a CE.

Hs9tjp3JQIYOaAgh8KDnxj5QShScU0nMkDa7

Para ello, comprobamos si data-action está clear. Si no es clear, buscamos el botón borrar y cambiar textContent.

if (action !== 'clear') {
  const clearButton = calculator.querySelector('[data-action=clear]')
  clearButton.textContent = 'CE'
}

En segundo lugar, si el tiempo llega a CE, la pantalla debe leer 0. Al mismo tiempo, CE debe revertirse a AC para que Tim pueda restablecer la calculadora a su estado inicial.**

Dv6SFw5LY8wB0WqTFQBe46-QoraBiq8TvpdY
if (action === 'clear') {
  display.textContent = 0
  key.textContent = 'AC'
  calculator.dataset.previousKeyType = 'clear'
}

En tercer lugar, si Tim golpea AC, restablezca la calculadora a su estado inicial.

Para restablecer la calculadora a su estado inicial, debemos borrar todos los atributos personalizados que hemos establecido.

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'
}

Eso es todo, para la parte de los casos de borde, de todos modos.

Puede obtener el código fuente de la parte de casos extremos a través de este enlace (despláza hacia abajo e ingresa tu dirección de correo electrónico en el cuadro, y te enviaré los códigos fuente directamente a tu buzón).

En este punto, el código que creamos juntos es bastante confuso. Probablemente te perderás si intentas leer el código por tu cuenta. Vamos a refactorizar para hacerlo más limpio.

Refactorizar el código

Cuando refactorizas, a menudo comienzas con las mejoras más obvias. En este caso, comencemos con calculate.

Antes de continuar, asegúrate de conocer estas prácticas/características de JavaScript. Los usaremos en la refactorización.

  1. Devoluciones anticipadas
  2. Operadores ternarios
  3. Funciones puras
  4. ES6 Desestructuración

Con eso, empecemos!

Refactorización de la función calcular

Esto es lo que tenemos hasta ahora.

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
}

Aprendiste que debemos reducir las reasignaciones tanto como sea posible. Aquí, podemos eliminar asignaciones si devolvemos el resultado del cálculo dentro de las sentencias if y 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 devolvemos todos los valores, podemos usar devoluciones anticipadas. Si lo hacemos, no hay necesidad de ninguna otra condición 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)
  }
}

Y como tenemos una sentencia por condición if, podemos quitar los corchetes. (Nota: sin embargo, algunos desarrolladores confían en los corchetes). Así es como se vería el 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)
}

Finalmente, llamamos a parseFloat ocho veces en la función. Podemos simplificarlo creando dos variables para contener valores flotantes:

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
}

Hemos terminado con calculate ahora. ¿No crees que es más fácil de leer en comparación con antes?

Refactorizar el detector de eventos

El código que creamos para el detector de eventos es enorme. Esto es lo que tenemos en este 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') { /* ... */ }
  }
})

¿Cómo empiezas a refactorizar este fragmento de código? Si no conoces las mejores prácticas de programación, puedes tener la tentación de refactorizar dividiendo cada tipo de acción en una función más pequeña:

// Don't do this!
const handleNumberKeys = (/* ... */) => {/* ... */}
const handleOperatorKeys = (/* ... */) => {/* ... */}
const handleDecimalKey = (/* ... */) => {/* ... */}
const handleClearKey = (/* ... */) => {/* ... */}
const handleCalculateKey = (/* ... */) => {/* ... */}

No hagas esto. No ayuda, porque simplemente estás dividiendo bloques de código. Cuando lo haces, la función se vuelve más difícil de leer.

Una mejor manera es dividir el código en funciones puras e impuras. Si lo haces, obtendrás un código que se ve así:

keys.addEventListener('click', e => {
  // Funcion pura
  const resultString = createResultString(/* ... */)
  
  // Cosas inpuras
  display.textContent = resultString
  updateCalculatorState(/* ... */)
})

Aquí, createResultString es una función pura que devuelve lo que debe mostrarse en la calculadora. updateCalculatorState es una función impura que cambia la apariencia visual y los atributos personalizados de la calculadora.

Creación de la cadena createResult

Como se mencionó anteriormente, createResultString debe devolver el valor que debe mostrarse en la calculadora.

Puedes obtener estos valores a través de partes del código que dice display.textContent = 'algun valor'.

display.textContent = 'algun valor'

En lugar de display.textContent = 'algun valor',  queremos devolver cada valor para poder usarlo más adelante.

// reemplaza lo anterior con esto
return 'algun valor'

Repasemos esto juntos, paso a paso, comenzando con las teclas numéricas.

Hacer la cadena de resultado para las teclas numéricas

Aquí está el código que tenemos para las teclas numéricas:

if (!action) {
  if (
    displayedNum === '0' ||
    previousKeyType === 'operator' ||
    previousKeyType === 'calculate'
  ) {
    display.textContent = keyContent
  } else {
    display.textContent = displayedNum + keyContent
  }
  calculator.dataset.previousKeyType = 'number'
}

El primer paso es copiar las partes que dicen display.textContent = 'algun valor' en createResultString. Al hacer esto, asegúrete de cambiar display.textContent = a return.

const createResultString = () => {
  if (!action) {
    if (
      displayedNum === '0' ||
      previousKeyType === 'operator' ||
      previousKeyType === 'calculate'
    ) {
      return keyContent
    } else {
      return displayedNum + keyContent
    }
  }
}

A continuación, podemos convertir la sentencia if/else en un operador ternario:

const createResultString = () => {
  if (action!) {
    return displayedNum === '0' ||
      previousKeyType === 'operator' ||
      previousKeyType === 'calculate'
      ? keyContent
      : displayedNum + keyContent
  }
}

Cuando refactorices, recuerda anotar una lista de variables que necesitas. Volveremos a la lista más tarde.

const createResultString = () => {
  // Variables required are:
  // 1. keyContent
  // 2. displayedNum
  // 3. previousKeyType
  // 4. action
  
  if (action!) {
    return displayedNum === '0' ||
      previousKeyType === 'operator' ||
      previousKeyType === 'calculate'
      ? keyContent
      : displayedNum + keyContent
  }
}

Creación de la cadena de resultados para la clave decimal

Aquí está el código que tenemos para la clave decimal:

if (action === 'decimal') {
  if (!displayedNum.includes('.')) {
    display.textContent = displayedNum + '.'
  } else if (
    previousKeyType === 'operator' ||
    previousKeyType === 'calculate'
  ) {
    display.textContent = '0.'
  }
  
  calculator.dataset.previousKeyType = 'decimal'
}

Como antes, queremos mover cualquier cosa que cambie display.textContent en createResultString.

const createResultString = () => {
  // ...
  
  if (action === 'decimal') {
    if (!displayedNum.includes('.')) {
      return = displayedNum + '.'
    } else if (previousKeyType === 'operator' || previousKeyType === 'calculate') {
      return = '0.'
    }
  }
}

Como queremos devolver todos los valores, podemos convertir las sentencoas else if en retornos anticipados.

const createResultString = () => {
  // ...
  
  if (action === 'decimal') {
    if (!displayedNum.includes('.')) return displayedNum + '.'
    if (previousKeyType === 'operator' || previousKeyType === 'calculate') return '0.'
  }
}

Un error común aquí es olvidar devolver el número que se muestra actualmente cuando ninguna de las condiciones coincide. Necesitamos esto porque reemplazaremos display.textContent con el valor devuelto por la createResultString. Si lo perdimos, createResultString devolverá undefined, que no es lo que deseamos.

const createResultString = () => {
  // ...
  
  if (action === 'decimal') {
    if (!displayedNum.includes('.')) return displayedNum + '.'
    if (previousKeyType === 'operator' || previousKeyType === 'calculate') return '0.'
    return displayedNum
  }
}

Como siempre, tome nota de las variables que se requieren. En este punto, las variables requeridas siguen siendo las mismas que antes:

const createResultString = () => {
  // variables requeridas son:
  // 1. keyContent
  // 2. displayedNum
  // 3. previousKeyType
  // 4. accion
}

Creación de la cadena de resultados para las claves de operador

Aquí está el código que escribimos para las teclas de 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
}

Ya conoces el ejercicio: queremos mover todo lo que cambia display.textContent en createResultString. Esto es lo que necesita 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)
    }
  }
}

Recuerda, createResultString debe devolver el valor que se mostrará en la calculadora. Si la condición if no coincide, aún queremos devolver el número mostrado.

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
    }
  }
}

Luego podemos refactorizar la instrucción if/else en un operador ternario:

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
  }
}

Si observas detenidamente, se darás cuenta de que no es necesario almacenar una secondValuevariable. Podemos usar displayedNum directamente en la función 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
  }
}

Finalmente, toma nota de las variables y propiedades requeridas. Esta vez, necesitamos calculator.dataset.firstValue y calculator.dataset.operator.

const createResultString = () => {
  // Variables & properties required are:
  // 1. keyContent
  // 2. displayedNum
  // 3. previousKeyType
  // 4. action
  // 5. calculator.dataset.firstValue
  // 6. calculator.dataset.operator
}

Creación de la cadena de resultados para la tecla borrar

Escribimos el siguiente código para manejar la 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 en el ejemplo anterior, desea mover todo lo que cambia de display.textContent a createResultString.

const createResultString = () => {
  // ...
  if (action === 'clear') return 0
}

Creación de la cadena de resultados para la tecla igual

Aquí está el código que escribimos para la tecla 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 en el ejemplo anterior, queremos copiar todo lo que cambia en display.textContent en createResultString. Esto es lo que necesita 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)
  }
}

Al copiar el código en createResulString, asegúrete de devolver valores para todos los escenarios posibles:

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
    }
  }
}

A continuación, queremos reducir las reasignaciones. Podemos hacerlo pasando los valores correctos a calculate a través de un operador ternario.

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
    }
  }
}

Puedes simplicar aún más el código anterior con otro operador ternario si te sientes cómodo con él:

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
  }
}

Llegados a este punto, queremos volver a tomar nota de las propiedades y variables necesarias:

const createResultString = () => {
  // Variables & properties required are:
  // 1. keyContent
  // 2. displayedNum
  // 3. previousKeyType
  // 4. action
  // 5. calculator.dataset.firstValue
  // 6. calculator.dataset.operator
  // 7. calculator.dataset.modValue
}

Pasando las variables necesarias

Necesitamos siete propiedades/variables en createResultString:

  1. keyContent
  2. displayedNum
  3. previousKeyType
  4. action
  5. firstValue
  6. modValue
  7. operator

Podemos conseguir keyContent y action de key. También podemos obtener firstValue, modValue, operator y previousKeyType de calculator.dataset.

Esto significa que la función createResultString necesita tres variabels —key, displayedNum y calculator.dataset. Dado que calculator.dataset representa el estado de la calculadora, vamos a utilizar una variable llamada 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
  // ... Refactor as necessary
}

// Using createResultString
keys.addEventListener('click', e => {
  if (e.target.matches('button')) return
  const displayedNum = display.textContent
  const resultString = createResultString(e.target, displayedNum, calculator.dataset)
  
  // ...
})

Si lo deseas, puedes desestructurar las variables:

const createResultString = (key, displayedNum, state) => {
  const keyContent = key.textContent
  const { action } = key.dataset
  const {
    firstValue,
    modValue,
    operator,
    previousKeyType
  } = state
  
  // ...
}

Coherencia de las sentencias if

En createResultString, utilizamos las siguientes condiciones para comprobar el tipo de teclas pulsadas:

// If key is number
if (!action) { /* ... */ }

// If key is decimal
if (action === 'decimal') { /* ... */ }

// If key is operator
if (
  action === 'add' ||
  action === 'subtract' ||
  action === 'multiply' ||
  action === 'divide'
) { /* ... */}

// If key is clear
if (action === 'clear') { /* ... */ }

// If key is calculate
if (action === 'calculate') { /* ... */ }

No son consistentes, por lo que son dificiles de leer. Si es posible, queremos que sean consistentes para poder escribir algo así:

if (keyType === 'number') { /* ... */ }
if (keyType === 'decimal') { /* ... */ }
if (keyType === 'operator') { /* ... */}
if (keyType === 'clear') { /* ... */ }
if (keyType === 'calculate') { /* ... */ }

Para ello, podemos crear una función llamda getKeyType. Esta función debe devolver el tipo de tecla que se ha pulsado.

const getKeyType = (key) => {
  const { action } = key.dataset
  if (!action) return 'number'
  if (
    action === 'add' ||
    action === 'subtract' ||
    action === 'multiply' ||
    action === 'divide'
  ) return 'operator'
  // For everything else, return the action
  return action
}

A continuación se explica cómo utilizar la función:

const createResultString = (key, displayedNum, state) => {
  const keyType = getKeyType(key)
  
  if (keyType === 'number') { /* ... */ }
  if (keyType === 'decimal') { /* ... */ }
  if (keyType === 'operator') { /* ... */}
  if (keyType === 'clear') { /* ... */ }
  if (keyType === 'calculate') { /* ... */ }
}

Hemos terminado con createResultString. Pasemos a updateCalculatorState.

Creando updateCalculatorState

updateCalculatorState es una función que cambia el aspecto visual de la calculadora y los atributos personalizados.

Al igual que con createResultString, necesitamos comprobar el tipo de clave sobre la que se ha hecho clic. En este caso, podemos reutilizar getKeyType.

const updateCalculatorState = (key) => {
  const keyType = getKeyType(key)
  
  if (keyType === 'number') { /* ... */ }
  if (keyType === 'decimal') { /* ... */ }
  if (keyType === 'operator') { /* ... */}
  if (keyType === 'clear') { /* ... */ }
  if (keyType === 'calculate') { /* ... */ }
}

Si miras el código sobrante, te darás cuenta de que cambiamos data-previous-key-type para cada tipo de clave. Este es el formato del código:

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'
  }
}

Esto es redundante porque ya conocemos el tipo de clave con getKeyType. Podemos refactorizar lo anterior a:

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') { /* ... */ }
}

Creando updateCalculatorState para teclas de operador

Visulamente, tenemos que asegurarnos de que todas las teclas liberan su estado deprimido. Aquí, podemos copiar y pegar el código que teníamos 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'))
}

Esto es lo que queda de lo que hemos escrito para las teclas de operador, después de mover las piezas relacionadas con display.textContent a 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
}

Puedes observar que podemos acortar el código con un operador ternario:

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 antes, toma nota de las variables y propiedades que necesitas. Aquí, necesitamos calculatedValue y displayedNum.

const updateCalculatorState = (key, calculator) => {
  // Variables and properties needed
  // 1. key
  // 2. calculator
  // 3. calculatedValue
  // 4. displayedNum
}

Creando updateCalculatorState para la tecla clear

Aquí está el código sobrante para la clave clear:

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'
}

No hay mucho que podamos refactorizar aquí. Siéntete libre de copiar/pegar todo en updateCalculatorState.

Creando updateCalculatorState para la tecla equals

Este es el código que escribimos para la tecla 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'
}

Esto es lo que nos queda si eliminamos todo lo que concierne a display.textContent.

if (action === 'calculate') {
  let secondValue = displayedNum
  
  if (firstValue) {
    if (previousKeyType === 'calculate') {
      secondValue = calculator.dataset.modValue
    }
  }
  
  calculator.dataset.modValue = secondValue
}

Podemos refactorizar esto en lo siguiente:

if (keyType === 'calculate') {
  calculator.dataset.modValue = firstValue && previousKeyType === 'calculate'
    ? modValue
    : displayedNum
}

Como siempre, fíjate en las propiedades y variables utilizadas:

const updateCalculatorState = (key, calculator) => {
  // Variables and properties needed
  // 1. key
  // 2. calculator
  // 3. calculatedValue
  // 4. displayedNum
  // 5. modValue
}

Pasando las variables necesarias

Sabemos que necesitamos cinco variables/propiedades para updateCalculatorState:

  1. key
  2. calculator
  3. calculatedValue
  4. displayedNum
  5. modValue

Dado que modValue puede obtenerse de calculator.dataset, sólo necesitamos pasar cuatro 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
  
  // Pass in necessary values
  updateCalculatorState(key, calculator, resultString, displayedNum)
})

Refactorización de updateCalculatorState de nuevo

Cambiamos tres tipos de valores en updateCalculatorState:

  1. calculator.dataset
  2. La clase para operarios de pressing/depressing
  3. Texto AC vs. CE

Si quieres hacerlo más limpio, puedes dividir (2) y (3) en otra función — updateVisualState. Esto es como updateVisualState se puede ver:

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'
  }
}

Conclusión

El código ha quedado mucho más limpio después de la refactorización. Si te fijas en el listener de eventos, sabrás lo qu hace cada función. Aquí está lo que el oyente de eventos se parece al final:

keys.addEventListener('click', e => {
  if (e.target.matches('button')) return
  const key = e.target
  const displayedNum = display.textContent
  
  // Pure functions
  const resultString = createResultString(key, displayedNum, calculator.dataset)
  
  // Update states
  display.textContent = resultString
  updateCalculatorState(key, calculator, resultString, displayedNum)
  updateVisualState(key, calculator)
})

Puedes obtener el código fuente de la parte de refactorización a través de este enlace (desplázate hacia abajo e introduce tu direccion de correo electrónico en la casilla, y te enviaré los códigos fuente directamente a tu buzón).

Espero que te haya gustado este artículo. Si lo has hecho, puedes que te encate Learn JavaScript, un curso en el que te enseño a construir 20 componentes, paso a paso, como la calculadora que hemos construido hoy.

Nota: podemos mejorar aún más la calculadora añadiendo compatibilidad con el teclado y funciones de accesibilidad como las regiones Live. ¿Quieres saber cómo? Visita Learn JavaScript :)