AngularJS — Вы уверены, что знаете как работает ng-if?


    Не так давно я уже писал про поведение ng-if директивы, но тогда я столкнулся с проверкой условия, но сегодня возникла другая проблема.

    В проекте достаточно много таких элементов как tooltip, popover, modal windows и так далее. Думаю, все вы понимаете, что это за элементы и рассказывать про них я не буду. Для многих из них используется абсолютное позиционирование. Если бы мы не использовали кастомные директивы, то проблем бы не было — все модальные окна лежали бы в конце body и показывались бы когда нужно. Но так, как все эти элементы объявлены как директивы, возникает проблема с позиционированием, так как у директивы может быть родитель с относительным позиционированием и так далее.

    <div style="position: relative; overflow:hidden">
      <button ng-click="visible = true">Greeting</button>
    
      <modal visible="visible">
          Hello, Habr!
      </modal>
    </div>
    


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

    module.directive('modal',[
    		'$rootElement',
    	function(
    		$rootElement
    	){
    		return {
    			restrict: 'E',
    			...
    			link: function(scope, element){
    				element.appendTo($rootElement);
    
    				scope.$on('$destroy', function(){
    					element.remove();
    				});
    
                    ...
    			}
    		}
    	}]
    });
    


    То есть мы выдераем элемент из текущего контекста и вставляем в рутовый элемент приложения. При удаление директивы — элемент удаляется. Все вроде ОК, но проблем не возникает до тех пор, пока в паре с таким подходом не используется ng-if директива.

    ng-if при отрицательном результате условия полностью удаляет DOM элемент, это я думаю многие знают, но не многие знают как это происходит.

    Вот исходники и собственно сам watcher ng-if атрибута.
    При положительном результате — создается комментарий document.createComment(' end ngIf: ' + $attr.ngIf + ' '); и в переменную block.clone помещается два значения:
    • 0 — сам элемент, для которого была объявлена ng-if директива
    • 1 — созданный комментарий


    В исходном коде страницы вы скорее всего часто видите подобное:


    На данном скриншоте — условие ng-if="!task.id" — положительное и элемент li, для которого объявлена директива есть в DOM дереве и находиться между комментариями и . Второе условие ng-if="validation.task.app_id" — отрицательное и между комментариями нету ничего.

    При отрицательном результате — destroy дочернего scope и удаление элементов. И самое интересное в функции getBlockElements:
    /**
     * Return the DOM siblings between the first and last node in the given array.
     * @param {Array} array like object
     * @returns {DOMElement} object containing the elements
     */
    function getBlockElements(nodes) {
      var startNode = nodes[0],
          endNode = nodes[nodes.length - 1];
      if (startNode === endNode) {
        return jqLite(startNode);
      }
    
      var element = startNode;
      var elements = [element];
    
      do {
        element = element.nextSibling;
        if (!element) break;
        elements.push(element);
      } while (element !== endNode);
    
      return jqLite(elements);
    }
    

    Что делает эта функция понятно из её описания — Return the DOM siblings between the first and last node in the given array.
    А аргумент nodes в нашем случаем массив из двух элементов, которые я описывал ваше. То есть функция вернет все элементы между основным элементом, для которого была объявлена директива ng-if и закрывающим комментарием , а если комментарий не был найден — то вернет все элементы после основного.

    К примеру, такой темплейт (#angular-application — рутовый элемент приложения):
    <div id="angular-application">
    	...
    	
    	<div style="position: relative; overflow: hidden">
    		<div style="position: absolute; right: 0; bottom: 0">
    			<modal ng-if="isFirstModal()" id="modal-1">...</modal>
    			<modal ng-if="isSecondModal()" id="modal-2">...</modal>
    		</div>
    		<div style="position: absolute; left: 0; bottom: 0">
    			<popover ng-if="isFirstPopover()" id="popover-1">...</popover>
    			<popover ng-if="isSecondPopover()" id="popover-2">...</popover>
    		</div>
    	</div>	
    
    	...
    </div>
    

    Компилится в такой html:
    <div id="angular-application">
    	...
    	
    	<div style="position: relative; overflow: hidden">
    		<div style="position: absolute; right: 0; bottom: 0">
    			<!-- ngIf: isFirstModal() -->
    			<!-- end ngIf: isFirstModal() -->
    			<!-- ngIf: isSecondModal() -->
    			<!-- end ngIf: isSecondModal() -->
    		</div>
    		<div style="position: absolute; left: 0; bottom: 0">
    			<!-- ngIf: isFirstPopover() -->
    			<!-- end ngIf: isFirstPopover() -->
    			<!-- ngIf: isSecondPopover() -->
    			<!-- end ngIf: isSecondPopover() -->
    		</div>
    	</div>	
    
    	...
    
    	<div id="popover-1" class="popover">...</div>
    	<div id="modal-1" class="modal-window">...</div>
    	<div id="modal-2" class="modal-window">...</div>
    	<div id="popover-2" class="popover">...</div>
    </div>
    

    То есть, как было написано выше — все модальные окна и поповеры, что бы не нарушалось позиционирование и их верстка перенесены в конце приложения, но комментарии остались на прежнем месте. И теперь, функция getBlockElements для .- вернет все элементы - #popover-1,#modal-1, #modal-2, #popover-2. То есть при отрицательном результате условия ng-if="isFirstPopover()" из DOM дерева будут удалены все эти элементы.

    Варианты решения:
    • Не делать так :) Не переносить элементы из директивы. Но такой вариант не подходит для сложных директив. К примеру у нас проекте есть кастомный фильтр для таблицы, который представлен одной директивной, но включает в себя кнопку, поповер, и модельное окно. И если не переность элемент - верстка внутри директивы ломается, и позиционирование активных элементов - тоже становится не верным;
    • Изначально размещать элементы в нужных местах. Тоже не подходит для меня, так как я считаю, что элементы должны находиться максимально близко к тем элементам, с которыми они взаимодействуют;
    • Не использовать ng-if в таких случаях. Именно так мы и поступили. Чуть чуть изменили код, но зато все работает. Что именно изменили не рассказываю в статье, потому что решение кастомнное для определенных директив - был добавлен дополнительный параметр enable;
    • Можно попробовать изменить приоритеты для директив. Приоритет кастомный директивы должен быть выше, чем у ng-if, то есть выше 600;
    • Переносить не рутовый элемент директивы, а дочерный. То есть: element.find('[append-to-root]').appendTo($rootElement);
    • Не объявлять для элемента ng-if директиву, а обернуть её:
      <div ng-if="condition">
        <my-custom-directive>...</my-custom-directive>
      </div>
      

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 8

    • UFO just landed and posted this here
        0
        Она вообще никак не влияет на то, о чем написано в топике. Тем более в коммит меседже написано Also, with WebComponents it is normal to have custom elements in the DOM.
        +1
        В angular ui bootstrap для модальных окон используют сервис, вместо директивы. Шаблон для модального окна передается в метод сервиса. Мне кажется, это более логичный подход для модальных окон, так как модальное окно как правило нужно открывать по какому-то событию, нежели привязывать жестко к значению какого-то логического выражения. Ну и модальное окно по логике не вписывается в шаблон основного документа, оно существует отдельно, и соответственно в dom должно быть тоже отдельно.
          0
          Как по мне, то лучше, что бы элементы, которые взаимодейсвуют друг с другом (например кнопка «Show modal» и само окно) были максимально близко к друг другу, тем более если используется много темплейтов.
          <button ng-click="show()">Show modal</button>
          <modal>
            Hello, I am modal window
          </modal>
          

          И так код более читаемый и понятный. Все заивсит от проекта. Можно использовать вместо директивы и сервис, а шаблон брать text/ng-template, но у нас много чего построено на именно директивах.
          0
          Странное решение, конечно. А, вообще, такое поведение у всех исключаемых элементов (у ng-repeat, например), вместо элемента вставляется комментарий, чтобы Ангуляр мог узнать куда потом добавлять клон. Это поведение transclude: 'element'. Тогда в element функции link будет комментарий (якорь), а в cloneElement функции transclude копия элемента (шаблон)
            0
            Решение как раз оказалось совсем не странное. Странно то, что мы автоматически перемещаем элемент из директивы в рутовый элемент. Но без этого никак:
            — Как говорил выше, я считаю, что элементы которые взаимодействуют друг с другом должны быть расположены максимально близко друг к другу в исходном коде
            — Если не перемещать элемент — то может поломаться его верстка (особенно если это сложные элементы — модальные окона, селект боксы, поповеры и так далее), так как он унаследует все CSS правила родителей.

            К примеру popover:
            <button popover-toggle>Add</button>
            <popover>
              Hello! I am popover
            </popover>
            

            На много удобнее, если он находится возле кнопки, с которой взаимодействует.
            Вполне возможно Shadow DOM будет в помощь :)

            Вообще я писал статью лишь по тому, что мен удивило как работает angularjs и для чего он использует комментарии.
              0
              Мы в проекте используем ui-bootstrap и таких проблем не возникало. Там, кстати, в настройках элемента можно указать, куда будет аппендится тултип (и т. п.), к родителю или к боди
              0
              А, вообще, такое поведение у всех исключаемых элементов (у ng-repeat, например), вместо элемента вставляется комментарий,

              Да, но я посмотрел их исходники уже после того, как написал топик.

            Only users with full accounts can post comments. Log in, please.