Class Activity Calculator (JS review)

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

Update

Simplified

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

to

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

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?

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

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.';
  }
});

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.