Angular version: 8.x
Node version: 10.9.0 or later

Вы работаете над приложением Angular, пытаясь добавить интерактивный элемент пользовательского интерфейса, например, простое раскрывающееся меню. У вас открыто двадцать вкладок браузера, и вы пытаетесь научиться делать это с помощью Angular. Вы хотите следовать лучшим практикам, но уровень вашего разочарования растет, и вы начинаете думать , почему так сложно сделать что-то, что заняло бы 10 минут с JQuery и Bootstrap. Знакомо? Если да, то вы попали в нужное место.

Не волнуйтесь - это у вас есть! На самом деле это идеальный вариант использования директивы настраиваемых атрибутов, и я расскажу все, что вам нужно знать, чтобы реализовать свою собственную за считанные минуты. Давайте нырнем!

Что такое атрибутная директива?

Директивы атрибутов изменяют внешний вид или поведение элемента, компонента или другой директивы. По сути, это класс, аннотированный декоратором Directive, в котором вы указываете, какое изменение вы хотите произвести и какое событие CSS (если есть) вы хотите инициировать это изменение. ngClass, ngStyle и ngModel являются примерами директив атрибутов, встроенных в структуру Angular.

Когда мне следует использовать директиву атрибута?

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

Начиная

Прежде чем я перейду к тому, как работают директивы атрибутов, мне нужно объяснить несколько вещей. Чтобы директивы атрибутов выполняли то, что мы хотим от них, они должны иметь доступ к элементам DOM и изменять их. В директиве Angular есть три способа сделать это:

  1. ElementRef
  2. Рендерер
  3. HostBinding (это должно быть вашим значением по умолчанию)

Я покажу вам примеры раскрывающегося меню, в котором используется каждый из этих методов, и объясню, как они работают. HTML и CSS в каждом примере будут одинаковыми. Только код в классе Dropdown будет меняться от примера к примеру.

HTML:

CSS:

Директива раскрывающегося списка добавит или удалит класс open к элементу кнопки при возникновении события щелчка. Класс кнопки имеет дочерний элемент div, содержащий раскрывающиеся ссылки с классом dropdown. По умолчанию для свойства display раскрывающегося div установлено значение none. Когда директива dropdown применяет класс open к кнопке раскрывающегося списка, свойство display раскрывающегося div устанавливается на block ниже в каскаде, что делает его видимым. При такой настройке добавление и удаление класса open будет тем, как мы отображаем или скрываем раскрывающийся список.

Директива атрибута с использованием ElementRef

В этом разделе мы рассмотрим реализацию раскрывающейся директивы с использованием ElementRef. Вы можете найти пример, на который я буду ссылаться на Stackblitz, здесь.

В этом примере так выглядит наша выпадающая директива:

В приведенном выше Gist у нас есть директива атрибутов, класс aDropdownDirective, аннотированный декоратором Directive. Как правило, декораторы в Angular содержат метаданные, необходимые компилятору для понимания того, как класс должен обрабатываться, создаваться и использоваться во время выполнения. Единственное свойство, требуемое директивой, - это свойство selector.

Так как же использовать раскрывающуюся директиву?

Во-первых, нам нужно импортировать его в наш модуль, добавив в массив объявлений. Затем мы используем раскрывающуюся директиву, помещая значение свойства selector в желаемый HTML-элемент, как если бы это был атрибут. В нашем случае это appDropdown.

Вы можете установить для свойства selector все, что захотите, но по соглашению это имя вашего класса с префиксом, имеющим отношение к вашему проекту. Причина использования префикса - помочь избежать конфликтов со стандартными атрибутами HTML и другими директивами, используемыми любыми сторонними библиотеками, которые вы можете импортировать.

Как это работает?

Когда синтаксический анализатор шаблона достигает элемента кнопки, на котором мы разместили нашу директиву appDropdown, компилятор Angular ищет директиву с селектором, установленным на appDropdown, и создает экземпляр класса, связанного с ним: класс DropdownDirective.

Чтобы добавить и удалить класс open и сделать нашу директиву функцией, нам нужно получить доступ к элементу кнопки, на котором мы разместили нашу директиву. Мы можем добиться этого, передав объект типа ElementRef в конструктор нашей директивы. Компилятор Angular понимает, что мы хотим, чтобы ссылка на элемент хоста была введена в нашу директиву и присвоена свойству elRef. В этом примере основным элементом является кнопка, потому что это элемент, на котором размещается наша выпадающая директива.

Декоратор HostListener позволяет нам указать, какое событие CSS мы хотим прослушивать в элементе хоста (наш элемент кнопки) и функцию, которую мы хотим выполнить при срабатывании этого события. В этом примере функция toggleDropdown будет выполняться, когда пользователь нажимает на элемент кнопки.

С точки зрения JQuery, подумайте о HostListener как о методе события (подумайте о .click()), а о функции toggleDropdown как о обратном вызове, который вы хотите выполнить при запуске события.

Вы можете подумать: HostListener странный. Разве я не могу просто прикрепить прослушиватель событий вручную? Краткий ответ: Небезопасно. Декоратор HostListener решает для вас действительно важные проблемы. В Angular Docs вы найдете следующий отрывок:

Конечно, вы можете войти в DOM с помощью стандартного JavaScript и вручную подключить прослушиватели событий. У этого подхода есть как минимум три проблемы:

1. Вы должны правильно написать слушателей.

2. Код должен отсоединить слушателя при уничтожении директивы, чтобы избежать утечки памяти.

3. Обращение к DOM API напрямую - не лучшая практика.

Теперь давайте посмотрим, что происходит внутри функции toggleClass. Мы получаем DOM-представление элемента кнопки из свойства nativeElement так же, как если бы мы использовали селектор элемента JQuery. Если свойство classList DOM содержит класс open, isOpen истинно, а метод toggle удаляет класс open и закрывает раскрывающийся список. Аналогично, если класс open не найден в classList, isOpen имеет значение false, а метод toggle добавляет класс open и открывает раскрывающийся список.

Помните, когда мы решили ранее, что нам следует использовать декоратор HostListener, потому что это не хорошая идея напрямую обращаться к DOM? Использование ElementRef для доступа к DOM и управления им делает именно это. Разрешение прямого доступа к DOM может сделать ваше приложение более уязвимым для XSS-атак. Для получения дополнительной информации ознакомьтесь с Руководством по безопасности Angular.

Кроме того, этот метод управления DOM тесно связывает DOM и слой рендеринга. Это проблематично, если вы когда-нибудь захотите использовать воркеров веб / сервисов, поскольку они не могут напрямую получить доступ к DOM. К счастью, есть альтернативный метод, который подводит меня ко второму способу реализации директивы атрибута dropdown.

Директива атрибута с использованием Renderer2

API Renderer2 предлагает способ обойти шаблоны Angular и внести изменения пользовательского интерфейса, которые нельзя выразить декларативно. Это означает, что веб-сайт и сервисы могут безопасно использовать этот метод. Вы можете найти этот пример на Stackblitz здесь.

Давайте теперь посмотрим, как выглядит наш dropdown.directive.ts файл.

В приведенном выше Gist мы по-прежнему вводим ElementRef, чтобы получить ссылку на элемент хоста; однако мы вводим Renderer2, чтобы внести изменения в представление.

Как и в предыдущем примере, мы получаем логическое значение isOpen, проверяя, содержит ли свойство classList класс open. Если это правда, раскрывающийся список открыт, и мы используем метод removeClass в API Renderer2, чтобы удалить класс open и закрыть раскрывающийся список. И наоборот, если isOpen ложно, мы вызываем метод addClass в Renderer2 API, чтобы добавить класс open и открыть раскрывающийся список.

Используя Renderer2 API, мы перехватываем вызовы средства визуализации и изменяем шаблон, а не вносим изменения непосредственно в DOM. Это отделяет слои отрисовки и DOM, делая возможным использование рабочих веб-служб и рабочих служб.

Как мы обсуждали ранее, поскольку мы все еще внедряем ElementRef, все еще существует вероятность того, что мы вводим уязвимость системы безопасности в наше приложение. Мы можем решить эту проблему с помощью декоратора HostBinding, который подводит меня к третьему способу реализации директивы атрибутов.

Директива атрибута с HostBinding

Декоратор HostingBinding позволяет нам пометить свойство DOM как свойство привязки к хосту. Другими словами, мы можем связать свойство DOM в элементе хоста с локальным свойством в нашем классе DropdownDirective, передав свойство DOM в декоратор Hostbinding. Вы можете найти этот пример на Stackblitz здесь.

Давайте теперь посмотрим на наш dropdown.directive.ts файл.

Обратите внимание на то, что в Gist выше мы ничего не внедряем в наш DropdownDirective класс. Это потому, что нам действительно не нужен доступ к элементу. Нам просто нужно связать свойство isOpen с тем, применяется ли класс open к элементу хоста. Мы можем сделать это, передав селектор CSS с префиксом class. в декоратор HostBinding. Если класс open установлен в элементе хоста, он вернет true. В противном случае он вернет false.

Примечание. Вы можете привязать столько свойств к элементу хоста, сколько необходимо. В нашем случае нам нужен только один.

В функции toggleDropdown мы можем добавить / удалить класс open из хост-элемента, установив для свойства isOpen противоположное логическое значение. Когда функция, аннотированная декоратором HostListener, изменяет свойство класса директивы, привязанного к свойству DOM, нам не нужно вручную изменять его с помощью ElementRef или Renderer. Вместо этого Angular автоматически проверяет привязки свойств хоста во время обнаружения изменений, и если привязка изменяется, он обновляет хост-элемент директивы, используя версию Renderer.

Преимущества использования этого метода:

  • Наша DOM отделена от слоя рендеринга
  • Избегайте уязвимости к XSS-атакам, напрямую ссылаясь на DOM
  • Легче тестировать и улучшает читаемость

Заключение

Избегайте прямого доступа к DOM и манипулирования им. Вместо этого используйте декораторы Hostbinding и HostListener, чтобы избежать каких-либо уязвимостей, отделить уровень рендеринга от DOM и улучшить тестируемость.