Общение директив между собой в AngularJS

И снова привет!

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

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

Ну что же, приступим к написанию нашего приложения, которое прояснит нам смысл этой загадочной опции require:

Разметка:

<developer>developer</developer>

Код приложения:

angular.module("developerApp", [])
.directive("developer", function() {
	return {
		restrict: "E",
		controller: function($scope) {
			$scope.skills = [];
			this.iKnowJS = function() {
				$scope.skills.push("JS");
			};
			this.iKnowHTML = function() {
				$scope.skills.push("HTML");
			};
			this.iKnowAngularJS = function() {
				$scope.skills.push("AngularJS");
			};
		},
		link: function(scope, element){
			element.bind("mouseenter", function() {
				console.log(scope.skills);
			});
		}
	}
});

В этом коде мы видим директиву developer со своим контроллером, который будет использоваться в качестве API (Application Programming Interface) при взаимодействии с другими директивами. Суть директивы developer проста — при наведении на элемент в консоль выводится список навыков разработчика.

Далее, мы добавим следующий код, который создаёт ещё одну директиву внутри приложения для взаимодействия с директивой developer.

.directive("js", function() {
	return {
		require: "developer",
		link: function (scope, element, attrs, developerCtrl) {
			developerCtrl.iKnowJS();
		}
	};
});

Как видно из кода, у директивы есть свойство require, которое делает инъекцию контроллера, расположенного в другой директиве developer, в link-функцию создаваемой путём добавления параметра функции (здесь это developerCtrl).

Надо заметить, что у этого параметра, запрашивающего контроллер, нет требований и соглашений по именованию.

Создадим ещё две директивы — «html» и «angularjs»:

.directive("html", function() {
	return {
		require: "developer",
		link: function (scope, element, attrs, developerCtrl) {
			developerCtrl.iKnowHTML();
		}
	};
}).directive("angularjs", function() {
	return {
		require: "developer",
		link: function (scope, element, attrs, developerCtrl) {
			developerCtrl.iKnowAngularJS();
		}
	};
});

Теперь каждая из этих директив связана с внешними API-методами контроллера директивы developer.

Мы можем изменить нашу разметку, добавив новые навыки нашему разработчику:

<developer js html angularjs>Senior developer</developer>

Как и предполагалось, отдельные директивы взаимодействуют с контроллером директивы элемента.

Изолированная область видимости

Но не всё так хорошо, как нам кажется! Давайте создадим несколько экземпляров директивы developer:

<developer js html angularjs>Senior developer</developer>
<developer html>Junior developer</developer>

Несмотря на то, что у каждой из них свой набор дополняющих директив, каждый экземпляр обрабатывается последовательно. И поскольку область видимости у этих двух элементов с директивой developer общая, и инициализация каждой из них перезаписывает значение предыдущей, мы получаем следствие — на какой бы элемент мы не навели курсор, получим значения от последней директивы.

Решением данной проблемы будет создание изолированной области видимости для директивы developer. Давайте так и сделаем:

angular.module("developerApp", [])
.directive("developer", function() {
	return {
		restrict: "E",
		scope: {},
		controller: function($scope) {
			$scope.skills = [];
		...
	};
});

Вы можете посмотреть на полный код нашего приложения:



Общение с вышерасположенной директивой

В примерах выше происходило общение директив одного DOM-элемента (в нашем случае элемент developer). Но в некоторых случаях есть необходимость во взаимодействии между директивами на разных уровнях. Например, нижележащий элемент что-то делает с вышестоящим. На этот случай также есть готовое решение внутри фрэймворка. Для этого достаточно добавить в значение опции require префикс «^» («циркумфлекс» или «крышечка» в простонародье). Этот префикс означает, что директива будет искать контроллер на своем элементе или на элементах, расположенных выше.
Посмотрите на изменение кода в нашем приложении. Тут показано на примере общение вложенной директивы и расположенной на том же элементе:



Маленькое дополнение

Теперь, когда мы знаем, для чего нужна опция require, будет приятно узнать, что она может принимать в качестве значения несколько параметров, т.е. общение может происходить с несколькими директивами одновременно. В таком случае мы получим в функцию link вызывающей директивы не контроллер, а массив контроллеров. Пример:

require: ["developer", "^anyParentDirective"],
...
link: function(scope, element, attrs, controllers)

И ещё, хотелось бы с тобой поделиться советом из документации AngularJS по написанию директив. Кто-то из вас может спросить, в чём разница опции controller и link у директив? Основное различие в том, что контроллер может выставлять некий API наружу. Отсюда вывод, который относится к категории «Best Practices»: используйте опцию controller, если вы хотите дать API для других директив. В других случаях используйте функцию link.

Заключение

Ну что же? Я очень надеюсь, что эта статья тебе понравилась, и ты стал лучше понимать междирективное взаимодействие в AngularJS. Используя этот метод можно решить многие нетривиальные задачи, возникающие при разработке клиентского приложения. Не забудь поделиться ссылкой со всеми интересующимися! :)

До новых статей!