5

I'm working on a jQuery plugin but I'm encountering issues with the "scope" of variables. Each plugin will need to keep track of a considerably sized multidimensional array, as well as the root element that the jQuery plugin was attached to.

As shown below, I've defined var $rootEl = null; and var multidArray = null; at the top of the plugin code; however, when I run $(anySelector).pluginName("value"); on multiple elements (or when I call .pluginName() twice with different selectors), it would appear that these variables are not sandboxed / scoped to the plugin instance, so the second instance overwrites both of the values and the first instance looks blank in the DOM (because all jQuery actions are applied on $rootEl with the original value being overwritten on the second jQuery plugin call).

(function ($) {

    var $rootEl = null;
    var multidArray = null;

    directChildren = function(uuid) {

        return $rootEl.children("[data-uuid=" + uuid + "]");

    },

    incrementCounter = function() {
        this.counter++;
    },

    resetGenIteration = function() {
        this.counter = 0;
    },

    process = function(item) { // Will be called from a callback(val) in another
        // javascript file but still needs to have access to the local variables
        // of the current jQuery plugin

        incrementCounter();

        ...

    },

    ...

    $.fn.pluginName = function(rootvar) {

        this.each(function() {

            $rootEl = $(this);

            resetGenIteration();
            populate(rootvar);

            $rootEl.addClass("pluginName").delegate("div", "click", divClick);

        });

    }

}(jQuery));

Yesterday I attempted to convert the plugin to use init and this.varname to store the values, and it appeared to hold onto the this.$rootEl property correctly for each instance of the plugin, but I had to move all my functions inside of var PluginName = { ... } and switch them from funcName = function() to funcName: function() to be able to see the plugin instance's "local" variables. Then I encountered numerous errors where functions were not defined, so I tried prefixing all my function calls with this. and PluginName. to read as this.functionName() or PluginName.functionName() respectively, which worked for some functions, but not all. Some of my functions are called from another jQuery plugin that runs a callback function and passes data to it, and in these cases, this.$rootEl becomes undefined.

(function ($){

    var PluginName = {

        init: function(options, rootEl) {

            this.options = $.extend({}, this.options, options);

            this.rootEl  = rootEl;
            this.$rootEl = $(rootEl);

            this.setListeners();

            return this;

        },

        options: {
            rootvar: "test"
        },

        ...

        setListeners:function (){

            var self = this;

            this.$rootEl.on("div", "click", function () {
                $.proxy(self.divClick, self);
            });

        }

    };

    ...

    $.fn.pluginName = function(options) {

        this.init = function(options, rootEl) {

            this.options = $.extend({}, this.options, options);

            this.rootEl  = rootEl;
            this.$rootEl = $(rootEl);
            this.multidArray = null;


            return this;

        };

        this.each(function() {

            var plugInstance = Object.create(PluginName);
            plugInstance.init(options, this);
            $.data(this, 'pluginName', plugInstance);

        });

    }

}(jQuery));

How do I refactor my code to store the multi-dimensional array and root element in a way that only that given plugin can internally retrieve and modify? Surely I should not be using .data() for large arrays?

Thanks!

Chad
  • 1,380
  • 2
  • 17
  • 42

2 Answers2

1

A plugin is just a function that gets attached to the jQuery object. It doesn't have multiple instances, but rather it can be called multiple times. Each call creates a new scope, however, variables declared outside the pluginName function will be shared by every call.

Rather than the object oriented approach you are attempting in your second iteration, I would suggest going back to using multiple functions. However, you can pass these functions all required arguments so they don't have to rely on ambiguous this values.

Since you only included a partial snippet of your code, I can't get a clear idea of what you are trying to accomplish, but I've included an example plugin below that highlights how to properly use scope.

(function ($) {
  // declare variables that can be shared by every call of "pluginName" here

  var defaultColor = 'green'

  // passing the element as an argument means we don't have to rely on ambiguous 'this'.
  function someTransformation ($el, someColor) {
    return $el.css({backgroundColor: someColor})
  }


  $.fn.pluginName = function (color, idx) {
    // declare variables that should be reinitialized on every call of "pluginName" here
    var els = []

    this.each(function () {
      var $el = $(this)
      someTransformation($el, defaultColor)
      els.push($el)
    })

    // we can still use the array we stored
    if (els[idx]) {
      someTransformation(els[idx], color)
    }
  }
})($)

$('#first i').pluginName('red', 2)
$('#second i').pluginName('blue', 0)
i {
  display: inline-block;
  width: 50px;
  height: 50px;
  margin: 10px;
}

div {
 border: 1px solid black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<div id="first">
 <i></i><i></i><i></i><i></i>
</div>

<div id="second">
  <i></i><i></i><i></i><i></i>
</div>

If you are still having trouble, start by doing everything in the pluginName function. without declaring extra helpers. This will help you make sure that all of your logic is working properly. Then start moving any reused code into helper functions that accept all the arguments they need to run.

Somewhat relevant but not immediately helpful nitpick: The function expressions outside of pluginName (directChildren for example) are globals. Make sure to preface those with var to keep them scoped to your IIFE.

Damon
  • 3,566
  • 2
  • 14
  • 26
  • This answer is very helpful and appears elegant. I'm awarding you with the solution + an upvote for concision. – Chad Dec 24 '18 at 16:17
0

As @Damon said, there is nothing special in jquery plugin - it's just a function, and JS is all about the functions ;)

So all issues you have because not thinking through all your usages. I don't know what the usages you have (or planning to have), but here is how I would do it. It's a plugin that store "bigArray" for each instance. Increment it and puts into the objects html. Explanations after:

(function($) {
        let _plugInstances = [];

        function PlugFactory(options) {
            let _plugInstances = [],
                _create = function(options) {
                    let $el = $(this),
                        id = $el.data('PlugID') || (+new Date +''+Math.random()).replace('.', '_');
                    if (_plugInstances[id])
                        return _plugInstances[id];
                    $el.data('PlugID', id);
                    _plugInstances[id] = new Plug($el, options);
                    return _plugInstances[id];
                };

            this.instantiate = function(options) {
                let newPlugs = this.map(_create);
                return newPlugs.length > 1 ? newPlugs : newPlugs[0];
            }
        }
        function Plug(rootEl, options) {
            let _$rootEl = rootEl,
                _options = options,
                _bigArr = [];
            this.add = (item) => {
                _bigArr.push(item);
                return this;
            };
            this.refresh = () => {
                _$rootEl.html(_$rootEl.data('PlugID') + ":" + _bigArr.join('-'));
                return this;
            }
        }

        $.extend({plug: new PlugFactory});
        // extend plugin scope
        $.fn.extend({
            plug: $.plug.instantiate
        });
    })(jQuery);

    // Usage
    $(document).ready(function () {
        $('div').plug({opt: 'whatever'});
        $('div').on('click', function() {
            $(this).plug()
                .add(1)
                .refresh();
        })
        $('#i4').plug({opt: 'whatever'}).add(2).refresh();
    });
div{border: solid 2px gray;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div>1</div><div>2</div><div>3</div>
<br/>
<div id="i4">4</div>

Explanations:

As you can see the the thing you used in $.fn.pluginName is moved to the separate function (lets say class) (PluginFactory)

It's actually the function that will store the instances of your plugin.

As you probably noted, there is variables started from underscore they are private properties the thing is that they are closures so they are visible only inside functions where they are defined, and since they are variables they are not accessible as function instance property.

Also please take into account ES6 "context safe" things like short function notation and let.

Now the thing with the instances.

As you can see we have some UID generated for each element (you can use any another generator, that's just a POC)

Next we create new instance with new operator

And save it into the Map to simplify things with getting the instances.

And save our UID in the elements jquery data (PliginID), so now we know if our element already have an instance of the plugin.

Now the main part (Plugin function).

That's actually your instance that would live throughout the calls of ".plug".

Each time you get $('#element').plug() you'll receive your initially created instance.

So there you will have access to all your private members, and design your public methods.

PS. I'm not claiming that it's the best just definitely one of many, as an example of how to solve task of "store the multi-dimensional array and root element in a way that only that given plugin can internally retrieve and modify"

Hope it's general enough to be helpful ;) Cheers!

2oppin
  • 1,790
  • 18
  • 31
  • Thank you for the in-depth explanation. I'm awarding you the bounty because you clarified a number of confusing things I did not understand when conducting my own research on the topic. – Chad Dec 24 '18 at 16:15