Dynamic scrolling in combobox ext 4.0 - extjs

I am using extjs 4.0 and having a combobox with queryMode 'remote'. I fill it with data from server. The problem is that the number of records from server is too large, so I thought it would be better to load them by parts. I know there is a standart paginator tool for combobox, but it is not convinient because needs total number of records.
Is there any way to add dynamic scrolling for combobox? When scrolling to the bottom of the list I want to send request for the next part of records and add them to the list. I can not find appropriate listener to do this.

The following is my solution for infinite scrolling for combobox, Extjs 4.0
Ext.define('TestProject.testselect', {
extend:'Ext.form.field.ComboBox',
alias: ['widget.testselect'],
requires: ['Ext.selection.Model', 'Ext.data.Store'],
/**
* This will contain scroll position when user reaches the bottom of the list
* and the store begins to upload data
*/
beforeRefreshScrollTop: 0,
/**
* This will be changed to true, when there will be no more records to upload
* to combobox
*/
isStoreEndReached : false,
/**
* The main thing. When creating picker, we add scroll listener on list dom element.
* Also add listener on load mask - after load mask is hidden we set scroll into position
* that it was before new items were loaded to list. This prevents 'jumping' of the scroll.
*/
createPicker: function() {
var me = this,
picker = me.callParent(arguments);
me.mon(picker, {
'render' : function() {
Ext.get(picker.getTargetEl().id).on('scroll', me.onScroll, me);
me.mon(picker.loadMask, {
'hide' : function() {
Ext.get(picker.id + '-listEl').scroll("down", me.beforeRefreshScrollTop, false);
},
scope: me
});
},
scope: me
});
return picker;
},
/**
* Method which is called when user scrolls the list. Checks if the bottom of the
* list is reached. If so - sends 'nextPage' request to store and checks if
* any records were received. If not - then there is no more records to load, and
* from now on if user will reach the bottom of the list, no request will be sent.
*/
onScroll: function(){
var me = this,
parentElement = Ext.get(me.picker.getTargetEl().id),
parentElementTop = parentElement.getScroll().top,
scrollingList = Ext.get(me.picker.id+'-items');
if(scrollingList != undefined) {
if(!me.isStoreEndReached && parentElementTop >= scrollingList.getHeight() - parentElement.getHeight()) {
var multiselectStore = me.getStore(),
beforeRequestCount = multiselectStore.getCount();
me.beforeRefreshScrollTop = parentElementTop;
multiselectStore.nextPage({
params: this.getParams(this.lastQuery),
callback: function() {
me.isStoreEndReached = !(multiselectStore.getCount() - beforeRequestCount > 0);
}
});
}
}
},
/**
* Took this method from Ext.form.field.Picker to collapse only if
* loading finished. This solve problem when user scrolls while large data is loading.
* Whithout this the list will close before finishing update.
*/
collapse: function() {
var me = this;
if(!me.getStore().loading) {
me.callParent(arguments);
}
},
/**
* Reset scroll and current page of the store when loading all profiles again (clicking on trigger)
*/
doRawQuery: function() {
var me = this;
me.beforeRefreshScrollTop = 0;
me.getStore().currentPage = 0;
me.isStoreEndReached = false;
me.callParent(arguments);
}
});
When creating element, should be passed id to the listConfig, also I pass template for list, because I need it to be with id. I didn't find out more elegant way to do this. I appreciate any advice.
{
id: 'testcombo-multiselect',
xtype: 'testselect',
store: Ext.create('TestProject.testStore'),
queryMode: 'remote',
queryParam: 'keyword',
valueField: 'profileToken',
displayField: 'profileToken',
tpl: Ext.create('Ext.XTemplate',
'<ul id="ds-profiles-boundlist-items"><tpl for=".">',
'<li role="option" class="' + Ext.baseCSSPrefix + 'boundlist-item' + '">',
'{profileToken}',
'</li>',
'</tpl></ul>'
),
listConfig: {
id: 'testcombo-boundlist'
}
},
And the store:
Ext.define('TestProject.testStore',{
extend: 'Ext.data.Store',
storeId: 'teststore',
model: 'TestProject.testModel',
pageSize: 13, //the bulk of records to receive after each upload
currentPage: 0, //server side works with page numeration starting with zero
proxy: {
type: 'rest',
url: serverurl,
reader: 'json'
},
clearOnPageLoad: false //to prevent replacing list items with new uploaded items
});

Credit to me1111 for showing the way.
Ext.define('utils.fields.BoundList', {
override:'Ext.view.BoundList',
///#function utils.fields.BoundList.loadNextPageOnScroll
///Add scroll listener to load next page if true.
///#since 1.0
loadNextPageOnScroll:true,
///#function utils.fields.BoundList.afterRender
///Add scroll listener to load next page if required.
///#since 1.0
afterRender:function(){
this.callParent(arguments);
//add listener
this.loadNextPageOnScroll
&&this.getTargetEl().on('scroll', function(e, el){
var store=this.getStore();
var top=el.scrollTop;
var count=store.getCount()
if(top>=el.scrollHeight-el.clientHeight//scroll end
&&count<store.getTotalCount()//more data
){
//track state
var page=store.currentPage;
var clearOnPageLoad=store.clearOnPageLoad;
store.clearOnPageLoad=false;
//load next page
store.loadPage(count/store.pageSize+1, {
callback:function(){//restore state
store.currentPage=page;
store.clearOnPageLoad=clearOnPageLoad;
el.scrollTop=top;
}
});
}
}, this);
},
});

You can implement the infinite grid as list of the combobox.
Look at this example to implement another picker:
http://www.sencha.com/forum/showthread.php?132328-CLOSED-ComboBox-using-Grid-instead-of-BoundList

If anyone needs this in ExtJS version 6, here is the code:
Ext.define('Test.InfiniteCombo', {
extend: 'Ext.form.field.ComboBox',
alias: ['widget.infinitecombo'],
/**
* This will contain scroll position when user reaches the bottom of the list
* and the store begins to upload data
*/
beforeRefreshScrollTop: 0,
/**
* This will be changed to true, when there will be no more records to upload
* to combobox
*/
isStoreEndReached: false,
/**
* The main thing. When creating picker, we add scroll listener on list dom element.
* Also add listener on load mask - after load mask is hidden we set scroll into position
* that it was before new items were loaded to list. This prevents 'jumping' of the scroll.
*/
createPicker: function () {
var me = this,
picker = me.callParent(arguments);
me.mon(picker, {
'afterrender': function () {
picker.on('scroll', me.onScroll, me);
me.mon(picker.loadMask, {
'hide': function () {
picker.scrollTo(0, me.beforeRefreshScrollTop,false);
},
scope: me
});
},
scope: me
});
return picker;
},
/**
* Method which is called when user scrolls the list. Checks if the bottom of the
* list is reached. If so - sends 'nextPage' request to store and checks if
* any records were received. If not - then there is no more records to load, and
* from now on if user will reach the bottom of the list, no request will be sent.
*/
onScroll: function () {
var me = this,
parentElement = me.picker.getTargetEl(),
scrollingList = Ext.get(me.picker.id + '-listEl');
if (scrollingList != undefined) {
if (!me.isStoreEndReached && me.picker.getScrollY() + me.picker.getHeight() > parentElement.getHeight()) {
var store = me.getStore(),
beforeRequestCount = store.getCount();
me.beforeRefreshScrollTop = me.picker.getScrollY();
store.nextPage({
params: this.getParams(this.lastQuery),
callback: function () {
me.isStoreEndReached = !(store.getCount() - beforeRequestCount > 0);
}
});
}
}
},
/**
* Took this method from Ext.form.field.Picker to collapse only if
* loading finished. This solve problem when user scrolls while large data is loading.
* Whithout this the list will close before finishing update.
*/
collapse: function () {
var me = this;
if (!me.getStore().loading) {
me.callParent(arguments);
}
},
/**
* Reset scroll and current page of the store when loading all profiles again (clicking on trigger)
*/
doRawQuery: function () {
var me = this;
me.beforeRefreshScrollTop = 0;
me.getStore().currentPage = 1;
me.isStoreEndReached = false;
me.callParent(arguments);
}
});

First of all thanks to #me1111 for posting the code for dynamic rendering on scroll. That code is working for me after doing a small change.
tpl: Ext.create('Ext.XTemplate',
'<ul id="testcombo-boundlist-items"><tpl for=".">',
'<li role="option" class="' + Ext.baseCSSPrefix + 'boundlist-item' + '">',
'{profileToken}',
'</li>',
'</tpl></ul>'
),
listConfig: {
id: 'testcombo-boundlist'
}
in code from <ul id="ds-profiles-boundlist-items"><tpl for="."> i have changed id to "testcombo-boundlist-items". as we defined id as 'testcombo-boundlist' in listConfig.
Before modification, in onScroll method scrollingList = Ext.get(me.picker.id + '-items'); i am getting scrollList as a null. because me.picker.id will return 'testcombo-boundlist' on this we are appending '-items' so it became 'testcombo-boundlist-items' but this id does not exists. so iam getting null.
After modification of id in <ul id="testcombo-boundlist-items"> we have a list object on "testcombo-boundlist-items". so i got a object.
Hope this small change will help.

Related

Backbone/Marionette Fetching large collection causes browser to freeze

I have a CompositeView that I am working with in Marionette that when the collection fetches can potentially pull 1000s of records back from the API. The problem I am seeing is that when the UI loads, the browser wigs out and the script freezes the browser up.
I have tried following this blog post (http://lostechies.com/derickbailey/2011/10/11/backbone-js-getting-the-model-for-a-clicked-element/), but can't seem to figure out how to translate that to working in Marionette.
Here is my CompositeView:
var EmailsMenuView = Marionette.CompositeView.extend({
template: _.template(templateHTML),
childView: EmailItemView,
childViewOptions: function(model, index){
return {
parentIndex: index,
parentContainer: this.$el.closest('form')
};
},
childViewContainer: '.emails',
ui: {
emails: '.emails',
options: '.email-options',
subject: "#subject_line",
fromName: "#from_name",
fromEmail: "#from_email",
thumbnail: '.thumbnail img',
emailName: '.thumbnail figcaption'
},
events: {
'change #ui.subject': 'onSubjectChange',
'change #ui.fromName': 'onFromNameChange',
'change #ui.fromEmail': 'onFromEmailChange'
},
childEvents: {
'menu:selected:email': 'onMenuEmailSelect'
},
collectionEvents: {
'change:checked': 'onEmailSelected'
},
behaviors: {
EventerListener: {
behaviorClass: EventerListener,
eventerEvents: {
'menu:next-click': 'onNext',
'menu:previous-click': 'onPrev'
}
},
LoadingIndicator: {
behaviorClass: LoadingIndicator,
parentClass: '.emails'
}
},
renderItem: function(model) {
console.log(model);
},
/**
* Runs automatically during a render
* #method EmailsMenuView.onRender
*/
onRender: function () {
var colCount, showGridList, selected,
threshold = 300;
if(this.model.get('id')) {
selected = this.model;
}
// refresh its list of emails from the server
this.collection.fetch({
selected: selected,
success: function (collection) {
colCount = collection.length;
console.log(colCount);
},
getThumbnail: colCount <= threshold
});
this.collection.each(this.renderItem);
var hasEmail = !!this.model.get('id');
this.ui.emails.toggle(!hasEmail);
this.ui.options.toggle(hasEmail);
eventer.trigger(hasEmail ? 'menu:last' : 'menu:first');
}
});
My ItemView looks like this:
var EmailItemView = Marionette.ItemView.extend({
tagName: 'article',
template: _.template(templateHTML),
ui: {
radio: 'label input',
img: 'figure img',
figure: 'figure'
},
events: {
'click': 'onSelect',
'click #ui.radio': 'onSelect'
},
modelEvents: {
'change': 'render'
},
behaviors: {
Filter: {
behaviorClass: Filter,
field: "email_name"
}
},
/**
* initializes this instance with passed options from the constructor
* #method EmailItemView.initialize
*/
initialize: function(options){
this.parentIndex = options.parentIndex;
this.parentContainer = options.parentContainer;
this.listenTo(eventer, 'menu:scroll', this.onScroll, this);
},
/**
* Runs automatically when a render happens. Sets classes on root element.
* #method EmailItemView.onRender
*/
onRender: function () {
var checked = this.model.get("checked");
this.$el.toggleClass('selected', checked);
this.ui.radio.prop('checked', checked);
},
/**
* Runs after the first render, only when a dom refrsh is required
* #method EmailItemView.onDomRefresh
*/
onDomRefresh: function(){
this.onScroll();
},
/**
* Marks this item as checked or unchecked
* #method EmailItemView.onSelect
*/
onSelect: function () {
this.model.collection.checkSingle(this.model.get('id'));
this.trigger('menu:selected:email');
},
templateHelpers: {
formattedDate: function() {
var date = this.updated_ts.replace('#D:', '');
if (date !== "[N/A]") {
return new Moment(date).format('dddd, MMMM DD, YYYY [at] hh:mm a');
}
}
},
/**
* Delays the load of this view's thumbnail image until it is close to
* being scrolled into view.
* #method EmailItemView.onScroll
*/
onScroll: function () {
if (this.parentIndex < 10) {
// if it's one of the first items, just load its thumbnail
this.ui.img.attr('src', this.model.get('thumbnail') + '?w=110&h=110');
} else if (this.parentContainer.length && !this.ui.img.attr('src')) {
var rect = this.el.getBoundingClientRect(),
containerRect = this.parentContainer[0].getBoundingClientRect();
// determine if element is (or is about to be) visible, then
// load thumbnail image
if (rect.top - 300 <= containerRect.bottom) {
this.ui.img.attr('src', this.model.get('thumbnail') + '?w=110&h=110');
}
}
}
});
I am trying to adjust my CompositeView to work where I can build the HTML and then render the cached HTML versus appending 1000s of elements
Render all 1000 elements isn't good idea - DOM will become too big. Also on fetch there are parse for all 1000 models, and it works synchroniously. So there are two ways - load less data, or splist data to parse/render by chunks

kendo ui - tree, cannot update main node

I am using angular + kendo ui. I have created a tree and added first node as dummy
$scope.treeData = kendo.observableHierarchy({
data: [{ text: "" ,items: [] }]
});
after that when I have the right data , I want to place the right data
function init() {
var query = CSensorGroupRepositoryService.getQueyObject().where("ParentID", "==", 0); // groups with no parent
CSensorGroupRepositoryService.getEntitiesByQuery(query).then(function (results) {
var parent = { text: results[0].GroupName, items: [] };
buildTreeHierarchy(results[0], parent);
$scope.treeData[0].text = results[0].GroupName;
});
}
how ever in the view - the text is empty
Use ObservableObject.set so the change event is triggered and your widget updates its view:
$scope.treeData[0].set("text", results[0].GroupName);

Extjs4 add an empty option in a combobox

I have a combobox in ExtJS4 with this initial config
xtype: 'combobox',
name: 'myCombo',
store: 'MyStore',
editable: false,
displayField: 'name',
valueField: 'id',
emptyText: 'Select an Option'
I don't know if there is an easy way to tell the combo to add an option so the user can deselect the combo (first he select an option and then he want to not select anything... so he wants to return to "Select an Option")
I solved this before by adding an extra option to the fetched data so I can simulate to have the "Select an Option" as a valid option in the combo but I think there should be a better way.
You do not need any new design or graphics or any complex extensions. ExtJS has is all out of the box.
You should be able to use this:
Ext.define('Ext.ux.form.field.ClearCombo', {
extend: 'Ext.form.field.ComboBox',
alias: 'widget.clearcombo',
trigger2Cls: 'x-form-clear-trigger',
initComponent: function () {
var me = this;
me.addEvents(
/**
* #event beforeclear
*
* #param {FilterCombo} FilterCombo The filtercombo that triggered the event
*/
'beforeclear',
/**
* #event beforeclear
*
* #param {FilterCombo} FilterCombo The filtercombo that triggered the event
*/
'clear'
);
me.callParent(arguments);
me.on('specialkey', this.onSpecialKeyDown, me);
me.on('select', function (me, rec) {
me.onShowClearTrigger(true);
}, me);
me.on('afterrender', function () { me.onShowClearTrigger(false); }, me);
},
/**
* #private onSpecialKeyDown
* eventhandler for special keys
*/
onSpecialKeyDown: function (obj, e, opt) {
if ( e.getKey() == e.ESC )
{
this.clear();
}
},
onShowClearTrigger: function (show) {
var me = this;
if (show) {
me.triggerEl.each(function (el, c, i) {
if (i === 1) {
el.setWidth(el.originWidth, false);
el.setVisible(true);
me.active = true;
}
});
} else {
me.triggerEl.each(function (el, c, i) {
if (i === 1) {
el.originWidth = el.getWidth();
el.setWidth(0, false);
el.setVisible(false);
me.active = false;
}
});
}
// ToDo -> Version specific methods
if (Ext.lastRegisteredVersion.shortVersion > 407) {
me.updateLayout();
} else {
me.updateEditState();
}
},
/**
* #override onTrigger2Click
* eventhandler
*/
onTrigger2Click: function (args) {
this.clear();
},
/**
* #private clear
* clears the current search
*/
clear: function () {
var me = this;
me.fireEvent('beforeclear', me);
me.clearValue();
me.onShowClearTrigger(false);
me.fireEvent('clear', me);
}
});
Here's a JSFiddle
And a JSFiddle without a default combo.
Note that both examples don't require any new graphic or style
To the best of my knowledge there is no better way of doing this. Although others may have a hack to do what you do by means of a plugin.
If I'm perfectly honest, no selection option in comboboxes is somewhat of a user interface paradox - users are required to select 'no selection'. A blank option is not very clear - looks a bit like a bug.
From a user interface perspective I think the preferred solution to this scenario is to have a button next to the combobox saying 'clear selection' (or just one with an X icon). Once you manage to put a button there, you can just call clearValue().

Drag and Drop from Grid to Tree and backwards

Since a few days now I try to change an ExtJs ['Grid to Tree Drag and Drop' Example][1] to work in both directions. But all I get is an 'almost' working application.
Now it works as far as I can drag an item from grid to tree, within tree, within grid but if i drag it from tree to grid, it doesn't drop. It just shows the green hook.
I also tried to differ the ddGroup from tree and grid, but then nothing works anymore.
This is too much for an ExtJs beginner.
// Stücklisten Grid
stuecklistengrid = Ext.extend(Ext.grid.GridPanel, {
initComponent:function() {
var config = {
store:itemPartStore
,columns:[{
id:'PART_ITE_ID'
,header:"PART_ITE_ID"
,width:200, sortable:true
,dataIndex:'PART_ITE_ID'
},{
header:"IS_EDITABLE"
,width:100
,sortable:true
,dataIndex:'IS_EDITABLE'
},{
header:"IS_VISIBLE"
,width:100
,sortable:true
,dataIndex:'IS_VISIBLE'
}]
,viewConfig:{forceFit:true}
}; // eo config object
// apply config
Ext.apply(this, Ext.apply(this.initialConfig, config));
this.bbar = new Ext.PagingToolbar({
store:this.store
,displayInfo:true
,pageSize:10
});
// call parent
stuecklistengrid.superclass.initComponent.apply(this, arguments);
} // eo function initComponent
,onRender:function() {
// call parent
stuecklistengrid.superclass.onRender.apply(this, arguments);
// load the store
this.store.load({params:{start:0, limit:10}});
} // eo function onRender
});
Ext.reg('examplegrid', stuecklistengrid);
// Stücklisten Tree
var CheckTree = new Ext.tree.TreePanel({
root:{ text:'root', id:'root', expanded:true, children:[{
text:'Folder 1'
,qtip:'Rows dropped here will be appended to the folder'
,children:[{
text:'Subleaf 1'
,qtip:'Subleaf 1 Quick Tip'
,leaf:true
}]
},{
text:'Folder 2'
,qtip:'Rows dropped here will be appended to the folder'
,children:[{
text:'Subleaf 2'
,qtip:'Subleaf 2 Quick Tip'
,leaf:true
}]
},{
text:'Leaf 1'
,qtip:'Leaf 1 Quick Tip'
,leaf:true
}]},
loader:new Ext.tree.TreeLoader({preloadChildren:true}),
enableDD:true,
ddGroup:'grid2tree',
id:'tree',
region:'east',
title:'Tree',
layout:'fit',
width:300,
split:true,
collapsible:true,
autoScroll:true,
listeners:{
// create nodes based on data from grid
beforenodedrop:{fn:function(e) {
// e.data.selections is the array of selected records
if(Ext.isArray(e.data.selections)) {
// reset cancel flag
e.cancel = false;
// setup dropNode (it can be array of nodes)
e.dropNode = [];
var r;
for(var i = 0; i < e.data.selections.length; i++) {
// get record from selectons
r = e.data.selections[i];
// create node from record data
e.dropNode.push(this.loader.createNode({
text:r.get('PART_ITE_ID')
,leaf:true
,IS_EDITABLE:r.get('IS_EDITABLE')
,IS_VISIBLE:r.get('IS_VISIBLE')
}));
}
// we want Ext to complete the drop, thus return true
return true;
}
// if we get here the drop is automatically cancelled by Ext
}}
}
});
// Stücklisten Container
var itemPartList = new Ext.Container({
id: 'itemPartList',
title: 'Stücklisten',
border:false,
layout:'border',
items:[CheckTree, {
xtype:'examplegrid'
,id:'SLgrid'
,title:'Grid'
,region:'center'
,layout:'fit'
,enableDragDrop:true
,ddGroup:'grid2tree'
,listeners: {
afterrender: {
fn: function() {
// This will make sure we only drop to the view scroller element
SLGridDropTargetEl2 = Ext.getCmp('SLgrid').getView().scroller.dom;
SLGridDropTarget2 = new Ext.dd.DropTarget(SLGridDropTargetEl2, {
ddGroup : 'grid2tree',
notifyDrop : function(ddSource, e, data){
var records = ddSource.dragData.selections;
Ext.each(records, ddSource.grid.store.remove,
ddSource.grid.store);
Ext.getCmp('SLgrid').store.add(records);
return true
}
});
}
}
}
}]
});
You need to implement the onNodeDrop event of the grid. See http://docs.sencha.com/ext-js/4-1/#!/api/Ext.grid.header.DropZone-method-onNodeDrop

Ext JS Reordering a drag and drop list

I have followed the tutorial over at http://www.sencha.com/learn/Tutorial:Custom_Drag_and_Drop_Part_1
It is great, however now I need pointers on how to add functionally of being to be able reorder a single list. At the moment when I drop a item on the list it is appended at the end. However I wish to be able to drag a item between two others or to the front then drop it there.
Any advice appreciated, thanks.
I found Ext.GridPanel row sorting by drag and drop working example in blog http://hamisageek.blogspot.com/2009/02/extjs-tip-sortable-grid-rows-via-drag.html
It worked fine for me. Here my js code:
app.grid = new Ext.grid.GridPanel({
store: app.store,
sm: new Ext.grid.RowSelectionModel({singleSelect:false}),
cm: new Ext.grid.ColumnModel({
columns: app.colmodel
}),
ddGroup: 'dd',
enableDragDrop: true,
listeners: {
"render": {
scope: this,
fn: function(grid) {
// Enable sorting Rows via Drag & Drop
// this drop target listens for a row drop
// and handles rearranging the rows
var ddrow = new Ext.dd.DropTarget(grid.container, {
ddGroup : 'dd',
copy:false,
notifyDrop : function(dd, e, data){
var ds = grid.store;
// NOTE:
// you may need to make an ajax call
// here
// to send the new order
// and then reload the store
// alternatively, you can handle the
// changes
// in the order of the row as
// demonstrated below
// ***************************************
var sm = grid.getSelectionModel();
var rows = sm.getSelections();
if(dd.getDragData(e)) {
var cindex=dd.getDragData(e).rowIndex;
if(typeof(cindex) != "undefined") {
for(i = 0; i < rows.length; i++) {
ds.remove(ds.getById(rows[i].id));
}
ds.insert(cindex,data.selections);
sm.clearSelections();
}
}
// ************************************
}
})
// load the grid store
// after the grid has been rendered
this.store.load();
}
}
}
});
If you had hbox layout with 3 Grid side by side
new Ext.Panel(
{
layout: "hbox",
anchor: '100% 100%',
layoutConfig:
{
align: 'stretch',
pack: 'start'
},
items: [GridPanel1, GridPanel2, GridPanel3
})
then you must juse grid El instead of container
var ddrow = new Ext.dd.DropTarget(grid.getEl(), { ....

Resources