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

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

Каков наилучший подход для этого? Как я вижу это, у нас есть два варианта: 1. Пусть каждая директива связывается с событием и делает это волшебство для текущего элемента 2. Создайте глобальный прослушиватель событий, который выполняет селектор DOM, чтобы получить каждый элемент, на который должна применяться логика.

Вариант 1 имеет то преимущество, что у вас уже есть доступ к элементу, на котором вы хотите выполнить некоторые операции. Но... варианты 2 имеют то преимущество, что вам не нужно связывать несколько раз (для каждой директивы) на одном и том же мероприятии, которое может быть выгодным для производительности.

Обозначьте обе опции:

Вариант 1:

angular.module('app').directive('myDirective', function(){

     function doSomethingFancy(el){
         // In here we have our operations on the element
    }

    return {
        link: function(scope, element){
             // Bind to the window resize event for each directive instance.
             angular.element(window).on('resize', function(){
                  doSomethingFancy(element);
             });
        }
    };
});

Вариант 2:

angular.module('app').directive('myDirective', function(){

    function doSomethingFancy(){
         var elements = document.querySelectorAll('[my-directive]');
         angular.forEach(elements, function(el){
             // In here we have our operations on the element
         });
    }

    return {
        link: function(scope, element){
             // Maybe we have to do something in here, maybe not.
        }
    };

    // Bind to the window resize event only once.
    angular.element(window).on('resize', doSomethingFancy);
});

Оба подхода работают нормально, но я считаю, что вариант второй не действительно "Angular -ish".

Любые идеи?

Ответ 1

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

app.directive('resize', function($window) {
  return {
    link: function(scope) {
      function onResize(e) {
        // Namespacing events with name of directive + event to avoid collisions
        scope.$broadcast('resize::resize');
      }

      function cleanUp() {
        angular.element($window).off('resize', onResize);
      }

      angular.element($window).on('resize', onResize);
      scope.$on('$destroy', cleanUp);
    }
  }
});

Что можно использовать в основном случае в корневом элементе приложения

<body ng-app="myApp" resize>...

И затем прослушайте событие в других директивах

<div my-directive>....

закодировано как:

app.directive('myDirective', function() {
  return {
    link: function(scope, element) {
      scope.$on('resize::resize', function() {
        doSomethingFancy(element);
      });
    });
  }
});

Это имеет ряд преимуществ по сравнению с другими подходами:

  • Не хрупкий для точной формы того, как используются директивы. Ваш вариант 2 требует my-directive, когда Angular рассматривает следующее как эквивалентное: my:directive, data-my-directive, x-my-directive, my_directive, как показано в руководстве для директивы

  • У вас есть одно место, чтобы точно повлиять на то, как событие Javascript преобразуется в событие Angular, которое затем влияет на всех слушателей. Предположим, что позже вы захотите отменить событие javascript resize, используя функцию Lodash debounce. Вы можете изменить директиву resize на:

    angular.element($window).on('resize', $window._.debounce(function() {
      scope.$broadcast('resize::resize');
    },500));
    
  • Поскольку он не обязательно запускает события на $rootScope, вы можете ограничить события только частью своего приложения, просто переместив туда, куда вы поместите директиву resize

    <body ng-app="myApp">
      <div>
        <!-- No 'resize' events here -->
      </div>
      <div resize>
        <!-- 'resize' events are $broadcast here -->
      </div>
    
  • Вы можете расширить директиву с помощью опций и использовать ее по-разному в разных частях приложения. Предположим, вы хотите, чтобы разные разблокированные версии в разных частях:

    link: function(scope, element, attrs) {
      var wait = 0;
      attrs.$observe('resize', function(newWait) {
        wait = $window.parseInt(newWait || 0);
      });
      angular.element($window).on('resize', $window._.debounce(function() {
        scope.$broadcast('resize::resize');
      }, wait));
    }
    

    Используется как:

    <div resize>
      <!-- Undebounced 'resize' Angular events here -->
    </div>
    <div resize="500">
      <!-- 'resize' is debounced by 500 milliseconds -->
    </div>
    
  • Вы можете позже распространить эту директиву на другие события, которые могут быть полезны. Возможно, такие вещи, как resize::heightIncrease. resize::heightDecrease, resize::widthIncrease, resize::widthDecrease. Затем у вас есть одно место в вашем приложении, которое занимается запоминанием и обработкой точных размеров окна.

  • Вы можете передавать данные вместе с событиями. Скажем, как высота/ширина видового экрана, где вам может понадобиться решать проблемы с несколькими браузерами (в зависимости от того, как далеко назад вам нужна поддержка IE, и укажите ли вы другую библиотеку, чтобы помочь вам).

    angular.element($window).on('resize', function() {
      // From http://stackoverflow.com/a/11744120/1319998
      var w = $window,
          d = $document[0],
          e = d.documentElement,
          g = d.getElementsByTagName('body')[0],
          x = w.innerWidth || e.clientWidth || g.clientWidth,
          y = w.innerHeight|| e.clientHeight|| g.clientHeight;
      scope.$broadcast('resize::resize', {
        innerWidth: x,
        innerHeight: y
      });
    });
    

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

По существу, этот проект обеспечивает способ конвертировать глобальные события Javascript в локальные события Angular и локально не только к приложению, но и локально к различным частям приложения, в зависимости от размещения директива.

Ответ 2

При разработке поверх фреймворка я часто считаю полезным думать об ошибке перед созданием идиоматики. Ответ на "что" и "почему" вытесняет "как".

Ответ здесь действительно зависит от сложности doSomethingFancy(). Существуют ли данные, набор функциональных возможностей или объект домена, связанные с экземплярами этой директивы? Это чисто информационная проблема, например, настройка свойств width или height определенных элементов на соответствующие пропорции размера окна? Убедитесь, что вы используете правильный инструмент для работы; не приносите весь швейцарский армейский нож, когда работа требует пинцета, и у вас есть доступ к автономной паре. Для продолжения в этом ключе я буду работать с предположением, что doSomethingFancy() является чисто презентационной функцией.

. Опасность переноса глобального события в событии Angular может быть решена с помощью простой конфигурации фазы запуска:

angular.module('myApp')
    .run(function ($rootScope) {
        angular.element(window).on('resize', function () {
            $rootScope.$broadcast('global:resize');  
        })
    })
;

Теперь Angular не нужно выполнять всю работу, связанную с директивой, для каждого $digest, но вы получаете ту же функциональность.

Вторая проблема связана с количеством элементов n при запуске этого события. Опять же, если вам не нужны все колокола и свистки директивы, есть другие способы сделать это. Вы можете расширить или адаптировать подход в предыдущем блоке:

angular.module('myApp')
    .run(function () {
        angular.element(window).on('resize', function () {
            var elements = document.querySelectorAll('.reacts-to-resize');
        })
    })
;

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

/**
 * you can inject any services you want: $rootScope if you still want to $broadcast (in)
 * which case, you'd have a "Publisher" instead of a "Mediator"), one or more services 
 * that maintain some domain objects that you want to manipulate, etc.
 */
function ResizeMediator($window) {
    function doSomethingFancy() {
        // whatever fancy stuff you want to do
    }

    angular.element($window).bind('resize', function () {
        // call doSomethingFancy() or maybe some other stuff
    });
}

angular.module('myApp')
    .service('resizeMediator', ResizeMediator)
    .run(resizeMediator)
;

Теперь у нас есть инкапсулированная служба, которая может быть проверена модулем, но не запускает неиспользуемые этапы выполнения.

Пара вопросов, которые также повлияют на решение:

  • Мертвые слушатели - с вариантом 1 вы создаете хотя бы один прослушиватель событий для каждого экземпляра директивы. Если эти элементы динамически добавляются или удаляются из DOM и вы не вызываете $on('$destroy'), вы рискуете использовать обработчики событий, когда их элементы больше не существуют.
  • Производительность операторов ширины/высоты. Я предполагаю, что здесь существует логика модели, учитывая, что глобальное событие - это изменение размера браузера. Если нет, игнорируйте это; если это так, вы хотите быть осторожным в отношении того, какие свойства вы получаете и как часто, потому что переходы браузера могут быть огромным виновником ухудшения производительности.

Вероятно, этот ответ не как "Angular", как вы надеялись, но так, как я бы решил проблему, поскольку я понимаю это с добавлением предположения о логике только для модели.

Ответ 3

По-моему, я бы пошел с методом # 1 и немного подгонял использование сервиса $window.

angular.module('app').directive('myDirective', function($window){

     function doSomethingFancy(el){
         // In here we have our operations on the element
    }

    return {
        link: function(scope, element){
             // Bind to the window resize event for each directive instance.
             anguar.element($window).bind('resize', function(){
                  doSomethingFancy(element);
             });
        }
    };
});

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

EDIT. Чем больше я думаю об этом методе, тем больше я действительно начинаю любить его по первому... Отличный надежный способ прослушивания события изменения размера окна - возможно, в будущем что-то иначе нужно "знать" эту информацию, и если вы не сделаете что-то подобное, вам придется настроить - еще раз - еще один прослушиватель событий для события window.resize.

app.run

app.run(function($window, $rootScope) {
  angular.element($window).bind('resize', function(){
     $rootScope.$broadcast('window-resize');
  });
}

Директива  angular.module('app'). директива ('myDirective', function ($ rootScope) {

     function doSomethingFancy(el){
         // In here we have our operations on the element
    }

    return {
        link: function(scope, element){
             // Bind to the window resize event for each directive instance.
             $rootScope.$on('window-resize', function(){
                  doSomethingFancy(element);
             });
        }
    };
});

Наконец Огромным источником того, как делать материал, является сопровождение парней angular -ui, например ui-bootstrap. Я узнал о том, как набирать вещи у этих ребят, например, радости обучения до unit test в angular. Они обеспечивают отличную чистую кодовую базу для проверки.

Ответ 4

Второй подход кажется более хрупким, поскольку Angular предлагает множество способов ссылаться на директиву в шаблоне (my-directive, my_directive, my:directive, x-my-directive, data-my-directive и т.д.), поэтому селектор CSS, охватывающий их все, может стать действительно сложным.

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

Но я был бы прагматичным. Если вы имеете дело с несколькими экземплярами, перейдите к # 1. Если у вас их сотни, я бы пошел с № 2.

Ответ 5

В этом случае вы можете просто сохранить свои элементы в массиве, а затем в "глобальном событии вы можете прокручивать элементы и делать то, что вам нужно.

angular.module('app').directive('myDirective', function($window){

    var elements = [];

    $window.on('resize', function(){
       elements.forEach(function(element){
           // In here we have our operations on the element
       });
    });

    return {
        link: function(scope, element){
            elements.push(element);
        }
    };
});