Визуализация угловых директив в элементах Selectize.js

Я использую angular-selectize для использования Selectize.js в моем проекте angular.

Чтобы использовать настраиваемые элементы в селекторе Selectize.js, я использую Selectize.js' рендеринг:

render: {
  item: function(item, escape) {
    var avatar = '<div>' +
        '<span avatars="\'' + escape(item._id) +'\'" class="avatars">' +
        '</span>' +
        escape(item.nick) +
      '</div>';
    var compiledAvatar =  $compile(avatar)($rootScope);
    $timeout();
    return compiledAvatar.html();
  },

где аватары — это пользовательская директива с асинхронным поведением

Проблема в том, что функция render.item ожидает в качестве вывода строку HTML, но:

  • Невозможно синхронно вернуть обработанную или "$compileed" HTML-строку, как ожидается методом render.item.
  • Я не знаю, как впоследствии отображать элементы этого элемента, когда они уже добавлены в DOM.

Обратите внимание, что хотя $compile вызывается, возвращаемая строка будет не ожидаемым результатом компиляции, а строкой перед компиляцией из-за асинхронного характера $compile.


person atfornes    schedule 29.06.2016    source источник
comment
Добавлено как проблема в github angular-selectize: github.com/machineboy2045/angular-selectize/ вопросы/142   -  person atfornes    schedule 30.06.2016
comment
Возможно, это не самый предпочтительный способ ведения дел, но не могли бы вы использовать промисы для получения желаемого синхронного поведения?   -  person Pytth    schedule 01.07.2016
comment
@Pytth, я не знаю, как здесь может помочь обещание, обратите внимание, что функция render.item должна возвращать html string, а не обещание. Пожалуйста, поделитесь идеями о том, как использовать обещания для этой проблемы. Спасибо!   -  person atfornes    schedule 01.07.2016
comment
Вам действительно нужно использовать $compile() и $timeout()? Почти уверен, что это невозможно внутри render.item, потому что selectize ожидает, что это будет функция синхронизации. Вы пытались просто вернуть аватар в виде строки html?   -  person andree    schedule 04.07.2016
comment
Вы правы, @andree, $compile и $timeout не нужны. Я включил их в пример, чтобы можно было увидеть проблему. Я обновил вопрос, чтобы лучше отразить это.   -  person atfornes    schedule 04.07.2016


Ответы (2)


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

Чтобы имитировать ваш асинхронный вызов, я использую ngResource. Моя функция рендеринга возвращает строку "<div class='compiledavatar'>Temporary Avatar</div>" со специальной разметкой класса compiledavatar. На секунду или две вы увидите временный аватар при выборе элемента. Когда вызовы ngResource заканчиваются, я ищу элемент с классом compiledavatar, а затем заменяю html тем, что скачал. Вот полный код:

var app = angular.module('plunker', ['selectize', 'ngResource']);

app.controller('MainCtrl', function($scope, $resource, $document) {
  var vm = this;
  vm.name = 'World';
  vm.$resource = $resource;
  vm.myModel = 1;
  vm.$document = $document;

  vm.myOptions = [{
    id: 1,
    title: 'Spectrometer'
  }, {
    id: 2,
    title: 'Star Chart'
  }, {
    id: 3,
    title: 'Laser Pointer'
  }];

  vm.myConfig = {
    create: true,
    valueField: 'id',
    labelField: 'title',
    delimiter: '|',
    placeholder: 'Pick something',
    onInitialize: function(selectize) {
      // receives the selectize object as an argument
    },
    render: {
      item: function(item, escape) {
        var label = item.title;
        var caption = item.id;
        var Stub = vm.$resource('mydata', {});
        // This simulates your asynchronous call

        Stub.get().$promise.then(function(s) {
          var result = document.getElementsByClassName("compiledavatar")
          angular.element(result).html(s.compiledAvatar);

          // Once the work is done, remove the class so next time this element wont be changed
          // Remove class
          var elems = document.querySelectorAll(".compiledavatar");
          [].forEach.call(elems, function(el) {
              el.className = el.className.replace(/compiledavatar/, "");
          });

        });

        return "<div class='compiledavatar'>Temporary Avatar</div>"
      }
    },
    // maxItems: 1
  };
});

Чтобы имитировать JSON API, я только что создал файл в plunker mydata:

{
    "compiledAvatar": "<div><span style='display: block; color: black; font-size: 14px;'>an avatar</span></div>"
  }

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

Кроме того, если ваш динамический код является директивой Agular, вот второй планкер с пользовательской директивой и случайным образом данные, чтобы вы могли лучше увидеть решение:

Данные включают пользовательскую директиву my-customer:

[{
    "compiledAvatar": "<div><span style='display: block; color: black; font-size: 14px;'>an avatar  #1  <my-customer></my-customer></span></div>"
  },
  {
    "compiledAvatar": "<div><span style='display: block; color: black; font-size: 14px;'>an avatar  #2  <my-customer></my-customer></span></div>"
  },
(...)

Директива определяется как:

app.directive('myCustomer', function() {
  return {
    template: '<div>and a custom directive</div>'
  };
});

И основное отличие приложения в том, что при замене HTML нужно добавить $compile и текст должен показывать An avatar #(number) and a custom directive. Я получаю массив значений json и использую простой случайный выбор для выбора значения. После замены HTML я удаляю класс, поэтому в следующий раз будет изменен только последний добавленный элемент.

 Stub.query().$promise.then(function(s) {
      var index = Math.floor(Math.random() * 10);
      var result = document.getElementsByClassName("compiledavatar")
      angular.element(result).html($compile(s[index].compiledAvatar)($scope));

      // Remove class
      var elems = document.querySelectorAll(".compiledavatar");
      [].forEach.call(elems, function(el) {
        el.className = el.className.replace(/compiledavatar/, "");
      });
});

Кроме того, я просмотрел библиотеку selectize, и вы не можете вернуть обещание... как это делает html.replace для значения, возвращаемого рендером. Вот почему я пошел по пути временной строки с классом, чтобы получить его позже и обновить. Дайте мне знать, если это поможет.

person Gregori    schedule 06.07.2016
comment
К сожалению, это решение имеет некоторые проблемы и не работает полностью из-за кеша отображаемых элементов Selectize.js. Пожалуйста, посмотрите мой следующий ответ для решения этой проблемы: stackoverflow.com/a/38640167/4928558 - person atfornes; 28.07.2016
comment
@atfornes Хороший улов. Я не сталкивался с проблемой кеша при тестировании, но спасибо за улучшение решения и за то, что поделились с нами. - person Gregori; 28.07.2016

Этот ответ основан на полезном ответе @gregori со следующими отличиями:

  1. Примите во внимание кэш рендеринга Selectize.js. Стандартное поведение Selectize.js заключается в том, что элементы кэшируются в том виде, в каком они были возвращены функцией рендеринга, а не с внесенными в них изменениями. После добавления и удаления некоторых элементов будет отображаться кешированная, а не измененная версия, если мы не обновим соответственно кеш рендеринга.
  2. Использование случайных идентификаторов для определения элементов, которые нужно выбрать для управления из DOM.
  3. Использование наблюдателей, чтобы узнать, когда компиляция завершена

Во-первых, мы определяем метод для изменения кэша рендеринга selectize.js:

scope.selectorCacheUpdate = function(key, value, type){

  var cached = selectize.renderCache[type][key];

  // update cached element
  var newValue = angular.element(cached).html(value);

  selectize.renderCache[type][key] = newValue[0].outerHTML;

  return newValue.html();
};

Затем функция рендеринга определяется следующим образом:

function renderAvatar(item, escape, type){

  // Random id used to identify the element
  var randomId = Math.floor(Math.random() * 0x10000000).toString(16);

  var avatar = 
    '<div id="' + randomId + '">' +
      '<span customAvatarTemplate ...></span>' +
      ...
    '</div>';

  var compiled = $compile(avatar)($rootScope);

  // watcher to see when the element has been compiled
  var destroyWatch = $rootScope.$watch(
    function (){
      return compiled[0].outerHTML;
    },
    function (newValue, oldValue){
      if(newValue !== oldValue){

        var elem = angular.element(document.getElementById(randomId));

        var rendered = elem.scope().selectorCacheUpdate(item._id, compiled.html(), type);

        // Update DOM element
        elem.html(rendered);

        destroyWatch();
      }
    }
  );
});

return avatar;

}

Примечание. Ключом для кеша рендеринга является valueField элементов выбора, в данном случае _id

Наконец, мы добавляем эту функцию как функцию рендеринга selectize в объект конфигурации selectize:

config = {
  ...
  render: {
    item: function(i,e){
      return renderAvatar(i, e, 'item');
    },
    option: function(i,e){
      return renderAvatar(i, e, 'option');
    }
  },
  ...
}

Для получения дополнительной информации см., как это решение было добавлено в приложение, которое мотивировало этот вопрос: https://github.com/P2Pvalue/teem/commit/968a437e58c5f1e70e80cc6aa77f5aefd76ba8e3.

person atfornes    schedule 28.07.2016