Несколько экземпляров jQuery с vars, событиями и DOM-манипуляциями

Я работаю над проектом, который начинался как просто создание функции, которая принимает аргумент (XML файл) и преобразует его в структуру HTML/CSS (более ранняя версия, которую можно найти здесь). Это отлично поработало. Тем не менее, я бы хотел реализовать больше возможностей и большую гибкость. Я прочитал эту тему (например, 1, 2, 3), но я не могу обвести вокруг него голову.

Мой плагин имеет множество специфичных для экземпляра запросов:

  • Переменные
  • Параметры
  • обработчики событий

и есть несколько важных вариантов:

  • отобразить полноэкранную версию
  • отображает обычную версию (поставляется с кнопкой "open fs version" ).
  • укажите контейнер для нормальной версии
  • установить шрифт нормальной версии
  • установить возможные шрифты для версии fs (для увеличения и уменьшения дерева)
  • установите класс, который, когда пользователь нажимает на элемент с этим классом, открывает версию fs

Я нарисую базовую структуру текущего состояния плагина.

Два первых варианта - самые важные. Но по умолчанию используется true, и если у пользователя есть оба из них на false, плагин не будет выполняться.

Затем плагин назначает глобальные переменные и создает новые элементы DOM на основе этой информации. На практике это выглядит примерно так (обратите внимание, что глобальные переменные объявляются в верхней части моего 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());
}

Вы увидите, что я работаю с идентификаторами, что затрудняет работу с несколькими экземплярами. Один из способов решения этого вопроса, я думал, - добавить класс для стилизации и добавить идентификатор к каждому экземпляру с помощью глобального счетчика, который отслеживает экземпляры (counter++ для каждого экземпляра). Обратите внимание, что anyTree используется, когда я хочу настроить таргетинг на дерево FS, а также на дерево нормальных представлений. Это НЕ означает, что я хочу настроить таргетинг на все деревья всех экземпляров! Это должно быть ограничено и для каждого экземпляра.

Итак, мой вопрос: как мне разрешить несколько экземпляров и особенно: как я могу перейти от глобальных переменных к локальным переменным, не теряя при этом силы, которые у меня есть сейчас? В этот момент я могу работать с глобальными переменными и получать доступ к каждой переменной везде, где захочу. Но как я могу ограничить глобальную переменную на один экземпляр? Работайте с этим счетчиком, как я предложил?

Также, где я могу назначить события? В настоящее время это так, как мой плагин инициализирует (я оставил глобальные переменные и функции):

$.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();
            });
        });
    }

Это подходящее место для размещения обработчиков событий?

Ответ 1

Этот визуализатор XML выглядит хорошо. У меня есть несколько указателей для вас.

Во-первых, вы создаете jquery-component/widget/plugin, используя $.fn.componentName вместо $.componentName.

$.fn.treeVisualizer

Во-вторых, я бы рекомендовал переместить все глобальные значения в параметры или локальные переменные в ваш компонент. Этот фрагмент кода, который вы предоставили, должен работать. Каждое глобальное значение, которое может быть различным для каждого экземпляра, должно быть вариантом.

$.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
}

В-третьих, для ваших глобальных функций я бы предложил включить их в функцию самозапускания вместе с компонентом, чтобы компонент мог получить доступ к этой функции, и они не являются ненужными глобальными.

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

Или вы можете поместить функции внутри области компонентов.

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

В-четвертых, я бы рекомендовал использовать классы вместо id. Вы действительно можете решить эту проблему, убедившись, что идентификатор уникален, отслеживая количество экземпляров, но просто используйте классы (fe, xml-id-1). Или вы также можете использовать атрибут data-id/data-xml-id. Не волнуйтесь о том, что запрос элементов с одним и тем же классом в разных компонентах, я подойду к этому в следующем указателе.

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

Или, если вы хотите воспользоваться атрибутами данных (которые я рекомендую по классам, потому что html легче читать и имеет больше значения imo)

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

Наконец, каждый запрос и событие должны быть относительно вашего компонента, чтобы вы только искали элементы и добавляли событие в экземпляр вашего компонента. Это гарантирует, что вы случайно не выполните действия над другим экземпляром вашего компонента.

$.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
  );
}

Затем, наконец, инициализируйте свой компонент таким образом.

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

Дополнительный вопрос из комментариев:

Найти метод

Метод find действует точно так же, как и глобальный метод запроса, с той разницей, что он ищет только в DOM объекта jQueryObject.

Получает потомки каждого элемента в текущем наборе согласованных элементов, фильтруется селектором, объектом jQuery или элементом. Источник: Документация jQuery

Пример:

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

это использование

Внутри плагина this относится к jQueryObject, в который вы инициализировали плагин.

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

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

Внутри обратного вызова события это относится к собственному элементу, в который вы также привязывали обработчик событий. Вы должны обернуть его в объект jQuery для выполнения jQuery-действия на нем.

$('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
});

Всякий раз, когда вы не знаете, где находится this, просто зарегистрируйте его на консоли. С помощью этой информации вы можете видеть, что при использовании this.find('.tree-structure') внутри вашего компонента вы действительно будете искать только элементы с классом древовидной структуры внутри объекта jQuery, с которым вы инициализировали компонент.

Ответ 2

Код ниже - это базовый формат, который я использую, когда создаю плагин. Я надеюсь, что это поможет вам найти решение некоторых из ваших проблем.

(function($) {
    $.fn.FUNCTIONNAME = function(options) {
        var obj = $(this);
        var opts = $.extend({
            OPTION1 : 'OPTION1 VALUE',
            OPTION2 : 'OPTION2 VALUE',
        }, options);
    }

    function FUNCTIONNAME(PARAMETER) {
        FUNCTION CONTENT
        IF YOU WANT TO USE OPTION VALUE,
        YOU CAN CALL IT BY A FORM AS opts.OPTION1
    }        

    obj.find($('ID OR CLASS DOESNT MATTER')).FUNCTIONNAME(PARAMETER);
    // this will make the plugin only work for this element

    return this; // or you can each() over every functions and return it
})(jQuery);

+ И я рекомендую вам не использовать document.ready внутри плагина. Обработчики событий можно поместить куда угодно.

EDIT: this → obj

Ответ 3

Вы можете ссылаться на элемент DOM, который вы вставляете без повторного запроса, с appendTo, возвращающим ссылку на вставленные элементы

//before:
$(args.container).append('<div id="tree-visualizer" style="display: none"></div>');
$SS = $("#tree-visualizer"); //prefixing variables with $ when referencing jquery objects

//after:
$SS = $('<div style="display: none"></div>').appendTo( $(args.container) );

Если вы применяете привязки кликов как часть кода инициализации

$SS.append(SSHTML).on('click', '.tv-show-fs', function() {
    $FS.show();
});

вы заметите, что SS и FS теперь могут быть локально локальными.

В качестве альтернативы вы можете отключить функциональность "показать в полноэкранном режиме" от основного плагина и избежать дублирования функций. Наивный подход может заключаться в переключении некоторых стилей, но даже если вы хотите манипулировать DOM, вы получаете эту идею. Это не похоже на то, что вы можете иметь более одного полноценного полноэкранного экземпляра одновременно.

$SS.append(SSHTML).on('click', '.tv-show-fs', function() {
    $SS.toggleClass('tree-visualiser__fullscreen');
});