extjs

Ext JS 4: Ext.ux.RowExpander Tweaks

I recently looked into a few issues we were experiencing with the Ext.ux.RowExpander plugin, which resulted in a few small tweaks to the plugin.

The setup: Grid with checkbox selection model using Ext.ux.RowExpander plugin.  An expanded row is a nested grid.  This was implemented following a pattern that can be found around the web about nesting grids in the expanded rowbody (http://stackoverflow.com/questions/8254113/nested-grid-with-extjs-4).

Issue 1: When the row expands to show the nested grid, the expanded row takes the full width of the grid, instead of aligning to the right of the checkbox column and the expand/collapse column.  This is actually logged as a bug, with pictures at http://www.sencha.com/forum/showthread.php?263231-Content-inside-RowExpander-doesn-t-take-all-width-of-a-row-if-grid-has-CheckboxModel.

The problem is that within the RowExpander plugin, the logic deciding the colspan of the expanded row body does not take into account any other grid features that might impact the columns.  I updated the plugin code to look at the parent grid selection model before determining how wide the colspan should be.

Issue 2: In my UI there is a search panel and then the grid with the search results.  The user can go through and expand various rows, but if the user runs a search again I want the search results to return and the grid refreshed with the results and all the rows collapsed.  What was happening was the grid was refreshed, all rows collapsed, but the expand/collapse icon was in the expanded state.

To fix this I added a method, clearExpanded to the RowExpander plugin.  It’s very straightforward and just clears out the rowsExpanded property the plugin maintains to track which rows are expanded.  Then, I listen for the refresh event on the parent grid’s view, and at that point, call clearExpanded on the plugin.  The refresh event handler on the grid view looks like this:

onMyGridRefresh: function() {
    var rowExpander = this.getSearchGrid().getPlugin('rowexpanderplugin');
    if (rowExpander) {
        rowExpander.clearExpanded();
    }
    this.destroyNestedGrids();
}

Remember, if you are creating nested grids in the manner described above, you need to destroy those grids manually!  Hence, the destroyNestedGrids() method in the code above.  It does a component query on the nested grid xtype and calls destroy on the returned components.

Solution: That’s great, where’s the code?

The updated plugin is at Gist https://gist.github.com/aghuddleston/25b19dfe10f7eb40e54c.   It’s a little long to directly include here.

Version Info: Ext JS 4.1.1

Advertisements

Ext JS 4: Date Display Field

Ext JS Ext.form.field.Display works well for string text, but doesn’t render dates easily. Turns out you can use the method valueToRaw to apply a formatter. Here’s a simple extension for date fields:

This is 100% inspired by this post http://www.sencha.com/forum/showthread.php?148013-How-to-invoke-a-renderer-on-a-displayfield-xtype on the Sencha forums.

To use it’s as simple as having a config on your form like so:

{
  xtype: 'datedisplayfield',
  fieldLabel : 'My Date',
  name : 'myDate'
  datePattern: 'l, F d, Y g:i:s A'
}

datePattern is optional, default it ‘Y-m-d’.

Version Info: Ext JS 4.1.1

Ext JS 4: Simple Splash Page

Sometimes you need a simple splash page while your Ext JS site is loading. Thanks to IvanJouikov at the Sencha forums for the simple solution and helpful post at http://www.sencha.com/forum/showthread.php?230118-Splash-Screen&s=b823569ad51889a57ab5bcef408fc3c6&p=947869&viewfull=1#post947869

I modified a little bit for my situation (no images, just text) and to not use the deprecated center tag.

I added the following to the css:

#loading-parent {
  position: absolute;
  z-index:  20001;
  height: 100%;
  width: 100%;
  padding-top: 50px;
  text-align: center;
}

#loading-indicator {
  font-size: large;
  font-family: tahoma,arial,verdana,sans-serif;
  text-align: center;
  margin: 0 auto;
}

And in the HTML I put this in the body section:

<div id="loading-parent">
  <div id="loading-child">
      <br/><br/>
      <div id="loading-indicator">Loading My Application.  Please wait...</div>
  </div>
</div>

And then the code was just as in the post in the forums. In the launch method of the MVC application:

launch: function() {
  var me = this;
  
  // Create the actual viewport in body
  Ext.create('My.view.Viewport', {
      renderTo: Ext.getBody(),
      listeners: {
          afterrender: function() {
              var mask = Ext.get('loading-mask'),
                  parent = Ext.get('loading-parent');
              // Destroy the masks
              mask.fadeOut({callback: function(){ mask.destroy(); }});
              parent.fadeOut({callback: function(){ parent.destroy(); }});
          }
      }
  });
}

Version Info: Ext JS 4.1.1
Tested on IE8, Chrome and Firefox

Ext JS 4: Mocking Success and Failure AJAX Responses

Often when building an Ext JS app, I will create a functioning mock-up and work out the design and most client-side issues in my mock-ups. This has a lot to do w/the environment I am working on and it gets down to the fact that it is much faster for me to work with the mock-ups than my real app. Recently I’ve been creating a utility class for my app that has constants and utility methods and one thing that is helpful for me is my “random success/fail” ajax response method. I’ve found this is a great way for me to make sure I’m handling errors everywhere in my app, whether it is through a global error handler or on each AJAX request.

My stripped down utility class looks like this (usually I have a bit more in there, such as constants, app-specific vtypes, and logging.):

Ext.define('MyApp.Util', {
	singleton: true,

	UNKNOWN_ERROR: 'An unknown error occurred',

	randomSuccessFail : function (u) {
		var random = Math.floor(Math.random()*11), url;
		if (random > 8) {
			url = './data/dne.json'
		} else if (random > 3) {
			url = u ? u : '../util/successTrue.json';
		} else {
			url = '../util/successFalse.json';
		}
		return url;
	}

});

“dne.json” is a file that does not exist, so I make sure my mock-ups are handling 404s and other server errors. The other response files look like this:

//successTrue.json
{
	"success":true,
	"message":"success message here, if any"
}

// successFalse.json
{
	"success":false,
	"message":"Failure message goes here and make it a little bit longer and even longer"
}

And it gets called like so in a controller:

// A method inside a controller.  Ext.Ajax request, 
// returning default success/fail response
	saveUser: function() {
		var userDetail = this.getUserDetail(),
			params;

		if (userDetail.getForm().isValid()) {
			this.getDismissAlert().hide();

			params = userDetail.getValues();

			Ext.Ajax.request({
				url: MyApp.Util.randomSuccessFail(),
				jsonData: params,
				scope: this,
				callback: this.saveCallback
			});
		}
	},

// And here's another example, this time on a store load 
// and expecting a different success response
	loadUserIds: function () {
		var combo = this.getUserIdCombo(),
			store = combo.getStore();

		store.proxy.url = MyApp.Util.randomSuccessFail('./data/userids.json');
		store.load({
			scope: this,
			callback: this.loadUserIdsCallback
		});
	},

// Sample callback, shown here... just because
	saveCallback : function (options, success, response) {
		var responseJson, form;
		if (success) {
			responseJson = Ext.decode(response.responseText);
			if (responseJson.success === true) {
				// whatever stuff needs to happen on success
				this.showSuccessResults("Updates saved");
			} else {
				this.showErrorResults(responseJson.message);
			}
		} else {
			this.showErrorResults(MyApp.Util.UNKNOWN_ERROR);
		}
	},

And that’s it, a helpful utility.

Ext JS 4: Adding ComboBoxes via Remote Configuration in MVC

In an Ext JS 4 MVC application I am currently working on, I need to programatically add comboboxes to a form.  The list of comboboxes varies depending on the selected user.  In addition, the store backing each of the comboboxes is different.  Here is my approach.

The Model
In my case, although the stores might have different data, they have the same fields.  It is a generic label/value set up.

Ext.define('MyApp.model.LabelValue', {
	extend: 'Ext.data.Model',
	fields: [
		'label',
		'value'
	]
});

The Store
Because the stores are so similar for each combo, I created a store that was specific to my case, but generic enough to be used for all the combo boxes. The important part is the alias that is part of the store definition ‘store.mycombostore’. This will allow me to reference the store type, but ensure a new store is created for each combobox. This helpful piece of information is from the comments on the store config for ComboBox, found at Ext.form.field.ComboBox.

Ext.define('MyApp.store.MyComboStore', {
	extend: 'Ext.data.Store',
	alias: 'store.mycombostore',
	model: 'MyApp.model.LabelValue',
	autoLoad: false,

	proxy: {
		type: 'ajax',
		url : 'put/your/url/here',
		reader: {
			type: 'json',
			root: 'data'
		}
	}
});

The View
To reduce the amount of remote configuration that I need to generate I defined a custom combobox with all the static configurations that I wanted. I have a few other things going on in my combobox, like using my Remote To Local plugin. I also overrode beforeBlur to allow the user to delete and basically re-empty the combobox, and I’m setting some params before the store loads. I initially listened for the before store load event in the controller after adding the combobox configs to my form, but putting it right in the custom combobox was cleaner. Not sure if it violates MVC or not…

Ext.define('MyApp.view.MyCombo', {
	extend: 'Ext.form.field.ComboBox',
	alias: 'widget.mycombo',

	requires: [
		'MyApp.store.MyComboStore',
		'Ext.ux.form.field.RemoteToLocalComboBox'
	],

	allowBlank: true,
	forceSelection: true,
	queryMode: 'remote',
	minChars : 1, //begin query after user types 1 char
	msgTarget: 'side',
	queryDelay: 10,
	triggerAction: 'all',
	typeAhead: true,
	typeAheadDelay: 250,

	labelWidth: 200,
	width: 450,

	name: 'roleTemplates',
	displayField: 'label',
	valueField: 'value',

	plugins: [
		{ptype:'remotetolocalcombo'}
	],

	beforeBlur: function () {
		if (this.getRawValue().length === 0) {
			this.lastSelection = [];
		}
		this.doQueryTask.cancel();
		this.assertValue();
	},

	initComponent: function() {
		var me = this;

		Ext.applyIf(me, {
			emptyText: 'Process Access Request...',
			loadingText: 'Loading...',
			store: {type: 'roletemplatesremote'}
		});
		me.callParent(arguments);
		me.getStore().on('beforeload', this.beforeTemplateLoad, this);
	},

	beforeTemplateLoad: function(store) {
		store.proxy.extraParams = {
			moduleId: this.moduleId
		}
	}
});

The Controller
So, in my controller I’m loading all my “user” data to populate the form. As part of my AJAX JSON response, I have the remote configuration:

[
	{
		"xtype":"mycombo",
		"fieldLabel": "Module One",
		"moduleId":"module1"
	},
	{
		"xtype":"mycombo",
		"fieldLabel": "Module Two",
		"moduleId":"module2"
	}
]

And here is the AJAX request and response handling. The Ext.container.Container doc has a section on “Adding via remote configuration” that explains what to do.

Ext.define('MyApp.controller.UserDetail', {

	//...  all the standard controller stuff
	
	loadUser: function() {
		Ext.Ajax.request({
			url: 'your/url/here',
			jsonData: {
				userId: userId
			},
			scope: this,
			callback: this.loadUserCallback
		});
	}
		
	loadUserCallback: function(options, success, response) {
		// userDetail is the view where I am adding the comboboxes.  The
		// new comboboxes will be added to a fieldContainer.
		var userDetail = this.getUserDetail(), 
			responseJson,newCombos;
		if (success) {
			responseJson = Ext.decode(response.responseText);
			if (responseJson.success === true) {
				try {
					newCombos = eval(responseJson);
					userDetail.down('#comboFieldContainer').add(newCombos);
				} catch (e) {
					userDetail.down('#comboFieldContainer').add({
						xtype: 'component',
						html:'No pending access requests'
					});
				}
			} else {
				// showErrorResults is a function that I have to 
				// display an error message on my view
				this.showErrorResults(responseJson.message);
			}

		} else {
			// showErrorResults is a function that I have to 
			// display an error message on my view
			this.showErrorResults('Something went wrong.');
		}
	},
	
	//...
});

I have a bit more going on so this is a little stripped down, but looking at the Ext JS docs should clarify what is needed. Basically the AJAX request returns JSON w/the configuration objects. In the response handling, the JSON is eval’d to objects which are then added to the view. By creating a few custom components I am able to dynamically add to objects to my view, without having tons of configuration determined server-side.

Here is a picture. It’s not too exciting, but it’s exactly what I want, two user-specific comboboxes determined server-side at runtime.

RemoteConfigCombos

Version Info:
Ext JS 4.1.1

Ext JS 4: Empty Value in a ComboBox

Often, in an Ext JS combobox, it is difficult to go back to an empty value once you have selected an item, particularly if “forceSelection” is set to true.  Here is my roundup of alternative solutions found from around the web…

1. Override beforeBlur

The solution from  http://www.sencha.com/forum/showthread.php?182119-How-To-Re-Empty-ComboBox-when-forceSelection-is-Set-To-TRUE overrides beforeBlur on the combbox to clear out lastSelection.  Here is a copy of the override from the thread:

Ext.create('Ext.form.field.ComboBox', {
    ...
    allowBlank: true,
    forceSelection: true,
    beforeBlur: function(){
        var value = this.getRawValue();
        if(value == ''){
            this.lastSelection = [];
        }</span>
        this.doQueryTask.cancel();
       this.assertValue();
    }</span>
});

Or to apply the override to all combo boxes:

Ext.require("Ext.form.field.ComboBox", function () {
    Ext.override(Ext.form.field.ComboBox, {
        beforeBlur: function () {
            if (this.getRawValue().length === 0) {
                this.lastSelection = [];
            }
            this.callOverridden();
        }
    });
});

2. Add Clear Trigger

Another solution is to add a ‘clear’ trigger when the combobox has a value.  That solution is found at Combobox Extjs4 – Empty line.

3. ClearButton UX

On a related note, I really like the ClearButton plugin found at Ext.ux.form.field.ClearButton – Small clear button/icon over field.  I use this extensively in one of my applications.  It works well, except you can’t access the clear icon from the keyboard.

4. Empty Row

The other solution is to add an empty row to the data store backing the combobox, usually as the first record.

Version Info:
Ext JS 4.1.1

Ext JS 4: Updating Ext.ux.data.PagingStore

I am in the process of updating an Ext JS 3 application to use Ext JS 4. So far so good, until I got to one grid that uses Condor’s Ext.ux.data.PagingStore. I tried to get away with using the Ext.ux.data.PagingMemoryProxy, but it was not to be since I want to add and remove records from the store. I tried to find an updated version among the thread in the forum, but had no luck. Therefore, I tried my hand at updating Ext.ux.data.PagingStore for Ext JS 4.

The code for Ext.ux.data.PagingStore is at https://github.com/aghuddleston/Ext.ux.data.PagingStore. I have also included some unit tests, using Siesta as the testing tool. I’m just starting to integrate this into my application, so it’s not well tested yet… I’ll be updating the code as changes are needed.

The main difference from the previous version is that in Ext JS 4 is that “start”, “limit” and “page” are no longer set on the params in the load request, but are their own properties on the Ext.data.Operation config object. A request is only made to load data if the params or extraParams change from the previous load request. Otherwise, it will just page the data. Any proxy should be usable, but I have only tested “ajax” and “memory”. Samples of configuring the data store and loading data are in the comments in the code.

Helpful links:

Version Info:
Ext JS 4.1.1

Ext JS 4: State Management

I recently updated an Ext JS 3 application to use Ext JS 4. It’s really more of a page than a full blown app, which uses Ext JS state management to remember values for some of the fields for browser forward/back navigation. Some things I experienced along the way…

1. Default state events
When you make a field, such as a text field, stateful, you get some default state events, whether or not you want them. A text field for example, adds the state events “resize” and “change”. I didn’t want those events. I only want to save state when a certain button is clicked. Getting rid of those state events proved to be a major pain. I wanted to add a “removeStateEvents” method to the Ext.state.Stateful mixin, but I could never get it added to the prototype before the mixin was added to the text field definition.

I ended up adding the method to “Ext.form.field.Base”. I figured this was okay because the Stateful mixin is inherited by this class and in my case, everything I wanted to save state on was form field. “removeStateEvents” is almost the same as Ext.state.Stateful.addStateEvents, except that it deletes the event name from the stateEventsByName object and removes the doSaveState listener for the given event.

Ext.form.field.Base.addMembers({
  removeStateEvents: function (events) {
    var me = this,
        i, event, stateEventsByName;

    if (me.stateful && me.getStateId()) {
      if (typeof events == 'string') {
        events = Array.prototype.slice.call(arguments, 0);
      }

      stateEventsByName = me.stateEventsByName || (me.stateEventsByName = {});

      for (i = events.length; i--; ) {
        event = events[i];

        if (stateEventsByName[event]) {
          delete stateEventsByName[event];
          me.un(event, me.onStateChange, me);
        }
      }
    }
  }
});

And then I called it on my text field in the initComponent of my extended container.

initComponent: function () {
    //...
    this.callParent(arguments);
    this.down('#searchText').removeStateEvents(['change','resize']);
}

This whole solution is annoying. I would prefer to just avoid adding the state events in the first place, but that would mean some aggressive overriding and therefore I ended up with the “remove before you can do anything” solution.

Seems like turning on stateful for a field should allow the field to specify exactly what events to listen for, instead of giving you some defaults (and potentially very frequently fired defaults at that). Something like this post on the remote storage provider extension, where defaultStateEvents can be configured would be nice.

2. Timing Matters
I could see that the state info I wanted for my fields was being saved correctly, but I had issues with the values setting on comboboxes. It turns out that value of the combobox was being initialized from the state manager prior to loading the stores for the comboboxes. So early that “isLoading” was false and instead the combobox went down the path of being set with an unknown value, and since those aren’t allowed for my comboboxes… well.

To solve this issue I moved some stuff around and loaded the combobox stores earlier, taking advantage of a store id and store manager. But then I also delayed the loading of my viewport a few milliseconds, to give the AJAX requests for the data stores a chance to kick off.

// Create data stores for the comboboxes, with autoLoad true like this:
Ext.create('Ext.data.Store', {
	storeId: 'comboboxStore',
	fields: ['label', 'value'],
	autoLoad: true,
	proxy: {
		type: 'ajax',
		url: 'resourc/url/goes/here',
		reader: {
			type: 'json',
			root: 'data'
		}
	}
});

Ext.state.Manager.setProvider(Ext.create('My.state.Provider'));
var stateReset = this.resetState();
// Delay the UI build a little bit, so the data stores get a chance to start loading
var task = new Ext.util.DelayedTask(function(stateReset){
	this.buildViewPort();
	if ( !stateReset ) {
		// Must be from a back button, run the find to pre-populate the grid
		this.doFindUsers();
	}
}, this, [stateReset]);
task.delay(100);

The Ext JS 3 version is not MVC and the code is already embedded in a legacy jsp page, so this was part of my “init” function that runs at Ext.onReady.

Version Info: Ext JS 4.1.1

Ext JS 4: Bind Keys to a Form

Here is one way to bind a key to a form/component/element in Ext JS 4. The link below also shows examples of key bindings when using MVC.

Ext.define('My.App.QuickFindContainer', {
	extend: 'Ext.container.Container',
	alias: 'widget.quickfindcontainer',
	//all the needed configs, items and initComponent

	afterRender: function () {
		this.callParent(arguments);
		this.nav = Ext.create('Ext.util.KeyNav', this.el, {
			enter : this.doFind,
			scope: this
		});
	}
});

Helpful links: http://www.sencha.com/forum/showthread.php?131596-Form-submit-on-Enter

Version Info:
Ext JS 4.1.1