Below is a renderer for an Ext.grid.Panel column. Suppose contactStore has 2,000 values in it and all I care about is the name of the record based on the id (value parameter in this case), and my grid only has 25 rows/records in it for the page I'm on. How can I dynamically get the store so that I grab the relevant associated records (based on the foreign key) of the id of my grid column, rather than loading all 2,000 records? Is there a way to load the store and then in the callback, somehow have this "renderer" function display the values after the callback succeeded?
columns: [{
...
}, {
header: 'Contact Name',
flex: 1,
sortable: true,
dataIndex: 'contact_id',
renderer: function(value) {
var contactStore = Ext.StoreManager.lookup('Contacts');
return contactStore.getById(value).get('full_name');
}
}, {
You can adjust the collectData(records, startIndex) in the viewConfig for that:
Ext.create('Ext.grid.Panel', {
(...)
viewConfig: {
//this method needs to be adjusted
collectData: function(records, startIndex) {
var me = this;
//we can use a custom function for this
if (me.onBeforeCollectData) {
me.onBeforeCollectData(records);
}
var data = me.superclass.collectData.call(me, records, startIndex);
return data;
},
onBeforeCollectData: function(records) {
var newExtraParams = [];
var oldExtraParams;
var needToLoadStore = false;
var contactStore = Ext.StoreManager.lookup('Contacts');
if (contactStore) {
oldExtraParams = contactStore.oldExtraParams;
} else {
//don't use autLoad: true, this will be a local store
contactStore = Ext.create('Ext.data.Store', {
storeId:'Contacts',
(...)
});
needToLoadStore = true;
}
for (var x in records) {
//contact_id is read out here
var param = records[x].get('contact_id');
if (param) {
if (needToLoadStore == false && Ext.Array.contains(oldExtraParams, param) == false) {
needToLoadStore = true;
}
newExtraParams.push(param);
}
}
if (needToLoadStore == true) {
//we use this to load the store data => because of async: false property
Ext.Ajax.request({
scope: this,
//this is for synchronous calls
async: false,
url: (...),
method: (...),
params: newExtraParams,
success: function (res, opt) {
var responseObj = Ext.decode(res.responseText, false);
contactStore.loadData(responseObj); //or deeper in the responseObj if needed
contactStore.oldExtraParams = newExtraParams;
}
});
}
}
}
});
I have found as I am developing extjs applications (rally applications) that sometimes the data I need from a record is in record.raw and not in record.data. What is the difference between the two, and why might this be the case?
EDIT - adding example (a column for the parent that is sortable - one of my other questions)
{text: 'Parent', dataIndex: 'Parent',
doSort: function(state) {
var ds = this.up('grid').getStore();
var field = this.getSortParam();
ds.sort({
property: field,
direction: state,
sorterFn: function(v1, v2){
if (v1.raw.Parent) {
v1 = v1.raw.Parent.Name;
} else {
v1 = v1.data.Name;
}
if (v2.raw.Parent) {
v2 = v2.raw.Parent.Name;
} else {
v2 = v2.data.Name;
}
return v1.localeCompare(v2);
}
});
},
renderer: function(value, meta, record) {
var ret = record.raw.Parent;
if (ret) {
return ret.Name;
} else {
meta.tdCls = 'invisible';
return record.data.Name;
}
}
},
The data in raw is the raw data that has yet to be converted into the types specified in the fields config. Once a Reader does that conversion, the converted data is stored in data, which is typed based on the fields.
You can learn more from the docs about these here: http://docs.sencha.com/extjs/4.2.1/#!/api/Ext.data.Model
I have created a custom xtype for multiselect, but i am not able to understand what changes i need to perform to save the values as a string array instead of comma delimited string.
Currently it is storing the values as follows
Property industry
Type String
Value government,healthcare
Instead, i want to save the information as follows
Property industry
Type String[]
Value government,healthcare
Any suggestions, pointers highly appreciated.
CQ.Ext.form.Multiselect = CQ.Ext.extend(CQ.Ext.form.Field, {
store:null,
storeUrl:'',
displayField:'text',
valueField:'value',
allowBlank:true,
minLength:0,
blankText:CQ.Ext.form.TextField.prototype.blankText,
copy:false,
allowDup:false,
allowTrash:false,
legend:null,
focusClass:undefined,
delimiter:',',
view:null,
dragGroup:null,
dropGroup:null,
tbar:null,
appendOnly:false,
sortField:null,
sortDir:'ASC',
defaultAutoCreate : {tag: "div"},
initComponent: function(){
CQ.Ext.form.Multiselect.superclass.initComponent.call(this);
this.addEvents({
'dblclick' : true,
'click' : true,
'change' : true,
'drop' : true
});
},
onRender: function(ct, position){
var fs, cls, tpl;
CQ.Ext.form.Multiselect.superclass.onRender.call(this, ct, position);
cls = 'ux-mselect';
fs = new CQ.Ext.form.FieldSet({
renderTo:this.el,
title:this.legend,
height:this.height,
width:this.width,
style:"padding:1px;",
tbar:this.tbar
});
if(!this.legend){
//fs.el.down('.'+fs.headerCls).remove();
fs.body.addClass(cls);
}
tpl = '<tpl for="."><div class="' + cls + '-item';
if(CQ.Ext.isIE || CQ.Ext.isIE7 || CQ.Ext.isOpera )tpl+='" unselectable=on';
else tpl+=' x-unselectable"';
tpl+='>{' + this.displayField + '}</div></tpl>';
this.store = new CQ.Ext.data.JsonStore({
autoload:true,
url: CQ.HTTP.externalize(this.storeUrl),
fields:['value','text']
});
this.store.load();
this.view = new CQ.Ext.ux.DDView({
multiSelect: true, store: this.store, selectedClass: cls+"-selected", tpl:tpl,
allowDup:this.allowDup, copy: this.copy, allowTrash: this.allowTrash,
dragGroup: this.dragGroup, dropGroup: this.dropGroup, itemSelector:"."+cls+"-item",
isFormField:false, applyTo:fs.body, appendOnly:this.appendOnly,
sortField:this.sortField, sortDir:this.sortDir
});
fs.add(this.view);
this.view.on('click', this.onViewClick, this);
this.view.on('beforeClick', this.onViewBeforeClick, this);
this.view.on('dblclick', this.onViewDblClick, this);
this.view.on('drop', function(ddView, n, dd, e, data){
return this.fireEvent("drop", ddView, n, dd, e, data);
}, this);
this.hiddenName = this.name;
var hiddenTag={tag: "input", type: "hidden", value: "", name:this.name};
if (this.isFormField) {
this.hiddenField = this.el.createChild(hiddenTag);
} else {
this.hiddenField = CQ.Ext.get(document.body).createChild(hiddenTag);
}
fs.doLayout();
},
initValue:CQ.Ext.emptyFn,
onViewClick: function(vw, index, node, e) {
var arrayIndex = this.preClickSelections.indexOf(index);
if (arrayIndex != -1)
{
this.preClickSelections.splice(arrayIndex, 1);
this.view.clearSelections(true);
this.view.select(this.preClickSelections);
}
this.fireEvent('change', this, this.getValue(), this.hiddenField.dom.value);
this.hiddenField.dom.value = this.getValue();
this.fireEvent('click', this, e);
this.validate();
},
onViewBeforeClick: function(vw, index, node, e) {
this.preClickSelections = this.view.getSelectedIndexes();
if (this.disabled) {return false;}
},
onViewDblClick : function(vw, index, node, e) {
return this.fireEvent('dblclick', vw, index, node, e);
},
getValue: function(valueField){
var returnArray = [];
var selectionsArray = this.view.getSelectedIndexes();
if (selectionsArray.length == 0) {return '';}
for (var i=0; i<selectionsArray.length; i++) {
returnArray.push(this.store.getAt(selectionsArray[i]).get(((valueField != null)? valueField : this.valueField)));
}
return returnArray;
},
setValue: function(values) {
var index;
var selections = [];
this.view.clearSelections();
this.hiddenField.dom.value = '';
if (!values || (values == '')) { return; }
if (!(values instanceof Array)) { values = values.split(this.delimiter); }
for (var i=0; i<values.length; i++) {
index = this.view.store.indexOf(this.view.store.query(this.valueField,
new RegExp('^' + values[i] + '$', "i")).itemAt(0));
selections.push(index);
}
this.view.select(selections);
this.hiddenField.dom.value = values;
for (var i=0; i<values.length; i++) {
this.listOfIndustries=values[i];
alert(values[i]);
}
this.validate();
},
getRawValue: function(valueField) {
var tmp = this.getValue(valueField);
if (!tmp) {
tmp = [];
}
return tmp;
},
setRawValue: function(values){
setValue(values);
},
validateValue : function(value){
if (value.length < 1) { // if it has no value
if (this.allowBlank) {
this.clearInvalid();
return true;
} else {
this.markInvalid(this.blankText);
return false;
}
}
if (value.length < this.minLength) {
this.markInvalid(String.format(this.minLengthText, this.minLength));
return false;
}
if (value.length > this.maxLength) {
this.markInvalid(String.format(this.maxLengthText, this.maxLength));
return false;
}
return true;
}
});
CQ.Ext.reg("industriesmultiselect", CQ.Ext.form.Multiselect);
Envionment CQ 5.5
Short answer:
Instead of using the one hidden field to store your values, you need to use multiple underlying input elements, each having the same name property for the Sling Post Servlet to interpret the output as a multi-valued property. See the multifield widget's setValue and addItem methods at /libs/cq/ui/widgets/source/widgets/form/MultiField.js for an example of dynamically adding new fields.
Longer explanation:
It looks like your getValue does what you expect, but the problem is that that method isn't getting called to provide the value that gets submitted. If you're using this widget in a standard dialog, the parent form panel submits the values that are specified in the input elements beneath it in the DOM hierarchy.
In other words, you need to apply your multiple values to DOM elements.
The CQ.Ext.form.Field that you're extending only defines one underlying input element, which you're trying to set with your values array in setValue:
this.hiddenField.dom.value = values;
and in onViewClick
this.hiddenField.dom.value = this.getValue();
Since hiddenField is an input tag of type 'hidden', it holds a string value and when you try to set it this way, you're actually storing the result of calling toString() on your values array. This is why you end up with one string of comma separated values getting submitted.
You'll need to maintain a whole set of hidden fields if you want this widget to work with the standard form submission infrastructure. Alternatively, you could implement your own submit event listener wherever appropriate and use Ext or jQuery to POST an AJAX request with your array (directly from getValue()) as one of the parameters.
Is there any way to get ISO-dates parsed as javascript-dates om Backbone models.
Let's say some JSON returns model data like this:
{ prop1: "somevalue", date: "2011-05-11T18:30:00" }
To make this work as a date, i came up with this:
var Model = exports.Model = Backbone.Model.extend({
toJSON: function () {
return _.extend(this.attributes, {
date: new Date(this.attributes.date)
});
}
});
This works fine, and makes it possible to write template-markup as this:
"<%=date.toLocaleDateString() %>"
Its all fine, but couldn't it be done at the BackboneModel prototype somehow. This way i have to write this implementation, on every model property of type date - thats not very DRY
Because the JSON ISO8601-formatted dates are just strings, there is no magical way of just parsing them. You need to either declare or detect the fields which should be parsed.
Declaration is easy, just define an array of date fields on the model an parse each of them.
//declare dateFields on the model
var TestModel = Backbone.Model.extend({
dateFields: [
'createdDate',
'updatedDate'
]
});
//override toJSON
Backbone.Model.prototype.toJSON = function() {
return this._parseDates(this.attributes);
};
//hydrates string dates to Date objects
Backbone.Model.prototype._parseDates = function(attrs) {
attrs = _.clone(attrs);
if(!this.dateFields) { return attrs; }
_.each(this.dateFieds, function(field) {
attrs[field] = new Date(attrs[field]);
});
return atts;
};
That requires you to tell the date parser which fields to consider. Since you were hoping for something more automagical, the other option is to try to detect which fields are dates by looking at the shape of each string value:
//hydrates string dates to Date objects
Backbone.Model.prototype._parseDates = function(attrs) {
attrs = _.clone(attrs);
var iso8601Pattern = /^[0-9][0-9][0-9][0-9](-[0-1][0-9](-[0-3][0-9](T[0-9][0-9](:[0-9][0-9](:[0-9][0-9])(\.[0-9][0-9][0-9]?)?)?)?)?)?Z$/;
_.each(attrs, function(value, key) {
if(_.isString(value) && iso8601pattern.test(value)) {
attrs[key] = new Date(value);
}
});
return attrs;
};
Which doesn't require you to list all the date fields, but will add overhead to parsing, because it needs to test each value to see if it looks like a date.
I have the following code to filter a grid from values inputed in a form on a click of a button. The problem is that the first time the filters are activated, only the first filter (displayNameFilter) will be included in the request.
The second time and on, both filters will be included in a request. How can I work around that issue?
var nameFilter = grid.filters.getFilter('name');
if (!nameFilter) {
nameFilter = grid.filters
.addFilter({
type : 'string',
dataIndex : 'name'
});
}
nameFilter.setValue(Ext.getCmp('name-filter').getValue());
var displayNameFilter = grid.filters.getFilter('displayName');
if (!displayNameFilter) {
displayNameFilter = grid.filters
.addFilter({
type : 'string',
dataIndex : 'displayName'
});
}
displayNameFilter.setValue(Ext.getCmp('display-name-filter').getValue());
displayNameFilter.setActive(true, false);
nameFilter.setActive(true, false);
I had a similar problem. The solution is a bit hokey but it works for me, notice that the filter variable is defined a second time within the defer call (it is needed):
grid.filters.createFilters();
var nameFilter = grid.filters.getFilter('name');
if (!nameFilter) {
nameFilter = grid.filters
.addFilter({
type : 'string',
dataIndex : 'name'
});
}
nameFilter.setActive(true);
var displayNameFilter = grid.filters.getFilter('displayName');
if (!displayNameFilter) {
displayNameFilter = grid.filters
.addFilter({
type : 'string',
dataIndex : 'displayName'
});
}
displayNameFilter.setActive(true);
Ext.Function.defer(function() {
nameFilter = grid.filters.getFilter('name');
nameFilter.setValue(Ext.getCmp('name-filter').getValue());
displayNameFilter = grid.filters.getFilter('displayName');
displayNameFilter.setValue(Ext.getCmp('display-name-filter').getValue());
}, 10);
This worked for me:
handler : function() {
var filters = [];
// get filters from a form...
var values = filterForm.getValues();
for (var f in values) {
var value = values[f];
if (value) {
filters.push({ property: f, value: value });
}
}
//...and add all of them to the store used by the grid directly
if (filters.length) {
// prevent events firing here...
gridStore.clearFilter(true);
// ...because they will fire here
gridStore.filter(filters);
} else {
gridStore.clearFilter();
}
}