Применение сложных бизнес-ограничений в отношении данных, представленных пользователем, создает уникальные проблемы для значительного числа разработчиков. Недавно мы с моей командой столкнулись с такой проблемой при написании заявки на GiftCards.com. Нам нужно было найти способ, позволяющий нашим клиентам редактировать несколько продуктов в одном представлении нашего приложения, где каждый продукт имел уникальный набор правил проверки.
Это оказалось трудным делом, потому что нам требовалось иметь несколько тегов <form>
в исходном HTML-коде и поддерживать модель проверки для каждого экземпляра формы. Мы попробовали много подходов, таких как использование ngRepeat
для отображения дочерних форм, прежде чем ngRepeat
решение. Мы создадим одну директиву для каждого типа продукта (где каждая директива будет иметь <form>
в своем представлении) и привязать директиву к своему родительскому контроллеру. Это позволило нам воспользоваться преимуществами наследования дочерних / родительских форм Angular, чтобы гарантировать, что родительская форма была действительной, только если все дочерние формы были действительными.
В этом руководстве мы создадим простой экран обзора продукта (который выделяет ключевые компоненты нашего текущего приложения). У нас будет два продукта, каждый со своей директивой и каждый с уникальными правилами валидации. Там будет простая кнопка checkout
, которая будет гарантировать, что обе формы действительны.
Если вам не терпится увидеть это в действии, вы можете сразу перейти к нашей демонстрации или загрузить код из нашего репозитория GitHub .
Слово о директивах
Директива — это блок HTML-кода, который проходит через HTML-компилятор AngularJS ( $compile
) и добавляется в DOM. Компилятор отвечает за обход DOM в поисках компонентов, которые он может превратить в объекты, используя другие зарегистрированные директивы. Директивы работают в изолированном объеме и поддерживают свое собственное представление. Они являются мощными инструментами, которые продвигают повторно используемые компоненты, которые могут использоваться всеми приложениями. Для быстрой переподготовки ознакомьтесь с этой статьей SitePoint или документацией AngularJS .
Директивы решили нашу фундаментальную проблему двумя способами: во-первых, каждый экземпляр имеет изолированную область видимости, а во-вторых, директива использует проход компилятора, в результате чего компилятор идентифицирует элемент формы в HTML представления, используя директиву Angular ngForm
. Эта встроенная директива позволяет использовать несколько вложенных элементов form
, принимает необязательный атрибут name
для создания экземпляра Form Controller
и возвращает объект формы.
И слово о контроллерах форм
Когда компилятор идентифицирует любой объект формы в DOM, он будет использовать директиву ngForm
для создания экземпляра объекта Form Controller
. Этот контроллер будет сканировать все input
элементы select
и textarea
и создавать соответствующие элементы управления. Для элементов управления требуется атрибут model
чтобы установить двустороннюю привязку данных и обеспечить мгновенную обратную связь с пользователем с помощью различных предварительно созданных методов проверки. Предоставление мгновенного отзыва потребителю позволяет ему узнать, какая информация действительна, прежде чем отправлять HTTP-запрос
Предварительно построенные методы проверки
Angular поставляется с 14 стандартными методами проверки. К ним относятся валидаторы для min
, max
, которые required
max
, но не все. Они созданы для понимания и работы практически со всеми типами ввода HTML5 и совместимы с различными браузерами.
<form name="form" novalidate> Size: <input type="text" ng-model="size" name="size" ng-required="true" /> <span ng-show="form.size.$error.required">The value is required!</span> </form>
В приведенном выше примере показано использование валидатора директивы ngRequired
в Angular. Эта проверка гарантирует, что поле заполнено до того, как оно будет считаться действительным. Он не проверяет какие-либо данные, только то, что пользователь что-то ввел. Наличие атрибута novalidate
означает, что браузер не должен проверять после novalidate
.
Совет: не устанавливайте атрибут
action
в любой угловой форме. Это предотвратит попытки Angular обеспечить, чтобы форма не была отправлена туда и обратно.
Пользовательские методы проверки
Angular предоставляет обширный API для помощи в создании пользовательских правил проверки. Использование этого API дает вам возможность создавать и расширять ваши собственные правила проверки для сложных входных данных, которые не включены в стандартные проверки. Моя команда и я полагаемся на несколько пользовательских методов проверки для запуска сложных шаблонов RegEx, которые используются нашим сервером. Без возможности запуска сложных сопоставлений RegEx мы потенциально могли бы отправлять неверные данные на наш внутренний сервер. Это представит пользователю ошибки, которые вызывают нежелательное взаимодействие с пользователем. Пользовательские валидаторы используют синтаксис директивы и требуют ngModel
. Дополнительную информацию можно найти в документации AngularJS .
Создание контроллера
С этим из пути мы можем начать наше приложение. Вы можете найти обзор кода контроллера здесь .
Контроллер будет сердцем вещей. Он имеет только несколько обязанностей — его представление будет иметь элемент формы с именем parentForm
, у него будет только одно свойство, а его методы будут состоять из registerFormScope
, validateChildForm
и checkout
.
Свойства контроллера
Нам понадобится одно свойство в контроллере:
$scope.formsValid = false;
Это свойство используется для поддержания логического состояния общей достоверности форм. Мы используем это свойство, чтобы отключить состояние кнопки «Оформить заказ» после ее нажатия.
Метод: registerFormScope
$scope.registerFormScope = function (form, id) { $scope.parentForm['childForm'+id] = form; };
Когда вызывается registerFormScope
ему будет передан Form Controller
вместе с уникальным идентификатором директивы, созданным в экземпляре директивы. Этот метод затем добавит область формы к родительскому Form Controller
.
Метод: validateChildForm
Это метод, который будет использоваться для координации с внутренним сервером, который выполняет проверку. Он вызывается, когда пользователь редактирует контент, и он должен пройти дополнительную проверку. Концептуально мы не позволяем директивам осуществлять какую-либо внешнюю связь.
Обратите внимание, что я пропустил компонент бэкэнда для целей данного руководства. Вместо этого я отклоняю или разрешаю обещание в зависимости от того, входит ли сумма, которую вводит пользователь, в определенный диапазон (10–50 для продукта A и 25–500 для продукта B).
$scope.validateChildForm = function (form, data, product) { // Reset the forms so they are no longer valid $scope.formsValid = false; var deferred = $q.defer(); // Logic to validate the form and data // Must return either resolve(), or reject() on the promise. $timeout(function () { if (angular.isUndefined(data.amount)) { return deferred.reject(['amount']); } if ((data.amount < product.minAmount) || (data.amount > product.maxAmount)) { return deferred.reject(['amount']); } deferred.resolve(); }); return deferred.promise; }
Использование службы $q
позволяет директивам придерживаться интерфейса с состоянием успеха и сбоя. Характер интерфейса приложения изменяется между «Редактировать» и «Сохранить» в зависимости от редактирования данных модели. Следует отметить, что данные модели обновляются, как только пользователь начинает печатать.
Метод: Оформить заказ
Нажатие «Оформить заказ» означает, что пользователь закончил редактирование и хочет оформить заказ. Этот действующий элемент должен подтвердить, что все формы, загруженные в директивах, проходят проверку перед отправкой данных модели на сервер. В этой статье не рассматриваются методы, используемые для отправки данных на сервер. Я рекомендую вам изучить использование $ http сервиса для всех ваших соединений между клиентом и сервером.
$scope.checkout = function () { if($scope.parentForm.$valid) { // Connect with the server to POST data } $scope.formsValid = $scope.parentForm.$valid; };
Этот метод использует способность Angular для дочерней формы аннулировать родительскую форму. Родительская форма называется parentForm
чтобы ясно показать ее связь с дочерними формами. Когда childForm
использует свой метод $setValidity
, он автоматически поднимается к родительской форме, чтобы установить там валидность. Все формы в parentForm
должны быть действительными, чтобы его внутреннее свойство $valid
было истинным.
Создание наших директив
Наши директивы должны следовать общему интерфейсу, который обеспечивает полную совместимость и расширяемость. Названия наших директив зависят от продукта, который они содержат.
Обзор кода директивы можно найти здесь (продукт A) и здесь (продукт B) .
Изолированная директивная сфера
Каждая созданная директива получит изолированную область видимости, которая локализована в директиве и не знает внешних атрибутов. Однако AngularJS позволяет создавать директивы, которые используют методы и свойства родительской области видимости. При передаче внешних атрибутов в локализованную область вы можете указать, что хотите настроить двустороннюю привязку данных.
Наше приложение будет нуждаться в нескольких внешних двусторонних методах и свойствах, связанных с данными:
scope: { registerFormScope: '=', giftData: '=', validateChildForm: '=', product: '=' },
Метод: registerFormScope
Первое свойство в локальной области действия директивы — это метод, который регистрирует локальную область scope.form
в контроллере. Директиве нужен канал для передачи локального объекта Form Controller
главному Controller
.
Объект: giftData
Это централизованные данные модели, которые будут использоваться в представлениях директив. Эта информация будет двухсторонней привязкой данных, чтобы гарантировать, что обновления, которые происходят в Form Controller
будут распространяться на основной Controller
.
Метод: validateChildForm
Этот метод тот же, который определен внутри Controller
. Этот метод будет вызываться, когда пользователь обновляет информацию в представлении директивы.
Объект: продукт
Этот объект содержит информацию о продукте, который приобретается. Наше демо использует сравнительно небольшой объект с несколькими свойствами. Реальное приложение моей команды содержит большой объем информации, которая используется для принятия решений в приложении. Он передается в validateChildForm
для предоставления контекста тому, что проверяется.
Директива Связывание
Наши директивы будут использовать функцию postLink
, передавая ей объект области видимости. В дополнение к этому функция postLink
принимает несколько других параметров. Это следующие:
-
scope
— используется для получения доступа к изолированной области, созданной для каждого экземпляра директивы. -
iElement
— используется для получения доступа к элементарным предметам. Безопасно обновлять и изменять элемент, которому он был назначен, из функцииpostLink
. -
iAttrs
— используется для получения доступа к атрибутам, которые находятся в том же теге, в котором была создана директива. -
controller
— может использоваться в функциях связывания, если существуют зависимости от внешнего контроллера. Они должны соответствовать обязательному свойствуDirective Object
. -
transcludeFn
— функция такая же, как перечисленная в параметре$transclude
Directive Object
.
link
отвечает за присоединение всех слушателей DOM и обновление DOM с помощью элементов view.
link: function postLink(scope) { // Indicates if the form is disabled scope.disabled = true; scope.saveForm = function () { // Code for saving the form data }; // Register form scope $timeout(function() { }); }
Зарегистрируйте область формы
$timeout(function () { scope.form.fields = ['name','amount']; scope.registerFormScope(scope.form, scope.$id); });
Обертывание метода registerFormScope
пределах $timeout
задерживает выполнение до конца стека выполнения. Это дает компилятору достаточно времени для завершения всех необходимых связей между контроллером и директивой. scope.form.fields
— это массив, который представляет собой имя свойств, которые находятся в Form Controller
это важно для установки ошибок проверки на стороне сервера. Цель registerFormScope
— отправить Form Controller
родительскому контроллеру, позволяя вновь созданной форме быть дочерней по отношению к parentForm
.
Подтвердить, когда информация меняется
scope.saveForm = function () { scope.validateChildForm(scope.form, scope.giftData, scope.product) .then(function () { angular.forEach(scope.form.fields, function (val) { scope.form.$setValidity(val, true); scope.form[val].$error.server = false; }); scope.disabled = true; }, function (invalidFields) { angular.forEach(invalidFields, function (val) { if (angular.isDefined(scope.form[val])) { scope.form[val].$error.server = true; scope.form.$setValidity(val, false); } }); scope.disabled = false; }); };
Когда форма изменяется и пользователь готов к ее проверке, saveForm
метод saveForm
в директиве. Этот метод, в свою очередь, будет вызывать метод validateChildForm
контроллера, передавая Form Controller
, scope.giftData
и scope.product
. Контроллер возвращает обещание, которое будет разрешено или отклонено в зависимости от дополнительных правил проверки.
Когда обещание отклонено, контроллер вернет поля, которые были недействительными. Это используется для аннулирования формы (и parentForm
) наряду с установкой дополнительных ошибок на уровне поля. В нашей демонстрации мы используем простую пост-проверку поля количества и не возвращаем причину, по которой это не удалось. Отказ от validateChildForm
может быть настолько сложным или простым, как требует ваше приложение.
Когда обещание возвращается успешно, директива должна установить действительность полей в форме. Код также должен очищать любые ранее выявленные ошибки сервера. Это гарантирует, что Директива не дает ошибочных ошибок пользователю. Установка всех полей с $setValidity
ссылается на parentForm
в контроллере, чтобы также установить его действительность, при условии, что все дочерние формы действительны.
Установка наших взглядов
Представления не очень сложны, и для нашей демонстрации мы распределили продукты по следующим полям: name
и amount
. На следующем этапе мы рассмотрим мнения, необходимые для завершения этого приложения.
Обзор кода вида можно найти здесь (продукт A) и здесь (продукт B) .
Просмотр маршрута
<div data-ng-app="myApp" ng-controller="stageController"> <div id="main" class="container"> <h1>Review Order</h1> <form name="parentForm" novalidate> <div ng-repeat="gift in gifts" class="row"> <div class="col-lg-12" ng-if="gift.product.type == 'A'" product-A data-register-form-scope="registerFormScope" data-gift-data="gift.giftData" data-validate-child-form="validateChildForm" data-product="gift.product"> </div> <div class="col-lg-12" ng-if="gift.product.type == 'B'" product-B data-register-form-scope="registerFormScope" data-gift-data="gift.giftData" data-validate-child-form="validateChildForm" data-product="gift.product"> </div> </div> </form> <div class="row"> <div class="col-lg-12"> <button class="btn btn-primary" data-ng-click="checkout()">Checkout</button> <div class="alert alert-success" data-ng-show="formsValid">All forms are valid!</div> </div> </div> </div> </div>
Это представление важно, потому что оно устанавливает родительскую форму, которая будет использоваться для переноса всех дочерних форм, загружаемых из директив продукта. Использование ng-if
в ng-repeat
гарантирует, что DOM не будет заполняться некорректно неиспользуемым Form Controller
.
Директивный вид
<form name="form" novalidate> ... <label for="amountInput">Amount</label> <input id="amountInput" name="amount" class="text-center form-control" type="tel" data-ng-model="giftData.amount" data-ng-pattern="/^(?!\.?$)\d+(\.\d{0,2})?$/" data-ng-required="true" data-ng-disabled="disabled"/> ... <label>Actions</label> <button class="btn btn-info" ng-click="disabled=false;" ng-show="disabled">Edit</button> <button class="btn btn-success" ng-click="saveForm()" ng-show="!disabled">Save</button> ... <div class="row" data-ng-show="form.$submitted"> <div class="col-lg-12"> <div class="alert alert-danger" data-ng-show="form.name.$error.required && form.$submitted"> Recipient Name is a required field. </div> <div class="alert alert-danger" data-ng-show="form.amount.$error.pattern && form.$submitted"> The amount is invalid. </div> <div class="alert alert-danger" data-ng-show="form.amount.$error.server && form.$submitted"> The amount is not accepted. Must be between {{ product.minAmount }} and {{ product.maxAmount}}. </div> </div> </div> ... </form>
Примечание. Представление выше было усечено в местах, связанных с макетом демонстрации и не имеет значения для этой статьи.
amountInput
устанавливает шаблон проверки, который будет применяться валидатором Angular ngPattern
. Поля выше будут использовать директиву ngDisabled
Angular, которая оценивает выражение, и если true
, поле будет отключено.
В нижней части представления мы показываем все ошибки, чтобы предоставить обратную связь пользователю, когда он нажимает кнопку Save
. Это установит свойство $submitted
в дочерней форме.
Завершение
Соединяя все части вместе, вот что мы в итоге получаем:
И не забывайте, вы можете найти весь код на GitHub .
Вывод
Моя команда и я многому научились при создании нашего последнего приложения. Изучение отношений между родителями и детьми позволило нам упростить наш обзорный экран. Использование директив позволяет нам разработать одну форму, которую можно использовать в любом контексте, и продвигать хороший повторно используемый код. Директивы также позволили нам иметь модульно-проверенный код, чтобы гарантировать, что наши формы работают как задумано. Наше приложение находится в производстве и выполнило более 100 000 заказов.
Надеюсь, вам понравилось читать эту статью. Если у вас есть какие-либо вопросы или комментарии, я буду рад услышать их в комментариях ниже.