# 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:

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">

<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>

<body>
<img src="logo.png" alt="Logo">
<h1>Class Activity Calculator</h1>
<nav>
<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">
<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> |
</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)) {
} 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);
}
}

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

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

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 `weight`s 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!
I managed to generalize `setOutputValues()` mainly by adding the custom data attribute `weight` to all the `fieldset`s:

``````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);
}
}

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)) {
} 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);
}
}

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