Статьи

Освоение директив AngularJS

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

Что я имею в виду здесь под директивами   в основном пользовательские директивы во время урока. Я не буду пытаться научить вас, как использовать встроенные директивы, такие как ng-repeat , ng-show и т. Д. Я покажу вам, как использовать пользовательские директивы для создания ваших собственных компонентов.

  1. Простые Директивы
  2. Директивные ограничения
  3. Изолированная сфера
  4. Директивные Области применения
  5. Директивное наследование
  6. Директивная отладка
  7. Директивное модульное тестирование
  8. Директивная сфера тестирования
  9. Вывод

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

Виджет книги

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

01
02
03
04
05
06
07
08
09
10
angular.module(‘masteringAngularJsDirectives’, [])
.directive(‘book’, function() {
    return {
        restrict: ‘E’,
        scope: {
            data: ‘=’
        },
        templateUrl: ‘templates/book-widget.html’
    }
})

Директивная функция использовалась в вышеприведенном примере, чтобы сначала создать директиву. Название директивы — book . Эта директива возвращает объект, и давайте немного поговорим об этом объекте. restrict   для определения типа директивы, и это может быть A   (Дань), C ( C lass), E ( E lement) и M   (Совместно). Вы можете увидеть использование каждого соответственно ниже.

Тип использование
<div book > </ div>
С <div class = » book «> </ div>
Е < book data = «book_data»> </ book >
M <! — директива: книга ->

scope   для управления областью действия директивы. В приведенном выше случае данные книги переносятся в шаблон директивы с помощью "=" тип области Я подробно расскажу о сфере применения в следующих разделах. templateUrl   используется для вызова представления для отображения конкретного содержимого с использованием данных, переданных в область действия директивы. Вы также можете использовать template и предоставьте HTML-код напрямую, например так:

1
2
3
…..
template: ‘<div>Book Info</div>’
…..

В нашем случае у нас сложная структура HTML, и поэтому я выбрал templateUrl   вариант.

Директивы определены в файле JavaScript вашего проекта AngularJS и используются на странице HTML. Директивы AngularJS можно использовать на HTML-страницах следующим образом:

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

1
2
3
4
5
6
<ul>
    <li>Home</li>
    <li>Latest News</li>
    <li restricted>User Administration</li>
    <li restricted>Campaign Management</li>
</ul>

и директива следующая:

01
02
03
04
05
06
07
08
09
10
11
12
app.directive(«restricted», function() {
    return {
        restrict: ‘A’,
        link: function(scope, element, attrs) {
            // Some auth check function
            var isAuthorized = checkAuthorization();
            if (!isAuthorized) {
                element.css(‘display’, ‘none’);
            }
        }
    }
})

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

Итак, что это за link   функционировать там? Проще говоря, функция связи — это функция, которую вы можете использовать для выполнения специфических для директив операций. Директива не только отображает некоторый HTML-код, предоставляя некоторые входные данные. Вы также можете привязать функции к элементу директивы, вызвать сервис и обновить значение директивы, получить атрибуты директивы, если это E   директива типа и т. д.

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

1
2
3
4
5
6
<ul>
    <li>Home</li>
    <li>Latest News</li>
    <li class=»nav restricted»>User Administration</li>
    <li class=»nav active restricted»>Campaign Management</li>
</ul>

У каждого элемента уже есть класс для стиля, и поскольку добавленный класс restricted это фактически директива.

Вам не нужно использовать директиву внутри HTML-элемента. Вы можете создать свой собственный элемент, используя директиву AngularJS с ограничением E Допустим, в вашем приложении есть пользовательский виджет для отображения username , avatar и reputation   в нескольких местах в вашем приложении. Вы можете использовать директиву как это:

01
02
03
04
05
06
07
08
09
10
11
app.directive(«user», function() {
    return {
        restrict: ‘E’,
        link: function(scope, element, attrs) {
            scope.username = attrs.username;
            scope.avatar = attrs.avatar;
            scope.reputation = attrs.reputation;
        },
        template: ‘<div>Username: {{username}}, Avatar: {{avatar}}, Reputation: {{reputation}}</div>’
    }
})

HTML-код будет:

1
<user username=»huseyinbabal» avatar=»https://www.gravatar.com/avatar/ef36a722788f5d852e2635113b2b6b84?s=128&d=identicon&r=PG» reputation=»8012″></user>

В приведенном выше примере создается пользовательский элемент, и предоставляются некоторые атрибуты, такие как username , avatar и reputation . Я хочу обратить внимание на тело функции ссылки. Атрибуты элемента присваиваются области действия директивы. Первым параметром функции link является область действия текущей директивы. Третий параметр директивы — это объект атрибута директивы, что означает, что вы можете прочитать любой атрибут из пользовательской директивы, используя attrs.attr_name . Значения атрибутов присваиваются области, чтобы они использовались внутри шаблона.

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

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

1
2
3
4
5
6
app.directive(«comment», function() {
    return {
        restrict: ‘M’,
        template: ‘<textarea class=»comment»></textarea>’
    }
})

И в элементе HTML:

1
<!— directive:comment —>

Каждая директива имеет свою область видимости, но вы должны быть осторожны с привязкой данных к объявлению директивы. Допустим, вы реализуете basket   часть вашего приложения электронной коммерции. На странице корзины у вас уже есть товары, добавленные здесь ранее. У каждого предмета есть поле количества, чтобы выбрать, сколько предметов вы хотите купить, как показано ниже:

Простая корзина

Вот декларация директивы:

1
2
3
4
5
6
7
8
9
app.directive(«item», function() {
    return {
        restrict: ‘E’,
        link: function(scope, element, attrs) {
            scope.name = attrs.name;
        },
        template: ‘<div><strong>Name:</strong> {{name}} <strong>Select Amount:</strong> <select name=»count» ng-model=»count»><option value=»1″>1</option><option value=»2″>2</option></select> <strong>Selected Amount:</strong> {{count}}</div>’
    }
})

И для того, чтобы отобразить три элемента в HTML:

1
2
3
<item name=»Item-1″></item>
<item name=»Item-2″></item>
<item name=»Item-3″></item>

Проблема здесь в том, что всякий раз, когда вы выбираете сумму желаемого товара, все разделы количества товаров будут обновляться. Почему? Потому что существует двусторонняя привязка данных со count имен, но область действия не изолирована. Чтобы изолировать область, просто добавьте scope: {}   к атрибуту директивы в разделе возврата:

01
02
03
04
05
06
07
08
09
10
app.directive(«item», function() {
    return {
        restrict: ‘E’,
        scope: {},
        link: function(scope, element, attrs) {
            scope.name = attrs.name;
        },
        template: ‘<div><strong>Name:</strong> {{name}} <strong>Select Amount:</strong> <select name=»count» ng-model=»count»><option value=»1″>1</option><option value=»2″>2</option></select> <strong>Selected Amount:</strong> {{count}}</div>’
    }
})

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

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

Область действия «@». Этот тип области используется для передачи значения в область действия директивы. Допустим, вы хотите создать виджет для уведомления:

01
02
03
04
05
06
07
08
09
10
11
12
app.controller(«MessageCtrl», function() {
    $scope.message = «Product created!»;
})
app.directive(«notification», function() {
    return {
        restrict: ‘E’,
        scope: {
            message: ‘@’
        },
        template: ‘<div class=»alert»>{{message}}</div>’
    }
});

и вы можете использовать:

1
<notification message=»{{message}}»></notification>

В этом примере значение сообщения просто присваивается области действия директивы. Рендеринг HTML-контента будет:

1
<div class=»alert»>Product created!</div>

«=» Область: в этом типе области вместо значений передаются переменные области, что означает, что мы не передадим {{message}} , вместо этого мы передадим message . Причиной этой функции является построение двусторонней привязки данных между директивой и элементами страницы или контроллерами. Давайте посмотрим на это в действии.

1
2
3
4
5
6
7
8
9
.directive(«bookComment», function() {
    return {
        restrict: ‘E’,
        scope: {
            text: ‘=’
        },
        template: ‘<input type=»text» ng-model=»text»/>’
    }
})

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

1
2
<span>This is the textbox on the directive
<book-comment text=»commentText»></book-comment>

Это просто покажет текстовое поле на странице, поэтому давайте добавим что-то еще для взаимодействия с этой директивой:

1
2
3
4
5
<span>This is the textbox on the page
<input type=»text» ng-model=»commentText»/>
<br/>
<span>This is the textbox on the directive
<book-comment text=»commentText»></book-comment>

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

«&» Область действия: мы можем передать значение и ссылку на директивы. В этом типе области мы посмотрим, как передать выражения в директиву. В реальных случаях вам может понадобиться передать определенную функцию (выражение) в директивы, чтобы предотвратить связывание. Иногда директивам не нужно много знать об идее выражений. Например, директиве понравится книга для вас, но она не знает, как это сделать. Чтобы сделать это, вы можете использовать следующую структуру:

1
2
3
4
5
6
7
8
9
.directive(«likeBook», function() {
    return {
        restrict: ‘E’,
        scope: {
            like: ‘&’
        },
        template: ‘<input type=»button» ng-click=»like()» value=»Like»/>’
    }
})

В этой директиве выражение будет передано кнопке директивы через атрибут like . Давайте определим функцию в контроллере и передадим ее в директиву внутри HTML.

1
2
3
$scope.likeFunction = function() {
    alert(«I like the book!»)
}

Это будет внутри контроллера, и шаблон будет:

1
<like-book like=»likeFunction()»></like-book>

likeFunction() происходит из контроллера и передается в директиву. Что если вы хотите передать параметр likeFunction() ? Например, вам может понадобиться передать значение рейтинга в likeFunction() . Это очень просто: просто добавьте аргумент к функции внутри контроллера и добавьте элемент ввода в директиву, чтобы запросить начальный счет от пользователя. Вы можете сделать это, как показано ниже:

01
02
03
04
05
06
07
08
09
10
.directive(«likeBook», function() {
    return {
        restrict: ‘E’,
        scope: {
            like: ‘&’
        },
        template: ‘<input type=»text» ng-model=»starCount» placeholder=»Enter rate count here»/><br/>’ +
        ‘<input type=»button» ng-click=»like({star: starCount})» value=»Like»/>’
    }
})
1
2
3
$scope.likeFunction = function(star) {
    alert(«I like the book!, and gave » + star + » star.»)
}
1
<like-book like=»likeFunction(star)»></like-book>

Как видите, текстовое поле происходит от директивы. Значение текстового поля связано с аргументом функции, например like({star: starCount}) . star — для функции контроллера, а starCount для привязки значения текстового starCount .

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

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

1
2
3
4
5
6
7
app.directive(‘mouseClicked’, function() {
    return {
        restrict: ‘E’,
        scope: {},
        controller: «MouseClickedCtrl as mouseClicked»
    }
})

Это родительская директива, которая наследуется дочерними директивами. Как вы можете видеть, есть атрибут контроллера этой директивы, использующий директиву as. Давайте также определим этот контроллер:

01
02
03
04
05
06
07
08
09
10
11
12
13
app.controller(‘MouseClickedCtrl’, function($element) {
    var mouseClicked = this;
 
    mouseClicked.bookType = null;
 
    mouseClicked.setBookType = function(type) {
        mouseClicked.bookType = type
    };
 
    $element.bind(«click», function() {
        alert(«Typeof book: » + mouseClicked.bookType + » sent for statistical analysis!»);
    })
})

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
app.directive(‘ebook’, function() {
    return {
        require: «mouseClicked»,
        link: function(scope, element, attrs, mouseClickedCtrl) {
            mouseClickedCtrl.setBookType(«EBOOK»);
        }
    }
})
.directive(‘magazine’, function() {
    return {
        require: «mouseClicked»,
        link: function(scope, element, attrs, mouseClickedCtrl) {
            mouseClickedCtrl.setBookType(«MAGAZINE»);
        }
    }
})

Как видите, дочерние директивы используют ключевое слово require для использования родительской директивы. И еще один важный момент — это четвертый аргумент функции link в дочерних директивах. Этот аргумент относится к атрибуту controller родительской директивы, который означает, что setBookType директива может использовать функцию контроллера setBookType внутри контроллера. Если текущий элемент — электронная книга, вы можете использовать первую директиву, а если это журнал, вы можете использовать вторую:

1
2
<a><mouse-clicked ebook>Game of thrones (click me)</mouse-clicked></a><br/>
<a><mouse-clicked magazine>PC World (click me)</mouse-clicked></a>

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

Когда вы используете директивы внутри шаблона, вы видите на странице скомпилированную версию директивы. Иногда вы хотите увидеть фактическое использование директивы для целей отладки. Чтобы увидеть некомпилированную версию текущего раздела, вы можете использовать ng-non-bindable . Например, предположим, у вас есть виджет, который печатает самые популярные книги, и вот код для этого:

1
2
3
<ul>
    <li ng-repeat=»book in books»>{{book}}</li>
</ul>

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

Если вы хотите узнать, как используется директива за этим скомпилированным выводом, вы можете использовать эту версию кода:

1
2
3
<ul ng-non-bindable=»»>
    <li ng-repeat=»book in books»>{{book}}</li>
</ul>

На этот раз вывод будет как ниже:

Это круто до сих пор, но что, если мы хотим видеть как некомпилированные, так и скомпилированные версии виджета? Настало время написать пользовательскую директиву, которая будет выполнять расширенную операцию отладки.

01
02
03
04
05
06
07
08
09
10
11
12
app.directive(‘customDebug’, function($compile) {
    return {
        terminal: true,
        link: function(scope, element) {
            var currentElement = element.clone();
            currentElement.removeAttr(«custom-debug»);
            var newElement = $compile(currentElement)(scope);
            element.attr(«style», «border: 1px solid red»);
            element.after(newElement);
        }
    }
})

В этой директиве мы клонируем элемент, находящийся в режиме отладки, чтобы он не изменился после некоторого набора операций. После клонирования удалите custom-debug   директива, чтобы не действовать как режим отладки, а затем скомпилировать его с помощью $complile , который уже введен в директиву. Мы дали стиль элементу режима отладки, чтобы подчеркнуть отлаженный. Окончательный результат будет таким, как показано ниже:

Образец Директивы

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

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

Я буду использовать Жасмин для юнит-тестирования и Карму для юнита. Чтобы использовать Karma, просто установите его глобально, запустив npm install -g karma karma-cli (на вашем компьютере должны быть установлены Node.js и npm). После установки откройте командную строку, перейдите в корневую папку вашего проекта и введите karma init . Он задаст вам пару вопросов, как показано ниже, чтобы настроить требования к тестированию.

Инициализация теста кармы

Я использую Webstorm для разработки, и если вы также используете Webstorm, просто щелкните правой кнопкой мыши на karma.conf.js и выберите Run karma.conf.js. Это выполнит все тесты, которые настроены в Karma Conf. Вы также можете запустить тесты с помощью командной строки karma start в корневой папке проекта. Это все о настройке среды, так что давайте перейдем к тестовой части.

Допустим, мы хотим проверить директиву книги. Когда мы передаем название директиве, оно должно быть скомпилировано в подробный вид книги. Итак, начнем.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
describe(«Book Tests», function() {
    var element;
    var scope;
    beforeEach(module(«masteringAngularJsDirectives»))
    beforeEach(inject(function($compile, $rootScope) {
        scope = $rootScope;
        element = angular.element(«<booktest title=’test’></booktest>»);
        $compile(element)($rootScope)
        scope.$digest()
    }));
 
    it(«directive should be successfully compiled», function() {
        expect(element.html()).toBe(«test»)
    })
});

В приведенном выше тесте мы тестируем новую директиву под названием booktest . Эта директива принимает title аргумента   и создает div, используя этот заголовок. В тесте перед каждым тестовым разделом мы вызываем наш модуль masteringAngularJsDirectives   первый. Затем мы создаем директиву под названием booktest .   На каждом шаге теста выходные данные директивы будут проверяться. Этот тест только для проверки значения.

В этом разделе мы проверим область действия директивы booktest . Эта директива генерирует подробное представление книги на странице, и когда вы щелкаете этот подробный раздел, переменная области действия называется viewed   будет установлен как true . В нашем тесте мы проверим, если viewed   устанавливается в значение true, когда вызывается событие щелчка. Директива:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
.directive(‘booktest’, function() {
    return {
        restrict: ‘E’,
        scope: {
            title: ‘@’
        },
        replace: true,
        template: ‘<div>{{title}}</div>’,
        link: function(scope, element, attrs) {
            element.bind(«click», function() {
                console.log(«book viewed!»);
                scope.viewed = true;
            });
        }
    }
})

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
describe(«Book Tests», function() {
    var element;
    var scope;
    beforeEach(module(«masteringAngularJsDirectives»))
    beforeEach(inject(function($compile, $rootScope) {
        scope = $rootScope;
        element = angular.element(«<booktest title=’test’></booktest>»);
        $compile(element)($rootScope)
        scope.$digest()
    }));
 
    it(«scope liked should be true when book liked», function() {
        element.triggerHandler(«click»);
        expect(element.isolateScope().viewed).toBe(true);
    });
});

В разделе теста событие click запускается с помощью element.triggerHandler("click") . Когда срабатывает событие click, просматриваемая переменная должна быть установлена ​​в значение true . Это значение утверждается с использованием expect(element.isolateScope().viewed).toBe(true) .

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

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