Why doesn't any event work in dynamically loaded code? - javascript

I load an HTML page inside a modal
When I put the code out of the modal, the event works ( click, input, and others )
NOTE: I don't use and I don't want to use JQuery ( never )
Demo: https://codepen.io/jonathan_silva/pen/vYKqrvE?editors=0010
I've been trying to make it work for 3 days. It looks like everything is fine but...
Help me
const codeHTML = () => {
const code = `
<div id="page">
<div class="steps">
<article class="step1">
<div class="options-grid">
<div class="select-box">
<div class="options business-options">
<div class="business-option">
<input type="radio" class="radio" />
<label>Business A</label>
</div>
<div class="business-option">
<input type="radio" class="radio" />
<label>Business B</label>
</div>
<div class="business-option">
<input type="radio" class="radio" />
<label>Business C</label>
</div>
</div>
<div class="select-business">Select Business</div>
</div>
</div>
</article>
</div>
</div>
`;
return code;
}
/* MODAL */
const modal = async ({ target }) => {
const html = codeHTML(); // Load HTML page
let section = document.getElementById('modal-page');
if (!section) {
return;
}
document.getElementById('modal-content').insertAdjacentHTML('afterbegin', html);
section.classList.add('modal');
const onSectionClick = ({ target }) => {
if (!target.classList.contains('modal-close')) {
return;
}
section.classList.remove('modal');
section.removeEventListener('click', onSectionClick);
document.querySelector('#page').remove();
}
section.addEventListener('click', onSectionClick);
}
const openModal = document.querySelector('.announce a');
openModal.addEventListener('click', event => modal(event));
/* SELECT */
const selectBusiness = document.querySelector('.select-business');
const businessContainer = document.querySelector('.business-options');
const optionsBusiness = document.querySelectorAll('.business-option');
if (selectBusiness !== null) {
selectBusiness.addEventListener('click', e => {
console.log(e); // Nothing happens
businessContainer.classList.toggle("active");
});
}

The elements you're trying to querySelector aren't there when you call querySelector, since you only add them to the DOM within the modal() function. (You can do e.g. console.log(selectBusiness) to see that with your own eyes.)
You'll need to move that event binding in there, after the insertAdjacentHTML call.

Related

After adding querySelector inside function page crash

With this function, every div element that has children needs to wrap the children in a wrap div. Everything works fine if
<button id="run" onclick="wrapChildren(document.querySelector('#element1'))">Run</button>
but at the moment when I insert in the function:
var element = document.querySelector('#element1');
the page is crashing "devtools was disconnected from the page". Why is this happening and how to fix?
function wrapChildren() {
var element = document.querySelector('#element1');
const childElements = Array.from(element.children);
if (childElements.length === 0) {
return;
}
const wrapDiv = document.createElement('div');
wrapDiv.id = element.id+"wrap";
childElements.forEach((childElement) => {
wrapDiv.appendChild(childElement);
});
element.appendChild(wrapDiv);
childElements.forEach((childElement) => {
wrapChildren(childElement);
});
}
<div id="element1">
<div id="child1">
<div id="grandchild1"></div>
<div id="grandchild2">
<div id="granddrandchild1"></div>
<div id="granddrandchild2"></div>
<div id="granddrandchild3">
<div id="granddrandgrandchild1"></div>
</div>
</div>
</div>
<div id="child2"></div>
</div>
<button id="run" onclick="wrapChildren()">Run</button>
The way you have written it, your wrapChildren function doesn't actually take any arguments:
function wrapChildren()
so it was always running exactly the same code, even when you attempt to call it recursively with a different argument (in the forEach at the end of your function). As a result, your code leads to infinite recursion and hence a page crash.
To fix this, just give it an element as the argument, and use this element in the function body rather than hardcoding element to be the one with id element1.
I have made this change below and there is no crash any more. The function doesn't actually appear to do anything very much, but I'll leave that to you to sort out, or perhaps ask a new question about. (I don't actually know what this is trying to do.)
function wrapChildren(element) {
const childElements = Array.from(element.children);
if (childElements.length === 0) {
return;
}
const wrapDiv = document.createElement('div');
wrapDiv.id = element.id+"wrap";
childElements.forEach((childElement) => {
wrapDiv.appendChild(childElement);
});
element.appendChild(wrapDiv);
childElements.forEach((childElement) => {
wrapChildren(childElement);
});
}
<div id="element1">
<div id="child1">
<div id="grandchild1"></div>
<div id="grandchild2">
<div id="granddrandchild1"></div>
<div id="granddrandchild2"></div>
<div id="granddrandchild3">
<div id="granddrandgrandchild1"></div>
</div>
</div>
</div>
<div id="child2"></div>
</div>
<button id="run" onclick="wrapChildren(document.querySelector('#element1'))">Run</button>

why this eventListner callback function is running twice

const addons = document.querySelectorAll('.addon');
const toggleAddon = (e, addon) => {
console.log(addon);
console.log(addon.querySelector('input').checked);
if (addon.querySelector('input').checked)
addon.classList.remove('selected-plan');
else addon.classList.add('selected-plan');
};
addons.forEach((addon) => {
addon.addEventListener('click', (e) => toggleAddon(e, addon));
});
<label>
<div class="addon addon-2 selected-addon">
<input type="checkbox" name="addon-2" class="addon-chkbox" id="larger-storage">
<div class="addon-info">
<h3 class="addon-name">Larger Storage</h3>
<p class="addon-features">Extra 1TB of cloud save</p>
</div>
<div class="addon-pricing-box">
<h3 class="addon-pricing">$2/mo</h3>
</div>
</div>
</label>
Why when I click on this element, the function toggleAddon() runs twice and in first run console.log(addon.querySelector('input').checked) comes false and on second it comes true.
Thanks for the help.
This is another simplified example that will better show what I'm pointing out:
let counter = 0;
document.querySelector('.addon')
.addEventListener('click', event =>{
console.log(`[${++counter}]`);
console.log(event.target);
});
<label>
<div class="addon">
<input type="checkbox">
<div class="addon-info">
<h3 class="addon-name">Click here</h3>
</div>
</div>
</label>
The problem is with the <label> element, when it is clicked, it trigger another click event on the input element within.
I suggest using change event instead.
check it in codesandbox.
The whole label is for the input? so why not listen to the input click event only which will automatically be handled by the label? like:
let counter = 0;
document.querySelector('.addon-input')
.addEventListener('click', event =>{
console.log(`[${++counter}]`);
console.log(event.target);
});
<label>
<div class="addon">
<input class="addon-input" type="checkbox">
<div class="addon-info">
<h3 class="addon-name">Click here</h3>
</div>
</div>
</label>

Prevent buttons with same class from being clicked simultaneously

I have two buttons that have the same class name and have same functionality but different inputs must be added, so I used document.querySelectorAll() and the forEach() method to get them, but now when I click one, the other gets clicked too. Is there a way I can prevent this without having two addEventListener for both buttons? Enable it to click only one button at a time.
My code:
let inputElements = document.querySelectorAll('.inputElement');
const submitBtn = document.querySelectorAll('.submitBtn');
const backersElement = document.querySelector('.number-of-backers');
let donationsMade = [];
function calculateBamboo() {
inputElements.forEach(inputElement => {
const inputValue = parseFloat(inputElement.value);
if (inputValue < 25 || inputValue === '') return alert('Pledge must be at least $25.');
donationsMade.push(inputValue);
const donationsTotal = donationsMade.reduce((a, b) => a += b);
pledgedAmount.textContent = `$${donationsTotal}`;
backersElement.textContent = donationsMade.length;
return donationsTotal;
})
}
submitBtn.forEach(button => {
button.addEventListener('click', calculateBamboo);
})
It's not actually "clicking" both buttons. What's happening is the following:
Your calculateBamboo() functions loops through all the inputElements: inputElements.forEach(); and you're executing your logic for all the inputs. So, no matter which button you press, calculateBamboo() is processing each input.
Passing target input using data-attributes will help you to identify which input belongs to clicked button
Also, #JerryBen is right, you don't need to add event listener to each button but instead, we can wrap all the buttons in one element, add event listener to it and identify which element was clicked.
const wrapper = document.getElementById('wrapper');
const pledgedAmount = document.querySelector('.backed-users');
const backersElement = document.querySelector('.number-of-backers');
wrapper.addEventListener('click', calculateBamboo);
let donationsMade = [];
function calculateBamboo(event) {
/* Here you can access to the event argument,
which contains target: the clicked element*/
const el = event.target;
if (el.nodeName !== 'BUTTON' || !el.classList.contains('submitBtn')) {
return;
}
// Get target input from button's data-attr
const targetInput = el.dataset.input;
const inputElement = document.querySelector(`input[data-input="${targetInput}"]`);
// Continue with the code you had...
const inputValue = parseFloat(inputElement.value) || 0;
if (inputValue < 25 || inputValue === '') return alert('Pledge must be at least $25.');
donationsMade.push(inputValue);
const donationsTotal = donationsMade.reduce((a, b) => a += b);
pledgedAmount.textContent = `$${donationsTotal}`;
backersElement.textContent = donationsMade.length;
return donationsTotal;
}
<div class="backed">
<h1 class="backed-users">0</h1>
</div>
<div class="backers">
<h1 class="number-of-backers">0</h1>
</div>
<hr>
<div id="wrapper">
<div class=".pledge-edition">
<div class="pledge">
<section class="pledgeTwo"></section>
<div>
<h2>Bamboo Stand</h2>
Pledge $25 or more
<div>
<h2>101</h2>
<div>left</div>
</div>
</div>
</div>
<p>
You get an ergonomic stand made of natural bamboo. You've helped us launch our promotional campaign, and you’ll be added to a special Backer member list.
</p>
<div class="pledge-amount">
<p>Enter your pledge</p>
<div>
<input class="inputElement bambooInputElement" data-input="1" placeholder="$25" min="25" type="number">
<button class="submitBtn bambooBtn" data-input="1">Continue</button>
</div>
</div>
</div>
<div class=".pledge-edition">
<div class="pledge">
<section class="pledgeThree"></section>
<div>
<h2>Black Edition Stand</h2>
Pledge $75 or more
<div>
<h2>64</h2>
<div>left</div>
</div>
</div>
</div>
<p>
You get a Black Special Edition computer stand and a personal thank you. You’ll be added to our Backer member list. Shipping is included.
</p>
<div class="pledge-amount">
<p>Enter your pledge</p>
<div>
<input class="inputElement bambooInputElement" data-input="2" placeholder="$75" min="75" type="number">
<button class="submitBtn blackEditionBtn" data-input="2" placeholder="$75" min="75">Continue</button>
</div>
</div>
</div>
</div>
Adding event listeners to each button is considered a bad practice. Instead, use a single event listener to rule them all:
Wrap the buttons in a div element
Add click event only to the div wrapper
Pass an event object as an argument to the handler function
The event handler function will use the event.target to identify which specific button was clicked:
function calculateBamboo(evt){ const inputValue = parseFloat(evt.target.value) }
I hope this helps 🙄
My current JS
function calculateBamboo(target) {
let inputElement = document.querySelector(`input[data-input="${target}"]`);
donationsMade.push(inputElement);
const donationsTotal = donationsMade.reduce((a, b) => a += b);
backersElement.textContent = donationsMade.length;
pledgedAmount.textContent = `$${donationsTotal}`;
successElement.style.display = 'block';
return donationsTotal;
}
submitBtn.forEach(button => {
const target = button.dataset.input;
button.addEventListener('click', calculateBamboo.bind(target));
})
HTML buttons and input
<!-- this is how my buttons are placed -->
<div id="wrapper">
<div class=".pledge-edition">
<div class="pledge">
<section class="pledgeTwo"></section>
<div>
<h2>Bamboo Stand</h2>
Pledge $25 or more
<div>
<h2>101</h2>
<div>left</div>
</div>
</div>
</div>
<p>
You get an ergonomic stand made of natural bamboo. You've helped us launch our promotional campaign, and
you’ll be added to a special Backer member list.
</p>
<div class="pledge-amount">
<p>Enter your pledge</p>
<div>
<input class="inputElement bambooInputElement" data-input="1" placeholder="$25" min="25" type="number">
<button class="submitBtn bambooBtn" data-input="1">Continue</button>
</div>
</div>
</div>
<div class=".pledge-edition">
<div class="pledge">
<section class="pledgeThree"></section>
<div>
<h2>Black Edition Stand</h2>
Pledge $75 or more
<div>
<h2>64</h2>
<div>left</div>
</div>
</div>
</div>
<p>
You get a Black Special Edition computer stand and a personal thank you. You’ll be added to our Backer
member list. Shipping is included.
</p>
<div class="pledge-amount">
<p>Enter your pledge</p>
<div>
<input class="inputElement bambooInputElement" data-input="2" placeholder="$75" min="75" type="number">
<button class="submitBtn blackEditionBtn" data-input="2" placeholder="$75" min="75">Continue</button>
</div>
</div>
</div>
HTML for when the values are entered
<div class="backed">
<h1 class="backed-users">0</h1>
</div>
<div class="backers">
<h1 class="number-of-backers">0</h1>
</div>

Loop over API data and display multiple search items in the DOM with JavaScript

I'm trying to set up a search API app, using the https://restcountries.eu/ API and axios.
I've managed to display everything in the DOM, as I want. However, if I search for "United", or something with only part of the name. It either displays undefined or if I loop though it'll only display the last search item. I'd like to bring all the search items into the DOM, under any search criteria I input.
Below is the HTML:
<body>
<div class="search-container">
<form id="form" class="form">
<input type="text" id="search" placeholder="Search for country..." />
</form>
</div>
<main id="main">
<div id="map" class="google-map"></div>
</main>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script src="script.js"></script>
</body>
</html>
And here's the JS:
const APIURL = 'https://restcountries.eu/rest/v2/name/'
// const GOOGLE_MAPS_API = 'https://maps.googleapis.com/maps/api/js?key=AIzaSyDhlU1KMTlTh4C__bTJBxbVA-s7wvQbO9E&callback=initMap'
const main = document.getElementById('main')
const form = document.getElementById('form')
const search = document.getElementById('search')
async function getCountryData(name) {
try {
const { data } = await axios.get(APIURL + name)
data.forEach(res => {
let countryArr = res
createCountryCard(countryArr)
} )
} catch (err) {
if(err.response.status == 404) {
createErrorCard('No countries found')
setTimeout(() => {
main.innerHTML = ''}
, 1500);
}
}
}
function createCountryCard(country) {
const cardHTML = `
<div class="content-container">
<div class="card">
<div class="wrapper">
<div class="card-title">
<h2>${country.name}</h2>
<h4>Capital: ${country.capital}</h4>
<h5>Population: ${country.population.toLocaleString('en')}</h5>
</div>
<div class="card-image">
<img
src="${country.flag}"
alt="${country.name +'-flag'}"
/>
</div>
</div>
<div class="wrapper">
<div class="map-content">
<div id="map" class="google-map"></div>
</div>
<div class="card-content">
<ul class="card-list">
<li><strong>Region:</strong> ${country.region}</li>
<li><strong>Subregion:</strong> ${country.subregion}</li>
<li><strong>Currency:</strong> ${country.currencies[0].name}<span> ${country.currencies[0].symbol}</span></li>
<li><strong>Spoken Language:</strong> ${country.languages[0].name}</li>
<li><strong>Timezone:</strong> ${country.timezones}</li>
</ul>
</div>
</div>
</div>
</div>
`
main.innerHTML = cardHTML
console.log(country)
}
function createErrorCard(msg) {
const cardHTML = `
<div class="card">
<h1>${msg}</h1>
</div>
`
main.innerHTML = cardHTML
}
// Search Input
form.addEventListener('submit', (e) => {
e.preventDefault()
const countryName = search.value
if(countryName) {
getCountryData(countryName)
search.value = ''
}
})
Any help would be much appreciated.
Thanks
main.innerHTML = cardHTML line in createCountryCard() function overwrites the content of the #main element with each loop iteration. That's why you are only getting the last item from your search results. Try changing it to main.innerHTML += cardHTML.

Is it possible with vanilla js to dynamically add a button element next to a text node?

For a todo list, I'm trying to dynamically add a button as a child of a list element and a sibling of a text node as below.
<ul>
<li>Hello world <button>X</button></li>
</ul>
The unordered list is in the HTML file but the list, text and button need to be inserted dynamically from the JS file. Is this possible without wrapping the text in a p tag?
Here's the code:
const todoInputEl = document.querySelector(".todo__input");
const todoListEl = document.querySelector(".todo__list");
const todoItemEls = document.querySelector(".todo__item");
const deleteItemEl = document.querySelector(".done");
function addListItem() {
todoInputEl.addEventListener("keypress", function(e) {
if (e.keyCode === 13) {
let newListItem = createListItem(todoInputEl.value);
todoListEl.insertBefore(newListItem, todoListEl.childNodes[0]);
todoInputEl.value = "";
}
})
}
function createListItem(text) {
const deleteButton = document.createElement("button");
const newListElement = document.createElement("li");
const newParaElement = document.createElement("p");
deleteButton.setAttribute("type", "button");
deleteButton.classList.add("delete");
deleteButton.innerHTML = "×";
newListElement.appendChild(newParaElement);
newListElement.setAttribute("class", "todo__item");
newParaElement.setAttribute("class", "todo__p");
newParaElement.textContent = text;
newParaElement.parentNode.insertBefore(deleteButton, deleteButton.nextElementSibling);
return newListElement;
}
addListItem();
<main>
<div class="container">
<div class="todo">
<div class="todo__header">
<h1 class="todo__title">to dos</h1>
<label for="todo input">
<input type="text" class="todo__input" placeholder="enter a thing to do">
</label>
</div>
<div class="todo__body">
<ul class="todo__list">
</ul>
</div>
</div>
</div>
</main>
As you see, it works if the text is inside a p tag, but I can't get it to work insert to the same place if it's just a plain text node. I looked extensively for examples of this being done, but haven't found any yet.
DOM manipulation can be expensive (jQuery DOM manipulations - performance comparation?). Using innerHTML sidesteps this and in my opinion makes things simpler.
const todoInputEl = document.querySelector(".todo__input");
const todoListEl = document.querySelector(".todo__list");
const todoItemEls = document.querySelector(".todo__item");
const deleteItemEl = document.querySelector(".done");
function addListItem() {
todoInputEl.addEventListener("keypress", function(e) {
if (e.keyCode === 13) {
todoListEl.innerHTML += createListItem(todoInputEl.value);
}
})
}
function createListItem(text) {
let listTemplate = "<li>{{ToDo}} <button type='button' class='delete'>×</button></li>";
return listTemplate.replace("{{ToDo}}", text);
}
addListItem();
<main>
<div class="container">
<div class="todo">
<div class="todo__header">
<h1 class="todo__title">to dos</h1>
<label for="todo input">
<input type="text" class="todo__input" placeholder="enter a thing to do">
</label>
</div>
<div class="todo__body">
<ul class="todo__list">
</ul>
</div>
</div>
</div>
</main>

Categories

Resources