-
18 Feb 2012 8:08 PM #1
New "Multi-select field" extension for ST2 (RC)
New "Multi-select field" extension for ST2 (RC)
I created a new class that extends/overrides Select.js in a minimalistic way to provide a basic "Multi-select" field. It is based on Mitchell Simoens' concepts for his ST 1.1 multi-select field. The component stores the selected items as a comma separated string in the text field portion of itself. It uses a "List" dataview panel to provide a GUI for multiple selection of values. Other than that, it behaves identically to a "Select" field. It is designed to be added to an MVC folder structure, since I don't really consider it a "real" extension at this point. It has been tested in ST2 RC, and it seems to be working well so far. The code is below:
Form field item definition (add this to your "Form Panel" config, note the xtype):
Multiselect class (create a file called "MultiSelect.js" and put it in your "app/view/" folder - then paste in this code - modify as necessary):Code:{ xtype : 'multiselectfield', name : 'Tags', label : 'Tags', store : 'Tags', displayField : 'text', //don't change this property valueField : 'value', //don't change this property usePicker : false //required }
I am sure there are some things that may be better optimized in the code, but this is at least functional and tested. Hope this proves useful to whomever may need it until ST2 comes with its own Multi-select field. Let me know what you think. If there is an interest, I may post it on GitHub at a later date.Code:Ext.define('app.view.MultiSelect', { extend: 'Ext.field.Select', alias : 'widget.multiselectfield', xtype : 'multiselectfield', usePicker : false, //force list panel, not picker getTabletPicker: function() { //override with modified function var config = this.getDefaultTabletPickerConfig(); if (!this.listPanel) { this.listPanel = Ext.create('Ext.Panel', Ext.apply({ centered: true, modal: true, cls: Ext.baseCSSPrefix + 'select-overlay', layout: 'fit', hideOnMaskTap: false, items: { xtype: 'list', mode: 'MULTI', //set list to multi-select mode store: this.getStore(), itemTpl: '<span class="x-list-label">{' + this.getDisplayField() + '}</span>', listeners: { select : this.onListSelect, itemtap : this.onListTap, hide : this.onListHide, //new listener scope : this }, items: { //new button to trigger formatting/setting new field value with joined string xtype: 'button', text: 'Done', ui: 'action', height: '20px', width: '50%', docked: 'bottom', style: 'margin-top: 10px; margin-bottom: 10px; margin-left: auto; margin-right: auto;', listeners: { tap: this.onButtonTap, scope: this } } } }, config)); } return this.listPanel; }, applyValue: function(value) { //override with modified function var record = value, index; this.getOptions(); if (!(value instanceof Ext.data.Model)) { index = this.getStore().find(this.getValueField(), value, null, null, null, true); if (index == -1) { index = this.getStore().find(this.getDisplayField(), value, null, null, null, true); } //We do not want to get record from store //record = this.getStore().getAt(index); this.element.dom.lastChild.firstChild.firstChild.value = value; //display csv string in field when value applied } return record; }, updateValue: function(newValue, oldValue) { //override with modified function this.previousRecord = oldValue; this.record = newValue; // String does not have methods //this.callParent([newValue ? newValue.get(this.getDisplayField()) : '']); this.fireEvent('change', this, newValue, oldValue); }, getValue: function() { //override with modified function var record = this.record; return (record) // Use literal string value of field // ? record.get(this.getValueField()) : null; }, showPicker: function() { //override with modified function //check if the store is empty, if it is, return if (this.getStore().getCount() === 0) { return; } if (this.getReadOnly()) { return; } this.isFocused = true; //hide the keyboard //the causes https://sencha.jira.com/browse/TOUCH-1679 // Ext.Viewport.hideKeyboard(); if (this.getUsePicker()) { var picker = this.getPhonePicker(), name = this.getName(), value = {}; value[name] = this.record.get(this.getValueField()); picker.setValue(value); if (!picker.getParent()) { Ext.Viewport.add(picker); } picker.show(); } else { //reworked code to split csv string into array and select correct list items var listPanel = this.getTabletPicker(), list = listPanel.down('list'), store = list.getStore(), itemStringArray = new Array(), values = this.getValue().split(','), v = 0, vNum = values.length; if (!listPanel.getParent()) { Ext.Viewport.add(listPanel); } for (; v < vNum; v++) { itemStringArray.push(values[v]); } v = 0; for (; v < vNum; v++) { var record = store.findRecord(this.getDisplayField(), itemStringArray[v], 0, true, false, false ); list.select(record, true, false); } listPanel.showBy(this.getComponent()); listPanel.down('list').show(); } }, onListSelect: function(item, record) { //override with empty function }, onListTap: function() { //override with empty function }, onButtonTap: function() { this.setValue(''); this.listPanel.down('list').hide(); //force list hide event this.listPanel.hide({ type : 'fade', out : true, scope: this }); }, onListHide: function(cmp, opts) { var me = this, recordArray = this.listPanel.down('list').selected.items, itemStringArray = new Array(), v = 0, vNum = recordArray.length; for (; v < vNum; v++) { var value = this.getDisplayField(); itemStringArray.push(recordArray[v].data.value); } if (itemStringArray.length > 0) { me.setValue(itemStringArray.join(',')); this.listPanel.down('list').deselectAll(); } else { me.setValue('None'); } } });
Cheers!Last edited by shaneavery; 22 Feb 2012 at 8:19 PM. Reason: Upgraded to RC1
-
19 Feb 2012 7:17 AM #2Sencha - Senior Forum Manager
- Join Date
- Mar 2007
- Location
- St. Louis, MO
- Posts
- 34,121
- Vote Rating
- 453
Two things, you have an id set on the list which is bad for an extension. What if someone uses that id in their app or has two of these fields? They would conflict. Other thing is you should put this up on GitHub so that people can fork it and do pull requests to make it better.
Mitchell Simoens @SenchaMitch
Sencha Inc, Senior Forum Manager
________________
http://www.JSONPLint.com - Source to lint your JSONP!
Check out my GitHub, lots of nice things for Ext JS 4 and Sencha Touch 2
https://github.com/mitchellsimoens
Think my support is good? Get more personalized support via a support subscription. https://www.sencha.com/store/
Need more help with your app? Hire Sencha Services services@sencha.com
Want to learn Sencha Touch 2? Check out Sencha Touch in Action that is almost in print!
When posting code, please use BBCode's CODE tags.
-
19 Feb 2012 1:01 PM #3
Good points, thanks Mitchell...
Good points, thanks Mitchell...
I updated the code above to eliminate the id property and the dependance I unnecessarily created for it. I intend to post a GitHub link to this project on this thread as soon as I get a few more comments. This is my first stab at an extension, so I want a little more initial input in order to update this code before I call this an "official" extension. Your advice is a good start.
Thanks.
-
19 Feb 2012 5:31 PM #4
There's a couple of hard-coded references to the displayfield 'text'. These should be changed to this.getDisplayField().
Code:var record = store.findRecord('text', itemStringArray[v], 0, true, false, false ); itemStringArray.push(recordArray[v].data.text);
-
19 Feb 2012 6:02 PM #5
Thanks DodgyDave...
Thanks DodgyDave...
I updated the code above to reflect your corrections. Nice catch. This is the kind of constructive criticism I am looking for.
Thanks.
-
22 Feb 2012 8:13 PM #6
Updated for ST2 RC
Updated for ST2 RC
FYI...
I upgraded the code to fix a bug for the RC release. This code will not work for Beta3 anymore.
-
1 Apr 2012 7:29 AM #7
Really nice work,
for tablets/phones is it impossible to have a multiselect Picker?
-
12 Jun 2012 8:59 AM #8
It was very useful for me. Thanks!
It was very useful for me. Thanks!
Hello, I modified a little the script to support the DisplayField and ValueField, and get in Value a list of valueField values instead of a list of DisplayFields.
Thank you!
PHP Code:Ext.define('app.view.Multiselect', {
extend: 'Ext.field.Select',
alias : 'widget.multiselectfield',
xtype : 'multiselectfield',
usePicker : false, //force list panel, not picker
getTabletPicker: function() { //override with modified function
var config = this.getDefaultTabletPickerConfig();
if (!(this.listPanel)) {
this.listPanel = Ext.create('Ext.Panel', Ext.apply({
centered: true,
modal: true,
cls: Ext.baseCSSPrefix + 'select-overlay',
layout: 'fit',
hideOnMaskTap: false,
items: {
xtype: 'list',
mode: 'MULTI', //set list to multi-select mode
store: this.getStore(),
//data:this.getOptions(),
itemTpl: '<span class="x-list-label">{' + this.getDisplayField() + '}</span>',
listeners: {
select : this.onListSelect,
itemtap : this.onListTap,
hide : this.onListHide, //new listener
scope : this
},
items: { //new button to trigger formatting/setting new field value with joined string
xtype: 'button',
text: 'Done',
ui: 'action',
height: '20px',
width: '50%',
docked: 'bottom',
style: 'margin-top: 10px; margin-bottom: 10px; margin-left: auto; margin-right: auto;',
listeners: {
tap: this.onButtonTap,
scope: this
}
}
}
}, config));
}
return this.listPanel;
},
applyValue: function(value) { //override with modified function
var record = value,
displayStringArray = new Array(),
valueStringArray = new Array();
valueStringArray = (value == null? '':value.split(','));
if ((value != null )&& value.trim().localeCompare('')){
for (v=0;v <valueStringArray.length;v++){
item = this.getStore().find(this.getValueField(), valueStringArray[v], null, null, null, true);
displayStringArray.push(this.getOptions()[item][this.getDisplayField()]);
}
}
this.element.dom.lastChild.firstChild.firstChild.value = displayStringArray.join(','); //display csv string in field when value applied
return record;
},
updateValue: function(newValue, oldValue) { //override with modified function
this.previousRecord = oldValue;
this.record = newValue;
// String does not have methods //this.callParent([newValue ? newValue.get(this.getDisplayField()) : '']);
this.fireEvent('change', this, newValue, oldValue);
},
getValue: function() { //override with modified function
var record = this.record;
return (record) // Use literal string value of field // ? record.get(this.getValueField()) : null;
},
showPicker: function() { //override with modified function
//check if the store is empty, if it is, return
if (this.getStore().getCount() === 0) {
return;
}
if (this.getReadOnly()) {
return;
}
this.isFocused = true;
//hide the keyboard
//the causes https://sencha.jira.com/browse/TOUCH-1679
// Ext.Viewport.hideKeyboard();
if (this.getUsePicker()) {
var picker = this.getPhonePicker(),
name = this.getName(),
value = {};
value[name] = this.record.get(this.getValueField());
picker.setValue(value);
if (!picker.getParent()) {
Ext.Viewport.add(picker);
}
picker.show();
} else { //reworked code to split csv string into array and select correct list items
var listPanel = this.getTabletPicker(),
list = listPanel.down('list'),
store = list.getStore(),
itemStringArray = new Array(),
values = (this.getValue() == null? '':this.getValue().split(',')),
v = 0,
vNum = values.length;
if (!listPanel.getParent()) {
Ext.Viewport.add(listPanel);
}
for (v = 0; v < vNum; v++) {
itemStringArray.push(values[v]);
}
v = 0;
for (v = 0; v < vNum; v++) {
//var record = store.findRecord(this.getDisplayField(), itemStringArray[v], 0, true, false, false );
var record = store.findRecord(this.getValueField(), itemStringArray[v], 0, true,false,false );
list.select(record, true, false);
}
listPanel.showBy(this.getComponent());
listPanel.down('list').show();
}
},
onListSelect: function(item, record) { //override with empty function
},
onListTap: function() { //override with empty function
},
onButtonTap: function() {
this.setValue('');
this.listPanel.down('list').hide(); //force list hide event
this.listPanel.hide({
type : 'fade',
out : true,
scope: this
});
},
onListHide: function(cmp, opts) {
var me = this,
recordArray = this.listPanel.down('list').selected.items,
//itemStringArray = new Array(),
valueStringArray = new Array(),
v = 0,
vNum = recordArray.length;
for (v = 0; v < vNum; v++) {
if (recordArray[v].data[this.getDisplayField()]){
valueStringArray.push(recordArray[v].data[this.getValueField()]);
}
}
if (valueStringArray.length > 0) {
//me.setValue(itemStringArray.join(','));
me.setValue(valueStringArray.join(','));
this.listPanel.down('list').deselectAll();
} else {
me.setValue(null);
}
}
});
-
3 Sep 2012 8:08 PM #9
Great work, guys!
Great work, guys!
It works pretty nice!
One question: What happens if I want to predefine some values? I used Mitch's ST1 original version long time ago and << value: 'v1,v2,v3' >> was working perfectly.
-
4 Sep 2012 3:31 PM #10
Here are a couple of ways...
Here are a couple of ways...
Eistrati,
You can pre-define values in your store by using the "data" config property in a store:
Or by adding the "options" config property to your form panel:Code:Ext.define('example.store.Tags', { extend: 'Ext.data.Store', config: { model: 'example.model.Tag', autoSync: true, autoLoad: true, proxy: { type: 'memory' }, data : [ {text: 'Ed', value: 'Spencer'}, {text: 'Tommy', value: 'Maintz'}, {text: 'Aaron', value: 'Conran'}, {text: 'Jamie', value: 'Avins'} ] } });
Code:{ xtype : 'multiselectfield', name : 'Tags', label : 'Tags', displayField : 'text', //don't change this property valueField : 'value', //don't change this property usePicker : false //required options : [ {text: 'Ed', value: 'Spencer'}, {text: 'Tommy', value: 'Maintz'}, {text: 'Aaron', value: 'Conran'}, {text: 'Jamie', value: 'Avins'} ] }


Reply With Quote