Class Activity Calculator (JS review)

#1

Introduction

Class Activity Calculator is an online application to calculate the students’ class activity grades. The class activity grade (CA) is calculated based on the marks a student gets during the term. Here’s a sample grade sheet we use in class:

enter image description here

Please review the code and provide feedback.
Credit: Special thanks to the JavaScript guru, Paul Wilkins, for his valuable pointers, without which my app wouldn’t work!

Source

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

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="Calculate the class activity grades of the ILI students.">
  <title>Class Activity Calculator</title>
  <link rel="icon" href="favicon.ico">
  <link rel="stylesheet" href="style.css">
</head>

<body>
  <header>
    <img src="logo.png" alt="Logo">
    <h1>Class Activity Calculator</h1>
  </header>
  <nav>
    <a href="index.html" id="current">Adults: Old</a>
    <a href="adults-new.html">Adults: New</a>
    <a href="young-adults.html">Young Adults</a>
    <a href="kids.html">Kids</a>
  </nav>
  <main>
    <form autocomplete="off">
      <fieldset data-weight="4">
        <legend>Listening & Speaking</legend>
        <input type="number" step="any" min="0" max="100">
        <input type="number" step="any" min="0" max="100">
        <input type="number" step="any" min="0" max="100">
        <input type="number" step="any" min="0" max="100">
        <input type="number" step="any" min="0" max="100">
        <output></output>
      </fieldset>
      <fieldset data-weight="3">
        <legend>Reading</legend>
        <input type="number" step="any" min="0" max="100">
        <input type="number" step="any" min="0" max="100">
        <input type="number" step="any" min="0" max="100">
        <input type="number" step="any" min="0" max="100">
        <input type="number" step="any" min="0" max="100">
        <output></output>
      </fieldset>
      <fieldset data-weight="1">
        <legend>Writing</legend>
        <input type="number" step="any" min="0" max="100">
        <input type="number" step="any" min="0" max="100">
        <input type="number" step="any" min="0" max="100">
        <input type="number" step="any" min="0" max="100">
        <input type="number" step="any" min="0" max="100">
        <output></output>
      </fieldset>
      <div>
        <button type="button">Calculate</button>
        <output></output>
        <button type="reset">Reset</button>
      </div>
    </form>
  </main>
  <footer>
    Share on <a href="https://t.me/share/url?url=https%3A%2F%2Fclassactivitycalculator.github.io%2F&text=Class%20Activity%20Calculator%3A%20Calculate%20the%20class%20activity%20grades%20of%20the%20ILI%20students." title="Telegram: Share Web Page">Telegram</a> |
    <a href="https://www.facebook.com/dialog/share?app_id=2194746457255787&href=https%3A%2F%2Fclassactivitycalculator.github.io%2F" title="Post to Facebook">Facebook</a>
    <address><a href="https://t.me/MortezaMirmojarabian" title="Telegram: Contact @MortezaMirmojarabian" rel="author">Give feedback</a></address>
  </footer>
  <script>
    function setOutputValues() {
      var totalWeightedAverage = 0;
      var totalWeight = 0;
      var fieldsets = form.querySelectorAll('fieldset');
      for (var fieldset of fieldsets) {
        var average = averageInputValues(fieldset);
        var fieldsetOutput = fieldset.querySelector('output');
        if (average == undefined) {
          fieldsetOutput.value = 'You may only enter 0 to 100.';
        } else if (isNaN(average)) {
          fieldsetOutput.value = 'Please enter a grade.';
        } else {
          fieldsetOutput.value = 'avg: ' + average.toFixed(1);
        }
        totalWeightedAverage += average * fieldset.dataset.weight;
        totalWeight += Number(fieldset.dataset.weight);
      }
      var classActivity = totalWeightedAverage / totalWeight;
      var divOutput = form.querySelector('div output');
      if (isNaN(classActivity)) {
        divOutput.value = '';
      } else {
        divOutput.value = 'CA: ' + classActivity.toFixed(1);
      }
    }
  </script>
  <script src="global.js"></script>
</body>

</html>

global.js

var form = document.querySelector('form');

function averageInputValues(fieldset) {
  var totalValue = 0;
  var totalNumber = 0;
  var inputs = fieldset.querySelectorAll('input');
  for (var input of inputs) {
    if (!input.validity.valid) {
      return;
    }
    totalValue += Number(input.value);
    totalNumber += Boolean(input.value);
  }
  return totalValue / totalNumber;
}

form.querySelector('[type="button"]').addEventListener('click', setOutputValues);

function detectChange() {
  var inputs = form.querySelectorAll('input');
  for (var input of inputs) {
    if (input.value) {
      return true;
    }
  }
}

form.querySelector('[type="reset"]').addEventListener('click', function(event) {
  if (detectChange() && !confirm('Your changes will be lost.\nAre you sure you want to reset?')) {
    event.preventDefault();
  }
});

window.addEventListener('beforeunload', function(event) {
  if (detectChange()) {
    event.returnValue = 'Your changes may be lost.';
  }
});
1 Like
#2

Update

Simplified

form.querySelector('[type="reset"]').addEventListener('click', function(event) {

to

form.addEventListener('reset', function(event) {
1 Like
#3

Dear @Paul_Wilkins,

The setOutputValues function is different on each page: different max and weight values (no weights on two pages), and different CA ranges. I couldn’t find a short way to generalize setOutputValues() and that’s why I defined it in the inline script on each page separately and my global.js file includes only what the pages have in common. Is it a good practice?

#4

Normally it’s better to avoid having configuration information spread out among many different places.
A typical solution is to store that information in a database, or in a configuration file instead.

Instead of embedding separate setOutputValues functions as scripts on each page, I recommend at a minimum, that the script be saved as a separate .js file. That way it’s easier to compare those files at a later stage, and combine behaviour that’s common between those scripts.

1 Like
#5

You’re right! :ok_hand:
I managed to generalize setOutputValues() mainly by adding the custom data attribute weight to all the fieldsets:

var weight = fieldset.dataset.weight;
if (!weight) {
  weight = 1;
}

Final script

var form = document.querySelector('form');

function averageInputValues(fieldset) {
  var totalValue = 0;
  var totalNumber = 0;
  var inputs = fieldset.querySelectorAll('input');
  for (var input of inputs) {
    if (!input.validity.valid) {
      return;
    }
    totalValue += Number(input.value);
    totalNumber += Boolean(input.value);
  }
  return totalValue / totalNumber;
}

function setOutputValues() {
  var max = form.querySelector('input').max;
  var totalWeightedAverage = 0;
  var totalWeight = 0;
  var fieldsets = form.querySelectorAll('fieldset');
  for (var fieldset of fieldsets) {
    var average = averageInputValues(fieldset);
    var fieldsetOutput = fieldset.querySelector('output');
    if (average == undefined) {
      fieldsetOutput.value = 'You may only enter 0 to ' + max + '.';
    } else if (isNaN(average)) {
      fieldsetOutput.value = 'Please enter a grade.';
    } else {
      fieldsetOutput.value = 'avg: ' + average.toFixed(1);
    }
    var weight = fieldset.dataset.weight;
    if (!weight) {
      weight = 1;
    }
    totalWeightedAverage += average * weight;
    totalWeight += Number(weight);
  }
  var classActivity = totalWeightedAverage / totalWeight;
  var divOutput = form.querySelector('div output');
  if (isNaN(classActivity)) {
    divOutput.value = '';
  } else if (max == 5) { // Adults: New
    divOutput.value = 'CA: ' + (classActivity / (max / 100)).toFixed(1); // The class activity grade must be calculated out of 100.
  } else {
    divOutput.value = 'CA: ' + classActivity.toFixed(1);
  }
}

form.querySelector('[type="button"]').addEventListener('click', setOutputValues);

function detectChange() {
  var inputs = form.querySelectorAll('input');
  for (var input of inputs) {
    if (input.value) {
      return true;
    }
  }
}

form.addEventListener('reset', function(event) {
  if (detectChange() && !confirm('Your changes will be lost.\nAre you sure you want to reset?')) {
    event.preventDefault();
  }
});

window.addEventListener('beforeunload', function(event) {
  if (detectChange()) {
    event.returnValue = 'Your changes may be lost.';
  }
});