Сегодня мы с вами поговорим о том, как
сделать валидацию для наших приложений, используя AngularJS. Об использовании валидации на
стороне клиента вы можете посмотреть здесь в документации. Мы с вами рассмотрим, как можно использовать
одновременно клиентскую и серверную валидацию. Начнем, пожалуй, с первой
части и посмотрим, как реализовать валидацию на стороне клиента.
AngularJS Client Side Validation
Для этого у меня было создано приложение, в
котором я реализовал добавление студентов и отображение их. Для того чтобы
посмотреть, как это работает, взглянем на модель данных, которую мы
будем проверять.
public class Student
{
[Key]
public int
Id { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]
[StringLength(50,
MinimumLength = 1)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Display(Name = "First Name")]
[StringLength(50,
MinimumLength = 2, ErrorMessage = "First name must be between 2 and 50 characters.")]
public string FirstName { get; set; }
public string FullName
{
get
{
return FirstName + "
" + LastName;
}
}
}
Это все взаимодействует с нашим
контроллером через WebAPI контроллер.
public class StudentsController : ApiController
{
private StudentContext db = new StudentContext();
// GET: api/Students
public IQueryable<Student> GetStudents()
{
return db.Students;
}
// GET: api/Students/5
[ResponseType(typeof(Student))]
public IHttpActionResult GetStudent(int
id)
{
Student student = db.Students.Find(id);
if
(student == null)
{
return NotFound();
}
return Ok(student);
}
// PUT: api/Students/5
[ResponseType(typeof(void))]
public IHttpActionResult PutStudent(int
id, Student student)
{
if
(!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if
(id != student.Id)
{
return BadRequest();
}
db.Entry(student).State = EntityState.Modified;
try
{
db.SaveChanges();
}
catch
(DbUpdateConcurrencyException)
{
if
(!StudentExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return StatusCode(HttpStatusCode.NoContent);
}
// POST: api/Students
[ResponseType(typeof(Student))]
public IHttpActionResult PostStudent(Student student)
{
if
(!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.Students.Add(student);
db.SaveChanges();
return CreatedAtRoute("DefaultApi", new { id = student.Id },
student);
}
// DELETE: api/Students/5
[ResponseType(typeof(Student))]
public IHttpActionResult DeleteStudent(int
id)
{
Student student = db.Students.Find(id);
if
(student == null)
{
return NotFound();
}
db.Students.Remove(student);
db.SaveChanges();
return Ok(student);
}
protected override void Dispose(bool
disposing)
{
if
(disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
private bool
StudentExists(int id)
{
return db.Students.Count(e => e.Id == id) > 0;
}
}
Загружаем мы студентов для редактирования с
помощью контроллера editStudentCtr.
(function () {
'use strict';
angular
.module('app')
.controller('editStudentCtr', editStudentCtr);
editStudentCtr.$inject = ['$scope', '$http', '$routeParams', '$location'];
function
editStudentCtr($scope, $http, $routeParams, $location) {
console.log("editStudentCtr");
var
id = $routeParams.studentId;
var
url = 'api/Students/' + id;
$http.get(url)
.success(function (data) {
$scope.student = data;
})
.error(function (error) {
console.log(error.message);
});
$scope.save = function () {
$http.put('api/Students/' + id, $scope.student).success(function () {
$location.path("");
}).error(function (error) {
console.log(error.Message);
});
};
$scope.cancel = function () {
$location.path("");
};
}
})();
Теперь рассмотрим, как это работало раньше
до валидации.
<div id="createNewStudent">
<div class="row">
<form class="form-horizontal" role="form" name="studentForm">
<input type="hidden" id="studentId" ng-model="student.Id">
<div class="form-group">
<label class="control-label
col-md-2" for="firstName">First Name:</label>
<div class="col-md-10">
<input id="firstName" class="form-control" ng-model="student.FirstName" />
</div>
</div>
<div class="form-group">
<label class="control-label
col-md-2" for="lastName">Last Name:</label>
<div class="col-md-10">
<input id="lastName" class="form-control" ng-model="student.LastName" />
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2
col-md-10">
<button class="btn
btn-primary" id="saveStudent" ng-click="save()">Save</button>
<button class="btn
btn-primary" id="cancelStudent" ng-click="cancel()">Cancel</button>
</div>
</div>
</form>
</div>
</div>
Если запустить это пример, то можно
увидеть, что когда мы не вводим никаких данных, то есть поля для ввода у нас
пустые, кнопка сохранения все еще продолжает работать.
Это очень плохо. У нас сработает
только проверка на сервере, если она там присутствует. А что же делать, если ее
забыли там прописать? По сути, мы с вами разрешаем сами сохранять невалидную
модель. Поскольку это нам ни к чему, пора приступить к добавлению валидации на
клиентскую сторону. Для того чтобы это все заработало, нам нужно установить
свойство name для наших input контролов, для того
чтобы с ними нормально мог работать AngularJS, и добавить директиву required, и самое главное – нужно установить для
нашей формы директиву novalidate. Novalidate директива необходима, чтобы не
разрешать делать дефолтную валидацию, которая есть в браузере (будет красная
рамка вокруг контролов, которую проставляет вам конкретный браузер).
<div id="createNewStudent">
<div class="row">
<form class="form-horizontal" role="form" name="studentForm" novalidate>
<input type="hidden" id="studentId" ng-model="student.Id">
<div class="form-group">
<label class="control-label
col-md-2" for="firstName">First Name:</label>
<div class="col-md-10">
<input id="firstName" name="firstName" class="form-control" ng-model="student.FirstName" required/>
</div>
</div>
<div class="form-group">
<label class="control-label
col-md-2" for="lastName">Last Name:</label>
<div class="col-md-10">
<input id="lastName" name="lastName" class="form-control" ng-model="student.LastName" required/>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2
col-md-10">
<button class="btn
btn-primary" id="saveStudent" ng-click="save()">Save</button>
<button class="btn
btn-primary" id="cancelStudent" ng-click="cancel()">Cancel</button>
</div>
</div>
</form>
</div>
</div>
Ниже подчеркнуты изменения, которые
произошли в нашем примере.
К сожалению, после того как мы внесем эти
изменения, кнопка Save будет все равно доступна для сохранения. Для того
чтобы она перестала быть доступна, нам нужно указать для нее через директиву ng-disable невозможность ее
нажать, если на форме присутствуют невалидные данные.
<button class="btn btn-primary" id="saveStudent" ng-click="save()" ng-disabled="studentForm.$invalid">Save</button>
Теперь, как минимум, мы будем видеть, есть ли у
нас ошибки в модели. Мы не можем ничего сохранить в базу.
Но этого недостаточно. Нет подсветки
полей, в которых нужны данные, нет предупреждений о том, что данные не введены.
Поэтому начнем, пожалуй, с добавления текста о том, что наши поля обязательные
для заполнения. Для этого под нашим полем ввода для FirstName добавим следующий блок span:
<span class="help-block" ng-show="studentForm.firstName.$dirty && studentForm.firstName.$error.required">
The FirstName field is required.
</span>
Здесь мы с помощью директивы ng-show, если у нас срабатывает условие studentForm.firstName.$dirty && studentForm.firstName.$error.required показываем текст The FirstName field is required. Ниже приведен список того, как это все работает.
- $pristine: It will be TRUE, if the user has not interacted with the form yet
- $dirty: It will be TRUE, if the user has already interacted with the form.
- $valid: It will be TRUE, if all containing form and controls are valid
- $invalid: It will be TRUE, if at least one containing form and control is invalid.
- $error: Is an object hash, containing references to all invalid controls or forms, where:
- keys are validation tokens (error names)
- values are arrays of controls or forms that are invalid with given error.
Если кратко: $pristine – если мы не трогали форму или контрол, $dirty – если
взаимодействовали с контролом, $valid – если контрол или форма валидны, $invalid – соответственно, если форма или контрол невалидные, и $error – если содержатся какие-то ошибки. Ниже
приведены встроенные токены проверки, которые могут помочь в проверке формы.
- max
- maxlength
- min
- minlength
- number
- pattern
- required
- url
AngularJS также
предоставляет соответственные директивы для работы с CSS стилями.
- ng-pristine
- ng-dirty
- ng-valid
- ng-invalid
Для примера ниже приведен также список
атрибутов, которые можно применить на input поле.
- ng-required
- ng-minlength
- ng-maxlength
- ng-min
- ng-max
- ng-pattern
Часть из них мы сегодня и будем
использовать.
Думаю, после расписания такого количества
теории понятно, как оно работает. Осталось добавить такой же блок span для поля LastName.
<span class="help-block" ng-show="studentForm.lastName.$dirty && studentForm.lastName.$error.required">
The LastName field is required.
</span>
Полный измененный код приведен ниже:
<div class="form-group">
<label class="control-label
col-md-2" for="firstName">First Name:</label>
<div class="col-md-10">
<input id="firstName" name="firstName" class="form-control" ng-model="student.FirstName" required/>
<span class="help-block" ng-show="studentForm.firstName.$dirty && studentForm.firstName.$error.required">
The FirstName field is required.
</span>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="lastName">Last Name:</label>
<div class="col-md-10">
<input id="lastName" name="lastName" class="form-control" ng-model="student.LastName" required/>
<span class="help-block" ng-show="studentForm.lastName.$dirty && studentForm.lastName.$error.required">
The LastName field is required.
</span>
</div>
</div>
Если запустить проект, то можно увидеть,
что нужные данные у нас показываются, если мы ничего не вводим и не трогаем
поля.
Как-то все равно оно выглядит не очень. Для меня привычно, что рамочка вокруг поля с ошибкой должна быть подсвечена. Это можно
сделать, воспользовавшись комбинацией bootstrap + angularjs validation. Для этого с
помощью ng-class будем проставлять
для атрибута has-error. Ниже приведена
реализация всего этого.
<div class="col-md-10" ng-class="{'has-error': studentForm.firstName.$dirty && studentForm.firstName.$invalid}">
<input id="firstName" name="firstName" class="form-control" ng-model="student.FirstName" required/>
<span class="help-block" ng-show="studentForm.firstName.$dirty && studentForm.firstName.$error.required">
The FirstName field is required.
</span>
</div>
Аналогичное действие проводим для
следующего div блока, в котором
прописано поле input для изменения LastName. Посмотрим, как будет выглядеть измененная
форма для добавления и редактирования студентов.
Как видите, это уже больше похоже на правду
и подобно тому, что вы видели при валидации, например, на ASP.NET MVC. Предлагаю
закончить до конца наш пример и добавить проверку на максимальное и минимальное количество символов для наших полей, чтобы валидация соответствовала
действительности. Для этого нам нужно будет воспользоваться директивами ng-minlength и ng-max-length. Ниже приведен
обновленный пример с добавлением этих директив и отображение нужного текста, если данные по количеству символов невалидные.
<div class="form-group">
<label class="control-label
col-md-2" for="firstName">First Name:</label>
<div class="col-md-10" ng-class="{'has-error': studentForm.firstName.$dirty && studentForm.firstName.$invalid}">
<input id="firstName" name="firstName" class="form-control" ng-model="student.FirstName" required ng-minlength="1" ng-maxlength="50"/>
<span class="help-block" ng-show="studentForm.firstName.$dirty && studentForm.firstName.$error.required">
The FirstName field is required.
</span>
<span class="help-block" ng-show="studentForm.firstName.$error.minlength">
The LastName field must be more
than 1 symbols
</span>
<span class="help-block" ng-show="studentForm.firstName.$error.maxlength">
The LastName field must be less
than 50 symbols
</span>
</div>
</div>
<div class="form-group">
<label class="control-label
col-md-2" for="lastName">Last Name:</label>
<div class="col-md-10" ng-class="{'has-error': studentForm.lastName.$dirty && studentForm.lastName.$invalid}">
<input id="lastName" name="lastName" class="form-control" ng-model="student.LastName" required ng-minlength="2" ng-maxlength="50" />
<span class="help-block" ng-show="studentForm.lastName.$dirty && studentForm.lastName.$error.required">
The LastName field is required.
</span>
<span class="help-block" ng-show="studentForm.lastName.$error.minlength">
The LastName field must be more
than 2 symbols
</span>
<span class="help-block" ng-show="studentForm.lastName.$error.maxlength">
The LastName field must be less
than 50 symbols
</span>
</div>
</div>
Как видите, кода стало намного больше, так
как добавились две дополнительные проверки в каждый div блок. Что из этого получилось, можно
посмотреть ниже.
Как видите, у нас появилась проверка уже и
на длину текста с помощью AngularJS валидации. Вся проверка у нас была только
на стороне клиента. Но как вы понимаете, на сервере могут добавлять разные
уровни проверки, или мы что-то забыли проверить, так что настало самое время
реализовать еще серверную проверку. Тем более что у нас полностью не покрыта реализация LastName.
[RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]
[StringLength(50, MinimumLength = 1)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
Мы ее можем сделать на клиенте через ng-pattern, который позволяет
реализовать регулярки, но я предпочту это сделать на стороне сервера. Тем более, думаю, это может вам когда-то пригодиться.
AngularJS with ASP.NET Server side validation
Давайте посмотрим, как происходит
добавление нового студента с помощью AngularJS.
$scope.save
= function () {
$http.post('api/Students', $scope.student).success(function () {
$location.path("");
}).error(function (error) {
console.log(error.Message);
});
};
Результат мы просто залогировали. И если у нас сервер вернет ошибку, нам нужно подсветить нужный участок
кода, если у нас ошибка с нашей моделью данных. Посмотрите, например, на
серверный метод post, чтобы понять, как это работает.
//
POST: api/Students
[ResponseType(typeof(Student))]
public IHttpActionResult
PostStudent(Student student)
{
if
(!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.Students.Add(student);
db.SaveChanges();
return CreatedAtRoute("DefaultApi", new { id = student.Id },
student);
}
То есть, если модель невалидная, мы просто
проглатываем ошибку. Сейчас мы рассмотрим, что нужно делать и как с
этим бороться.
На помощь нам приходят директивы с AngularJS. Для этого добавляю в свою структуру
новую папку directives и создаю файл server-validate.js. Ниже приведена реализация этого файла.
"use
strict";
angular.module('app').directive('serverError', function () {
return {
restrict: 'A',
require: '?ngModel',
link: function (scope, element, attrs, ctrl) {
return element.on('change
keyup', function () {
return scope.$apply(function () {
return ctrl.$setValidity('server', true);
});
});
}
};
});
Здесь мы добавили, что для каждого элемента
мы меняем состояние ‘state’ и уведомляем об этом форму.
Теперь мы в наши поля для редактирования добавим нашу директиву. На
рисунке ниже показано, как нужно это сделать.
Теперь я добавлю еще один класс, который
будет перегонять то, что мне возвращает сервер, в ту нотацию, которую понимает
клиент. Для этого я создам новую папку в helpers и добавлю новый файл, который назову
error_helpers.js.
"use
strict";
var errorHelper = new
ErrorHelper();
function ErrorHelper() {
function
lowerizeFirstLetter(string) {
return string.charAt(0).toLowerCase() + string.slice(1);
};
this.validateForms = function(form, scopeErrors, modelState) {
angular.forEach(modelState, function(error, field) {
var
existField = lowerizeFirstLetter(field.split('.')[1]);
if
(!form.hasOwnProperty(existField)) {
console.log(existField + ' not found for validation');
} else
{
form[existField].$setValidity('server', false);
scopeErrors[existField] =
error.join(', ');
}
});
};
}
С помощью класса ErrorHelper я добавил метод validateForms, который принимает форму, массив с ошибками
и modelState. Затем просто
бегает по ошибкам и для тех полей, которым с сервера вернулось значение об
ошибке, проставляем поле ‘server’ в false. Если поле не найдено на форме, то я это просто логирую. На самом деле, вам лучше бросать ошибку в этом случае. Я делаю
это с помощью toast.
Здесь есть еще более плохая вещь, которую
постарайтесь не делать ни в коем случае. Вот она.
var errorHelper =
new ErrorHelper();
Я слегка ленив, поэтому создал глобальную
переменную, чтобы иметь доступ со всех своих контроллеров. Хотя можно было все
это де сделать через injection в AngularJS. Поэтому лучше делать, как правильно.
Затем нужно поменять логику сохранения в
контроллере. Для этого изменим метод save(), как показано ниже.
$scope.errors
= {}; //clean up previous
server errors
$scope.save
= function () {
$http.post('api/Students', $scope.student).success(function () {
$location.path("");
}).error(function (error) {
$scope.errors = {};
errorHelper.validateForms($scope.studentForm, $scope.errors,
error.modelState);
console.log(error.Message);
});
};
Мы добавили объект errors, в котором будем хранить список ошибок,
которые нам вернет наш сервер. Если я запущу пример и введу в поле LastName текст “dddd”, который не
подходит для регулярного выражения, которое проверяется на сервере, я получу
соответствующую ошибку:
Но к сожалению, на форме ничего не
изменится, и о том, что у меня случилась ошибка я не узнаю.
Итак, поле подсветилось, а
что за ошибка, я так и не узнал. Для того чтобы показать соответствующий текст, нам нужно добавить еще один span. Ниже приведен пример его реализации для поля LastName.
<span class="help-block" ng-show="studentForm.lastName.$error.server">{{errors.lastName}}</span>
Полный код приведен ниже.
<div class="form-group">
<label class="control-label
col-md-2" for="lastName">Last Name:</label>
<div class="col-md-10" ng-class="{'has-error': studentForm.lastName.$dirty && studentForm.lastName.$invalid}">
<input id="lastName" name="lastName" class="form-control" ng-model="student.LastName" required server-error ng-minlength="2" ng-maxlength="50" />
<span class="help-block" ng-show="studentForm.lastName.$dirty && studentForm.lastName.$error.required">
The LastName field is required.
</span>
<span class="help-block" ng-show="studentForm.lastName.$error.minlength">
The LastName field must be more
than 2 symbols
</span>
<span class="help-block" ng-show="studentForm.lastName.$error.maxlength">
The LastName field must be less
than 50 symbols
</span>
<span class="help-block" ng-show="studentForm.lastName.$error.server">{{errors.lastName}}</span>
</div>
</div>
Запускаем опять наш пример и проверяем, что
серверная валидация работает.
Итоги
Сегодня мы рассмотрели, как
реализовать валидацию с помощью AngularJS как на стороне сервера, так и на стороне
клиента. Ниже вы найдете исходники, в которых сами все сможете запустить и
посмотреть. Ничего сверхсложного в этом всем нет. Нужно только
знать, как работают AngularJS директивы и то, как нужно делать валидацию.
Надеюсь, что эта статья поможет вам сэкономить немного времени, если вам
предстанет задача реализовать свою валидацию в AngularJS.