0

Kind of embarassed about this, but lets say I have this group of, 'child' elements within a parent element called iBlock, and within a function I'm adding an 'onclick' event to each child, like so...

someFunction(iBlock) {
    var el, iTotal = iBlock.children.length;
     for (var i= 0; i < iTotal; i++) {
             el=iBlock.children[i];
             el.onclick=function(event){myHandler(this, i)};
             }
    }

So given an onclick handler like this...

function myHandler(elem, n) {
     alert("number is: " +  n);
    }

It was a little surprising that regardless of which child element I clicked on, the handler in this example would always display the one less than the total number of children. But I understand that the total - 1 was highest number the variable 'i' got to before the loop ended, and the handler isn't going to pass anything but the value of the variable at the time the event occurs. But understanding that doesn't help me answer these 2 questions...

  1. If I really wanted the second argument in the onclick function to pass the value 'i' actually was at the time the onclick handler was attached, what could I pass instead of the raw 'i' variable?
  2. Since 'i' was declared by a 'var' keyword within the function that added the handlers, why did it still even have a value (in this case the total - 1), after installing function had returned? I would have been less surprised if the handler displayed 'undefined'. I realize this is a 'scope of variables' question, but I'm sure its related.

On the first question, I've tried passing: i+'x', just to turn it into a string, hoping it would then generate a unique instance. That doesn't work either... and element 'clicked' on just triggers the handler shown to display '4x'. I've also tried creating a second variable, as a string, then extracting the numerical portion, and passing the final 'value' from the new variable to the onclick handler, like this...

someFunction(iBlock) {
        var el, iTotal = iBlock.children.length;
         for (var i= 0; i < iTotal; i++) {
            el=iBlock.children[i];
            var n= i+'x';  // make 'i' into a new longer string variable
            var r = /\d+/;    // regex, to capturei digits
            n = n.match(r); // convert the string to only contain the digits
            el.onclick=function(event){myHandler(this, n)};
            }
        }

No change. the handler still displays the total-1 on any element I click.

I have looked at similar posts on this subject such as Extract a number from a string (JavaScript), and I still don't understand a remedy to my first question.

PS (I've not tried creating the variable with 'let' instead of 'var', because I'm pretty committed to supporting certain older browsers). EDIT: I'd like to include IE-8 among those 'old browsers' if possible. I realize I can't always do so, but in this case I'd like to.

EDIT: I have found an interesting workaround, which is to add my own variable to each element, in the loop where I add handlers, like this...

someFunction(iBlock) {
    var el, iTotal = iBlock.children.length;
     for (var i= 0; i < iTotal; i++) {
             el=iBlock.children[i];
             el.onclick=function(event){myHandler(this, i)};
             el.myIndex=i;
             }
    }

Then of course in my handler, where I'm passing 'this' anyway, I can properly recover the desired value of 'i' by examining 'this.myIndex. I've heard this is dangerous, because it might clash with a future DOM property.

But that seems 'kludge', so I'm still interested in the answers!

Randy
  • 301
  • 2
  • 9
  • 2
    n is not redeclared and your event handler basically points to the same n variable so it would get the last value assigned at r time of execution. Try this: `el.onclick=(function(localn){return function(event){myHandler(this, localn)};})(n);` – gp. Sep 25 '20 at 18:03
  • it looks to me like you need to capture that var using closures. Have your tried that? – Nikola Mitic Sep 25 '20 at 18:06
  • In the function, you are declaring a variable called `el`, you don't assign it any value, yet you use `el.onclick=....` Shouldn't you be assigning each iBlock child to the el variable first? – ATD Sep 25 '20 at 18:08
  • also el is not defined – Nikola Mitic Sep 25 '20 at 18:08
  • `el = iBlock.children[i] `or something? – Nikola Mitic Sep 25 '20 at 18:09
  • @NikolaMitic - yes... sorry... I left that out the 'el' declaration but have edited the question. Also, I have tried closures, and actually the original function as well as the handlers are already both closures in a larger function. – Randy Sep 25 '20 at 18:28
  • @ATD - yes... sorry... question edited. – Randy Sep 25 '20 at 18:28
  • @Randy - And, the code still doesn't work as required? – ATD Sep 25 '20 at 18:32
  • @ATD - no, the edited code shows what I'm already doing. I just forgot to add that line when I created the simplified example (my bad for not testing it). So the problem remains. In my edit, I did place a work-around, which I'll use for now. But like many 'work-arounds', it is probably not the ideal. But maybe it is? – Randy Sep 25 '20 at 18:38

4 Answers4

2

You need to create a closure using IIFE to capture iterator var.

More about closures

More about IIFE

function handleClick(element, n) {
    alert("number is: " +  n);
}

function iterateOverChildNOde(parentNode) {
  var el = parentNode.children
  var iTotal = parentNode.children.length;
 
  
    for (var i= 0; i < iTotal; i++) {

    (function(iterator){
      el[i].onclick=function(event){handleClick(el[iterator], iterator)};
      })(i)
  }
}

iterateOverChildNOde(document.getElementById('parent'));
<div id="parent">
  <div class="child">C</div>
  <div class="child">L</div>
  <div class="child">I</div>
  <div class="child">I</div>
  <div class="child">C</div>
  <div class="child">L</div>
</div>

Also, you need to be aware that this is a bad practise. You should just attach event handler to parent element and then use e.target.

More about event delegation

  function handleClick(element, n) {
        alert("number is: " +  n);
    }

document.getElementById('parent').onclick=function(e){handleClick(e.target, Array.prototype.indexOf.call(this.children, e.target))}
    <div id="parent">
      <div class="child">C</div>
      <div class="child">L</div>
      <div class="child">I</div>
      <div class="child">I</div>
      <div class="child">C</div>
      <div class="child">L</div>
    </div>
Nikola Mitic
  • 821
  • 1
  • 7
  • 20
  • One issue is that as I mentioned, I'm committed to supporting older browsers, and I should have clarified that I like to incude IE-8 when possible. It looks like IFFE began working partially in IE9. But also,formy own understanding, I've read, IEFE functions are invoked as they are declared. Does that men they are 'called' as soon as they are declared? If so, when would I want (for example) an 'onclick' handler to run before I actually clicked on the element? – Randy Sep 25 '20 at 19:04
  • I am NOT aware that IIFE will not work in IE-8, IIFE is a part of JS language so I think it should work. You are right IIFE will run as soon as they are declared, nothing wrong with that. onClick handler is a handler or better say callback function, it will only when browser dispatch click event, so when user clicks on it. – Nikola Mitic Sep 25 '20 at 19:08
  • Is there anything stopping you from not using for loop and not attaching all of those event listeners but just use e.target like I showed you in second example? – Nikola Mitic Sep 25 '20 at 19:09
  • I did find the IE-9+support on an MDM web docs chart, but to get to your question, this is part of a navigation menu system on a somewhat "legacy" site I'm renovating. Because a navigation system is likely to be updated often, I'm creating it in such a way that templates and items in arrays are converted to appropriate icons, text links, and actions. As you can imagine, adding to an array is way less cumbersome & error prone than updating hard code. I know there's no way to update a whole legacy site without losing SOME browsers, but the Navigation system must work. Thanks – Randy Sep 25 '20 at 19:47
  • 1
    I see, then IIFE (first example I posted) will work for you because they are not part of web api, but rather the language itself. I even believe that event delegation (second example) will work on IE-8, but I am not sure – Nikola Mitic Sep 26 '20 at 09:23
  • I see that it works, and thanks for the tip on delegating. The delegation looks a little convoluted, but good to know there is a way. I may stick with the last method I came up with (adding my own index property to the element) as it seems simple and old browser compatible. I'm going to have to use your example to make a test page to see if it works in IE-8, because apparently I can't even navigate to a stackOverflow page on that browser. But I agree, its straight java script so it should work. I'll mark you're answer accepted. – Randy Sep 28 '20 at 15:16
  • @Randy great, glad that I helped! Regarding the event delegation, yes I agree that it is a little bit convoluted but regarding of how many event listeners you want to attach and how many dom manipulation you wanna have delegation can be a must. Do not forget that every time a node is removed from the DOM you will need to manually clean the event handlers in order not to introduce memory leaks. – Nikola Mitic Sep 28 '20 at 20:16
  • Well that's interesting. I actually do clean up event handlers when i need to re-format or re-build a list of clickable items, but I've been told that, at least as far as "on' events go, attached like I'm doing, those handlers are removed by a background garbage collection process once the elements are destroyed, just by something like parent.innerHTML="" . Paranoid as I am, I always set my handlers to 'null' in a similar loop before doing so, but I HAVE been told repeatedly this is overkill. I'll look for the thread I started on that and post it in a comment later. – Randy Sep 29 '20 at 17:18
2

As stated in comments, the issue is that your inline click handler function is referencing the variable i in the closure, which gets mutated while looping. Each handler references that loop variable, so by the time the loop finishes, i is set to iTotal, and all handlers reference that same value.

This might be a good time to use the bind method of functions. What that does is return a new version of a function with the this context established and parameters pre-populated.

for (var i = 0; i < iTotal; i++) {
  el = iBlock.children[i];
  el.addEventListener('click', myHandler.bind(this, this, i));
}

Each el receives a unique function with the first two parameters pre-populated.

Jacob
  • 72,750
  • 22
  • 137
  • 214
  • @Nikola's approach of attaching the handler to the parent is the better option, but I'm posting so you can see the bit about `bind` if you do need the individual listener functions and don't want to use IIFEs. – Jacob Sep 25 '20 at 18:46
  • This looks interesting and I will upvote it. However I will have to update my question to indicate that when I said I want to support older browsers, I'd like that to included IE-8, if possible, and .bind wont work in that browser. I'm starting to think my 'work-around', in the edit of my question, might be the est solution under those constraints. – Randy Sep 25 '20 at 19:08
  • Ah, with no `bind` you'll have to go with the IIFE or event delegation. – Jacob Sep 25 '20 at 19:21
0

el is not being assigned to anything. Try:

someFunction(iBlock) {
    var iTotal = iBlock.children.length;
    for (var i= 0; i < iTotal; i++) {
        iBlock.children[i].onclick=function(event){myHandler(this, i)};
    }
}
ATD
  • 1,199
  • 1
  • 2
  • 8
  • Sorry... that was pointed out in a comment and I've edited it to show how I assign 'el' to the currently indexed child. But either my edit or yours doesn't fix the fact that in the handler, 'i' will always display the total-1 – Randy Sep 25 '20 at 18:33
  • 1
    @Randy you need to create a closure to capture that iterator value, check my answer. You would not need this if you could just use let. Because let is not hoisted, it is being lexically scooped so this would not be an issue – Nikola Mitic Sep 25 '20 at 18:35
  • @NikolaMitic Yes, the first time I realized 'let' didn't work in IE-8, I was pretty upset. But anyway, now having done some research, I find that my discovered solution of adding a property to the element is more widespread than I thought. It is available in the event handler as a property of 'this', and even though it was created in the loop just by assigning it to the 'i' variable, the property reflects the value 'i' had at the time it was added, which is perfect. The only caveat I see is I need to avoid adding a property name that might clash with a future standard property. – Randy Sep 25 '20 at 20:48
0

After careful consideration of all the answers offered, I'm going to stick with the solution I discovered and posted, in the final edit of my OP. Specifically, It is assigning a new attribute to each element as I'm adding my events...

someFunction(iBlock) {
    var el, iTotal = iBlock.children.length;
     for (var i= 0; i < iTotal; i++) {
             el=iBlock.children[i];
             el.onclick=function(event){myHandler(this)};
             el.myIndex=i;
             }
    }

Here, I'm no longer even attempting to send the value of 'i' to the handler as a second arg, due the to problems noted. Yet for whatever reason, when the added element 'myIndex' is assigned to 'i', it retains the identifying value at the time of assignment. So when the handler is called it can be accessed via 'this.myIndex'. I don't fully understand why doing it this way causes 'i' to be passed by value rather than a reference / instance of itself, but it works.

No one has offered any reason NOT to do this, and it seems the least complex in terms of added code. I believe the only risk is choosing a name that clashes with some future 'standard' attribute. Maybe this is why I see suggestions tp call add on sttributes something like "data-myindex". But either way, it seems to work all IE platforms I've tried back to IE8, also Edge, Chrome and Firefox back to the last ones distributed for Windows-XP through the latest on Win-10. It works on Mobil Safari back to IOS-7, and all the android devices I'm able to test with. Still open to suggestions, but sometimes, as they say, "the simplest solution is the best"

Randy
  • 301
  • 2
  • 9
  • 1
    I think you are under misconception that closures and event delegation or using bind() is somehow more complicated then your solution. Here is three main reasons why you should NOT go with the above solution. 1. It is not simple 2. It will add unnecessary even handlers 3. You do not understand how it works (which is the reason why it is not simple) The code you write will be read by someone else. Closures, event delegation or bind() are all common javascript techniques and are easily undesirable. Ofc decision is yours. Wish you a nice day! :) – Nikola Mitic Sep 30 '20 at 11:27
  • on the side note, "this" is referencing the element on which event handler is attached on. All of our solution are based on the fact that you need index to be accessible in event handlers. But what you actually needed was element on which the handler is attached. So you question should have been framed differently :)If you care about it – Nikola Mitic Sep 30 '20 at 11:29
  • e.target is what "this" is to you. So you can save yourself from for loops and not needed events listeners if you use event delegation. – Nikola Mitic Sep 30 '20 at 14:27
  • @NikolaMitic Well my title did express my issue. Yes, my event handlers need 'this' for several reasons. One was to access various links or functions from a configuration array. These arrays control properties of my elements as they are built, & specify what the element will do when clicked. All I lacked was the 0 based number identifying the element, as an index into the array. To your points, #1 I disagree. #2 I do agree, but the number of items is small in my case, #3 I DO understand. You are finding the index based on its position within the array of the parents children. Appreciated! – Randy Sep 30 '20 at 14:50
  • @NikolaMitic Please don't think for a minute I didn't appreciate or learn from what you explained. I do use closures all the time, but yes... event delegation is something I never considered, and certainly will in the future. – Randy Sep 30 '20 at 14:53
  • all good mate :) most important is that you solved the issue ;) – Nikola Mitic Sep 30 '20 at 21:45