В прошлой статье мы подробно рассмотрели процесс создания библиотеки для валидации данных, полученных из поля ввода. Библиотека хорошо работает и справляется со своей задачей. Тем не менее, если оценить библиотеку в более глобальном масштабе, то можно заметить, что она состоит из двух частей: валидации данных и коммуникации с DOM.
Для нетерпеливых: посмотреть весь приведённый ниже код и опробовать можно на codepen, также код доступен на gist.github.com, можно скачать zip архив с готовым примером использования.
Валидация данных не зависит от того, что будет происходит с DOM элементами, поэтому всё, что с ней связано можно выделить в отдельный модуль и использовать наследование для доступа к отдельным методам. Создадим для этих целей новый конструктор DataValidator
, который будет отвечать только за проверку переданных ему данных и на выходе предоставлять полный отчёт по проделанной работе в виде подобного объекта:
var results = {
data: 'email.gmail.com',
passed: [{ rule: 'max', param: 50 }, { rule: 'min', param: 5 }],
failed: [{ rule: 'match', param: 'email' }],
valid: false
}
На входе конструктор принимает только сами данные и с помощью метода init
, в который передаётся объект с правилами, проводит их валидацию:
var dataValidation = new DataValidator('email@gmail.com');
var results = dataValidation.init({
min: 5,
max: 50,
match: 'email'
});
Другими словами, всё, что требуется от DataValidator
— провести проверку данных и вернуть полный отчёт по результатам. Конструктор не будет манипулировать DOM элементами, писать сообщения или выводить ошибки. С подобными вещами будут работать его потомки.
Процесс создания валидатора данных уже должен быть вам знаком, поэтому я не буду детально описывать каждый аспект его создания. Новый конструктор обладает лишь одним новым методом init
, который отвечает за валидацию и создание объекта об ошибках:
DataValidator.prototype.init = function(rules) {
var results = { data: this.data, passed: [], failed: [] };
for (var rule in rules) {
var param = rules[rule];
var config = { rule: rule, param: param };
if (!this[rule](param)) { results.failed.push(config); }
else { results.passed.push(config); }
}
results.valid = results.failed.length === 0;
return results;
};
После получении объекта с правилами метод init
создаёт объект результатов валидации, который будет содержать четыре свойства:
data
— данные, полученные от конструктора, с которыми и будут проводиться все проверкиpassed
— массив всех правил, прошедших валидациюfailed
— массив всех правил, проваливших валидациюvalid
— свойство, показывающее были ли переданные данные полностью валидными
Объект результатов получается достаточно ёмким и решение кардинально отличается от предыдущего, где при нахождении ошибки итерации по объекту правил немедленно прекращалась. Новое подход менее оптимизированный, но, тем не менее, DataValidator
даёт полную оценку переданным ему данным и в результате пользователь получает может выбрать то, что ему нужно.
Полный код модуля с конструктором-родителем:
var DataValidator = (function() {
'use strict';
var regExps = {
email: /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i,
url: /^((https?):\/\/(\w+:{0,1}\w*@)?(\S+)|)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/,
numbers: /^\d+(\.\d{1,2})?$/,
digits: /[0-9]*$/,
letters: /[a-z][A-Z]*$/
};
var DataValidator = function(data) {
this.data = data.trim();
this.length = this.data.length;
};
DataValidator.prototype.min = function(param) { return this.length >= param; };
DataValidator.prototype.max = function(param) { return this.length <= param; };
DataValidator.prototype.required = function() { return this.length > 0; };
DataValidator.prototype.match = function(param) { return regExps[param].test(this.data); };
DataValidator.prototype.init = function(rules) {
var results = { data: this.data, passed: [], failed: [] };
for (var rule in rules) {
var param = rules[rule];
var config = { rule: rule, param: param };
if (!this[rule](param)) { results.failed.push(config); }
else { results.passed.push(config); }
}
results.valid = results.failed.length === 0;
return results;
};
return DataValidator;
})();
Теперь, когда у нас есть удобный способ работы с данными мы можем приступать к созданию "грязной" части — конструктора, который будет заниматься работой с DOM, писать сообщения об ошибках и вызывать callback функции. Конструктор InputValidator
должен иметь доступ к методу init
и здесь нам поможет наследование. Перед тем, как приступим к созданию конструктора, я ещё раз напомню, как он работает и что принимает на входе:
- Объект с правилами
rules
, в соответсвие с которыми и будет проводиться валидация - Объект
messages
с шаблонами сообщений, которые будут формироваться на основе ошибок валидации - Callback функции
onError
иonSuccess
, которые будут вызваны в зависимости от результатов проверки
var emailInput = new InputValidator(document.getElementById('email'), {
rules: {
min: 5,
max: 20,
match: 'email'
},
messages: {
min: 'Это поле должно содержать минимум %rule% символов. Значение %data% не подходит',
max: 'Это поле должно содержать максимум %rule% символов. Значение %data% не подходит',
match: 'Это поле должно содержать адрес электронной почты. Значение %data% не подходит'
},
onError: function() { console.log(this.message); },
onSuccess: function() { console.log('Валидация прошла успешно'); }
});
У нас уже есть функция _createMessage
, отвечающая за генерирование понятных сообщений об ошибках, поэтому сразу добавим её в новый модуль. Также мы знаем, что при инициализации конструктора нам необходимо записать свойство element
, которое будет ссылать на переданный DOM элемент, а также свойство settings
, содержащее объект настроек.
var InputValidator = (function() {
'use strict';
var _createMessage = function(message, settings) {
for (var key in settings) {
message = message.replace('%' + key + '%', settings[key]);
}
return message;
};
var InputValidator = function(element, settings) {
this.element = element;
this.settings = settings;
};
return InputValidator;
})();
Конструктор DataValidator
работает со свойством data
, и это значит, что и InputValidator
должен обладать данным свойством. Разумеется, мы можем просто записать в конструкторе this.data = element.value
, но такой подход не обеспечит нам полной безопасности: со временем конструктор DataValidator
может обновиться и оперировать уже совсем другим свойством. Чтобы избежать подобных проблем достаточно вызвать функцию-конструктор родителя без ключевого слова new
внутри конструктора потомка. Подобное подход позволит записать все необходимые свойства, выборочно с помощью метода call
или полностью с помощью apply
(подробнее об использовании конструкторов, как обычных функций):
var InputValidator = function(element, settings) {
DataValidator.call(this, element.value);
this.element = element;
this.settings = settings;
};
В данном случае мы не можем использовать метод apply
, так как хотим передать не список аргументов, а значение, получаемое в результате операции с исходным аргументом.
Итак, у нас есть свойство data
, а значит мы можем приступить к наследованию. Всё реализуется достаточно просто — необходимо всего лишь перезаписать прототип InputValidator
с помощью Object.create
:
InputValidator.prototype = Object.create(DataValidator.prototype);
InputValidator.prototype.construnctor = DataValidator;
Теперь у нас есть доступ ко всем свойствам DataValidator
внутри конструктора InputValidator
, в том числе и к необходимому нам свойству init
. Остаётся создать ещё один метод validate
, который будет проводить валидацию и заниматься "грязной" работой:
InputValidator.prototype.validate = function() {
DataValidator.call(this, this.element.value);
var results = this.init(this.rules);
if (!results.valid) {
var failed = results.failed[0];
this.message = _createMessage(this.settings.messages[failed.rule], {
data: results.data,
rule: failed.param
});
this.settings.onError.call(this);
} else {
this.settings.onSuccess.call(this);
}
};
При каждом запуске метода validate
мы хотим перезаписать свойство data
, чтобы всегда проверять свежие данные. После проведения валидации мы хотим записать первый неудачный результат в переменную failed
, на основе которой и будет строиться сообщение об ошибке.
На самом деле, записывать свойство data
при инициализации конструктора InputValidator
необязательно, так как оно в любом случае будет перезаписано при использовании метода validate
.
Весь код модуля InputValidator
:
var InputValidator = (function() {
'use strict';
var _createMessage = function(message, settings) {
for (var key in settings) {
message = message.replace('%' + key + '%', settings[key]);
}
return message;
};
var InputValidator = function(element, settings) {
this.element = element;
this.settings = settings;
};
InputValidator.prototype = Object.create(DataValidator.prototype);
InputValidator.prototype.construnctor = DataValidator;
InputValidator.prototype.validate = function() {
DataValidator.call(this, this.element.value);
var results = this.init(this.settings.rules);
if (!results.valid) {
var failed = results.failed[0];
this.message = _createMessage(this.settings.messages[failed.rule], {
data: results.data,
rule: failed.param
});
this.settings.onError.call(this);
} else {
this.settings.onSuccess.call(this);
}
};
return InputValidator;
})();
Прошлая версия библиотеки располагала достаточно скудным набором методов для валидации данных, так как предполагалось, что пользователь сам допишет всё, что ему будет необходимо. В новой версии добавлять свои способы для валидации данных настолько же просто, но новые методы необходимо добавлять родителю DataValidator
:
// Отдельный файл с кодом пользователя, подключенный после DataValidator и InputValidator
DataValidator.prototype.password = function() {
return this.data.toLowerCase() === '12345qwerty';
};
Посмотреть весь приведённый выше код и опробовать можно на codepen, также код доступен на gist.github.com, можно скачать zip архив с готовым примером использования.