Detecting Dynamically Added Elements With jQuery
The other day I was creating a task manager UI for my 30 websites 30 days
challenge. The website allows a user to create and delete tasks. This requires a create
and delete button. There are two click
listeners: one for creating a new task,
another one for completing a task.
var createElement = function() {
var text = $("#todo-create-input").val();
var newElement = "<form class='todo-item'><p class='todo-text'>" + text +
"</p><button type='submit' class='delete-button'>Completed</button></form>"
$("main").append(newElement);
};
var displayCongratulations = function(e) {
e.preventDefault();
$('img').fadeTo( 'slow', '1');
$('header').children().remove();
$('header').append("<h1>Good job! You did something!</h1>");
this.remove();
};
$(document).ready(function(){
$("#todo-create-button").on("click", createElement);
$(".delete-button").on("click", displayCongratulations);
});
The limitation of $(document).ready()
The above code does not work because we are attempting to bind an event listener
to .delete-button
before it has been created. The element which is being created is the same element we are trying to
bind our click
listener to in our ready()
method. When developing dynamic web
applications it is common that to add new elements to the DOM that need to be
mapped to events. How do we reconcile creating new elements which will require
event listeners, with the notion that all elements are binded to event only when
the DOM loads? Usually our content is binded to an event listener right after the
DOM loads, but if we are adding new elements after the binding method has run, then our new elements will
not map to events correctly.
The solution: DOMNodeInserted event
We can bind an event to the body
element of our HTML with the
DOMNodeInserted
event. With DOMNodeInserted
, when a new
element is inserted onto html
element during
runtime the provided callback is executed. That callback can be used for binding click
listeners to events. The syntax for doing that would be as follows:
$('body').on('DOMNodeInserted', callback)
.
The above code can be refactored to include DOMNodeInserted
event:
var createElement = function() {
var text = $("#todo-create-input").val();
var newElement = "<form class='todo-item'><p class='todo-text'>" + text +
"</p><button type='submit' class='delete-button'>Completed</button></form>"
$("main").append(newElement);
};
var displayCongratulations = function(e) {
e.preventDefault();
$('img').fadeTo( 'slow', '1');
$('header').children().remove();
$('header').append("<h1>Good job! You did something!</h1>");
this.remove();
};
var bindEventToNewElement = function(e) {
var target = $(e.target).find('button');
$(target).on("click", displayCongratulations);
}
$(document).ready(function(){
$("#todo-create-button").on("click", createElement);
$("body").on('DOMNodeInserted', bindEventToNewElement);
});
The newly created elements map to the correct events and everything will work. Merry Christmas!
Limitations of DOMNodeInserted event
New solutions generally create new problems. Problems are never really solved, because every different solution requires a different implementation, which leads to different limitations. We will always have problems. We can't solve problems permanantly, but we can have solutions which lead to higher quality problems.
Consider this: Right now only one kind of element is being added to the DOM: the delete button. What if we want to add multiple elements to the DOM, each with its own unique event listener? The above solution would not work because the same callback is being executed regardless of what we add to the DOM, or where on the DOM we add it.
In the above example, what if we attatched something new to the DOM which had nothing to do with our delete functionality? It would attempt to rebind all delete buttons to their associated events.
One solution would be to bind DOMNodeInserted to the container of wherever we
are appending our dynamically generated content. Instead of listening on
our body
tag we could listen only on the container of wherever we
are adding our new content. For example, with
$("body").on('DOMNodeInserted', bindEventToNewElement);
we are
initiating bindEventToNewElement
callback whenever something is added to the
body
element. We could make this more precise by instead binding
the DOMNodeInserted
event to the container that we are attatching the new element to.
Doing so would look like:
var createElement = function() {
var text = $("#todo-create-input").val();
var newElement = "<form class='todo-item'><p class='todo-text'>" + text +
"</p><button type='submit' class='delete-button'>Completed</button></form>"
$("main").append(newElement);
};
var displayCongratulations = function(e) {
e.preventDefault();
$('img').fadeTo( 'slow', '1');
$('header').children().remove();
$('header').append("<h1>Good job! You did something!</h1>");
this.remove();
};
var bindEventToNewElement = function(e) {
var target = $(e.target).find('button');
$(target).on("click", displayCongratulations);
}
$(document).ready(function(){
$("#todo-create-button").on("click", createElement);
$(".list-of-ToDos").on('DOMNodeInserted', bindEventToNewElement);
});
Now the delete buttons will only map to events when an element is added to
.list-of-toDos
, rather than mapping when any element is added to the
body.