Building a simple Number Bonds website

I recently built a simple web site to help my 6 year old master Number Bonds...

Number bonds are pairs of numbers that combine to make a given total. They are fundamental to developing a solid understanding of addition and subtraction in mathematics. By practising number bonds, children can enhance their mental arithmetic skills, making more complex calculations easier to handle.

Background

I have a six-year-old (at the time of writing) who shows a decent level of arithmetic skill for their age when in a relaxed environment. Yet, for some reason, their weekly number bonds score is considerably lower than their ability suggests. Upon speaking with their teacher, it seems that for whatever reason(s) they don't perform well in a timed test (10 questions in 1 minute), likely due to being easily distracted. I decided that the best way to tackle this was to give my child more opportunities to practise mental arithmetic in a timed environment. As an aspiring coder, I pulled bits of code from various sources, chopping, changing, and asking for help when I got stuck, to produce a little website that gives my child the ability to test more often. It's perfect for use in the car, or while waiting to go into football coaching, etc.

Solution

This blog post dissects and discusses the JavaScript code used to create the number bonds practice webpage, an interactive tool designed to help users practise number bonds by answering maths questions within a specified time limit.

I will not go into the HTML and CSS components as there isn't that much to them, well, with the exception of selecting a random colour scheme so that each page load looks vibrant but a little different from the last time, although as it is random, it may in fact look the same as the last time the page loaded, but I digress. To see the full code, including HTML and CSS files, checkout my GitHub repository

Event Listeners

We begin by setting up event listeners for various buttons on the page:

document.getElementById('start-btn').addEventListener('click', startTest);
document.getElementById('next-btn').addEventListener('click', nextQuestion);
document.getElementById('restart-btn').addEventListener('click', restartTest);

These listeners call specific functions when the corresponding buttons are clicked.

Global Variables

Next, we define several global variables to keep track of the state of the application:

let timer;
let questions;
let currentQuestion = 0;
let timeRemaining;
let userAnswers = [];
let totalTime;
let invalidAttempts = 0;
let numberOfQuestions = 0;

Starting the Test

The startTest function is called when the user clicks the "Start" button. It initialises the test parameters based on the user input:

function startTest() {
    const time = parseInt(document.getElementById('time').value);
    numberOfQuestions = parseInt(document.getElementById('questions').value);
    const level = parseInt(document.getElementById('level').value);

    if (isNaN(time) || isNaN(numberOfQuestions) || isNaN(level)) {
        alert('Please fill out all fields with valid numbers.');
        return;
    }

    timeRemaining = time;
    totalTime = time;
    questions = generateQuestions(numberOfQuestions, level);
    currentQuestion = 0;
    userAnswers = [];
    invalidAttempts = 0;

    document.querySelector('.input-section').style.display = 'none';
    document.querySelector('.test-section').style.display = 'block';
    document.getElementById('result-section').style.display = 'none';

    updateTimer();
    showQuestion();
    startTimer();
}

This function validates the user input, initialises the test variables, and hides/shows relevant sections of the webpage.

Generating Questions

The generateQuestions function creates a list of random maths questions based on the specified level:

function generateQuestions(number, level) {
    let questions = [];
    for (let i = 0; i < number; i++) {
        const isAddition = Math.random() < 0.5;
        const x = Math.floor(Math.random() * level);
        const y = Math.floor(Math.random() * (level - x));

        if (isAddition) {
            const format = Math.floor(Math.random() * 3);
            if (format === 0) {
                questions.push({ question: `${x} + ${y} = __`, answer: x + y });
            } else if (format === 1) {
                questions.push({ question: `${x} + __ = ${x + y}`, answer: y });
            } else {
                questions.push({ question: `__ + ${x} = ${x + y}`, answer: y });
            }
        } else {
            const format = Math.floor(Math.random() * 3);
            if (format === 0) {
                questions.push({ question: `${x + y} - ${x} = __`, answer: y });
            } else if (format === 1) {
                questions.push({ question: `${x + y} - __ = ${x}`, answer: y });
            } else {
                questions.push({ question: `__ - ${x} = ${y}`, answer: x + y });
            }
        }
    }
    return questions;
}

We want to randomly decide whether to create an addition or subtraction question and formats the question in one of three ways.

Timer Functionality

The startTimer function manages the countdown timer:

function startTimer() {
    timer = setInterval(() => {
        timeRemaining--;
        updateTimer();
        if (timeRemaining <= 0) {
            clearInterval(timer);
            endTest();
        }
    }, 1000);
}

It updates the remaining time every second and calls endTest when the time runs out.

Displaying Questions

The showQuestion function displays the current question to the user:

function showQuestion() {
    if (currentQuestion < questions.length) {
        document.getElementById('question').textContent = questions[currentQuestion].question;
        document.getElementById('answer').value = '';
        document.getElementById('answer').focus();
    } else {
        endTest();
    }
}

If there are more questions to be answered, it displays the button for next question. Otherwise, it calls endTest.

Handling User Answers

The nextQuestion function processes the user's answer:

function nextQuestion() {
    const answer = document.getElementById('answer').value;

    if (isNaN(answer) || answer.trim() === '') {
        invalidAttempts++;
        if (invalidAttempts > 2) {
            alert('You have entered an invalid answer too many times. Scoring 0 for this question.');
            userAnswers.push({ 
                question: questions[currentQuestion].question, 
                correctAnswer: questions[currentQuestion].answer, 
                userAnswer: '0' 
            });
            invalidAttempts = 0;
            currentQuestion++;
            showQuestion();
        } else {
            alert('Please enter a valid number.');
            document.getElementById('answer').value = '';
            document.getElementById('answer').focus();
        }
    } else {
        userAnswers.push({ 
            question: questions[currentQuestion].question, 
            correctAnswer: questions[currentQuestion].answer, 
            userAnswer: parseInt(answer) 
        });
        invalidAttempts = 0;
        currentQuestion++;
        showQuestion();
    }
}

This function validates the user's input, handles invalid attempts, and updates the list of user answers.

Ending the Test

The endTest function stops the timer and shows the results:

function endTest() {
    clearInterval(timer);
    document.querySelector('.test-section').style.display = 'none';
    displayResults();
    document.getElementById('result-section').style.display = 'block';
}

Displaying Results

The displayResults function presents the user's performance:

function displayResults() {
    const resultBody = document.getElementById('result-body');
    resultBody.innerHTML = '';

    let correctCount = 0;
    userAnswers.forEach(answer => {
        const tr = document.createElement('tr');
        const questionTd = document.createElement('td');
        const answerTd = document.createElement('td');
        const resultTd = document.createElement('td');
        
        questionTd.textContent = answer.question;
        answerTd.textContent = answer.userAnswer;
        if (answer.userAnswer === answer.correctAnswer) {
            resultTd.innerHTML = '✔️';
            correctCount++;
        } else {
            resultTd.innerHTML = '❌';
        }

        tr.appendChild(questionTd);
        tr.appendChild(answerTd);
        tr.appendChild(resultTd);
        resultBody.appendChild(tr);
    });

    const scoreElement = document.getElementById('score');
    const totalQuestions = numberOfQuestions;
    const percentage = isNaN(correctCount / totalQuestions) ? 0 : (correctCount / totalQuestions) * 100;
    let message = percentage >= 90 ? '👍' : 'Keep trying!';
    scoreElement.textContent = `Score: ${correctCount} out of ${totalQuestions} (${percentage.toFixed(2)}%) ${message}`;
}

It creates a table with the questions, user's answers, and correctness of each answer. It also calculates the user's score and displays a message based on their performance.

Restarting the Test

The restartTest function resets the interface to allow the user to start a new test but by default, with the same parameters as before, however these can be changed before starting the test, this just avoids having to re-enter the same inputs when doing several rapid tests:

function restartTest() {
    document.querySelector('.input-section').style.display = 'block';
    document.querySelector('.test-section').style.display = 'none';
    document.getElementById('result-section').style.display = 'none';
}

This function simply shows the input section and hides the test and results sections, allowing the user to start a new test.


This project is available in my GitHub repository. It is copyleft and open to contributions by anyone who wants to help make it better.

To see the project in action, click here.

Feel free to use this code as a reference for creating similar educational applications.