state provider

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

Advertisements

ExtJS – Creating and using a State Provider

Steps to create and user a state provider. In my case, the state provider was not the provided cookie provider, but instead sessvars, which is from Thomas Frank Session variables without cookies.

Note: Ext JS 4 version below

Step 1: Create the session provider

Ext.ux("Ext.ux.state");
/**
 * @class Ext.ux.state.SessVarsProvider
 * @extends Ext.state.Provider
 * Session state provider that stores state information on the client side
 * for the length of the session.  Important - sessvars.js must be included
 * in the page.
 * @depends sessvars.js
 */
Ext.ux.state.SessVarsProvider = Ext.extend(Ext.state.Provider, {
    
    constructor : function(config){
         Ext.ux.state.SessVarsProvider.superclass.constructor.call(this);
         Ext.apply(this, config);
	 this.state = sessvars;
    },
	
    // override
    get : function(name, defaultValue) {
        return typeof this.state[name] == "undefined" ?
            defaultValue : this.state[name];
    },
	
    // override
    set : function(name, value){
        Ext.ux.state.SessVarsProvider.superclass.set.call(this, name, value);
    },
	
    /**
     * Clear out all the remembered state values 
     */
    clearAll : function(){
	sessvars.$.clearMem();
    }
	
});

Step 2: Use the session provider, put this in Ext.onReady

Ext.state.Manager.setProvider(new Ext.ux.state.SessVarsProvider());

Step 3: Set up the fields to save state. Each field that is going to save state needs the following added to their config:

	stateful: true,
	stateId: 'myStateId',
	stateEvents: ['mystateevent'],
	getState: function() { return this.getValue(); },
	applyState: function(state) { this.setValue(state); }			

Step 4: Can use standard events, or custom events. To make the fields aware of a custom event in my app, I had to forward the event like this:

    MyPanel = Ext.Extend(Ext.FormPanel, {
        initComponent : function() {

                 MyPanel.superclass.initComponent();

                 // relay mystateevent to children
                 this.basicFS.getComponent('searchText').relayEvents(this, 'mystateevent');
        }
    });

Ext JS 4 – Creating and Using a State Provider
Version Info: Ext JS 4.1.1
1. The session provider, found at gist Ext.ux.state.SessVarsProvider.js

2. Use the session provider, put this in Ext.onReady

Ext.state.Manager.setProvider(Ext.create('Ext.ux.state.SessVarsProvider'));

3. Set up the fields to save state. Each field that is going to save state needs the following added to their config. Only difference w/my Ext JS 3 version is that the State manager expects the state info to be a property.

	stateful: true,
	stateId: 'myStateId',
	stateEvents: ['mystateevent'],
	getState: function() { return {val:this.getValue()}; },
	applyState: function(state) { 
		if (typeof state.val === 'undefined') {
			state.val = '';
		}
		this.setValue(state.val);
	}			

4. Can call directly, use standard events, or custom events. To make the fields aware of a custom event in my app, I had to forward the event like this:

    MyPanel = Ext.Extend(Ext.FormPanel, {
        initComponent : function() {
              // whatever init is needed
              this.callParent(arguments);

              // relay mystateevent to children
              this.down('#myItemId').relayEvents(this, ['mystatevent']);
        }
    });

Helpful links:
Session variables without cookies
Alternative Client-Side Storage using Sessvars.js
Ext.state.Manager server backup session provider
Events and ExtJS This finally helped me understand “relayEvent”.
And of course the Ext JS API docs, have a lot for state providers.