Javascript - How to Remove DOM elements using click events and classes? - javascript

I am having some difficulty using parentNode.removeChild(). I have a list of 'items' in an un-ordered list, each have there own delete button. I am trying bind a click event to each individual button that will delete it's respective parent 'item'.
My code so far:
<ul class="list">
<h2>This is a list</h2>
<li class="item">
<h3>Some Item</h3>
<button class="delete">Delete</div>
</li>
<li class="item">
<h3>Some Item</h3>
<button class="delete">Delete</div>
</li>
<li class="item">
<h3>Some Item</h3>
<button class="delete">Delete</div>
</li>
</ul>
var childElements = document.getElementsByClassName('item');
var buttonElement = document.getElementsByClassName('delete');
function deleteItem(buttonsClass, childClass) {
for (var i=0;i<buttonsClass.length;i++) {
var child = childClass[i];
buttonsClass[i].addEventListener('click', function(child) {
childClass[i].parentNode.removeChild(childClass[i]);
}, false);
}
}
deleteItem(buttonElement, childElements);
I know there is an easier way to do this with jQuery but i really want to solve this with plain javascript. Thank you for any and all help.

This is a perfect case for event delegation. No need for jQuery at all:
(function(window, htmlElement) {
'use strict';
htmlElement.addEventListener("click", handleClick, false);
function handleClick(event) {
if (event.target.classList.contains("delete")) {
event.preventDefault();
removeItem(event.target);
}
}
function removeItem(button) {
var item = getItem(button),
confirmMessage;
if (item) {
confirmMessage = item.getAttribute("data-confirm");
if (!confirmMessage || window.confirm(confirmMessage)) {
item.parentNode.removeChild(item);
}
}
else {
throw new Error("No item found");
}
}
function getItem(button) {
var element = button.parentNode,
item = null;
while (element) {
if (element.nodeName === "LI" || element.nodeName === "TR") {
item = element;
break;
}
element = element.parentNode;
}
return item;
}
})(this, this.document.documentElement);
You have one click handler for the entire page, regardless of how many delete buttons you have. This should also work for list items or table rows, and by specifying a data-confirm attribute on your buttons, it will pop up a confirm box before removing it.
<button type="button" class="delete"
data-confirm="Are you sure you want to delete this item?">
Delete
</button>
You can also easily change this so it uses another attribute to find the delete button:
<button type="button" class="delete"
data-delete
data-confirm="...">
Delete
</button>
Just change the condition of the if statement in the handleClick function to:
if (event.target.hasAttribute("data-delete")) {
event.preventDefault();
removeItem(event.target);
}
This decouples your behavior from styling. You can use the delete class name for styling, and the data-delete attribute for JavaScript behavior. If you need to change the name of the CSS delete class for any reason, or use a different class, because you decide to use a third party CSS framework like Bootstrap, then you don't need to change a single line of JavaScript.
The last advantage here is that the click handler is attached to the document.documentElement object, which is available the moment JavaScript begins executing and represents the <html> element. No need for jQuery's document ready event handler. Just attach the click handler and import the script at any point on the page.

The problem is that your childClass[i] that you call when you click an element, is not what you expect when you define the function.
You should use event.target for catch the element clicked
var childElements = document.getElementsByClassName('item');
var buttonElement = document.getElementsByClassName('delete');
var _handler = function(e) {
e.target.parentNode.parentNode.removeChild(e.target.parentNode);
}
function deleteItem(buttonsClass, childClass) {
for (var i=0;i<buttonsClass.length;i++) {
buttonsClass[i].addEventListener('click', _handler, false);
}
}
deleteItem(buttonElement, childElements);
-- edit --
If you want to use the original approach, then you can solve it in this way:
function deleteItem(buttonsClass, childClass) {
for (var i=0;i<buttonsClass.length;i++) {
(function(child) {
buttonsClass[i].addEventListener('click', function(e) {
child.parentNode.removeChild(child);
}, false);
})(childClass[i]);
}
}
With encapsulation (function(encapsulatedChild) { })(child) you can store the value of child in a context that does not change during the next cycle.

Looks like you want to bind a click event to delete buttons, and on that event delete that item.
You need to fetch the child classes individual buttonsClass elements.
function deleteItem( childElements )
{
Array.prototype.slice.call( childElements ).forEach( function( item ){
var deleteChildren = item.getElementsByClassName( "delete" );
Array.prototype.slice.call( deleteChildren ).forEach( function( deleteBtn ){
deleteBtn.addEventListener('click', function()
{
this.parentNode.removeChild( this );
}, false);
});
});
}
or even more simply, just pass the list of buttons on clicking which parent item will be deleted
function deleteItem( buttonElement )
{
Array.prototype.slice.call( buttonElement ).forEach( function( button ){
button.addEventListener('click', function()
{
this.parentNode.removeChild( this );
}, false);
});
}

Related

Event listener for A tag to close overlay menu not working

I have an overlay menu that I need to shut when clicking on the links. I have some event listeners but it doesn't work on the links. The menu event used on the burger icon works, the menuItems is for the links that doesn't work. I need it to also work with Pjax link
I have tried target the a tags like menuItems = document.querySelectorAll('.__overlay_nav_content_list_item a'); but it does not work.
(function($) {
"use strict";
var app = function() {
var body = undefined;
var menu = undefined;
var menuItems = undefined;
var init = function init() {
body = document.querySelector('body');
menu = document.querySelector('.burger_menu_icon');
menuItems = document.querySelectorAll('.__overlay_nav_content_list_item');
applyListeners();
};
var applyListeners = function applyListeners() {
menu.addEventListener('click', function() {
return toggleClass(body, '__overlay_nav-active');
});
menuItems.addEventListener('click', function() {
return toggleClass(body, '__overlay_nav-active');
});
};
var toggleClass = function toggleClass(element, stringClass) {
if (element.classList.contains(stringClass))
element.classList.remove(stringClass);
else element.classList.add(stringClass);
};
init();
}();
})(jQuery);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
Have you checked event bubbling? If menuItems are descendants of menu, clicking the menu items will trigger menuItems.addEventListener('click', function() { and hence toggle the class, then the event will bubble up to menu and trigger menu.addEventListener('click', function() {, removing the class you just added. So the end result is that it looks like nothing changed.
If that is the issue, only use the click event on menu, or stop the bubbling up of the event of the menuItems by using event.stopPropagation().
Although i would prefer only using the click event of the menu, first try:
Keep in midn that querySelectorAll() returns a nodeList, so it's an arraylike object containing all the links, not a single link.
Array.from( menuItems ).forEach(function( menuItem ) {
menuItem.addEventListener('click', function( event ) { // add event here so you have access to it!
event.stopPropagation(); // call stopPropagation() first
return toggleClass(body, '__overlay_nav-active'); // Once you return, the function stops.
});
});
so we know that this is the problem or not. Do not forget to add event as the parameter for the event handler function.

js line causes code above it not to execute

Here's a pen with the full html: https://codepen.io/froggomad/pen/WLdzoB
I'm writing 2 functions - one to show hidden content, and one to hide it. I'm wanting the show() function to execute on the parent div and the hide() function to execute on the div with the selector .click-text.
However, I'm switching text on .click-text from show to hide so I don't want the hide function to remain on the text at all times. I also want it obvious that its interactive text when changing to a hide function, so I make it a link.
That's all well, but when attempting to set the onclick Attr of the parent back to the show() function, nothing in the hide block executes at all.
If I remove the line setting the parent's onclick Attr, the script executes as expected. If I set another element's onclick Attr, the script executes as expected.
However, with that line in there, nothing happens and there's no output in the console to indicate an error. I even set an alert with the type of element and classname to ensure I'm targeting the right element.
Get closest parent of element matching selector:
var getClosest = function (element, selector) {
for ( ; element && element !== document; element = element.parentNode ) {
if ( element.matches(selector) ) return element;
}
return null;
}
Show Hidden Element ul.service-category-menu
function show(elem) {
var menu = elem.querySelector("ul.service-category-menu"),
click = elem.querySelector(".click-text"),
parent = getClosest(elem, '.service-category');
;
if (menu.style.display === "none" || menu.style.display === "") {
menu.style.display = "block";
click.innerHTML = "<a href=\"#\">Click to Hide<\/a>";
click.setAttribute('onclick','hide(this);');
elem.setAttribute('onclick', 'null');
}
}
Hide Element
function hide(elem) {
var parent = getClosest(elem, '.service-category'),
menu = parent.querySelector("ul.service-category-menu"),
click = parent.querySelector(".click-text")
;
alert(parent + "\n" + parent.className);
//Outputs div element with expected class name (class name is unique on each div)
if (menu.style.display === "block") {
menu.style.display = "none";
click.innerHTML = "Click to Show";
click.setAttribute('onclick', 'null');
//the above lines don't execute when the following line is in place. There's no error in console.
parent.setAttribute('onclick','show(this)');
}
}
First off, I must confess that I'm against using onclick attributes. If you're not using a framework such as VueJS or React, I think HTML and JS should remain separated for better control and maintainability.
You can use addEventListener, removeEventListener, and e.stopPropagation() to avoid triggering multiple event handlers.
Events have two phases:
Event capture: the event spreads from the document all the way down to the target element.
To catch an event during this phase, do:
elm.addEventListener('click', myFunc, true);
Event bubbling: the event bounces back from the target to the document.
To catch an event during this phase, do:
elm.addEventListener('click', myFunc, false); /* or just omit the 3rd param */
Using e.stopPropagation() allows you to break that chain.
// When the DOM is ready
window.addEventListener("DOMContentLoaded", init);
function init() {
// Get all categories
var $categories = document.querySelectorAll(".service-category");
// For each of them
Array.from($categories).forEach(function($category) {
// Add an event listener for clicks
$category.addEventListener("click", show);
});
}
function getClosest(element, selector) {
for (; element && element !== document; element = element.parentNode) {
if (element.matches(selector)) return element;
}
return null;
}
function show(e) {
var $menu = this.querySelector("ul.service-category-menu"),
$click = this.querySelector(".click-text");
if (["none", ""].includes($menu.style.display)) {
$menu.style.display = "block";
$click.innerHTML = 'Click to Hide';
$click.addEventListener("click", hide);
// Remove the `show` event listener
this.removeEventListener("click", show);
}
e.stopPropagation();
}
function hide(e) {
var $parent = getClosest(this, ".service-category"),
$menu = $parent.querySelector("ul.service-category-menu"),
$click = $parent.querySelector(".click-text");
if (!["none", ""].includes($menu.style.display)) {
$menu.style.display = "none";
$click.innerHTML = "Click to Show";
$click.removeEventListener("click", hide);
$parent.addEventListener("click", show);
}
e.stopPropagation();
}
.service-category{display:inline-block;border:3px solid #ccc;margin:1%;font-weight:700;font-size:3.5vw;cursor:pointer;background-color:#fff;z-index:3;background-position:center;background-size:cover;color:#000}.click-text{text-align:right;font-size:1.25vw;font-style:italic;font-weight:700;padding-right:1%}.service-category:hover .click-text{color:#b22222}.service-category-menu{display:none;margin-left:8%;margin-right:8%;margin-top:1%;background-color:#fff;font-weight:700;font-size:1.6vw;border-radius:10px}
<div class="service-category web-back" id="web-back">
<div class="row-overlay">
Web <br /> Development
<div class="click-text">Click to Show</div>
<ul class="service-category-menu web">
<li>
Some text...
</li>
</ul>
</div>
</div>
<div class="service-category web-front" id="web-front">
<div class="row-overlay">
Web <br /> Design
<div class="click-text">Click to Show</div>
<ul class="service-category-menu web">
<li>
Some text...
</li>
</ul>
</div>
</div>
It is executed, it's just after you click that Click to Hide, the event continues to parent and the event handler of the parent executed. Thus, what exactly happen is (with that line), after hide() called, you inadvertently called show().
In javascript it's usually called bubbles (when you click the children, the click handler of parent will also be executed after click handler of children complete).
So the solution, you can add this line at the end of the hide() function
event.stopPropagation();
To stop the event from continuing to the parent
Setting event.stopPropagation as mentioned in the other answer will potentially fix your issue. Alternatively, you can change the last line of your hide function to window.setTimeout(e => parent.setAttribute('onclick','show(this)'), 0).
What's happening right now is:
You click
it executes your hide function, and during that function it binds a click event to the parent
The click propagates to the parent and executes the newly bound function, re-showing the content.
By using setTimeout(fn, 0), you're making sure the click event completes before the function is bound to the parent.

Why two event listeners are triggered instead of one?

I have element which has two event listeners that are triggered depending of his class name. During click event, his className is changing, and this another class has its own different event listener. Two events should be triggered alternately by every click.
During first click listener calls function editClub, but all the next clicks calls two functions. I don't know why that removed-class-event is triggered. Maybe its because after each event function callListeners is executed, and there are multiple listeners on one object? But should be triggered just one. Later I wrote removeListeners function which remove all existing listeners and put her to call just before callListeners function. But then just editClub function is executed by every click. What's wrong with my code?
function callListeners()
{
if ( document.getElementsByClassName('editBtn') )
{
let x = document.getElementsByClassName('editBtn');
for ( let i = 0; i < x.length; i++ )
{
x[i].addEventListener('click', editClub);
}
}
if ( document.getElementsByClassName('saveBtn') )
{
let x = document.getElementsByClassName('saveBtn');
for ( let i = 0; i < x.length; i++ )
{
x[i].addEventListener('click', saveClub);
}
}
}
function editClub(event)
{
event.preventDefault();
this.setAttribute('src','img/\icon_save.png');
this.setAttribute('class','saveBtn');
//removeListeners(); <-- here I placed removeListeners function
callListeners();
}
function saveClub(event)
{
event.preventDefault();
this.setAttribute('src', 'img/\icon_edit.png');
this.setAttribute('class', 'editBtn');
//removeListeners(); <-- here I placed removeListeners function
callListeners();
}
It looks like you are not removing the classes when the click event happens so after the first click the element has both classes.
Just changing an element's class does not change the event listeners that were setup on it previously. They will still be there unless you explicitly remove them, or the elements themselves are destroyed. And you get multiple calls because you keep adding new listeners without removing the old ones.
You could on each click remove the old listener and add the new listener
function editClub(){
this.removeEventListener("click",editClub);
this.addEventListener("click",saveClub);
}
function saveClub(){
this.removeEventListener("click",saveClub);
this.addEventListener("click",editClub);
}
But that is a bit tedious. Instead you can setup a delegated event listener on a static parent like document. Doing so allows for a single event listener which will be called when either button is clicked. Then in that listener you can check the element's class, and execute the appropriate function for it:
function clubAction(event){
//get a reference to the element clicked
var element = event.target;
//call the appropriate function
//or just do the work here
if(element.classList.contains("editClub")){
editClub.call(element,event);
} else if(element.classList.contains("saveClub")) {
saveClub.call(element,event);
}
}
document.addEventListener("click",clubAction);
classList is a property that lets you get,set,remove, and other operations for the classes of the element.
Demo
function clubAction(event) {
var element = event.target;
if (element.classList.contains("editClub")) {
editClub.call(element,event);
} else if (element.classList.contains("saveClub")) {
saveClub.call(element,event);
}
}
document.addEventListener("click", clubAction);
function editClub(event) {
event.preventDefault();
this.classList.remove('editClub');
this.classList.add('saveClub');
this.innerText = "Save";
}
function saveClub(event) {
event.preventDefault();
this.classList.add('editClub');
this.classList.remove('saveClub');
this.innerText = "Edit";
}
.saveClub {
background:#0F0;
}
.editClub {
background:#FF0;
}
<button class="saveClub">Save</button>
<button class="editClub">Edit</button>

How to rebind an event listener to elements that are removed and added again

I have built a pretty complex slider and now have to build it so it can be removed and re-added to the page based on a selection. I have a simple click event listener for the pagination to call all my animations and timers that looks like this
let $slideItems = $slideShow.querySelector('.slideshow-items'),
$slideshowNav = $slideShow.querySelector('.slideshow-nav'),
$slideshowNavButton = $slideshowNav.getElementsByTagName('button');
forEach($slideshowNavButton, (index, el) => {
el.addEventListener('click', function() {
let isActive = this.classList.contains('active');
if (!isActive) {
clearTimeout(timer);
slideshowClick($slideShow, this);
slideshowAnimations($slideShow, index);
slideTimer();
}
});
});
I use the forEach function as a for loop to go through all the elements I need, like having multiple $slideShow's on the page, and return them as an indexed array. The issue I am having is that I need to add a functionality in which the $slideshowNav and all the $slideshowNavButtons get removed and rebuilt from a function outside of the $slideshow function and can't figure out how to rebind the click event without repeating all of the code. Is there a way to bind this event to the $slideshow object, similar to the way jQuery's .on function works or rebind the click event to the new $slideshowNavButton's after they are created? I am not able to use jQuery so I can't use the .on function.
its hard to give you correct answer since you motion too many classes without visual placement but hope this helps:
var btnWraper = document.querySelectorAll('.btnWraper > button');
btnWraper.forEach(function(e){
e.onclick = buttonClicking;;
})
let remake = document.getElementById('reMakeMe');
remake.addEventListener('click', function(){
var btnWraper = document.querySelectorAll('.btnWraper > button');
//if deleted
if(!btnWraper.length)
{
createButtons('Btn1');
createButtons('Btn2');
createButtons('Btn3');
createButtons('Btn4');
}
},false)
let rest = document.getElementById('resetMe');
rest.addEventListener('click', function(){
var btnWraper = document.querySelectorAll('.btnWraper > button');
btnWraper.forEach(function(e){
e.remove();
})
},false) ;
function buttonClicking (){
alert(this.innerHTML);
}
function createButtons(value){
var btn = document.createElement("button");
btn.innerHTML = value;
btn.onclick = buttonClicking;
var parentElement = document.getElementsByClassName("btnWraper")[0];
parentElement.appendChild(btn);
}
<div class="btnWraper">
<button>Btn1</button>
<button>Btn2</button>
<button>Btn3</button>
<button>Btn4</button>
</div>
<div>
<button id="resetMe">Reset All</button>
<button id="reMakeMe">ReMake All</button>
</div>

Deleting a button onclick from a class

onclick of a button with a class of obliterate, I want to have my following code take a bunch of identical buttons, besides its unique id and then delete it. The Buttons can be through innerHTML at any time from user input. I want to remove the button onclick, after a confirm This is my current code:
document.getElementsByClassName('obliterate').onclick = function() {
if (confirm('Are you sure you want to delete this?')){
//get this then remove it with remove(this.id);
}
}
Should be straight forward, attach event handler to elements, remove element
var elems = document.querySelectorAll('.obliterate');
for (var i=0; i<elems.length; i++) {
elems[i].addEventListener('click', function() {
if ( confirm('Are you sure you want to delete this?') ) {
this.parentNode.removeChild(this);
}
}, false);
}
If the elements don't exist on pageload, you have to delegate, and doing that without library can be somewhat complicated, depending on what selector you want to match, if there are children inside the clicked element etc. but here's a simlified version
document.addEventListener('click', function(event) {
var clicked = event.target;
var elems = [].slice.call(document.querySelectorAll('.obliterate'));
if ( elems.indexOf(clicked) !== -1 ) {
if ( confirm('Are you sure you want to delete this?') ) {
clicked.parentNode.removeChild(clicked);
}
}
}, false);
FIDDLE
You can use the bind function to provide context so in your case:
element.onclick = function(){ ... }.bind(this);

Categories

Resources