jQuery Always
, (*1)
This is a jQuery plugin that enables you to execute callbacks anytime an element matching a specified selector is added to the DOM (can also be used without jQuery)., (*2)
The same callbacks will also be executed once for all matching elements already found in the DOM, ensuring your callback will always run for every element matched by the selector, now and in the future, always., (*3)
The Problem
Whenever you want to execute some jQuery-based logic on an element found on the page, you would generally wait for the DOMContentLoaded
event and then target your element:, (*4)
$(function () {
$('.slider').makeSlider();
});
A huge problem with this approach arises when .slider
elements are added after the DOMContentLoaded
event has already fired - a very common situation is loading content with AJAX. When new elements are added they are not going to have the .makeSlider();
logic executed on them since the event already fired., (*5)
Event-based logic has a sort of solution, for example, if you want to listen for clicks on button
elements, using $('button').on('click', ...);
won't work for dynamically added elements for the same reasons. Due to the bubbling nature of events, though, you could simply listen for clicks on a parent element you know exists during DOMContentLoaded
, and simply filter the target like so:, (*6)
$(function () {
$(document.body).on('click', 'button', function () {
// ...
});
});
Unfortunately not all functionality can be solved this way., (*7)
Going back to the first example, you'd generally want to listen for a "element matching .slider
inserted on the page" type of event, so that you could call .makeSlider();
on the element., (*8)
The Solution
Most modern browsers (and some old ones too!) have a feature implemented called a MutationObserver which basically allows you to listen for all kinds of changes to the DOM, no matter when or where they are coming from., (*9)
This plugin makes use of mutation observers to track element insertions and removals and execute the registered callbacks for elements that match the specified selector., (*10)
This way you only need to wrap your existing logic in a simple call to the plugin to ensure it will also be executed for future additions of the element., (*11)
Installation
The plugin supports installing via Bower, Composer and NPM with name jquery-always
., (*12)
Usage
Each call to the plugin must be performed on a parent element also known as a scope. Doing so will ensure the callbacks are executed only for insertions and removals of child elements, immediate or deep., (*13)
You can always use document.body
as a scope for a "catch-it-all" solution, but if you know or only care about target elements inside a specific scope, you should always use it, for performance reasons., (*14)
The more-specific the scope, with less elements and operations performed on it and it's children, the less stress the plugin will cause to the browser., (*15)
Each registered callback will receive the matched DOM Element
as this
. If more than one element is matched by the selector, the respective callbacks will be called for each of them, thus this
will only point to exactly one DOM element., (*16)
You can also use never()
to detach previously attached callbacks., (*17)
Synopsis
Always.always()
function always (
// scope element
element: HTMLElement,
// target elements selector
selector: string,
// optional callback to be executed upon insertion
onInserted?: (this: HTMLElement) => void,
// optional callback to be executed upon removal
onRemoved?: (this: HTMLElement) => void
): void
Always.never()
function never (
// scope element
element: HTMLElement,
// target elements selector
selector?: string,
// optional reference to a previously registered "inserted" callback to be detached
onInserted?: (this: HTMLElement) => void,
// optional reference to a previously registered "removed" callback to be detached
onRemoved?: (this: HTMLElement) => void
): void
Attaching an "inserted" callback
Use the following example to add a callback to be executed when elements matching .element
are inserted inside #scope
., (*18)
Always.always(scope, '.element', function () {
doStuff(this);
});
Attaching a "removed" callback
Use the following example to add a callback to be executed when elements matching .element
are removed from #scope
., (*19)
Always.always(scope, '.element', null, function () {
undoStuff(this);
});
...or both simultaneously
Always.always(scope, '.element', function () {
doStuff(this);
}, function () {
undoStuff(this);
});
Removing all attached callbacks
Always.never(scope);
Removing attached callbacks matching a selector
Always.never(scope, '.element');
Removing a specific attached callback
Always.never(scope, '.element', onInsertedCallbackReference, onRemovedCallbackReference);
jQuery bindings
The plugin can also be used as a jQuery plugin with a nearly identical syntax:, (*20)
var added = function () {
doStuffWhenAdded(this);
};
var removed = function () {
doStuffWhenRemoved(this);
};
$('#scope')
.always('.element', added, removed)
.never('.element', added, removed);
Notes
Here are a couple of a-ha moments to keep in mind when working with the plugin., (*21)
The selector in never()
When calling never()
you must specify the same selector that was used when adding the callback(s) you want to remove since callbacks are grouped by the string selectors themselves, not what they might resolve to., (*22)
E.g. calling never('div')
will not remove callbacks registered with always('div.some-class')
even though both selectors may match the same element., (*23)
Selectors are also case-sensitive, so this: div
is different than this: Div
., (*24)
Selector normalization
In order to alleviate some of the restrictions imposed by the above rule, selectors will go through a selector normalization process:, (*25)
- Selector is split into parts delimited by a comma.
- Any whitespace prefix and suffix is trimmed from each part.
- Parts are sorted alphabetically.
- Parts are joined back together using a comma as delimiter.
Normalization will ensure selectors like a, b
and a,b
and even b,a
are treated as the same thing., (*26)
this
in onRemoved
The this
variable available inside the onRemoved
callback will point to the element that was just removed from the DOM. Thanks to the magic of mutation observers, the original DOM tree will continue to exist at this point, meaning you can still execute jQuery operations on this
, like find()
-ing child elements etc. Even so, you should keep in mind that at this point the element has been detached from the DOM, so any operations you perform on it will not be reflected in the DOM., (*27)
Browser Compatibility
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Yes |
Yes |
6 |
14 |
Yes (*1) |
11 (*2) |
4.4 |
6 |
*1 - Opera is not covered in tests since the web driver only supports up to version 12.16 which is outdated. Manually running the tests on the latest version passes., (*28)
*2 - Children of removed sub-trees are not reported as removals. Consider IE as not supported if you also need to detect removals., (*29)
Sponsored by, (*30)
, (*31)