Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple jQuery instances with vars, events, and DOM manipulation

I am working on a project that started out as simply building a function that takes an argument (XML file) and converts it into a HTML/CSS structure (earlier version to be found here). This worked great. However, I'd like to implement more options and more flexibility. I've read on the topic (e.g. 1, 2, 3) but I can't wrap my head around it.

My plugin has a lot instance-specific needs:

  • variables
  • options
  • event handlers

and there are some important options:

  • display a fullscreen version
  • display a normal version (comes with a "open fs version" button)
  • specify a container for the normal version
  • set fontsize of normal version
  • set possible fontsizes for fs version (for zooming in and out the tree)
  • set a class that, when a user clicks on an element with that class, opens the fs version

I'll sketch the base structure of the current state of the plugin.

The two first options are the most important ones. But default to true, and if a user has both of them on false, the plugin won't execute.

The plugin then assigns global variables and creates new DOM elements based on this information. In practice it looks something like this (note that the global variables are declared at the top of my script).

function initVars(args) {
    fontsizes = fsFontSizes;
    errorContainer = $(".tv-error");
    var trees = [],
        tooltips = [];

    if (args.normalView) {
        normalView = true;
        $(args.container).append('<div id="tree-visualizer" style="display: none"></div>');
        SS = $("#tree-visualizer");
        var SSHTML = '<div class="tv-error" style="display: none"><p></p></div>' +
            '<div class="tree" style="font-size: ' + args.fontSize + 'px;"></div>' +
            '<aside class="tooltip" style="display: none"><ul></ul>' +
            '<button>&#10005;</button></aside>';
        if (args.fsView) {
            SSHTML += '<button class="tv-show-fs">Fullscreen</button>';
        }
        SS.append(SSHTML);

        treeSS = SS.find(".tree");
        tooltipSS = SS.find(".tooltip");

        trees.push("#tree-visualizer .tree");
        tooltips.push("#tree-visualizer .tooltip");
    }
    if (args.fsView) {
        fsView = true;
        $("body").append('<div id="fs-tree-visualizer-" class=""fs-tree-visualizer" style="display: none"></div>');
        FS = $("#fs-tree-visualizer");
        var FSHTML = '<div class="tv-error" style="display: none"><p></p></div>' +
            '<div class="tree"></div><aside class="tooltip" style="display: none"><ul></ul>' +
            '<button>&#10005;</button></aside><div class="zoom-opts"><button class="zoom-out">-</button>' +
            '<button class="zoom-default">Default</button><button class="zoom-in">+</button>' +
            '<button class="close">&#10005;</button></div>';
        FS.hide().append(FSHTML);
        treeFS = FS.find(".tree");
        tooltipFS = FS.find(".tooltip");
        zoomOpts = FS.find(".zoom-opts");
        zoomCounter = Math.round(fontSizes.length / 2);

        trees.push("#fs-tree-visualizer .tree");
        tooltips.push("#fs-tree-visualizer .tooltip");
    }

    if (args.fsBtn != "") {
        $(args.fsBtn).addClass("tv-show-fs");
    }

    anyTree = $(trees.join());
    anyTooltip = $(tooltips.join());
}

You'll see that I am working with IDs which makes it hard to work with multiple instances. One way to solve this, I thought, would be to add a class for styling, and add an ID to each instance by using a global counter that keeps track of the instantiations (counter++ on each instance). Note that anyTree is used when I wish to target FS tree as well as the normal views tree. This does NOT mean that I want to target all trees of all instances! This has to be restricted per instance as well.

So my question is, how do I allow for multiple instances and especially: how can I move from global variables to local variables without losing the power that I have now? At this moment I can work with global variables and access each variable wherever I want. But how can I restrict the global variable per instance? Work with that counter as I proposed?

Also, where do I assign events? Currently this is how my plugin initializes (I left out global variables and functions):

$.treeVisualizer = function(xml, options) {
        var args = $.extend({}, $.treeVisualizer.defaults, options);

        /* At least one of the arguments (default true) have to be true */
        if (args.normalView || args.fsView) {
            initVars(args);
            loadXML(xml);

        } else {
            console.error("Cannot initialize Tree Visualizer: either the container " +
                "does not exist, or you have set both normal and fullscreen view to " +
                "false, which does not make sense.");
        }

        /* Event handlers -- only after we've initiated the variables to globals */
        $(document).ready(function() {
            // Show fs-tree-visualizer tree
            $(".tv-show-fs").click(function(e) {
                // Show corresponding fullscreen version
                FS.show();
                // Execute some functions
                sizeTreeFS();
                e.preventDefault();
            });

            // Zooming
            zoomOpts.find("button").click(function() {
                var $this = $(this);
                // Do something
            });

            anyTree.on("click", "a", function(e) {
                // Do something, and execute function: has to 
                // target the right tooltip
                tooltipPosition();
                e.preventDefault();
            });
        });
    }

Is this the right place to put event handlers?

like image 273
Bram Vanroy Avatar asked Mar 21 '16 11:03

Bram Vanroy


1 Answers

That XML visualizer looks nice. I have a couple of pointers for you.

First off, you create a jquery-component/widget/plugin by using $.fn.componentName instead of $.componentName.

$.fn.treeVisualizer

Second, I would recommend to move all the global values into options, or local variables into your component. The code snippet you provided should work. Every global value that could be different for every instance has to be an option.

$.fn.myComponent(options){
  var defaults = {
     option1: true,
     option2: 5
  };
  options = $.extend({}, defaults, options);//defaults and options are being merged into {}
  var constantValue = 15; //values that won't change, you can just move locally from global
}

Third, for your global functions, I would suggest putting them in a self-invoking function together with the component, so that the component can access the function and they are not unnecesarely global.

(function initComponent(){
  $.fn.myComponent(){
    someFunction();
  }
  function someFunction(){
    //do something
  }
})();
//immediately invoke function/ self-invoking function

Or you can put the functions inside the component scope.

$.fn.myComponent = function(){
  someFunction();
  function someFunction(){
    //do something
  }
}

Fourth, I would recommend using classes instead of id's. You could indeed solve this by making sure the id is unique by keeping track of the amount of instances there are, but simply use classes (fe, xml-id-1). Or you could also use a data-id/data-xml-id attribute. Don't worry about querying elements with the same class in different components, I'll get to that in the next pointer.

$jqueryElement.addClass('xml-id-'+id);
$('.xml-id-'+id);//you can query it by class-name like this

Or if you want to take advantage of data-attributes (which I recommend over classes because the html is easier to read and has more meaning imo)

$jqueryElement.attr('data-xml-id', id); //fe <div data-xml-id="1"></div>
$('[data-xml-id="'+id+'"]'); //you can query it like this

Lastly, every query and event needs to be relative to your component, so that you only search for elements and add event within the instance of your component. This makes sure that you don't accidently perform actions on another instance of your component.

$.fn.myComponent=function(){
  $('[data-xml-id="1"]'); //this queries the whole DOM, don't do this!
  var $el = this.find('[data-xml-id="1"]'); //this only looks for matching elements within the current instance of the component
  $el.click(function(){// also add events to elements you searched within your components
    //do something
  );
}

Then finally initialize your component like so.

$('#tree1').treeVisualizer(xml,
  {
    fullscreen: true
  }
);
$('#tree2').treeVisualizer(xml);

Extra question from comments:

Find method

The find method acts just the same as the global query method, with the difference that it only searches within the DOM of the jQueryObject.

Get the descendants of each element in the current set of matched elements, filtered by a selector, jQuery object, or element. Source: jQuery documentation

Example:

var footer = $('footer');
footer.find('div');// searches for div's inside the footer

this usage

Inside a plugin this refers to the jQueryObject you initialized the plugin on.

$.fn.myComponent = function(){
  console.log(this);// refers to $el
}

var $el = $('#id');
$el.myComponent();

Inside an event callback, this refers to the native element where you attached the event handler too. You have to wrap it in a jQuery object to perform jQuery action upon it.

$('button').click(function(){
  console.log(this); //prints out the button that was clicked
  $(this).css('background','#000'); //wrap native button element in jQuery object and perform jQuery methods on it
});

Whenever you are unsure where this refers too, simply log it to the console. With this info, you can see that when you use this.find('.tree-structure') inside your component, you will indeed only search for elements with the 'tree-structure'-class inside the jQuery-object you initialized the component with.

like image 190
Swimburger Avatar answered Nov 16 '22 01:11

Swimburger