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,и вы заинтересованы в том, чтобы продолжить обучение и попробовать его.