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 дереве и находиться между комментариями <!-- ngIf: !task.id --> и <!-- end ngIf: !task.id -->. Второе условие 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 и закрывающим комментарием <!-- eng ngIf: .... -->, а если комментарий не был найден — то вернет все элементы после основного.

К примеру, такой темплейт (#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 ng-if="isFirstPopover()" id="popover-1">.вернет все элементы#popover-1,#modal-1, #modal-2, #popover-2. То есть при отрицательном результате условия ng-if="isFirstPopover()" из DOM дерева будут удалены все эти элементы.

Варианты решения:

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

ссылка на оригинал статьи http://habrahabr.ru/post/225891/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *