DigitalDoughnut
DigitalDoughnut

Reputation: 45

A function called by a click event listener is being invoked without clicking, but only on second run through

Here's a puzzle:

I am attempting to create a simple choose your own path game, which takes its content from a local JSON file. When run, it breaks each scene ("Section" in the JSON file) up into paragraphs and adds the first paragraph to the DOM with an event listener attached to its surrounding div. Each time the user clicks on the "container", another paragraph is added from that section until all paragraphs are displayed.

After that, if the user clicks again on the "container", three choices are added, each with its own event listener attached. When one of these choices is clicked, the scene (or "section") is changed to the one selected. The HTML "container" is emptied, and the process begins again by adding the first paragraph of the new scene (section).

The problem is that it works fine for the first section and choices, but after a user clicks a choice, it loads two paragraphs of the new scene rather than one. It should load one paragraph and wait for a click event, but it actually loads a second. The second paragraph is loaded via the readyToUpdate function, which should only be called by an event listener (which has not been triggered).

{  
    "Section_1": {
        "Content": "Dark corner paragraph one<>Dark corner paragraph two<>Dark corner paragraph three",
        "Choices": [
            "Go to the garden<>24",
            "Go to the terrace<>95",
            "Go to the beach<>145"
        ]

    },

    "Section_24": {
        "Content": "Garden paragraph one<>Garden paragraph two<>Garden paragraph three",
        "Choices": [
            "Go to the dark corner<>1",
            "Go to the terrace<>95",
            "Go to the beach<>145"
        ]
    },

    "Section_95": {
        "Content": "Terrace paragraph one<>Terrace paragraph two<>Terrace paragraph three",
        "Choices": [
            "Go to the dark corner<>1",
            "Go to the garden<>24",
            "Go to the beach<>145"
        ]
    },

    "Section_145": {
        "Content": "Beach paragraph one<>Beach paragraph two<>Beach paragraph three",
        "Choices": [
            "Go to the dark corner<>1",
            "Go to the garden<>24",
            "Go to the terrace<>95"
        ]
    }
}
    <div id="container"></div>
    fetch("js/contentFile.json")
    .then(response => response.json())
    .then(json => initialSetup(json));

    let narrativeText;

    let gameData = {
        section: "",
        paraCount: 0,
        choices: [],
        choiceText: [],
        choiceDest: [],
        paragraphs: "",
        paraSections: []
    };

    function updateParagraphs(){
        console.log("Enter updateParagraphs");
        let container = document.getElementById("container");

        let node = document.createElement("p");             
        let textnode = 
    document.createTextNode(gameData.paraSections[gameData.paraCount]);  
    node.appendChild(textnode);                    
    container.appendChild(node);
    container.addEventListener('click', readyToUpdate, {once: true});


        console.log(gameData.paraCount + " is less than " + 
        gameData.paraSections.length);
        console.log("Exit updateParagraphs");
        console.log(gameData);
    }

    function readyToUpdate(e) {

        console.log("Enter readyToUpdate");
        console.log(e);

        gameData.paraCount ++;
        let container = document.getElementById("container");   
        update();

        console.log("Exit readyToUpdate");
        console.log(gameData);
    }



    function choiceUpdater(e){
        console.log("Enter choiceUpdater");
        let choiceNumber = e.target.id.split("_")[1];
        gameData.section = "Section_" + gameData.choiceDest[choiceNumber];
        document.getElementById("container").innerHTML = "";
        gameData.paraCount = 0;
        update();
        console.log("Exit choiceUpdater");
        console.log(gameData);
    }

    function addChoices() {
        console.log("Enter addChoices");
        gameData.choices = narrativeText[gameData.section].Choices;
        console.log(gameData.choices);


        for (let i=0; i<gameData.choices.length; i++){
            let choice = document.createElement("h4");
            let choicesSplit = gameData.choices[i].split("<>");
            gameData.choiceText[i] = choicesSplit[0];
            gameData.choiceDest[i] = choicesSplit[1];
            let choiceTextNode = document.createTextNode(gameData.choiceText[i]);
            choice.appendChild(choiceTextNode);
            choice.setAttribute("id", "choice_" + i);
            choice.addEventListener("click", choiceUpdater, {once: true});
            document.getElementById("container").appendChild(choice);
        }
        console.log("Exit addChoices");
        console.log(gameData);
    }

function update() {
    console.log("Enter update");
    gameData.paragraphs = narrativeText[gameData.section].Content;
    gameData.paraSections = gameData.paragraphs.split("<>");

    if (gameData.paraCount < gameData.paraSections.length) {
        updateParagraphs();
    }

    else {
        addChoices();


    }
    console.log("Exit update");
    console.log(gameData);
}

function initialSetup(data) {
    console.log("Enter initial setup")
    narrativeText = data;
    gameData.section = "Section_1";
    gameData.paraCount = 0;
    update();
    console.log("Exit initial setup");
    console.log(gameData);
};

Update: When I change the updateParagraphs function as below, it works. But looking at performance in Chrome developer, the number of listeners keeps increasing. I'm using Chrome 80, so it should remove them using the "once" property, but I was having the same initial issues even when I was manually removing the listeners with removeEventListener().

    function updateParagraphs(){
        console.log("Enter updateParagraphs");
        let container = document.getElementById("container");
        let node1 = document.createElement("div");
        node1.setAttribute("id", "innerContainer");
        document.getElementById("container").appendChild(node1);
        let innerContainer = document.getElementById("innerContainer");
        let node = document.createElement("p");             
        let textnode = 
        document.createTextNode(gameData.paraSections[gameData.paraCount]);  
        node.appendChild(textnode);                    
        innerContainer.appendChild(node);
        innerContainer.addEventListener('click', readyToUpdate, {once: true});
    }

Upvotes: 1

Views: 182

Answers (2)

Aioros
Aioros

Reputation: 4383

I believe what is happening is that when you click on a choice, and the choice click listener is invoked, you are going through update() and updateParagraphs() and adding a new click listener to the container. At the same time, though, the click event is propagating to the container, and gets captured by the new container listener that you just added.

So you have two options: the first, as suggested by @wth193, is to use setTimeout() to add the container listener at the next tick.

function updateParagraphs(){
    // ...
    // container.addEventListener('click', readyToUpdate, {once: true});
    setTimeout(function () {
        container.addEventListener('click', readyToUpdate, {once: true});
    });
}

A cleaner (and clearer) solution, in my opinion, would be to use Event.stopPropagation().

function choiceUpdater(e){
  console.log("Enter choiceUpdater");
  e.stopPropagation();
  // ...
}

This way, the click event gets captured just by the choice element, and not by the container.

Upvotes: 2

wth193
wth193

Reputation: 56

function updateParagraphs(){
    ...
    // container.addEventListener('click', readyToUpdate, {once: true});
    setTimeout(function () {
        container.addEventListener('click', readyToUpdate, {once: true});
    });
}

i figure setTimeout to break the call stacks seems to work, but i can't explain why.



Upvotes: 2

Related Questions