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]
});
}
}
});
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]
});
}
}
});