Transfer HTML elements from one list to another with JavaScript - javascript

I have a problem with the following JavaScript function. I have two UL and I need that when the user clicks on a LI, this element transfers to the other UL.
I've managed to move them onClick from on list to the other, the problem appears when I try to move again a LI that was previously in the other UL, when that happens it just doesn't work...
function testList() {
usersA = document.getElementById("users-a");
usersB = document.getElementById("users-b");
for (var i=0; i < usersA.getElementsByTagName("li").length; i++) {
usersA.getElementsByTagName("li")[i].onclick = function() {
transfer = this.cloneNode(true);
usersB.appendChild(transfer);
usersA.removeChild(this);
return false;
}
}
for (var i=0; i < usersB.getElementsByTagName("li").length; i++) {
usersB.getElementsByTagName("li")[i].onclick = function() {
transfer = this.cloneNode(true);
usersA.appendChild(transfer);
usersB.removeChild(this);
return false;
}
}
}
I know that my logic sucks but it's all I could come up with. Any ideas why it works the first time I transfer a LI but when I try to move back to its original UL it doesn't work?

You're not "moving" elements, you're creating a copy and deleting the original. Although this seems like a "move" from the user's point of view the new elements that you create do not have click handlers assigned. From MDN: "Cloning a node copies all of its attributes and their values but does not copy event listeners."
According to MDN, .appendChild() will remove the child from its current parent so you shouldn't need the two-step clone/remove that you are currently doing. I haven't tested it, but perhaps using just .appendChild() it will keep the handlers? If so you need to remove that handler and assign a new one to allow for which list it now belongs to.
Or, rewrite your handlers so that they check which list is the current parent (have a look at .parentNode) and move to the other list as appropriate.
Bearing in mind that click events "bubble up" starting from the target/source element and up through the parent heirarchy you're probably better off setting your click handlers on the parent <ul> elements and then testing which <li> was clicked. That way you don't have to worry about setting new click handlers on new child <li> elements.
function testList() {
var usersA = document.getElementById("users-a"),
usersB = document.getElementById("users-b");
usersA.onclick = function(e) {
// allow for the IE and non-IE way of handling the event object
if (!e) e = window.event;
var el = e.target || e.srcElement;
if (el.tagName === "li") {
usersB.appendChild(el);
}
}
usersB.onclick = function(e) {
// allow for the IE and non-IE way of handling the event object
if (!e) e = window.event;
var el = e.target || e.srcElement;
if (el.tagName === "li") {
usersA.appendChild(el);
}
}
}
Also, if you're using .getElementsByTagName() call it once in the loop initialisation and assign the result to a variable, then use the variable - don't keep calling the function again and again to test the length, or to access individual elements inside your loop:
for (var i=0, lis = usersA.getElementsByTagName("li"); i < lis.length; i++) {
lis[i].onclick = function() {
// etc
}
}

The problem is that even after you've moved an element from list A to list B, it still keeps its old onclick handler, which still says "remove me from list A and add me to list B". You need to change its onclick handler to say "remove me from list B and add me to list A". Here's one way to fix that:
var usersA = document.getElementById("users-a");
var usersB = document.getElementById("users-b");
var onclickA;
var onclickB = function() {
usersA.appendChild(this);
this.onclick = onclickA;
return false;
};
onclickA = function() {
usersB.appendChild(this);
this.onclick = onclickB;
return false;
};
for (var i=0; i < usersA.getElementsByTagName("li").length; i++)
usersA.getElementsByTagName("li")[i].onclick = onclickA;
for (var i=0; i < usersB.getElementsByTagName("li").length; i++)
usersB.getElementsByTagName("li")[i].onclick = onclickB;
(I also got rid of the cloneNode stuff, for exactly the reasons that nnnnnn gives.)

Related

Adding one handler with different parameters

I am creating elements in list tag dynamically and I want to add different event handler for all elements in list. How can I achieve this?
var menuLinks = document.getElementsByClassName('test');
for(var j = 0; j < menuLinks.length; j++)
{
var menuLink = menuLinks[j];
menuLink.onclick = function(e) {
e.preventDefault();
console.log(menuLink.innerHTML);
};
}
I added the elements in class name test but no matter which ever element I click it always gives me last element.
Your problem is that you are creating a closure inside a loop. See JavaScript closure inside loops – simple practical example for an explanation and generic solution.
In short: JavaScript doesn't have block scope. Since there is only a single menuLink variable, every handler refers to that one variable. The variable can of course only have one value, which is the one that is set in the last iteration.
However, in your case there is a simpler solution: You can use this inside the event handler to refer to the element. You don't need to rely on the loop variable:
menuLinks[j].onclick = function(e) {
e.preventDefault();
console.log(this.innerHTML);
};
Learn more about this in event handlers.
try this,
var menuLinks = document.getElementsByClassName('test');
for(var j = 0; j < menuLinks.length; j++)
{
var menuLink = menuLinks[j];
menuLink.onclick = function(e) {
e.preventDefault();
console.log(e.target.innerHTML);
};
}

Calling function to only one div of a certain class using pure JS

as part of my learning process as front end developer, I have decided to produce a product without jQuery and instead use vanilla JavaScript to do all my interactions.
I want to call a function onClick and retrieve the positioning of only the DOM node I clicked which is part of a class. However I don't know how this would be done in pure JavaScript. Basically I want to replicate what $(this) does in jQuery.
Here is my code below so far:
function expand(name) {
var elem = document.querySelectorAll(name)
for (var i = 0; i < elem.length; i++) {
var rect = elem[i].getBoundingClientRect();
console.log(rect.top, rect.right, rect.bottom, rect.left);
}
}
document.querySelectorAll(".box").onclick = expand(this)
Thanks in advance and any other possible solutions to the problem are much appreciated.
document.querySelectorAll returns a NodeList. This is Array-like and needs to be iterated over
You can use this inside your handler to refer to the node the handler is attached to
The vanilla way to listen for an event is EventTarget.addEventListener, this only attaches a handler for one event type at a time so if you want to be able to use multiple you'll need to loop here again (you didn't do this in your example but jQuery lets you do this)
function expand() {
var rect = this.getBoundingClientRect();
console.log(rect.top, rect.right, rect.bottom, rect.left);
}
function nodeListOn(list, type, handler, useCapture) {
var i, t;
type = type.split(/s+/);
for (i = 0; i < list.length; ++i) {
for (t = 0; t < type.length; ++t) {
list[i].addEventListener(type[t], handler, useCapture);
}
}
}
var nodes = document.querySelectorAll('.box');
nodeListOn(nodes, 'click', expand);
I think this is what you want:
var elements = document.querySelectorAll(".box");
Array.prototype.forEach.call(elements, function(element) {
element.onclick = expand.bind(element);
});
This will result in the this scope being set to the element being clicked on.

Cleaning up memory leaks

I am using jQuery to clone elements, then I save a reference to an element within that clone. And much later remove the clone. Here is a basic example:
HTML
<div> <span></span> </div>
Script
var i, $clone, $span,
$saved = $('span'),
$orig = $('div');
for (i = 0; i < 100; i++) {
$clone = $orig.clone().appendTo('body');
$span = $clone.find('span');
$saved = $saved.add($span);
$clone.remove();
}
console.log( 'leaking = ', $saved.length);
The console log outputs a length of 101.
I need to clean up the $saved jQuery object and remove references to elements no longer attached to the DOM. So I wrote this basic function to clean it all up.
var cleanUpLeaks = function ($el) {
var el, remove,
index = $el.length - 1;
while (index >= 0) {
el = $el[index];
remove = true;
while (el) {
el = el.parentNode;
if (el && el.nodeName === 'HTML') {
remove = false;
break;
}
}
if (remove) {
$el.splice(index, 1);
}
index--;
}
return $el;
};
console.log( 'cleaned up = ', cleanUpLeaks( $saved ).length );
This time the console outputs 1.
So now my questions are:
How could I have prevented the memory leak in the first place?
And if that isn't possible, should I be using .splice() in the cleanUpLeaks function to remove the reference? Or would it be better to set that reference to null as is recommended? Because when I do set it to null, $saved remains at a length of 101.
Demo: http://jsfiddle.net/Mottie/6q2hjazg/
To elaborate, I save a reference to the span in $saved. There are other functions that use this value for styling and such. This is a very basic example; and no, I do not immediately remove the clone after appending it to the body, it was done here to show how the memory leak is occurring.
The better solution here is to stop saving dynamic DOM elements in a persistent jQuery variable. If your page is regularly removing content from the DOM, then saving these in a persistent jQuery object just sets you up for having to deal with memory leaks, rather than changing the design to a design that does not have to save references to DOM elements at all.
If instead, you just tag interesting elements with a particular class name that is not used elsewhere in the document, you can generate the desired list of elements at any time with a simple jQuery selector query and you will have no issues at all with leaks because you aren't ever retaining DOM references in persistent variables.
One possible solution is that you take a leaf out of AngularJS's book and monkey-patch jQuery to fire an event when an element is removed. Then you can add a handler for that event and restore the state of $saved to what it was before you added the $span.
First, monkey patch jQuery (taken from AngularJS source):
// All nodes removed from the DOM via various jQuery APIs like .remove()
// are passed through jQuery.cleanData. Monkey-patch this method to fire
// the $destroy event on all removed nodes.
var originalCleanData = jQuery.cleanData;
var skipDestroyOnNextJQueryCleanData;
jQuery.cleanData = function (elems) {
var events;
if (!skipDestroyOnNextJQueryCleanData) {
for (var i = 0, elem;
(elem = elems[i]) != null; i++) {
events = jQuery._data(elem, "events");
if (events && events.$destroy) {
jQuery(elem).triggerHandler('$destroy');
}
}
} else {
skipDestroyOnNextJQueryCleanData = false;
}
originalCleanData(elems);
};
Next, add in your $destroy event handler and restore the captured original state of $saved.
var i, $clone, $span,
$saved = $('span'),
$orig = $('div');
for (i = 0; i < 100; i++) {
(function ($originalSaved) {
$clone = $orig.clone().appendTo('body');
$span = $clone.find('span');
$clone.on('$destroy', function () {
$saved = $originalSaved;
$originalSaved = null;
});
$saved = $saved.add($span);
$clone.remove();
})($saved);
}
console.log('original length = ', $saved.length); // => 1
Here is a jsFiddle with this working. In my testing in Chrome, this doesn't introduce additional leaks.

Set attribute on multiple selectors

I'm trying the following, which doesn't work, but am wondering if there is something similar?
Basically, I'm trying to set the same attribute on multiple elements:
document.querySelectorAll("#id1, #id2, #id3").setAttribute('onclick','return false;');
I'm using vanilla Javascript, no library.
querySelectorAll() will return an array-like node list that you need to loop through:
var elems = document.querySelectorAll("#id1, #id2, #id3");
for(var i = 0; i < elems.length; i ++){
elems[i].setAttribute('onclick', 'return false');
}
JSFiddle
There are several approaches for this, the most basic one is to iterate the set and add click handlers (In your case, you want an addEventHandler not an attribute!)
var elements = document.querySelectorAll("#id1, #id2, #id3");
[].forEach.call(elements, function addClickHandler(el) {
el.addEventListener('click', function() { return false; });
};
Perhaps a better approach if you have many elements is to set one event handler on the document, and see if it matches a query selector:
var elements = [].slice.call(document.querySelectorAll("#id1, #id2, #id3")); //Turn into an array
document.addEventHandler('click', function(e) {
if (elements.indexOf(e.target) !== -1) { //Element in list was clicked!
return false;
}
};

Not able attach click event for a button using Javascript oop

I'm working on attaching a click event on a button which is having class "nextPage", but its not working. Let me show you the code.
function myContent() {
}
myContent.prototype.clickNext = function() {
alert("clicked");
};
var objMyContent = new myContent();
var el = document.getElementsByClassName('nextPage');
el.onclick=objMyContent.clickNext();
Please take a look into it. Please let me know where I did mistake.
You need to reference the function, not execute it, when assigning the click handler.
Instead of:
el.onclick = objMyContent.clickNext();
Use this:
el.onclick = objMyContent.clickNext;
The first piece of code executes clickNext, and assigns it's return value to el.onclick.
The second line assigns a reference to the clickNext function to el.onclick, instead. (Which is what you want)
Also, getElementsByClassName returns an HTMLCollection (Which is basically an array of HTML elements).
You'll need to assign the click handler to each found element in that collection:
for(var i = 0; i < el.length; i++){
el[i].onclick = objMyContent.clickNext;
}
iterate the array and the assign the onclick event
var objMyContent = new myContent();
var el = document.getElementsByClassName('nextPage'); for(var i=0;i<el.length;i++) el[i].onclick=objMyContent.clickNext;

Categories

Resources