简体   繁体   中英

How to add a class to dynamically created element

I want to add listener to dynamically created element in JavaScript but It doesn't seem to work. It throws no error so I have no idea on what i have to begin with. Do you have any ideas?

 { const API_KEY = "9829fe5eab6c963bbe4850df2d7de4aa"; const container = document.getElementById("container"); const flippers = document.getElementsByClassName("header__flipper"); const cityTemplate = () => { const template = `<section class="weather"> <button class="header__flipper"><span aria-hidden="true">&rarr;</span></button> <header class="header"> <h1 class="header__heading">dfgdfgd </h1> </header> </section>`; return template; }; const addListeners = (collection, ev, fn) => { for (let i = 0; i < collection.length; i++) { collection[i].addEventListener(ev, fn, false); } } const req = (id, key) => { const url = `https://api.openweathermap.org/data/2.5/forecast?id=${id}&APPID=${key}`; fetch(url).then((res) => { res.json().then((data) => { container.innerHTML += cityTemplate(data); }); }) } req("6695624", API_KEY); req("6695624", API_KEY); req("6695624", API_KEY); addListeners(flippers, "click", () => { alert("test"); }) } 
 <div id="container"></div> 

The core issue is due to the req() function being asynchronous - meaning the req() function is called but it finishes at some unknown point in the future. While each req() is waiting to finish the script continues and the addListeners() function is called, using the .header__flipper selector - but due to the asynchronous behavior the .header__flipper elements aren't created yet so the event listeners aren't added.

As a demo, I've added a timeout to the addListeners() function so it waits 1 second before being called. This gives the req() functions time to complete and allows the event listeners to attach correctly.

However - setTimeout() IS NOT the solution - the snippet below is only for demonstrating the issue, scroll down for the correct solution.

 { const API_KEY = "9829fe5eab6c963bbe4850df2d7de4aa"; const container = document.getElementById("container"); const cityTemplate = () => { const template = `<section class="weather"> <button class="header__flipper"><span aria-hidden="true">&rarr;</span></button> <header class="header"> <h1 class="header__heading">dfgdfgd </h1> </header> </section>`; return template; }; const addListeners = (collection, ev, fn) => { for (let i = 0; i < Array.from(collection).length; i++) { collection[i].addEventListener(ev, fn, false); } } const req = (id, key) => { const url = `https://api.openweathermap.org/data/2.5/forecast?id=${id}&APPID=${key}`; fetch(url).then((res) => { res.json().then((data) => { container.innerHTML += cityTemplate(data); }); }) } req("6695624", API_KEY); req("6695624", API_KEY); req("6695624", API_KEY); // For Demo Only // The req() function is asynchronous so the addListeners() function was attempting to attach to the elements before they were created window.setTimeout(function() { addListeners(document.getElementsByClassName("header__flipper"), "click", () => { alert("test"); }) }, 1000) } 
 <div id="container"></div> 

Solution

The solution is to attach the event listeners to a parent selector (as @Nishad has recommended). The idea is to attach the click event listener to a parent element (like #container ), and within the listener callback function check if the event target is one of the new dynamic elements.

In your case, the addition of the <span class="header__flipper__aria" aria-hidden="true">&rarr;</span> within the button complicates things a bit because the event target could be either the <button> or the <span> . This requires us to check if the event target is either of those elements.

 { const API_KEY = "9829fe5eab6c963bbe4850df2d7de4aa"; const container = document.getElementById("container"); const cityTemplate = () => { const template = `<section class="weather"> <button class="header__flipper"><span class="header__flipper__aria" aria-hidden="true">&rarr;</span></button> <header class="header"> <h1 class="header__heading">dfgdfgd </h1> </header> </section>`; return template; }; const addListeners = (collection, ev, fn) => { collection.addEventListener(ev, fn, false); } const req = (id, key) => { const url = `https://api.openweathermap.org/data/2.5/forecast?id=${id}&APPID=${key}`; fetch(url).then((res) => { res.json().then((data) => { container.innerHTML += cityTemplate(data); }); }) } req("6695624", API_KEY); req("6695624", API_KEY); req("6695624", API_KEY); addListeners(document.getElementById("container"), "click", (event) => { var classes = event.target.classList; if (classes.contains("header__flipper") || classes.contains("header__flipper__aria")) { alert("test"); } }) } 
 <div id="container"></div> 

Alternative Solution

An alternative would be to attach the event listener to the button within each dynamic element in the callback when the dynamic element is created, like this:

 { const API_KEY = "9829fe5eab6c963bbe4850df2d7de4aa"; const container = document.getElementById("container"); const cityTemplate = () => { const newEl = document.createElement("section"); newEl.classList.add("weather"); const template = `<button class="header__flipper"><span class="header__flipper__aria" aria-hidden="true">&rarr;</span></button> <header class="header"> <h1 class="header__heading">dfgdfgd </h1> </header>`; newEl.innerHTML = template; return newEl; }; const req = (id, key) => { const url = `https://api.openweathermap.org/data/2.5/forecast?id=${id}&APPID=${key}`; fetch(url).then((res) => { res.json().then((data) => { const city = cityTemplate(data); city.querySelector("button").addEventListener("click", function(){ alert("test"); }, false); container.appendChild(city); }); }) } req("6695624", API_KEY); req("6695624", API_KEY); req("6695624", API_KEY); } 
 <div id="container"></div> 

The problem is that you add the event listeners before the fetch requests have completed. The triggers aren't yet in the DOM at the time you call addListeners .

I've modified the req method to return the promise for the fetch . With a Promise.all the code will wait till all three fetches are done. This still doesn't fully solve the problem, the code knows when the fetches are done but that is not the same as the req method having added the cityTemplate to the DOM.

Two solutions come to mind:

  1. Use a setTimeout in the Promise.all handler. This will most likely delay adding the event listeners long enough for the templates to be added in the DOM. I've added some console.log statements that will show the Promise.all log line appears before the last rendered log line.
  2. Have the req method return a promise that you create yourself, instead of the fetch promise. Resolve the self created promise after the cityTemplate has been added to the DOM. This way you know for sure that the Promise.all won't get fulfilled until everything is in the DOM.

Solution 1 is not a very robust solution and should be avoided. Solution 2 provides the kind of control you want. My answer show a basic setup for solution 2, it doesn't do any kind of error handling.

 { const API_KEY = "9829fe5eab6c963bbe4850df2d7de4aa"; const container = document.getElementById("container"); const flippers = document.getElementsByClassName("header__flipper"); const cityTemplate = () => { const template = `<section class="weather"> <button class="header__flipper"><span aria-hidden="true">&rarr;</span></button> <header class="header"> <h1 class="header__heading">dfgdfgd </h1> </header> </section>`; return template; }; const addListeners = (collection, ev, fn) => { for (let i = 0; i < collection.length; i++) { collection[i].addEventListener(ev, fn, false); } } const req = (id, key) => { console.log(`getting ${id}`); // Return a new promise, this promise will be fulfilled once the template // has been added with the retrieved data. return new Promise(resolve => { const url = `https://api.openweathermap.org/data/2.5/forecast?id=${id}&APPID=${key}`; // Get the data fetch(url).then((res) => { res.json().then((data) => { // Add the template to the DOM container.innerHTML += cityTemplate(data); console.log(`rendered ${id}`); // Relove that promise that was returned by the method. resolve(); }); }) }); } // Wait for all three promises to be done. These promises will be fulfilled after // the DOM has been updated. Promise.all([req("6695624", API_KEY), req("6695624", API_KEY), req("6695624", API_KEY)]) .then(() => { console.log(`promise all done`); // There is no longer a need for a timeout, due to the change to the // req method. addListeners(flippers, "click", () => { alert("test"); }) }); } 
 <div id="container"></div> 

you need to use the below code snippet to trigger click event for dynamically created elements

$(document).on("click", ".className", function(){
    alert('this works')
});

for JavaScript solution, you can refer this : adding onclick event to dynamically added button?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM