Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at the jQuery Conference 2010 (Boston, MA) with: Alex Sexton
Ben Nadel at the jQuery Conference 2010 (Boston, MA) with: Alex Sexton@SlexAxton )

Creating jqLite Plugins In AngularJS

By Ben Nadel on

Yesterday, I looked at how jQuery is integrated with AngularJS and, what that means for developers who want to leverage jQuery within their AngularJS directives. This got me thinking about plugins. Many of us are used to writing jQuery plugins; but, what if we don't want to include jQuery? Can we write JQLite plugins? It doesn't mention anything in the AngularJS documentation; but, it turns out, writing a JQLite plugin is very similar to writing a jQuery plugin.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

When using jQuery, we [typically] write plugins by adding functions to the jQuery prototype, which is more widely known as the "fn" object. If we're not using jQuery - if we're only using jqLite - the principle is the same. If we want to write jqLite plugins, we have to add them to the jqLite prototype.

AngularJS doesn't provide a short-hand notation, like "fn", for the jqLite prototype. As such, we have to explicitly reference the jqLite prototype as exposed through angular.element:

angular.element.prototype

Once we have this, we can write plugins in the same way that we used to, for jQuery. Within each prototype method, "this" refers to the current jqLite collection. If you return a collection from a plugin, you have to wrap that collection in a new jqLite object so we don't break method chaining.

To experiment with this idea, I decided to refactor my demo from yesterday using nothing by jqLite. Since yesterday's demo used jQuery plugins like .is(), .filter(), and .appendTo(), it means that I would have to recreate this plugins as jqLite plugins.

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Creating jqLite Plugins In AngularJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body>
  •  
  • <h1>
  • Creating jqLite Plugins In AngularJS
  • </h1>
  •  
  • <p>
  • <em>Start clicking, bro.</em>
  • </p>
  •  
  • <ul bn-demo>
  • <!-- Dynamically populated. -->
  • </ul>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.3.6.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I add an element to the point of a click and then randomly select one of the
  • // existing elements in the list.
  • app.directive(
  • "bnDemo",
  • function() {
  •  
  • // Return the directive configuration.
  • return({
  • link: link,
  • restrict: "A"
  • });
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I bind the JavaScript events to the local scope.
  • function link( scope, element, attributes ) {
  •  
  • element.on(
  • "click",
  • function handleClickEvent( event ) {
  •  
  • // If the user clicked on an existing LI, then don't change
  • // the contents of the container, just select the target.
  • if ( angular.element( event.target ).is( "li" ) ) {
  •  
  • // Select the target element.
  • element.children()
  • .removeClass( "selected" )
  • .filter( event.target )
  • .addClass( "selected" )
  • ;
  •  
  • return;
  •  
  • }
  •  
  • // Create a new element at the click position.
  • angular.element( "<li></li>" )
  • .xyo( event.pageX, event.pageY, -25 )
  • .appendTo( element )
  • ;
  •  
  • // Select a random element in the list.
  • element.children()
  • .removeClass( "selected" )
  • .random()
  • .addClass( "selected" )
  • ;
  •  
  • }
  • );
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Define jqLite plugins. Without jQuery, we can still define custom AngularJS
  • // JQLite plugins using the same approach - defining methods on the "prototype"
  • // of JQLite. Remember, in jQuery, the ".fn" property is just a convenient
  • // reference to the jQuery.prototype object.
  • app.run(
  • function createJQLitePlugins() {
  •  
  • // Get a short-hand reference to the element method.
  • var JQLite = angular.element;
  •  
  •  
  • // I safely compare the new nodes by wrapping them in JQLite containers
  • // first. This way, they may or may not be raw DOM node references.
  • function safeEquals( a, b ) {
  •  
  • return( JQLite( a )[ 0 ] === JQLite( b )[ 0 ] );
  •  
  • }
  •  
  •  
  • // I append the current collection to the target element.
  • JQLite.prototype.appendTo = function( target ) {
  •  
  • JQLite( target ).append( this );
  •  
  • return( this );
  •  
  • };
  •  
  •  
  • // I filter the current collection using the given operator. Currently
  • // supports DOM reference of function (which must return true).
  • JQLite.prototype.filter = function( operator ) {
  •  
  • // If the operator is not a function, normalize it so that it is
  • // a function that returns true if the current element matches the
  • // given target.
  • if ( ! angular.isFunction( operator ) ) {
  •  
  • var target = JQLite( operator );
  •  
  • operator = function compareNode( node ) {
  •  
  • return( safeEquals( node, target ) );
  •  
  • };
  •  
  • }
  •  
  • var subset = [];
  •  
  • angular.forEach(
  • this,
  • function checkNodeMatch( value ) {
  •  
  • if ( operator( value ) === true ) {
  •  
  • subset.push( value );
  •  
  • }
  •  
  • }
  • );
  •  
  • // Make sure to wrap the DOM list in a JQLite object.
  • return( JQLite( subset ) );
  •  
  • };
  •  
  •  
  • // I check to see if the first element in the collection matches the given
  • // selector. Currently supports DOM element, node name, class syntax.
  • // --
  • // ex: .is( targetElement )
  • // ex: .is( "ul" )
  • // ex: .is( ".some-class" )
  • JQLite.prototype.is = function( selector ) {
  •  
  • // If no elements, can't possibly match.
  • if ( ! this.length ) {
  •  
  • return( false );
  •  
  • }
  •  
  • // If the value is not a string, assume DOM node.
  • if ( ! angular.isString( selector ) ) {
  •  
  • return( safeEquals( this, selector ) );
  •  
  • // If starts with "." assume class notation.
  • } else if ( selector.charAt( 0 ) === "." ) {
  •  
  • return( this.hasClass( selector.slice( 1 ) ) );
  •  
  • // Else, assume node name.
  • } else {
  •  
  • return( this[ 0 ].nodeName === selector.toUpperCase() );
  •  
  • }
  •  
  • };
  •  
  •  
  • // I select a random element in the current collection.
  • JQLite.prototype.random = function() {
  •  
  • return( this.eq( Math.floor( this.length * Math.random() ) ) );
  •  
  • };
  •  
  •  
  • // I position the elements using the given X and Y coordinates. If
  • // provided, the optional offset is applied to the X and Y coordinates
  • // of each element.
  • JQLite.prototype.xyo = function( x, y, offset ) {
  •  
  • this.css({
  • left: ( ( x + ( offset || 0 ) ) + "px" ),
  • top: ( ( y + ( offset || 0 ) ) + "px" )
  • });
  •  
  • return( this );
  •  
  • };
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, I am defining my jqLite plugins in a .run() block. This way, they will be defined once the AngularJS application has been bootstrapped, which will make them available to all of my directives.

In the AngularJS world, it has become trendy to stop using jQuery; but, that doesn't mean we have to abandon the features that make jQuery so powerful. If there's something missing from jqLite, such as an .appendTo() method, you can write a jqLite plugin for it.

Tweet This Deep thoughts by @BenNadel - Creating jqLite Plugins In AngularJS Thanks my man — you rock the party that rocks the body!



Reader Comments

Nice post. Thanks.

I thought adding method using <CommonClass>.prototype was evil ...
I would use Object.defineProperty() instead, no ?
Something like that :

Object.defineProperty(jqLite.prototype, 'myMethod', function(){
return {
value: function(){},
enumerable: false,
...
};
})

Reply to this Comment

man this is quite helpful post , never thought of defining JQuery plugins again in JQlite , but now I reckon I'll consider this more often

Reply to this Comment

@M'sieur,

I believe that the only benefit of using Object.defineProperty() would be if you want to explicitly control how the method is seen. But, to be honest, I don't have much experience with Object.defineProperty() as I've never really understood the need for it. Meaning, it doesn't solve a problem that I am *actually having* ... at least, not that I can see.

But, I'm open to understanding it better.

Reply to this Comment

@Yazan,

Thanks my man! I know people hate on jQuery, but I think there's a lot we can learn from it.

Reply to this Comment

@Ben,

Hi Ben, you are right. In this simple case, it does not change anything. In the past, I have been in trouble with manually updating the prototypes. By this time, I have red it was best pratice to use defineProperty() over manually updating the prototype ...

As far as I can tell, defineProperty() has two benefits :

1. It avoids this kind of behavior :

var obj = {};
obj.foo = 'foo';
Object.prototype.bar = function(){
console.log('bar')
};
for(var i in obj){
console.log(i + ': ' + obj[i])
}

... that will give you :
foo: foo
bar: function (){console.log('bar')}

... which is probably not what you expected. ;)

2. It lets you make dynamic setter/getter :

function myClass() {
var _foo;
Object.defineProperty(this, 'foo', {
set: function(v){_foo = 2 * v;},
get: function(){return 'my value is ' + _foo;}
});
}

var obj = new myClass();
obj.foo = 3;
console.log(obj.foo);

... that will give you :
"my value is 6";

This post helped me a lot at the time : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.