В первой части этого руководства был представлен общий обзор директив 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: '&' }, .... // 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 можно запутаться при выборе правильного объема директивы. По умолчанию директива не создает новую область и использует область родителя. Но во многих случаях это не то, что мы хотим. Если ваша директива сильно манипулирует свойствами родительской области и создает новые, она может загрязнить область. Позволить всем директивам использовать одну и ту же родительскую область не очень хорошая идея, потому что любой может изменить наши свойства области. Таким образом, следующие рекомендации могут помочь вам выбрать правильный объем для вашей директивы.
- Родительская
scope: false(scope: false) — это случай по умолчанию. Если ваша директива не манипулирует свойствами родительской области, вам может не потребоваться новая область. В этом случае с помощью родительской области все в порядке. - Дочерняя
scope:true(scope:true) — это создает новую дочернюю область для директивы, которая прототипно наследуется от родительской области. Если свойства и функции, заданные вами в области, не относятся к другим директивам и родительскому элементу, вам, вероятно, следует создать новую дочернюю область. При этом у вас также есть все свойства и функции области, определенные родителем. - 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, будут добавлены в область видимости. -
noteTextdivсвязан с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 .