На недавнем проекте передо мной стояла задача разработать калькулятор для расчета оконных изделий. В качестве исходных данных была предоставлена таблица в формате 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.
Комментариев нет:
Отправить комментарий