0

I'm having trouble understanding the basic idea of Javascript event handling and variable scope. I come from Python, in which I've built a small GUI app which displays random English irregular verbs and asks the user to enter the past and participle forms. I'm trying to write the same thing in JS.

score = 0
maxQuestions = 10

function loadJSON(url, success, error) {
    const xhr = new XMLHttpRequest()
    xhr.open("GET", url, true)
    xhr.responseType = 'json'
    xhr.send()
    xhr.onload = function() {
        if (xhr.status == 200) {
            success(xhr.response)
        } else {
            error(xhr)
        }
    }
}

/*
The JSON file looks like this:
[
    [
        "leap",
        "leapt/leaped",
        "leapt/leaped"
    ],
etc...
*/

function main(verbs) {
    var verb = verbs.splice(Math.floor(Math.random() * verbs.length), 1)[0]
    var present = verb[0]
    document.getElementById('present').innerHTML = present
    document.getElementById('button').addEventListener('click', function() {
        check(verb)
    })
}

function check(verb) {
    var preterit = verb[1].split('/'),
        participle = verb[2].split('/')
    var user_preterit = document.getElementById('preterit').value
    var user_participle = document.getElementById('participle').value
    if (preterit.includes(user_preterit)) {
        score += 1
    }
    if (participle.includes(user_participle)) {
        score += 1
    }
    document.getElementById('score').innerHTML = score
}

function error() {
    console.log('Error loading JSON file.')
}

loadJSON('js/verbs-list.json', main, error)

This works as intended, but I'm not sure how build a loop correctly in order to ask e.g. 10 questions.

I want to keep a main() function in order to set up the event listener, have an introductory text and an option to start over, but I need to get the verb selection code into a different function that can run repeatedly without adding an event listener every time. How can I do that while keeping a reference to the verb variable?

This was easy for me to do in Python because the app was contained in a class. I could refer to "globals" such as score, current verb and the verb list by using self. Is it supposed to be used the same way in JS, i.e. with a class and this, or can it be done in a simpler fashion?

Cirrocumulus
  • 520
  • 3
  • 15
  • You can't use a loop. You will need to use a recursive approach with callback style for event handlers. In your `click` handler, after `check(verb)`, you'll want to select and another verb, until you hit the `maxQuestions`. You will need an extra counter for how many questions you've asked so far. Each "iteration step" will be a function, there is no `for` syntax. – Bergi Sep 05 '21 at 11:28
  • OK, but do I `addEventListener` each time? The `check` function needs to be passed a parameter when its called — do I just pile event listeners one atop the other? – Cirrocumulus Sep 05 '21 at 12:23
  • If you do create a new button for each question, you would also need to assign a new event listener each time. If you do use the same button and same event listener, you would need to make the event listener so that it knows the current question and reacts accordingly. (You can also keep the button and unregister the old listener before adding the new one, but yes, you shouldn't pile them on top of each other, which would lead to firing all of them on every click) – Bergi Sep 05 '21 at 13:44

3 Answers3

1

Referring to verb as a global variable in JS is the way you do it with score and maxQuestions variables, cause you define them out of any scope. If you define verb at the top and fill it with the response you can access it from any other function.

About asking 10 times, adding an event listener to dynamically generated elements may be a little different from static ones, you should use event delegation to handle the events without assigning event listener every time.

As suggestion, you can use a next button to reveal next question and append a cloned element of the question row to the question containers like this:

let clonedQuestion = document.querySelector('#question').cloneNode(true);

// do whatever you want with the cloned element 
// like assigning the next present verb and a unique identifier

document.querySelector('#question-container').appendChild(clonedQuestion);
Mehrnoosh
  • 111
  • 1
  • 7
1

I editted your code a little. In js there is idea of of lexical scopping where variables are outside a function body are visible inside that function body depending on where they are located physically. Also var was dropped in favor of let and const. let is useful when variable can be reassigned later on in the code. and const for constants. I also used 'fetch' here https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch instead of xhr

Let me know if this works :)

score = 0
maxQuestions = 10

async function loadJSON(url) {
    try {
        const res = await fetch(url)
        const verbs = await res.json()
        return verbs
    catch(err) {
       console.log(err)
    }
}

/*
The JSON file looks like this:
[
    [
        "leap",
        "leapt/leaped",
        "leapt/leaped"
    ],
etc...
*/

async function main() {
    const verbs = await loadJson('js/verbs-list.json')
    let verb = [];
    let present = ""
    // dom elements
    const presentElmt = document.getElementById('present')
    const btnElmt = document.getElementById('button')
    
    let counter = 0

    while(counter <= maxQuestions) {
        verb = getRandomVerb(verbs)
        present = verb[0]

        presentElmt.innerHTML = present

        btnElmt.addEventListener('click', function(){
            check(verb)
        })

        counter++ 

    }
}

function getRandomVerbs(verbs) {
    if(!verbs) return [] // return an empty array if list of verbs is empty
    return verbs.splice(Math.floor(Math.random() * verbs.length), 1)[0]
}

function check(verb) {
    const preterit = verb[1].split('/'),
        participle = verb[2].split('/')
    const user_preterit = document.getElementById('preterit').value
    const user_participle = document.getElementById('participle').value
    if (preterit.includes(user_preterit)) {
        score += 1
    }
    if (participle.includes(user_participle)) {
        score += 1
    }

    document.getElementById('score').innerHTML = score
}



main()
nmurgor
  • 402
  • 4
  • 8
  • Thank you. I need to learn more about async functions before I fully dig this but I get the general idea. I also see that global variables are not frowned upon as much as I'm used too... But isn't your code adding a new event listener on each loop iteration? It seems inelegant coming from Python & Qt. Is it a common pattern in JS? (cf. @Mehrnoosh's answer) – Cirrocumulus Sep 05 '21 at 10:06
  • Also, your code seems to cycle over the list instantly. What I'm trying to undestand is how to code a loop where each step is triggered after the button is clicked. – Cirrocumulus Sep 05 '21 at 10:25
  • It seems to loop over the list instantly. I think you could devise a way to let user answer a question then advance the to another random verb. about reassigning event listener I could'nt think of a better way to do it. I think in Js variable declaration need not to be global at all. I think declaring the variables "next to where they are immediately used" is a great idea IMO – nmurgor Sep 05 '21 at 11:00
0

I'm answering my own question in case this solution can help someone. After the comments by @Bergi about the impossibility of using a loop, I've finally settled on a simple solution that keeps track of the UI state with a global variable.

// Globals
const maxQuestions = 10
let verbs
let questionCount = 0
let currentVerb
let uiState

// Read JSON file from server
function getData(url) {
    return fetch(url).then(response => {
        return response.json()
    })
}

// Main entry point
async function main() {
    // Get verbs list
    verbs = await getData('js/verbs-list.json')
    document.getElementById('button').addEventListener('click', onClick)
    newGame()
}

// Handle button click based on UI state
function onClick() {
    switch(uiState) {
        case 'start':
            newGame()
            break;
        case 'check':
            checkVerb()
            break;
        case 'next':
            nextVerb()
            break;
        default:
            throw 'Undefined game state.'
    }
}

// Update the UI state and the button text
function setUIState(state) {
    uiState = state
    document.getElementById('button').value = uiState
}

// Initialize game data
function newGame() {
    score = 0
    // etc
    nextVerb()
}

function nextVerb() {
    // Display the next random verb
    currentVerb = verbs.splice(Math.floor(Math.random() * verbs.length), 1)[0]
    // etc
    setUIState('check')
}

function checkVerb() {
    // Do all checking and UI updating
    if (questionCount < maxQuestions) {
        setUIState('next')
    } else {
        gameOver()
    }
}

function gameOver() {
    // Display end of round information
    setUIState('start')
}

main()

There is only one eventListener on the button. The code reacts to that event based on the uiState context. The loop uses a simple variable increment instead of for or while.

Cirrocumulus
  • 520
  • 3
  • 15