Статьи

Расширение HTML с помощью директив AngularJS

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

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

1
<!DOCTYPE html> <html> <head> </head> <body> </body> </html>

Теперь добавьте файл angular.min.js из CDN Google в <head> документа:

1
<script src=»https://ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.min.js»></script>

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

Поместите этот код в тег скрипта внизу <head> :

1
var module = angular.module(‘example’, []);

У нас нет никаких зависимостей, поэтому массив во втором аргументе angular.module() пуст, но не удаляйте его полностью, иначе вы получите ошибку $ injector: nomod , потому что angular.module с одним аргументом angular.module() возвращает ссылку на уже существующий модуль вместо создания нового.

Вы также должны добавить атрибут ng-app="example" в <body> чтобы приложение работало. После этого файл должен выглядеть так:

1
2
3
4
5
6
7
8
<!DOCTYPE html>
<html>
    <head>
        <script src=»https://ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.min.js»></script> <script> var module = angular.module(‘example’, []);
    </head>
    <body ng-app=»example»>
    </body>
</html>

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

Директивы объявляются с использованием module.directive() :

1
module.directive(‘exampleBindLeet’, function () {

Первый аргумент — это имя директивы. Он должен быть в camelCase , но поскольку HTML не чувствителен к регистру, вы будете использовать его в нижнем регистре (example-bind-leet) в вашем HTML-коде.

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

1
2
3
4
return {
        link: link
    };
});

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

1
function link($scope, $elem, attrs) {

$scope — это объект области видимости Angular, $elem — это элемент DOM, которому соответствует эта директива (она заключена в jqLite , подмножество AngularJS наиболее часто используемых функций jQuery), а attrs — это объект со всеми атрибутами элемента (с нормализованными именами, поэтому example-bind-leet будет доступен как attrs.exampleBindLeet ).

Простейший код для этой функции в нашей директиве будет выглядеть так:

1
2
3
4
5
6
var leetText = attrs.exampleBindLeet.replace(/[abegilostz]/gmi, function (letter) {
        return leet[letter.toLowerCase()];
    });
 
    $elem.text(leetText);
}

Сначала мы заменим некоторые буквы в тексте, представленном в атрибуте example-bind-leet их заменами из таблицы leet. Таблица выглядит так:

1
2
3
4
5
6
var leet = {
    a: ‘4’, b: ‘8’, e: ‘3’,
    g: ‘6’, i: ‘!’, l: ‘1’,
    o: ‘0’, s: ‘5’, t: ‘7’,
    z: ‘2’
};

Вы должны разместить его в верхней части <script> . Как видите, это самый простой конвертер Leet, поскольку он заменяет только десять символов.

После этого мы преобразовали строку в leet говорит, что мы используем метод text() jqLite, чтобы поместить его во внутренний текст элемента, которому соответствует эта директива.

Теперь вы можете проверить это, поместив этот HTML-код в <body> документа:

1
<div example-bind-leet=»This text will be converted to leet speak!»></div>

Вывод должен выглядеть так:

Но это не совсем то, как ngBind директива ngBind . Мы изменим это на следующих шагах.

Прежде всего, вещь, передаваемая в атрибуте example-bind-leet должна быть ссылкой на переменную в текущей области, а не на текст, который мы хотим преобразовать. Для этого нам нужно создать изолированную область видимости для директивы.

Мы можем достичь этого, добавив объект области видимости к возвращаемому значению нашей директивной функции:

1
2
3
4
5
6
7
8
9
module.directive(‘exampleBindLeet’, function () {
    …
    return {
        link: link,
        scope: {
 
        }
    };
);

Каждое свойство в этом объекте будет доступно в области действия директивы. Это значение будет определяться значением здесь. Если мы используем ‘-‘, значение будет равно значению атрибута с тем же именем, что и у свойства. Использование ‘=’ скажет компилятору, что мы ожидаем, что переменная из текущей области будет передана — которая будет работать так же, как ngBind :

1
2
3
scope: {
    exampleBindLeet: ‘=’
}

Вы также можете использовать что угодно в качестве имени свойства и поставить нормализованное (преобразованное в camelCase) имя атрибута после — или =:

1
2
3
scope: {
    text: ‘=exampleBindLeet’
}

Выберите то, что работает лучше для вас. Теперь нам также нужно изменить функцию ссылки, чтобы использовать $scope вместо attr :

1
2
3
4
5
6
7
function link($scope, $elem, attrs) {
    var leetText = $scope.exampleBindLeet.replace(/[abegilostz]/gmi, function (letter) {
        return leet[letter.toLowerCase()];
    });
 
    $elem.text(leetText);
}

Теперь используйте ngInit или создайте контроллер и измените значение атрибута divexample-bind-leet на имя переменной, которую вы использовали:

1
2
3
<body ng-app=»example» ng-init=»textToConvert = ‘This text will be converted to leet speak!'»>
    <div example-bind-leet=»textToConvert»></div>
</body>

Но это по-прежнему не так, как работает ngBind . Чтобы увидеть это, давайте добавим поле ввода, чтобы изменить значение textToConvert после загрузки страницы:

1
<input ng-model=»textToConvert»>

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

Чтобы изменить это, мы будем использовать метод $ scope. $ Watch () . Он принимает два параметра: первый — это угловое выражение, которое будет оцениваться каждый раз при изменении области действия, второй — функция обратного вызова, которая будет вызываться при изменении значения выражения.

Во-первых, давайте поместим код, который у нас был в функции link() в локальную функцию внутри нее:

1
2
3
4
5
6
7
8
9
function link($scope, $elem, attrs) {
    function convertText() {
        var leetText = $scope.exampleBindLeet.replace(/[abegilostz]/gmi, function (letter) {
            return leet[letter.toLowerCase()];
        });
 
        $elem.text(leetText);
    }
}

Теперь после этой функции мы будем вызывать $scope.$watch() следующим образом:

1
$scope.$watch(‘exampleBindLeet’, convertLeet);

Если вы откроете страницу сейчас и что-то измените в поле ввода, вы увидите, что содержимое нашего div также меняется, как и ожидалось.

Теперь мы напишем директиву, которая создаст индикатор прогресса для нас. Для этого мы будем использовать новый элемент: <example-progress> .

Чтобы наш индикатор выполнения выглядел как индикатор выполнения, нам нужно использовать немного CSS. Поместите этот код в элемент <style> в <head> документа:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
example-progress {
    display: block;
    width: 100%;
    position: relative;
    border: 1px solid black;
    height: 18px;
}
 
example-progress .progressBar {
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    background: green;
}
 
example-progress .progressValue {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    text-align: center;
}

Как вы можете видеть, это довольно просто — мы используем комбинацию position: relative и position: absolute для позиционирования зеленой полосы и значения внутри нашего элемента <example-progress> .

Этот из них потребует немного больше опций, чем предыдущий. Взгляните на этот код (и вставьте его в <script> ):

01
02
03
04
05
06
07
08
09
10
11
module.directive(‘exampleProgress’, function () {
    return {
        restrict: ‘E’,
        scope: {
            value: ‘=’,
            max: ‘=’
        },
        template: »,
        link: link
    };
});

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

  • restrict: ‘E’ — этот указывает компилятору искать элементы вместо атрибутов. Возможные значения:
    • «A» — соответствует только именам атрибутов (это поведение по умолчанию, поэтому вам не нужно устанавливать его, если вы хотите сопоставлять только атрибуты)
    • ‘E’ — соответствует только именам элементов
    • ‘C’ — соответствует только именам классов
  • Вы можете объединить их, например, «AEC» будет соответствовать именам атрибутов, элементов и классов.
  • template: » — это позволяет нам изменить внутренний HTML-код нашего элемента (есть также templateUrl, если вы хотите загрузить свой HTML-файл из отдельного файла)

Конечно, мы не оставим шаблон пустым. Поместите этот HTML туда:

1
<div class=»progressBar»></div><div class=»progressValue»>{{ percentValue }}%</div>

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

Эта функция будет аналогична предыдущей директиве. Во-первых, создайте локальную функцию, которая будет выполнять логику директивы — в этом случае обновите percentValue и установите div.progressBar :

1
2
3
4
5
6
7
function link($scope, $elem, attrs) {
    function updateProgress() {
        var percentValue = Math.round($scope.value / $scope.max * 100);
        $scope.percentValue = Math.min(Math.max(percentValue, 0), 100);
        $elem.children()[0].style.width = $scope.percentValue + ‘%’;
    }
}

Как видите, мы не можем использовать .css() для изменения ширины div.progressBar, потому что jqLite не поддерживает селекторы в .children() . Нам также нужно использовать Math.min() и Math.max() чтобы сохранить значение от 0% до 100% — Math.max() вернет 0, если precentValue меньше 0, а Math.min() вернет 100 если percentValue больше 100.

Теперь вместо двух вызовов $scope.$watch() (мы должны следить за изменениями в $scope.value и $scope.max ), давайте использовать $scope.$watchCollection() , который похож, но работает с коллекциями свойств:

1
$scope.$watchCollection(‘[value, max]’, updateProgress);

Обратите внимание, что в качестве первого параметра мы передаем строку, которая выглядит как массив, а не как массив JavaScript.

Чтобы увидеть, как это работает, сначала измените ngInit чтобы инициализировать еще две переменные:

1
<body ng-app=»example» ng-init=»textToConvert = ‘This text will be converted to leet speak!’; progressValue = 20; progressMax = 100″>

А затем добавьте элемент <example-progress> ниже div мы использовали ранее:

1
<example-progress value=»progressValue» max=»progressMax»></example-progress>

<body> теперь должно выглядеть так:

1
2
3
4
<body ng-app=»example» ng-init=»textToConvert = ‘This text will be converted to leet speak!’; progressValue = 20; progressMax = 100″>
    <div example-bind-leet=»textToConvert»></div>
    <example-progress value=»progressValue» max=»progressMax»></example-progress>
</body>

И вот результат:

Если вы добавите входные данные для progressValue и progressMax как это:

1
2
<input ng-model=»progressValue»>
<input ng-model=»progressMax»>

Вы заметите, что при изменении любого из значений изменение ширины происходит немедленно. Чтобы это выглядело немного лучше, давайте используем jQuery для его анимации. Хорошая вещь об использовании jQuery с AngularJS состоит в том, что когда вы включаете jQuery <script> Angular автоматически заменяет jqLite на него, делая $elem объектом jQuery.

Итак, давайте начнем с добавления скрипта jQuery в <head> документа перед AngularJS:

1
<script src=»http://code.jquery.com/jquery-2.1.0.min.js»></script>

Теперь мы можем изменить нашу updateProgress() чтобы использовать метод jQuery .animate() . Измените эту строку:

1
$elem.children()[0].style.width = $scope.percentValue + ‘%’;

К этому:

1
$elem.children(‘.progressBar’).stop(true, true).animate({ width: $scope.percentValue + ‘%’ });

И вы должны иметь красиво анимированный индикатор выполнения. Нам пришлось использовать метод .stop (), чтобы остановить и завершить любые ожидающие анимации в случае, если мы изменим любое значение во время анимации (попробуйте удалить его и быстро изменить значения во входных данных, чтобы понять, зачем это нужно).

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

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

Вот несколько ссылок на документацию AngularJS: