json

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: Loading Reference Data Prior to Starting Application

I have recently found that I either need to load reference data or meta data to support role-based navigation before building the viewport.

In the first example below, I’m populating a variety of data stores prior to actually loading my application. This example does not use the MVC pattern.

In the second example, I’m loading toolbar configurations and other meta-data such as titles that are role-based before loading my application.

Loading Reference Data – Example 1

Ext.define('MyApp', {

	requires: [
		'Ext.Viewport',
	],

    launch: function() {

		this.storeOne = Ext.create('Ext.data.Store', {
			storeId: 'storeOne',
			model: 'MyApp.MyModel',
			autoLoad: false
		});
		
		this.storeTwo = Ext.create('Ext.data.Store', {
			storeId: 'storeTwo',
			model: 'MyApp.AnotherModel',
			autoLoad: false
		});
		
		Ext.Ajax.request({
			url: './url/for/refdata',
			jsonData: storeList,
			scope: this,
			success: this.onRefDataLoaded,
			failure: this.onRefDataLoadError
		});
	},

	onRefDataLoaded : function (response) {
		var json = Ext.decode(response.responseText), msg, item,
			success = false;

		if (typeof json.success !== "undefined" && json.success === false) {
			msg = json.message;
		} else {
			try {
				this.storeOne.loadRawData(json.storeOneData);
				this.storeTwo.loadRawData(json.storeTwoData);
				success = true;
			} catch (err) {
				msg = "An unexpected error occurred.";
			}
		}

		if (success) {
			item = {
				// put the application config stuff here.
				// i have a panel with a form on it
			};
		} else {
			// build a panel that will show the error message
			item = {
				xtype: 'panel',
				autoScroll: true,
				html: msg,
				bodyPadding: 10
			};
		}

		this.mainView = Ext.create('Ext.container.Viewport', {
            layout: 'border',
            items : [
				{
					id: 'center-container',
					xtype: 'container',
					region: 'center',
					layout: 'fit',
					items: [item]
				}
			]
		});
	},

	// Couldn't properly load the reference data, the AJAX request failed
	// Build the viewport with the error message
	onRefDataLoadError : function () {
		this.mainView = Ext.create('Ext.container.Viewport', {
			layout: 'border',
			items : [
				{
					id: 'center-container',
					xtype: 'container',
					region: 'center',
					layout: 'fit',
					items: {
						xtype: 'panel',
						autoScroll: true,
						html: "Something went wrong",
						bodyPadding: 10
					}
				}
			]
		});
	}
});

The JSON response for the ref data load looks like this:

{
	"storeOneData" : {
		"success" : true,"rows" : [
			{"id":1, "type":"UserType1"},
			{"id":2, "type":"UserType2"}
		]
	},
	"storeTwoData" : {
		"success" : true,"rows" : [
			{"id":1, "genre":"Jazz"},
			{"id":2, "genre":"Rock"},
	        {"id":3, "genre":"Pop"},
	        {"id":4, "genre":"Hip Hop"}
		]
	},
}

Loading Navigation Configurations – Example 2
In this example, instead of populating data stores, I’m querying the server to get configs for a toolbar and other data for the specific user. This example is using the MVC pattern, but dynamically loading the the controllers, following the pattern described here: An ExtJS MVC application with dynamically loaded views and controllers. The interesting part are the launch and onLoadNavButtons methods.

Ext.application({
	autoCreateViewport: false,
	name: 'MyApp',

	requires: [
	],

	appFolder: 'myapp/app',

	launch : function () {
		// Load the app navigation buttons
		Ext.Ajax.request({
			url: './url/to/get/refdata',
			scope:this,
			success:this.onLoadNavButtons,
			failure:function () {
				Ext.msg.alert("My App Initialization Failed", "Initial Loading Error");
			}
		});
	},

	onLoadNavButtons : function(xhr) {
		var viewPortData = Ext.decode(xhr.responseText),
			i, tot;
		this.navButtons = viewPortData.navButtons;
		this.defaultAction = viewPortData.defaultAction;
		this.viewportMetaData = viewPortData.viewportMetaData;
		this.buildViewPort();
	},

	buildViewPort : function () {
		this.viewport = Ext.create( "MyApp.view.Viewport", {
			viewportMetaData : this.viewportMetaData,
			navButtons: this.navButtons
		});
		var c = this.getController('Viewport'),
			da = this.defaultAction;
		c.init();
		this.runAction(da.controllerName, da.actionName, da.extra);
	},

	runAction : function (controllerName, actionName) {
		Ext.log(controllerName + ' ' + actionName);
		var controller = this.getController(controllerName),
			viewPort = this.getController('Viewport');
		controller.init(this);
		controller['action'+actionName].apply(
		    controller, Array.prototype.slice.call(arguments, 2));
		viewPort.toggleButtons.apply(viewPort,Array.prototype.slice.call(arguments, 2));
	},

	setMainView: function(view) {
		var center =  this.viewport.layout.centerRegion;
		if(center.items.indexOf(view)===-1){
			center.add(view);
        	}
		center.getLayout().setActiveItem(view);
	}
});

And the JSON that is returned from the server is

{
	"viewportMetaData" : {
		"title" : "My App Title Here",
		"hideToolbar" : false
	},
	"defaultAction" : {
		"controllerName" : "Home",
		"actionName" : "Show",
		"extra": "homeBtn"
	},
	"navButtons":[
		{
			"text": "Home",
			"itemId": "homeBtn",
			"iconCls": "icon-house"
		},
		"-",
		{
			"text":"Another Button",
			"itemId":"anotherBtn",
			"iconCls":"icon-door-in"
		},
		"-",
		{
			"text":"One More Button",
			"itemId":"moreButton",
			"iconCls":"icon-arrow-redo"
		}
	]
}

And then, in my Viewport “view” I use the navigation button configs when building the toolbar for the viewport.

Helpful Links:

Version Info: Ext JS 4.1.1

Spring 3 One Approach to Exception Handling for MVC when using JSON

Whenever an exception (such as a binding exception) came up in my Spring 3 MVC app, Spring would return and HTML page with the stack trace. I don’t want this. Besides not exposing the stack trace, I need well formatted JSON in my response, not an HTML page. I haven’t quite got a grip on exception handling in Spring. I’ve read a ton of entries in stack overflow and I get that @Exception works per controller, but for this case, I wanted to handle the exceptions that occur before you even get to the controller. I finally found one model that is working for me. There are probably other ways to do this and I hope to discover and understand them at some point, but this was actually fairly simple.

Thanks to Google App Engine + JAVA + Spring + REST + Mapping Exceptions to Errors for JSON for providing me with a solution. The entry at Wet Feet is very well written. I’m just copying my version here.

1. Create your own class that implements the HandlerExceptionResolver. I used my own view, but the Wet Feet blog entry uses MappingJacksonJsonView. The WebUtil call in the code is my own code to build the JSON.

package com.ahlearns.mystuff;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

public class JsonExceptionResolver implements HandlerExceptionResolver {
	
	protected final Logger log = LogManager.getLogger(getClass());
	
	@Override
	public ModelAndView resolveException(HttpServletRequest request,
			HttpServletResponse response, Object handler, Exception ex) {
		log.error("Error occurred for handler [" + handler + "]", ex);
		ModelAndView mav = new ModelAndView();
        	mav.setViewName("error");
        	mav.addObject("jsonMsg", WebUtil.getJsonSuccessMsg(false, "An unexpected error occurred."));
        	return mav;
	}
}

2. Add the following bean definition to your spring config. Spring will detect it and route exceptions to the resolver.

<bean id="exceptionResolver" class="com.ahlearns.mystuff.JsonExceptionResolver" />