Created
October 26, 2011 08:04
-
-
Save willywongi/1315751 to your computer and use it in GitHub Desktop.
My own way of playing with the Y.App framework.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* global YUI=true */ | |
/* Note: the "png" prefix you're seeing through this file is just my own | |
namespace. No reference to the Portable Network Graphic intended. */ | |
YUI.add('png-entity', function(Y) { | |
/** | |
The ContainmentUnit's purposes are: creating Entity extensions, store them and handle | |
the relations between them. | |
@class ContainmentUnit | |
@constructor | |
**/ | |
/** | |
Ane extension to Model, with io/sync functions baked in | |
@class Entity | |
@extends Model | |
@constructor | |
**/ | |
var M = Y.namespace('png'), | |
sub = Y.Lang.sub, | |
substitute = Y.substitute, | |
CU; | |
CU = function(baseName) { | |
this.baseName = baseName; | |
this._entityClasses = {}; | |
}; | |
/** | |
Build a new class, extending Y.png.Entity, and add the constructor function to the | |
current ContainmentUnit. It wraps a call to Y.Base.create | |
@method create | |
@param {String} name The name for the new Entity Class. This is the defaulf for entityName. | |
@param {Class[]} ext A list of classes for mixins. See Y.Base.create. | |
@param {Object} [protos] This object will be mixed to the prototype. | |
@param {String} [protos.entityName] The entity name, as understood from the server. | |
@param {String[]} [protos.key] A list of Attributes wich represent the logical key in the | |
server environment. | |
@param {Object} The static properties to be mixed to the class object (constructor | |
function) | |
@param {Object} [statics.ATTRS] The Attributes for the class; see. Y.Attribute. | |
@return {Class} A class (constructor function) that inherits from Y.png.Entity. | |
**/ | |
CU.prototype.create = function(name, ext, protos, statics) { | |
var ecs = this._entityClasses, | |
newName = this.baseName + "-" + name, | |
entityClass; | |
if (ecs.hasOwnProperty(name)) { | |
Y.error('Warning: <'+name+'> already exists in this ContainmentUnit.'); | |
return; | |
} | |
protos = protos || {}; | |
protos.entityName = protos.entityName || name; | |
protos.key = protos.key || []; | |
if (protos.key.length && ! protos.idAttribute) { | |
protos.idAttribute = protos.key[0]; | |
} else if (protos.idAttribute && ! protos.key.length) { | |
protos.key = [protos.idAttribute]; | |
} | |
entityClass = Y.Base.create(newName, Y.png.Entity, ext, protos, statics); | |
ecs[name] = entityClass; | |
return entityClass; | |
}; | |
/** | |
Returns an already defined class | |
@method getClass | |
@param {String} name The name used during class creation (see @method create). | |
@return {Class} A class (constructor function) that inherits from Y.png.Entity. | |
**/ | |
CU.prototype.getClass = function(name) { | |
if (! name in this._entityClasses) { | |
Y.log('The class <'+name+'> can\'t be found in this (<'+this.baseName+'>)', 'warn'); | |
} | |
return this._entityClasses[name]; | |
}; | |
/** | |
* Returns a new instance of the desired class | |
* | |
* @method getNew | |
* @param {String} name The name of the class (see @method create). | |
* @param {Object} config The config object for the new instance. | |
*/ | |
CU.prototype.getNew = function(name, config) { | |
var entityClass = this.getClass(name); | |
config = config || {}; | |
return new entityClass(config); | |
}; | |
/** | |
* Returns a config object for an attribute that represent a 1-1 relation between Entities | |
* | |
* @example | |
* var cu = new Y.png.ContainmentUnit('test'); | |
* cu.create('Test', ...); | |
* [...] | |
* ATTRS: { | |
* test: cu.one('Test') | |
* } | |
* | |
* @method one | |
* @param {String} name The name used during create | |
* @return {Object} A config object for Attribute | |
*/ | |
CU.prototype.one = function(name) { | |
var cu = this; | |
return { | |
valueFn: function() { return null; }, | |
cloneDefault: false, | |
setter: function(obj) { | |
var EntityClass = cu.getClass(name); | |
if (Y.Lang.isNull(obj)) { | |
return obj; | |
} else if (obj instanceof EntityClass) { | |
return obj; | |
} else { | |
return new EntityClass(obj); | |
} | |
} | |
}; | |
}; | |
/** | |
* Returns a config object for an attribute that represent a 1-n relation between Entities | |
* @example | |
* var cu = new Y.png.ContainmentUnit('test'); | |
* cu.create('Test', ...); | |
* [...] | |
* ATTRS: { | |
* tests: cu.many('Test') | |
* } | |
* | |
* @method many | |
* @param {String} name The name used during create | |
* @return {Object} A config object for Attribute | |
*/ | |
CU.prototype.many = function(name) { | |
var cu = this; | |
return { | |
cloneDefault: false, | |
readOnly: true, | |
valueFn: function() { | |
var EntityClass = cu.getClass(name); | |
return new Y.png.EntityList({model: EntityClass}); | |
} | |
}; | |
}; | |
M.ContainmentUnit = CU; | |
/** | |
* Base class for Entities. Usually called within the ContainmentUnit. | |
* @param config {Object} Object literal with Attributes configuration. | |
* | |
* @class Entity | |
* @constructor | |
* @extends Model | |
*/ | |
M.Entity = Y.Base.create('PNG-Entity', Y.Model, [], { | |
entityName: 'Entity', | |
baseUrl: '/someEntryPoint', | |
key: ['id'], | |
initializer: function(cfg) { | |
this.setAttrs(cfg); | |
}, | |
/** | |
* Returns a object literal that represents the minimum information | |
* to identify the Entity | |
* | |
* @method asKey | |
* @return {Object} An object. | |
*/ | |
asKey: function() { | |
var k = {}, | |
key = this.key; | |
Y.Array.each(key, function(f) { | |
k[f] = this.get(f); | |
}, this); | |
return k; | |
}, | |
/** | |
* Restituisce un object literal che contiene i valori rilevanti per | |
* questa entità. Viene usato per il salvataggio. | |
* | |
* Returns a object literal that represents all the information | |
* related to this Entity. | |
* | |
* @method asPost | |
* @return {Object} An object. | |
*/ | |
asPost: function() { | |
var n, load = this.getAttrs(Y.Object.keys(this._classes[0].ATTRS)); | |
for (n in load) { | |
if (!load.hasOwnProperty(n)) { | |
continue; | |
} | |
if (load[n] instanceof M.Entity) { | |
load[n] = load[n].asKey(); | |
} | |
if (load[n] instanceof M.EntityList) { | |
// Avoid dumping the EntityList; to be FIXED | |
delete load[n]; | |
} | |
} | |
return load; | |
}, | |
/** | |
* Synchronize the Entity data with the server. Implementers should probably | |
* override this in order to match server urls and behaviour. | |
* | |
* @method sync | |
* @param {String} action Which action to run (read|delete|create|update) | |
* @param {Object} options | |
* @param {callback} [callback] Called when the sync operation finishes. | |
* @param {Error|null} callback.err If an error occurred, this parameter will | |
* contain the error. If the sync operation succeeded, _err_ will be | |
* `null`. | |
* @param {Any} callback.response Should contain a json object that update the Entity. | |
* If "create" it should contain the server assigned id. | |
* | |
* @return void | |
*/ | |
sync: function(action, options, callback) { | |
/* possible actions: | |
load() -> read, | |
destroy() -> delete, | |
save() -> create, | |
save() -> update | |
*/ | |
var data = [ | |
//sub(this.baseUrl, YUI.Env.png), // I use this to populate some environment stuff. | |
this.baseUrl, | |
'model=' + this.entityName | |
], | |
load, | |
method; | |
if (action === 'create' || action === 'update') { | |
Y.log('Entity.sync/'+action, 'debug'); | |
load = this.asPost(); | |
Y.mix(load, this.asKey()); | |
if (! load) { | |
callback(null, {}); | |
return; | |
} | |
data.push('load='+encodeURIComponent(Y.JSON.stringify(load))); | |
method = 'post'; | |
} else if (action === 'delete') { | |
data.push('function=remove'); | |
load = this.asKey(); | |
data.push('load='+encodeURIComponent(Y.JSON.stringify(load))); | |
method = 'post'; | |
} else if (action === 'read') { | |
load = this.asKey(); | |
data.push('load='+encodeURIComponent(Y.JSON.stringify(load))); | |
} | |
// This method just wrap an IO and a JSON.parse to handle json responses. | |
// FIXME | |
Y.png.ajax('/', { | |
method: method, | |
data: data.join("&"), | |
success: function(json) { | |
if (action === 'create' || action === 'update') { | |
this.setAttrs(json.payload, {silent: true}); | |
} | |
callback(null, json.payload); | |
}, | |
failure: function(json) { callback(json.payload, null); }, | |
context: this | |
}); | |
} | |
}, { | |
// static properties | |
// an object config that can be reused to handle Date attributes. | |
dateAttribute: { | |
getter: function(newVal) { | |
return new Date(newVal * 1000); | |
}, | |
setter: Number | |
} | |
}); | |
/** | |
* Base class to create Entity Lists. | |
* @param config {Object} Object literal config. | |
* @param config.model {EntityClass} A Entity class (Y.png.Entity) | |
* | |
* @class EntityList | |
* @constructor | |
* @extends ModelList | |
*/ | |
M.EntityList = Y.Base.create('PNG-EntityList', Y.ModelList, [], { | |
model: null, | |
initializer: function(cfg) { | |
if (! cfg.model) { | |
//throw "Impossibile avere una lista senza un model"; | |
Y.error("No way this is accettable: a EntityList without a Entity"); | |
} else { | |
// I heard this is going to change in YUI 3.5: FIXME | |
this.model = cfg.model; | |
} | |
this.toDestroy = []; | |
}, | |
/** | |
Loads this list of models from the server. | |
This method delegates to the sync() method to perform the actual load | |
operation, which is an asynchronous action. Specify a _callback_ function to | |
be notified of success or failure. | |
If the load operation succeeds, a reset event will be fired. | |
@method load | |
@param {Object} [options] Options to be passed to sync() and to | |
reset() when adding the loaded models. | |
@param {Object} [options.filter={}] This object will be used server side | |
to build an expression to gather the required models; every key/value pair | |
will be chained in a AND operation of 'equal to' tests, eg.: AND(*(e.key == value)) | |
@param {String} [options.query=''] If passed this string will be | |
searched in a "like" manner through every property of the server side model. | |
@param {Array} [options.fields=[]] if passed, this fields will be used | |
to refine the search term in the "search" parameter. | |
@param {Function} [callback] Called when the sync operation finishes. | |
@param {Error} callback.err If an error occurred, this parameter will | |
contain the error. If the sync operation succeeded, _err_ will be | |
falsy. | |
@param {Any} callback.response The server's response. This value will | |
be passed to the parse() method, which is expected to parse it and | |
return an array of model attribute hashes. | |
@chainable | |
**/ | |
/** | |
Mark a model as to be destroyed in the next store operation; if that | |
operation is successful, the model will be actually destroyed. | |
@method destroyModel | |
**/ | |
destroyModel: function(model) { | |
var td = this.toDestroy, | |
clientId = model.get('clientId'); | |
if (td.indexOf(clientId) == -1) { | |
td.push(clientId); | |
} | |
}, | |
save: function (options, callback) { | |
var self = this; | |
// Allow callback as only arg. | |
if (typeof options === 'function') { | |
callback = options; | |
options = {}; | |
} | |
this.sync('write', options, function (err, response) { | |
if (!err) { | |
self.reset(self.parse(response), options); | |
} | |
callback && callback.apply(null, arguments); | |
}); | |
return this; | |
}, | |
/** | |
* Syncs with the server; if saving, but there's nothing to save, | |
* runs the callback immediately. Implementers should probably override | |
* this method to match their server environment. | |
* | |
* @method sync | |
* @param {String} action Which action (read|write) | |
* @param {Object} options | |
* @param {callback} [callback] Called when the sync operation finishes. | |
* @param {Error|null} callback.err If an error occurred, this parameter will | |
* contain the error. If the sync operation succeeded, _err_ will be | |
* `null`. | |
* @param {Any} callback.response This is the object response: it should be | |
* an array with objects that will update the entity list. | |
* | |
* @return void | |
*/ | |
sync: function(action, options, callback) { | |
var euri = encodeURIComponent, | |
data = [ | |
//sub(this.model.prototype.baseUrl, YUI.Env.png), | |
this.model.prototype.baseUrl, | |
'function=many', | |
'model='+this.model.prototype.entityName | |
]; | |
if (action === 'read') { | |
if (options.filter) { | |
data.push('filter='+euri(Y.JSON.stringify(options.filter))); | |
} | |
if (options.query) { | |
data.push('query='+euri(options.search)); | |
} | |
if (options.fields) { | |
data.push('fields='+euri(options.fields.join(","))); | |
} | |
// This method just wrap an IO and a JSON.parse to handle json responses. | |
// FIXME | |
Y.png.ajax('/', { | |
method: 'get', | |
data: data.join("&"), | |
success: function(json) { callback(null, json.payload); }, | |
failure: function(json) { callback(json.payload, null); } | |
}); | |
} else if (action === 'write') { | |
var load = [], | |
remove = [], | |
entities = [], | |
GO = false; | |
this.each(function(m, i) { | |
var changes = m.asPost(); | |
if (changes) { | |
load.push(changes); | |
entities.push(m); | |
} | |
}); | |
GO = GO || load.length > 0; | |
if (load.length) { | |
data.push('load='+encodeURIComponent(Y.JSON.stringify(load))); | |
} | |
Y.Array.each(this.toDestroy, function(clientId, i) { | |
var m = this.getByClientId(clientId); | |
if (! m.isNew()) { | |
remove.push(m.get('id')); | |
} | |
}, this); | |
GO = GO || remove.length > 0; | |
if (remove.length) { | |
data.push('remove='+encodeURIComponent(Y.JSON.stringify(remove))); | |
} | |
if (! GO) { | |
callback(null, []); | |
return; | |
} | |
// This method just wrap an IO and a JSON.parse to handle json responses. | |
// FIXME | |
Y.png.ajax('/', { | |
method: 'post', | |
data: data.join("&"), | |
success: function(json) { | |
Y.Array.each(json.payload, function(k, i) { | |
// cfr.: http://yuilibrary.com/yui/docs/api/classes/Model.html#property_changed | |
entities[i].setAttrs(k, {silent: true}); | |
entities[i].changed = {}; | |
}); | |
Y.Array.each(remove, function(id) { | |
this.getById(id).destroy(); | |
}, this); | |
this.toDestroy = []; | |
callback(null, json.payload); | |
}, | |
failure: function(json) { | |
callback(json.payload, null); | |
}, | |
context: this | |
}); | |
} else { | |
callback('Unsupported sync action: ' + action); | |
} | |
} | |
}); | |
M.Entity.withModel = function(fun) { | |
/** | |
* Decorates a method for a DOM event callback on a View to allow | |
* the decorated method to receive the model as the second argument. | |
* It should be noted that the DOM target (e.target) has to be descendent | |
* of an element with data-id = entity.clientId. | |
* | |
* @example: | |
* openDetail: Y.png.Entity.withModel(function(e, model) { | |
* // do stuff with your model. | |
* }), | |
* | |
* @method withModel | |
* @param fun {Function} The decorated function | |
* | |
*/ | |
return function(e) { | |
var id = e.target.ancestor('[data-id]').getAttribute('data-id'), | |
model = this.modelList.getByClientId(id); | |
fun.call(this, e, model); | |
}; | |
}; | |
}, '1.0', {requires: ['model', 'model-list', 'png-ajax', 'substitute', 'json']}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment