JavaScript to capture a class value

Hi
I am completely new to this and i need to have a javascript to capture the class value when a user clicks on a button. I have 4 buttons like facebook, twitter, google plus etc., and the website view source code is as below.

	<div id="footer">
			<div>
				<div class="connect">
					<a href="http://freewebsitetemplates.com/go/facebook/" class="facebook">facebook</a>
					<a href="http://freewebsitetemplates.com/go/twitter/" class="twitter">twitter</a>

				<p>&copy; 2023 Freeeze. All Rights Reserved.</p>

If I click on facebook, i need to return a the class which is facebook in the above code to a variable and respectively for others also.

Any help please with a javascript function. I want to use the data layer concept to get this done. But unable to complete.
Kindly help.

Hah, I tried to have a go at this with my rudimentary JS, but it didnā€™t work, though Iā€™m not sure why. This is what I tried:

var links = document.querySelectorAll('.connect a');

for (var i = 0; i < links.length; i++) {
  links[i].addEventListener('click', function(e){
    console.log(links[i].className);
    e.preventDefault;
  }, false);
}

Can you help, @Paul_Wilkins @James_Hibbard ?

2 Likes

Hey Ralph,

You are almost there :slight_smile:

The reason itā€™s not working as expected is due to closure. The terminating condition of the for loop is i < links.length, so i will be 2 when the loop finishes running. The anonymous functions you are passing to addEventListener do not each have their own copy of i at the time of iteration, rather each anonymous function refers to the same copy of i which is defined in its parent scope.

That means that when any of the links is clicked, you are effectively running the code console.log(links[2].className); Obviously there is no third link (arrays being zero indexed), so it logs undefined.

So how can we fix this?

There are a lot of ways to make sure each anonymous function has its own copy of i. Some linters tell you not to declare functions inside of loops (for exactly this reason), but with the introduction of ES6ā€™s let keyword, I think that is the easiest solution here.

You also have a small typo: e.preventDefault; should be e.preventDefault();

That gives you the following, which will work as expected:

var links = document.querySelectorAll('.connect a');

for (let i = 0; i < links.length; i++) {
  links[i].addEventListener('click', function(e){
    console.log(i);
    e.preventDefault();
  }, false);
}

Further reading.

3 Likes

The less easy way, though infinitely more compatible with non-ES6 browsers, is to pass the index variable to a function, so that the returned function retains knowledge of the index variable.

This is achieved by using a technique that you might have head about before called Closure. When the logClassNameFactory function comes to a close, the returned logClassName function retains knowledge of variables that were accessible from the factory function.

function logClassNameFactory(index) {
    return function logClassName(e) {
        console.log(links[index].className);
        e.preventDefault();
    };
}
var links = document.querySelectorAll('.connect a');

for (var i = 0; i < links.length; i++) {
  links[i].addEventListener('click', logClassNameFactory(i), false);
}

Commonly the same variable name (i in this case) is used throughout for both the loop and the function, but Iā€™ve made the function variable different to help demonstrate that thereā€™s no connection between the two, other than what is passed as a function parameter.

1 Like

To be fair ES6 is a ratified standard and support for let is excellent.
https://kangax.github.io/compat-table/es6/#test-let

1 Like

Does knowing that it has only 2/3 support from browsers worldwide affect things?

1 Like

Depends who your audience is. When I start a new project, I normally offer the client support for the latest two versions of the main browsers, so in my case, probably not.

1 Like

Itā€™s those damned annoying Android and iOS (and Safari) browsers. Theyā€™re letting the side down badly.

For reference, the caniuse.com site gives global usage info for let as being 66.84%, sourcing figures from from http://kangax.github.io/compat-table/es6/#test-let

1 Like

Yup, fair dos. FWIW, your solution demonstrates the concept (problem and solution) nicely.

2 Likes

Wow, very interesting answers, guys. A lot of it over my head, unfortunately, as there are concepts/terminologies Iā€™m yet to grasp, but happy to learn and try to process it all.

Seems overly complicated to do something so simple but o well!

Iā€™ve been looking at ES6, but a lot of it is explained within the context of earlier versions, much of which Iā€™m yet to learn. So Iā€™m still bewildered by the finer points of let, const, arrow functions etc., and Iā€™m inclined to leave them alone for now until Iā€™ve got a better grasp of ES<=5.

@James_Hibbard, I sort of get the reason why the anonymous functions donā€™t get their own value of i, but I guess the safest thing is not to put functions inside a for loop. Great linked resource, by the way. Must look that over more fully. :slight_smile:

@Paul_Wilkins I also tried taking the function out of the loop as you did, but really wasnā€™t in the hunt after that. Returning a whole function is a new idea to me. Pretty cool. :slight_smile:

Now I guess the final step is to do what @kishoremcp asked for, and return the class to a variable, rather than just console.log() it. Although Iā€™m not quite sure what he wants to do with it after that. @kishoremcpā€”are you still around?

Just for fun, I tried grabbing the className and displaying it in an element on the page:

<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"></head><body>

<div class="connect">
  <a href="https://www.facebook.com" class="facebook">Facebook</a>
  <a href="https://twitter.com/" class="twitter">Twitter</a>
</div>

<div id="displayClass"></div>

<script>
function logClassNameFactory(index) {
    return function logClassName(e) {
        var className = links[index].className;
        var displayDiv = document.getElementById("displayClass");
        displayDiv.innerHTML = className;
        e.preventDefault();
    };
}
var links = document.querySelectorAll('.connect a');

for (var i = 0; i < links.length; i++) {
  links[i].addEventListener('click', logClassNameFactory(i), false);
}

</script></body></html>

Nice :slight_smile:

Iā€™d move that out of the function, as otherwise you are repeatedly querying the DOM unecessarily.

1 Like

Ah yes, good point. :slight_smile:

Just for my own understanding couldnā€™t Ralph have simply said:

 console.log(this.className);

rather than:

 console.log(links[i].className);

Or is that problematic also?

Nah, that would have worked, as this would then refer to the current link. I couldnā€™t see any reason that itā€™s problematic.

1 Like

As this thread seems to be about closures now, hereā€™s another way to pass the current index to the event listener w/o the need for a factory functionā€¦ :-)

var links = document.querySelectorAll('.connect a');

for (var i = 0; i < links.length; i++) {
  window.addEventListener.call(
    links[i],
    'click',
    function(e) {
      console.log(this.className);
      e.preventDefault();
    },
    false
  );
}

This way links[i] gets immediately tied to addEventListener() as its execution context.

Another common way would be not to use a for loop at all, but the Array forEach() method ā€“ this way you donā€™t have to worry about closures as each element gets its own callback anyway. Like

Array.from(links).forEach(function(link) {
  link.addEventListener(/* ... */);
});
3 Likes

and not even the latest standard as ES7 (or ES2016) was ratified in June this year.

There are plenty of transpilers around that can convert ES7 code back into ES5 until such time as those browsers that donā€™t support modern JavaScript cease to be used by enough people to matter.

You can then wrap all your script inside a feature test testing for something those old browsers donā€™t support so that the script simply doesnā€™t run for those browsers that donā€™t support modern JavaScript.

1 Like

Wow, cool. Many ways to skinCat() in JS. :stuck_out_tongue: Dunno how you learn how all these things work, though. The explanations of them all leave me for dead. Guess Iā€™d better just get back to practicing Chopsticks for now.

Yeah, I had that at first, but I heard that God (aka D. Crockford) doesnā€™t use this, so for fun I also like to see if I can do without it. :slight_smile:

1 Like

[quote=ā€œPaulOB, post:14, topic:236630ā€]Yeah, I had that at first, but I heard that God (aka D. Crockford) doesnā€™t use this, so for fun I also like to see if I can do without it. :slight_smile:
[/quote]

Nice one. The way to do without the this keyword in the click event handler, is to get the event target instead. For example:

var links = document.querySelectorAll('.connect a');

for (var i = 0; i < links.length; i++) {
  window.addEventListener.call(
    links[i],
    'click',
    function(evt) {
      var link = evt.target;
      console.log(link.className);
      evt.preventDefault();
    },
    false
  );
}

Although, that Array.from technique is certainly the most pleasing of all those seen here thus far:

function clickLinkHandler(evt) {
    var link = evt.target;
    console.log(link.className);
    evt.preventDefault();
}

var links = document.querySelectorAll('.connect a');
Array.from(links).forEach(function(link) {
    link.addEventListener("click", clickLinkHandler, false);
});

I can only think of one thing better than that, and that is to not place a handler on each link, but to sit and watch from the connect element instead:

function connectHandler(evt) {
    var link = evt.target;
    if (link.nodeName !== "A") {
        return;
    }
    console.log(link.className);
    evt.preventDefault();
}

var connect = document.querySelector('.connect');
connect.addEventListener("click", connectHandler, false);

Itā€™s a toss-up whether to do an early exit if itā€™s not a link, or place the code within an if statement. I prefer to go with the guard clause, as it tends to result in less indented and easier to follow code. The other option though is given below.

function connectHandler(evt) {
    var link = evt.target;
    if (link.nodeName === "A") {
        console.log(link.className);
        evt.preventDefault();
    }
}

var connect = document.querySelector('.connect');
connect.addEventListener("click", connectHandler, false);
3 Likes

Certainly is - particularly if the processing for each is the same (except for identifying the element that triggered the event so as to perform the processing on that).

2 Likes