В первой части этого руководства был представлен общий обзор директив 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
, будут добавлены в область видимости. -
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 .