Здравствуйте, Читатели.
Мал по-малу из моего опыта и наших проектов родилась небольшая библиотека для работы с моделями в джаваскрипте. Она так и называется — Model.js.
Я расскажу вам вкратце об этой библиотеке и этим постом запрашиваю обратную связь у тех, кто создавая сложные джаваскрипт-приложения, уже решает эту проблему каким-то определенным образом без фреймворков. Интересно также и мнение тех, кто только подыскивает подходящий инструмент для своих нужд: какой инструмент вам нужен и насколько вам подходит Model.js?
Зачем?
Чтобы упростить работу со слоем данных, не используя фреймворков. Текущая первая версия библиотеки — v0.1 весом около 12К — призвана помочь в первую очередь с валидацией данных и управлением событиями, в частности c событиями при изменении данных.
Что особенного?
Сахар. Обыкновенный приятный глазу синтаксический сахар.
Модель описыватся проще некуда.
var Note = new Model('Note', function () {
this.attr('id!', 'number');
this.attr('title', 'string', 'nonempty');
this.attr('text', 'string');
});
Затем создаем сущности, как обычные объекты.
var note = new Note({ id: 123, title: "Hello World" });
Публичные свойства сущности
Геттеры значений атрибутов. В нашем примере это:
note.data.id
note.data.title
note.data.text
Сеттеры. Кроме изменения значений также «выстреливают» событие change.
note.data.id =
note.data.title =
note.data.text =
note.data = {…}
Геттер note.get(attrName[, attrName, …])
возвратит объект со значениями запрашиваемых атрибутов.
note.get()
возвратит копию всех данных.
note.data()
— то же самое, что и note.get()
.
Сеттеры note.set(attrName, value)
и note.set({…})
не “выстреливают” событие change.
note.hasChanged
говорит, изменены ли данные сущности с момента их последнего сохранения.
note.isNew
говорит, сохранены ли данные сущности вообще хоть раз.
note.isPersisted
говорит, сохранены ли последние изменения.
note.bind(eventName, handler)
«вешает» обработчик. Кстати говоря, повесить обработчик любого события можно не только на отдельную сущность, но и на все сущности класса (Note.bind
).
note.isValid
говорит, валидны ли текущие данные модели.
note.errors
собственно, возвращает ошибки данных, если они есть.
note.revert()
откатывает несохраненные изменения и «выстреливает» событие revert
.
Это все.
Вы спросите, где же методы сохранения и прочее-прочее? — Отвечу: использование библиотеки подразумевает, что разработчик должен реализовать эти методы самостоятельно как того требует логика его приложения.
Нужно отметить, что с сохранением есть один нюанс: если данные сохранились успешно, нужно об этом сообщить сущности при помощи приватного метода note._persist()
, который также «выстреливает» событие persist
.
Объяснение примером хорошо. Допустим, наше приложение работает в браузерном окружении и метод note.save()
должен сохранить данные при помощи аджакса.
Note.prototype.save = function () {
var note = this;
return $.ajax({
type: 'PUT',
url: '/notes/'+note.data.id,
data: note.data(),
dataType: 'json'
}).done(function (json) {
note._persist();
});
}
note.bind('persist', function () {
$('h1', 'div#note'+this.data.id).html(this.data.title);
});
note.data = { id: 123, title: "abc", text: "" }
if (note.hasChanged && note.isValid) { // оппа Django-style
note.save();
}
Проверка данных
При создании модели обязательно описывается каждый ее атрибут.
this.attr('title', 'string', 'nonempty')
Сперва указываем название атрибута, затем его валидаторы. Валидаторы, когда придет время, в объявленном порядке будут проверять значение атрибута.
Вообще, валидаторы — это обычные функции, которые принимают значения и возвращают ошибки, когда эти значения невалидны.
function validateMinLength(value, minLength) {
if (value.length < minLength) return 'tooshort';
}
Если у вас есть необходимость использовать валидатор несколько раз — резонно его зарегистрировать, чтобы подключать по имени.
Model.registerValidator('array', function (value) {
if (typeof(value) !== 'array') return 'wrongtype';
});
Model.registerValidator('minLength', validateMinLength);
Model.js имеет несколько базовых (уже зарегистрированных) валидаторов: number
, string
, boolean
, nonnull
, nonempty
и in
. Их, совсем как в нашем примере, можно подключать к атрибутам по названию.
Чтобы передать параметр валидатору при описании атрибута, нужно записать его, как в примере ниже, в виде массива:
this.attr('title', 'string', 'nonempty', [ 'minLength', 6 ]);
Валидаторы можно подключать и обычным дедовским методом, не регистрируя их.
this.attr('title', 'string', 'nonempty', [ 'minLength', 6 ], function (title) {
if (title[0] !== title[0].toUpperCase()) return 'downcase';
});
Все это нужно, чтобы note.isValid
мог сказать true
или false
, а note.errors
мог вернуть объект с ошибками, если они есть.
note.data = { id: 'abc', title: '', text: 3 }; // полностью неверные данные
note.isValid // false
note.errors // { id: 'wrongtype', title: 'empty', text: 'wrongtype' }
События
Есть четыре события: initialize
, change
, persist
и revert
. Как говорилось выше, можно повесить обработчик на конкретную сущность (note.bind
), а можно и на класс (Note.bind
), так что обработчик станет общим для всех сущностей.
initialize
«выстреливается» единожды при создании сущности: var note = new Note({…})
. Так что вешать обработчик initialize
имеет смысл только на класс.
change
срабатывает каждый раз, когда меняется значение атрибута. Есть, правда, сеттеры, которые не дергают change (об этом было рассказано выше).
perist
— когда сущность получает сигнал о том, что изменения успешно сохранены.
revert
— когда логика приложения приказала откатить еще несохраненные изменения (при помощи метода note.revert()
).
var ALLOWED_LANGUAGES = ['en', 'ua', 'ru'];
var Note = new Model('Note', function () {
this.attr('id!', 'number');
this.attr('title', 'string', 'nonempty');
this.attr('lang', 'string', 'nonempty', [ 'in', ALLOWED_LANGUAGES]);
this.attr('text', 'string');
});
Note.bind('initialize', function () {
if (!this.data.lang) {
this.set('lang', 'en'); // change не выстрелит — специальный такой сеттер!
}
});
note.bind('change', function (changes) {
if (changes.title) $('h1', 'div#note'+this.data.id).html(changes.title);
});
В завершение
Сейчас Model.js — это чистый state of the art, это промежуточный, но уверенно работающий результат. Если вы заинтересовались библиотекой, если у вас возникло желание попробовать ее применить, я буду рад ответить на ваши вопросы. Больше информации о том, как что работает можно найти в документации на гитхабе и в тестах.
Ну а пока пожелайте малютке доброго пути, ведь чтобы ей стать «взрослой» библиотекой, нужно проделать еще немало работы.
Автор: mcmlxxxiii