PDA

View Full Version : Ext.ux.data.StoreLink



ambience
13 Dec 2007, 3:04 PM
I started an extension yesterday for ControlPath and I thought it could perhaps be userful for others. Basically, I have a grouped Grid that groups based on a value not directly included in the data it is showing. I have the ID of an related element with the attribute I would like to group on. Further more, the desired attribute COULD change in the other store and I want the group names to update to reflect this change.

As a result, I've created a class that borrows from the idea of foreign keys. Using it, you can link to stores together in a way such that one table has 'psuedo-fields' that do not actually exist in your data source but are populated from a second store and are updated with the second store changes, or the key value changes.

StoreLink.js:


Ext.namespace("Ext.ux.data");
/**
* Establishes a relationship between two tables, the source (store) and the destination(dst) such that specified
* fields in the destination store will be set to values found in the source store. Values are selected from the
* source store based on the foreign key config parameter, if more then one record in the source table has the same
* foreign key value the behavior of the link is not assured.
*/
Ext.ux.data.StoreLink = function(cfg){
Ext.ux.data.StoreLink.superclass.constructor.call(this);

this.src = Ext.type(cfg.store) == "string" ? Ext.StoreMgr.get(cfg.store) : cfg.store;
this.dst = Ext.type(cfg.dst) == "string" ? Ext.StoreMgr.get(cfg.dst) : cfg.dst;
this.localKey = Ext.type(cfg.fk) == "string" ? cfg.fk : cfg.fk.local;
this.foreignKey = Ext.type(cfg.fk) == "string" ? null : cfg.fk.remote;

var fields = [];
Ext.each([].concat(cfg.fields), function(field){
fields.push({
local: field.local || field,
remote: field.remote || field
});
});
this.fields = fields;

this.addEvents(
/**
* @event update
* Fires when a link was established and the value copied. Links that result in no value change will not
* fire this event.
* @param {StoreLink} this
* @param {Ext.data.Record} source The source table record
* @param {Ext.data.Record} destination The destination record
* @param {Object} value The value copied
* @param {String} sourceField The source field
* @param {String} destField The destination field
*/
'update'
);

this.initEvents();
};
Ext.extend(Ext.ux.data.StoreLink, Ext.util.Observable, {
/**
* @cfg {String/Ext.data.Store} store The store that holds the values to be copied to the dst store. If the
* provided value is a String it is assumed to be a store in the Ext.StoreMgr.
*/
/**
* @cfg {String/Ext.data.Store} dst The store that values from the source store are to be copied too.If the
* provided value is a String it is assumed to be a store in the Ext.StoreMgr.
*/
/**
* @cfg {String/Object} fk The foreign key configuration. foreign keys are used to link each record in the
* destination store to a record in the source store. The relationship should be one-to-many from the source to
* the destination. If the value is a string, it will be used as the local key field and linked against the
* source store's default ID field. If an object is passed it must have the 'remote' and 'local' properties to
* indicate what keys are to be used on each side of the relationship.
*/
/**
* @cfg {String/Object/Array[String/Object]} fields The field link specification. This can be either a single
* field specification or a list of specifications. Each specification can be a String if both the source store
* and destination store fields share the same name, or it can be an object that contains the fields 'local' and
* 'remote' to indicate the linked fields in the respective stores.
*/

//private
initEvents: function(){
this.dst.on({
'update': function(store, record, action){
if(action == Ext.data.Record.EDIT)
this.evaluate(record);
},
'load': function(store, records){
if(this.evaluate(records))
store.fireEvent("datachanged ", store);
},
'add': function(store, records){
if(this.evaluate(records))
store.fireEvent("datachanged ", store);
},
scope: this
});
this.src.on({
'update': function(store, record, action){
if(action == Ext.data.Record.EDIT && this.evaluate(this.getEffectedRows(record)))
this.dst.fireEvent("datachanged ", this.dst);
},
'load': function(store, record, action){
var changed = false;

this.dst.each(function(record){
changed = this.evaluate(record) || changed;
}, this);

if(changed)
this.dst.fireEvent("datachanged ", this.dst);
},
scope: this
});
},

/**
* Force a full re-evaluation of the destination store.
*/
refresh: function(){
this.dst.each(this.evaluate.createDelegate(this, [], true));
},

/**
* Scan the destination store for records linked to the provided remoteRec from the source store.
*
* @return Array[Ext.data.Record] Returns a list of destination records linked to the provided remoteRec.
*/
getEffectedRows: function(remoteRec){
var effected = [];
var id = !this.foreignKey ? remoteRec.id : remoteRec.get(this.foreignKey);

this.dst.each(function(record){
if(record.get(this.localKey) == id)
effected.push(record);
})

return effected;
},

/**
* Attempts to resolve any of the linked fields in the provided record to their values in the source store.
* If an update to a field is found, the underlying record value will be updated by the field will NOT be
* flagged as modified and no update event will be fired.
*
* @return Boolean true if the evaluation resulted in a one or more field updates.
*/
evaluate: function(record){
var changed = false;
if(Ext.type(record) == "array"){
Ext.each(record, function(r){
changed = this.evaluate(r) || changed;
}, this);

return changed;
};

var src = !this.foreignKey ?
this.src.getById(record.get(this.localKey)) :
this.src.getAt(this.src.find(this.foreignKey, record.get(this.localKey)));

Ext.each(this.fields, function(field){
var val = src ? src.get(field.remote) : null;
if(record.data[field.local] != val){
this.fireEvent("update", this, src, record, val, field.local, field.remote);
record.data[field.local] = val;
changed = true;
}
}, this);

return changed;
}
});


I have also created a custom store that, along with a couple of other features, supports the idea of multiple links. The following lines appear in the constructor:


if(cfg.links){
var links = this.links = [];
[].concat(cfg.links).each(function(link){
links.push(new CP.data.StoreLink(Ext.apply({dst: this}, link)));
}.bind(this));
}


Which allows me to do something like:


// A grouping extension of the custom store.
new CP.data.GroupingStore({
url: '...',
reader: new CP.data.JsonReader({},['active', 'id', 'permissionId', 'permissionName', 'relations', 'direction', 'templates', 'scope']),
links: {
store: 'permissions',
fk: {remote: 'id', local: 'permissionId'},
fields: [{local: 'permissionName', remote: 'displayName'}, 'scope']
},
sortInfo: {field: 'permissionName', direction: 'ASC'},
groupField: 'scope',
autoLoad: true,
listeners: {
"load": this.onLoad,
scope: this
}
})

*The permission name and store fields do not actually appear in the returned JSON but are populated by the link
*No I can not share any of the CP classes, they provide exactly the same functionality as their Ext counterparts with some additional functionality not leveraged by this extension.

For those of you wondering why I didn't write this into record and have the get value fetch update values live. I chose to copy the data as the linked store would not properly sort (or group) on data that was not actually in the collection (as it does not use getters when sorting). Additionally, it minimizes the number of classes I have to share and the Ext.data.Record is a little difficult to provide extension class for due to the factory method.

This is fresh off the presses, as it were, so I am sure it has some bugs. If people like it, I will post bug fixes as I make them. Also, we use prototype, but I've tried to remove any non-ext code. Please let me know if I've missed something.

krycek
19 Dec 2007, 8:50 AM
I have understood the idea and I think it is very useful. I'm in a cenario where it would prevent me to write a lot of code. But I dont understood how to use it.



links: {
store: 'permissions',
fk: {remote: 'id', local: 'permissionId'},
fields: [{local: 'permissionName', remote: 'displayName'}, 'scope']
},

I can have more than one link, cant I? All right, and these links are another stores, right? Where is the another store in the code?

Sorry, but I dont get it :(

If you had an example it would be great.

Thank you

ambience
22 Dec 2007, 9:41 AM
Sorry, 'permissions' is a Ext.data.Store registered in the Ext.StoreMgr singleton in the example. You can provide an actual store object to the 'store' parameter and that will be used. And yes, you can link several fields.