6

If I click on the first "Edit" I get a console.log('click happend') But if I add a one of these boxes via javascript (click on "Add box") and then the Edit click from this new box does not work. I know it's because the javascript run when the element was not there and that's why there is no click event listener. I also know with jQuery I could do like so:

$('body').on('click', '.edit', function(){ // do whatever };

and that would work.

But how can I do this with plain Javascript? I couldn't find any helpful resource. Created a simple example which I would like to be working. What is the best way to solve this?

So the problem is: If you add a box and then click on "Edit" nothing happens.

var XXX = {};
XXX.ClickMe = function(element){
    this.element = element;
    
    onClick = function() {
        console.log('click happend');
    };
    
    this.element.addEventListener('click', onClick.bind(this));
};

[...document.querySelectorAll('.edit')].forEach(
    function (element, index) {
        new XXX.ClickMe(element);
    }
);


XXX.PrototypeTemplate = function(element) {
    this.element = element;
    var tmpl = this.element.getAttribute('data-prototype');

    addBox = function() {
        this.element.insertAdjacentHTML('beforebegin', tmpl);
    };

    this.element.addEventListener('click', addBox.bind(this));
};


[...document.querySelectorAll('[data-prototype]')].forEach(
    function (element, index) {
        new XXX.PrototypeTemplate(element);
    }
);
[data-prototype] {
  cursor: pointer;
}
<div class="box"><a class="edit" href="#">Edit</a></div>

<span data-prototype="<div class=&quot;box&quot;><a class=&quot;edit&quot; href=&quot;#&quot;>Edit</a></div>">Add box</span>

JSFiddle here

This Q/A is useful information but it does not answer my question on how to solve the problem. Like how can I invoke the eventListener(s) like new XXX.ClickMe(element); for those elements inserted dynamically into DOM?

caramba
  • 19,727
  • 15
  • 78
  • 118
  • Possible duplicate of [What is DOM Event delegation?](https://stackoverflow.com/questions/1687296/what-is-dom-event-delegation) – Heretic Monkey Jul 31 '17 at 14:17

4 Answers4

2

You can do something like this...

document.addEventListener('click',function(e){
    if(e.target && e.target.className.split(" ")[0]== 'edit'){
     new XXX.ClickMe(e.target);}
 })

var XXX = {};
XXX.ClickMe = function(element) {
  this.element = element;


  this.element.addEventListener('click', onClick.bind(this));
};



XXX.PrototypeTemplate = function(element) {
  this.element = element;
  var tmpl = this.element.getAttribute('data-prototype');

  addBox = function() {
    this.element.insertAdjacentHTML('beforebegin', tmpl);
  };

  this.element.addEventListener('click', addBox.bind(this));
};


[...document.querySelectorAll('[data-prototype]')].forEach(
  function(element, index) {
    new XXX.PrototypeTemplate(element);
  }
);


document.addEventListener('click', function(e) {
  if (e.target && e.target.className.split(" ")[0] == 'edit') {
    console.log('click happend');
  }
})
[data-prototype] {
  cursor: pointer;
}
<div class="box"><a class="edit" href="#">Edit</a></div>

<span data-prototype="<div class=&quot;box&quot;><a class=&quot;edit&quot; href=&quot;#&quot;>Edit</a></div>">Add box</span>
Tushar
  • 11,306
  • 1
  • 21
  • 41
  • Assuming there is a missing space `me.split(" ")[0][HERE]== 'edit'` I don't like [this fiddle](https://jsfiddle.net/noeuesps/1/). Sometimes I get no click, sometimes two. Just doesn't feels good and clean – caramba Jul 31 '17 at 14:13
  • yes because you need to tweak your code. If you use this code with the other one, then there would be multiple binding of clicks – Tushar Jul 31 '17 at 14:15
  • @caramba I have also updated my answer with running Code. – Tushar Jul 31 '17 at 14:21
  • Thank you but I still struggle on how to call or invoke `new XXX.ClickMe(element);` – caramba Jul 31 '17 at 14:41
2

Do it like jQuery: have a parent element that controls the event delegation. In the following, I use document.body as the parent:

document.body.addEventListener('click', e => {
  if (e.target.matches('.edit')) {
    // do whatever 
  }
});

Working example:

var XXX = {};
XXX.PrototypeTemplate = function(element) {
  this.element = element;
  var tmpl = this.element.getAttribute('data-prototype');
  addBox = function() {
    this.element.insertAdjacentHTML('beforebegin', tmpl);
  };
  this.element.addEventListener('click', addBox.bind(this));
};


new XXX.PrototypeTemplate(document.querySelector('[data-prototype]'));

document.body.addEventListener('click', e => {
  if (e.target.matches('.edit')) {
    // do whatever
    console.log('click happend');
  }
});
[data-prototype] {
  cursor: pointer;
}
<div class="box"><a class="edit" href="#">Edit</a></div>
<span data-prototype="<div class=&quot;box&quot;><a class=&quot;edit&quot; href=&quot;#&quot;>Edit</a></div>">Add box</span>

Take a look at what MDN says about Element.prototype.matches.

PeterMader
  • 5,739
  • 1
  • 16
  • 27
  • I'd like to know that, too. It doesn't work well with your `new XXX.ClickMe(element);` design, but it should work with a few changes. – PeterMader Jul 31 '17 at 14:20
  • Thanks for helping now my problem is if the Object `XXX.ClickMe()` has different properties they are all lost. So I would like to kind of invoke or instantiate `new XXX.ClickMe(element);` – caramba Jul 31 '17 at 14:44
  • You can't use event delegation and `XXX.ClickMe` at the same time. You could of course have an array of `XXX.ClickMe`s and add a new one whenever an element is clicked that isn't in the array. But I recommend you create a new `XXX.ClickMe` in `addBox`, without event delegation. – PeterMader Jul 31 '17 at 14:49
  • That sounds like what I want. Just not sure on how to do it. Also there are lots of links inside one box so it would be awesome to have a way to "run" the javascript again or on a set of given functions after a new box is inserted... – caramba Jul 31 '17 at 14:54
2

Here's a method that mimics $('body').on('click', '.edit', function () { ... }):

document.body.addEventListener('click', function (event) {
  if (event.target.classList.contains('edit')) {
    ...
  }
})

Working that into your example (which I'll modify a little):

var XXX = {
  refs: new WeakMap(),
  ClickMe: class {
    static get (element) {
      // if no instance created
      if (!XXX.refs.has(element)) {
        console.log('created instance')
        // create instance
        XXX.refs.set(element, new XXX.ClickMe(element))
      } else {
        console.log('using cached instance')
      }
      
      // return weakly referenced instance
      return XXX.refs.get(element)
    }

    constructor (element) {
      this.element = element
    }
    
    onClick (event) {
      console.log('click happened')
    }
  },
  PrototypeTemplate: class {
    constructor (element) {
      this.element = element
      
      var templateSelector = this.element.getAttribute('data-template')
      var templateElement = document.querySelector(templateSelector)
      // use .content.clone() to access copy fragment inside of <template>
      // using template API properly, but .innerHTML would be more compatible
      this.template = templateElement.innerHTML
      
      this.element.addEventListener('click', this.addBox.bind(this))
    }
    
    addBox () {
      this.element.insertAdjacentHTML('beforeBegin', this.template, this.element)
    }
  }
}

Array.from(document.querySelectorAll('[data-template]')).forEach(function (element) {
  // just insert the first one here
  new XXX.PrototypeTemplate(element).addBox()
})

// event delegation instead of individual ClickMe() event listeners
document.body.addEventListener('click', function (event) {
  if (event.target.classList.contains('edit')) {
    console.log('delegated click')
    // get ClickMe() instance for element, and create one if necessary
    // then call its onClick() method using delegation
    XXX.ClickMe.get(event.target).onClick(event)
  }
})
[data-template] {
  cursor: pointer;
}

/* compatibility */
template {
  display: none;
}
<span data-template="#box-template">Add box</span>

<template id="box-template">
  <div class="box">
    <a class="edit" href="#">Edit</a>
  </div>
</template>

This uses WeakMap() to hold weak references to each instance of ClickMe(), which allows the event delegation to efficiently delegate by only initializing one instance for each .edit element, and then referencing the already-created instance on future delegated clicks through the static method ClickMe.get(element).

The weak references allow instances of ClickMe() to be garbage collected if its element key is ever removed from the DOM and falls out-of-scope.

Patrick Roberts
  • 40,065
  • 5
  • 74
  • 116
0

Thanks to all answering on the question, it is all helpfull information. What about the following:

Wrap all the functions needed inside a XXX.InitializeAllFunctions = function(wrap) {} and pass the document as the wrap on first page load. So it behaves like it did before. When inserting new DOM Elements just pass those also to this function before inserting into DOM. Works like a charm:

var XXX = {};

XXX.ClickMe = function(element){
    this.element = element;
    onClick = function() {
        console.log('click happend');
    };
    this.element.addEventListener('click', onClick.bind(this));
};

XXX.PrototypeTemplate = function(element) {
    this.element = element;

    addBox = function() {
        var tmpl = this.element.getAttribute('data-prototype');
        var html = new DOMParser().parseFromString(tmpl, 'text/html');

        XXX.InitializeAllFunctions(html);  // Initialize here on all new HTML
                                           // before inserting into DOM

        this.element.parentNode.insertBefore(
            html.body.childNodes[0],
            this.element
        );
    };

    this.element.addEventListener('click', addBox.bind(this));
};

XXX.InitializeAllFunctions = function(wrap) {

    var wrap = wrap == null ? document : wrap;

    [...wrap.querySelectorAll('[data-prototype]')].forEach(
        function (element, index) {
            new XXX.PrototypeTemplate(element);
        }
    );

    [...wrap.querySelectorAll('.edit')].forEach(
        function (element, index) {
            new XXX.ClickMe(element);
        }
    );
};

XXX.InitializeAllFunctions(document);
[data-prototype] {
  cursor: pointer;
}
<div class="box"><a class="edit" href="#">Edit</a></div>
<span data-prototype="<div class=&quot;box&quot;><a class=&quot;edit&quot; href=&quot;#&quot;>Edit</a></div>">Add box</span>
caramba
  • 19,727
  • 15
  • 78
  • 118
  • Well, this doesn't use event delegation like you requested. Each `.edit` contains its own `this.element.addEventListener('click', onClick.bind(this))`, so you're generating several scoped functions on the fly. In contrast, my answer uses event delegation to create instances of `ClickMe` when necessary, but does not bind an event listener for each one, but rather invokes its member method from a single delegated event listener. – Patrick Roberts Aug 02 '17 at 14:51
  • @PatrickRoberts thank you for checking back and the comment! Much appreciated! – caramba Aug 02 '17 at 14:53