Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaScript: Get number of edited/updated inputs

Tags:

Scenario

Every semester my students need to take at least one science, one physics and one history test. The following form gives the right average grades as well as the final grade of a student:

document.getElementById('calcBtn').addEventListener('click', function() {    var scienceTest1 = document.getElementById('scienceTest1').value;    var scienceTest2 = document.getElementById('scienceTest2').value;    var scienceTest3 = document.getElementById('scienceTest3').value;    var physicsTest1 = document.getElementById('physicsTest1').value;    var physicsTest2 = document.getElementById('physicsTest2').value;    var physicsTest3 = document.getElementById('physicsTest3').value;    var historyTest1 = document.getElementById('historyTest1').value;    var historyTest2 = document.getElementById('historyTest2').value;    var historyTest3 = document.getElementById('historyTest3').value;    var scienceAverage = document.getElementById('scienceAverage');    var physicsAverage = document.getElementById('physicsAverage');    var historyAverage = document.getElementById('historyAverage');    var finalGrade = document.getElementById('finalGrade');    scienceAverage.value = (Number(scienceTest1) + Number(scienceTest2) + Number(scienceTest3)) / 3;    physicsAverage.value = (Number(physicsTest1) + Number(physicsTest2) + Number(physicsTest3)) / 3;    historyAverage.value = (Number(historyTest1) + Number(historyTest2) + Number(historyTest3)) / 3;    finalGrade.value = (scienceAverage.value * 5 + physicsAverage.value * 3 + historyAverage.value * 2) / 10;  });
<form>    Science: <input type="number" id="scienceTest1">    <input type="number" id="scienceTest2">    <input type="number" id="scienceTest3">    <output id="scienceAverage"></output>    <br> Physics: <input type="number" id="physicsTest1">    <input type="number" id="physicsTest2">    <input type="number" id="physicsTest3">    <output id="physicsAverage"></output>    <br> History: <input type="number" id="historyTest1">    <input type="number" id="historyTest2">    <input type="number" id="historyTest3">    <output id="historyAverage"></output>    <br>    <input type="button" value="Calculate" id="calcBtn">    <output id="finalGrade"></output>  </form>

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

scienceAverage.value = (Number(scienceTest1) + Number(scienceTest2) + Number(scienceTest3)) / 3; physicsAverage.value = (Number(physicsTest1) + Number(physicsTest2) + Number(physicsTest3)) / 3; historyAverage.value = (Number(historyTest1) + Number(historyTest2) + Number(historyTest3)) / 3; 

Question

What is a simple approach to get the number of changed input fields in the following single row? I'll try to understand your method and then develop my form to multiple rows.

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;  });
<form>    <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>
like image 486
Mori Avatar asked Mar 04 '19 08:03

Mori


2 Answers

It looks like you need to check the values of inputs are valid numbers before using them in the arithmetic that calculates the per-course averages. One way to do this would be via the following check:

if (!Number.isNaN(Number.parseFloat(input.value))) {   /* Use input.value in average calculation */ } 

You might also consider adjusting your script and HTML as shown below, which would allow you to generalize and re-use the average calculation for each of the three classes as detailed below:

document.getElementById('calcBtn').addEventListener('click', function() {      /* Generalise the calculation of updates for specified course type */    const calculateForCourse = (cls) => {        let total = 0      let count = 0        /* Select inputs with supplied cls selector and iterate each element */      for (const input of document.querySelectorAll(`input.${cls}`)) {          if (!Number.isNaN(Number.parseFloat(input.value))) {                  /* If input value is non-empty, increment total and count for          subsequent average calculation */          total += Number.parseFloat(input.value);          count += 1;        }      }        /* Cacluate average and return result */      return { count, average : count > 0 ? (total / count) : 0 }    }      /* Calculate averages using shared function for each class type */    const calcsScience = calculateForCourse('science')    const calcsPhysics = calculateForCourse('physics')    const calcsHistory = calculateForCourse('history')        /* Update course averages */    document.querySelector('output.science').value = calcsScience.average    document.querySelector('output.physics').value = calcsPhysics.average    document.querySelector('output.history').value = calcsHistory.average        /* Update course counts */    document.querySelector('span.science').innerText = `changed:${calcsScience.count}`    document.querySelector('span.physics').innerText = `changed:${calcsPhysics.count}`    document.querySelector('span.history').innerText = `changed:${calcsHistory.count}`      /* Update final grade */    var finalGrade = document.getElementById('finalGrade');      finalGrade.value = (calcsScience.average * 5 + calcsPhysics.average * 3 + calcsHistory.average * 2) / 10;  });
<!-- Add class to each of the course types to allow script to distinguish       between related input and output fields -->  <form>    Science:    <input type="number" class="science" id="scienceTest1">    <input type="number" class="science" id="scienceTest2">    <input type="number" class="science" id="scienceTest3">    <output id="scienceAverage" class="science"></output>    <span class="science"></span>    <br> Physics:    <input type="number" class="physics" id="physicsTest1">    <input type="number" class="physics" id="physicsTest2">    <input type="number" class="physics" id="physicsTest3">    <output id="physicsAverage" class="physics"></output>    <span class="physics"></span>    <br> History:    <input type="number" class="history" id="historyTest1">    <input type="number" class="history" id="historyTest2">    <input type="number" class="history" id="historyTest3">    <output id="historyAverage" class="history"></output>    <span class="history"></span>    <br>    <input type="button" value="Calculate" id="calcBtn">    <output id="finalGrade"></output>  </form>

Update

To extend on the first answer, please see the documentation in the snippet below responding to your question's update:

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');        /* This variable counts the number of inputs that have changed */    var changesDetected = 0;        /* If value of test1 field "not equals" the empty string, then     we consider this a "changed" field, so we'll increment our     counter variable accordinly */    if(test1 != '') {      changesDetected = changesDetected + 1;    }    /* Apply the same increment as above for test2 field */    if(test2 != '') {      changesDetected = changesDetected + 1;    }    /* Apply the same increment as above for test3 field */    if(test3 != '') {      changesDetected = changesDetected + 1;    }        /* Calculate average from changesDetected counter.    We need to account for the case where no changes    have been detected to prevent a "divide by zero" */    if(changesDetected != 0) {      average.value = (Number(test1) + Number(test2) + Number(test3)) / changesDetected;    }    else {      average.value = 'Cannot calculate average'    }        /* Show a dialog to box to display the number of fields changed */    alert("Detected that " + changesDetected + " inputs have been changed")  });
<form>    <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>

Update 2

The prior Update can be simplified with a loop like so:

document.getElementById('calcBtn').addEventListener('click', function() {        let changesDetected = 0;    let total = 0;    const ids = ['test1', 'test2', 'test3'];        for(const id of ids) {      const value = document.getElementById(id).value;      if(value != '') {        changesDetected += 1;        total += Number(value);      }    }        var average = document.getElementById('average');        if(changesDetected != 0) {      average.value = total / changesDetected;    }    else {      average.value = 'Cannot calculate average'    }          alert("Detected that " + changesDetected + " inputs have been changed")  });
<form>    <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>

Update 3

Another concise approach based on your JSFiddle would be the following:

document.getElementById('calculator').addEventListener('click', function() {    var physicsAverage = document.getElementById('physicsAverage'),      historyAverage = document.getElementById('historyAverage');      physicsAverage.value = calculateAverageById('physics')    historyAverage.value = calculateAverageById('history');  });    function calculateAverageById(id) {    /* Get all input descendants of element with id */    const inputs = document.querySelectorAll(`#${id} input`);      /* Get all valid grade values from selected input elements */    const grades = Array.from(inputs)      .map(input => Number.parseFloat(input.value))      .filter(value => !Number.isNaN(value));      /* Return average of all grades, or fallback message if no valid grades present */    return grades.length ? (grades.reduce((sum, grade) => (sum + grade), 0) / grades.length) : 'No assessment made!'  }
<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>

The main differences here are:

  • the use of document.querySelectorAll(#${id} input); with a template literal to extract the input elements of a element with id
  • the use of Array.from(inputs) for a more readable means of converting the result of the query to an array
  • the use of Number.parseFloat and Number.isNaN when transforming and filtering input elements to valid numeric values for the subsequent average calculation

Hope that helps!

like image 91
Dacre Denny Avatar answered Oct 16 '22 16:10

Dacre Denny


A good start is to change your ID to Class to put your inputs into logical groups. The next step is to get the inputs from a particular group that has a value that is not null. We can do this by selecting for example .scienceTest and then filtering out empty string items.

I added a helper function values to extract the values from a nodelist and put them into a normal Array.

We can use a Boolean to test the empty strings. We also cast all strings to numbers using Number. This is done in the onlyNumbers function.

Next, we need to calculate the averages of each group. This is easy since we have a filtered list of numbers. All we do is calculate the sum and divide by the Array length. This is done with our little avrg function.

     document.getElementById('calcBtn').addEventListener('click', function() {    var scienceTest = getGrades('.scienceTest')    var physicsTest = getGrades('.physicsTest')    var historyTest = getGrades('.historyTest')        var scienceAverage = document.getElementById('scienceAverage');    var physicsAverage = document.getElementById('physicsAverage');    var historyAverage = document.getElementById('historyAverage');        var finalGrade = document.getElementById('finalGrade');        scienceAverage.value = avrg(scienceTest)    physicsAverage.value = avrg(physicsTest)    historyAverage.value = avrg(historyTest)        finalGrade.value = (scienceAverage.value * 5 + physicsAverage.value * 3 + historyAverage.value * 2) / 10;      });    function avrg(list) {  	return list.length ? list.reduce((acc, i) => acc + i, 0) / list.length : 0  }    function getGrades(selector) {  	return onlyNumbers(values(document.querySelectorAll(selector)))  }  function onlyNumbers(list) {  		return list.filter(Boolean).map(Number)  }    function values(nodelist) {  		return Array.prototype.map.call(nodelist, (node) => node.value)  }
<form>    Science: <input type="number" class="scienceTest">    <input type="number" class="scienceTest">    <input type="number" class="scienceTest">    <output id="scienceAverage"></output>    <br> Physics: <input type="number" class="physicsTest">    <input type="number" class="physicsTest">    <input type="number" class="physicsTest">    <output id="physicsAverage"></output>    <br> History: <input type="number" class="historyTest">    <input type="number" class="historyTest">    <input type="number" class="historyTest">    <output id="historyAverage"></output>    <br>    <input type="button" value="Calculate" id="calcBtn">    <output id="finalGrade"></output>  </form>

Update: Simplified example

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');    // Put all field values in array, Filter empty values out, cast values to Number    var rowValues = [test1, test2, test3].filter(Boolean).map(Number)      console.log('Number of changed fields', rowValues.length)      // calculate average by reducing the array to the sum of its remaining values then divide by array length    average.value = rowValues.reduce((sum, grade) => sum + grade, 0) / rowValues.length;  });
<form>    <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>

Update Extra: Based on OP's jsfiddle example in the comments

document.getElementById('calculator').addEventListener('click', function() {    var physicsAverage = document.getElementById('physicsAverage'),      historyAverage = document.getElementById('historyAverage');      physicsAverage.value = calculateAverageById('physics')    historyAverage.value = calculateAverageById('history');  });    function calculateAverageById(id) {  	// Get all inputs under Id    var inputs = document.getElementById(id).getElementsByTagName('input')      var values =      Array.prototype.slice.call(inputs) // From HTMLCollection to Array      .map(e => e.value.trim()) // Return all .value from input elements      .filter(Boolean) // Filter out any empty strings ""      .map(Number) // convert remaining values to Numbers    return (values.length) ? // if length is greater then 0      values.reduce((sum, grade) => sum + grade, 0) / values.length // Return average      :      'No assessment made!' // else return this message  }
    <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>
like image 37
Jordan Maduro Avatar answered Oct 16 '22 15:10

Jordan Maduro