Статьи

AngularJS — получите первое впечатление

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