Skip to content

Instantly share code, notes, and snippets.

@willywongi
Created October 26, 2011 08:04
Show Gist options
  • Save willywongi/1315751 to your computer and use it in GitHub Desktop.
Save willywongi/1315751 to your computer and use it in GitHub Desktop.
My own way of playing with the Y.App framework.
/* 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