tonybogdanov/jquery-always

A jQuery plugin to execute callbacks on dynamically inserted elements

Installs: 10

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 1

Forks: 0

Open Issues: 0

Language:TypeScript

Type:project

v1.1.3 2018-04-05 10:11 UTC

This package is auto-updated.

Last update: 2023-01-25 03:24:22 UTC


README

Latest Stable Version Build Status BrowserStack Status Scrutinizer Code Quality License Buy Me a Coffee

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).

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.

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:

$(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.

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:

$(function () {
    $(document.body).on('click', 'button', function () {
        // ...
    });
});

Unfortunately not all functionality can be solved this way.

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.

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.

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.

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.

Installation

The plugin supports installing via Bower, Composer and NPM with name jquery-always.

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.

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.

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.

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.

You can also use never() to detach previously attached callbacks.

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.

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.

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:

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.

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.

E.g. calling never('div') will not remove callbacks registered with always('div.some-class') even though both selectors may match the same element.

Selectors are also case-sensitive, so this: div is different than this: Div.

Selector normalization

In order to alleviate some of the restrictions imposed by the above rule, selectors will go through a selector normalization process:

  1. Selector is split into parts delimited by a comma.
  2. Any whitespace prefix and suffix is trimmed from each part.
  3. Parts are sorted alphabetically.
  4. 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.

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.

Browser Compatibility

Desktop Desktop Desktop Desktop Desktop Mobile Mobile Mobile
Chrome Firefox Safari Edge Opera Internet Explorer Android Safari
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.

*2 - Children of removed sub-trees are not reported as removals. Consider IE as not supported if you also need to detect removals.

Sponsored by