Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

We’re showing branches in this repository, but you can also compare across forks.

base fork: gijs/jewel
base: master
...
head fork: hairyhum/jewel
compare: master
  • 12 commits
  • 17 files changed
  • 0 commit comments
  • 3 contributors
146 Readme.md
View
@@ -2,6 +2,13 @@
Operate with DOM elements like they're ActiveRecord models.
+# Features
+
+- DOM elements as models
+- CRUD methods you are already used to
+- Validation (built-in rules + ability to set custom ones)
+- No additional dependencies (except of jQuery, which, I guess you already have)
+
# Getting Started
**Dependencies**
@@ -16,7 +23,7 @@ Operate with DOM elements like they're ActiveRecord models.
- Opera
- IE - not tested
-**Include Jewel** (0.7kb minified and gzipped):
+**Include Jewel** (2.3kb minified and gzipped):
```html
<!-- jQuery must be included before that -->
@@ -37,6 +44,9 @@ Let's say we have such HTML:
<h1>Second title</h1>
<p>Second body</p>
</div>
+ <p class="not-found">
+ No posts here, sorry.
+ </p>
</div>
```
@@ -48,6 +58,11 @@ var Post = Jewel.define('#posts', { // selector for a wrapper element
title: 'h1', // selector for title, h1 in our case. Will be used as #posts .post h1
body: 'p', // selector for body. Will be used as #posts .post p
},
+ validation: {
+ title: ['required', 'min(10)'],
+ body: ['required']
+ },
+ emptyView: 'p.not-found', // selector for view, that shows up only when there is no posts
template: function(fields){ // function that returns HTML for new posts, you can use custom templating engine here.
return "<div class=\"post\"><h1>" + fields.title + "</h1><p>" + fields.body + "</p></div>"
}
@@ -89,9 +104,132 @@ post.body = 'Latest content';
post.save(); // will be prepended to #posts
```
+## Validation
+
+You don't need to call any additional methods to validate your data, Jewel will do it for you:
+
+```javascript
+var post = new Post;
+post.save(); // return false, because title and body fields are required
+```
+
+You can also define your own validation rules. Let's make one, which ensures that title starts with an *uppercase* or *downcase* letter:
+
+```javascript
+
+// Before declaring model
+
+Jewel.Validation.define({
+ rule: 'startsWith', // name of your rule
+ message: ':name should start with :param', // error message, :name is the name of the field, :param is the value of param(uppercase or downcase)
+ validator: function(value, param) { // validator itself
+ if (param === 'uppercase') {
+ return String.fromCharCode(value.charCodeAt(0) + 32) === value[0].toLowerCase();
+ }
+
+ if (param === 'downcase') {
+ return String.fromCharCode(value.charCodeAt(0) - 32) === value[0].toUpperCase();
+ }
+
+ return false;
+ }
+});
+
+// Now, let's apply this rule to some model
+
+var Post = Jewel.define('div.posts', {
+ keys: {
+ title: 'h1',
+ body: 'p'
+ },
+ validation: {
+ title: ['required', 'startsWith(uppercase)'] // uppercase is a param
+ },
+ template: function(fields){ // function that returns HTML for new posts, you can use custom templating engine here.
+ return "<div class=\"post\"><h1>" + fields.title + "</h1><p>" + fields.body + "</p></div>"
+ }
+});
+
+post = new Post;
+post.title = 'post title';
+post.save(); // false, because p is not a capital letter
+post.errorMessages[0]; // "Title should start with uppercase"
+post.errors[0]; // "title"
+
+post = new Post;
+post.title = 'Post title';
+post.save(); // true, DOM node inserted, all validations passed
+```
+
+## Events
+
+You can listen to events which are emitted by **all instances** of the model:
+
+```javascript
+Post.on('create', function(post){
+ post.title == 'Post title'; // true
+});
+
+var post = new Post;
+post.title = 'Post title';
+post.body = 'Post body';
+post.save();
+```
+
+Available events: create, update, remove and save (fires on create and update events too).
+
+## Hooks
+
+Jewel offers ability to define hooks to listen to events on per-model basis:
+
+```javascript
+var Post = Jewel.Model.define('div.posts', {
+ keys: {
+ title: 'h1',
+ body: 'p'
+ },
+ template: function(fields){
+ "<div class=\"post\"><h1>" + fields.title + "</h1><p>" + fields.body + "</p></div>"
+ },
+ hooks: {
+ beforeSave: function(){
+ this.title; // Value of model's "title" field
+ }
+ }
+});
+```
+
+Available hooks (for save, create, update and remove): before, around, after. For example, beforeSave, aroundRemove or afterUpdate.
+
+## CoffeeScript
+
+If you are fan of CoffeeScript, you will like the ability to define Jewel models just like you expect, using native CoffeeScript methods:
+
+```coffee-script
+class Post extends Jewel.Model
+ selector: 'div.posts'
+ keys:
+ title: 'h1'
+ body: 'p'
+ validation:
+ title: ['required', 'min(5)']
+ body: 'required'
+ emptyView: 'p.not-found'
+ template: (fields) ->
+ "<div class=\"post\"><h1>#{ fields.title }</h1><p>#{ fields.body }</p></div>"
+
+ beforeSave: -> # notice, that you specify hooks like class methods, not in hooks object
+ @title
+
+Post = Jewel.Model.setup Post # you should let Jewel prepare the model for its correct functioning
+```
+
+After this, you can use all functionality you've seen before.
+
+
# Tests
-Run tests by opening **test/test.hmtl** in browser.
+Run tests by opening **test/test.html** in browser.
# Contributing
@@ -101,6 +239,10 @@ Run tests by opening **test/test.hmtl** in browser.
- Commit & push
- Send pull request
+# Roadmap
+
+- Sync with server
+
# License
(The MIT License)
443 dist/jewel.js
View
@@ -1,9 +1,9 @@
-/*! Jewel - v0.1.0 - 2012-05-06
+/*! Jewel - v0.1.6 - 2012-05-26
* http://github.com/vdemedes/jewel/
* Copyright (c) 2012 Vadim Demedes; Licensed MIT */
-// Generated by CoffeeScript 1.3.1
-var Jewel, Model;
+// Generated by CoffeeScript 1.3.3
+var Jewel;
Function.prototype.clone = function() {
var clone, property;
@@ -17,35 +17,271 @@ Function.prototype.clone = function() {
return clone;
};
-Jewel = (function() {
+if (!String.prototype.trim) {
+ String.prototype.trim = function() {
+ return this.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
+ };
+}
- Jewel.name = 'Jewel';
+Jewel = (function() {
function Jewel() {}
- Jewel.define = function(selector, options) {
- var model;
+ return Jewel;
+
+})();
+
+// Generated by CoffeeScript 1.3.3
+
+Jewel.Validation = (function() {
+
+ function Validation() {}
+
+ Validation.rules = {};
+
+ Validation.define = function(options) {
+ return this.rules[options.rule] = options;
+ };
+
+ Validation.validate = function(name, value, rules) {
+ var errors, humanizedName, param, rule, ruleName, valid, _i, _len, _ref;
+ valid = true;
+ errors = [];
+ for (_i = 0, _len = rules.length; _i < _len; _i++) {
+ rule = rules[_i];
+ param = void 0;
+ ruleName = rule;
+ if (/\(/.test(rule)) {
+ _ref = /([A-Za-z0-9_]+)\((.+)\)/.exec(rule), rule = _ref[0], ruleName = _ref[1], param = _ref[2];
+ }
+ if (!this.rules[ruleName].validator(value, param)) {
+ valid = false;
+ humanizedName = name.replace(/[^A-Za-z0-9]/, '').replace(/^.|\s\S/g, function(v) {
+ return v.toUpperCase();
+ });
+ errors.push(this.rules[ruleName].message.replace(':name', humanizedName).replace(':param', param));
+ break;
+ }
+ }
+ return [valid, errors];
+ };
+
+ return Validation;
+
+})();
+
+Jewel.Validation.define({
+ rule: 'required',
+ message: ':name is required',
+ validator: function(value) {
+ return !!value;
+ }
+});
+
+Jewel.Validation.define({
+ rule: 'email',
+ message: ':name is not valid email',
+ validator: function(value) {
+ return /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(value);
+ }
+});
+
+Jewel.Validation.define({
+ rule: 'min',
+ message: ':name should be bigger than :param',
+ validator: function(value, param) {
+ param = parseInt(param);
+ return (typeof value === 'number' ? value : value.length) >= param;
+ }
+});
+
+Jewel.Validation.define({
+ rule: 'max',
+ message: ':name should be smaller than :param',
+ validator: function(value, param) {
+ param = parseInt(param);
+ return (typeof value === 'number' ? value : value.length) <= param;
+ }
+});
+
+Jewel.Validation.define({
+ rule: 'onlyLetters',
+ message: ':name should contain only letters',
+ validator: function(value) {
+ return !/[^A-Za-z ]/.test(value);
+ }
+});
+
+Jewel.Validation.define({
+ rule: 'onlyNumbers',
+ message: ':name should contain only numbers',
+ validator: function(value) {
+ return !/[^0-9]/.test(value);
+ }
+});
+
+Jewel.Validation.define({
+ rule: 'onlyLettersAndNumbers',
+ message: ':name should contain only letters and numbers',
+ validator: function(value) {
+ return !/[^A-Za-z0-9 ]/.test(value);
+ }
+});
+
+// Generated by CoffeeScript 1.3.3
+
+Jewel.Events = (function() {
+
+ function Events() {}
+
+ Events.listeners = {};
+
+ Events.on = function(model, event, listener) {
+ if (!this.listeners[model]) {
+ this.listeners[model] = {};
+ }
+ if (!this.listeners[model][event]) {
+ this.listeners[model][event] = [];
+ }
+ return this.listeners[model][event].push(listener);
+ };
+
+ Events.emit = function(model, event, data) {
+ var listener, _i, _len, _ref, _results;
+ if (!this.listeners[model] || !this.listeners[model][event]) {
+ return;
+ }
+ _ref = this.listeners[model][event];
+ _results = [];
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ listener = _ref[_i];
+ _results.push(listener(data));
+ }
+ return _results;
+ };
+
+ return Events;
+
+})();
+
+// Generated by CoffeeScript 1.3.3
+
+Jewel.Cache = (function() {
+
+ function Cache() {}
+
+ Cache.cache = {};
+
+ Cache.get = function(selector) {
+ if (!this.cache[selector]) {
+ this.cache[selector] = $(selector);
+ }
+ return this.cache[selector];
+ };
+
+ Cache.set = function(selector, value) {
+ this.cache[selector] = value;
+ return value;
+ };
+
+ Cache.invalidate = function(selector, value) {
+ if (this.cache[selector]) {
+ delete this.cache[selector];
+ }
+ if (value) {
+ return this.cache[selector] = value;
+ }
+ };
+
+ return Cache;
+
+})();
+
+// Generated by CoffeeScript 1.3.3
+
+Jewel.Model = (function() {
+
+ Model.prototype.errors = [];
+
+ Model.prototype.errorMessages = [];
+
+ Model.define = function(selector, options) {
+ var hook, model;
if (options == null) {
options = {};
}
- model = Model.clone();
- model.prototype.selector = model.selector = selector;
- model.prototype.options = model.options = options;
+ model = {
+ selector: selector,
+ keys: options.keys,
+ template: options.template,
+ validation: options.validation
+ };
+ if (options.hooks) {
+ for (hook in options.hooks) {
+ model[hook] = options.hooks[hook];
+ }
+ }
+ return this.bake(model);
+ };
+
+ Model.setup = function(model) {
+ return this.bake(model.prototype);
+ };
+
+ Model.bake = function(options) {
+ var children, container, hook, model;
+ model = Jewel.Model.clone();
+ model.prototype.selector = model.selector = options.selector;
+ delete options.selector;
+ model.prototype.options = model.options = {
+ keys: options.keys,
+ template: options.template
+ };
+ delete options.keys;
+ delete options.template;
+ model.prototype.validation = model.validation = options.validation;
+ delete options.validation;
+ for (hook in options) {
+ model.prototype[hook] = options[hook];
+ }
model.__defineGetter__('all', function() {
return model.find();
});
+ model.__defineGetter__('first', function() {
+ return model.find({
+ limit: 1
+ })[0];
+ });
+ model.__defineGetter__('last', function() {
+ return model.find({
+ limit: 1,
+ order: 'desc'
+ })[0];
+ });
+ model.prototype.view = model.view = {
+ limit: 0,
+ insert: 'prepend'
+ };
+ container = Jewel.Cache.get(model.prototype.selector);
+ $.each(container.attr('data-view').split(','), function(index, value) {
+ var key, _ref;
+ _ref = value.split(':'), key = _ref[0], value = _ref[1];
+ key = key.trim();
+ return model.prototype.view[key] = model.view[key] = value.trim();
+ });
+ model.prototype.emptyViewSelector = model.emptyViewSelector = options.emptyView || '.not-found';
+ model.prototype.emptyView = model.emptyView = container.find(model.prototype.emptyViewSelector);
+ children = Jewel.Cache.set("" + model.prototype.selector + ":children", container.children(":not(" + model.prototype.emptyViewSelector + ")"));
+ if (children.length > 0) {
+ model.prototype.emptyView.hide();
+ } else {
+ model.prototype.emptyView.show();
+ }
+ model.prototype.childrenSelector = model.childrenSelector = "" + model.prototype.selector + ":children";
model.prototype.model = model.model = model;
return model;
};
- return Jewel;
-
-})();
-
-Model = (function() {
-
- Model.name = 'Model';
-
function Model() {}
Model.prototype.init = function(element) {
@@ -69,40 +305,71 @@ Model = (function() {
}
};
+ Model.prototype.updateAttributes = function(fields) {
+ var key, validFields, _results;
+ validFields = this.fields();
+ _results = [];
+ for (key in fields) {
+ if (validFields[key]) {
+ _results.push(this[key] = fields[key]);
+ } else {
+ _results.push(void 0);
+ }
+ }
+ return _results;
+ };
+
Model.find = function(options) {
- var i, items, model, results;
+ var i, item, items, loopTimes, model, modifier, results, _i, _len;
if (options == null) {
options = {};
}
- items = $(this.selector).children();
- results = void 0;
+ items = Jewel.Cache.get(this.childrenSelector);
+ results = [];
if (items.length > 0) {
- if (options.skip || options.limit) {
- i = 0;
- results = [];
+ if (options.skip || options.limit || options.order || options.offset) {
+ if (options.order && options.order.toLowerCase() === 'desc') {
+ i = items.length - 1;
+ modifier = -1;
+ } else {
+ i = 0;
+ modifier = 1;
+ }
+ loopTimes = 0;
while (true) {
+ if (loopTimes >= options.limit) {
+ break;
+ }
if (!items[i]) {
break;
}
- if (i >= (options.skip || -1) && i < (options.limit || items.length)) {
+ if (loopTimes >= (options.skip || options.offset || -1)) {
model = new this.model;
model.init(items[i]);
results.push(model);
}
- i++;
+ i += modifier;
+ loopTimes++;
+ }
+ } else {
+ for (_i = 0, _len = items.length; _i < _len; _i++) {
+ item = items[_i];
+ model = new this.model;
+ model.init(item);
+ results.push(model);
}
}
}
- if (!results) {
- results = items;
- }
return results;
};
Model.prototype.fields = function() {
var fields, key, notFields;
- notFields = ['init', 'save', 'remove', 'selector', 'options', 'model'];
+ notFields = ['init', 'save', 'remove', 'selector', 'options', 'model', 'create', 'update', 'fields', 'updateAttributes', 'childrenSelector', 'emptyView', 'emptyViewSelector', 'errors', 'errorMessages', 'validate', 'validation', 'view'];
fields = {};
+ for (key in this.options.keys) {
+ fields[key] = void 0;
+ }
for (key in this) {
if (-1 === notFields.indexOf(key)) {
fields[key] = this[key];
@@ -111,35 +378,133 @@ Model = (function() {
return fields;
};
+ Model.prototype.validate = function() {
+ var error, errors, fields, key, keys, result, rules, valid, _i, _len, _ref;
+ this.errors = [];
+ this.errorMessages = [];
+ fields = this.fields();
+ keys = this.validation;
+ if (!keys) {
+ return true;
+ }
+ valid = true;
+ for (key in keys) {
+ rules = keys[key] instanceof Array ? keys[key] : [keys[key]];
+ _ref = Jewel.Validation.validate(key, fields[key], rules), result = _ref[0], errors = _ref[1];
+ if (errors.length > 0) {
+ this.errors.push(key);
+ for (_i = 0, _len = errors.length; _i < _len; _i++) {
+ error = errors[_i];
+ this.errorMessages.push(error);
+ }
+ valid = false;
+ }
+ }
+ return valid;
+ };
+
Model.prototype.save = function() {
+ if (!this.validate()) {
+ return false;
+ }
+ if (this.beforeSave) {
+ this.beforeSave.call(this);
+ }
+ if (this.aroundSave) {
+ this.aroundSave.call(this);
+ }
if (!this.element) {
- return this.create();
+ this.create();
} else {
- return this.update();
+ this.update();
+ }
+ Jewel.Cache.invalidate(this.childrenSelector, Jewel.Cache.get(this.selector).children(":not(" + this.emptyViewSelector + ")"));
+ if (this.aroundSave) {
+ this.aroundSave.call(this);
}
+ if (this.afterSave) {
+ this.afterSave.call(this);
+ }
+ Jewel.Events.emit(this.selector, 'save', this);
+ return true;
};
Model.prototype.create = function() {
+ var all, limit;
+ if (this.beforeCreate) {
+ this.beforeCreate.call(this);
+ }
this.element = $(this.options.template(this.fields()));
- return this.element.prependTo(this.selector);
+ this.element["" + this.view.insert + "To"](this.selector);
+ limit = this.view.limit || this.view.max;
+ limit = parseInt(limit);
+ all = Jewel.Cache.get(this.childrenSelector);
+ if (all.length > 0) {
+ if (limit > 0 && all.length >= limit) {
+ all.last().remove();
+ all = void 0;
+ }
+ } else {
+ this.emptyView.hide();
+ }
+ if (this.afterCreate) {
+ this.afterCreate.call(this);
+ }
+ return Jewel.Events.emit(this.selector, 'create', this);
};
Model.prototype.update = function() {
- var fields, key, _results;
+ var fields, key;
+ if (this.beforeUpdate) {
+ this.beforeUpdate.call(this);
+ }
+ if (this.aroundUpdate) {
+ this.aroundUpdate.call(this);
+ }
fields = this.fields();
- _results = [];
for (key in fields) {
- _results.push($(this.element).find(this.options.keys[key]).html(fields[key]));
+ $(this.element).find(this.options.keys[key]).html(fields[key]);
}
- return _results;
+ if (this.aroundUpdate) {
+ this.aroundUpdate.call(this);
+ }
+ if (this.afterUpdate) {
+ this.afterUpdate.call(this);
+ }
+ return Jewel.Events.emit(this.selector, 'update', this);
};
Model.prototype.remove = function() {
- return $(this.element).remove();
+ if (this.beforeRemove) {
+ this.beforeRemove.call(this);
+ }
+ if (this.aroundRemove) {
+ this.aroundRemove.call(this);
+ }
+ $(this.element).remove();
+ if (Jewel.Cache.get(this.childrenSelector).length === 1) {
+ this.emptyView.show();
+ }
+ Jewel.Cache.invalidate(this.childrenSelector, Jewel.Cache.get(this.selector).children(":not(" + this.emptyViewSelector + ")"));
+ if (this.aroundRemove) {
+ this.aroundRemove.call(this);
+ }
+ if (this.afterRemove) {
+ this.afterRemove.call(this);
+ }
+ return Jewel.Events.emit(this.selector, 'remove', this);
};
Model.remove = function() {
- return $(this.selector).remove();
+ if (Jewel.Cache.get(this.childrenSelector).length === 1) {
+ this.emptyView.show();
+ }
+ Jewel.Cache.get(this.childrenSelector).remove();
+ return Jewel.Cache.invalidate(this.childrenSelector, Jewel.Cache.get(this.selector).children(":not(" + this.emptyViewSelector + ")"));
+ };
+
+ Model.on = function(event, listener) {
+ return Jewel.Events.on(this.selector, event, listener);
};
return Model;
4 dist/jewel.min.js
View
@@ -1,4 +1,4 @@
-/*! Jewel - v0.1.0 - 2012-05-06
+/*! Jewel - v0.1.6 - 2012-05-26
* http://github.com/vdemedes/jewel/
* Copyright (c) 2012 Vadim Demedes; Licensed MIT */
-var Jewel,Model;Function.prototype.clone=function(){var a,b;a=function(){};for(b in this)this.hasOwnProperty(b)&&(a[b]=this[b]);return a.prototype=this.prototype,a},Jewel=function(){function a(){}return a.name="Jewel",a.define=function(a,b){var c;return b==null&&(b={}),c=Model.clone(),c.prototype.selector=c.selector=a,c.prototype.options=c.options=b,c.__defineGetter__("all",function(){return c.find()}),c.prototype.model=c.model=c,c},a}(),Model=function(){function a(){}return a.name="Model",a.prototype.init=function(a){var b,c,d,e;if(!(a instanceof HTMLElement)){e=[];for(c in a)e.push(this[c]=a[c]);return e}this.element=a;if(this.options.keys){d=[];for(c in this.options.keys)b=$(a).find(this.options.keys[c]),d.push(this[c]=b.html());return d}},a.find=function(a){var b,c,d,e;a==null&&(a={}),c=$(this.selector).children(),e=void 0;if(c.length>0)if(a.skip||a.limit){b=0,e=[];for(;;){if(!c[b])break;b>=(a.skip||-1)&&b<(a.limit||c.length)&&(d=new this.model,d.init(c[b]),e.push(d)),b++}}return e||(e=c),e},a.prototype.fields=function(){var a,b,c;c=["init","save","remove","selector","options","model"],a={};for(b in this)-1===c.indexOf(b)&&(a[b]=this[b]);return a},a.prototype.save=function(){return this.element?this.update():this.create()},a.prototype.create=function(){return this.element=$(this.options.template(this.fields())),this.element.prependTo(this.selector)},a.prototype.update=function(){var a,b,c;a=this.fields(),c=[];for(b in a)c.push($(this.element).find(this.options.keys[b]).html(a[b]));return c},a.prototype.remove=function(){return $(this.element).remove()},a.remove=function(){return $(this.selector).remove()},a}();
+var Jewel;Function.prototype.clone=function(){var a,b;a=function(){};for(b in this)this.hasOwnProperty(b)&&(a[b]=this[b]);return a.prototype=this.prototype,a},String.prototype.trim||(String.prototype.trim=function(){return this.replace(/^\s\s*/,"").replace(/\s\s*$/,"")}),Jewel=function(){function a(){}return a}(),Jewel.Validation=function(){function a(){}return a.rules={},a.define=function(a){return this.rules[a.rule]=a},a.validate=function(a,b,c){var d,e,f,g,h,i,j,k,l;i=!0,d=[];for(j=0,k=c.length;j<k;j++){g=c[j],f=void 0,h=g,/\(/.test(g)&&(l=/([A-Za-z0-9_]+)\((.+)\)/.exec(g),g=l[0],h=l[1],f=l[2]);if(!this.rules[h].validator(b,f)){i=!1,e=a.replace(/[^A-Za-z0-9]/,"").replace(/^.|\s\S/g,function(a){return a.toUpperCase()}),d.push(this.rules[h].message.replace(":name",e).replace(":param",f));break}}return[i,d]},a}(),Jewel.Validation.define({rule:"required",message:":name is required",validator:function(a){return!!a}}),Jewel.Validation.define({rule:"email",message:":name is not valid email",validator:function(a){return/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(a)}}),Jewel.Validation.define({rule:"min",message:":name should be bigger than :param",validator:function(a,b){return b=parseInt(b),(typeof a=="number"?a:a.length)>=b}}),Jewel.Validation.define({rule:"max",message:":name should be smaller than :param",validator:function(a,b){return b=parseInt(b),(typeof a=="number"?a:a.length)<=b}}),Jewel.Validation.define({rule:"onlyLetters",message:":name should contain only letters",validator:function(a){return!/[^A-Za-z ]/.test(a)}}),Jewel.Validation.define({rule:"onlyNumbers",message:":name should contain only numbers",validator:function(a){return!/[^0-9]/.test(a)}}),Jewel.Validation.define({rule:"onlyLettersAndNumbers",message:":name should contain only letters and numbers",validator:function(a){return!/[^A-Za-z0-9 ]/.test(a)}}),Jewel.Events=function(){function a(){}return a.listeners={},a.on=function(a,b,c){return this.listeners[a]||(this.listeners[a]={}),this.listeners[a][b]||(this.listeners[a][b]=[]),this.listeners[a][b].push(c)},a.emit=function(a,b,c){var d,e,f,g,h;if(!this.listeners[a]||!this.listeners[a][b])return;g=this.listeners[a][b],h=[];for(e=0,f=g.length;e<f;e++)d=g[e],h.push(d(c));return h},a}(),Jewel.Cache=function(){function a(){}return a.cache={},a.get=function(a){return this.cache[a]||(this.cache[a]=$(a)),this.cache[a]},a.set=function(a,b){return this.cache[a]=b,b},a.invalidate=function(a,b){this.cache[a]&&delete this.cache[a];if(b)return this.cache[a]=b},a}(),Jewel.Model=function(){function a(){}return a.prototype.errors=[],a.prototype.errorMessages=[],a.define=function(a,b){var c,d;b==null&&(b={}),d={selector:a,keys:b.keys,template:b.template,validation:b.validation};if(b.hooks)for(c in b.hooks)d[c]=b.hooks[c];return this.bake(d)},a.setup=function(a){return this.bake(a.prototype)},a.bake=function(a){var b,c,d,e;e=Jewel.Model.clone(),e.prototype.selector=e.selector=a.selector,delete a.selector,e.prototype.options=e.options={keys:a.keys,template:a.template},delete a.keys,delete a.template,e.prototype.validation=e.validation=a.validation,delete a.validation;for(d in a)e.prototype[d]=a[d];return e.__defineGetter__("all",function(){return e.find()}),e.__defineGetter__("first",function(){return e.find({limit:1})[0]}),e.__defineGetter__("last",function(){return e.find({limit:1,order:"desc"})[0]}),e.prototype.view=e.view={limit:0,insert:"prepend"},c=Jewel.Cache.get(e.prototype.selector),$.each(c.attr("data-view").split(","),function(a,b){var c,d;return d=b.split(":"),c=d[0],b=d[1],c=c.trim(),e.prototype.view[c]=e.view[c]=b.trim()}),e.prototype.emptyViewSelector=e.emptyViewSelector=a.emptyView||".not-found",e.prototype.emptyView=e.emptyView=c.find(e.prototype.emptyViewSelector),b=Jewel.Cache.set(""+e.prototype.selector+":children",c.children(":not("+e.prototype.emptyViewSelector+")")),b.length>0?e.prototype.emptyView.hide():e.prototype.emptyView.show(),e.prototype.childrenSelector=e.childrenSelector=""+e.prototype.selector+":children",e.prototype.model=e.model=e,e},a.prototype.init=function(a){var b,c,d,e;if(!(a instanceof HTMLElement)){e=[];for(c in a)e.push(this[c]=a[c]);return e}this.element=a;if(this.options.keys){d=[];for(c in this.options.keys)b=$(a).find(this.options.keys[c]),d.push(this[c]=b.html());return d}},a.prototype.updateAttributes=function(a){var b,c,d;c=this.fields(),d=[];for(b in a)c[b]?d.push(this[b]=a[b]):d.push(void 0);return d},a.find=function(a){var b,c,d,e,f,g,h,i,j;a==null&&(a={}),d=Jewel.Cache.get(this.childrenSelector),h=[];if(d.length>0)if(a.skip||a.limit||a.order||a.offset){a.order&&a.order.toLowerCase()==="desc"?(b=d.length-1,g=-1):(b=0,g=1),e=0;for(;;){if(e>=a.limit)break;if(!d[b])break;e>=(a.skip||a.offset||-1)&&(f=new this.model,f.init(d[b]),h.push(f)),b+=g,e++}}else for(i=0,j=d.length;i<j;i++)c=d[i],f=new this.model,f.init(c),h.push(f);return h},a.prototype.fields=function(){var a,b,c;c=["init","save","remove","selector","options","model","create","update","fields","updateAttributes","childrenSelector","emptyView","emptyViewSelector","errors","errorMessages","validate","validation","view"],a={};for(b in this.options.keys)a[b]=void 0;for(b in this)-1===c.indexOf(b)&&(a[b]=this[b]);return a},a.prototype.validate=function(){var a,b,c,d,e,f,g,h,i,j,k;this.errors=[],this.errorMessages=[],c=this.fields(),e=this.validation;if(!e)return!0;h=!0;for(d in e){g=e[d]instanceof Array?e[d]:[e[d]],k=Jewel.Validation.validate(d,c[d],g),f=k[0],b=k[1];if(b.length>0){this.errors.push(d);for(i=0,j=b.length;i<j;i++)a=b[i],this.errorMessages.push(a);h=!1}}return h},a.prototype.save=function(){return this.validate()?(this.beforeSave&&this.beforeSave.call(this),this.aroundSave&&this.aroundSave.call(this),this.element?this.update():this.create(),Jewel.Cache.invalidate(this.childrenSelector,Jewel.Cache.get(this.selector).children(":not("+this.emptyViewSelector+")")),this.aroundSave&&this.aroundSave.call(this),this.afterSave&&this.afterSave.call(this),Jewel.Events.emit(this.selector,"save",this),!0):!1},a.prototype.create=function(){var a,b;return this.beforeCreate&&this.beforeCreate.call(this),this.element=$(this.options.template(this.fields())),this.element[""+this.view.insert+"To"](this.selector),b=this.view.limit||this.view.max,b=parseInt(b),a=Jewel.Cache.get(this.childrenSelector),a.length>0?b>0&&a.length>=b&&(a.last().remove(),a=void 0):this.emptyView.hide(),this.afterCreate&&this.afterCreate.call(this),Jewel.Events.emit(this.selector,"create",this)},a.prototype.update=function(){var a,b;this.beforeUpdate&&this.beforeUpdate.call(this),this.aroundUpdate&&this.aroundUpdate.call(this),a=this.fields();for(b in a)$(this.element).find(this.options.keys[b]).html(a[b]);return this.aroundUpdate&&this.aroundUpdate.call(this),this.afterUpdate&&this.afterUpdate.call(this),Jewel.Events.emit(this.selector,"update",this)},a.prototype.remove=function(){return this.beforeRemove&&this.beforeRemove.call(this),this.aroundRemove&&this.aroundRemove.call(this),$(this.element).remove(),Jewel.Cache.get(this.childrenSelector).length===1&&this.emptyView.show(),Jewel.Cache.invalidate(this.childrenSelector,Jewel.Cache.get(this.selector).children(":not("+this.emptyViewSelector+")")),this.aroundRemove&&this.aroundRemove.call(this),this.afterRemove&&this.afterRemove.call(this),Jewel.Events.emit(this.selector,"remove",this)},a.remove=function(){return Jewel.Cache.get(this.childrenSelector).length===1&&this.emptyView.show(),Jewel.Cache.get(this.childrenSelector).remove(),Jewel.Cache.invalidate(this.childrenSelector,Jewel.Cache.get(this.selector).children(":not("+this.emptyViewSelector+")"))},a.on=function(a,b){return Jewel.Events.on(this.selector,a,b)},a}();
6 grunt.js
View
@@ -4,7 +4,7 @@ module.exports = function(grunt) {
// Project configuration.
grunt.initConfig({
meta: {
- version: '0.1.0',
+ version: '0.1.7',
banner: '/*! Jewel - v<%= meta.version %> - ' +
'<%= grunt.template.today("yyyy-mm-dd") %>\n' +
'* http://github.com/vdemedes/jewel/\n' +
@@ -13,7 +13,7 @@ module.exports = function(grunt) {
},
concat: {
dist: {
- src: ['<banner:meta.banner>', '<file_strip_banner:lib/jewel.js>'],
+ src: ['<banner:meta.banner>', '<file_strip_banner:lib/jewel.js>', '<file_strip_banner:lib/components/validation.js>', '<file_strip_banner:lib/components/events.js>', '<file_strip_banner:lib/components/cache.js>', '<file_strip_banner:lib/components/model.js>'],
dest: 'dist/jewel.js'
}
},
@@ -25,7 +25,7 @@ module.exports = function(grunt) {
},
watch: {
scripts: {
- files: ['lib/jewel.js'],
+ files: ['lib/jewel.js', 'lib/components/validation.js', 'lib/components/events.js', 'lib/components/cache.js', 'lib/components/model.js'],
tasks: 'concat min'
}
},
15 lib/components/cache.coffee
View
@@ -0,0 +1,15 @@
+class Jewel.Cache
+ @cache: {}
+
+ @get: (selector) ->
+ @cache[selector] = $(selector) if not @cache[selector]
+
+ @cache[selector]
+
+ @set: (selector, value) ->
+ @cache[selector] = value
+ value
+
+ @invalidate: (selector, value) ->
+ delete @cache[selector] if @cache[selector]
+ @cache[selector] = value if value
32 lib/components/cache.js
View
@@ -0,0 +1,32 @@
+// Generated by CoffeeScript 1.3.3
+
+Jewel.Cache = (function() {
+
+ function Cache() {}
+
+ Cache.cache = {};
+
+ Cache.get = function(selector) {
+ if (!this.cache[selector]) {
+ this.cache[selector] = $(selector);
+ }
+ return this.cache[selector];
+ };
+
+ Cache.set = function(selector, value) {
+ this.cache[selector] = value;
+ return value;
+ };
+
+ Cache.invalidate = function(selector, value) {
+ if (this.cache[selector]) {
+ delete this.cache[selector];
+ }
+ if (value) {
+ return this.cache[selector] = value;
+ }
+ };
+
+ return Cache;
+
+})();
13 lib/components/events.coffee
View
@@ -0,0 +1,13 @@
+class Jewel.Events
+ @listeners: {}
+
+ @on: (model, event, listener) ->
+ @listeners[model] = {} if not @listeners[model]
+ @listeners[model][event] = [] if not @listeners[model][event]
+ @listeners[model][event].push listener
+
+ @emit: (model, event, data) ->
+ return if not @listeners[model] or not @listeners[model][event]
+
+ for listener in @listeners[model][event]
+ listener data
35 lib/components/events.js
View
@@ -0,0 +1,35 @@
+// Generated by CoffeeScript 1.3.3
+
+Jewel.Events = (function() {
+
+ function Events() {}
+
+ Events.listeners = {};
+
+ Events.on = function(model, event, listener) {
+ if (!this.listeners[model]) {
+ this.listeners[model] = {};
+ }
+ if (!this.listeners[model][event]) {
+ this.listeners[model][event] = [];
+ }
+ return this.listeners[model][event].push(listener);
+ };
+
+ Events.emit = function(model, event, data) {
+ var listener, _i, _len, _ref, _results;
+ if (!this.listeners[model] || !this.listeners[model][event]) {
+ return;
+ }
+ _ref = this.listeners[model][event];
+ _results = [];
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ listener = _ref[_i];
+ _results.push(listener(data));
+ }
+ return _results;
+ };
+
+ return Events;
+
+})();
227 lib/components/model.coffee
View
@@ -0,0 +1,227 @@
+class Jewel.Model
+ errors: []
+ errorMessages: []
+
+ @define: (selector, options = {}) ->
+ model=
+ selector: selector
+ keys: options.keys
+ template: options.template
+ validation: options.validation
+
+ if options.hooks
+ for hook of options.hooks
+ model[hook] = options.hooks[hook]
+
+ @bake model
+
+ @setup: (model) ->
+ @bake model::
+
+ @bake: (options) ->
+ model = do Jewel.Model.clone
+ model::selector = model.selector = options.selector
+ delete options.selector
+ model::options = model.options=
+ keys: options.keys
+ template: options.template
+ delete options.keys
+ delete options.template
+ model::validation = model.validation = options.validation
+ delete options.validation
+
+ for hook of options
+ model::[hook] = options[hook]
+
+ model.__defineGetter__ 'all', ->
+ do model.find
+
+ model.__defineGetter__ 'first', ->
+ model.find(limit: 1)[0]
+
+ model.__defineGetter__ 'last', ->
+ model.find(limit: 1, order: 'desc')[0]
+
+ model::view = model.view=
+ limit: 0
+ insert: 'prepend'
+
+ container = Jewel.Cache.get(model::selector)
+
+ $.each container.attr('data-view').split(','), (index, value) ->
+ [key, value] = value.split ':'
+ key = key.trim()
+ model::view[key] = model.view[key] = value.trim()
+
+ model::emptyViewSelector = model.emptyViewSelector = options.emptyView or '.not-found'
+ model::emptyView = model.emptyView = container.find(model::emptyViewSelector)
+
+ children = Jewel.Cache.set("#{ model::selector }:children", container.children(":not(#{ model::emptyViewSelector })"))
+ if children.length > 0 then model::emptyView.hide() else model::emptyView.show()
+
+ model::childrenSelector = model.childrenSelector = "#{ model::selector }:children"
+
+ model::model = model.model = model
+
+ model
+
+ constructor: ->
+
+ init: (element) ->
+ if element instanceof HTMLElement
+ @element = element
+ if @options.keys
+ for key of @options.keys
+ e = $(element).find @options.keys[key]
+ @[key] = e.html()
+ else
+ for key of element
+ @[key] = element[key]
+
+ updateAttributes: (fields) ->
+ validFields = @fields()
+ for key of fields
+ @[key] = fields[key] if validFields[key]
+
+ @find: (options = {}) ->
+ items = Jewel.Cache.get(@childrenSelector)
+ results = []
+ if items.length > 0
+ if options.skip or options.limit or options.order or options.offset
+ if options.order and options.order.toLowerCase() is 'desc'
+ i = items.length - 1
+ modifier = -1
+ else
+ i = 0
+ modifier = 1
+
+ loopTimes = 0
+ loop
+ break if loopTimes >= options.limit
+ break if not items[i]
+
+ if loopTimes >= (options.skip or options.offset or -1)
+ model = new @model
+ model.init items[i]
+ results.push model
+
+ i += modifier
+ loopTimes++
+ else
+ for item in items
+ model = new @model
+ model.init item
+ results.push model
+
+ results
+
+ fields: ->
+ notFields = [
+ 'init', 'save', 'remove', 'selector',
+ 'options', 'model', 'create', 'update',
+ 'fields', 'updateAttributes', 'childrenSelector',
+ 'emptyView', 'emptyViewSelector', 'errors',
+ 'errorMessages', 'validate', 'validation', 'view'
+ ]
+ fields = {}
+ for key of @options.keys
+ fields[key] = undefined # just setting key
+
+ for key of @
+ fields[key] = @[key] if -1 is notFields.indexOf key
+
+ fields
+
+ validate: ->
+ @errors = []
+ @errorMessages = []
+ fields = @fields()
+ keys = @validation
+ return true if not keys
+
+ valid = yes
+
+ for key of keys
+ rules = if keys[key] instanceof Array then keys[key] else [keys[key]]
+ [result, errors] = Jewel.Validation.validate key, fields[key], rules
+ if errors.length > 0
+ @errors.push key
+ for error in errors
+ @errorMessages.push error
+
+ valid = no
+
+ valid
+
+ save: ->
+ return false if not @validate()
+
+ @beforeSave.call @ if @beforeSave
+ @aroundSave.call @ if @aroundSave
+
+ if not @element then @create() else @update()
+ Jewel.Cache.invalidate @childrenSelector, Jewel.Cache.get(@selector).children(":not(#{ @emptyViewSelector })")
+ #Jewel.Cache.invalidate @selector
+
+ @aroundSave.call @ if @aroundSave
+ @afterSave.call @ if @afterSave
+ Jewel.Events.emit @selector, 'save', @
+
+ true
+
+ create: ->
+ @beforeCreate.call @ if @beforeCreate
+
+ @element = $ @options.template(@fields())
+ @element["#{ @view.insert }To"] @selector
+
+ limit = @view.limit or @view.max
+ limit = parseInt limit
+ all = Jewel.Cache.get @childrenSelector
+ if all.length > 0
+ if limit > 0 and all.length >= limit
+ all.last().remove()
+ all = undefined
+ else
+ @emptyView.hide()
+
+ @afterCreate.call @ if @afterCreate
+
+ Jewel.Events.emit @selector, 'create', @
+
+ update: ->
+ @beforeUpdate.call @ if @beforeUpdate
+ @aroundUpdate.call @ if @aroundUpdate
+
+ fields = @fields()
+
+ for key of fields
+ $(@element).find(@options.keys[key]).html(fields[key])
+
+ @aroundUpdate.call @ if @aroundUpdate
+ @afterUpdate.call @ if @afterUpdate
+
+ Jewel.Events.emit @selector, 'update', @
+
+ remove: ->
+ @beforeRemove.call @ if @beforeRemove
+ @aroundRemove.call @ if @aroundRemove
+
+ do $(@element).remove
+ @emptyView.show() if Jewel.Cache.get(@childrenSelector).length is 1
+ Jewel.Cache.invalidate @childrenSelector, Jewel.Cache.get(@selector).children(":not(#{ @emptyViewSelector })")
+ #Jewel.Cache.invalidate @selector
+
+ @aroundRemove.call @ if @aroundRemove
+ @afterRemove.call @ if @afterRemove
+
+ Jewel.Events.emit @selector, 'remove', @
+
+ @remove: ->
+ @emptyView.show() if Jewel.Cache.get(@childrenSelector).length is 1
+ Jewel.Cache.get(@childrenSelector).remove()
+ Jewel.Cache.invalidate @childrenSelector, Jewel.Cache.get(@selector).children(":not(#{ @emptyViewSelector })")
+ #Jewel.Cache.invalidate @selector
+
+ @on: (event, listener) ->
+ Jewel.Events.on @selector, event, listener
313 lib/components/model.js
View
@@ -0,0 +1,313 @@
+// Generated by CoffeeScript 1.3.3
+
+Jewel.Model = (function() {
+
+ Model.prototype.errors = [];
+
+ Model.prototype.errorMessages = [];
+
+ Model.define = function(selector, options) {
+ var hook, model;
+ if (options == null) {
+ options = {};
+ }
+ model = {
+ selector: selector,
+ keys: options.keys,
+ template: options.template,
+ validation: options.validation
+ };
+ if (options.hooks) {
+ for (hook in options.hooks) {
+ model[hook] = options.hooks[hook];
+ }
+ }
+ return this.bake(model);
+ };
+
+ Model.setup = function(model) {
+ return this.bake(model.prototype);
+ };
+
+ Model.bake = function(options) {
+ var children, container, hook, model;
+ model = Jewel.Model.clone();
+ model.prototype.selector = model.selector = options.selector;
+ delete options.selector;
+ model.prototype.options = model.options = {
+ keys: options.keys,
+ template: options.template
+ };
+ delete options.keys;
+ delete options.template;
+ model.prototype.validation = model.validation = options.validation;
+ delete options.validation;
+ for (hook in options) {
+ model.prototype[hook] = options[hook];
+ }
+ model.__defineGetter__('all', function() {
+ return model.find();
+ });
+ model.__defineGetter__('first', function() {
+ return model.find({
+ limit: 1
+ })[0];
+ });
+ model.__defineGetter__('last', function() {
+ return model.find({
+ limit: 1,
+ order: 'desc'
+ })[0];
+ });
+ model.prototype.view = model.view = {
+ limit: 0,
+ insert: 'prepend'
+ };
+ container = Jewel.Cache.get(model.prototype.selector);
+ $.each(container.attr('data-view').split(','), function(index, value) {
+ var key, _ref;
+ _ref = value.split(':'), key = _ref[0], value = _ref[1];
+ key = key.trim();
+ return model.prototype.view[key] = model.view[key] = value.trim();
+ });
+ model.prototype.emptyViewSelector = model.emptyViewSelector = options.emptyView || '.not-found';
+ model.prototype.emptyView = model.emptyView = container.find(model.prototype.emptyViewSelector);
+ children = Jewel.Cache.set("" + model.prototype.selector + ":children", container.children(":not(" + model.prototype.emptyViewSelector + ")"));
+ if (children.length > 0) {
+ model.prototype.emptyView.hide();
+ } else {
+ model.prototype.emptyView.show();
+ }
+ model.prototype.childrenSelector = model.childrenSelector = "" + model.prototype.selector + ":children";
+ model.prototype.model = model.model = model;
+ return model;
+ };
+
+ function Model() {}
+
+ Model.prototype.init = function(element) {
+ var e, key, _results, _results1;
+ if (element instanceof HTMLElement) {
+ this.element = element;
+ if (this.options.keys) {
+ _results = [];
+ for (key in this.options.keys) {
+ e = $(element).find(this.options.keys[key]);
+ _results.push(this[key] = e.html());
+ }
+ return _results;
+ }
+ } else {
+ _results1 = [];
+ for (key in element) {
+ _results1.push(this[key] = element[key]);
+ }
+ return _results1;
+ }
+ };
+
+ Model.prototype.updateAttributes = function(fields) {
+ var key, validFields, _results;
+ validFields = this.fields();
+ _results = [];
+ for (key in fields) {
+ if (validFields[key]) {
+ _results.push(this[key] = fields[key]);
+ } else {
+ _results.push(void 0);
+ }
+ }
+ return _results;
+ };
+
+ Model.find = function(options) {
+ var i, item, items, loopTimes, model, modifier, results, _i, _len;
+ if (options == null) {
+ options = {};
+ }
+ items = Jewel.Cache.get(this.childrenSelector);
+ results = [];
+ if (items.length > 0) {
+ if (options.skip || options.limit || options.order || options.offset) {
+ if (options.order && options.order.toLowerCase() === 'desc') {
+ i = items.length - 1;
+ modifier = -1;
+ } else {
+ i = 0;
+ modifier = 1;
+ }
+ loopTimes = 0;
+ while (true) {
+ if (loopTimes >= options.limit) {
+ break;
+ }
+ if (!items[i]) {
+ break;
+ }
+ if (loopTimes >= (options.skip || options.offset || -1)) {
+ model = new this.model;
+ model.init(items[i]);
+ results.push(model);
+ }
+ i += modifier;
+ loopTimes++;
+ }
+ } else {
+ for (_i = 0, _len = items.length; _i < _len; _i++) {
+ item = items[_i];
+ model = new this.model;
+ model.init(item);
+ results.push(model);
+ }
+ }
+ }
+ return results;
+ };
+
+ Model.prototype.fields = function() {
+ var fields, key, notFields;
+ notFields = ['init', 'save', 'remove', 'selector', 'options', 'model', 'create', 'update', 'fields', 'updateAttributes', 'childrenSelector', 'emptyView', 'emptyViewSelector', 'errors', 'errorMessages', 'validate', 'validation', 'view'];
+ fields = {};
+ for (key in this.options.keys) {
+ fields[key] = void 0;
+ }
+ for (key in this) {
+ if (-1 === notFields.indexOf(key)) {
+ fields[key] = this[key];
+ }
+ }
+ return fields;
+ };
+
+ Model.prototype.validate = function() {
+ var error, errors, fields, key, keys, result, rules, valid, _i, _len, _ref;
+ this.errors = [];
+ this.errorMessages = [];
+ fields = this.fields();
+ keys = this.validation;
+ if (!keys) {
+ return true;
+ }
+ valid = true;
+ for (key in keys) {
+ rules = keys[key] instanceof Array ? keys[key] : [keys[key]];
+ _ref = Jewel.Validation.validate(key, fields[key], rules), result = _ref[0], errors = _ref[1];
+ if (errors.length > 0) {
+ this.errors.push(key);
+ for (_i = 0, _len = errors.length; _i < _len; _i++) {
+ error = errors[_i];
+ this.errorMessages.push(error);
+ }
+ valid = false;
+ }
+ }
+ return valid;
+ };
+
+ Model.prototype.save = function() {
+ if (!this.validate()) {
+ return false;
+ }
+ if (this.beforeSave) {
+ this.beforeSave.call(this);
+ }
+ if (this.aroundSave) {
+ this.aroundSave.call(this);
+ }
+ if (!this.element) {
+ this.create();
+ } else {
+ this.update();
+ }
+ Jewel.Cache.invalidate(this.childrenSelector, Jewel.Cache.get(this.selector).children(":not(" + this.emptyViewSelector + ")"));
+ if (this.aroundSave) {
+ this.aroundSave.call(this);
+ }
+ if (this.afterSave) {
+ this.afterSave.call(this);
+ }
+ Jewel.Events.emit(this.selector, 'save', this);
+ return true;
+ };
+
+ Model.prototype.create = function() {
+ var all, limit;
+ if (this.beforeCreate) {
+ this.beforeCreate.call(this);
+ }
+ this.element = $(this.options.template(this.fields()));
+ this.element["" + this.view.insert + "To"](this.selector);
+ limit = this.view.limit || this.view.max;
+ limit = parseInt(limit);
+ all = Jewel.Cache.get(this.childrenSelector);
+ if (all.length > 0) {
+ if (limit > 0 && all.length >= limit) {
+ all.last().remove();
+ all = void 0;
+ }
+ } else {
+ this.emptyView.hide();
+ }
+ if (this.afterCreate) {
+ this.afterCreate.call(this);
+ }
+ return Jewel.Events.emit(this.selector, 'create', this);
+ };
+
+ Model.prototype.update = function() {
+ var fields, key;
+ if (this.beforeUpdate) {
+ this.beforeUpdate.call(this);
+ }
+ if (this.aroundUpdate) {
+ this.aroundUpdate.call(this);
+ }
+ fields = this.fields();
+ for (key in fields) {
+ $(this.element).find(this.options.keys[key]).html(fields[key]);
+ }
+ if (this.aroundUpdate) {
+ this.aroundUpdate.call(this);
+ }
+ if (this.afterUpdate) {
+ this.afterUpdate.call(this);
+ }
+ return Jewel.Events.emit(this.selector, 'update', this);
+ };
+
+ Model.prototype.remove = function() {
+ if (this.beforeRemove) {
+ this.beforeRemove.call(this);
+ }
+ if (this.aroundRemove) {
+ this.aroundRemove.call(this);
+ }
+ $(this.element).remove();
+ if (Jewel.Cache.get(this.childrenSelector).length === 1) {
+ this.emptyView.show();
+ }
+ Jewel.Cache.invalidate(this.childrenSelector, Jewel.Cache.get(this.selector).children(":not(" + this.emptyViewSelector + ")"));
+ if (this.aroundRemove) {
+ this.aroundRemove.call(this);
+ }
+ if (this.afterRemove) {
+ this.afterRemove.call(this);
+ }
+ return Jewel.Events.emit(this.selector, 'remove', this);
+ };
+
+ Model.remove = function() {
+ if (Jewel.Cache.get(this.childrenSelector).length === 1) {
+ this.emptyView.show();
+ }
+ Jewel.Cache.get(this.childrenSelector).remove();
+ return Jewel.Cache.invalidate(this.childrenSelector, Jewel.Cache.get(this.selector).children(":not(" + this.emptyViewSelector + ")"));
+ };
+
+ Model.on = function(event, listener) {
+ return Jewel.Events.on(this.selector, event, listener);
+ };
+
+ return Model;
+
+})();
49 lib/components/validation.coffee
View
@@ -0,0 +1,49 @@
+class Jewel.Validation
+ @rules: {}
+
+ @define: (options) ->
+ @rules[options.rule] = options
+
+ @validate: (name, value, rules) ->
+ valid = yes
+ errors = []
+
+ for rule in rules
+ param = undefined
+ ruleName = rule
+
+ if /\(/.test rule # there is a param
+ [rule, ruleName, param] = /([A-Za-z0-9_]+)\((.+)\)/.exec rule
+
+ if not @rules[ruleName].validator(value, param)
+ valid = no
+ humanizedName = name.replace(/[^A-Za-z0-9]/, '').replace /^.|\s\S/g, (v) ->
+ v.toUpperCase()
+
+ errors.push @rules[ruleName].message.replace(':name', humanizedName).replace(':param', param)
+ break
+
+ [valid, errors]
+
+Jewel.Validation.define rule: 'required', message: ':name is required', validator: (value) ->
+ !!value
+
+Jewel.Validation.define rule: 'email', message: ':name is not valid email', validator: (value) ->
+ /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test value
+
+Jewel.Validation.define rule: 'min', message: ':name should be bigger than :param', validator: (value, param) ->
+ param = parseInt param
+ (if typeof value is 'number' then value else value.length) >= param
+
+Jewel.Validation.define rule: 'max', message: ':name should be smaller than :param', validator: (value, param) ->
+ param = parseInt param
+ (if typeof value is 'number' then value else value.length) <= param
+
+Jewel.Validation.define rule: 'onlyLetters', message: ':name should contain only letters', validator: (value) ->
+ not /[^A-Za-z ]/.test value
+
+Jewel.Validation.define rule: 'onlyNumbers', message: ':name should contain only numbers', validator: (value) ->
+ not /[^0-9]/.test value
+
+Jewel.Validation.define rule: 'onlyLettersAndNumbers', message: ':name should contain only letters and numbers', validator: (value) ->
+ not /[^A-Za-z0-9 ]/.test value
96 lib/components/validation.js
View
@@ -0,0 +1,96 @@
+// Generated by CoffeeScript 1.3.3
+
+Jewel.Validation = (function() {
+
+ function Validation() {}
+
+ Validation.rules = {};
+
+ Validation.define = function(options) {
+ return this.rules[options.rule] = options;
+ };
+
+ Validation.validate = function(name, value, rules) {
+ var errors, humanizedName, param, rule, ruleName, valid, _i, _len, _ref;
+ valid = true;
+ errors = [];
+ for (_i = 0, _len = rules.length; _i < _len; _i++) {
+ rule = rules[_i];
+ param = void 0;
+ ruleName = rule;
+ if (/\(/.test(rule)) {
+ _ref = /([A-Za-z0-9_]+)\((.+)\)/.exec(rule), rule = _ref[0], ruleName = _ref[1], param = _ref[2];
+ }
+ if (!this.rules[ruleName].validator(value, param)) {
+ valid = false;
+ humanizedName = name.replace(/[^A-Za-z0-9]/, '').replace(/^.|\s\S/g, function(v) {
+ return v.toUpperCase();
+ });
+ errors.push(this.rules[ruleName].message.replace(':name', humanizedName).replace(':param', param));
+ break;
+ }
+ }
+ return [valid, errors];
+ };
+
+ return Validation;
+
+})();
+
+Jewel.Validation.define({
+ rule: 'required',
+ message: ':name is required',
+ validator: function(value) {
+ return !!value;
+ }
+});
+
+Jewel.Validation.define({
+ rule: 'email',
+ message: ':name is not valid email',
+ validator: function(value) {
+ return /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(value);
+ }
+});
+
+Jewel.Validation.define({
+ rule: 'min',
+ message: ':name should be bigger than :param',
+ validator: function(value, param) {
+ param = parseInt(param);
+ return (typeof value === 'number' ? value : value.length) >= param;
+ }
+});
+
+Jewel.Validation.define({
+ rule: 'max',
+ message: ':name should be smaller than :param',
+ validator: function(value, param) {
+ param = parseInt(param);
+ return (typeof value === 'number' ? value : value.length) <= param;
+ }
+});
+
+Jewel.Validation.define({
+ rule: 'onlyLetters',
+ message: ':name should contain only letters',
+ validator: function(value) {
+ return !/[^A-Za-z ]/.test(value);
+ }
+});
+
+Jewel.Validation.define({
+ rule: 'onlyNumbers',
+ message: ':name should contain only numbers',
+ validator: function(value) {
+ return !/[^0-9]/.test(value);
+ }
+});
+
+Jewel.Validation.define({
+ rule: 'onlyLettersAndNumbers',
+ message: ':name should contain only letters and numbers',
+ validator: function(value) {
+ return !/[^A-Za-z0-9 ]/.test(value);
+ }
+});
75 lib/jewel.coffee
View
@@ -1,4 +1,4 @@
-Function::clone = ->
+Function::clone = -> # function cloning
clone = ->
for property of @
clone[property] = @[property] if @hasOwnProperty property
@@ -6,71 +6,10 @@ Function::clone = ->
clone.prototype = @prototype
clone
-class Jewel
- @define: (selector, options = {}) ->
- model = do Model.clone
- model::selector = model.selector = selector
- model::options = model.options = options
- model.__defineGetter__ 'all', ->
- do model.find
- model::model = model.model = model
- model
+if not String::trim
+ String::trim = ->
+ @replace(/^\s\s*/, '').replace(/\s\s*$/, '')
-class Model
- constructor: ->
-
- init: (element) ->
- if element instanceof HTMLElement
- @element = element
- if @options.keys
- for key of @options.keys
- e = $(element).find @options.keys[key]
- @[key] = e.html()
- else
- for key of element
- @[key] = element[key]
-
- @find: (options = {}) ->
- items = $(@selector).children()
- results = undefined
- if items.length > 0
- if options.skip or options.limit
- i = 0
- results = []
- loop
- break if not items[i]
-
- if i >= (options.skip or -1) and i < (options.limit or items.length)
- model = new @model
- model.init items[i]
- results.push model
- i++
-
- results = items if not results
- results
-
- fields: ->
- notFields = ['init', 'save', 'remove', 'selector', 'options', 'model']
- fields = {}
- for key of @
- fields[key] = @[key] if -1 is notFields.indexOf key
- fields
-
- save: ->
- if not @element then @create() else @update()
-
- create: ->
- @element = $ @options.template @fields()
- @element.prependTo @selector
-
- update: ->
- fields = @fields()
-
- for key of fields
- $(@element).find(@options.keys[key]).html(fields[key])
-
- remove: ->
- do $(@element).remove
-
- @remove: ->
- do $(@selector).remove
+# Main class
+
+class Jewel
131 lib/jewel.js
View
@@ -1,5 +1,5 @@
-// Generated by CoffeeScript 1.3.1
-var Jewel, Model;
+// Generated by CoffeeScript 1.3.3
+var Jewel;
Function.prototype.clone = function() {
var clone, property;
@@ -13,131 +13,16 @@ Function.prototype.clone = function() {
return clone;
};
-Jewel = (function() {
+if (!String.prototype.trim) {
+ String.prototype.trim = function() {
+ return this.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
+ };
+}
- Jewel.name = 'Jewel';
+Jewel = (function() {
function Jewel() {}
- Jewel.define = function(selector, options) {
- var model;
- if (options == null) {
- options = {};
- }
- model = Model.clone();
- model.prototype.selector = model.selector = selector;
- model.prototype.options = model.options = options;
- model.__defineGetter__('all', function() {
- return model.find();
- });
- model.prototype.model = model.model = model;
- return model;
- };
-
return Jewel;
})();
-
-Model = (function() {
-
- Model.name = 'Model';
-
- function Model() {}
-
- Model.prototype.init = function(element) {
- var e, key, _results, _results1;
- if (element instanceof HTMLElement) {
- this.element = element;
- if (this.options.keys) {
- _results = [];
- for (key in this.options.keys) {
- e = $(element).find(this.options.keys[key]);
- _results.push(this[key] = e.html());
- }
- return _results;
- }
- } else {
- _results1 = [];
- for (key in element) {
- _results1.push(this[key] = element[key]);
- }
- return _results1;
- }
- };
-
- Model.find = function(options) {
- var i, items, model, results;
- if (options == null) {
- options = {};
- }
- items = $(this.selector).children();
- results = void 0;
- if (items.length > 0) {
- if (options.skip || options.limit) {
- i = 0;
- results = [];
- while (true) {
- if (!items[i]) {
- break;
- }
- if (i >= (options.skip || -1) && i < (options.limit || items.length)) {
- model = new this.model;
- model.init(items[i]);
- results.push(model);
- }
- i++;
- }
- }
- }
- if (!results) {
- results = items;
- }
- return results;
- };
-
- Model.prototype.fields = function() {
- var fields, key, notFields;
- notFields = ['init', 'save', 'remove', 'selector', 'options', 'model'];
- fields = {};
- for (key in this) {
- if (-1 === notFields.indexOf(key)) {
- fields[key] = this[key];
- }
- }
- return fields;
- };
-
- Model.prototype.save = function() {
- if (!this.element) {
- return this.create();
- } else {
- return this.update();
- }
- };
-
- Model.prototype.create = function() {
- this.element = $(this.options.template(this.fields()));
- return this.element.prependTo(this.selector);
- };
-
- Model.prototype.update = function() {
- var fields, key, _results;
- fields = this.fields();
- _results = [];
- for (key in fields) {
- _results.push($(this.element).find(this.options.keys[key]).html(fields[key]));
- }
- return _results;
- };
-
- Model.prototype.remove = function() {
- return $(this.element).remove();
- };
-
- Model.remove = function() {
- return $(this.selector).remove();
- };
-
- return Model;
-
-})();
263 test/jewel.test.coffee
View
@@ -1,37 +1,226 @@
-Post = Jewel.define 'div.posts',
- keys:
- title: 'h1'
- body: 'p',
- template: (fields) ->
- "<div class=\"post\"><h1>#{ fields.title }</h1><p>#{ fields.body }</p></div>"
-
-describe 'Jewel', ->
- it 'should fetch all posts', ->
- expect(Post.all.length).to.be 2
-
- it 'should fetch first post', ->
- expect(Post.find(limit: 1)[0].title).to.be 'First post'
-
- it 'should fetch second post', ->
- expect(Post.find(skip: 1)[0].title).to.be 'Second post'
-
- it 'should create new post', ->
- post = new Post
- post.title = 'Third post'
- post.body = 'Third content'
- post.save()
- expect(Post.find(limit: 1)[0].title).to.be 'Third post'
-
- it 'should update third post', ->
- post = Post.find(limit: 1)[0]
- post.title = 'Latest post'
- post.save()
- expect(Post.find(limit: 1)[0].title).to.be 'Latest post'
-
- it 'should remove second post', ->
- Post.find(skip: 1)[0].remove()
- expect(Post.all.length).to.be 2
-
- it 'should remove all posts', ->
- Post.remove()
- expect(Post.all.length).to.be 0
+Jewel.Validation.define rule: 'startsWith', message: ':name should start with :param', validator: (value, param) ->
+ if param is 'uppercase'
+ return String.fromCharCode(value.charCodeAt(0) + 32) is value[0].toLowerCase()
+
+ if param is 'downcase'
+ return String.fromCharCode(value.charCodeAt(0) - 32) is value[0].toUpperCase()
+
+ no
+
+$ ->
+
+ ###
+ class Post extends Jewel.Model
+ selector: 'div.posts'
+ keys:
+ title: 'h1'
+ body: 'p'
+ validation:
+ title: ['required', 'startsWith(uppercase)', 'min(5)']
+ body: 'required'
+ template: (fields) ->
+ "<div class=\"post\"><h1>#{ fields.title }</h1><p>#{ fields.body }</p></div>"
+
+ Post = Jewel.Model.setup Post
+ ###
+
+ Post = Jewel.Model.define 'div.posts',
+ keys:
+ title: 'h1'
+ body: 'p',
+ validation:
+ title: ['required', 'startsWith(uppercase)', 'min(5)']
+ body: 'required'
+ emptyView: 'p.not-found'
+ template: (fields) ->
+ "<div class=\"post\"><h1>#{ fields.title }</h1><p>#{ fields.body }</p></div>"
+
+ describe 'Jewel', ->
+ describe 'CRUD', ->
+ it 'should fetch all posts', ->
+ expect(Post.all.length).to.be 2
+
+ it 'should fetch first post', ->
+ expect(Post.first.title).to.be 'First post'
+
+ it 'should fetch second post', ->
+ expect(Post.find(skip: 1)[0].title).to.be 'Second post'
+
+ it 'should fetch last post', ->
+ expect(Post.last.title).to.be 'Second post'
+
+ it 'should create new post', ->
+ post = new Post
+ post.title = 'Third post'
+ post.body = 'Third content'
+ post.save()
+ expect(Post.all.length).to.be(2) and expect(Post.first.title).to.be 'Third post'
+
+ it 'should update latest post', ->
+ post = Post.first
+ post.title = 'Latest post'
+ post.save()
+ expect(Post.first.title).to.be 'Latest post'
+
+ it 'should return posts in reverse order', ->
+ posts = Post.find order: 'desc'
+ expect(posts[0].title).to.be 'First post'
+ expect(posts[1].title).to.be 'Latest post'
+
+ it 'should remove second post', ->
+ Post.last.remove()
+ expect(Post.all.length).to.be 1
+
+ it 'should remove all posts', ->
+ Post.remove()
+ expect(Post.all.length).to.be 0
+
+ it 'should update specified attributes', ->
+ post = new Post
+ post.updateAttributes title: 'Title', body: 'Body', isAdmin: yes
+ expect(post.isAdmin).to.be undefined
+
+ describe 'Validation', ->
+ it 'should validate fields successfully', ->
+ post = new Post
+ post.title = 'Very nice title'
+ post.body = 'Woot'
+ expect(post.save()).to.be true
+ Post.remove()
+
+ it 'should validate fields with error', ->
+ post = new Post
+ expect(post.save()).to.be(false) and
+ expect(post.errors.length).to.be(2) and
+ expect(post.errorMessages.length).to.be(2)
+
+ describe 'Rules', ->
+ it 'should validate existance of the field', ->
+ expect(Jewel.Validation.validate('key', 'value', ['required'])[0]).to.be(true) and
+ expect(Jewel.Validation.validate('key', '', ['required'])[0]).to.be(false)
+
+ it 'should validate an email', ->
+ expect(Jewel.Validation.validate('email', 'test@test.com', ['email'])[0]).to.be(true) and
+ expect(Jewel.Validation.validate('email', 'lalala.com', ['email'])[0]).to.be(false)
+
+ it 'should validate minimum value', ->
+ expect(Jewel.Validation.validate('number', 2, ['min(1)'])[0]).to.be(true) and
+ expect(Jewel.Validation.validate('number', 2, ['min(3)'])[0]).to.be(false) and
+ expect(Jewel.Validation.validate('string', 'abc', ['min(2)'])[0]).to.be(true) and
+ expect(Jewel.Validation.validate('string', 'abc', ['min(4)'])[0]).to.be(false)
+
+ it 'should validate maximum value', ->
+ expect(Jewel.Validation.validate('number', 2, ['max(3)'])[0]).to.be(true) and
+ expect(Jewel.Validation.validate('number', 2, ['max(1)'])[0]).to.be(false) and
+ expect(Jewel.Validation.validate('string', 'abc', ['max(4)'])[0]).to.be(true) and
+ expect(Jewel.Validation.validate('string', 'abc', ['max(2)'])[0]).to.be(false)
+
+ it 'should pass only letters', ->
+ expect(Jewel.Validation.validate('string', 'Nice sentence', ['onlyLetters'])[0]).to.be(true) and
+ expect(Jewel.Validation.validate('string', '2 nice sentences', ['onlyLetters'])[0]).to.be(false)
+
+ it 'should pass only numbers', ->
+ expect(Jewel.Validation.validate('string', '124235', ['onlyNumbers'])[0]).to.be(true) and
+ expect(Jewel.Validation.validate('string', '2 nice sentences', ['onlyNumbers'])[0]).to.be(false)
+
+ it 'should pass only letters and numbers', ->
+ expect(Jewel.Validation.validate('string', '2 nice sentences', ['onlyLettersAndNumbers'])[0]).to.be(true) and
+ expect(Jewel.Validation.validate('string', '2 nice sentences!', ['onlyLettersAndNumbers'])[0]).to.be(false)
+
+ describe 'Events', ->
+ it 'should catch create, update and remove events', (done) ->
+ events = ['create', 'update', 'remove']
+ step = ->
+ events.shift()
+ do done if events.length is 0
+
+ Post.on 'create', (post) ->
+ step()
+
+ Post.on 'update', (post) ->
+ step()
+
+ Post.on 'remove', (post) ->
+ step()
+
+ post = new Post
+ post.title = 'Woohoo'
+ post.body = 'Test'
+ post.save()
+
+ post.title = 'Woo hoo'
+ post.save()
+
+ post.remove()
+
+ describe 'Hooks', ->
+ it 'should catch before|around|after save|create|update|remove hooks', (done) ->
+ hooks = [
+ 'beforeSave', 'aroundSave', 'afterSave',
+ 'beforeCreate', 'aroundCreate', 'afterCreate',
+ 'beforeUpdate', 'aroundUpdate', 'afterUpdate',
+ 'beforeRemove', 'aroundRemove', 'afterRemove'
+ ]
+
+ step = ->
+ hooks.shift()
+ do done if hooks.length is 0
+
+ Post = Jewel.Model.define 'div.posts',
+ keys:
+ title: 'h1'
+ body: 'p'
+ hooks:
+ beforeSave: ->
+ step()
+
+ aroundSave: ->
+ step()
+
+ afterSave: ->
+ step()
+
+ beforeCreate: ->
+ step()
+
+ aroundCreate: ->
+ step()
+
+ afterCreate: ->
+ step()
+
+ beforeUpdate: ->
+ step()
+
+ aroundUpdate: ->
+ step()
+
+ afterUpdate: ->
+ step()
+
+ beforeRemove: ->
+ step()
+
+ aroundRemove: ->
+ step()
+
+ afterRemove: ->
+ step()
+ template: (fields) ->
+ "<div class=\"post\"><h1>" + fields.title + "</h1><p>" + fields.body + "</p></div>"
+
+ post = new Post
+ post.title = 'Post title!'
+ post.body = 'Post body'
+ post.save()
+
+ post.title = 'New post title'
+ post.save()
+
+ post.remove()
+
+ describe 'Utilities', ->
+ it 'should display empty view', ->
+ emptyView = $ 'div.posts p.not-found'
+ expect(emptyView.length).to.be(1)
+ emptyView.remove()
292 test/jewel.test.js
View
@@ -1,59 +1,247 @@
-// Generated by CoffeeScript 1.3.1
-var Post;
+// Generated by CoffeeScript 1.3.3
-Post = Jewel.define('div.posts', {
- keys: {
- title: 'h1',
- body: 'p'
- },
- template: function(fields) {
- return "<div class=\"post\"><h1>" + fields.title + "</h1><p>" + fields.body + "</p></div>";
+Jewel.Validation.define({
+ rule: 'startsWith',
+ message: ':name should start with :param',
+ validator: function(value, param) {
+ if (param === 'uppercase') {
+ return String.fromCharCode(value.charCodeAt(0) + 32) === value[0].toLowerCase();
+ }
+ if (param === 'downcase') {
+ return String.fromCharCode(value.charCodeAt(0) - 32) === value[0].toUpperCase();
+ }
+ return false;
}
});
-describe('Jewel', function() {
- it('should fetch all posts', function() {
- return expect(Post.all.length).to.be(2);
- });
- it('should fetch first post', function() {
- return expect(Post.find({
- limit: 1
- })[0].title).to.be('First post');
- });
- it('should fetch second post', function() {
- return expect(Post.find({
- skip: 1
- })[0].title).to.be('Second post');
- });
- it('should create new post', function() {
- var post;
- post = new Post;
- post.title = 'Third post';
- post.body = 'Third content';
- post.save();
- return expect(Post.find({
- limit: 1
- })[0].title).to.be('Third post');
- });
- it('should update third post', function() {
- var post;
- post = Post.find({
- limit: 1
- })[0];
- post.title = 'Latest post';
- post.save();
- return expect(Post.find({
- limit: 1
- })[0].title).to.be('Latest post');
- });
- it('should remove second post', function() {
- Post.find({
- skip: 1
- })[0].remove();
- return expect(Post.all.length).to.be(2);
+$(function() {
+ /*
+ class Post extends Jewel.Model
+ selector: 'div.posts'
+ keys:
+ title: 'h1'
+ body: 'p'
+ validation:
+ title: ['required', 'startsWith(uppercase)', 'min(5)']
+ body: 'required'
+ template: (fields) ->
+ "<div class=\"post\"><h1>#{ fields.title }</h1><p>#{ fields.body }</p></div>"
+
+ Post = Jewel.Model.setup Post
+ */
+
+ var Post;
+ Post = Jewel.Model.define('div.posts', {
+ keys: {
+ title: 'h1',
+ body: 'p'
+ },
+ validation: {
+ title: ['required', 'startsWith(uppercase)', 'min(5)'],
+ body: 'required'
+ },
+ emptyView: 'p.not-found',
+ template: function(fields) {
+ return "<div class=\"post\"><h1>" + fields.title + "</h1><p>" + fields.body + "</p></div>";
+ }
});
- return it('should remove all posts', function() {
- Post.remove();
- return expect(Post.all.length).to.be(0);
+ return describe('Jewel', function() {
+ describe('CRUD', function() {
+ it('should fetch all posts', function() {
+ return expect(Post.all.length).to.be(2);
+ });
+ it('should fetch first post', function() {
+ return expect(Post.first.title).to.be('First post');
+ });
+ it('should fetch second post', function() {
+ return expect(Post.find({
+ skip: 1
+ })[0].title).to.be('Second post');
+ });
+ it('should fetch last post', function() {
+ return expect(Post.last.title).to.be('Second post');
+ });
+ it('should create new post', function() {
+ var post;
+ post = new Post;
+ post.title = 'Third post';
+ post.body = 'Third content';
+ post.save();
+ return expect(Post.all.length).to.be(2) && expect(Post.first.title).to.be('Third post');
+ });
+ it('should update latest post', function() {
+ var post;
+ post = Post.first;
+ post.title = 'Latest post';
+ post.save();
+ return expect(Post.first.title).to.be('Latest post');
+ });
+ it('should return posts in reverse order', function() {
+ var posts;
+ posts = Post.find({
+ order: 'desc'
+ });
+ expect(posts[0].title).to.be('First post');
+ return expect(posts[1].title).to.be('Latest post');
+ });
+ it('should remove second post', function() {
+ Post.last.remove();
+ return expect(Post.all.length).to.be(1);
+ });
+ it('should remove all posts', function() {
+ Post.remove();
+ return expect(Post.all.length).to.be(0);
+ });
+ return it('should update specified attributes', function() {
+ var post;
+ post = new Post;
+ post.updateAttributes({
+ title: 'Title',
+ body: 'Body',
+ isAdmin: true
+ });
+ return expect(post.isAdmin).to.be(void 0);
+ });
+ });
+ describe('Validation', function() {
+ it('should validate fields successfully', function() {
+ var post;
+ post = new Post;
+ post.title = 'Very nice title';
+ post.body = 'Woot';
+ expect(post.save()).to.be(true);
+ return Post.remove();
+ });
+ it('should validate fields with error', function() {
+ var post;
+ post = new Post;
+ return expect(post.save()).to.be(false) && expect(post.errors.length).to.be(2) && expect(post.errorMessages.length).to.be(2);
+ });
+ return describe('Rules', function() {
+ it('should validate existance of the field', function() {
+ return expect(Jewel.Validation.validate('key', 'value', ['required'])[0]).to.be(true) && expect(Jewel.Validation.validate('key', '', ['required'])[0]).to.be(false);
+ });
+ it('should validate an email', function() {
+ return expect(Jewel.Validation.validate('email', 'test@test.com', ['email'])[0]).to.be(true) && expect(Jewel.Validation.validate('email', 'lalala.com', ['email'])[0]).to.be(false);
+ });
+ it('should validate minimum value', function() {
+ return expect(Jewel.Validation.validate('number', 2, ['min(1)'])[0]).to.be(true) && expect(Jewel.Validation.validate('number', 2, ['min(3)'])[0]).to.be(false) && expect(Jewel.Validation.validate('string', 'abc', ['min(2)'])[0]).to.be(true) && expect(Jewel.Validation.validate('string', 'abc', ['min(4)'])[0]).to.be(false);
+ });
+ it('should validate maximum value', function() {
+ return expect(Jewel.Validation.validate('number', 2, ['max(3)'])[0]).to.be(true) && expect(Jewel.Validation.validate('number', 2, ['max(1)'])[0]).to.be(false) && expect(Jewel.Validation.validate('string', 'abc', ['max(4)'])[0]).to.be(true) && expect(Jewel.Validation.validate('string', 'abc', ['max(2)'])[0]).to.be(false);
+ });
+ it('should pass only letters', function() {
+ return expect(Jewel.Validation.validate('string', 'Nice sentence', ['onlyLetters'])[0]).to.be(true) && expect(Jewel.Validation.validate('string', '2 nice sentences', ['onlyLetters'])[0]).to.be(false);
+ });
+ it('should pass only numbers', function() {
+ return expect(Jewel.Validation.validate('string', '124235', ['onlyNumbers'])[0]).to.be(true) && expect(Jewel.Validation.validate('string', '2 nice sentences', ['onlyNumbers'])[0]).to.be(false);
+ });
+ return it('should pass only letters and numbers', function() {
+ return expect(Jewel.Validation.validate('string', '2 nice sentences', ['onlyLettersAndNumbers'])[0]).to.be(true) && expect(Jewel.Validation.validate('string', '2 nice sentences!', ['onlyLettersAndNumbers'])[0]).to.be(false);
+ });
+ });
+ });
+ describe('Events', function() {
+ return it('should catch create, update and remove events', function(done) {
+ var events, post, step;
+ events = ['create', 'update', 'remove'];
+ step = function() {
+ events.shift();
+ if (events.length === 0) {
+ return done();
+ }
+ };
+ Post.on('create', function(post) {
+ return step();
+ });
+ Post.on('update', function(post) {
+ return step();
+ });
+ Post.on('remove', function(post) {
+ return step();
+ });
+ post = new Post;
+ post.title = 'Woohoo';
+ post.body = 'Test';
+ post.save();
+ post.title = 'Woo hoo';
+ post.save();
+ return post.remove();
+ });
+ });
+ describe('Hooks', function() {
+ return it('should catch before|around|after save|create|update|remove hooks', function(done) {
+ var hooks, post, step;
+ hooks = ['beforeSave', 'aroundSave', 'afterSave', 'beforeCreate', 'aroundCreate', 'afterCreate', 'beforeUpdate', 'aroundUpdate', 'afterUpdate', 'beforeRemove', 'aroundRemove', 'afterRemove'];
+ step = function() {
+ hooks.shift();
+ if (hooks.length === 0) {
+ return done();
+ }
+ };
+ Post = Jewel.Model.define('div.posts', {
+ keys: {
+ title: 'h1',
+ body: 'p'
+ },
+ hooks: {
+ beforeSave: function() {
+ return step();
+ },
+ aroundSave: function() {
+ return step();
+ },
+ afterSave: function() {
+ return step();
+ },
+ beforeCreate: function() {
+ return step();
+ },
+ aroundCreate: function() {
+ return step();
+ },
+ afterCreate: function() {
+ return step();
+ },
+ beforeUpdate: function() {
+ return step();
+ },
+ aroundUpdate: function() {
+ return step();
+ },
+ afterUpdate: function() {
+ return step();
+ },
+ beforeRemove: function() {
+ return step();
+ },
+ aroundRemove: function() {
+ return step();
+ },
+ afterRemove: function() {
+ return step();
+ }
+ },
+ template: function(fields) {
+ return "<div class=\"post\"><h1>" + fields.title + "</h1><p>" + fields.body + "</p></div>";
+ }
+ });