misner3456
misner3456

Reputation: 404

Using replaceWith() method to target by ID and variable

So my jquery code works like this: when one of the default icons is clicked on and an image is loaded, it replaces that default icon that was clicked on. (There are 2 default icons with hidden file inputs). I was able to get this working by targeting the default icon's parent's classname, .border, but for some reason, I cannot target by the parent's ID name + [integer variable] - #bd1 or #bd2. Can you please help me get my code to work so I'm able to target by ID name + [integer variable]?

Upvotes: 1

Views: 132

Answers (1)

mshell_lauren
mshell_lauren

Reputation: 5236

Running your code in jsfiddle, it looks like there are two separate issues going on here:

Issue #1: Event Propagation

There is an event bubbling issue with your click events (unrelated to your jquery selectors).

When you click on the parent .border, it triggers a click on the child .imginp. When you trigger a click on the child element, the event listener on the parent is triggered again. This creates an infinite loop where the click event listener on the parent triggers a click on the child which triggers the parent click event listener etc... This results in RangeError: Maximum call stack size exceeded.

Is this the behavior you are seeing as well in your console?

This happens because of event bubbling in Javascript. First, the click on the .imginp is handled by that element, then by it's parent, then by it's grandparent and so on and so forth, until it gets to the window element. That means, that even though the target was the .imginp element, a click also happens on the .border elements. This is what gets the code into a loop state.

You can fix it by adding a listener on the child to stop the propagation of the event to the parent:

$('#inp'+a).click(function (e) {
   e.stopPropagation();
});

Issue #2: Variable Scope

This IS related to your jquery selectors. Specifically, your use of selectors using the loop variable a and the execution of the the change event callback.

A quick example of what is happening here.

for (var i = 0; i < 3; i++){
    setTimeout(function() {
        console.log(i)
    }, 100)
}

When you see this code at first glance, you might expect for it to log 0, 1,2. Instead, it will log 3,3,3. This is because by the time the timeout function runs, the value of a is 3.

This is exactly what is happening at this line in your code $('#bd'+a).replaceWith(template);. If you add a console.log here you can see this behavior in action. At the time of execution, the value of a here is not 1 or 2, but 3. This is because it is using the most recent value of a, not the value of a at the time the function was defined.

This has to do with the behavior of the event loop in javascript. When the change callback is executed and #bd + a is evalutated, the value of a is 3. We can fix this using a closure which would enable us to 'close' over the value of a with what it was at the time of the function declaration in the loop. This is a pretty good example of how a closure is used with async functions defined in for loops.

ES6 provides another option for how to prevent this issue. The keyword let enables us to have block level scoping of our variables. This means, that we can use it in our for loop to define a for each loop with block level scoping to the loop and the change callback defined in it. You would use it like so:

for (let a = 1; a < 3; a++) {...}

This has the added benefit of also fixing the way your for loop is currently written: with a as a global variable. This is typically not desirable and can have unintended consequences if you define or use a variable a elsewhere in your code.

Final Code:

for (let a = 1; a < 3; a++) {
    $("#bd"+a).click(function(e) {
        $(this).children(".imginp").click()
    });
    $('#inp'+a).click(function (e) {
       e.stopPropagation();
    });

    $('#inp'+a).on('change', function(e){
      var files = e.target.files;
      $.each(files, function(i, file){
        var reader = new FileReader();
        reader.readAsDataURL(file);
        reader.onload = function(e){
            template = 
            '<div class="border">'+
                '<input id="inp'+a+'" class="imginp" type="file" hidden>'+
            '<img class="blah" src="'+e.target.result+'">'+
            '</div>';
            $('#bd'+a).replaceWith(template);
        };
      });
    });
}

Upvotes: 1

Related Questions