Thursday, May 3, 2012

How can I give a plugin"s default settings access to the final settings?


I'm working on a jQuery plugin. When the plugin runs, the first thing it does is determine its settings. It does this by taking some default settings and overriding some or all of them with whatever the user passed in.



Here's a simplified version:




(function( $ ) {
$.fn.somePlugin = function(userOptions) {
// Short name for internal use - exposed below for external modification
var defaults = $.fn.somePlugin.defaults;

// Decide settings
// User settings overrule defaults, but merge both into an empty
// hash so that original defaults hash isn't modified
var settings = $.extend(true, {}, defaults, userOptions);

settings.alerter.call();

// More code, etc.
}

$.fn.somePlugin.defaults = {
name: 'Captain Slappy von Martinez, Esquire, DDS',
favoriteColor: 'red',
alerter: function(){alert("I got bones!");}
}

})( jQuery );



So, if you call the plugin like this:




$('div').somePlugin({alerter: function(){alert('No bones here!');}});



... you override the default alerter function, but use the default favoriteColor .



I'm defining the defaults outside the plugin itself so that they are publicly accessible. So if you want to change the default value for favoriteColor , you can just do this:




$.fn.somePlugin.defaults.favoriteColor = 'blue';



... and that will change it for every instance on the page after that.



All of this works fine.



Here's my problem: in one of the default functions, I need to refer to settings . Something like this:




$.fn.somePlugin.defaults = {
name: 'Captain Slappy von Martinez, Esquire, DDS',
favoriteColor: 'red',
alerter: function(){alert("Paint me " + settings.favoriteColor);}
}



But when I try this, I get a Javascript error on the line where the default alerter function is declared: "settings is not defined." True enough: it's not defined yet when the parser hits that line. It WILL be defined by the time the function runs, but that doesn't seem to matter.



So my question is: How can I import my default function into the plugin in such a way that it has access to settings ?



  • Is there a way to delay evaluation of settings until the function is actually called?

  • If so, is there a way to make sure it has access to the local scope of the plugin, even though I assume that, as an object, this function is being passed by reference? I know that I can alter the value of this inside a function by using someFunction.call(valueToUseForThis) , but I don't know whether I can alter its scope after the fact.



Some Stuff that Doesn't Work



  • Initially, it seemed that if I declared settings in the outer, self-executing function, that would make it available to the defaults. But declaring it there causes problems: if I use my plugin multiple times on a page, the instances get their settings mixed up.


Source: Tips4all

3 comments:

  1. Any attempts to change scope after the fact are doomed. There's no dynamic scoping in JavaScript.

    Workaround 1.
    Pass settings to the alerter explicitly:

    $.fn.somePlugin = function(userOptions) {
    var settings = $.extend(...);
    settings.alerter(settings)
    }

    $.fn.somePlugin.defaults = {
    favoriteColor: 'red',
    alerter: function(settings) { alert("Paint me " + settings.favoriteColor) }
    }


    Workaround 2. Pass settings to the alerter implicitly:

    $.fn.somePlugin = function(userOptions) {
    var settings = $.extend(...);
    settings.alerter()
    }

    $.fn.somePlugin.defaults = {
    favoriteColor: 'red',
    alerter: function() { alert("Paint me " + this.favoriteColor) }
    }


    Both solutions tested and work well with multiple uses of the plugin. If you have no plans for using this keyword in your alerter, you can use it for referencing settings.

    The third valid solution, which you and fudgey mentioned, is to attach settings object to the DOM element itself:

    $.fn.somePlugin = function(userOptions) {
    var settings = $.extend(...);
    $(this).data('somePlugin', {settings: settings})
    settings.alerter.call(this)
    }

    $.fn.somePlugin.defaults = {
    favoriteColor: 'red',
    alerter: function() {
    alert("Paint me " + $(this).data('somePlugin').settings.favoriteColor)
    }
    }




    Extra fun. Emulating dynamic scope, a.k.a don't do this

    function dynamic(fn) {
    var parts = fn.toString().split('{')
    fn = parts.shift() + '{ with (args) { ' + parts.join('{') + '}'
    return {bind: function(args) { return eval('(' + fn + ')') }}
    }

    a = b = c = 0

    function adder(c) {
    alert(a + b + c)
    }

    dynamic(adder).bind({a: 1, b: 2})(3) // alert(6)


    Usage in a plugin:

    $.fn.somePlugin = function(userOptions) {
    var settings = $.extend(...);
    dynamic(settings.alerter).bind({settings: settings})()
    }

    $.fn.somePlugin.defaults = {
    favoriteColor: 'red',
    alerter: function() { alert("Paint me " + settings.favoriteColor) }
    }

    ReplyDelete
  2. The settings variable is scoped to the $.fn.somePlugin function so it is not available outside of this, which is where you're trying to reference it in $.fn.somePlugin.defaults. Can you not simply move the default object inside $.fn.somePlugin and declare it in there?

    Try this (code untested):

    (function( $ ) {
    $.fn.somePlugin = function(userOptions) {
    // Short name for internal use - exposed below for external modification
    var settings = {},
    defaults = {
    name: 'Captain Slappy von Martinez, Esquire, DDS',
    favoriteColor: 'red',
    alerter: function(){alert("Paint me " + settings.favoriteColor);}
    };

    // Decide settings
    // User settings overrule defaults, but merge both into an empty
    // hash so that original defaults hash isn't modified
    settings = $.extend(true, {}, defaults, userOptions);

    settings.alerter.call();

    // More code, etc.
    }

    })( jQuery );


    Edit: In light of your comment below, the simplest approach is probably to just move the declaration of settings up a level into the outer function - being locally scoped to this function, it will still be private to external callers. Revised code below:

    (function( $ ) {

    var settings = {}

    $.fn.somePlugin = function(userOptions) {
    // Decide settings
    // User settings overrule defaults, but merge both into an empty
    // hash so that original defaults hash isn't modified
    settings = $.extend(true, {}, $.fn.somePlugin.defaults, userOptions);

    settings.alerter.call();

    // More code, etc.
    }

    $.fn.somePlugin.defaults = {
    name: 'Captain Slappy von Martinez, Esquire, DDS',
    favoriteColor: 'red',
    alerter: function(){alert("Paint me " + settings.favoriteColor);}
    }

    })( jQuery );

    $.fn.somePlugin.defaults.favoriteColor = 'blue';

    $.fn.somePlugin(); // Should alert 'Paint me blue'

    alert(settings.favoriteColour) // Will not work here;

    ReplyDelete
  3. Simply change settings to this:

    $.fn.somePlugin.defaults = {
    name: 'Captain Slappy von Martinez, Esquire, DDS',
    favoriteColor: 'red',
    alerter: function () { alert("Paint me " + this.favoriteColor); }
    }


    That's it!

    EDIT: Oh! I just noticed something in your code! Don't use .call() to call settings.alerter. That changes the meaning of this. Just call the method directly. If you must use .call(), pass in settings as the parameter for this.

    settings.alerter.call(); // this won't work.
    settings.alerter(); // but this will!
    settings.alerter.call(settings); // this works too


    Here's a complete working example based on your code: http://jsfiddle.net/gilly3/k2Eep/2/

    ReplyDelete