Get the number of edited inputs


#1

Scenario

Every semester my students need to take at least one test. The following form gives the right average grade of a student:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Average Grade</title>
  </head>
  <body>
   <form>
Math: <input type="number" id="test1">
    <input type="number" id="test2">
    <input type="number" id="test3">
    <output id="average"></output>
    <br>
    <input type="button" value="Calculate" id="calcBtn">
   </form>
   <script>
    document.getElementById('calcBtn').addEventListener('click', function() {
     var test1 = document.getElementById('test1').value;
     var test2 = document.getElementById('test2').value;
     var test3 = document.getElementById('test3').value;
     var average = document.getElementById('average');
     average.value = (Number(test1)+Number(test2)+Number(test3)) / 3;
    });
   </script>
  </body>
</html>

DEMO

The problem is it works right only if all the fields are edited. If the student doesn’t take some tests, the average grade won’t show the right value. I know it’s because of dividing by the fixed number 3 when it calculates the average grade:

average.value = (Number(test1)+Number(test2)+Number(test3)) / 3;

Question

What is a simple approach to get the number of changed input fields?


#2

Hi,

If you don’t mind using ES6, this is what I’d do:

const calculate = document.getElementById('calcBtn');
const average = document.getElementById('average');

calculate.addEventListener('click', () => {
  const [...inputs] = document.querySelectorAll('input[type="number"]');
  const values = inputs
    .map(input => Number(input.value))
    .filter(Number);
  const total = values.reduce( (sum, current) => sum + current, 0 );
  average.value = total / values.length;
});

Basically, here we:

  • grab all of the input elements of type “number”
  • map their values to an array
  • filter out any zeros or non-numerical values
  • sum the array
  • divide the sum by the length of the array (i.e. the number of inputs that contained a valid value)

Here’s a complete demo:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Average Grade</title>
  </head>
  <body>
   <form>
    Math: <input type="number" id="test1">
    <input type="number" id="test2">
    <input type="number" id="test3">
    <output id="average"></output>
    <br>
    <input type="button" value="Calculate" id="calcBtn">
   </form>
   <script>
    const calculate = document.getElementById('calcBtn');
    const average = document.getElementById('average');

    calculate.addEventListener('click', () => {
      const [...inputs] = document.querySelectorAll('input[type="number"]');
      const values = inputs
        .map(input => Number(input.value))
        .filter(Number);
      const total = values.reduce( (sum, current) => sum + current, 0 );
      average.value = total / values.length;
    });
   </script>
  </body>
</html>

HTH


#3

Thanks for the answer, but your demo doesn’t work.


#4

Can you define “doesn’t work”. Which browser are you using?


#5

Nothing happens when I click the Calculate button.


#6

Ah, sorry, semi-colon in the wrong place. It should work now.


#7

ES6 is all Greek to me! :cry:


#8

I hear you, but it has been a ratified standard for a long time now.

If you do very much with JavaScript, I’d recommend learning it. This is a good resource:

Is there anything in my example that doesn’t make sense?


#9

Is there anything in my example that doesn’t make sense?

I need to study it. I’m more familiar with the older version, but all in all I’m not good at JavaScript.
By the way, I wonder how you develop the form so it includes two rows of inputs for two different school subjects.


#10

Here’s the same functionality using ES5:

var calculate = document.getElementById('calcBtn');
var average = document.getElementById('average');

calculate.addEventListener('click', function() {
  var inputs = document.querySelectorAll('input[type="number"]');

  var values = Array.prototype.slice.call(inputs)
    .map(function(input){ return Number(input.value) })
    .filter(Number);
  var total = values.reduce( function(sum, current) { return sum + current }, 0);
  average.value = total / values.length;
});

Does that help any?


#11

Extract the functionality from within the event listener into its own function, then call that on a per row basis.


#12

The code that you’re starting with has several improvements that can be made.
Here’s a link to the code that you’re starting with: https://jsfiddle.net/pmw57/a3ky2nvu/

To divide by the right number, we need to know the number of fields that have values in them.

Get the count

We can start of rather simply by adding values to a total variable, and increase a count variable. That lets us then figure out total / count.

  var total = 0;
  var count = 0;
  var test1 = document.getElementById('test1').value;
  if (test1) {
    total += Number(test1);
    count += 1;
  }
  var test2 = document.getElementById('test2').value;
  if (test2) {
    total += Number(test2);
    count += 1;
  }
  var test3 = document.getElementById('test3').value;
  if (test3) {
    total += Number(test3);
    count += 1;
  }
  var average = document.getElementById('average');
  average.value = total / count;

The above code is found at https://jsfiddle.net/pmw57/a3ky2nvu/1/

There is room for improvement here though.

Use a loop

You can use a loop to help reduce the amount of duplication.

  for (var i = 1; i <= 3; i += 1) {
    var test = document.getElementById('test' + i).value;
    if (test) {
      total += Number(test);
      count += 1;
    }
  }
  // var test1 = document.getElementById('test1').value;
  // if (test1) {
  //   total += Number(test1);
  //   count += 1;
  // }
  // var test2 = document.getElementById('test2').value;
  // if (test2) {
  //   total += Number(test2);
  //   count += 1;
  // }
  // var test3 = document.getElementById('test3').value;
  // if (test3) {
  //   total += Number(test3);
  //   count += 1;
  // }

The above code is found at https://jsfiddle.net/pmw57/a3ky2nvu/2/

That is a simple ES3 way of doing things.

It can also be done by using an array, which helps to open up other benefits.

Use an array

By putting the values into an array we can easily get the number of values, and use a sum function to add them together.

var function add(values) {
  var total = 0;
  for (var i = 0; i < values.length; i += 1) {
    total += Number(values[i]);
  }
  return total;
}
...
  var values = [];
  // var totals = 0;
  // var count = 0;
  for (var i = 1; i <= 3; i += 1) {
    var test = document.getElementById('test' + i).value;
    if (test) {
      values.push(test);
      // total += Number(test);
      // count += 1;
    }
  }
  var average = document.getElementById('average');
  var total = add(values);
  average.value = total / values.length;
  // average.value = total / count;

The above code is found at https://jsfiddle.net/pmw57/a3ky2nvu/3/

That’s well and good for ES3, but ES5 brings array methods that can be to our benefit.

Use forEach instead of loops

For loops have several problems, so the forEach loop is used instead to help reduce the number of issues.

  values.forEach(function (value) {
    total += Number(value);
  });
  // for (var i = 0; i < values.length; i += 1) {
  //   total += Number(values[i]);
  // }
...
  var inputs = document.querySelectorAll("input[type=number]");
  inputs.forEach(function (input) {
    if (input.value) {
      values.push(input.value);
    }
  });
  // for (var i = 1; i <= 3; i += 1) {
  //   var test = document.getElementById('test' + i).value;
  //   if (test) {
  //     values.push(test);
  //   }
  // }

Because of the forEach loops, the code now doesn’t need to be updated when the number of inputs are change.

We can also remove the unique identifiers from the form inputs, as they are no longer needed.

  Math: <input type="number">
  <input type="number">
  <input type="number">
<!--  Math: <input type="number" id="test1">
  <input type="number" id="test2">
  <input type="number" id="test3">-->

The above code is found at https://jsfiddle.net/pmw57/a3ky2nvu/4/

Use functions to simplify the code

Other benefits from using ES5 techniques is that they help to make the code more expressive. To get the total we want to get the input values and filter them for meaningful numbers. That can be expressed purely as code, with:
var values = inputs.map(value).map(toNumber).filter(isNumber)

That combined with a function called mean, that averages the values, helps to make the code easier to understand.

  function value(input) {
    return input.value;
  }
  function toNumber(str) {
    return Number(str);
  }
  function isNumber(val) {
    return Boolean(val);
  }
  var inputs = document.querySelectorAll("input[type=number]");
  var values = Array.from(inputs).map(value).map(toNumber).filter(isNumber);
  // var values = [];
  // var inputs = document.querySelectorAll("input[type=number]");
  // inputs.forEach(function (input) {
  //   if (input.value) {
  //     values.push(input.value);
  //   }
  // });

We can also use a proper function called mean, to calculate the mean average of the values.

function mean(values) {
  var sum = 0;
  values.forEach(function(value) {
    sum += value;
  });
  return values.length ? sum / values.length : 0;
}
// function add(values) {
//   var total = 0;
//   values.forEach(function (value) {
//     total += Number(value);
//   });
//   return total;
// }
...
  average.value = mean(values);
  // var total = add(values);
  // average.value = total / values.length;

The updated code is found at https://jsfiddle.net/pmw57/a3ky2nvu/6/

Reduce code in event functions, and separate long lines

The functions in the event function don’t need to be in there. Moving them out of the function makes it easier to understand what the function is supposed to be doing.

function value(input) {
  return input.value;
}
function toNumber(str) {
  return Number(str);
}
function isNumber(val) {
  return Boolean(val);
}
document.getElementById('calcBtn').addEventListener('click', function() {
  // function value(input) {
  //   return input.value;
  // }
  // function toNumber(str) {
  //   return Number(str);
  // }
  // function isNumber(val) {
  //   return Boolean(val);
  // }
  ...
});

And, instead of spreading out code across a long line, it can be clearer when placed on separate lines instead.

  var values = Array.from(inputs)
      .map(value)
      .map(toNumber)
      .filter(isNumber)
  );
  // var values = Array.from(inputs).map(value).map(toNumber).filter(isNumber);

The above code improvements are found at https://jsfiddle.net/pmw57/a3ky2nvu/7/

Use a getValues function too

Moving the code to get values outside of the event function, helps to make the code in there easier to understand too.

function getValues(inputs) {
  return Array.from(inputs)
    .map(value)
    .map(toNumber)
    .filter(isNumber);
}
...
  var values = getValues(inputs);
  // var values = Array.from(inputs)
  //   .map(value)
  //   .map(toNumber)
  //   .filter(isNumber);

The event function now gets the inputs and the average fields, gets the values from the inputs, and figures out the average value.

document.getElementById('calcBtn').addEventListener('click', function() {
  var inputs = document.querySelectorAll("input[type=number]");
  var average = document.getElementById('average');
  var values = getValues(inputs);
  average.value = mean(values);
});

That’s nice and simple now.

The above code update is found at https://jsfiddle.net/pmw57/a3ky2nvu/7/

Use ES6 arrow notation

The arrow notation helps to reduce the visual size of simple functions.

const value = input => input.value;
const toNumber = str => Number(str);
const isNumber = val => Boolean(val);
// function value(input) {
//   return input.value;
// }
// function toNumber(str) {
//   return Number(str);
// }
// function isNumber(val) {
//   return Boolean(val);
// }

And to help inform people that we are using ES6 code, I should use const and let instead of var in other parts of the code too.

function mean(values) {
  let sum = 0;
  // var sum = 0;
  ...
}
...
document.getElementById('calcBtn').addEventListener('click', function() {
  const inputs = document.querySelectorAll("input[type=number]");
  const average = document.getElementById('average');
  const values = getValues(inputs);
  // var inputs = document.querySelectorAll("input[type=number]");
  // var average = document.getElementById('average');
  // var values = getValues(inputs);
  average.value = mean(values);
});

The above updated code is found at https://jsfiddle.net/pmw57/a3ky2nvu/8/

Improving code by removing let

The let keyword is used instead of const, when values need to change.
It’s not good for values to change as errors can easily occur, so removing such occurrences results in better code.

Instead of the mean function using a sum variable, we can use the ES5 reduce method to add things together instead.

  const sum = values.reduce(function(total, value) {
    return total + value;
  });
  // let sum = 0;
  // values.forEach(function(value) {
  //   sum += value;
  // });

We can also use arrow notation to simplify the above code too:

  const sum = values.reduce(
    (total, value) => total + value
  );
  // const sum = values.reduce(function(total, value) {
    // return total + value;
  // });

The above improvements are found in the code at https://jsfiddle.net/pmw57/a3ky2nvu/9/

Conclusion

The final scripting that we end up with is:

function mean(values) {
  const sum = values.reduce(
    (total, value) => total + value
  );
  return values.length ? sum / values.length : 0;
}

const value = input => input.value;
const toNumber = str => Number(str);
const isNumber = val => Boolean(val);

function getValues(inputs) {
  return Array.from(inputs)
    .map(value)
    .map(toNumber)
    .filter(isNumber);
}
document.getElementById('calcBtn').addEventListener('click', function() {
  const inputs = document.querySelectorAll("input[type=number]");
  const average = document.getElementById('average');
  const values = getValues(inputs);
  average.value = mean(values);
});

Summary

The above sets of code helps to demonstrate how code is modified and adjusted from simple beginnings, to result in code that is more capable of handling a wider range of situations, while also retaining an easy to understand nature about it.


#13

Dear Paul,
You’re quite a mentor! Many thanks for the detailed explanation! :heart:
Among the different approaches, I finally decided to choose a loop. This is what I managed to create for two school subjects. I wonder if you see any problems:

<!DOCTYPE HTML>
<html lang="en">
<head>

<title>Average Grade</title>

</head>
<body>

 <form>
  <p id="physics">
  Physics:
  <input type="number">
  <input type="number">
  <input type="number">
  <output id="physicsAverage"></output>
  </p>
  <p id="history">
  History:
  <input type="number">
  <input type="number">
  <input type="number">
  <output id="historyAverage"></output>
  </p>
  <button type="button" id="calculator">Calculate</button>
 </form>

<script>
document.getElementById('calculator').addEventListener('click', function() {
    var physicsTests = document.getElementById('physics').getElementsByTagName('input'),
        historyTests = document.getElementById('history').getElementsByTagName('input'),
        physicsTestsCount = 0,
        historyTestsCount = 0,
        physicsAverage = document.getElementById('physicsAverage'),
        historyAverage = document.getElementById('historyAverage'),
        i;
    for (i = 0; i < physicsTests.length; i++) {
        if (physicsTests[i].value) {
            physicsTestsCount++;
        }
        if (!physicsTestsCount) {
            physicsAverage.value = 'No assessment made!';
        } else {
            physicsAverage.value = (Number(physicsTests[0].value) + Number(physicsTests[1].value) + Number(physicsTests[2].value)) / physicsTestsCount;
        }
    }
    for (i = 0; i < historyTests.length; i++) {
        if (historyTests[i].value) {
            historyTestsCount++;
        }
        if (!historyTestsCount) {
            historyAverage.value = 'No assessment made!';
        } else {
            historyAverage.value = (Number(historyTests[0].value) + Number(historyTests[1].value) + Number(historyTests[2].value)) / historyTestsCount;
        }
    }
});
</script>

</body>
</html>

DEMO


#14

Just realized I had the average calculations inside the for loop. That means I calculated the average 3 times, 2 times too many. Here’s the corrected version:

<!DOCTYPE HTML>
<html lang="en">
<head>

<title>Average Grade</title>

</head>
<body>

 <form>
  <p id="physics">
  Physics:
  <input type="number">
  <input type="number">
  <input type="number">
  <output id="physicsAverage"></output>
  </p>
  <p id="history">
  History:
  <input type="number">
  <input type="number">
  <input type="number">
  <output id="historyAverage"></output>
  </p>
  <input type="button" value="Calculate" id="calculator">
 </form>

<script>
document.getElementById('calculator').addEventListener('click', function() {
    var physicsTests = document.getElementById('physics').getElementsByTagName('input'),
        historyTests = document.getElementById('history').getElementsByTagName('input'),
        physicsTestsCount = 0,
        historyTestsCount = 0,
        physicsAverage = document.getElementById('physicsAverage'),
        historyAverage = document.getElementById('historyAverage'),
        i;
    for (i = 0; i < physicsTests.length; i++) {
        if (physicsTests[i].value) {
            physicsTestsCount++;
        }
    }
        if (physicsTestsCount) {
            physicsAverage.value = (Number(physicsTests[0].value) + Number(physicsTests[1].value) + Number(physicsTests[2].value)) / physicsTestsCount;
        } else {
            physicsAverage.value = 'No assessment made!';
        }
    for (i = 0; i < historyTests.length; i++) {
        if (historyTests[i].value) {
            historyTestsCount++;
        }
    }
        if (historyTestsCount) {
            historyAverage.value = (Number(historyTests[0].value) + Number(historyTests[1].value) + Number(historyTests[2].value)) / historyTestsCount;
        } else {
            historyAverage.value = 'No assessment made!';
        }
});
</script>

</body>
</html>

#15

Yes, there are many minor ones. You can use JSLint to easily be informed about them.

I also recommend moving no-assesment statements before the for loop.

Edit:

Whoops, I was remembering older code where it was inside the loop.


#16

Is that what you mean: https://jsfiddle.net/Mori/p2m0tvfy/8/
If so, may I know the reason?


#17

Even better is to set the default before the calculation, like this:

    physicsAverage.value = 'No assessment made!';
    if (physicsTestsCount) {
        physicsAverage.value = (Number(physicsTests[0].value) + Number(physicsTests[1].value) + Number(physicsTests[2].value)) / physicsTestsCount;
    }

The main benefit of coding that way is that it results in less mental overhead for the programmer looking at the code. When you don’t have to keep so many if/else possibilities in mind, that frees up brain processing cycles to consider other things instead.


#18

Makes sense! I tried to modify the code as you said, checked everything many times, but it doesn’t work: https://jsfiddle.net/Mori/p2m0tvfy/9


#19

You shouldn’t have the average values before the loops.

You can either group them by task, with the for loops together and average values after that.
Or you can group them by category with the physics loop and average values, followed by the history loop and average values.

I prefer to do the latter https://jsfiddle.net/hx61jcv8/, as that makes it easier to then put similar code into a function to reduce duplication.


#20

As demonstrated by the code shown at https://jsfiddle.net/hx61jcv8/3/

function calculateAverage(tests) {
  var total = 0;
  var count = 0;
  var i;
  for (i = 0; i < tests.length; i++) {
    if (tests[i].value) {
      total += Number(tests[i].value);
      count++;
    }
  }
  if (!count) {
    return 'No assessment made!';
  }
  return total / count;
}

document.getElementById('calculator').addEventListener('click', function() {
  var physicsTests = document.getElementById('physics').getElementsByTagName('input');
  var physicsAverage = document.getElementById('physicsAverage');
  physicsAverage.value = calculateAverage(physicsTests);

  var historyTests = document.getElementById('history').getElementsByTagName('input');
  var historyAverage = document.getElementById('historyAverage');
  historyAverage.value = calculateAverage(historyTests);
});