user169867
user169867

Reputation: 5870

How to have member variables and public methods in a jQuery plugin?

I'm trying to create a jQuery plugin that will create something like an autoCompleteBox but with custom features. How do I store member variables for each matching jQuery element?

For example I'll need to store a timerID for each. I'd also like to store references to some of the DOM elements that make up the control.

I'd like to be able to make a public method that works something like:

$("#myCtrl").autoCompleteEx.addItem("1");

But in the implementation of addItem() how can I access the member variables for that particular object like its timerID or whatever?

Below is what I have so far...

Thanks for any help or suggestions!

(function($)
{     
    //Attach this new method to jQuery   
    $.fn.autoCompleteEx = function(options)
    {    
        //Merge Given Options W/ Defaults, But Don't Alter Either
        var opts = $.extend({}, $.fn.autoCompleteEx.defaults, options);


        //Iterate over the current set of matched elements   
        return this.each(function() 
        {
            var acx = $(this); //Get JQuery Version Of Element (Should Be Div)

            //Give Div Correct Class & Add <ul> w/ input item to it
            acx.addClass("autoCompleteEx"); 
            acx.html("<ul><li class=\"input\"><input type=\"text\"/></li></ul>");

            //Grab Input As JQ Object
            var input = $("input", acx);

            //Wireup Div
            acx.click(function()
            {
                input.focus().val( input.val() );
            });


            //Wireup Input
            input.keydown(function(e)
            {
                var kc = e.keyCode;
                if(kc == 13)   //Enter
                {

                }
                else if(kc == 27)  //Esc
                {

                }
                else
                {
                    //Resize TextArea To Input
                    var width = 50 + (_txtArea.val().length*10);
                    _txtArea.css("width", width+"px");    
                }
            });

        });   //End Each JQ Element

    }; //End autoCompleteEx()

    //Private Functions
    function junk()
    {

    };

    //Public Functions
    $.fn.autoCompleteEx.addItem = function(id,txt)
    {
        var x = this;
        var y = 0;
    };

    //Default Settings
    $.fn.autoCompleteEx.defaults =
    {
        minChars:  2,
        delay:     300,
        maxItems:  1
    };

    //End Of Closure
})(jQuery); 

Upvotes: 5

Views: 8736

Answers (6)

Ken Earley
Ken Earley

Reputation: 769

Here's my take on it:

I have an object within the closure that is used to create instance objects. The instance objects are attached to the element node using jQuery's data() method. These instance objects have public methods that you can call as needed.

(function($)
{     
// This is used to create AutoComplete object that are attatched to each element that is matched
// when the plugin is invoked
var AutoCompleteEx = function(options, acx) {

  // PRIVATE VARIABLES
  var timerID;
  var input;

  //Give Div Correct Class & Add <ul> w/ input item to it
  acx.addClass("autoCompleteEx"); 
  acx.html("<ul><li class=\"input\"><input type=\"text\"/></li></ul>");

  //Grab Input As JQ Object
  input = $("input", acx);

  //Wireup Div
  acx.click(function()
  {
      input.focus().val( input.val() );
  });


  //Wireup Input
  input.keydown(function(e)
  {
      var kc = e.keyCode;
      if(kc == 13)   //Enter
      {

      }
      else if(kc == 27)  //Esc
      {

      }
      else
      {
          //Resize TextArea To Input
          var width = 50 + (_txtArea.val().length*10);
          _txtArea.css("width", width+"px");    
      }
  });

  // PUBLIC METHODS

  this.setTimerID = function(id) {
    timerID = id;
  };

  this.getTimerID = function() {
    return timerID;
  };

};


//Attach this new method to jQuery   
$.fn.autoCompleteEx = function(options)
{    
    //Merge Given Options W/ Defaults, But Don't Alter Either
    var opts = $.extend({}, $.fn.autoCompleteEx.defaults, options);

    //Iterate over the current set of matched elements   
    return this.each(function() 
    {
        var acx = $(this); //Get JQuery Version Of Element (Should Be Div)

        // creating a new AutoCompleteEx object and attach to the element's data, if not already attached
        if (!acx.data('autoCompleteEx')) {
          acx.data('autoCompleteEx', new AutoCompleteEx(options, acx));
        }

    });   //End Each JQ Element

}; //End autoCompleteEx()

//Default Settings
$.fn.autoCompleteEx.defaults =
{
    minChars:  2,
    delay:     300,
    maxItems:  1
};

//End Of Closure
})(jQuery);

You can call the methods like this:

$("div#someDiv").autoCompleteEx();
$("div#someDiv").data('autoCompleteEx').setTimerID(123);
var timerId = $("div").data('autoCompleteEx').getTimerID();
console.log(timerId); // outputs '123'

And if you are instantiating more than one:

$("div.someDiv").autoCompleteEx();
$("div.someDiv").eq(0).data('autoCompleteEx').setTimerID(123);
$("div.someDiv").eq(1).data('autoCompleteEx').setTimerID(124);
var firstTimerId = $("div").eq(0).data('autoCompleteEx').getTimerID();
var secondTimerId = $("div").eq(1).data('autoCompleteEx').getTimerID();
console.log(firstTimerId); // outputs '123'
console.log(secondTimerId); // outputs '124'

Upvotes: 1

user188143
user188143

Reputation:

Instance manipulation! That's what you want, right? Good old fashion, real time instance manipulation. That's what I wanted too. I Googled the same question and couldn't get a good answer anywhere (like the ones above) so I figured it out. I don't like my solution because it seems like a round about way to get access to instance methods and strange for a jquery consumer but jQuery is freakin weird in the first place but lovely. I wrote a simple plugin to fade images but then once I needed to do more with it, I wanted to expose methods to a live instance to get this result here -> example, I did the following:

var instanceAccessor = {};
var pluginOptions = {'accessor':instanceAccessor}
$('div').myPlugin(pluginOptions);

Then inside the plugin, I add on methods to this passed in object 'accessor' since its an object. I expose the methods inside the plugin like this:

if (pluginOptions.accessor != null && typeof(pluginOptions.accessor) === 'object') {
   pluginOptions.accessor.exposedMethod = function (someParam) {
     // call some private function here and access private data here
   };
}

Then, the consumer of this plugin can call the instance method or methods any time during runtime like we used to do before jquery made this strange:

instanceAccessor.exposedMethod('somevalue');

You can look for "dumb cross fade" on jquery plugin search to find my dumb plugin and see the code for yourself.

Upvotes: 0

gnarf
gnarf

Reputation: 106342

I've found that the jQuery UI way of handling this seems to work out the best. You create your 'extra methods' as a string argument to your plugin:

$('#elem').autoCompleteEx('addItem', '1');

Then the 'this' context is preserved, and you can do something along these lines:

function addItem() {
  // this should be == jquery object when this get called
}

$.fn.autoCompleteEx = function(options) {
   if (options === 'addItem') {
     return addItem.apply(this, Array.prototype.splice.call(arguments, 1));
   }
};

Upvotes: 7

Borgar
Borgar

Reputation: 38652

Here is a template that I am experimenting with when building more complex widget plugins:

(function($){

  // configuration, private helper functions and variables
  var _defaultConfig = {
        /* ... config ... */
      },
      _version = 1;


  // the main controller constructor
  $.myplugin = function ( elm, config ) {

    // if this contructor wasn't newed, then new it...
    if ( this === window ) { return new $.myplugin( elm, config || {} ); }

    // store the basics
    this.item = $( elm );
    this.config = new $.myplugin.config( config );

    // don't rerun the plugin if it is already present
    if ( this.item.data( 'myplugin' ) ) { return; }

    // register this controlset with the element
    this.item.data( 'myplugin', this );

    // ... more init ...

  };
  $.myplugin.version = _version;
  $.myplugin.config = function ( c ) { $.extend( this, $.myplugin.config, c ); };
  $.myplugin.config.prototype = _defaultConfig;
  $.myplugin.prototype = {

    /* ... "public" methods ... */

  };

  // expose as a selector plugin
  $.fn.myplugin = function ( config ) {
    return this.each(function(){
      new $.myplugin( this, config );
    });
  };

})(jQuery);

I put the default config and version at the top simply because it the most likely thing anyone reading the code is seeking. Most of the time you just want to examine the settings block.

This will expose "myplugin" in two places, as a constructor for the widget's "controller" on $ and as a collection method on $.fn. As you can see $.fn method doesn't really do anything except instanciate new controllers.

The config is a prototypally inherited object where the default is the prototype. This give extended flexibility with asigining values as you may assign the "next" defaults into $.myplugin.config, or alter every running plugin's default with $.myplugin.config.prototype. This does require you to allways assign into these with $.extend or you will break the system. More code could counter that, but I prefer to know what I'm doing. :-)

The instance of the controller is binds itself to the element through jQuery's data() method, and in fact uses it to test that it isn't run twice on the same element (although you might want to allow reconfiguring it).

This gives you the following interface to the controller:

// init:
$( 'div#myid' ).myplugin();

// call extraMethod on the controller:
$( 'div#myid' ).data('myplugin').extraMethod();

The biggest flaw on this approach is that it is a bit of a pain to maintain the "this" context with every event assignment. Until context for events arrives in jQuery this needs to be done with a liberal amount of closures.

Here is a rough example of how an (incomplete and useless) plugin might look:

(function($){

  // configuration, private helper functions and variables
  var _defaultConfig = {
        openOnHover: true,
        closeButton: '<a href="#">Close</a>',
        popup: '<div class="wrapper"></div>'
      },
      _version = 1;

  // the main controller constructor
  $.myplugin = function ( elm, config ) {

    // if this contructor wasn't newed, then new it...
    if ( this === window ) { return new $.myplugin( elm, config || {} ); }
    this.item = $( elm );
    this.config = new $.myplugin.config( config );
    if ( this.item.data( 'myplugin' ) ) { return; }
    this.item.data( 'myplugin', this );

    // register some events
    var ev = 'click' + ( this.config.openOnHover ) ? ' hover' : '';
    this.item.bind(ev, function (e) {
      $( this ).data( 'myplugin' ).openPopup();
    });

  };
  $.myplugin.version = _version;
  $.myplugin.config = function ( c ) { $.extend( this, $.myplugin.config, c ); };
  $.myplugin.config.prototype = _defaultConfig;
  $.myplugin.prototype = {

    openPopup: function () {
      var C = this.config;
      this.pop = $( C.popup ).insertAfter( this.item );
      this.pop.text( 'This says nothing' );
      var self = this;
      $( C.closeButton )
          .appendTo( pop )
          .bind('click', function () {
            self.closePopup();  // closure keeps context
            return false;
          });
      return this;  // chaining
    },

    closePopup: function () {
      this.pop.remove();
      this.pop = null;
      return this;  // chaining
    }

  };

  // expose as a selector plugin
  $.fn.myplugin = function ( config ) {
    return this.each(function(){
      new $.myplugin( this, config );
    });
  };

})(jQuery);

Upvotes: 4

Anthony Mills
Anthony Mills

Reputation: 8784

Use something like this:

acx.data("acx-somename", datavalue);

Then you can later retrieve it with:

var datavalue = acx.data("acx-somename");

Upvotes: 1

Corey Downie
Corey Downie

Reputation: 4769

Take a look at the .data functionality of jQuery. It lets you store key/value pairs on any object.

Upvotes: 1

Related Questions