Saturday, January 31, 2015

Making jQuery a bit more Angular

One of my favorite features of AngularJS is the use of HTML attributes to apply controllers and directives directly to your DOM elements. Why is this so useful?

  • It is intuitive for developers to discover what code is being applied to elements.
  • It enables generic registration, removing boiler plate document ready methods.
  • It provides hierarchical scope, encouraging single responsibility controls.

jQuery plugins are already designed to be applied to collections of elements, so let's just add the ability to dynamically apply plugins via HTML attributes! This is how we can make jQuery a bit more Angular.

Sample Script

Our sample jQuery plugin is super simple; it just makes an element fade in and out continuously. This is a very simple behavior, but the point is that it is just a jQuery plugin!

(function ($) {
    $.fn.blink = function() {
        var $el = this;
        setInterval(blinkEl, 1000);
 
        function blinkEl() {
            $el.fadeToggle();
        }
    };
})(jQuery);

The "Old" jQuery Way

Notice that this wire up requires us to use the document ready method and an Id selector to find the target element and apply our behavior. This is the "old" jQuery way of doing wireups!

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>The Old Way</title>
        <script type="text/javascript" src="Scripts/jquery-1.10.2.js">
        </script>
        <script type="text/javascript" src="Scripts/Behaviors/blink.js">
        </script>
        <script type="text/javascript">
            $(function () {
                $('#blinkMe').blink();
            });
        </script>
    </head>
    <body>
        <div id="blinkMe">Hello world!</div>
    </body>
</html>

The "New" Angular Way

Notice that this wire up requires no document ready registrations, no global selectors, and no initialization code what so ever! Instead all we have to do is add a data-behavior attribute to our target element. This is the "new" Angular way of doing wireups.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>The New Way</title>
        <script type="text/javascript" src="Scripts/jquery-1.10.2.js">
        </script>
        <script type="text/javascript" src="Scripts/behaviors.js">
        </script>
        <script type="text/javascript" src="Scripts/Behaviors/blink.js">
        </script>
    </head>
    <body>
        <div data-behavior="blink">Hello world!</div>
    </body>
</html>

Behaviors.js Implemenation

How can I use this?
The only thing that you have to do in order to start using this pattern is simply include the following JavaScript file on your site.

How does it work?
On document ready it will scan your DOM for data-behavior attributes, parse their content, and then apply those plugins on the elements. It will cache a flag in jQuery's data store stating that the behavior has been initialized, so you will never have to worry about a plugin being applied multiple times.

How do I apply additional behaviors after document ready?
If you add new or edit elements then you will need to call $.behaviors() to apply updates. There are ways of auto applying updates, but they are much more complex and can sometimes lead to performance problems. If you would like me to write another article about that, please ask in the comments!

(function ($) {
    // Key used by local data store to cache list of applied behaviros
    // for a given element.
    var dataKey = 'initalized-behaviors';
 
    // A method to apply behaviors for the entire document. This is
    // invoked by document ready. This is safe to call multiple times.
    $.behaviors = globalBehaviors;
 
    // A method to apply behaviors to an specific element. This is also
    // safe to call multiple times.
    $.fn.behaviors = elementBehaviors;
 
    $(globalBehaviors);
 
    function  globalBehaviors() {
        var $els = $('[data-behavior]');
        $els.behaviors();
    }
 
    function  elementBehaviors() {
        for (var i = 0; i < this.length; i++) {
            var el = this[i];
            singleElementBehaviors(el);
        }
        return this;
    }
 
    function  singleElementBehaviors(el) {
        var $el = $(el),
            behaviorAttr = $el.attr('data-behavior') || '',
            behaviorList = behaviorAttr.split(',');
 
        for (var i = 0; i < behaviorList.length; i++) {
            // Get the name of the jQuery plugin / behavior, and load the
            // previously applied list of behaviors from jQuery's data store.
            var behaviorName = behaviorList[i].trim(),
                initalizedBehaviors = $el.data(dataKey) || [];
 
            // Only continue if the specified jQuery plugin exists.
            if (typeof $el[behaviorName] !== 'function') {
                tryLog('Behavior not found: ' + behaviorName);
                continue;
            }
 
            // If the behavior has already been applied, then do not apply
            // it again.
            if (initalizedBehaviors.indexOf(behaviorName) !== -1) {
                continue;
            }
 
            // Save the name of the behavior in data storage so that we
            // we will know that it has already been applied.
            initalizedBehaviors.push(behaviorName);
            $el.data(dataKey, initalizedBehaviors);
 
            // Finally, apply the jQuery plugin to our element.
            $el[behaviorName]();
        }
    }
 
    function  tryLog(message) {
        // Only write things to the console if it exists.
        if (console && typeof console.log === 'function ') {
            console.log(message);
        }
    }
 
})(jQuery);

Enjoy,
Tom

1 comment:

Real Time Web Analytics