AngularJS — это удивительный JavaScript-фреймворк, предназначенный для создания одностраничных приложений (SPA) с использованием архитектуры MVC. В этой статье я хотел бы продемонстрировать силу фреймворка и показать, как использовать угловые базовые функции в действии.
В качестве демонстрационного приложения мы создадим конвертер валют «Fancy Currency». Вариант использования приложения прост: пользователь вводит значение в исходной валюте и переводит значение в другие популярные мировые валюты. Помимо таблицы с преобразованными значениями приложение будет иметь диаграмму, дающую визуальное представление результатов. Также мы увидим, как реализовать сортировку таблиц, проверку формы и другие общие функции приложения.
Если вам интересно, что мы получим в итоге, перейдите по ссылке, чтобы посмотреть на результат.
Исходный код доступен здесь .
Хотите знать, как создать это приложение с AngularJS? Пожалуйста, продолжайте читать.
Структура приложения
Прежде чем мы начнем с Fancy Currency, давайте посмотрим на структуру нашего приложения.
Это довольно просто:
У нас есть каталог для стилей (css) и для кода javascript (js), который содержит контроллеры (js / controllers), модели (js / app / models), сценарии вендора (js / vendor) и общие сценарии приложения (app.js и db.js). Index.html — это главная страница SPA.
Список валют
Начнем с создания списка валют. Angular использует декларативный подход для создания представлений. Мы поместили следующую разметку в файл index.html:
<!DOCTYPE html>
<html ng-app>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<title>Fancy Currency</title>
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" href="css/bootstrap.css" />
<link rel="stylesheet" href="css/app.css" />
</head>
<body>
<header>
<div class="container">
<h2>Fancy Currency</h2>
</div>
</header>
<div class="container" ng-controller="app.ctrls.CurrencyConvertCtrl">
<div class="row">
<div class="span6">
<table class="table table-hover table-striped">
<tr>
<th>Currency</th>
<th>Ticker</th>
<th>Rate</th>
<th>Converted</th>
</tr>
<tr ng-repeat="currency in currencies">
<td>{{currency.name}}</td>
<td>{{currency.ticker}}</td>
<td>{{currency.rate}}</td>
<td>{{currency.value}}</td>
</tr>
</table>
</div>
</div>
</div>
<hr>
<script src="js/vendor/jquery-1.9.1.js"></script>
<script src="js/vendor/bootstrap.js"></script>
<script src="js/vendor/angular.js"></script>
<script src="js/vendor/highcharts.js"></script>
<script src="js/app.js"></script>
<script src="js/models/Currency.js"></script>
<script src="js/contollers/CurrencyConvertCtrl.js"></script>
<script src="js/db.js"></script>
</body>
</html>
Мы используем Twitter Bootstrap, чтобы образец выглядел красиво. Вот почему мы видим здесь некоторые классы CSS, связанные с начальной загрузкой, такие как contaner, row, span6 и table-striped.
Самое интересное в разметке — это угловые атрибуты:
-
ng-app запускает приложение. На странице должна быть только одна директива ng-app. Обычно он помещается в корень страницы.
-
ng-controller определяет контроллер, соответствующий представлению. В нашем случае это app.ctrls.CurrencyConvertCtrl.
-
ng-repeat определяет цикл for-each над коллекцией элементов. В нашем примере есть коллекция валют. Строка таблицы будет отображаться для каждой валюты.
Разметка списка валют готова. Теперь нам нужно определить модель (Currency) и контроллер (CurrencyConvertCtrl). Одна из лучших угловых функций — использование простых объектов Javascript для описания моделей и контроллеров. Нет необходимости наследовать от конкретной базовой модели, определенной базовой модели или около того. Это подводит нас к мертвому простому описанию модели:
app.models.Currency = function(attrs) {
angular.extend(this, {
name: "",
ticker: "",
rate: 0,
value: 0,
}, attrs);
};
Мы определяем поля модели со значениями по умолчанию и расширяем их значениями, передаваемыми в конструктор.
Контроллер тоже простой объект.
app.ctrls.CurrencyConvertCtrl = function($scope) {
$scope.currencies = [];
angular.forEach(app.db.Currencies, function(currency) {
$scope.currencies.push(new app.models.Currency(currency));
});
};
Конструктор контроллера имеет входной параметр с именем $ scope. Содержит данные модели представления. $ scope похож на ссылку между контроллером и представлением. Обычно конструктор контроллера расширяет $ scope полями и методами для представления.
Приложение является корневым пространством имен приложения. Это определено в app.js:
window.app = {
models: {},
ctrls: {}
};
В нашем демо мы определяем статический список валют в db.js. Конечно, мы могли бы загрузить данные с сервера, но это не так.
app.db = {
Currencies: [
{ name: "Australian Dollar", ticker: "AUD" },
...
{ name: "Swiss Franc", ticker: "CHF" },
{ name: "Turkish Lira", ticker: "TRY" },
{ name: "U.S. Dollar", ticker: "USD" },
]
};
Теперь мы можем запустить приложение и увидеть список валют:
Вы можете найти весь исходный код здесь .
обмен валюты
Это ясно с рендерингом простого списка. Теперь посмотрим, как сделать интерактивный просмотр. Позволяет код функциональности конвертации валюты.
Во-первых, мы добавляем форму для ввода конвертируемой стоимости и исходной валюты.
<div class="container" ng-controller="app.ctrls.CurrencyConvertCtrl">
<div class="row">
<div class="span12">
<form class="well form-inline">
<input type="number" class="input-small" ng-model="value">
<select ng-model="currency">
<option ng-repeat="currency in currencies" value="{{currency.ticker}}">
{{currency.name}} ({{currency.ticker}})
</option>
</select>
<button class="btn-primary btn" ng-click="convert()">Convert</button>
</form>
</div>
</div>
...
</div>
Мы видим два новых для нас угловых атрибута:
-
ng-model устанавливает двустороннюю связь между моделью представления и контролем HTML. Директива обычно используется для ввода, выбора или текстовой области.
-
ng-click — это выражение обработчика события click, запускающего элемент. В нашем случае после нажатия на кнопку конвертации будет выполнен метод $ scope.convert. Контекст выполнения выражения является моделью представления.
Теперь нам нужно реализовать метод convert в контроллере.
var CONVERT_SERVICE_URL = "https://rate-exchange.appspot.com/currency";
app.ctrls.CurrencyConvertCtrl = function($scope) {
var currencies = [];
angular.forEach(app.db.Currencies, function(currency) {
currencies.push(new app.models.Currency(currency));
});
angular.extend($scope, {
currencies: currencies,
convert: function() {
var self = this,
valueToConvert = self.value,
currencyTicker = self.currency;
angular.forEach(self.currencies, function(currency) {
if(currency.ticker === currencyTicker) {
currency.rate = 1;
currency.value = parseFloat(valueToConvert);
return;
}
$.ajax({
type: "GET",
url: CONVERT_SERVICE_URL,
dataType: "jsonp",
data: {
from: currencyTicker,
to: currency.ticker,
q: valueToConvert
}
}).done(function(data) {
currency.rate = data.rate;
currency.value = data.v;
self.$apply();
});
});
}
});
};
Мы перебираем список валют и делаем ajax запросы к сервису конвертации валют. При выполнении обратного вызова каждого запроса результат конвертации и курс обмена устанавливаются в модель. Запрос делается с помощью jQuery и выполняется вне углового контекста, поэтому нам необходимо уведомить angular об изменениях в модели вручную, вызвав специальный метод $ apply. Обычно, когда вы используете угловые утилиты и механизмы, нет необходимости вызывать $ apply, фреймворк делает это сам.
Давайте добавим логику форматирования чисел в нашу модель Currency.
Currency.prototype = {
formattedRate: function() {
return this._formatNumber(this.rate);
},
formattedValue: function() {
return this._formatNumber(this.value);
},
_formatNumber: function(number) {
return number ? number.toFixed(2) : "-";
}
};
Функциональность конвертации готова. Посмотрите на результат:
Дифф для шага можно найти здесь .
Таблица конверсий
Добавим гистограмму, отображающую результаты конверсии. Для этого мы будем использовать HighchartsJS — очень мощную и простую в использовании бесплатную библиотеку диаграмм javascript.
Мы добавляем новый div для графика.
<div class="row">
<div class="span5">
<table class="table table-hover table-striped">
<tr ng-repeat="currency in currencies">
...
</tr>
</table>
</div>
<div class="span7">
<div id="chart" class="convert-chart"></div>
</div>
</div>
Функция конструктора контроллера создает диаграмму:
var chart = new Highcharts.Chart({
chart: {
renderTo: "chart",
type: "bar"
},
series: [{
name: "Value",
data: []
}],
title: {
text: "Value in world currencies"
},
xAxis: {
title: {
text: null
}
},
yAxis: {
min: 0,
title: {
text: null
},
labels: {
overflow: "justify"
}
},
legend: {
enabled: false
},
plotOptions: {
bar: {
dataLabels: {
enabled: true,
formatter: function() {
return this.y.toFixed(2);
}
}
}
},
credits: {
enabled: false
}
});
Мы не будем слишком много говорить о графике. Вы можете найти подробную информацию на странице документации HighchartsJS . Два наиболее важных варианта:
-
renderTo — это идентификатор HTML-элемента, в котором будет отображаться диаграмма
-
тип — это тип диаграммы (мы используем гистограмму)
График готов. Все, что нам нужно, это установить ряд данных. Однако есть одна проблема: процесс преобразования данных является асинхронным, и мы можем устанавливать последовательности только после выполнения всех запросов. Как известно, $ .ajax возвращает обещание, разрешенное при успешном выполнении запроса. Мы объединяем обещания для всех запросов с $ .when и устанавливаем данные диаграммы для обратного вызова комбинированного обещания. Итак, давайте изменим функцию преобразования следующим образом:
convert: function() {
var self = this,
valueToConvert = self.value,
currencyTicker = self.currency,
promises = [];
angular.forEach(self.currencies, function(currency) {
if(currency.ticker === currencyTicker) {
currency.rate = 1;
currency.value = parseFloat(valueToConvert);
return;
}
var promise = $.ajax({
type: "GET",
url: CONVERT_SERVICE_URL,
dataType: "jsonp",
data: {
from: currencyTicker,
to: currency.ticker,
q: valueToConvert
}
}).done(function(data) {
currency.rate = data.rate;
currency.value = data.v;
});
promises.push(promise);
});
$.when.apply(null, promises).done(function() {
self.setChartData();
self.$apply();
});
}
Теперь есть одно обещание, поэтому мы можем вызвать функцию $ apply только один раз. Данные настройки функции для диаграммы довольно просты:
setChartData: function() {
var currencyTickers = [],
values = [];
angular.forEach(this.currencies, function(currency) {
currencyTickers.push(currency.ticker);
values.push(currency.value);
});
chart.xAxis[0].setCategories(currencyTickers);
chart.series[0].setData(values);
}
После внесения изменений у нас есть график с результатами конверсии.
Перейдите по ссылке, чтобы найти разницу в шаге.
Сортировка таблицы валют
Таблица сортировки часто является необходимой функцией. Давайте реализуем возможность сортировки для нашей таблицы валют. Сортировка выполняется нажатием на заголовок столбца таблицы. Чтобы добавить логику в заголовок таблицы, мы изменим рендеринг столбцов:
<table class="currency-table table table-hover table-striped">
<tr>
<th ng-repeat="column in columns" ng-click="setSorting(column.field)"
ng-class="columnClass(column.field)">
{{ columnSortingPrefix(column.field) }} {{column.title}}
</th>
</tr>
...
Мы видим один новый угловой атрибут здесь:
-
ng-class устанавливает класс CSS в элемент HTML. В нашем случае класс CSS зависит от текущего поля сортировки.
Конструктор контроллера имеет следующие изменения:
angular.extend($scope, {
...
columns: [
{ field: "name", title: "Currency" },
{ field: "ticker", title: "Ticker" },
{ field: "rate", title: "Rate" },
{ field: "value", title: "Converted" }
],
sorting: {
field: "name",
asc: true
},
columnClass: function(field) {
return this.sorting.field === field ? "sorted" : "";
},
columnSortingPrefix: function(field) {
if(this.sorting.field === field) {
return this.sorting.asc ? "↑" : "↓";
}
},
setSorting: function(field) {
var sorting = this.sorting;
if(sorting.field === field) {
sorting.asc = !sorting.asc;
} else {
sorting.field = field;
sorting.asc = true;
}
}
});
У $ scope есть несколько новых ингредиентов:
-
columns это массив с колонками таблицы.
-
сортировка — это простой объект с информацией о текущей сортировке (имя поля и направление сортировки).
-
columnClass возвращает класс CSS для HTML-элемента th.
-
columnSortingPrefix возвращает знак стрелки вверх / вниз для поля, чтобы указать направление сортировки.
-
setSorting устанавливает текущее поле сортировки и направление
«Где актуальная сортировка?», Спросите вы. Хороший вопрос. Сортировка действительно простая с угловым:
<tr ng-repeat="currency in currencies | orderBy : sorting.field : !sorting.asc">
<td>{{currency.name}}</td>
<td>{{currency.ticker}}</td>
<td>{{currency.formattedRate()}}</td>
<td>{{currency.formattedValue()}}</td>
</tr>
Делаем сортировку с помощью специального углового фильтра orderBy: expression: reverse.
У него есть два параметра:
-
Выражение используется для сортировки массива. В нашем сценарии это имя поля сортировки.
-
reverse говорит, нужно ли переворачивать элементы в массиве. Значение параметра зависит от направления сортировки.
Это все с этим. Дифф доступен здесь .
5. Загрузка и проверка
Теперь, когда все основные функциональные возможности готовы, пришло время немного поработать. Во-первых, было бы лучше дать пользователю некоторую обратную связь во время процесса преобразования, например, чтобы показать индикатор загрузки.
Чтобы уведомить пользователя во время процесса преобразования, мы добавляем поле флага, называемое loading, в область действия $. В начале функции преобразования флаг установлен в значение true. Обратный вызов обещания конвертации устанавливает флаг в значение false. В разметку добавляем загрузку с указанием gif.
<img src="img/ajax-loader.gif" ng-show="loading">
Появился новый угловой атрибут:
-
ng-show определяет, показывать ли HTML-элемент в зависимости от выражения. В нашем случае выражение является значением загрузки поля флага.
Это также хорошая практика для проверки ввода пользователя перед преобразованием. Angular позволяет с легкостью проводить валидацию. Мы можем использовать атрибуты HTML5, такие как «обязательные» и определенные типы ввода, такие как «число». Чтобы проверить, все ли введенные значения действительны, мы можем использовать преобразование. $ Invalid, где «Converting» — это имя формы. Поле $ invalid также доступно для каждого поля формы. Чтобы сообщить пользователю, что он должен вводить правильные значения, мы добавляем в него div оповещения с сообщением.
<form name="converting" class="well form-inline">
<input type="number" class="input-small" ng-model="value" placeholder="Enter Value"
required ng-class="{ error: converting.value.$invalid }">
<select ng-model="currency" required ng-class="{ error: converting.currency.$invalid }">
<option ng-repeat="currency in currencies" value="{{currency.ticker}}">
{{currency.name}} ({{currency.ticker}})
</option>
</select>
<button class="btn-primary btn" ng-click="convert()"
ng-disabled="converting.$invalid || loading">Convert</button>
<img src="img/ajax-loader.gif" ng-show="loading">
<div class="alert alert-info" ng-show="converting.$invalid">
Enter numeric value and select source currency to convert.</div>
</form>
Кнопка преобразования отключена, а значения формы недействительны. Мы достигаем этого с помощью атрибута ng-disabled . Для предотвращения одновременных преобразований кнопка конвертации также отключена при загрузке.
Директива ng-class является умной и позволяет использовать не только строковое значение, но и простой объект, где каждое поле является именем класса CSS, а значение поля является логическим значением, определяющим, присоединять ли класс. В нашем случае класс «ошибка» добавляется для каждого поля с недопустимым значением.
Теперь, если ввести неправильное значение, мы увидим следующее:
Разность шагов можно найти здесь .
Выводы
AngularJS — это очень мощная и гибкая инфраструктура JavaScript для создания SPA. В этом посте мы узнали, как использовать его основные функции: создавать представления и контроллеры, определять двусторонние привязки, прикреплять обработчики щелчков, проверять формы и сортировать таблицы. Мы увидели, как легко это можно использовать с другими библиотеками javascript, такими как jQuery и HighchartsJS. Одним из лучших преимуществ angular является то, что он позволяет использовать простые объекты javascript, объявляющие модели и контроллеры, в то время как другим средам часто требуется расширять определенные предопределенные типы. В Angular есть много других важных функций, таких как определение модулей, маршрутизация, пользовательские директивы (атрибуты) и внедрение зависимостей пользовательских служб. К сожалению, они выходят за рамки нашей короткой статьи. Надеюсь, у вас первое впечатление от AngularJS,и вы заинтересованы в том, чтобы продолжить обучение и попробовать его.