1

I'm beginner in coding, still in learning process, and I stuck on some problem creating a simple Calculator app as part of my practice in JS. I have problem that when I make some prompt in the calculator screen as 3+3, than when I press equal I get the result 3+3undefined.

First I checked if I selected the variables correctly, then if I assigned add Listener correctly, then I tried to use math expression evaluation, but all without success. Everything I've tried always either throws Error or undefined.

(function() {
  let screen = document.querySelector('#display');
  let buttons = document.querySelectorAll('.btn');
  let clear = document.querySelector('#clear');
  let equal = document.querySelector('#equals');

  buttons.forEach(function(button) {
    button.addEventListener('click', function(e) {
      let value = e.target.dataset.num;
      screen.value += value;
    });
  });

  equal.addEventListener('click', function(e) {
    if (screen.value === '') {
      screen.value = "";
    } else {
      let answer = eval(screen.value); /* every check says that there is a code error in this line.*/
      screen.value = answer;
    }
  });

  clear.addEventListener('click', function(e) {
    screen.value = "";
  });
})();
<div class="calculator">
  <form>
    <input type="text" class="screen" id="display">
  </form>

  <div class="buttons">
    <button type="button" class="btn btn-yellow" id="multiply" data-num="*">*</button>
    <button type="button" class="btn btn-yellow" id="divide" data-num="/">/</button>
    <button type="button" class="btn btn-yellow" id="subtract" data-num="-">-</button>
    <button type="button" class="btn btn-yellow" id="add" data-num="+">+</button>
    <button type="button" class="btn" id="nine" data-num="9">9</button>
    <button type="button" class="btn" id="eight" data-num="8">8</button>
    <button type="button" class="btn" id="seven" data-num="7">7</button>
    <button type="button" class="btn" id="decimal" data-num=".">.</button>
    <button type="button" class="btn" id="six" data-num="6">6</button>
    <button type="button" class="btn" id="five" data-num="5">5</button>
    <button type="button" class="btn" id="four" data-num="4">4</button>
    <button type="button" class="btn btn-red" id="clear">C</button>
    <button type="button" class="btn" id="three" data-num="3">3</button>
    <button type="button" class="btn" id="two" data-num="2">2</button>
    <button type="button" class="btn" id="one" data-num="1">1</button>
    <button type="button" class="btn btn-green" id="equals">=</button>
    <button type="button" class="btn" id="zero" data-num="0">0</button>
  </div>
</div>

<script src="./script.js"></script>
mplungjan
  • 169,008
  • 28
  • 173
  • 236
VladoBagzi
  • 21
  • 3
  • “*`/* every check says that there is a code error in this line.*/`*” can you provide the full stack trace of the error in question as part of your [mre]? – esqew May 07 '23 at 15:26
  • change eval(screen.value) to eval("screen.value") eval run the script stored in string, or simply remove eval – Dickens A S May 07 '23 at 15:50
  • 1
    if you go to any javascript programming site, they say, "Do NOT use eval()" – Rick May 07 '23 at 15:54
  • @mplungjan yes there is a danger here. [Self-XSS](https://en.wikipedia.org/wiki/Self-XSS) *is* a threat. Sure they could do about the same by going through the console, but people will be less vigilant if all they do is paste some text in a website's UI they do trust. And all of a sudden all their private data is POSTed on a random server. – Kaiido May 08 '23 at 06:15
  • @OP, if you really want to only handle the few buttons that your UI is showing, then map each button's click to update internal values and don't read the `.value` at all, or at least filter that input to only the few values you do support (i.e you refuse everything but digits + all your operators). – Kaiido May 08 '23 at 06:18
  • @mplungjan I mean lure someone into pasting some obfuscated code that will load a script into OP's "trusted" page and execute arbitrary scripts with all the privileges it needs to pretend to be OP. So if e.g OP is writing a plugin for a bank website, that would mean that the attacker could track everything on the user's account. If they only do redirect to their own website they won't be able to access any data from OP's website. If they can make OP's website execute their code, OP is screwed. – Kaiido May 08 '23 at 06:51
  • @mplungjan and yet using eval with user provided input is exactly one of the cases where `eval` is truly dangerous. One has to tell this to a student. As you stated, unless you know what you're doing `eval` should be treated as evil. Here OP clearly doesn't know what they're doing with it nor the risks they take by doing so. It is our duty to teach them why it's harmful, and to show them how to do avoid this risk. Then it's their call to follow our advice or not, but we can't tell them it's not dangerous when it is. – Kaiido May 08 '23 at 08:11
  • @mplungjan even if this specific script doesn't seem harmful in itself, it truly is. Maybe in a few years OP will be in a rush, their employer will ask them to integrate a simple calculator, OP will think "Hey, I made one a few years ago, let's just reuse it and let's not review it thouroughly because I don't have time. Then it stays there in the final product and when an attacker finds out about it (because an attacker has all the time and resources in the world), OP will be responsible. – Kaiido May 08 '23 at 08:15
  • Very well. I consider this student now very well informed. – mplungjan May 08 '23 at 08:39
  • I have added a sanitizer to [my script](https://stackoverflow.com/a/76194907/295783) – mplungjan May 08 '23 at 08:44

4 Answers4

2

You need to test you get an actual value from the button's data attribute.

It makes it easier if you delegate and you can simply use the textContent of the buttons. I have removed the data attributes completely

Added a sanitiser

(function() {
  // strip anything other than digits, (), -+/* and .
  const sanitizer = str => str.replace(/[^-()\d/*+.]/g, '');
  const screen = document.getElementById("display");
  document.querySelector(".buttons").addEventListener('click', function(e) {
    const tgt = e.target; // what was clicked
    if (tgt.matches("#equals")) {
      if (screen.value !== "") { // simpler test too
        try { 
          let answer = eval(sanitizer(screen.value)); 
        }
        catch (e) {
          console.log(e.message);
          return;
        }
        screen.value = answer;
      }
    } else if (tgt.matches("#clear")) {
      screen.value = "";
    } else { 
      let value = tgt.textContent;
      if (value) screen.value += value;
    }
  });
})();
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" />

<div class="calculator">
  <form>
    <input type="text" class="screen" id="display">
  </form>

  <div class="buttons">
    <button type="button" class="btn btn-yellow" id="multiply">*</button>
    <button type="button" class="btn btn-yellow" id="divide">/</button>
    <button type="button" class="btn btn-yellow" id="subtract">-</button>
    <button type="button" class="btn btn-yellow" id="add">+</button><br/>
    <button type="button" class="btn">9</button>
    <button type="button" class="btn">8</button>
    <button type="button" class="btn">7</button>
    <button type="button" class="btn">.</button><br/>
    <button type="button" class="btn">6</button>
    <button type="button" class="btn">5</button>
    <button type="button" class="btn">4</button>
    <button type="button" class="btn btn-red" id="clear">C</button><br/>
    <button type="button" class="btn">3</button>
    <button type="button" class="btn">2</button>
    <button type="button" class="btn">1</button>
    <button type="button" class="btn btn-green" id="equals">=</button><br/>
    <button type="button" class="btn">0</button>
  </div>
</div>

<script src="./script.js"></script>
mplungjan
  • 169,008
  • 28
  • 173
  • 236
2

Your equal button is firing two events.

One for button.addEventListener and the other equal.addEventListener. When the equal button is clicked it shouldn't run the button code because data-num does not exist. This is why undefined is being raised in your eval.

A cleaner solution is to check if the data attribute data-num exists instead of adding data-num to the equal button. Helps avoid adding unnecessary HTML.

  buttons.forEach(function(button) {
    button.addEventListener('click', function(e) {
      if (e.target.dataset.hasOwnProperty('num')) {
        let value = e.target.dataset.num;
        screen.value += value;
      }
    });
  });
Arty.Simon
  • 844
  • 2
  • 7
  • 21
0

this code fix your problem

equals button dose not have data-num property trigger undefine

buttons.forEach(function(button) {
    button.addEventListener('click', function(e) {
      
      if(e.target.id !="equals"){
        let value = e.target.dataset.num;
        screen.value += value;
       } 
   
    });
});
paliz
  • 347
  • 2
  • 5
0

On this line in your HTML:

<button type="button" class="btn btn-green" id="equals">=</button>

Add data-num=""

<button type="button" class="btn btn-green" data-num="" id="equals">=</button>

Explanation: You always add the data-num value to the input field in your JS, if it is undefined you will get a value such as "9+9undefined", and that can't be handled by eval. If you add an emptry string the same value will be "9+9".

Edit (after problem solved)

As you said, you are still learning, and I think your implementation and your logic looks just fine.

So this is only a sidenote: If you want to you can let JS do more of the work of 'drawing' the calculator and also listen to events a little bit differently, by listening to clicks on the body and then filter out if any of the clicks actually hit a certain button (this is called delegated event handling). There are so many flavors of how you solve something like a simple calculator in JS and that's what's makes programming fun. Here's how I would have done it, if in a hurry :)

Also: Make sure that 0.1 + 0.2 returns 0.3, this may take some work. See: Is floating point math broken?

document.body.innerHTML = `
    <div class="calculator">
      <input type="text" placeholder="0" readonly>`
  + '789/456*123-C0.+()^='.split('')
    .map(x => `<div class="btn `
      + `${isNaN(x) && x !== '.' ? 'ctrl' : ''}">`
      + `${x}</div>`).join('') + '</div>';

document.body.addEventListener('click', e => {
  let btn = e.target.closest('.btn');
  if (!btn) { return; }
  let input = document.querySelector('input');
  let val = btn.innerText;
  val === 'C' && (input.value = '');
  val === '=' && (() => {
    try {
      input.value = eval(input.value.replaceAll('^', '**'))
        .toFixed(10).replace(/(\.[1-9]*)0{1,}$/g, '$1')
        .replace(/\.$/g, '');
    }
    catch (e) { input.value = ''; }
  })();
  !'C='.includes(val) && (input.value += val);
});
.calculator {
  border-radius: 0.5vw;
  background-color: rgb(41, 111, 164);
  padding: 10px;
  display: inline-block;
}

.calculator::after {
  content: "";
  display: table;
  clear: both;
}

.btn {
  width: 3vw;
  height: 3vw;
  background-color: black;
  margin: 1px;
  color: white;
  float: left;
  font-size: 2vw;
  font-family: Verdana;
  padding: 0.8vw 0.5vw 0.2vw;
  text-align: center;
  cursor: pointer;
}

.btn:nth-child(4n+2) {
  clear: both;
}

.btn.ctrl {
  background-color: #333;
}

input {
  font-family: Verdana;
  font-size: 2vw;
  width: 16vw;
  height: 3vw;
  margin-bottom: 1vw;
  display: block;
  text-align: right;
}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>Calculator</title>
</head>
<body>
</body>
</html>
Thomas Frank
  • 1,404
  • 4
  • 10
  • Not my downvote, but to dump this code on a beginner is a bit harsh. Especially without any comments in the code. Also I would expect a delegation from `.calculator` rather than body – mplungjan May 08 '23 at 04:36