Статьи

Практическое руководство по директивам AngularJS – часть вторая

В первой части этого руководства был представлен общий обзор директив AngularJS. В конце урока мы также узнали, как изолировать область действия директивы. Эта статья поможет вам понять, где именно закончилась первая часть. Во-первых, мы увидим, как вы можете получить доступ к свойствам родительской области внутри вашей директивы, поддерживая изолированную область. Далее мы обсудим, как выбрать правильную область действия для директивы, исследуя такие понятия, как функции controller и включения. Статья завершается пошаговым описанием приложения для заметок.

Связывание между изолированными и родительскими свойствами

Часто удобно изолировать область действия директивы, особенно если вы манипулируете многими моделями области действия. Но вам также может понадобиться доступ к некоторым родительским свойствам области действия внутри директивы, чтобы код работал. Хорошая новость заключается в том, что Angular дает вам достаточно гибкости для выборочной передачи свойств родительской области в директиву через привязки. Давайте вернемся к нашей директиве hello world , которая автоматически меняет цвет фона, когда кто-то вводит имя цвета в текстовое поле. Вспомните, что мы изолировали область действия директивы и код перестал работать? Что ж, давайте сделаем это сейчас!

Предположим, что переменная app инициализирована и ссылается на угловой модуль. Директива показана ниже.

 app.directive('helloWorld', function() { return { scope: {}, restrict: 'AE', replace: true, template: '<p style="background-color:{{color}}">Hello World</p>', link: function(scope, elem, attrs) { elem.bind('click', function() { elem.css('background-color','white'); scope.$apply(function() { scope.color = "white"; }); }); elem.bind('mouseover', function() { elem.css('cursor', 'pointer'); }); } }; }); 

Разметка с директивой utils показана в следующем примере кода.

 <body ng-controller="MainCtrl"> <input type="text" ng-model="color" placeholder="Enter a color"/> <hello-world/> </body> 

Этот код в настоящее время не работает. Поскольку у нас есть изолированная область видимости, выражение {{color}} внутри шаблона директивы сравнивается с этой областью (не родительской). Но директива ng-model для элемента input ссылается на color свойства родительской области видимости. Итак, нам нужен способ связать эти два изолированных и родительских свойства области видимости. В Angular это связывание может быть достигнуто путем установки атрибутов для элемента директивы в HTML и настройки свойства области в объекте определения директивы. Давайте рассмотрим несколько способов настройки привязки.

Вариант 1. Использование @ для односторонней привязки текста

В определении директивы, показанном ниже, мы указали, что color изолированного свойства области должен быть связан с атрибутом colorAttr , который применяется к директиве в HTML. Если вы посмотрите на разметку, то увидите, что выражение {{color}} назначено для color-attr . Когда значение выражения изменяется, атрибут color-attr также изменяется. Это, в свою очередь, меняет color изолированного свойства области видимости.

 app.directive('helloWorld', function() { return { scope: { color: '@colorAttr' }, .... // the rest of the configurations }; }); 

Обновленная разметка показана ниже.

 <body ng-controller="MainCtrl"> <input type="text" ng-model="color" placeholder="Enter a color"/> <hello-world color-attr="{{color}}"/> </body> 

Мы называем это односторонним связыванием, потому что с помощью этого метода вы можете только передавать строки в атрибут (используя выражения {{}} ). Когда свойство родительской области изменяется, ваша изолированная модель области также изменяется. Вы даже можете наблюдать это свойство области действия внутри директивы и запускать задачи, когда происходит изменение. Однако обратное неверно! Вы не можете изменить родительскую модель области действия, управляя изолированной областью действия.

Замечания:
Если свойство изолированного контекста и имя атрибута совпадают, вы можете написать определение директивы следующим образом:

 app.directive('helloWorld', function() { return { scope: { color: '@' }, .... // the rest of the configurations }; }); 

Директива вызывается в HTML следующим образом:

 <hello-world color="{{color}}"/> 

Вариант 2: использовать = для двухстороннего связывания

Давайте изменим определение директивы, как показано ниже.

 app.directive('helloWorld', function() { return { scope: { color: '=' }, .... // the rest of the configurations }; }); 

И измените HTML следующим образом:

 <body ng-controller="MainCtrl"> <input type="text" ng-model="color" placeholder="Enter a color"/> <hello-world color="color"/> </body> 

В отличие от @ , этот метод позволяет вам присвоить атрибуту фактическую модель контекста, а не просто строки. В результате вы можете передавать значения в диапазоне от простых строк и массивов до сложных объектов в изолированную область. Также существует двусторонняя привязка. Всякий раз, когда изменяется родительское свойство области, соответствующее изолированное свойство области также изменяется, и наоборот. Как обычно, вы можете наблюдать это свойство области для изменений.

Вариант 3. Использование & для выполнения функций в родительской области

Иногда необходимо вызывать функции, определенные в родительской области действия, из директивы с изолированной областью действия. Для ссылки на функции, определенные во внешней области, мы используем & . Допустим, мы хотим вызвать функцию sayHello() из директивы. Следующий код объясняет, как это достигается.

 app.directive('sayHello', function() { return { scope: { sayHelloIsolated: '&amp;' }, .... // the rest of the configurations }; }); 

Директива используется в HTML следующим образом:

 <body ng-controller="MainCtrl"> <input type="text" ng-model="color" placeholder="Enter a color"/> <say-hello sayHelloIsolated="sayHello()"/> </body> 

Этот пример Plunker демонстрирует эту концепцию.

Родительская Область против Детской Области против Изолированной Области

Как новичок в Angular можно запутаться при выборе правильного объема директивы. По умолчанию директива не создает новую область и использует область родителя. Но во многих случаях это не то, что мы хотим. Если ваша директива сильно манипулирует свойствами родительской области и создает новые, она может загрязнить область. Позволить всем директивам использовать одну и ту же родительскую область не очень хорошая идея, потому что любой может изменить наши свойства области. Таким образом, следующие рекомендации могут помочь вам выбрать правильный объем для вашей директивы.

  1. Родительская scope: false ( scope: false ) – это случай по умолчанию. Если ваша директива не манипулирует свойствами родительской области, вам может не потребоваться новая область. В этом случае с помощью родительской области все в порядке.
  2. Дочерняя scope:true ( scope:true ) – это создает новую дочернюю область для директивы, которая прототипно наследуется от родительской области. Если свойства и функции, заданные вами в области, не относятся к другим директивам и родительскому элементу, вам, вероятно, следует создать новую дочернюю область. При этом у вас также есть все свойства и функции области, определенные родителем.
  3. Isolated Scope ( scope:{} ) – Это как песочница! Это необходимо, если директива, которую вы собираетесь создать, является автономной и может использоваться повторно. Ваша директива может создавать множество свойств и функций области действия, которые предназначены для внутреннего использования и никогда не должны быть замечены внешним миром. Если это так, лучше иметь изолированную область. Как и ожидалось, изолированная область не наследует родительскую область.

включение

Transclusion – это функция, которая позволяет нам оборачивать директиву произвольным контентом. Позже мы можем извлечь и скомпилировать его в нужной области видимости и, наконец, поместить в указанное положение в шаблоне директивы. Если вы установите transclude:true в определении директивы, будет создана новая область transclude, которая прототипно наследуется от родительской области. Если вы хотите, чтобы ваша директива с изолированной областью действия содержала произвольный фрагмент содержимого и выполняла его в родительской области, можно использовать transclusion.

Допустим, у нас есть зарегистрированная директива:

 app.directive('outputText', function() { return { transclude: true, scope: {}, template: '<div ng-transclude></div>' }; }); 

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

 <div output-text> <p>Hello {{name}}</p> </div> 

ng-transclude говорит, куда поместить трансклидированный контент. В этом случае содержимое DOM <p>Hello {{name}}</p> извлекается и помещается в <div ng-transclude></div> . Важно помнить, что выражение {{name}} интерполируется против свойства, определенного в родительской области, а не в изолированной области. Плункер для экспериментов находится здесь . Если вы хотите узнать больше об областях применения, перейдите по этому документу .

Различия между transclude:'element' и transclude:true

Иногда нам нужно включить элемент, к которому применяется директива, а не только его содержимое. В этих случаях transclude:'element' . Это, в отличие от transclude:true , включает сам элемент в шаблон директивы, помеченный ng-transclude . В результате включения ваша функция link получает функцию связи включения, предварительно привязанную к правильной области действия директивы. Этой функции связывания также передается другая функция с клоном элемента DOM, который должен быть включен. Вы можете выполнять такие задачи, как изменение клона и добавление его в DOM. Директивы типа ng-repeat используют эту технику для повторения элементов DOM. Посмотрите на следующий Plunker, который повторяет элемент DOM, используя эту технику, и меняет цвет фона второго экземпляра.

Также обратите внимание, что с помощью transclude:'element' , к которому применяется директива, преобразуется в комментарий HTML. Таким образом, если вы объедините transclude:'element' с replace:false , шаблон директивы по существу получает innerHTML комментария innerHTML – что означает, что на самом деле ничего не происходит! Вместо этого, если вы выберете replace:true шаблон директивы заменит HTML-комментарий, и все будет работать как положено. Использование replace:false с transclude:'element' хорошо для случаев, когда вы хотите повторить элемент DOM и не хотите сохранять первый экземпляр элемента (который преобразуется в комментарий).

Функция controller и require

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

 app.directive('outerDirective', function() { return { scope: {}, restrict: 'AE', controller: function($scope, $compile, $http) { // $scope is the appropriate scope for the directive this.addChild = function(nestedDirective) { // this refers to the controller console.log('Got the message from nested directive:' + nestedDirective.message); }; } }; }); 

Этот код присоединяет к директиве controller именем outerDirective . Когда другая директива хочет связаться, она должна объявить, что ей требуется экземпляр controller вашей директивы. Это сделано как показано ниже.

 app.directive('innerDirective', function() { return { scope: {}, restrict: 'AE', require: '^outerDirective', link: function(scope, elem, attrs, controllerInstance) { //the fourth argument is the controller instance you require scope.message = "Hi, Parent directive"; controllerInstance.addChild(scope); } }; }); 

Разметка будет выглядеть примерно так:

 <outer-directive> <inner-directive></inner-directive> </outer-directive> 

require: '^outerDirective' указывает Angular искать контроллер на элементе и его родительском элементе. В этом случае найденный экземпляр controller передается в качестве четвертого аргумента функции link . В нашем случае мы отправляем родительскую область действия вложенной директивы. Чтобы попробовать, откройте этот Plunker с открытой консолью браузера. Последний раздел этого углового ресурса дает превосходный пример межправительственной связи. Это обязательно нужно прочитать!

Приложение для заметок

В этом разделе мы собираемся создать простое приложение для создания заметок с использованием директив. Мы будем использовать HTML5 localStorage для хранения заметок. Конечный продукт будет выглядеть следующим образом . Мы создадим директиву, которая будет отображать блокнот. Пользователь может просматривать список заметок, которые он / она сделал. Когда он нажимает кнопку « add new блокнот становится редактируемым и позволяет создавать заметки. Заметка автоматически сохраняется при нажатии кнопки « back . Заметки сохраняются с использованием фабрики notesFactory с помощью localStorage . Заводской код довольно прост и не требует пояснений. Итак, давайте сосредоточимся только на коде директивы.

Шаг 1

Начнем с регистрации директивы notepad .

 app.directive('notepad', function(notesFactory) { return { restrict: 'AE', scope: {}, link: function(scope, elem, attrs) { }, templateUrl: 'templateurl.html' }; }); 

Обратите внимание на несколько вещей о директиве:

  • Область действия изолирована, так как мы хотим, чтобы директива использовалась повторно. Директива будет иметь много свойств и функций, которые не имеют отношения снаружи.
  • Директива может использоваться как атрибут или элемент, как указано в свойстве restrict .
  • Функция link изначально пуста.
  • Директива получает свой шаблон из templateurl.html .

Шаг 2

Следующий HTML-код формирует шаблон для директивы.

 <div class="note-area" ng-show="!editMode"> <ul> <li ng-repeat="note in notes|orderBy:'id'"> <a href="#" ng-click="openEditor(note.id)">{{note.title}}</a> </li> </ul> </div> <div id="editor" ng-show="editMode" class="note-area" contenteditable="true" ng-bind="noteText"></div> <span><a href="#" ng-click="save()" ng-show="editMode">Back</a></span> <span><a href="#" ng-click="openEditor()" ng-show="!editMode">Add Note</a></span> 

Важные моменты, на которые следует обратить внимание:

  • Объект note инкапсулирует title , id и content .
  • ng-repeat используется для циклического просмотра notes и их сортировки в порядке возрастания автоматически сгенерированного id .
  • У нас будет свойство editMode которое будет указывать режим, в котором мы находимся. В режиме редактирования это свойство будет true и редактируемый div будет виден. Пользователь пишет заметку здесь.
  • Если editMode имеет значение false мы находимся в режиме просмотра и отображаем notes .
  • Две кнопки также отображаются / скрываются в зависимости от editMode .
  • Директива ng-click используется для реагирования на нажатия кнопок. Эти методы вместе со свойствами, такими как editMode , будут добавлены в область видимости.
  • noteText div связан с noteText , который содержит введенный пользователем текст. Если вы хотите редактировать существующую заметку, эта модель инициализирует этот div с содержимым этой заметки.

Шаг 3

Давайте создадим новую функцию в нашей области действия с именем restore() которая будет инициализировать различные элементы управления для нашего приложения. Это будет вызвано, когда функция link запускается и каждый раз, когда нажимается кнопка save .

 scope.restore = function() { scope.editMode = false; scope.index = -1; scope.noteText = ''; }; 

Мы создаем эту функцию внутри функции link . editMode и noteText уже были объяснены. index используется для отслеживания, какая заметка редактируется. Если мы создаем новую заметку, index равен -1. Если мы редактируем существующую заметку, она содержит id объекта note .

Шаг 4

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

 scope.openEditor = function(index) { scope.editMode = true; if (index !== undefined) { scope.noteText = notesFactory.get(index).content; scope.index = index; } else { scope.noteText = undefined; } }; scope.save = function() { if (scope.noteText !== '') { var note = {}; note.title = scope.noteText.length > 10 ? scope.noteText.substring(0, 10) + '. . .' : scope.noteText; note.content = scope.noteText; note.id = scope.index != -1 ? scope.index : localStorage.length; scope.notes = notesFactory.put(note); } scope.restore(); }; 

Важными моментами об этих функциях являются:

  • openEditor готовит редактор. Если мы редактируем заметку, она получает содержимое этой заметки и обновляет редактируемый div благодаря ng-bind .
  • Если мы создаем новую заметку, нам нужно установить для noteText значение undefined чтобы наблюдатели срабатывали при сохранении заметки.
  • Если index аргумента функции не определен, это означает, что пользователь собирается создать новую заметку.
  • Функция save использует справку notesFactory для сохранения заметки. После сохранения он обновляет массив notes чтобы наблюдатели могли обнаружить изменения и список заметок можно было обновить.
  • Функция save вызывает restore() в конце, чтобы сбросить элементы управления, чтобы мы могли вернуться в режим просмотра из режима редактирования.

Шаг 5

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

 var editor = elem.find('#editor'); scope.restore(); // initialize our app controls scope.notes = notesFactory.getAll(); // load notes editor.bind('keyup keydown', function() { scope.noteText = editor.text().trim(); }); 

Шаг 6

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

 <h1 class="title">The Note Making App</h1> <notepad/> 

Вывод

Важно отметить, что все, что мы делаем с jQuery, можно сделать с помощью Angular-директив с гораздо меньшим количеством кода. Итак, перед использованием jQuery попытайтесь выяснить, можно ли сделать то же самое лучше без каких-либо манипуляций с DOM. Попробуйте свести к минимуму использование jQuery с Angular.

Что касается демонстрации создания заметок, то функция удаления заметки была намеренно исключена. Читателю предлагается поэкспериментировать и реализовать эту функцию. Исходный код для демонстрации доступен для скачивания с GitHub .