2
\$\begingroup\$

I am trying to tackle two fairly massive concepts through JavaScript and jQuery, the first being object-oriented programming and the second being how to build robust modular plugins with an OOP approach. So far I've learned it's hard.

Here is an example plugin I built, that I somewhat based my much larger project on; any opinions you guys have on my approach would be awesome.

;(function($, window, document, undefined) {
  "use strict";

  $.fn.myPlugin = function(opts) {

  var settings = $.extend({},{ color  : 'blue', normal : 'black' }, opts);

  var proto  = {
    show : function () { $(this.self).css( "color", this.settings.color ); },
    hide : function () { $(this.self).css( "color", this.settings.normal); },
    log  : function () { console.log(this); }
  }

  return this.each(function(id, elem) {

    if (!$.data(elem, 'parent')) // Base parent object                                                                                                                                                                                
      $.data(elem, 'parent', Object.create(proto, {
        'settings' : { value        : settings,
                       writable     : true,
                       enumerable   : true,
                       configurable : true
        }
      }));

      $(elem).children('span').each(function(idx, elmx) {

        if (!$.data(elmx, 'child')) // Child obj w/ delegation from parent                                                                                                                                                              
          $.data(elmx, 'child', Object.create($.data(elem, 'parent'), {
            'foo'  : { value        : 'bar',
                       writable     : true,
                       enumerable   : true,
                       configurable : true
            },

            'self' : { value        : elmx,
                       writable     : false,
                       enumerable   : true,
                       configurable : false
            }
          }));

        $(elmx).hover(
          function() {
            $.data(elmx, 'child').log();
            $.data(elmx, 'child').show();
          }, function() {
            $.data(elmx, 'child').hide();
          });
        });
      });
  };
}(jQuery, window, document));

Here is where the code is running, basically all it does is search for <span> tags inside of the <p> tags the plugin is instantiated on and highlights them with a given user setting or the defaults. It shows the use of delegation for inheritance and very basic event management (no callbacks as of yet)

\$\endgroup\$
0

3 Answers 3

1
\$\begingroup\$

The chief characteristics of this plugin appear to be :

  1. it is an exercise in a form of classical inheritance, by which methods bound to an element are inherited by selected child elements.
  2. methods are defined as properties of an object bound to DOM elements with jquery.data. These methods are intended(?) for private use (within the plugin) but are ultimately accessible by user code.
  3. the child elements (spans) will retain a relationship with their original parent after being moved elsewhere within the DOM.
  4. the selection of first child spans is hardcoded.

All of which is interesting and potentially useful, but rather odd in some regards.

[1]: is successfully and concisely coded.

[2]: could be regarded as subverting jQuery. As it stands, the plugin leverages jQuery to provide an alternative way to bind methods to DOM elements, whereas we would normally expect a jQuery plugin to expose public methods via the jQuery interface and/or have private functions for internal use.

[3]: could be inconsequential, useful, or a total annoyance, depending on the role of the plugin in the context of a wider application.

[4]: you could consider a mechanism for user code to override the default selection of .children('span'), eg by passing a selector in the options hash.

Out of interest, a more conventional jQuery plugin to achieve the same ends (with some extra features) might look something like this :

;(function($) {
    "use strict";
    // Private Members
    var pluginName = 'myPlugin_1';

    // Public Methods
    var methods = {
        init: function(options) {
            return this.each(function(i, el) {
                var settings = { color: 'blue', normal: 'black', selector: '>span' };
                $.extend(settings, options || {});
                $(el).on('mouseenter.' + pluginName, settings.selector, function(e) {
                    console.log(settings);
                    $(e.target).css( 'color', settings.color );
                }).on('mouseleave.' + pluginName, settings.selector, function(e) {
                    $(e.target).css( 'color', settings.normal );
                });
            });
        },
        destroy: function () {
            return this.off('.' + pluginName);
        }
    };

    // Supervisor
    $.fn[pluginName] = function( method ) {
        if ( methods[method] ) {
            return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
        } else if ( typeof method === 'object' || !method ) {
            return methods.init.apply( this, arguments );
        } else {
            $.error( 'Method ' + method + ' does not exist in jQuery.' + pluginName );
        }
    };
}(jQuery));

Note: No inheritance is necessary here. The same effect is achieved through closure of the init method.

Sample calls

$('.p1').myPlugin({ color: 'blue' }); // exactly as in the question, using the default selector `>span` .

$('.p2').myPlugin({ color: 'red', selector: '>span.a' }); // to select first level spans with class="a".
$('.p2').myPlugin({ color: 'green', selector: '>span.b' }); // to select first level spans with class="b".
\$\endgroup\$
11
  • \$\begingroup\$ Very interesting, I knew I had a few things off. That gives me a very clear outlook on what things I am still shaky on so I appreciate it greatly. One of the big concepts I am missing is the line between using closures and inheritance. Obviously this application is so small that inheritance is super unnecessary, but projects can grow quickly and that's where my brain starts to get stretched. \$\endgroup\$
    – Mike
    Oct 14, 2016 at 18:34
  • \$\begingroup\$ In my experience, it's actually unusual for closure to be useful in jQuery plugins. It works here because event handlers attached by the init method are the only functions (other than 'init' iteslf) to use settings. More typically, other plugin methods also need access to settings in which case, settings needs to be bound to DOM elements using jQuery(...).data(...). Your inheritance approach is stretching my brain too. I need time to work out how useful it might be. \$\endgroup\$ Oct 14, 2016 at 18:52
  • \$\begingroup\$ yea you're approach looks other worldly to me. I think I see what it is you're getting at by using the init as a 'container'. The 'destroy' method has me a bit confused but I am sure it'll become clear as I play with the pattern. It's all about understanding other people's styles and learning from them \$\endgroup\$
    – Mike
    Oct 14, 2016 at 19:01
  • \$\begingroup\$ To understand 'destroy', read the bits about event namespaces in .on and .off. \$\endgroup\$ Oct 14, 2016 at 19:07
  • \$\begingroup\$ Is there somewhere that describes the pattern of this plugin? like the concept behind using a methods object with an init, destroy, etc... that feeds into the conditionals within the plugin? I have been doing a lot of research on plugins an OOP and have not yet come across this approach and I would like to read some more about it if there's more to be read. \$\endgroup\$
    – Mike
    Oct 14, 2016 at 19:25
1
\$\begingroup\$

The example you posted makes no use of inherited behaviour as far as I can tell, and can be implemented as the example below.

I think however that you are preparing for something that this example does not showcase, since you're asking about delegated implementations. It's hard to tell what exactly you are aiming for though, so for now I'll just supply the simplification of your example.

;(function($, window, document, undefined) {
  "use strict";

  function show(elem, settings) {
    $(elem).css("color", settings.color);
  }

  function hide(elem, settings){
    $(elem).css("color", settings.normal);
  }

  var defaultSettings = {
      color: 'blue',
      normal: 'black'
  };

  function setHoverHandlers(elem, settings) {
    $(elem).hover(function() {
      show(elem, settings);
    }, function() {
      hide(elem, settings);
    });
  }

  $.fn.myPlugin = function(opts) {
    var elemSettings = $.extend({}, defaultSettings, opts);

    return this.each(function(id, elem) {
      $(elem).children('span').each(function(_, elem){
        setHoverHandlers(elem, elemSettings);
      });
    });
  };
}(jQuery, window, document));

As a side note, I generally discourage the use of Object.create, in favour of using plain data structures and functions working on them, or encapsulating state using closures. Crockford elaborates on it in this talk.

As another side note, it's funny that Crockford holds this talk, when he is in fact the single reason thatObject.create is in JavaScript in the first place. It was literally added to the standard after him nagging about it long enough.

\$\endgroup\$
1
  • \$\begingroup\$ I agree, my approach was a little over the top. However, one key design choice was to see how the objects would interact with each other. Simply a show case of delegation as I have never done any OOP before, let alone classless OOP. Object.create and New have been battling in my head for some time now while I try to form opinions on their usefulness. I do like the approach you took, it seems far cleaner than my rat's nest of inheritance. I am checking out the video and am already picking up a lot from it. \$\endgroup\$
    – Mike
    Oct 14, 2016 at 13:52
0
\$\begingroup\$

I just wanted to add onto the answer @Roamer-1888 gave me. After playing around with his approach to use strings as function calls and the arguments object to pass in arguments, I ran into a few issues.

My first issue was the lack of any chaining ability. While this functionality is not required, any jQuery dev would agree that they would want it built into whichever plugin they want to use.

However, introducing this will overwrite the Arguments object as a result of .each taking control of it:

return this.each(function() {
    // if blocks here
});

Just redefining the Arguments object still leaves you with one shot at holding the passed in properties.

As soon as you call

Array.prototype.splice.call(arguments, 1);

within our else if you lose any reference made to the Arguments object you need seeing as splice quite literally removes the data from the passed "array" and assigns it to where you point it, which in our case is just into a function where it will vanish after use.

Wanting to keep the theme, this is the solution I came up with:

$.fn.Plugin = function ( passed ) {
    var args = Array.prototype.splice.call(arguments, 1);
    return $(this).each(function() {
        if(!$.data(this, 'plugin')) {
            if (typeof passed === 'object' || !passed)
                $.data(this, 'plugin', new Plugin(this, passed));
            else
                console.log('Error, invalid object define');
        }
        else if (passed in $.data(this, 'plugin'))
            $.data(this, 'plugin')[passed].apply($(this), args);
        else
            console.log('error');
    });
}

As you can see, I splice the arguments object first and save it locally to the plugin call. While this can be dangerous in some edge case situations of certain applications, I feel it is fairly safe/explicit in the context I am using it. The only time extra arguments are allowed is during a function call. If I am not calling a function, I am either defining a new object which only takes one object as its argument, or throwing an error since whatever was passed is considered invalid input.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.