AngularJS: от прототипирования до функционального кода. Часть 2
AngularJS

AngularJS: от прототипирования до функционального кода. Часть 2

Оригинал: AngularJS: From Prototyping to Functional Code, Fernando Villalobos

Вторая часть перевода статьи Фернандо Виллалобоса, посвященная созданию прототипа приложения на AngularJS. Автор подробно описывает процесс создания CRUD-интерфейса.

AngularJS: от прототипирования до функционального кода. Часть 1
AngularJS: от прототипирования до функционального кода. Часть 3

5. Создание прототипа

Изначально HTML содержимое нашего проекта довольно простое:


<div class="container"> 
    <div class="page-header"> 
        <h1>Clients</h1>
    </div> 
</div>

Блок div.container отлично подойдет в качестве контейнера для AngularJS-приложения. Теперь нам нужно инициализировать приложение clientsApp в файле scripts/app.js следующим образом:


angular.module('clientsApp', []);

Дальше нам нужно положить куда-то всю логику управления клиентами. Отпраделим контроллер UsersCtrl в файле scripts/controllers/clientsCtrl.js.


angular.module('clientsApp').controller('ClientsCtrl', function($scope) {
});

Теперь в наш index.html мы должны добавить директивы ng-app и ng-controller, так Angular будет знать, какой областью видимости он должен оперировать.


<div class="container" ng-app="clientsApp" ng-controller="ClientsCtrl">
  ...
</div>

Теперь мы готовы начать разработку нашего прототипа! Глубокий вдох и за работу.

5.1. Список пользователей

Первая задача, стоящая перед нами — это отображение списка клиентов — но откуда должна приходить эта информация? Поскольку мы только начинаем разработку прототипа, у нас нет никакого API (а на самом деле нет даже бэкенда), с которым мы могли бы взаимодействовать. Опыт подсказывает, что мы будем иметь дело с JSON REST API со всеми принципами чтения/записи информации (GET, POST, PUT/PATCH, DELETE), но разработка API на этапе прототипирования может привести к пустой трате времени и сил. Мы должны подождать, пока не получим больше информации, и, что более важно, пока наш проект не будет утвержден.

Как же нам преодолеть эту неопределенность? Единственное, что мы знаем на этом этапе — мы будем манипулировать данными в приложении, чтобы выполнить задачи.

5.1.1 Подстановка данных для прототипа

Благодаря тому, что AngularJS работает с объектами JavaScript, также известными как Plain Old Javascript Objects (POJOs), для моделей мы можем использовать тестовые данные, напрямую вставленные в контроллер, независимо от источника данных или стандарта взаимодействия с бэкендом. Мы знаем наверняка, что в конечном итоге эти данные будут храниться в виде базовой структуры JavaScript, например, в виде массива внутри области видимости, а представления и директивы AngularJS без проблем будут манипулировать этими данными. Давайте используем этот факт и загрузим наши данные непосредственно в контроллер.


// scripts/controllers/usersCtrl.js 
angular.module('clientsApp').controller('ClientsCtrl', function($scope) { 
    $scope.clients = [ 
        { id: 1, name: 'John', age: 25, percentage: 0.3 }, 
        { id: 2, name: 'Jane', age: 39, percentage: 0.18 }, 
        { id: 3, name: 'Jude', age: 51, percentage: 0.54 }, 
        { id: 4, name: 'James', age: 18, percentage: 0.32 } 
    ]; 
});

В представлении мы можем использовать директиву ng-repeat, чтобы отобразить список клиентов в виде таблицы, так же, как мы делали бы в полноценном AngularJS-приложении с бэкендом.


<!-- index.html --> 
<div class="container" ng-app="clientsApp" ng-controller="ClientsCtrl"> 
    <table class="table"> 
        <thead> 
            <tr> 
                <th>Id</th> 
                <th>Name</th> 
                <th>Age</th> 
                <th>Percentage</th> 
            </tr> 
        </thead> 
        <tbody> 
            <tr ng-repeat="client in clients"> 
                <td>{{ client.id }}</td> 
                <td>{{ client.name }}</td> 
                <td>{{ client.age }}</td> 
                <td>{{ client.percentage }}</td> 
            </tr> 
        </tbody> 
    </table> 
</div>
Вывод списка элементов AngularJS

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

5.2. Форматирование данных

Часто, когда мы показываем прототип, клиент упускает главную цель демонстрации из-за мелких (даже незначительных) деталей. «Эй, почему эти числа отформатированы не как процентные значения?» — звучит знакомо? Такие мелочи могут отвлечь клиента от ключевых деталей.

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

В наших тестовых данных есть некоторые числовые (процентные) значения, и пользователь будет ожидать, что эти значения будут отображаться правильно; давайте реализуем это:


$scope.percentageOf = function(percentage) { 
    return percentage * 100 + ' %'; 
};

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

Нам только нужно вызвать этот метод в представлении, передав ему свойство client.percentage:


    <thead> 
        <tr> 
            ... 
            <td>{{ percentageOf(client.percentage) }}</td>
         </tr> 
    </thead> 
</table>
Форматирование значений при выводе AngularJS

Готово! Довольно просто мы получили отформатированные данные. Теперь данные будут отображаться правильно, и, если это сделает нашего клиента счастливым, мы сможем (надеюсь, в ближайшем будущем) реализовать более надежное, полноценное решение.

6. Удаление клиентов

После того, как мы закончили с задачей отображения списка клиентов, пора заняться манипуляциями с данными. Удаление клиентов является простой задачей. Благодаря природе данных, которые мы используем, удаление будет осуществляться за счет удаления нужного элемента из массива.

Добавим метод delete в контроллер ClientsCtrl, который будет принимать в качестве параметра объект client, который будет удален:


$scope.delete = function(client) { 
    var index = $scope.clients.indexOf(client); 
    $scope.clients.splice(index, 1); 
};

В представлении разместим кнопку Delete внутри директивы ng-repeat, чтобы иметь возможность удалить любого клиента.


<thead> 
    <tr> 
        <th>Id</th> 
        <th>Name</th> 
        <th>Age</th> 
        <th>Percentage</th> 
        <th>Actions</th> 
    </tr> 
</thead> 
<tbody> 
    <tr ng-repeat='client in clients'> 
        <td>{{ client.id }}</td> 
        <td>{{ client.name }}</td> 
        <td>{{ client.age }}</td> 
        <td>{{ percentageOf(client.percentage) }}</td> 
        <td><a class='btn btn-danger' href='' ng-click='delete(client)'> Delete </a></td> 
    </tr> 
</tbody>

Давайте проверим. Обновим страницу, кликнем по ссылке Delete и …

Удаление элементов из списка AngularJS

Работает! Мы реализовали ограниченную функцию удаления — ровно настолько, чтобы визуально продемонстрировать клиенту поведение системы. Стоит отметить, что, после того, как обновим страницу, «удаленные» клиенты восстановятся, поскольку данные «зашиты» в контроллере.

7. Создание клиентов

ОК, теперь мы отображаем клиентов в виде таблицы и удаляем клиентов — половина пути пройдена!

Реализуем возможность добавления нового клиента. Мы создадим форму добавления клиента с валидацией введенной информации, и если вся введенная информация корректна — форма будет добавлять нового клиента в список. Если введенные данные окажутся некорректными — отобразим соответствующие сообщения об ошибках.

7.1. Форма добавления клиента

В первую очередь, мы должны предоставить нашим пользователям возможность добавлять информацию в нашу «базу данных». Главная идея состоит в том, чтобы создать форму, в которую пользователь сможет вводить необходимую информацию. Для этого создадим обычную HTML-форму с добавлением angular-директив.

Давайте создадим форму с тремя текстовыми полями, имя, возраст и процент. Директива ng-model позволит нам связать поля формы с объектом, который будет передаваться в контроллер.


<form role='form'> 
    <div class='form-group'> 
        <label for='name'>Name:</label> 
        <input class='form-control' ng-model='newClient.name' type='text'> 
    </div> 
    <div class='form-group'> 
        <label for='age'>Age</label> 
        <input class='form-control' ng-model='newClient.age' type='number'> 
    </div> 
    <div class='form-group'> 
        <label for='percentage'>Percentage</label> 
        <input class='form-control' ng-model='newClient.percentage' type='number'> 
    </div> 
</form>

После того, как форма создана и ее поля связаны с объектом newClient, мы должны добавить кнопку submit. Эта кнопка помимо отправки данных в контроллер должна предотвратить отправку HTTP-запроса. В противном случае страница обновится и мы потеряем информацию, введенную ранее. Мы можем решить эту задачу двумя способами: добавить директиву ng-submit форме или директиву ng-click — кнопке. Давайте реализуем второй вариант; когда пользователь кликнет по кнопке, директива ng-click будет вызывать метод create контроллера clientsCtrl (который мы добавим позже).


<form role='form'> 
    ... 
    <button class='btn btn-primary' ng-click='create()'>Save</button> 
</form>

Когда эта информация отправляется, мы ожидаем, что добавленный клиент отобразится в списке клиентов; другими словами, мы должны добавить его в коллекцию клиентов в области видимости. Звучит как добавление нового элемента в массив? Конечно, именно это нам и нужно сделать.

Давайте добавим метод create в контроллер ClientsCtrl. Он будет принимать новый объект и добавлять его в массив:


$scope.create = function() { 
    $scope.newClient.id = $scope.clients.length + 1; 
    $scope.clients.push($scope.newClient); 
    $scope.newClient = null; 
};

Обратите внимание, как мы имитируем увеличение id клиента. Обновим страницу и добавим нового клиента с помощью формы.

Добавление элемента AngularJS

Нажимаем кнопку Save и видим, что новый клиент добавился в список. Работает!

Добавление элемента AngularJS

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

7.1.1. Валидация

Мы можем довольно легко добавить клиентскую валидацию с помощью директив AngularJS. Необходимо просто добавить эти директивы к HTML-тегам. В зависимости от желаемой проверки можно использовать различные директивы. Мы добавим несколько простых проверок: обязательность для всех полей и min- и max-значение для числовых полей.


<form role='form'> 
    <div class='form-group'> 
        <label for='name'>Name:</label> 
        <input class='form-control' ng-model='newClient.name' type='text' required> 
    </div> 
    <div class='form-group'> 
        <label for='age'>Age</label> 
        <input class='form-control' ng-model='newClient.age' type='number' required max='100' min='1'> 
    </div> 
    <div class='form-group'> 
        <label for='percentage'>Percentage</label> 
        <input class='form-control' ng-model='newClient.percentage' type='number' required max='1' min='0'> 
    </div> 
    <button class='btn btn-primary' ng-click='create()'>Save</button> 
</form>

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


<form novalidate role='form'>
</form>

Хорошая практика - деактивировать кнопку, если форма невалидна. Для этого мы будем использовать директиву ng-disabled, нам нужно ли задать атрибут name формы. Это позволит нам проверять состояние формы в целом или отдельного атрибута. Для нашего прототипа нам необходимо проверить состояние атрибута $valid формы.


<form name='clientForm' novalidate role='form'> 
    ... 
    <button class='btn btn-primary' ng-click='create()' ng-disabled='clientForm.$invalid'>Save</button> 
</form>

И наконец, улучшим юзабилити, добавив сообщения об ошибке каждому элементу формы. Angular позволяет определить, когда данные некорректны. Если задать атрибут name полям формы, можно использовать атрибут $error.validation и директиву ng-show в каждом блоке form-group, чтобы показать пользователю, что данные некорректны и в чем проблема:


<form name='clientForm' novalidate role='form'> 
    <div class='form-group'> 
        <label for='name'>Name:</label> 
        <input class='form-control' ng-model='newClient.name' name='name' type='text' required> 
        <span class='help-block' ng-show='clientForm.name.$error.required'>Name is required</span> 
    </div> 
    <div class='form-group'> 
        <label for='age'>Age</label> 
        <input class='form-control' ng-model='newClient.age' name='age' type='number' required max='100' min='1'> 
        <span class='help-block' ng-show='clientForm.age.$error.required'>Age is required</span>
        <span class='help-block' ng-show='clientForm.age.$error.min'>Age should be greater than 1</span> 
        <span class='help-block' ng-show='clientForm.age.$error.max'>Age should be lesser than 100</span> 
    </div> 
    <div class='form-group'> 
        <label for='percentage'>Percentage</label> 
        <input class='form-control' ng-model='newClient.percentage' name='percentage' type='number' required max='1' min='0'> 
        <span class='help-block' ng-show='clientForm.percentage.$error.required'>Percentage is required</span> 
        <span class='help-block' ng-show='clientForm.percentage.$error.min'>Percentage should be greater than 0</span> 
        <span class='help-block' ng-show='clientForm.percentage.$error.max'>Percentage should be lesser than 1</span> 
    </div> 
    <button class='btn btn-primary' ng-click='create()' ng-disabled='clientForm.$invalid'> Save </button> 
</form>
Валидация полей формы AngularJS

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

7.2. Редактирование пользователей

Наш прототип почти готов и нам осталось рализовать только одно действие CRUD. Редактирование клиента будет осуществляться непосредственно в таблице и мы будем использовать те же правила валидации, что и при добавлении.

7.2.1. Редактирование в таблице

В данном случае, клиент попросил, чтобы редактирование пользователей осуществлялось непосредственно в таблице, когда строка таблицы переключается в режим редактирования и обратно после завершения редактирования (и отображает измененные данные). Первое, что нам нужно — это создать альтернативное представление для строки таблицы. Для этого мы добавим новые теги tr в HTML-структуру после каждого существующего тега tr. Каждая колонка в таких строках будет содержать поле ввода, связанное, связанное с атрибутами соответствущей модели с помощью директивы ng-model, так же, как мы это сделали для добавления.


<tbody ng-repeat='client in clients'> 
    <tr> <!-- read-only cells --> </tr> 
    <tr> 
        <td>{{ client.id }}</td> 
        <td><input class='form-control' ng-model='client.name' type='text'></td> 
        <td><input class='form-control' ng-model='client.age' type='text'></td> 
        <td><input class='form-control' ng-model='client.percentage' type='text'> </td> 
        <td></td>
    </tr>
</tbody>

Обратите внимание, мы перенесли директиву ng-repeat в тег tbody, чтобы иметь возможность отобразить сразу две строки в одной итерации. Такого поведения можно добиться и другими способами.

Редактирование элемента AngularJS

Уже ближе, но нам нужно отображать строки таблицы в зависимости от определенного условия. Директива ng-if нам в этом поможет! С помощью этой директивы у нас есть возможность показывать/скрывать (а если быть точнее, рендерить или нет) HTML-теги в зависимости от условия. Какое условие мы должны учитывать?

Мы может использовать мы можем использовать переменную activeClient, в которой будет храниться активный объект (находящийся в режиме редактирования), остальные объекты будут находиться в режиме просмотра. Преобразуем это в программный код:


<tbody ng-repeat='client in clients'>
    <tr ng-if='client != activeClient'><!-- read-only cells --></tr>
    <tr ng-if='client == activeClient'><!-- editable cells --></tr>
</tbody>

Теперь нам нужно добавить кнопки редактирования/сохранения, которые на самом деле будут преключать активный объект activeClient. В директиве ng-click этих кнопок вызовем соответствующие методы:


<tbody ng-repeat='client in clients'>
    <tr ng-if='client != activeClient'>
        <!-- read-only cells -->
        <td>
            <a class='btn btn-primary' href='' ng-click='edit(client)'> Edit </a>
            <a class='btn btn-danger' href='' ng-click='delete(client)'> Delete </a>
        </td>
    </tr>
    <tr ng-if='client == activeClient'>
        <!-- editable cells -->
        <td>
            <a class='btn btn-primary' href='' ng-click='update(client)'> Update </a>
        </td>
    </tr>
</tbody>

И последнее, но не менее важное, мы должны реализовать оба эти метода в контроллере. Явно инициализируем значение activeClient значением null в коде. Метод edit будет присваивать activeClient текущий объект, метод update — возвращать начальное значение null, тем самым переключая представление в режим просмотра. Реализация выглядит следующим образом:


$scope.edit = function(client) {
    $scope.activeClient = client;
}; 
$scope.update = function(client) { 
    $scope.activeClient = null; 
};
Редактирование элемента AngularJS

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

Кажется, все реализовано, мы закончили наш прототип. Настал момент истины: демо для наших клиентов. Надеемся, он им понравится, и даже в случае, если они попросят модификаций — не страшно, т.к. мы написали небольшое количество кода. Скрестим пальцы...

Рассылка
Подпишитесь на рассылку и получайте дайджест новостей и статей.
Никакого спама!
Подписаться