Comment créer une application de calculatrice HTML à partir de zéro en utilisant JavaScript

Ceci est un article épique où vous apprendrez à construire une calculatrice à partir de zéro. Nous allons nous concentrer sur le code JavaScript que vous devez écrire - comment penser à construire la calculatrice, comment écrire le code et, éventuellement, comment nettoyer votre code.

À la fin de l'article, vous devriez vous procurer une calculatrice qui fonctionne exactement comme une calculatrice pour iPhone (sans les fonctionnalités +/- et pourcentage).

Les prérequis

Avant de suivre la leçon, assurez-vous de bien maîtriser le JavaScript. Au minimum, vous devez savoir ces choses:

  1. Déclarations if / else
  2. Pour les boucles
  3. Fonctions JavaScript
  4. Fonctions de flèche
  5. && et || les opérateurs
  6. Comment changer le texte avec la propriété textContent
  7. Comment ajouter des écouteurs d'événement avec le modèle de délégation d'événement

Avant que tu commences

Je vous exhorte à construire vous-même la calculatrice avant de suivre la leçon. C’est une bonne pratique, car vous vous entraînerez à penser comme un développeur.

Revenez à cette leçon une fois que vous avez essayé pendant une heure (peu importe que vous réussissiez ou non. Lorsque vous essayez, vous réfléchissez et cela vous aidera à assimiler la leçon en un temps très rapide).

Cela dit, commençons par comprendre le fonctionnement d’une calculatrice.

Construire la calculatrice

Tout d'abord, nous voulons construire la calculatrice.

La calculatrice est composée de deux parties: l’affichage et les touches.

  
0   

Nous pouvons utiliser CSS Grid pour créer les clés, car elles sont organisées dans un format semblable à celui d’une grille. Cela a déjà été fait pour vous dans le fichier de démarrage. Vous pouvez trouver le fichier de démarrage dans ce stylo.

.calculator__keys {
  affichage: grille;
  / * autres CSS nécessaires * /
}

Pour nous aider à identifier les clés opérateur, décimale, claire et égales, nous allons fournir un attribut data-action décrivant ce qu’elles font.

   +    - & times;   

Écoute des touches

Cinq choses peuvent se produire lorsqu'une personne se procure une calculatrice. Ils peuvent frapper:

  1. une touche numérique (0–9)
  2. une clé opérateur (+, -, ×, ÷)
  3. la touche décimale
  4. la clé d'égalité
  5. la clé claire

Les premières étapes pour construire cette calculatrice sont de pouvoir (1) écouter toutes les pressions sur les touches et (2) déterminer le type de touche sur laquelle on appuie. Dans ce cas, nous pouvons utiliser un modèle de délégation d'événements pour écouter, car les clés sont toutes des enfants de .calculator__keys.

const calculator = document.querySelector (‘. calculator ')
const keys = calculator.querySelector ('. calculator__keys')
keys.addEventListener (‘click’, e => {
 if (e.target.matches ('button')) {
   // Faire quelque chose
 }
})

Ensuite, nous pouvons utiliser l'attribut data-action pour déterminer le type de clé sur lequel l'utilisateur a cliqué.

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

Si la clé ne possède pas d'attribut data-action, il doit s'agir d'une clé numérique.

si (! action) {
  console.log ('clé numérique!')
}

Si la clé a une action de données qui est soit additionner, soustraire, multiplier ou diviser, nous savons que la clé est un opérateur.

si (
  action === 'add' ||
  action === 'soustraire' ||
  action === 'multiplie' ||
  action === 'diviser'
) {
  console.log ('clé de l'opérateur!')
}

Si l’action data de la clé est décimale, nous savons que l’utilisateur a cliqué sur la clé décimale.

Suivant le même processus de réflexion, si l'action data de la clé est claire, nous savons que l'utilisateur a cliqué sur la clé clear (celle qui dit AC). Si l'action data de la clé est calculée, nous savons que l'utilisateur a cliqué sur la clé égale.

if (action === 'décimal') {
  console.log ('clé décimale!')
}
if (action === 'clear') {
  console.log ('touche effacée!')
}
if (action === 'calculer') {
  console.log ('clé égale!')
}

À ce stade, vous devriez obtenir une réponse console.log de chaque clé de la calculatrice.

Construire le chemin heureux

Voyons ce que ferait une personne moyenne quand elle prendrait une calculatrice. Ce «ce que ferait une personne moyenne» est appelé le chemin heureux.

Appelons notre personne moyenne Mary.

Lorsque Mary prend une calculatrice, elle peut appuyer sur l’une des touches suivantes:

  1. une touche numérique (0–9)
  2. une clé opérateur (+, -, ×, ÷)
  3. la touche décimale
  4. la clé égale
  5. la clé claire

Il peut être fastidieux d’envisager cinq types de clés à la fois, passons donc pas à pas.

Lorsqu'un utilisateur appuie sur une touche numérique

À ce stade, si la calculatrice indique 0 (nombre par défaut), le nombre cible doit remplacer zéro.

Si la calculatrice affiche un nombre différent de zéro, le nombre cible doit être ajouté au nombre affiché.

Ici, il faut savoir deux choses:

  1. Le numéro de la clé sur laquelle vous avez cliqué
  2. Le nombre actuellement affiché

Nous pouvons obtenir ces deux valeurs par le biais de la propriété textContent de la clé cliquée et de .calculator__display, respectivement.

const display = document.querySelector ('. calculator__display')
keys.addEventListener ('click', e => {
  if (e.target.matches ('bouton')) {
    clé const = e.target
    action const = key.dataset.action
    const keyContent = key.textContent
    const displayNum = display.textContent
    // ...
  }
})

Si la calculatrice affiche 0, nous voulons remplacer l’affichage de la calculatrice par la touche sur laquelle vous avez cliqué. Nous pouvons le faire en remplaçant la propriété textContent de display.

si (! action) {
  si (affichéNum === '0') {
    display.textContent = keyContent
  }
}

Si la calculatrice affiche un nombre différent de zéro, nous souhaitons ajouter la clé cliquée au nombre affiché. Pour ajouter un nombre, nous concaténons une chaîne.

si (! action) {
  si (affichéNum === '0') {
    display.textContent = keyContent
  } autre {
    display.textContent = displayNum + keyContent
  }
}

À ce stade, Marie peut cliquer sur l'une des clés suivantes:

  1. Une clé décimale
  2. Une clé d'opérateur

Disons que Mary appuie sur la touche décimale.

Lorsqu'un utilisateur appuie sur la touche décimale

Lorsque Marie appuie sur la touche décimale, une décimale doit apparaître à l'écran. Si Mary frappe un nombre après avoir appuyé sur une touche décimale, ce nombre doit également être ajouté à l'écran.

Pour créer cet effet, nous pouvons concaténer. au numéro affiché.

if (action === 'décimal') {
  display.textContent = displayNum + '.'
}

Ensuite, supposons que Mary continue son calcul en appuyant sur une touche de l’opérateur.

Lorsqu'un utilisateur appuie sur une touche de l'opérateur

Si Mary appuie sur une touche de l'opérateur, l'opérateur doit être mis en évidence pour que Mary sache que l'opérateur est actif.

Pour ce faire, nous pouvons ajouter la classe is-depressed à la clé d'opérateur.

si (
  action === 'add' ||
  action === 'soustraire' ||
  action === 'multiplie' ||
  action === 'diviser'
) {
  key.classList.add ('est-déprimé')
}

Une fois que Mary a appuyé sur une touche de l'opérateur, elle frappera un autre numéro.

Lorsqu'un utilisateur appuie sur une touche numérique après une touche opérateur

Lorsque Mary appuie à nouveau sur une touche numérique, l'affichage précédent doit être remplacé par le nouveau numéro. La touche opérateur doit également libérer son état enfoncé.

Pour libérer l'état pressé, nous supprimons la classe is-déprimée de toutes les clés via une boucle forEach:

keys.addEventListener ('click', e => {
  if (e.target.matches ('bouton')) {
    clé const = e.target
    // ...
    // Supprimer la classe .is-depressed de toutes les clés
    Array.from (key.parentNode.children)
      .forEach (k => k.classList.remove ('est-déprimé'))
  }
})

Ensuite, nous voulons mettre à jour l'affichage à la clé cliquée. Avant de faire cela, nous avons besoin d’un moyen de savoir si la clé précédente est une clé d’opérateur.

Une façon de faire est d'utiliser un attribut personnalisé. Appelons cet attribut personnalisé data-previous-key-type.

const calculator = document.querySelector ('. calculator')
// ...
keys.addEventListener ('click', e => {
  if (e.target.matches ('bouton')) {
    // ...
    si (
      action === 'add' ||
      action === 'soustraire' ||
      action === 'multiplie' ||
      action === 'diviser'
    ) {
      key.classList.add ('est-déprimé')
      // Ajouter un attribut personnalisé
      calculator.dataset.previousKeyType = 'operator'
    }
  }
})

Si previousKeyType est un opérateur, nous voulons remplacer le numéro affiché par le nombre cliqué.

const previousKeyType = calculator.dataset.previousKeyType
si (! action) {
  if (displayNum === '0' || previousKeyType === 'operator') {
    display.textContent = keyContent
  } autre {
    display.textContent = displayNum + keyContent
  }
}

Ensuite, supposons que Mary décide de terminer son calcul en appuyant sur la touche égal à.

Lorsqu'un utilisateur appuie sur la touche d'égalité

Lorsque Marie appuie sur la touche d'égalité, la calculatrice doit calculer un résultat qui dépend de trois valeurs:

  1. Le premier numéro entré dans la calculatrice
  2. L'opérateur
  3. Le deuxième numéro entré dans la calculatrice

Après le calcul, le résultat doit remplacer la valeur affichée.

À ce stade, nous ne connaissons que le deuxième numéro, c'est-à-dire le numéro actuellement affiché.

if (action === 'calculer') {
  const secondValue = displayNum
  // ...
}

Pour obtenir le premier numéro, nous devons stocker la valeur affichée par la calculatrice avant de l’effacer. Une façon de sauvegarder ce premier numéro consiste à l'ajouter à un attribut personnalisé lorsque l'utilisateur clique sur le bouton de l'opérateur.

Pour obtenir l'opérateur, nous pouvons également utiliser la même technique.

si (
  action === 'add' ||
  action === 'soustraire' ||
  action === 'multiplie' ||
  action === 'diviser'
) {
  // ...
  calculator.dataset.firstValue = displayNum
  calculator.dataset.operator = action
}

Une fois que nous avons les trois valeurs dont nous avons besoin, nous pouvons effectuer un calcul. Finalement, nous voulons que le code ressemble à ceci:

if (action === 'calculer') {
  const firstValue = calculator.dataset.firstValue
  opérateur const = calculator.dataset.operator
  const secondValue = displayNum
  display.textContent = calculer (firstValue, operator, secondValue)
}

Cela signifie que nous devons créer une fonction de calcul. Il doit prendre en compte trois paramètres: le premier numéro, l'opérateur et le second numéro.

const calc = (n1, opérateur, n2) => {
  // Effectue le calcul et renvoie la valeur calculée
}

Si l'opérateur est add, nous voulons ajouter des valeurs ensemble. Si l'opérateur est soustrait, nous voulons soustraire les valeurs, et ainsi de suite.

const calc = (n1, opérateur, n2) => {
  laisser résultat = ''
  if (opérateur === 'ajouter') {
    résultat = n1 + n2
  } else if (opérateur === 'soustraire') {
    résultat = n1 - n2
  } else if (opérateur === 'multiplier') {
    résultat = n1 * n2
  } else if (opérateur === 'diviser') {
    résultat = n1 / n2
  }
  résultat retourné
}

Rappelez-vous que firstValue et secondValue sont des chaînes à ce stade. Si vous additionnez des chaînes, vous les concaténerez (1 + 1 = 11).

Donc, avant de calculer le résultat, nous voulons convertir les chaînes en nombres. Nous pouvons le faire avec les deux fonctions parseInt et parseFloat.

  • parseInt convertit une chaîne en un entier.
  • parseFloat convertit une chaîne en float (cela signifie un nombre avec des décimales).

Pour une calculatrice, nous avons besoin d'un float.

const calc = (n1, opérateur, n2) => {
  laisser résultat = ''
  if (opérateur === 'ajouter') {
    résultat = parseFloat (n1) + parseFloat (n2)
  } else if (opérateur === 'soustraire') {
    resultat = parseFloat (n1) - parseFloat (n2)
  } else if (opérateur === 'multiplier') {
    résultat = parseFloat (n1) * parseFloat (n2)
  } else if (opérateur === 'diviser') {
    resultat = parseFloat (n1) / parseFloat (n2)
  }
  résultat retourné
}

C’est la bonne voie!

Vous pouvez récupérer le code source du chemin heureux via ce lien (faites défiler vers le bas et entrez votre adresse électronique dans la boîte. J'enverrai les codes source directement dans votre boîte aux lettres).

Les cas de bord

Le bon chemin ne suffit pas. Pour construire une calculatrice robuste, vous devez la rendre résiliente à des modèles de saisie étranges. Pour ce faire, vous devez imaginer un fauteur de troubles qui tente de casser votre calculatrice en appuyant sur les touches dans le mauvais ordre. Appelons ce fauteur de troubles Tim.

Tim peut appuyer sur ces touches dans n'importe quel ordre:

  1. Une touche numérique (0 à 9)
  2. Une clé opérateur (+, -, ×, ÷)
  3. La touche décimale
  4. La clé d'égalité
  5. La clé claire

Que se passe-t-il si Tim appuie sur la touche décimale

Si Tim appuie sur une touche décimale alors que l'écran affiche déjà un point décimal, rien ne devrait se produire.

Ici, on peut vérifier que le numéro affiché contient un. avec la méthode includes.

inclut les chaînes de vérification pour un match donné. Si une chaîne est trouvée, elle retourne true; sinon, il retourne faux.

Remarque: includes est sensible à la casse.

// Exemple de comment comprend le travail.
const string = 'Les hamburgers sont plutôt bons!'
const hasExclaimation = string.includes ('!')
console.log (hasExclaimation) // true

Pour vérifier si la chaîne a déjà un point, procédez comme suit:

// Ne fait rien si la chaîne a un point
if (! displayNum.includes ('.')) {
  display.textContent = displayNum + '.'
}

Ensuite, si Tim appuie sur la touche décimale après avoir appuyé sur une touche de l'opérateur, l'écran doit afficher 0 ..

Ici, nous devons savoir si la clé précédente est un opérateur. Nous pouvons le savoir en vérifiant l'attribut personnalisé, type de clé de données précédente, défini dans la leçon précédente.

data-previous-key-type n'est pas encore complet. Pour identifier correctement si previousKeyType est un opérateur, nous devons mettre à jour previousKeyType pour chaque clé cliquée.

si (! action) {
  // ...
  calculator.dataset.previousKey = 'nombre'
}
if (action === 'décimal') {
  // ...
  calculator.dataset.previousKey = 'décimal'
}
if (action === 'clear') {
  // ...
  calculator.dataset.previousKeyType = 'clear'
}
if (action === 'calculer') {
 // ...
  calculator.dataset.previousKeyType = 'calculer'
}

Une fois que nous avons le type correct keyKey précédent, nous pouvons l’utiliser pour vérifier si la clé précédente est un opérateur.

if (action === 'décimal') {
  if (! displayNum.includes ('.')) {
    display.textContent = displayNum + '.'
  } else if (previousKeyType === 'opérateur') {
    display.textContent = '0.'
  }
calculator.dataset.previousKeyType = 'décimal'
}

Que se passe-t-il si Tim appuie sur une touche de l'opérateur?

Si Tim appuie d'abord sur une touche de l'opérateur, celle-ci doit s'allumer. (Nous avons déjà pris en charge ce problème, mais comment? Voyez si vous pouvez identifier ce que nous avons fait).

Deuxièmement, rien ne devrait se passer si Tim appuie plusieurs fois sur la même clé de l’opérateur. (Nous sommes déjà couverts pour ce cas de bord aussi).

Remarque: si vous souhaitez fournir une meilleure expérience utilisateur, vous pouvez montrer à l'opérateur que l'utilisateur clique à plusieurs reprises sur celui-ci avec certaines modifications CSS. Nous ne l’avons pas fait ici, mais voyez si vous pouvez le programmer vous-même comme défi de codage supplémentaire.

Troisièmement, si Tim appuie sur une autre clé d’opérateur après avoir appuyé sur la première clé, celle-ci doit être relâchée. Ensuite, la deuxième clé de l'opérateur doit être enfoncée. (Nous avons couvert pour ce cas de bord aussi - mais comment?).

Quatrièmement, si Tim frappe un nombre, un opérateur, un nombre et un autre opérateur, dans cet ordre, l'affichage doit être mis à jour avec une valeur calculée.

Cela signifie que nous devons utiliser la fonction de calcul quand firstValue, operator et secondValue existent.

si (
  action === 'add' ||
  action === 'soustraire' ||
  action === 'multiplie' ||
  action === 'diviser'
) {
  const firstValue = calculator.dataset.firstValue
  opérateur const = calculator.dataset.operator
  const secondValue = displayNum
// Note: Il suffit de vérifier firstValue et operator car secondValue existe toujours
  if (firstValue && operator) {
    display.textContent = calculer (firstValue, operator, secondValue)
  }
key.classList.add ('est-déprimé')
  calculator.dataset.previousKeyType = 'operator'
  calculator.dataset.firstValue = displayNum
  calculator.dataset.operator = action
}

Bien que nous puissions calculer une valeur lorsque la clé de l’opérateur est cliquée une seconde fois, nous avons également introduit un bogue à ce stade: des clics supplémentaires sur la clé de l’opérateur permettent de calculer une valeur alors qu’elle ne le devrait pas.

Pour empêcher la calculatrice d’effectuer un calcul sur les clics ultérieurs sur la clé d’opérateur, nous devons vérifier si le type de clé previousKey est un opérateur. Si c’est le cas, nous ne faisons pas de calcul.

si (
  firstValue &&
  opérateur &&
  previousKeyType! == 'opérateur'
) {
  display.textContent = calculer (firstValue, operator, secondValue)
}

Cinquièmement, après que la clé de l’opérateur ait calculé un nombre, si Tim touche un nombre, suivi d’un autre opérateur, l’opérateur doit poursuivre le calcul comme suit: 8 - 1 = 7, 7 - 2 = 5, 5 - 3 = 2 .

À l'heure actuelle, notre calculatrice ne peut pas effectuer de calculs consécutifs. La deuxième valeur calculée est fausse. Voici ce que nous avons: 99 - 1 = 98, 98 - 1 = 0.

La deuxième valeur est mal calculée car nous avons introduit les mauvaises valeurs dans la fonction de calcul. Passons en revue quelques images pour comprendre ce que fait notre code.

Comprendre notre fonction de calcul

Tout d’abord, supposons qu’un utilisateur clique sur un nombre, 99. À ce stade, rien n’est encore enregistré dans la calculatrice.

Deuxièmement, supposons que l’utilisateur clique sur l’opérateur de soustraction. Après avoir cliqué sur l'opérateur de soustraction, nous avons défini firstValue à 99. Nous avons également paramétré l'opérateur sur soustraction.

Troisièmement, supposons que l’utilisateur clique sur une deuxième valeur - cette fois, elle est 1. À ce stade, le nombre affiché est mis à jour à 1, mais notre première valeur, opérateur et notre deuxième valeur restent inchangés.

Quatrièmement, l'utilisateur clique à nouveau sur soustraire. Juste après avoir cliqué sur Soustraire, avant de calculer le résultat, nous avons défini secondValue comme nombre affiché.

Cinquièmement, nous effectuons le calcul avec firstValue 99, soustraction opérateur et secondValue 1. Le résultat est 98.

Une fois le résultat calculé, nous ajustons l'affichage au résultat. Ensuite, nous configurons l’opérateur pour soustraire, et firstValue au nombre précédent affiché.

Eh bien, c’est terriblement faux! Si nous voulons continuer avec le calcul, nous devons mettre à jour firstValue avec la valeur calculée.

const firstValue = calculator.dataset.firstValue
opérateur const = calculator.dataset.operator
const secondValue = displayNum
si (
  firstValue &&
  opérateur &&
  previousKeyType! == 'opérateur'
) {
  const calcValue = calcule (firstValue, operator, secondValue)
  display.textContent = calcValue
// Met à jour la valeur calculée comme première valeur
  calculator.dataset.firstValue = calcValue
} autre {
  // S'il n'y a pas de calcul, définissez displayNum comme première valeur.
  calculator.dataset.firstValue = displayNum
}
key.classList.add ('est-déprimé')
calculator.dataset.previousKeyType = 'operator'
calculator.dataset.operator = action

Avec ce correctif, les calculs consécutifs effectués par les clés de l'opérateur doivent maintenant être corrects.

Que se passe-t-il si Tim appuie sur la touche d'égalité?

Premièrement, rien ne devrait se produire si Tim appuie sur la touche égal à avant les touches de l'opérateur.

Nous savons que l'utilisateur n'a pas encore cliqué sur les clés de l'opérateur si firstValue n'est pas défini sur un nombre. Nous pouvons utiliser cette connaissance pour empêcher les égaux de calculer.

if (action === 'calculer') {
  const firstValue = calculator.dataset.firstValue
  opérateur const = calculator.dataset.operator
  const secondValue = displayNum
si (première valeur) {
    display.textContent = calculer (firstValue, operator, secondValue)
  }
calculator.dataset.previousKeyType = 'calculer'
}

Deuxièmement, si Tim frappe un nombre, suivi d'un opérateur, suivi d'un égal, la calculatrice devrait calculer le résultat de telle sorte que:

  1. 2 + = -> 2 + 2 = 4
  2. 2 - = -> 2 - 2 = 0
  3. 2 × = -> 2 × 2 = 4
  4. 2 = -> 2 2 = 1

Nous avons déjà pris en compte cette entrée étrange. Pouvez-vous comprendre pourquoi? :)

Troisièmement, si Tim appuie sur la touche égal à la fin du calcul, un autre calcul doit être effectué à nouveau. Voici comment le calcul devrait se lire:

  1. Tim appuie sur les touches 5 et 1
  2. Tim frappe égal. La valeur calculée est 5 - 1 = 4
  3. Tim frappe égal. La valeur calculée est 4 - 1 = 3
  4. Tim frappe égal. La valeur calculée est 3 - 1 = 2
  5. Tim frappe égal. La valeur calculée est 2 - 1 = 1
  6. Tim frappe égal. La valeur calculée est 1 - 1 = 0

Malheureusement, notre calculatrice gâche ce calcul. Voici ce que notre calculatrice montre:

  1. Tim appuie sur la touche 5–1
  2. Tim frappe égal. La valeur calculée est 4
  3. Tim frappe égal. La valeur calculée est 1

Corriger le calcul

Tout d’abord, disons que notre utilisateur clique 5. À ce stade, rien n’est encore enregistré dans la calculatrice.

Deuxièmement, supposons que l’utilisateur clique sur l’opérateur de soustraction. Après avoir cliqué sur l'opérateur de soustraction, nous avons défini firstValue sur 5. Nous avons également paramétré l'opérateur sur soustraction.

Troisièmement, l'utilisateur clique sur une deuxième valeur. Disons que c’est 1. À ce stade, le nombre affiché est mis à jour à 1, mais nos firstValue, operator et secondValue restent inchangées.

Quatrièmement, l'utilisateur clique sur la clé égale. Juste après avoir cliqué sur égal, mais avant le calcul, nous définissons secondValue comme indiqué.

Cinquièmement, la calculatrice calcule le résultat de 5 - 1 et donne 4. Le résultat est mis à jour à l'écran. firstValue et l'opérateur sont reportés au prochain calcul car nous ne les avons pas mis à jour.

Sixièmement, lorsque l'utilisateur clique à nouveau sur égal, nous définissons deuxièmeValeur à displayNum avant le calcul.

Vous pouvez dire ce qui ne va pas ici.

Au lieu de secondValue, nous voulons que set firstValue corresponde au nombre affiché.

if (action === 'calculer') {
  let firstValue = calculator.dataset.firstValue
  opérateur const = calculator.dataset.operator
  const secondValue = displayNum
si (première valeur) {
    if (previousKeyType === 'calculer') {
      firstValue = displayNum
    }
display.textContent = calculer (firstValue, operator, secondValue)
  }
calculator.dataset.previousKeyType = 'calculer'
}

Nous souhaitons également reporter la seconde valeur précédente dans le nouveau calcul. Pour que secondValue persiste dans le prochain calcul, nous devons le stocker dans un autre attribut personnalisé. Appelons cet attribut personnalisé modValue (correspond à la valeur du modificateur).

if (action === 'calculer') {
  let firstValue = calculator.dataset.firstValue
  opérateur const = calculator.dataset.operator
  const secondValue = displayNum
si (première valeur) {
    if (previousKeyType === 'calculer') {
      firstValue = displayNum
    }
display.textContent = calculer (firstValue, operator, secondValue)
  }
// Définir l'attribut modValue
  calculator.dataset.modValue = secondValue
  calculator.dataset.previousKeyType = 'calculer'
}

Si previousKeyType est calcule, nous savons que nous pouvons utiliser calculator.dataset.modValue en tant que secondValue. Une fois que nous savons cela, nous pouvons effectuer le calcul.

si (première valeur) {
  if (previousKeyType === 'calculer') {
    firstValue = displayNum
    secondValue = calculator.dataset.modValue
  }
display.textContent = calculer (firstValue, operator, secondValue)
}

Avec cela, nous avons le calcul correct lorsque la clé égale est cliquée consécutivement.

Retour à la clé d'égalité

Quatrièmement, si Tim appuie sur une touche décimale ou sur une touche numérique après la touche de la calculatrice, l'affichage doit être remplacé par 0 ou le nouveau numéro, respectivement.

Ici, au lieu de simplement vérifier si previousKeyType est opérateur, nous devons également vérifier s’il est calculé.

si (! action) {
  si (
    displayNum === '0' ||
    previousKeyType === 'opérateur' ||
    previousKeyType === 'calculer'
  ) {
    display.textContent = keyContent
  } autre {
    display.textContent = displayNum + keyContent
  }
  calculator.dataset.previousKeyType = 'nombre'
}
if (action === 'décimal') {
  if (! displayNum.includes ('.')) {
    display.textContent = displayNum + '.'
  } sinon si (
    previousKeyType === 'opérateur' ||
    previousKeyType === 'calculer'
  ) {
    display.textContent = '0.'
  }
calculator.dataset.previousKeyType = 'décimal'
}

Cinquièmement, si Tim appuie sur une touche de l'opérateur juste après la touche d'égalité, la calculatrice ne doit pas calculer.

Pour ce faire, nous vérifions si le previousKeyType est calculé avant d'effectuer des calculs avec des clés d'opérateur.

si (
  action === 'add' ||
  action === 'soustraire' ||
  action === 'multiplie' ||
  action === 'diviser'
) {
  // ...
si (
    firstValue &&
    opérateur &&
    previousKeyType! == 'operator' &&
    previousKeyType! == 'calculer'
  ) {
    const calcValue = calcule (firstValue, operator, secondValue)
    display.textContent = calcValue
    calculator.dataset.firstValue = calcValue
  } autre {
    calculator.dataset.firstValue = displayNum
  }
// ...
}

La clé en clair a deux utilisations:

  1. Tout effacer (indiqué par AC) efface tout et réinitialise la calculatrice à son état initial.
  2. Effacer l'entrée (indiquée par CE) efface l'entrée actuelle. Il garde les numéros précédents en mémoire.

Lorsque la calculatrice est dans son état par défaut, AC doit apparaître.

D'abord, si Tim appuie sur une touche (n'importe quelle touche sauf la touche clear), AC doit être remplacé par CE.

Nous faisons cela en vérifiant si l'action data est claire. Si ce n’est pas clair, on cherche le bouton clear et on change son textContent.

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

Deuxièmement, si Tim touche CE, l’écran doit afficher 0. En même temps, CE doit redevenir AC afin que Tim puisse réinitialiser la calculatrice à son état initial. **

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

Troisièmement, si Tim touche AC, réinitialisez la calculatrice à son état initial.

Pour réinitialiser la calculatrice à son état initial, nous devons effacer tous les attributs personnalisés que nous avons définis.

if (action === 'clear') {
  if (key.textContent === 'AC') {
    calculator.dataset.firstValue = ''
    calculator.dataset.modValue = ''
    calculator.dataset.operator = ''
    calculator.dataset.previousKeyType = ''
  } autre {
    key.textContent = 'AC'
  }
display.textContent = 0
  calculator.dataset.previousKeyType = 'clear'
}

C’est tout - pour la partie des cas extrêmes, en tout cas!

Vous pouvez récupérer le code source de la partie des cas périphériques via ce lien (faites défiler vers le bas et entrez votre adresse e-mail dans la zone, et j'enverrai les codes source directement dans votre boîte aux lettres).

À ce stade, le code que nous avons créé ensemble est assez déroutant. Vous allez probablement vous perdre si vous essayez de lire le code vous-même. Refactorisons le pour le rendre plus propre.

Refactoriser le code

Lorsque vous effectuez une refactorisation, vous commencez souvent par les améliorations les plus évidentes. Dans ce cas, commençons par calculer.

Avant de continuer, assurez-vous de connaître ces pratiques / fonctionnalités JavaScript. Nous les utiliserons dans le refactor.

  1. Premiers retours
  2. Opérateurs ternaires
  3. Fonctions pures
  4. ES6 Destructuration

Sur ce, commençons!

Refactoriser la fonction de calcul

Voici ce que nous avons jusqu'à présent.

const calc = (n1, opérateur, n2) => {
  laisser résultat = ''
  if (opérateur === 'ajouter') {
    résultat = firstNum + parseFloat (n2)
  } else if (opérateur === 'soustraire') {
    resultat = parseFloat (n1) - parseFloat (n2)
  } else if (opérateur === 'multiplier') {
    résultat = parseFloat (n1) * parseFloat (n2)
  } else if (opérateur === 'diviser') {
    resultat = parseFloat (n1) / parseFloat (n2)
  }
  résultat retourné
}

Vous avez appris que nous devrions réduire autant que possible les réaffectations. Ici, nous pouvons supprimer des assignations si nous retournons le résultat du calcul dans les instructions if et else if:

const calc = (n1, opérateur, n2) => {
  if (opérateur === 'ajouter') {
    return firstNum + parseFloat (n2)
  } else if (opérateur === 'soustraire') {
    retourne parseFloat (n1) - parseFloat (n2)
  } else if (opérateur === 'multiplier') {
    retourne parseFloat (n1) * parseFloat (n2)
  } else if (opérateur === 'diviser') {
    retourne parseFloat (n1) / parseFloat (n2)
  }
}

Puisque nous renvoyons toutes les valeurs, nous pouvons utiliser les retours anticipés. Si nous le faisons, il n’y aura pas besoin d’autre chose si les conditions le permettent.

const calc = (n1, opérateur, n2) => {
  if (opérateur === 'ajouter') {
    return firstNum + parseFloat (n2)
  }
  if (opérateur === 'soustraire') {
    retourne parseFloat (n1) - parseFloat (n2)
  }
  if (opérateur === 'multiplie') {
    retourne parseFloat (n1) * parseFloat (n2)
  }
  if (opérateur === 'diviser') {
    retourne parseFloat (n1) / parseFloat (n2)
  }
}

Et puisque nous avons une déclaration par condition if, nous pouvons supprimer les crochets. (Remarque: certains développeurs ne jurent que par les accolades). Voici à quoi ressemblerait le code:

const calc = (n1, opérateur, n2) => {
  if (opérateur === 'add') retourne parseFloat (n1) + parseFloat (n2)
  if (opérateur === 'soustraire') retourne parseFloat (n1) - parseFloat (n2)
  if (opérateur === 'multiplier') retourne parseFloat (n1) * parseFloat (n2)
  if (opérateur === 'diviser') retourne parseFloat (n1) / parseFloat (n2)
}

Enfin, nous avons appelé parseFloat huit fois dans la fonction. Nous pouvons le simplifier en créant deux variables contenant des valeurs flottantes:

const calc = (n1, opérateur, n2) => {
  const firstNum = parseFloat (n1)
  const secondNum = parseFloat (n2)
  if (opérateur === 'add') retourne firstNum + secondNum
  if (opérateur === 'soustraire') renvoie firstNum - secondNum
  if (operator === 'multiply') renvoie firstNum * secondNum
  if (opérateur === 'diviser') retourne firstNum / secondNum
}

Nous avons fini avec calculer maintenant. Ne pensez-vous pas que c'est plus facile à lire qu'auparavant?

Refactoriser l'écouteur d'événement

Le code que nous avons créé pour l'écouteur d'événement est énorme. Voici ce que nous avons pour le moment:

keys.addEventListener ('click', e => {
  if (e.target.matches ('bouton')) {
    si (! action) {/ * ... * /}
    if (action === 'add' ||
      action === 'soustraire' ||
      action === 'multiplie' ||
      action === 'diviser') {
      / * ... * /
    }
    if (action === 'clear') {/ * ... * /}
    if (action! == 'clear') {/ * ... * /}
    if (action === 'calculer') {/ * ... * /}
  }
})

Comment commencez-vous à refactoriser ce morceau de code? Si vous ne connaissez pas les meilleures pratiques de programmation, vous pourriez être tenté de refactoriser en divisant chaque type d’action en une fonction plus petite:

// Ne fais pas ça!
const handleNumberKeys = (/ * ... * /) => {/ * ... * /}
const handleOperatorKeys = (/ * ... * /) => {/ * ... * /}
const handleDecimalKey = (/ * ... * /) => {/ * ... * /}
const handleClearKey = (/ * ... * /) => {/ * ... * /}
const handleCalculateKey = (/ * ... * /) => {/ * ... * /}

Ne fais pas ça. Cela n’aide pas, car vous ne faites que diviser des blocs de code. Lorsque vous le faites, la fonction devient plus difficile à lire.

Un meilleur moyen consiste à scinder le code en fonctions pures et impures. Si vous le faites, vous obtiendrez un code ressemblant à ceci:

keys.addEventListener ('click', e => {
  // fonction pure
  const resultString = createResultString (/ * ... * /)
  // trucs impurs
  display.textContent = resultString
  updateCalculatorState (/ * ... * /)
})

Ici, createResultString est une fonction pure qui renvoie ce qui doit être affiché sur la calculatrice. updateCalculatorState est une fonction impure qui modifie l'apparence visuelle et les attributs personnalisés de la calculatrice.

Faire createResultString

Comme mentionné précédemment, createResultString doit renvoyer la valeur qui doit être affichée sur la calculatrice.
Vous pouvez obtenir ces valeurs via des parties du code qui indiquent display.textContent = 'une valeur.

display.textContent = 'une valeur'

Au lieu de display.textContent = 'une valeur', nous voulons renvoyer chaque valeur pour pouvoir l'utiliser plus tard.

// remplace ce qui précède par ceci
retourne 'une certaine valeur'

Examinons cela ensemble, étape par étape, en commençant par les touches numériques.

Faire la chaîne de résultat pour les clés numériques

Voici le code que nous avons pour les touches numériques:

si (! action) {
  si (
    displayNum === '0' ||
    previousKeyType === 'opérateur' ||
    previousKeyType === 'calculer'
  ) {
    display.textContent = keyContent
  } autre {
    display.textContent = displayNum + keyContent
  }
  calculator.dataset.previousKeyType = 'nombre'
}

La première étape consiste à copier les éléments tels que display.textContent = 'une valeur' ​​dans createResultString. Lorsque vous faites cela, assurez-vous de changer display.textContent = en return.

const createResultString = () => {
  si (! action) {
    si (
      displayNum === '0' ||
      previousKeyType === 'opérateur' ||
      previousKeyType === 'calculer'
    ) {
      retourneContenu
    } autre {
      retourne affichéNum + cléContenu
    }
  }
}

Ensuite, nous pouvons convertir l'instruction if / else en un opérateur ternaire:

const createResultString = () => {
  si (action!) {
    retour displayNum === '0' ||
      previousKeyType === 'opérateur' ||
      previousKeyType === 'calculer'
      ? cléContenu
      : displayNum + keyContent
  }
}

Lorsque vous effectuez une refactorisation, n'oubliez pas de noter une liste de variables dont vous avez besoin. Nous reviendrons sur la liste plus tard.

const createResultString = () => {
  // Les variables requises sont:
  // 1. contenu clé
  // 2. displayNum
  // 3. previousKeyType
  // 4. action
  si (action!) {
    retour displayNum === '0' ||
      previousKeyType === 'opérateur' ||
      previousKeyType === 'calculer'
      ? cléContenu
      : displayNum + keyContent
  }
}

Faire la chaîne de résultat pour la clé décimale

Voici le code que nous avons pour la clé décimale:

if (action === 'décimal') {
  if (! displayNum.includes ('.')) {
    display.textContent = displayNum + '.'
  } sinon si (
    previousKeyType === 'opérateur' ||
    previousKeyType === 'calculer'
  ) {
    display.textContent = '0.'
  }
  calculator.dataset.previousKeyType = 'décimal'
}

Comme précédemment, nous voulons déplacer tout ce qui change display.textContentinto à createResultString.

const createResultString = () => {
  // ...
  if (action === 'décimal') {
    if (! displayNum.includes ('.')) {
      return = displayNum + '.'
    } else if (previousKeyType === 'opérateur' || previousKeyType === 'calculer') {
      return = '0.'
    }
  }
}

Puisque nous voulons renvoyer toutes les valeurs, nous pouvons convertir d’autres instructions if en retours anticipés.

const createResultString = () => {
  // ...
  if (action === 'décimal') {
    if (! displayNum.includes ('.')) renvoie displayNum + '.'
    if (previousKeyType === 'opérateur' || previousKeyType === 'calculer') renvoie '0.'
  }
}

Une erreur courante consiste à oublier de renvoyer le numéro actuellement affiché lorsqu'aucune des conditions ne correspond. Nous en avons besoin car nous allons remplacer display.textContent par la valeur renvoyée par createResultString. Si nous l'avons manqué, createResultString retournera undefined, ce qui n'est pas ce que nous souhaitons.

const createResultString = () => {
  // ...
  if (action === 'décimal') {
    if (! displayNum.includes ('.')) renvoie displayNum + '.'
    if (previousKeyType === 'opérateur' || previousKeyType === 'calculer') renvoie '0.'
    retour affichéNum
  }
}

Comme toujours, notez les variables requises. À ce stade, les variables requises restent les mêmes qu'auparavant:

const createResultString = () => {
  // Les variables requises sont:
  // 1. contenu clé
  // 2. displayNum
  // 3. previousKeyType
  // 4. action
}

Création de la chaîne de résultat pour les clés d'opérateur

Voici le code que nous avons écrit pour les clés d’opérateur.

si (
  action === 'add' ||
  action === 'soustraire' ||
  action === 'multiplie' ||
  action === 'diviser'
) {
  const firstValue = calculator.dataset.firstValue
  opérateur const = calculator.dataset.operator
  const secondValue = displayNum
  si (
    firstValue &&
    opérateur &&
    previousKeyType! == 'operator' &&
    previousKeyType! == 'calculer'
  ) {
    const calcValue = calcule (firstValue, operator, secondValue)
    display.textContent = calcValue
    calculator.dataset.firstValue = calcValue
  } autre {
    calculator.dataset.firstValue = displayNum
  }
  key.classList.add ('est-déprimé')
  calculator.dataset.previousKeyType = 'operator'
  calculator.dataset.operator = action
}

Vous connaissez déjà l’exercice: nous voulons déplacer tout ce qui change display.textContent dans createResultString. Voici ce qui doit être déplacé:

const createResultString = () => {
  // ...
  si (
    action === 'add' ||
    action === 'soustraire' ||
    action === 'multiplie' ||
    action === 'diviser'
  ) {
    const firstValue = calculator.dataset.firstValue
    opérateur const = calculator.dataset.operator
    const secondValue = displayNum
    si (
      firstValue &&
      opérateur &&
      previousKeyType! == 'operator' &&
      previousKeyType! == 'calculer'
    ) {
      retour calculer (firstValue, operator, secondValue)
    }
  }
}

Rappelez-vous que createResultString doit renvoyer la valeur à afficher sur la calculatrice. Si la condition if ne correspond pas, nous souhaitons toujours renvoyer le nombre affiché.

const createResultString = () => {
  // ...
  si (
    action === 'add' ||
    action === 'soustraire' ||
    action === 'multiplie' ||
    action === 'diviser'
  ) {
    const firstValue = calculator.dataset.firstValue
    opérateur const = calculator.dataset.operator
    const secondValue = displayNum
    si (
      firstValue &&
      opérateur &&
      previousKeyType! == 'operator' &&
      previousKeyType! == 'calculer'
    ) {
      retour calculer (firstValue, operator, secondValue)
    } autre {
      retour affichéNum
    }
  }
}

Nous pouvons ensuite reformuler la déclaration if / else en un opérateur ternaire:

const createResultString = () => {
  // ...
  si (
    action === 'add' ||
    action === 'soustraire' ||
    action === 'multiplie' ||
    action === 'diviser'
  ) {
    const firstValue = calculator.dataset.firstValue
    opérateur const = calculator.dataset.operator
    const secondValue = displayNum
    return firstValue &&
      opérateur &&
      previousKeyType! == 'operator' &&
      previousKeyType! == 'calculer'
      ? calculer (firstValue, operator, secondValue)
      : affichéNum
  }
}

Si vous regardez de près, vous vous rendrez compte qu’il n’est pas nécessaire de stocker une secondeValueVariable. Nous pouvons utiliser displayNum directement dans la fonction de calcul.

const createResultString = () => {
  // ...
  si (
    action === 'add' ||
    action === 'soustraire' ||
    action === 'multiplie' ||
    action === 'diviser'
  ) {
    const firstValue = calculator.dataset.firstValue
    opérateur const = calculator.dataset.operator
    return firstValue &&
      opérateur &&
      previousKeyType! == 'operator' &&
      previousKeyType! == 'calculer'
      ? calculer (première valeur, opérateur, nombre affiché)
      : affichéNum
  }
}

Enfin, notez les variables et les propriétés requises. Cette fois, nous avons besoin de calculator.dataset.firstValue et de calculator.dataset.operator.

const createResultString = () => {
  // Les variables et propriétés requises sont:
  // 1. contenu clé
  // 2. displayNum
  // 3. previousKeyType
  // 4. action
  // 5. calculator.dataset.firstValue
  // 6. calculator.dataset.operator
}

Faire la chaîne de résultat pour la clé en clair

Nous avons écrit le code suivant pour gérer la clé en clair.

if (action === 'clear') {
  if (key.textContent === 'AC') {
    calculator.dataset.firstValue = ''
    calculator.dataset.modValue = ''
    calculator.dataset.operator = ''
    calculator.dataset.previousKeyType = ''
  } autre {
    key.textContent = 'AC'
  }
  display.textContent = 0
  calculator.dataset.previousKeyType = 'clear'
}

Comme ci-dessus, vous souhaitez déplacer tout ce qui change display.textContentinto à createResultString.

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

Faire la chaîne de résultat pour la clé equals

Voici le code que nous avons écrit pour la clé equals:

if (action === 'calculer') {
  let firstValue = calculator.dataset.firstValue
  opérateur const = calculator.dataset.operator
  laisser secondValue = displayNum
  si (première valeur) {
    if (previousKeyType === 'calculer') {
      firstValue = displayNum
      secondValue = calculator.dataset.modValue
    }
    display.textContent = calculer (firstValue, operator, secondValue)
  }
  calculator.dataset.modValue = secondValue
  calculator.dataset.previousKeyType = 'calculer'
}

Comme ci-dessus, nous souhaitons copier tout ce qui change. Voici ce qui doit être copié:

if (action === 'calculer') {
  let firstValue = calculator.dataset.firstValue
  opérateur const = calculator.dataset.operator
  laisser secondValue = displayNum
  si (première valeur) {
    if (previousKeyType === 'calculer') {
      firstValue = displayNum
      secondValue = calculator.dataset.modValue
    }
    display.textContent = calculer (firstValue, operator, secondValue)
  }
}

Lors de la copie du code dans createResultString, assurez-vous de renvoyer des valeurs pour chaque scénario possible:

const createResultString = () => {
  // ...
  if (action === 'calculer') {
    let firstValue = calculator.dataset.firstValue
    opérateur const = calculator.dataset.operator
    laisser secondValue = displayNum
    si (première valeur) {
      if (previousKeyType === 'calculer') {
        firstValue = displayNum
        secondValue = calculator.dataset.modValue
      }
      retour calculer (firstValue, operator, secondValue)
    } autre {
      retour affichéNum
    }
  }
}

Ensuite, nous voulons réduire les réaffectations. Nous pouvons le faire en passant les valeurs correctes dans calcul par l’intermédiaire d’un opérateur ternaire.

const createResultString = () => {
  // ...
  if (action === 'calculer') {
    const firstValue = calculator.dataset.firstValue
    opérateur const = calculator.dataset.operator
    const modValue = calculator.dataset.modValue
    si (première valeur) {
      return previousKeyType === 'calcule'
        ? calculer (affichéNum, opérateur, modValue)
        : calculer (valeur première, opérateur, nombre affiché)
    } autre {
      retour affichéNum
    }
  }
}

Vous pouvez simplifier davantage le code ci-dessus avec un autre opérateur ternaire si vous êtes à l'aise avec ce code:

const createResultString = () => {
  // ...
  if (action === 'calculer') {
    const firstValue = calculator.dataset.firstValue
    opérateur const = calculator.dataset.operator
    const modValue = calculator.dataset.modValue
    retour première valeur
      ? previousKeyType === 'calculer'
        ? calculer (affichéNum, opérateur, modValue)
        : calculer (valeur première, opérateur, nombre affiché)
      : affichéNum
  }
}

À ce stade, nous souhaitons prendre note des propriétés et des variables requises à nouveau:

const createResultString = () => {
  // Les variables et propriétés requises sont:
  // 1. contenu clé
  // 2. displayNum
  // 3. previousKeyType
  // 4. action
  // 5. calculator.dataset.firstValue
  // 6. calculator.dataset.operator
  // 7. calculator.dataset.modValue
}

Passer les variables nécessaires

Il faut sept propriétés / variables dans createResultString:

  1. cléContenu
  2. affichéNum
  3. previousKeyType
  4. action
  5. première valeur
  6. valeur mod
  7. opérateur

Nous pouvons obtenir le contenu clé et l'action de la clé. Nous pouvons également obtenir firstValue, modValue, operator et previousKeyType de calculator.dataset.

Cela signifie que la fonction createResultString a besoin de trois variables: key, displayNum et calculator.dataset. Puisque calculator.dataset représente l’état de la calculatrice, utilisons une variable appelée état au lieu de celle-ci.

const createResultString = (clé, nombre affiché, état) => {
  const keyContent = key.textContent
  action const = key.dataset.action
  const firstValue = state.firstValue
  const modValue = state.modValue
  opérateur const = state.operator
  const previousKeyType = state.previousKeyType
  // ... refactor si nécessaire
}
// Utilisation de createResultString
keys.addEventListener ('click', e => {
  if (e.target.matches ('button')) renvoie
  const displayNum = display.textContent
  const resultString = createResultString (e.target, displayNum, calculator.dataset)
  // ...
})

N'hésitez pas à déstructurer les variables si vous le souhaitez:

const createResultString = (clé, nombre affiché, état) => {
  const keyContent = key.textContent
  const {action} = key.dataset
  const {
    première valeur,
    valeur modale,
    opérateur,
    previousKeyType
  } = état
  // ...
}

Cohérence dans les déclarations if

Dans createResultString, nous avons utilisé les conditions suivantes pour tester le type de clé sur lequel l'utilisateur a cliqué:

// Si la clé est un nombre
si (! action) {/ * ... * /}
// Si la clé est décimale
if (action === 'décimal') {/ * ... * /}
// Si la clé est opérateur
si (
  action === 'add' ||
  action === 'soustraire' ||
  action === 'multiplie' ||
  action === 'diviser'
) {/ * ... * /}
// Si la clé est claire
if (action === 'clear') {/ * ... * /}
// Si la clé est calculée
if (action === 'calculer') {/ * ... * /}

Ils ne sont pas cohérents, ils sont donc difficiles à lire. Si possible, nous voulons les rendre cohérents pour pouvoir écrire quelque chose comme ceci:

if (keyType === 'numéro') {/ * ... * /}
if (keyType === 'décimal') {/ * ... * /}
if (keyType === 'opérateur') {/ * ... * /}
if (keyType === 'clear') {/ * ... * /}
if (keyType === 'calculer') {/ * ... * /}

Pour ce faire, nous pouvons créer une fonction appelée getKeyType. Cette fonction doit renvoyer le type de clé sur lequel vous avez cliqué.

const getKeyType = (clé) => {
  const {action} = key.dataset
  si (! action) retourne 'numéro'
  si (
    action === 'add' ||
    action === 'soustraire' ||
    action === 'multiplie' ||
    action === 'diviser'
  ) retourne 'opérateur'
  // Pour tout le reste, retourne l'action
  action de retour
}

Voici comment vous utiliseriez la fonction:

const createResultString = (clé, nombre affiché, état) => {
  const keyType = getKeyType (clé)
  if (keyType === 'numéro') {/ * ... * /}
  if (keyType === 'décimal') {/ * ... * /}
  if (keyType === 'opérateur') {/ * ... * /}
  if (keyType === 'clear') {/ * ... * /}
  if (keyType === 'calculer') {/ * ... * /}
}

Nous avons terminé avec createResultString. Passons à updateCalculatorState.

Faire updateCalculatorState

updateCalculatorState est une fonction qui modifie l'apparence visuelle et les attributs personnalisés de la calculatrice.

Comme avec createResultString, nous devons vérifier le type de clé sur lequel vous avez cliqué. Ici, nous pouvons réutiliser getKeyType.

const updateCalculatorState = (clé) => {
  const keyType = getKeyType (clé)
  if (keyType === 'numéro') {/ * ... * /}
  if (keyType === 'décimal') {/ * ... * /}
  if (keyType === 'opérateur') {/ * ... * /}
  if (keyType === 'clear') {/ * ... * /}
  if (keyType === 'calculer') {/ * ... * /}
}

Si vous examinez le code restant, vous remarquerez peut-être que nous modifions le type de données précédent-clé pour chaque type de clé. Voici à quoi ressemble le code:

const updateCalculatorState = (clé, calculatrice) => {
  const keyType = getKeyType (clé)
  si (! action) {
    // ...
    calculator.dataset.previousKeyType = 'nombre'
  }
  if (action === 'décimal') {
    // ...
    calculator.dataset.previousKeyType = 'décimal'
  }
  si (
    action === 'add' ||
    action === 'soustraire' ||
    action === 'multiplie' ||
    action === 'diviser'
  ) {
    // ...
    calculator.dataset.previousKeyType = 'operator'
  }
  if (action === 'clear') {
    // ...
    calculator.dataset.previousKeyType = 'clear'
  }
  if (action === 'calculer') {
    calculator.dataset.previousKeyType = 'calculer'
  }
}

Ceci est redondant car nous connaissons déjà le type de clé avec getKeyType. Nous pouvons restructurer ce qui précède pour:

const updateCalculatorState = (clé, calculatrice) => {
  const keyType = getKeyType (clé)
  calculator.dataset.previousKeyType = keyType
  if (keyType === 'numéro') {/ * ... * /}
  if (keyType === 'décimal') {/ * ... * /}
  if (keyType === 'opérateur') {/ * ... * /}
  if (keyType === 'clear') {/ * ... * /}
  if (keyType === 'calculer') {/ * ... * /}
}

Création de updateCalculatorState pour les clés d'opérateur

Visuellement, nous devons nous assurer que toutes les clés libèrent leur état déprimé. Ici, nous pouvons copier et coller le code que nous avions auparavant:

const updateCalculatorState = (clé, calculatrice) => {
  const keyType = getKeyType (clé)
  calculator.dataset.previousKeyType = keyType
  Array.from (key.parentNode.children) .forEach (k => k.classList.remove ('is-déprimé'))
}

Voici ce qui reste de ce que nous avons écrit pour les clés d’opérateur, après avoir déplacé des éléments liés à display.textContent dans createResultString.

if (keyType === 'operator') {
  if (firstValue &&
      opérateur &&
      previousKeyType! == 'operator' &&
      previousKeyType! == 'calculer'
  ) {
    calculator.dataset.firstValue = CalculValue
  } autre {
    calculator.dataset.firstValue = displayNum
  }
  key.classList.add ('est-déprimé')
  calculator.dataset.operator = key.dataset.action
}

Vous remarquerez peut-être que nous pouvons raccourcir le code avec un opérateur ternaire:

if (keyType === 'operator') {
  key.classList.add ('est-déprimé')
  calculator.dataset.operator = key.dataset.action
  calculator.dataset.firstValue = firstValue &&
    opérateur &&
    previousKeyType! == 'operator' &&
    previousKeyType! == 'calculer'
    ? valeur calculée
    : affichéNum
}

Comme précédemment, notez les variables et propriétés dont vous avez besoin. Ici, nous avons besoin de valeurs calculées et de valeurs affichées.

const updateCalculatorState = (clé, calculatrice) => {
  // Variables et propriétés nécessaires
  // 1. clé
  // 2. calculatrice
  // 3. valeur calculée
  // 4. displayNum
}

Faire updateCalculatorState pour la clé en clair

Voici le code restant pour la clé en clair:

if (action === 'clear') {
  if (key.textContent === 'AC') {
    calculator.dataset.firstValue = ''
    calculator.dataset.modValue = ''
    calculator.dataset.operator = ''
    calculator.dataset.previousKeyType = ''
  } autre {
    key.textContent = 'AC'
  }
}
if (action! == 'clear') {
  const clearButton = calculator.querySelector ('[data-action = clear]')
  clearButton.textContent = 'CE'
}

Il n’ya rien d’autre que nous puissions refactoriser ici. N'hésitez pas à tout copier / coller dans updateCalculatorState.

Faire updateCalculatorState pour la clé equals

Voici le code que nous avons écrit pour la clé equals:

if (action === 'calculer') {
  let firstValue = calculator.dataset.firstValue
  opérateur const = calculator.dataset.operator
  laisser secondValue = displayNum
  si (première valeur) {
    if (previousKeyType === 'calculer') {
      firstValue = displayNum
      secondValue = calculator.dataset.modValue
    }
    display.textContent = calculer (firstValue, operator, secondValue)
  }
  calculator.dataset.modValue = secondValue
  calculator.dataset.previousKeyType = 'calculer'
}

Voici ce qu’il nous reste si nous supprimons tout ce qui concerne display.textContent.

if (action === 'calculer') {
  laisser secondValue = displayNum
  si (première valeur) {
    if (previousKeyType === 'calculer') {
      secondValue = calculator.dataset.modValue
    }
  }
  calculator.dataset.modValue = secondValue
}

Nous pouvons reformuler ceci dans ce qui suit:

if (keyType === 'calculer') {
  calculator.dataset.modValue = firstValue && previousKeyType === 'calculer'
    ? valeur mod
    : affichéNum
}

Comme toujours, notez les propriétés et les variables utilisées:

const updateCalculatorState = (clé, calculatrice) => {
  // Variables et propriétés nécessaires
  // 1. clé
  // 2. calculatrice
  // 3. valeur calculée
  // 4. displayNum
  // 5. valeur_modale
}

Passer les variables nécessaires

Nous savons que nous avons besoin de cinq variables / propriétés pour updateCalculatorState:

  1. clé
  2. calculatrice
  3. valeur calculée
  4. affichéNum
  5. valeur mod

Puisque modValue peut être récupéré à partir de calculator.dataset, il suffit de transmettre quatre valeurs:

const updateCalculatorState = (clé, calculatrice, valeur calculée, nombre affiché) => {
  // ...
}
keys.addEventListener ('click', e => {
  if (e.target.matches ('button')) renvoie
  clé const = e.target
  const displayNum = display.textContent
  const resultString = createResultString (clé, nombre affiché, calculator.dataset)
  display.textContent = resultString
  // Transmettre les valeurs nécessaires
  updateCalculatorState (clé, calculatrice, chaîne de résultats, chaîne d'affichage)
})

Refactoring updateCalculatorState à nouveau

Nous avons modifié trois types de valeurs dans updateCalculatorState:

  1. calculator.dataset
  2. La classe des opérateurs presseurs / déprimants
  3. Texte AC vs CE

Si vous voulez le rendre plus propre, vous pouvez scinder (2) et (3) en une autre fonction - updateVisualState. Voici ce à quoi updateVisualState peut ressembler:

const updateVisualState = (clé, calculatrice) => {
  const keyType = getKeyType (clé)
  Array.from (key.parentNode.children) .forEach (k => k.classList.remove ('is-déprimé'))
  if (keyType === 'opérateur') key.classList.add ('est-déprimé')
  if (keyType === 'clear' && key.textContent! == 'AC') {
    key.textContent = 'AC'
  }
  if (keyType! == 'clear') {
    const clearButton = calculator.querySelector ('[data-action = clear]')
    clearButton.textContent = 'CE'
  }
}

Emballer

Le code devient beaucoup plus propre après le refactor. Si vous regardez dans l'écouteur d'événements, vous saurez ce que fait chaque fonction. Voici à quoi ressemble l'écouteur d'événements à la fin:

keys.addEventListener ('click', e => {
  if (e.target.matches ('button')) renvoie
  clé const = e.target
  const displayNum = display.textContent
  // fonctions pures
  const resultString = createResultString (clé, nombre affiché, calculator.dataset)
  // mise à jour des états
  display.textContent = resultString
  updateCalculatorState (clé, calculatrice, chaîne de résultats, chaîne d'affichage)
  updateVisualState (clé, calculatrice)
})

Vous pouvez récupérer le code source de la partie refactor via ce lien (faites défiler vers le bas et entrez votre adresse e-mail dans la zone, et j'enverrai les codes source directement dans votre boîte aux lettres).

J'espère que vous avez apprécié cet article. Dans ce cas, vous adorerez peut-être apprendre JavaScript, un cours dans lequel je vous montrerai comment construire pas à pas 20 composants, comme la manière dont nous avons construit cette calculatrice aujourd'hui.

Remarque: nous pouvons encore améliorer la calculatrice en ajoutant une prise en charge du clavier et des fonctionnalités d'accessibilité telles que les régions en direct. Voulez-vous savoir comment? Allez vérifier Learn JavaScript :)