Scope issues inside an Event Listener? - javascript

The following code basically shows/hides paragraph tags, I'm having to re-declare the paras variable. Is this because I'm dynamically injecting the button into the DOM, or is it to do with scope? How could I better construct this markup?
// vars
var revealContainer = document.querySelector('.reveal-more');
var paras = revealContainer.querySelectorAll('p');
var status = true;
// return
if (paras && paras.length <= 3) return;
// generate show more link
revealContainer.innerHTML += '<button class="button--text reveal-more__btn">Read more</button>';
var revealBtn = revealContainer.querySelector('.reveal-more__btn');
// click event
revealBtn.addEventListener('click', function () {
var paras = revealContainer.querySelectorAll('p');
// toggle show/hide class
for (var i = 0; i < paras.length; i++) {
var p = paras[i];
p.classList.toggle('is-shown');
}
// check status
if (status) {
this.textContent = 'Read less';
status = false;
} else {
this.textContent = 'Read more';
status = true;
}
});

You can use the live HTMLCollection returned by .getElementsByTagName() instead of the static NodeList returned by .querySelectorAll()
The getElementsByTagName method of Document interface returns an HTMLCollection of elements with the given tag name. The complete document is searched, including the root node. The returned HTMLCollection is live, meaning that it updates itself automatically to stay in sync with the DOM tree without having to call document.getElementsByTagName() again.
var paragraphs = document.getElementById("container").getElementsByTagName("p");
console.log(paragraphs.length);
setInterval(function() {
document.getElementById("container").insertAdjacentHTML("beforeend", "<p>p</p>");
}, 1000);
setInterval(function() {
console.log(paragraphs.length);
}, 2000);
<div id="container"></div>

Below is a really simple Snippet that demonstrates delegated events in pure Javascript, instead of using jQuery.
Here you can see I've attached the eventListener to the div with id elements, this will then listen for click events under this, a simple matches is used just in case you have other elements your not interested in..
document.querySelector("#elements").addEventListener("click", (e) => {
if (!e.target.matches('.element')) return
console.log(`Clicked ${e.target.innerText}`);
});
.element {
border: 1px solid black;
margin: 5px;
}
<div id="elements">
<div class="element">1</div>
<div class="element">2</div>
<div class="element">3</div>
<div>Clicking this does nothing.</div>
</div>

Related

Only the last element I added using innerHTML keeps its event handlers, why?

I am trying to make a script that injects interactable object information in a list of the markup page. Whenever I try to add an onclick event on a div, it works fine, however whenever I try to add more within a for loop, it does not work the way I intended.
I took a look of what is going on using breakpoints in the webpage debugger, and I see that the problem is that it seems to delete the event on the previous div before adding to the next div. In the end, the only event remaining is the last div after the loop exits.
I want to keep these events on all my divs, not just the last one... what seems to be the problem here?
var objects = ['Tom', 'Sauna', 'Traum'];
for (var i = 0; i < objects.length; i++){
document.getElementById('list').innerHTML += "<div class='item' id='"+ i +"'>" + objects[i] + "</div>";
document.getElementById(i).addEventListener("mouseup", function() {
Select(this);
});
}
function Select(char) {
console.log(char);
}
div.item {
border: 1px solid black;
padding: 4px;
margin: 4px;
}
<div id="list"></div>
When you change innerHTML browser reconstructs the element's contents, throwing away all event handlers attached. Use DOM methods instead:
for (let i = 0; i < objects.length; i++){
var block = document.createElement('div');
block.setAttribute('id', i);
document.getElementById('list').appendChild( block );
block.addEventListener("mouseup", function() {
Select(this);
});
}
UPD: alternatively use a insertAdjacentHTML method instead of redefining innerHTML:
document.getElementById('list').insertAdjacentHTML(
'beforeend', "<div id='"+ i +"'>" + i + "</div>");
The reason is the way you are appending. innerHtml += effectively overwrites the existing content in the list. So, any elements that you added and bound are simply gone, and new items are added each time.
There are a couple ways to make this work.
First instead of assigning an innerHtml you can append elements.
const items = ['taco', 'apple', 'pork'];
const list = document.getElementById("list");
for (const item of items) {
const el = document.createElement("div");
el.addEventListener('click', (e) => console.log(`clicked ${item}`));
el.innerText = item;
list.appendChild(el);
}
<div id="list"></div>
Since we are appending an explicit element and not overwriting content, this will work.
A better approach would be to use delegation. We assign a single event handler onto the list and listen for any clicks. We then figure out what specific element was clicked.
const items = ['taco', 'apple', 'pork'];
const list = document.getElementById("list");
const add = document.getElementById("add");
list.addEventListener('click', (e) => {
const parent = e.target.closest("[data-item]");
if (parent != null) {
console.log(`clicked on ${parent.dataset['item']}`);
}
});
for (const item of items) {
list.innerHTML += `<div data-item="${item}">${item}</div>`;
}
add.addEventListener('click', () => {
const item = `item ${Date.now()}`;
list.innerHTML += `<div data-item="${item}">${item}</div>`;
})
<div id="list"></div>
<button id="add">add</button>
The magic here is we assign a single event handler on the parent, and use closest to figure out what item was clicked. I'm using innerHTML here for simplicity but it should be avoided for security reasons.
A good pattern to use when appropriate is event delegation. It allows following the Don't Repeat Yourself principle, making code maintenance considerably easier and potentially making scripts run significantly faster. And in your case, it avoids the pitfalls of an element being responsible for modifying its own content.
For example:
const container = document.getElementById('container');
container.addEventListener("click", toggleColor); // Events bubble up to ancestors
function toggleColor(event) { // Listeners automatically can access triggering events
const clickedThing = event.target; // Event object has useful properties
if(clickedThing.classList.contains("click-me")){ // Ensures this click interests us
clickedThing.classList.toggle("blue");
}
}
.click-me{ margin: 1em 1.5em; padding 1em 1.5em; }
.blue{ color: blue; }
<div id="container">
<div id="firstDiv" class="click-me">First Div</div>
<div id="firstDiv" class="click-me">Second Div</div>
</div>

Create multiple elements and delete a single one JavaScript

I'm working on a JavaScript project where a user can click a button to create a text element. However, I also want a feature where I can click a different button and the element that was created most recently will be removed, so In other words, I want to be able to click a button to create an element and click a different button to undo that action.
The problem I was having was that I created the element, then I would remove the element using:
element.parentNode.removeChild(element); , but it would clear all of the elements that were created under the same variable.
var elem = document.createElement("div");
elem.innerText = "Text";
document.body.appendChild(elem);
This code allows an element to be created with a button click. All elemente that would be created are under the "elem" variable. so when I remove the element "elem", all element are cleared.
Is there a simple way to remove on element at a time that were all created procedurally?
Thanks for any help
When you create the elements, give the a class. When you want to remove an element, just get the last element by the className and remove it.
The below snippet demonstrates it -
for(let i = 0; i<5; i++){
var elem = document.createElement("div");
elem.innerText = "Text " + i;
elem.className = "added";
document.body.appendChild(elem);
}
setTimeout(function(){
var allDivs = document.getElementsByClassName("added");
var lastDiv = allDivs.length-1;
document.body.removeChild(allDivs[lastDiv]);
}, 3000);
I would probably use querySelectors to grab the last element:
// optional
// this is not needed it's just a random string added as
// content so we can see that the last one is removed
function uid() {
return Math.random().toString(36).slice(2);
}
document.querySelector('#add')
.addEventListener('click', (e) => {
const elem = document.createElement('div');
elem.textContent = `Text #${uid()}`;
document.querySelector('#container').appendChild(elem);
// optional - if there are elements to remove,
// enable the undo button
document.querySelector('#undo').removeAttribute('disabled');
});
document.querySelector('#undo')
.addEventListener('click', (e) => {
// grab the last child and remove
document.querySelector('#container > div:last-child').remove();
// optional - if there are no more divs we disable the undo button
if (document.querySelectorAll('#container > div').length === 0) {
document.querySelector('#undo').setAttribute('disabled', '');
}
});
<button id="add">Add</button>
<button id="undo" disabled>Undo</button>
<div id="container"></div>

addEventListener on class

I need to use getElementsByClassName because I have several same buttons etc.
I work on a wordpress loop that displays a button for each new article, and a registration form must appear on each event when we click on the button.
When I click on the button, I want the form to be displayed and the button to be hidden.
Someone can help me ?
Sorry if there are mistakes, I am French.
var bouton = document.getElementsByClassName('btn_inscription');
var formulaire = document.getElementsByClassName('formulaire');
var MyFonction = function{
formulaire.style.display = 'block';
bouton.style.display ='none';
}
for (var i = 0; i < bouton.length; i++) {
bouton[i].addEventListener('click', MyFonction);
}
getElementsByClassName (along with .getElementsByTagName and .getElementsByName) return node list objects (array-like containers). You can't interact with individual element properties and methods on node lists, you have to work with objects within the container. To set up event handlers on all the elements in the node list, you can loop through the container and set the handler one at a time.
Now, getElementsByClassName returns a "live node list", which is one that re-scans the document every time you interact with it to ensure that your container has the most up to date set of matching elements. This can cause big drops in page performance and the need for live node lists is pretty uncommon. Instead, use the more modern .querySelectorAll(), which allows you to pass any valid CSS selector in and returns a static node list.
// Get all the desired elements into a node list
let elements = document.querySelectorAll(".test");
// Convert the node list into an Array so we can
// safely use Array methods with it
let elementsArray = Array.prototype.slice.call(elements);
// Loop over the array of elements
elementsArray.forEach(function(elem){
// Assign an event handler
elem.addEventListener("click", function(){
console.log("You clicked me!");
this.style.backgroundColor = "#ff0";
});
});
<div class="test">Something</div>
<div>Something</div>
<div class="test">Something</div>
<div>Something</div>
<div class="test">Something</div>
You need to use .bind(thisArg[, arg1[, arg2[, ...]]]) in order to pass the current index and element (rif. this):
var bouton = document.getElementsByClassName('btn_inscription');
var formulaire = document.getElementsByClassName('formulaire');
var MyFonction = function(idx, event) {
formulaire[idx].style.display = 'block';
this.style.display ='none';
}
for (var i = 0; i < bouton.length; i++) {
bouton[i].addEventListener('click', MyFonction.bind(bouton[i], i));
// ^^^^^^^^^^^^^^^^^^^
}
.formulaire {
display: none;
}
<button class="btn_inscription">1</button>
<button class="btn_inscription">2</button>
<button class="btn_inscription">3</button>
<div class="formulaire">1</div>
<div class="formulaire">2</div>
<div class="formulaire">3</div>
You need to make use of this inside your event handler:
var buttons = document.getElementsByClassName('btn_inscription');
var MyFunction = function() {
this.closest('.formulaire').style.display = 'block';
this.style.display ='none';
}
for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', MyFunction.bind(buttons[i]));
}

Create my own Radio-like-button with a DIV?

I'm trying to make a site where users can create there own social networking buttons. (I know its been done but its mostly for practice). A part of the site will allow users to choose the shape of the buttons. Here is the HTML:
<div class="design" id="shape">
<div class="shapeSelect square" id="square"></div>
<div class="shapeSelect rounded" id="rounded"></div>
<div class="shapeSelect circle" id="circle"></div>
</div>
What I would like to do is add an event listener when the div is clicked. After it's clicked the class attribute would be changed to "selected." When another one would be click then the first clicked one would be cleared and the next one would be selected. Just like with radio buttons.
I am familiar with JavaScript and my idea was this:
window.onload = function () {
'use strict';
document.getElementById("square").addEventListener('click', function (e) {//adds the event listener
divArray = document.getElementById("shape");//Here is my first issue: an array is not returned
if (!(document.getElementById("square").getAttribute("class") == "shapeSelect square selected")) {// checks to make sure its not already selected
for (i = 0, count = document.getElementById("shape").length; i < count; i++) {// if it isn't go through the array
divArray[i]// and this is where i also get stuck. I Can't figure out how i would return the class attribute to be class="shapeSelect circle" instead of class="shapeSelect circle selected"
};
}
}, false);
}
A more simple version of scdavis41's answer:
$(document).ready(function(){
$('#shape > .shapeSelect').click(function(){
$('#shape > .shapeSelect').removeClass('selected');
$(this).addClass('selected');
});
});
I also put a selector that includes the control's main div id in case you want to put this control more then once in your page.
** EDIT **
If you absolutly want to use javascript and DOM try this:
document.getElementById("square").addEventListener('click', function (e) {
var divArray = document.getElementById("shape").getElementsByTagName("div"); //Get all the div child element of the main div
for (i = 0, count = divArray.length; i < count; i++) {
if(divArray[i].getAttribute("class").indexOf("selected") !== -1) { //check if the selected class is contained in the attribute
divArray[i].setAttribute("class", divArray[i].getAttribute("class").replace("selected", "")); // clear the selected class from the attribute
}
};
document.getElementById("square").setAttribute("class", document.getElementById("square").getAttribute("class").concat(" selected")); //select the square
}, false);
This is verbose, but you could use:
$(document).ready(function(){
$('#square').click(function(){
$('.shapeSelect').removeClass('selected');
$(this).addClass('selected');
});
$('#circle').click(function(){
$('.shapeSelect').removeClass('selected');
$(this).addClass('selected');
});
$('#rounded').click(function(){
$('.shapeSelect').removeClass('selected');
$(this).addClass('selected');
});
});
This is jQuery, which means you have to load the jQuery library, but putting this above your script tag:
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
If you are looking for a pure JavaScript solution, you could try this:
if(option == 'add'){
element.className = element.className + ' selected';
element.onclick = function() {select(this.id, 'remove')};
element.innerHTML = '✓';
}
else if(option == 'remove'){
element.className = element.className.replace(/\bselected\b/,'');
element.onclick = function() {select(this.id, 'add')};
element.innerHTML = '';
}
JSFiddle: http://jsfiddle.net/hKePD/
**EDIT**
Or if you were looking for a checkbox to be always checked, you could try this: http://jsfiddle.net/hKePD/1/
Building on scadvis41's answer, this is much shorter:
$(document).ready(function(){
$('.shapeSelect').click(function(){
$('.shapeSelect').removeClass('selected');
$(this).addClass('selected');
});
});

Wrapping a set of DOM elements using JavaScript

I have a series of p tags on my page and I want to wrap them all into a container, e.g.
<p>foo</p>
<p>bar</p>
<p>baz</p>
I want to wrap all the above tags into a container as follows:
<div>
<p>foo</p>
<p>bar</p>
<p>baz</p>
</div>
How to wrap a NodeList in an element using vanilla JavaScript?
Posted below are a pure JavaScript version of jQuery's wrap and wrapAll methods. I can't guarantee they work exactly as they do in jQuery, but they do in fact work very similarly and should be able to accomplish the same tasks. They work with either a single HTMLElement or an array of them. I haven't tested to confirm, but they should both work in all modern browsers (and older ones to a certain extent).
Unlike the selected answer, these methods maintain the correct HTML structure by using insertBefore as well as appendChild.
wrap:
// Wrap an HTMLElement around each element in an HTMLElement array.
HTMLElement.prototype.wrap = function(elms) {
// Convert `elms` to an array, if necessary.
if (!elms.length) elms = [elms];
// Loops backwards to prevent having to clone the wrapper on the
// first element (see `child` below).
for (var i = elms.length - 1; i >= 0; i--) {
var child = (i > 0) ? this.cloneNode(true) : this;
var el = elms[i];
// Cache the current parent and sibling.
var parent = el.parentNode;
var sibling = el.nextSibling;
// Wrap the element (is automatically removed from its current
// parent).
child.appendChild(el);
// If the element had a sibling, insert the wrapper before
// the sibling to maintain the HTML structure; otherwise, just
// append it to the parent.
if (sibling) {
parent.insertBefore(child, sibling);
} else {
parent.appendChild(child);
}
}
};
See a working demo on jsFiddle.
wrapAll:
// Wrap an HTMLElement around another HTMLElement or an array of them.
HTMLElement.prototype.wrapAll = function(elms) {
var el = elms.length ? elms[0] : elms;
// Cache the current parent and sibling of the first element.
var parent = el.parentNode;
var sibling = el.nextSibling;
// Wrap the first element (is automatically removed from its
// current parent).
this.appendChild(el);
// Wrap all other elements (if applicable). Each element is
// automatically removed from its current parent and from the elms
// array.
while (elms.length) {
this.appendChild(elms[0]);
}
// If the first element had a sibling, insert the wrapper before the
// sibling to maintain the HTML structure; otherwise, just append it
// to the parent.
if (sibling) {
parent.insertBefore(this, sibling);
} else {
parent.appendChild(this);
}
};
See a working demo on jsFiddle.
You can do like this:
// create the container div
var dv = document.createElement('div');
// get all divs
var divs = document.getElementsByTagName('div');
// get the body element
var body = document.getElementsByTagName('body')[0];
// apply class to container div
dv.setAttribute('class', 'container');
// find out all those divs having class C
for(var i = 0; i < divs.length; i++)
{
if (divs[i].getAttribute('class') === 'C')
{
// put the divs having class C inside container div
dv.appendChild(divs[i]);
}
}
// finally append the container div to body
body.appendChild(dv);
I arrived at this wrapAll function by starting with Kevin's answer and fixing the problems presented below as well as those mentioned in the comments below his answer.
His function attempts to append the wrapper to the next sibling of the first node in the passed nodeList. That will be problematic if that node is also in the nodeList. To see this in action, remove all the text and other elements from between the first and second <li> in his wrapAll demo.
Contrary to the claim, his function won't work if multiple nodes are passed in an array rather than a nodeList because of the looping technique used.
These are fixed below:
// Wrap wrapper around nodes
// Just pass a collection of nodes, and a wrapper element
function wrapAll(nodes, wrapper) {
// Cache the current parent and previous sibling of the first node.
var parent = nodes[0].parentNode;
var previousSibling = nodes[0].previousSibling;
// Place each node in wrapper.
// - If nodes is an array, we must increment the index we grab from
// after each loop.
// - If nodes is a NodeList, each node is automatically removed from
// the NodeList when it is removed from its parent with appendChild.
for (var i = 0; nodes.length - i; wrapper.firstChild === nodes[0] && i++) {
wrapper.appendChild(nodes[i]);
}
// Place the wrapper just after the cached previousSibling,
// or if that is null, just before the first child.
var nextSibling = previousSibling ? previousSibling.nextSibling : parent.firstChild;
parent.insertBefore(wrapper, nextSibling);
return wrapper;
}
See the Demo and GitHub Gist.
Here's my javascript version of wrap(). Shorter but you have to create the element before calling the function.
HTMLElement.prototype.wrap = function(wrapper){
this.parentNode.insertBefore(wrapper, this);
wrapper.appendChild(this);
}
function wrapDiv(){
var wrapper = document.createElement('div'); // create the wrapper
wrapper.style.background = "#0cf"; // add style if you want
var element = document.getElementById('elementID'); // get element to wrap
element.wrap(wrapper);
}
div {
border: 2px solid #f00;
margin-bottom: 10px;
}
<ul id="elementID">
<li>Chair</li>
<li>Sofa</li>
</ul>
<button onclick="wrapDiv()">Wrap the list</button>
If you're target browsers support it, the document.querySelectorAll uses CSS selectors:
var targets = document.querySelectorAll('.c'),
head = document.querySelectorAll('body')[0],
cont = document.createElement('div');
cont.className = "container";
for (var x=0, y=targets.length; x<y; x++){
con.appendChild(targets[x]);
}
head.appendChild(cont);
Taking #Rixius 's answer a step further, you could turn it into a forEach loop with an arrow function
let parent = document.querySelector('div');
let children = parent.querySelectorAll('*');
let wrapper = document.createElement('section');
wrapper.className = "wrapper";
children.forEach((child) => {
wrapper.appendChild(child);
});
parent.appendChild(wrapper);
* { margin: 0; padding: 0; box-sizing: border-box; font-family: roboto; }
body { padding: 5vw; }
span,i,b { display: block; }
div { border: 1px solid lime; margin: 1rem; }
section { border: 1px solid red; margin: 1rem; }
<div>
<span>span</span>
<i>italic</i>
<b>bold</b>
</div>

Categories