6

I'm trying to get rid of jquery from my page and rewrite some functionalities to pure js. There are 2 lists with class work, containing a few li elements. Each li element should have an action on click to add class 'active' to it.

In jquery it is very simple:

$('.work li').on('click', function () {
            var that = $(this);
            that.parent().find('li.active').removeClass('active');
            $(this).addClass('active');
        })

Is there a nicer solution in pure js rather than making something like this with nested loops:

    var lists = document.getElementsByClassName('work');
    for(var i=0; i<lists.length; i++){
       var children = lists[i].querySelectorAll('li');
       for(var j=0; j<children.length;j++){
        children[j].addEventListener();
       }
    }
Christian
  • 4,902
  • 4
  • 24
  • 42
mjanisz1
  • 1,478
  • 3
  • 17
  • 43
  • Now you see why JQuery was developed and has become so popular. – jalynn2 Dec 23 '14 at 13:51
  • 1
    Now i see how much i don't know about js – mjanisz1 Dec 23 '14 at 13:54
  • Working directly with the DOM is tedious and requires a lot of searching loops like this. Also, note that there are some differences between browsers for method names and capabilities. They were the two problems that jQuery set out to solve initially. Why are you trying to eliminate it? – jalynn2 Dec 23 '14 at 14:00
  • Why would you NOT use jQuery when playing around with the DOM? – AndreasHassing Dec 23 '14 at 14:01
  • Because jQuery treats the DOM as a humongous, mutable, global object in the middle of your application? By the way, @jalynn2, what are the differences between browsers for method names? –  Dec 23 '14 at 14:03
  • 2
    My page has some threejs in it and currently uses jquery only for this purpose (i've rewritten all other functionalities to pure js, and they work fine). I think there is no sense in keeping a separate library just to be able to assign onclick listeners in a comfortable way + i want to learn something new ;-) – mjanisz1 Dec 23 '14 at 14:04
  • @mjanisz1 - that's fine, you are on the right track and there is no way to get around the looping. If you are doing a lot of it, you can start to refactor it into common functions, but then you are starting to reinvent jQuery :-) – jalynn2 Dec 23 '14 at 14:06
  • jQuery is not huge - it's about 29kb gzipped. Seriously, 29kb bothers you enough to make you want to write loops like that? – Adam Jenkins Dec 23 '14 at 14:07
  • If it bothers you that much, you can always do a custom build - https://github.com/jquery/jquery#how-to-build-your-own-jquery - of jQuery to just include the DOM API and remove everything else. – Adam Jenkins Dec 23 '14 at 14:08
  • @torazaburo - the DOM IS a humongous, mutable, global object in the middle of your application. You can craft code to access it in a controlled way, but there is no changing that fact. I don't remember differences off the top of my head, except with the AJAX objects, but they are out there. I've been using jQuery too long and they have faded from memory. – jalynn2 Dec 23 '14 at 14:09
  • 4
    I think the OP definitely has it the right way round here. If your using a complicated library for a single feature, which is easily achievable in vanilla then your doing something wrong. If you had a greater need, like UI elements or AJAX as well, then I'd say stick with jQuery. – nepeo Dec 23 '14 at 14:44
  • jquery can dissapear – SuperUberDuper Jul 15 '15 at 16:01

6 Answers6

5

querySelectorAll() is quite powerful tool.

I've made a JSFiddle for you.

Code is still nice and readable:

for (var item of document.querySelectorAll(".work li")) {
 item.addEventListener("click", function (evt) {
     evt.target.classList.add("active");
 }, false);
}

Another way is (if you still want to iterate using forEach):

Array.prototype.forEach.call(document.querySelectorAll(".work li"), function(item) {
   item.addEventListener("click", function (evt) {
        evt.target.classList.add("active");
   }, false);
});

Here (JSFiddle) we are interating through a NodeList returned by querySelectorAll(".work li") by calling the Array.prototype.forEach method on it.

And here (Jsfiddle) is the full example, that toggles the class:

Array.prototype.forEach.call(document.querySelectorAll(".work li"), function(item) {
   item.addEventListener("click", function (evt) {
        evt.target.toggleActive = !evt.target.toggleActive;
       evt.target.classList[!!evt.target.toggleActive ? 'add' : 'remove']("active");
   }, false);
});

And finally, if there should be only one active element at a time (Jsfiddle):

var currentActiveElement;
Array.prototype.forEach.call(document.querySelectorAll('.work li'), function(item) {
item.addEventListener('click', function(evt) {
  currentActiveElement && currentActiveElement.classList.remove('active');
  currentActiveElement = evt.target;
  currentActiveElement.classList.add('active');
 }, false);
});
AlignItems
  • 59
  • 11
Alexander Arutinyants
  • 1,619
  • 2
  • 23
  • 49
  • how come we can do this but not use for each for querySelectorAll? – SuperUberDuper Jul 15 '15 at 16:03
  • 2
    If I understand your question correctly. `querySelectorAll()` returns an object [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), witch [can not be treated exactely as Array](https://developer.mozilla.org/en-US/docs/Web/API/NodeList#Why_is_NodeList_not_an_Array.3F). – Alexander Arutinyants Jul 16 '15 at 07:31
5

There are 2 lists with class work, containing few li elements. Each li element should have an action on click to add class 'active' to it.

You could create that entire functionality by adding an event listener to all lis returned by the querySelectorAll. The querySelectorAll returns a nodeList and not an array, so need to map it in order to iterate it. However, note that we are still iterating the set.

Example Snippet:

var lists = document.querySelectorAll(".work li"), 
    doSomething = function() {
        [].map.call(lists, function(elem) { elem.classList.remove("active") });
        this.classList.add("active");
    };
[].map.call(lists, function(elem) {
    elem.addEventListener("click", doSomething, false);
});
li { cursor: pointer; }
.active { background-color: yellow; }
<ul class="work"><li>One</li><li>Two</li><li>Three</li></ul>
<ul class="work"><li>Four</li><li>Five</li><li>Six</li></ul>

In fact you could also use event delegation and add event listener only on the ul and then use the event.target to handle your routine. This is better than adding an event listener to each and every li, in case there are many of them. There will be only as many handlers as uls.


Is there a nicer solution in pure js rather than making something like this with nested loops

Not really. You have to iterate over the set of elements anyway. Nicer in terms of jazzy, yes. Nicer in terms of avoiding the loops, no. Nicer in terms of efficiency, well yes. Compare the above vanilla javascript code with the jQuery one in your question:

$('.work li').on('click', function () { // internally will iterate and add listener to each li
    var that = $(this);
    that.parent().find('li.active').removeClass('active'); // go to parent and then find li
    $(this).addClass('active');
});

.

Abhitalks
  • 27,721
  • 5
  • 58
  • 81
  • 1
    thanks! this looks like a tidy solution! i also like the [].map.call for going through the set of elements! – mjanisz1 Dec 30 '14 at 09:17
3

You could actually attach click event listener on wrapper element and filter out clicked li element using event.target (or for IE event.srcElement).

Try out below snippet (not production code by any means, it should be enhanced especially for browser compatibility). Good side of this solution is that you don't have to watch for new elements dynamically added to wrapper element...

function clickHandler(e) {
  var elem, evt = e ? e : event;
  if (evt.srcElement) {
    elem = evt.srcElement;
  } else if (evt.target) {
    elem = evt.target;
  }

  // Filter out li tags
  if (elem && ('LI' !== elem.tagName.toUpperCase())) {
    return true;
  }

  console.log('You clicked: ' + elem.innerHTML)
  return true;
}

var lists = document.getElementsByClassName('work');
for (var i = 0; i < lists.length; i++) {
  var children = lists[i].addEventListener('click', clickHandler);
}
<ul class="work">
  <li>1.1</li>
  <li>1.2</li>
  <li>1.3</li>
  <li>1.4</li>
</ul>
<ul class="work">
  <li>2.1</li>
  <li>2.2</li>
  <li>2.3</li>
  <li>2.4</li>
</ul>
<ul class="work">
  <li>3.1</li>
  <li>3.2</li>
  <li>3.3</li>
  <li>3.4</li>
</ul>
sbgoran
  • 3,451
  • 2
  • 19
  • 30
2

Sure. Why not just make better use of document.querySelectorAll? It does use the syntax of css, so we can grab both in one go.

Perhaps something like this:

{
 liList = document.querySelectorAll('.work li');
 var i, n = liList.length;
 for (i=0; i<n; i++)
  liList[i].addEventListener(eventName, functionName, useCapture);
}
enhzflep
  • 12,927
  • 2
  • 32
  • 51
1

People complain about the wordiness of using native DOM methods, but there's no problem a few bits of strategically-placed sugar could not help. Start off by writing it the way you'd like to:

eachClass('work', function(work) {
    eachSelector(work, 'li', function(li) {
        on(li, 'click', function() {
            eachChild(parent(li), active.remove);
            active.add(li);
        });
    });
});

Now you just have to write eachClass, eachSelector, eachChild, parent, and on, something like the following. They'll each be about 2-3 lines. You just have to write them once, and voila, you have your own little library.

function eachClass(cls, fn) {
    [].forEach.call(document.getElementsByClassName(cls), fn);
}

What is active? It's a little object we can create for setting and removing a particular class:

function makeClassSetter(cls) {
    return {
        add: function(elt) { elt.classList.add(cls); },
        remove: function(elt) {elt.classList.remove(cls); }
    };
}
var active = makeClassSetter('active');

And so on. Great programmers are constantly building little frameworks and layers and utilities and mini-languages. That's what makes their code compact, readable, less buggy, and easier to write.

1

I too was looking for a concise but useful solution to this.

This is the best approach I came up with:

tag = document.querySelectorAll('.tag');

tag.forEach(element => {
  element.addEventListener("mouseover", aFunction);
});

function aFunction(){
  this.style.background = "grey";
}

querySelectorAll is a powerful method, a foreach is always much shorter and easier to use when the use-case is there. I prefer naming the functions and adding to them, although I suppose you could easily replace that with another arrow function within the foreach if you prefer