PDA

View Full Version : ResourcesStore (linking stores)



DiMarcello
8 Jul 2008, 8:19 AM
Because needed, and not found anywhere, i made my own store extention

Short intro, i use ruby on rails, and had big trouble with the stores. I wanted to use the store as a client backend to the active records. I first managed, with a little help from an ext plugin, to create stores with the associations as simulated fields. This works fine, until several models associate, but my app is a little more complex then that.
Eg:
Person
Employee belongs_to person
Pupil belongs_to person
now, when i update a employee, the views attached to the pupil store, don't update.
So, i needed something new.

I read a piece about querystore, which came closer to my needs, but was too complicated the other way around (it wants to use sql). Next i read something about a object store, what gave me my last push in the right direction, especially his fixes.js, which fixes the grid, not using record.data[key] but record.get(key)

And now, after 2 days work, i finally have a working solution.
I dont have comments yet, maybe soon i'll post them.

How to use it?

new Ext.ux.data.ResourcesStore({
resource: 'persons', (also the storeId if not given)
singular: 'person', (to save a record)
autoLoad: true
}

new Ext.ux.data.ResourcesStore({
resource: 'pupils', (also the storeId if not given)
singular: 'pupil', (to save a record)
autoLoad: true
}

the data returned by the server on load of pupils:
{metaData: {fields: [...,{name: 'person_id', type: 'int'},]}, associations: [{
name: "person",
key: "person_id",
store: "persons",
type: Ext.ux.data.Resource.BELONGS_TO
}]

now if i need the name of a pupil,
just call : record.get('person.name')
or more complicated: record.get('person.car.manufacturer.name')

Still to do:
implement has_and_belongs_to_many

ENJOY!

ps. as last words, i would like to say something to developers, please be consistent in coding extjs:
If something uses a store, use get/set functions
Change DataReader to allow using a custom Record, now you have to copy alot of code, to override this functionality


/**
* @author DiMarcello
*/
Ext.ns("Ext.ux.data");

Ext.ux.data.StoreSyncronizer = new (Ext.extend(function(){
this.syncs = [];
this.fires = [];
}, {
fire: function(event, store){
if(this.hasFired(store))
return;
this.fires.push(store);
var ts = this.getTargets(store);
var args = Array.prototype.slice.call(arguments, 2);
args.unshift(event);
for(var i = 0; i < ts.length; i++){
ts[i].fireEvent.apply(ts[i], args);
}
this.fires.remove(store);
},

hasFired: function(store){
for(var i = 0; i < this.fires.length; i++){
if(this.fires[i] === store)
return true;
}
return false;
},

add: function(trigger, target, events){
trigger = this.store(trigger);
target = this.store(target);
if(typeof events == 'string')
events = [events];
if(!trigger || !target || this.has(trigger, target))
return;
var s = this.getSync(trigger);
if(s) {
s.targets.push(target);
}else {
this.syncs.push({trigger: trigger,targets: [target]})
}
for(var i = 0; i < events.length; i++){
trigger.on(events[i], this.fire.createDelegate(this, [events[i], trigger], 0), this)
}
},

getSync: function(trigger){
for(var i = 0; i < this.syncs.length; i++){
if(this.syncs[i].trigger === trigger)
return this.syncs[i];
}
},

getTargets: function(trigger){
var s = this.getSync(trigger);
return s ? s.targets : undefined;
},

has: function(trigger, target){
var ts = this.getTargets(trigger);
if(ts){
for(var i = 0; i < ts.length; i++){
if(ts[i] == target)
return true;
}
}
return false;
},

store: function(store){
return Ext.StoreMgr.lookup(store);
}
}))();

/*
* association: {
* name: "persoon",
* key: "persoon_id",
* store: "personen",
* type: Ext.ux.data.Resource
* }
*/

Ext.ux.data.Resource = Ext.extend(Ext.data.Record, {
join: function(store){
this.store = store;
this.store.bindAssociations(this.associations);
},

/**
* Set the named field to the specified value.
* @param {String} name The name of the field to set.
* @param {Object} value The value to set the field to.
*/
set: function(name, value){
var names = name.split(".");
if(names.length > 1)
this._get(names.shift()).set(names.join('.'));
else
this._set(name, value);
},

_set : function(name, value){
if(String(this.data[name]) == String(value)){
return;
}
this.dirty = true;
if(!this.modified){
this.modified = {};
}
if(typeof this.modified[name] == 'undefined'){
this.modified[name] = this.data[name];
}
this.data[name] = value;
if(!this.editing && this.store){
this.store.afterEdit(this);
}
},

/**
* Get the value of the named field.
* @param {String} name The name of the field to get the value of.
* @return {Object} The value of the field.
*/
get: function(name){
var names = name.split(".");
var first = this._get(names.shift());
return first && first.get && names.length > 0 ?
first.get(names.join('.')) :
first;
},

_get: function(name){
if(this.data[name])
return this.data[name];
var a = this.constructor.getAssociation(name);
if (a) {
var store = Ext.StoreMgr.lookup(a.store), id;
if (a.through){
var t = this.get(a.through);
return t ? t.get(name) : undefined;
}else{
switch (a.type) {
case Ext.ux.data.Resource.BELONGS_TO:
return store.getById(this.data[a.key])
case Ext.ux.data.Resource.HAS_ONE:
id = store.find(a.key, this.data.id);
return id ? store.getAt(id) : undefined;
case Ext.ux.data.Resource.HAS_MANY:
id = 0;
var i = 0, rl = [];
while (id = store.find(a.key, this.data.id, id)) {
rl.push(store.getAt(id));
}
return rl;
case Ext.ux.data.Resource.HAS_AND_BELONGS_TO_MANY:
// TODO
break;
}
}
}
}
});

Ext.ux.data.Resource.BELONGS_TO = 'belongs_to';
Ext.ux.data.Resource.HAS_ONE = 'has_one';
Ext.ux.data.Resource.HAS_MANY = 'has_many';
Ext.ux.data.Resource.HAS_AND_BELONGS_TO_MANY = 'has_and_belongs_to_many';


Ext.ux.data.Resource.create = function(o, associations){
var f = Ext.extend(Ext.ux.data.Resource, {});
var p = f.prototype;

p.fields = new Ext.util.MixedCollection(false, function(field){
return field.name;
});
for (var i = 0, len = o.length; i < len; i++) {
p.fields.add(new Ext.data.Field(o[i]));
}
f.getField = function(name){
return p.fields.get(name);
};

p.associations = new Ext.util.MixedCollection(false, function(association){
return association.name;
});
if (associations){
for (var i = 0; i < associations.length; i++) {
p.associations.add(associations[i]);
}
}
f.getAssociation = function(name){
return p.associations.get(name);
}

return f;
};

Ext.ux.data.ResourcesReader = Ext.extend(function(meta, recordType, associations){
/**
* This DataReader's configured metadata as passed to the constructor.
* @type Mixed
* @property meta
*/
this.meta = meta = {};
if(!recordType)
recordType = meta.fields;
if(!associations)
associations = meta.associations;
this.recordType = Ext.isArray(recordType) ?
Ext.ux.data.Resource.create(recordType, associations) : recordType;
}, Ext.data.JsonReader, {
/**
* Create a data block containing Ext.data.Records from a JSON object.
* @param {Object} o An object which contains an Array of row objects in the property specified
* in the config as 'root, and optionally a property, specified in the config as 'totalProperty'
* which contains the total size of the dataset.
* @return {Object} data A data block which is used by an Ext.data.Store object as
* a cache of Ext.data.Records.
*/
readRecords : function(o){ // a dumb function, only needed to change one line!!!
/**
* After any data loads, the raw JSON data is available for further custom processing. If no data is
* loaded or there is a load exception this property will be undefined.
* @type Object
*/
this.jsonData = o;
if(o.metaData){
delete this.ef;
this.meta = o.metaData;
this.recordType = Ext.ux.data.Resource.create(o.metaData.fields, o.associations);
this.onMetaChange(this.meta, this.recordType, o);
}
var s = this.meta, Record = this.recordType,
f = Record.prototype.fields, fi = f.items, fl = f.length;

// Generate extraction functions for the totalProperty, the root, the id, and for each field
if (!this.ef) {
if(s.totalProperty) {
this.getTotal = this.getJsonAccessor(s.totalProperty);
}
if(s.successProperty) {
this.getSuccess = this.getJsonAccessor(s.successProperty);
}
this.getRoot = s.root ? this.getJsonAccessor(s.root) : function(p){return p;};
if (s.id) {
var g = this.getJsonAccessor(s.id);
this.getId = function(rec) {
var r = g(rec);
return (r === undefined || r === "") ? null : r;
};
} else {
this.getId = function(){return null;};
}
this.ef = [];
for(var i = 0; i < fl; i++){
f = fi[i];
var map = (f.mapping !== undefined && f.mapping !== null) ? f.mapping : f.name;
this.ef[i] = this.getJsonAccessor(map);
}
}

var root = this.getRoot(o), c = root.length, totalRecords = c, success = true;
if(s.totalProperty){
var v = parseInt(this.getTotal(o), 10);
if(!isNaN(v)){
totalRecords = v;
}
}
if(s.successProperty){
var v = this.getSuccess(o);
if(v === false || v === 'false'){
success = false;
}
}
var records = [];
for(var i = 0; i < c; i++){
var n = root[i];
var values = {};
var id = this.getId(n);
for(var j = 0; j < fl; j++){
f = fi[j];
var v = this.ef[j](n);
values[f.name] = f.convert((v !== undefined) ? v : f.defaultValue, n);
}
var record = new Record(values, id);
record.json = n;
records[i] = record;
}
return {
success : success,
records : records,
totalRecords : totalRecords
};
}
});

Ext.ux.data.ResourcesStore = Ext.extend(function(config){
if (config.resource) {
this.resourceUrl = '/' + encodeURI(config.resource);
config.url = this.resourceUrl + '.json';
if(!config.storeId && !Ext.StoreMgr.lookup(config.resource))
config.storeId = config.resource;
}
Ext.applyIf(config, {
proxy: !config.data ? new Ext.data.HttpProxy({url: config.url}) : undefined,
reader: new Ext.ux.data.ResourcesReader(config, config.fields, config.association)
});
Ext.ux.data.ResourcesStore.superclass.constructor.call(this, config);
this.on('load', function(){ this.loading = false; this.loaded = true; }, this);
}, Ext.data.Store, {
autoCommit: true,

bindAssociations: function(associations){
if(this.associationsBound)
return;
associations.each(function(a){
var s = Ext.StoreMgr.lookup(a.store);
Ext.ux.data.StoreSyncronizer.add(this, s, ['load', 'update']);
}, this);
this.associationsBound = true;
},

load: function(){
if(!this.loading){
this.loading = true;
delete this.loaded;
Ext.ux.data.ResourcesStore.superclass.load.call(this);
}
},

recordUrl: function(rec){
return (this.resourceUrl + '/' + encodeURI(this.reader.getId(rec)) + '.json')
},

// private
afterEdit : function(record){
Ext.ux.data.ResourcesStore.superclass.afterEdit.call(this, record);
if(this.autoCommit)
this.commitChanges();
},

commitChanges: function(){
var m = this.modified.slice(0);
this.modified = [];
for(var i = 0, len = m.length; i < len; i++){
var p = {_method: 'PUT'};
p[this.singular] = m[i].getChanges();
Ext.Ajax.request({
url: this.recordUrl(m[i]),
params: DiMarcello.urlEncode(p), // I needed a better encode for object, which allows nesting (rails accepts defaultly {person: {name: 'bla', foo: 'bar'}})
success: function(request, options){
this.commit();
},
failure: function(request, options){
this.reject();
},
scope: m[i]
});
}
}
});

jclawson
8 Jul 2008, 8:43 AM
We have developed something similar called StoreLink. It is a little less complicated than this. Also, the server doesn't have to do anything special. The relationships between data are setup on the client.

There is going to be a serious problem with these UX extensions. We also have a utility called StoreSynchronizer which does something completely different than yours. It synchronizes changes from one store to another and is able to filter the changes based on a callback.

Also... here is a link to my QueryStore idea (http://extjs.com/forum/showthread.php?t=36328) for those who are interested.

DiMarcello
8 Jul 2008, 8:50 AM
lol.

Well as said above, i read your post, but couldn't find the code, i think i even read that you didnt had the time to build it. But well, your concept wasnt exactly what i needed, as this isn't what you need... Just keep on coding

jclawson
8 Jul 2008, 1:17 PM
Yeah :-P no I havn't even attempted to tackle developing QueryStore... that is a very difficult problem. In my post I mention using StoreLink and StoreSynchronizer to acheive the same thing that my example SQL query would do. We haven't released the code for StoreLink or StoreSynchronizer yet. We might feature some of this code in our upcoming Ext spotlight article.