2015-06-12

Основы AngularJS

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

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

Приложения и контроллеры

Пример создания приложения с контроллером:

// модуль
angular.module('CalcApp', [])

// контроллер
// здесь $scope и Calc в аргументах - это внедряемые зависимости (dependency injection), может быть задан любой набор необходимых включений
//   $scope: область видимости контроллера
//   Calc: реализованный ниже сервис
.controller('CalcController', function ($scope, Calc) {
    $scope.Calc = Calc;

    $scope.discounts = [];
    $scope.discount = null;

    $scope.isDiscount = function (discount) {
         return $scope.discount == discount;
    };

  // отслеживание изменений
    $scope.$watch('Calc.qty', function (new_val, old_val) {
    // определяем текущую скидку для изменившегося кол-ва и записываем ее в $scope.discount
        var discount = null;
        for (var i = 0, m = $scope.discounts.length; i < m; i++)
        {
            var discount_item = $scope.discounts[i];
            if ((new_val || 0) < discount_item.min)
            {
                break;
            }
            discount = discount_item;
        }
        $scope.discount = discount;
    });
})

Как отмечено выше в коде, $scope.$watch следит за изменениями значения и вызывает функцию если они происходят.
В качестве первого аргумента может выступать любое выражение и даже функция, возвращающая отслеживаемое значение.
Существуют еще функции для наблюдения за списком $watchCollection и группой выражений $watchGroup.

Сервисы, фабрики и т.п.

AngularJS - модульный фреймворк. Можно легко оформлять повторно используемый код в виде модулей и потом внедрять их в разные приложения.

// сервис
.service('Calc', function () {
  // количество
    this.qty = 1;
  // цена за единицу
    this.cost = 0;
  // функция расчета итоговой стоимости
    this.total = function total() {
        return this.qty * this.cost;
    };
})

Двухстороннее связывание

В приложении задается связь между моделью и представлением. Связывание осуществляется через атрибут ng-model.

Количество: <input type="number" min="0" ng-model="Calc.qty"><br>
Стоимость: <input type="number" min="0" ng-model="Calc.cost"><br>
<!--
выражение содержит функцию расчета и фильтр
Calc.total(): функция из сервиса (см. код выше)
значение на странице будет автоматически обновляться при изменении Calc.qty или Calc.cost
-->
<strong>Итого:</strong> {{Calc.total() | currency}}

Фильтры

В приведенном выше коде currency является встроенным фильтром и изменяет выводимое значение - отображает итоговую сумму в денежном формате представления.
Для отображения в формате “1 234,56 руб.” необходимо подключить файл русской локализации.

{{ expression | filter1 | filter2:argument1:argument2:... }}

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

// свой фильтр, будет возвращать текст соответствующий фильтруемому значению
.filter('countRate', function (Calc) {
    return function (input, limits) {
        input = parseInt(input, 10) || 0;
        var lims = ('0:' + limits).split(':');
        var lim_i = 0;
        for (var i = 0, m = lims.length; i < m; i+=2)
        {
            if (input <= lims[i])
            {
                break;
            }
            lim_i = i;
        }
        return lims[lim_i + 1];
    };
})

Использование фильтра countRate:

<!--
рядом с полем ввода будет отображаться соответствующий комментарий к заданному кол-ву
от 0 до 5 - один текст, от 6 до 10 - другой и т.д.
-->
Количество: <input type="number" min="0" ng-model="qty">
{{Calc.qty | countRate:'маловато будет:5:нормально:10:хорошо:15:отлично'}}
<br>

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

Директивы

Очень интересная возможность. Позволяет создавать свои теги и атрибуты.

Пример создания аргумента “number”, добавляющего для поля ввода новое поведение - разрешающего только четные значения:

// директива
.directive('number', function() {
    return {
        require: 'ngModel', // без модели не имеет смысла
        restrict: 'A', // атрибут, может быть еще элементом и классом
        link: function (scope, element, attrs, ctrlModel) {
      // валидатор, будет помечать элемент ввода невалидным для нечетных чисел
            ctrlModel.$validators.number = function(modelValue) {
                return (modelValue % 2) == (attrs.number != 'even');
            };
      // также мы можем влиять на значение модели ($modelValue) и отображаемое значение ($viewValue)
      //   с помощью добавления своих функций в $parsers и $formatters
      // Важное замечание:
      //   если мы производим неявное изменение модели/dom-дерева, то необходимо его применить с помощью функции $apply() для соответствующего scope
        }
    };
})

Формы и валидаторы

В AngularJS хорошо поставлена работа с формами.
Любые поля ввода имеют свойства, по которым можно определить их валидность ($valid), список ошибок ($error), было ли поле изменено ($pristine/$dirty) и было ли просто тронуто ($touched/$untouched). Родительская форма агрегирует в себе вышеперечисленные свойства от дочерних элементов, то есть если какой-то элемент ввода стал невалидным, то то же самое можно будет сказать и про форму.

Финальный код шаблона

<!--
не забываем подключить AngularJS и js-код приложения
-->
<!-- ng-init: вызывается при инициализации приложения -->
<div ng-app="CalcApp" ng-controller="CalcController" ng-cloak ng-init="discounts = [{min: 2, val: 5}, {min: 5, val: 10}, {min: 7, val: 15}]; Calc.cost = 1;">
    <h1>Расчет</h1>
  <!-- ng-form: аналог тега form, с той лишь разницей, что ng-form могут быть вложенными -->
    <div ng-form="formCalc">
    <!--
    ng-model: задает связь с моделью (из scope контроллера)
    number="even": своя директива с атрибутом
    -->
        Количество: <input type="number" min="0" ng-model="Calc.qty" number="even"> <span ng-if="!formCalc.$error.number">{{Calc.qty | countRate:'маловато будет:5:нормально:10:хорошо:15:отлично'}}</span><br>
        Стоимость: <input type="number" min="0" ng-model="Calc.cost"><br>
    </div>
    <strong>Итого:</strong> {{Calc.total() | currency}}<br>
    Скидка:
    <ul>
    <!--
    ng-repeat: выводит элементы массива скидок отсортированные по ключу val объекта скидки
    ng-class: задает css-классы в зависимости от условий
    -->
        <li ng-repeat="discount in discounts | orderBy:'val'" ng-class="{active: isDiscount(discount)}">{{discount.val}}</li>
    </ul>
</div>

Promise, удаленные запросы, ресурсы, роутинг и многое другое

В текущем примере не показано использование promise и $http для удаленных запросов.
Просто хочется упомянуть, что есть такие полезные возможности.

Promise используется для асинхронных действий в $q, $timeout, $http и, возможно, еще в других модулях. Например, $http возвращает такой объект, можно назначить функции-обработчики положительного и ошибочного ответа, как только запрос закончит выполнение.

$resource предназначен для работы по REST.

С использованием роутинга и подключаемых шаблонов (ng-view) можно создавать многостраничные приложения.

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