I recently worked on a POC to integrate Solr search with the Ext JS 4 infinite scrolling grid. This allows you to scroll through 234k+ records without having the user page through the data; the scrolling does the data buffering automatically. Other features include hit highlighting, wild card searching, resizing windows and word-wrapped columns. However, the most interesting part to me was using the new MVC approach that Sencha introduced in this release to organize your project much like you would a Grails or Java Web project. I’ll detail the approach I took to make that happen and point out some gotchas along the way.
First, let’s start from the model. In the code below you can see I’ve defined a model and proxy which will be the piece that will pull the data. There’s nothing too special here with the exception of the namespace I used to define the model, ESearch.model.EPart. These are crucial that they are spelled correctly because they will be used later in other parts of the MVC.
Ext.define('ESearch.model.EPart', {
extend: 'Ext.data.Model',
idProperty: 'id',
fields: [
{name:'id', type:'int'}, 'description', 'item_number', 'part_number'
],
proxy: {
// load using script tags for cross domain, if the data in on the same domain as
// this page, an HttpProxy would be better
type: 'jsonp',
url: 'http://solrdev1/solr-eti/select/',
callbackKey: 'json.wrf',
limitParam: 'rows',
extraParams: {
q: '*',
wt:'json',
hl:'on',
'hl.fl': 'description',
'json.nl':'arrarr'
},
reader: {
root: 'response.docs',
totalProperty: 'response.numFound'
},
// sends single sort as multi parameter
simpleSortMode: true
}
});
The next thing to consider is the store. Arguably, this is not part of MVC per se, but it is used in conjunction with the model to define what type of store we want to use. In this case, we want to use a buffered store of with a page size of 500. You’ll also notice that in this code I create some listeners for beforeload and load so that I can allow sorting with Solr and to be able to do hit highlighting and query times. You’ll also notice that I link the model to the store by using the namespace for the model parameter. Let’s take a look:
// default to wildcard search
var query = '*';
Ext.define('ESearch.store.EParts', {
extend: 'Ext.data.Store',
model: 'ESearch.model.EPart',
pageSize: 200,
remoteSort: true,
autoLoad:false,
// allow the grid to interact with the paging scroller by buffering
buffered: true,
listeners: {
beforeload:{ fn: function(store, options) {
if (options && options.sorters) {
var sorters = options.sorters;
for (var i=0; i 1) {
var queryParsed = query.replace(/\*/g,'').replace(/"/g, '').trim();
var queries = queryParsed.split(' ');
for (var i=0; i < queries.length; i++) {
if (queries[i]) {
var q = escapeRegExChars(queries[i]);
// Check to highlight text only in grid-body
var node = Ext.get("grid-inf").dom.childNodes;
for (var j=0; j < node.length;j++ ) {
if (node[j].className.contains("x-grid-body",true)) {
node = node[j];
break;
}
}
highlightText(node,
q + "+", 'HL', true);
}
}
}
// temporary fix to address issue with scrollbars not resizing
var grid = Ext.getCmp('grid-inf');
grid.resetScrollers();
}
}
}
});
Now that I have the data being consumed the way I want it the next step is to put it into an infinity scrolling grid. Here’s the code to do that:
Ext.define('ESearch.view.parts.List', {
extend: 'Ext.grid.Panel',
alias: 'widget.partslist',
store: 'EParts',
initComponent: function() {
var groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
groupHeaderTpl: 'Group: {name} ({rows.length})',
startCollapsed: false
});
var selectFeature = Ext.create('qcom.grid.SelectFeature');
var config = {
name: 'qparts-grid',
id: 'grid-inf',
verticalScrollerType: 'paginggridscroller',
loadMask: true,
invalidateScrollerOnRefresh: false,
disableSelection: false,
features: [groupingFeature,selectFeature],
viewConfig: {
trackOver: false
},
// grid columns
columns:[{xtype: 'rownumberer',width: 45, sortable: false},{
id: 'id-col',
header: "ID",
dataIndex: 'id',
width:60
},{
id:"descr",
header: "Description",
dataIndex: 'description',
width: 300,
renderer: columnWrap
},{
id:"itemnum",
header: "Item Numbers",
dataIndex: 'item_number',
width: 100
},{
id: "partnum",
header: "Part Numbers",
dataIndex: 'part_number',
flex: 1,
renderer: columnWrap
}]
,selModel:{
selType:'rowmodel'
,allowDeselect:true
,mode:'MULTI'
},
tbar:
['Search:',{
xtype: 'textfield',
name: 'searchField',
hideLabel: true,
width: 250,
emptyText: "Enter search terms separated by space",
listeners: {
change: {
fn: function adjustQuery(field) {
// temporary fix to address issue with scrollbars not resizing
this.store.resetData();
// Regex query and add wildcards where appropriate
if (field.value.length >= 1) {
var values = field.getValue().match(/[A-Za-z0-9_%\/\.\-\|]+|"[^"]+"/g),
value =[];
if (values && values.length > 1) {
for ( var i=0; i < values.length; i++ ) {
if (values[i].indexOf("\"") >= 0 ) {
value.push(values[i].toLowerCase());
}
else {
value.push("*" + values[i].toLowerCase() + "*");
}
}
query = value.join(" ");
if (Ext.isChrome) {
console.log(query);
}
}
else {
if (field.getValue().indexOf("\"") >= 0 ) {
value.push(field.getValue().toLowerCase());
query = value.join(" ");
}
else {
// temporary fix because regex not picking up 1 char
var temp = values ? values[0] : field.getValue();
query = "*" + temp.toLowerCase() + "*";
}
if (Ext.isChrome) {
console.log(query);
}
}
this.store.load({
params: {q:query}
});
}
},
scope: this,
buffer: 500
}
}
},
{
xtype: 'tbfill'
},{
xtype: 'displayfield',
name: 'totalText',
id: 'totalText',
hideLabel: true,
baseCls: 'x-toolbar-text',
style: 'text-align:right;',
width:180
}
]
};
// apply config object
Ext.apply(this, config);
// call parent initComponent
this.callParent(arguments);
}
});
So from the above code you see that I’m defining a Grid Panel and assigning an alias to it called “partslist” (more on that later), but one gotcha I found is that I could not use the full namespace for the store definition — I had to just simply call it “EParts”. Finally, you’ll see me set-up the columns and create a top bar that will hold the search field. I do a regex to process the search field to create a wildcard search and to preserve quotes. I also set the buffer to 500 so they it will wait 500ms for keystrokes before firing the search again.
Now that we have the grid, we need to put it some where. This is where I bring the window into the picture. In the code below, I simply define my window size, where I want it in the browser, window capabilities like maximize, collapse and closable, and finally the items. Notice for the items, I’m using the alias partslist from the previously defined grid as the xtype. This allows me to insert a grid as I defined it before without having to instantiate it as a variable. Let’s take a look:
Ext.define( 'ESearch.view.Portal', {
extend: 'Ext.window.Window',
alias: 'widget.portal',
width: 800,
height:600,
x: 150,
y: 80,
layout:'fit',
border: false,
closable: true,
maximizable: true,
collapsible: true,
title: 'EParts Search',
items: [{
xtype: 'partslist',
itemId:'myPartList'
}]
});
So to finish up the MVC portion, we need a controller. In the code below you will see how we create the controller and then define the models, stores, views, and any references needed. You’ll also see in the init function where I invoke the store for the initial load of data as well as an example of how we could listen for certain events and do something with that event. Notice again, that the alias from the grid comes into play (partslist) so that we can capture button events from the grid. This wasn’t completely implemented, but it gives an example how it might be implemented.
Ext.define('ESearch.controller.Search', {
extend: 'Ext.app.Controller',
models:[
'EPart'
],
stores:[
'EParts'
],
views:[
'parts.List'
],
refs:[{
ref:'PartsList',
selector:'partslist'
}],
init:function(app) {
var store = this.getEPartsStore();
store.guaranteeRange(0, 199);
this.control({
'partslist button':{
click:this.onButtonClick
}
});
},
onButtonClick: function(btn, e) {
if (btn.operation === 'newSearch') {
//TODO need to find a nice way to instantiate a new window
}
}
});
The last little bit of code simply defines the application and sets some criteria as to what paths we should use and which pieces of Ext JS are required for this application to function. One gotcha I noticed is that you must define the first part of your namespace as the the folder your app will fall under. You’ll notice that I have mapped the path “app” to “ESearch” so thusly my directory structure for my application must follow something like this:
So for instance, ESearch.view.Portal, must live as a file called Portal.js under app/view and same for the other files you see there. The App.js file that contains the following code will be under the “webapp” directory adjacent to “app” to maintain relative pathing. All I do is create my viewport based on its namespace and fire .show() to kick the whole thing off.
Ext.Loader.setConfig({enabled: true,
paths: {
'Ext.ux':'lib/extjs4/ux/',
'ESearch': 'app'
}
});
Ext.require([
'Ext.grid.*',
'Ext.data.*',
'Ext.util.*',
'ESearch.view.Portal',
'Ext.grid.PagingScroller',
'Ext.ux.grid.FiltersFeature',
'Ext.grid.feature.Grouping',
'Ext.grid.plugin.CellEditing',
'Ext.state.CookieProvider'
]);
Ext.application({
name:'ESearch',
appFolder:'app',
autoCreateViewport:false,
controllers:['Search'],
launch:function() {
Ext.state.Manager.setProvider(new Ext.state.CookieProvider());
this.viewport = Ext.create('ESearch.view.Portal', {
stateId:'esearchWindow'
});
window[this.name].app = this;
this.viewport.show();
}
});
And finally, we have an HTML file that points to all the necessary JS files for this application to work, which is pretty standard stuff to bootstrap the application. However, with this approach, I only had to define the App.js file and not all the underlying JS files in the MVC portion. This is because the pathing we used in the previous section.
I hope this is useful for folks that would like to explore MVC in Ext JS 4 a little more. I really find it useful because it helps break up a larger component much along the lines I’m used to. In this way you could have multiple DEVs work on the same project pretty easily without walking over each other.