Angularjs Custom select2 директива

Я создал простую пользовательскую директиву AngularJs для этого удивительного плагина jquery jQuery-Select2 следующим образом:

Директива

app.directive("select2",function($timeout,$parse){
    return {
        restrict: 'AC',
        link: function(scope, element, attrs) {
            $timeout(function() {
                $(element).select2();
            },200); 
        }
    };
});

Использование в HTML-шаблонах:

<select class="form-control" select2 name="country"
data-ng-model="client.primary_address.country"
ng-options="c.name as c.name for c in client.countries">
     <option value="">Select Country</option>
</select>

Он работает как ожидалось, и мой обычный select элемент заменяется плагинами select2.

Однако есть одна проблема, хотя иногда она показывает значение по умолчанию i.e Select Country здесь, хотя в выпадающем меню автоматически выбирается значение модели.

Теперь, если я увеличиваю $timeout интервал от 200 до некоторого высокого значения, скажем 1500, он работает, но задерживает рендеринг директивы. Также я думаю, что это не является правильным решением для него, так как мои данные загружаются через ajax.

Я также попытался обновить директиву следующим образом, но в этом не повезло:

app.directive("select2",function($timeout,$parse){
    return {
        restrict: 'AC',
        require: 'ngModel',
        link: function(scope, element, attrs) {
            var modelAccessor = $parse(attrs.ngModel);
            $timeout(function() {
                $(element).select2();
            });
            scope.$watch(modelAccessor, function (val) {
                if(val) {
                    $(element).select2("val",val);
                }
            });
        }
    };
});

PS: Я знаю, что есть аналогичный модуль, присутствующий ui-select, но он требует некоторой разной разметки в форме <ui-select></ui-select>, и мое приложение уже полностью разработано, и я просто хочу заменить стандартный флажок select2.

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

Ответы

Ответ 1

Это может быть проще, чем вы ожидали!

Пожалуйста, посмотрите на Plunker

В принципе, все плагины, часы Angularjs $должны основываться на чем-то. Я не уверен на 100% для jQuery-select2; но я думаю, что просто контроль нормальных событий DOM. (И в случае Angular $watch, это "грязный контрольный цикл" )

Моя идея заключается в том, чтобы позволить доверять jquery-Select2 и AngularJS для обработки этих событий изменений.

Нам просто нужно следить за изменениями в Angular и обновлять выбор в Select2 способами

var refreshSelect = function() {
    if (!element.select2Initialized) return;
    $timeout(function() {
        element.trigger('change');
    });
};

//...

scope.$watch(attrs.ngModel, refreshSelect);

Примечание: Я добавил в 2 новых часах, которые, я думаю, вы хотели бы иметь!

Ответ 2

Я не знаком с select2 (поэтому фактический API для получения и установки отображаемого значения в элементе управления может быть некорректным), но я предлагаю это в качестве альтернативы:

app.directive("select2",function($timeout){
    return {
        restrict: 'AC',
        require: 'ngModel',
        link: function(scope, element, attrs, model) {

            $timeout(function() {
                element.select2();
            });

            model.$render = function() {
                element.select2("val",model.$viewValue);
            }
            element.on('change', function() {
                scope.$apply(function() {
                    model.$setViewValue(element.select2("val"));
                });
            })
        }
    };
});

Первый $timeout необходим, потому что вы используете ng-options, поэтому параметры не будут находиться в DOM до следующего цикла дайджест. Проблема в том, что новые параметры не будут добавлены в элемент управления, если модель стран позже будет изменена вашим приложением.

Ответ 3

Angular не понравится, если данные модели будут изменены сторонним плагином. Мое предположение основывается на том, что при использовании $timeout существует условие гонки между Angular обновлением опций или модели и плагина select2. Решение, с которым я столкнулся, состоит в том, чтобы сделать обновление главным образом из рук Angular и сделать это вручную из директивы, таким образом вы можете гарантировать, что все будет соответствовать независимо от того, кто изменяет. В этой директиве я придумал:

app.directive("select2",function($timeout,$parse){
    return {
        restrict: 'AC',
        link: function(scope, element, attrs) {
            var options = [],
                el = $(element),
                angularTriggeredChange = false,
                selectOptions = attrs["selectOptions"].split(" in "),
                property = selectOptions[0],
                optionsObject = selectOptions[1];
            // watch for changes to the defining data model
            scope.$watch(optionsObject, function(n, o){
                var data = [];
                // format the options for select2 data interface
                for(var i in n) {
                    var obj = {id: i, text: n[i][property]};
                    data.push(obj);
                }
                el.select2({data: data});
                // keep local copy of given options
                options = n;
            }, true);
            // watch for changes to the selection data model
            scope.$watch(attrs["selectSelection"], function(n, o) {
                // select2 is indexed by the array position,
                // so we iterate to find the right index
                for(var i in options) {
                    if(options[i][property] === n) {
                        angularTriggeredChange = true;
                        el.val(i).trigger("change");
                    }
                }
            }, true);
            // Watch for changes to the select UI
            el.select2().on("change", function(e){
                // if the user triggered the change, let angular know
                if(!angularTriggeredChange) { 
                    scope.$eval(attrs["selectSelection"]+"='"+options[e.target.value][property]+"'");
                    scope.$digest();
                }
                // if angular triggered the change, then nothing to update
                angularTriggeredChange = false;
            });

        }
    };
});

Я добавил атрибуты select-options и select-model. Они будут использоваться для заполнения и обновления данных с помощью интерфейса select2. Вот пример html:

<select id="sel" class="form-control" select2 name="country"
  select-selection="client.primary_address.country" 
  select-options="name in client.countries" >
     <option value="">Select Country</option>
</select>
<div>Selected: {{client.primary_address.country}}</div>

Обратите внимание, что в директиве все еще есть какая-то очистка, и при принятии каких-либо предположений о входных данных, таких как "in" в атрибуте select-options, есть что-то. Он также не применяет атрибуты, а просто терпит неудачу, если они не существуют.

Также обратите внимание, что я использовал Select2 version 4, о чем свидетельствует el.val(i).trigger("change"). Возможно, вам придется отменить некоторые вещи, если используете более старую версию.

Вот jsfiddle demo директивы в действии.

Ответ 4

Он напрямую не отвечает на ваш вопрос, но, пожалуйста, возьмите его, поскольку есть некоторые люди, которые хотят сделать другой подход, а не придерживаться jQuery select2.

Я создал свой собственный для этой цели, потому что меня не удовлетворяли существующие, которые не соответствуют принципам Angular, сначала HTML.

Это еще рано, но я думаю, что все функции работают во всех современных браузерах.

https://github.com/allenhwkim/angular-autocomplete

Вот примеры

Ответ 5

Я попытался воспроизвести вашу проблему, и, похоже, она работает хорошо. вот скрипка, с которой я пришел:

http://jsfiddle.net/s24gLdgq/

У вас может быть другое поведение в зависимости от версии Angular и/или Select2, которую вы используете, не могли бы вы указать это?

Также, если вы хотите предотвратить мерцание, обязательно скройте тег <select> по умолчанию, чтобы ничего не отображалось до того, как выйдет элемент select2.

Это также делается в моем jsfiddle с помощью CSS

.form-control { width: 200px; opacity: 0 }