In this article we will review how you can save the state of your Ext JS components on the server side. Let me give you an example so you can understand what this feature allows you to do.
Suppose that you created an Ext JS application where one of the screens renders an Ext JS grid with a set of records. You have set up this grid so the application’s users can sort, rearrange, hide or show grid columns as needed. And you want that when any user runs the app, the app remembers which columns of the grid were visible, what was the order of the columns, and how the grid was sorted the previous time the user ran the app, regardless of whether it was run on the same or a different computer or mobile device.
Having an app that remembers the state of its UI for a given user makes users feel like the app is working for them and not the other way around. This feature is particularly important in large applications with complex user interfaces. It avoids frustration and saves users time by not having to rearrange user interface components every time they go in the app.
To illustrate this feature we will create a small sample application with an Ext JS grid that displays data related to a hypothetical model cars collection. We will give the grid the ability to save the state of its columns (visible or invisible, column order and sort) to the server, as well as load this state from the server when the application is launched.
Before we continue, remember that you can find more articles like this in the Ext JS tutorials section of my blog MiamiCoder.com. Check it out here: Ext JS tutorials by MiamiCoder.
An Introduction to Ext JS Component State Persistence
The Ext JS Framework gives some of its Components the ability to load the state of a number of their properties from a source external to the component itself. Those components also have the ability to save the state of such properties to the external source.
For example, Panels can save or retrieve their width, height and collapsed state. Grids can do the same with their column order and visibility, store filters, store sorters and store groupers, in addition to their width, height and collapsed state.
The heart of this mechanism resides inside the state aware component itself, where two methods, initState and saveState, are in charge of retrieving and saving the values of the component’s stateful properties. These methods are injected into the component via the Ext.state.Stateful mixin in Ext JS 5.
For the methods to work, you need to both set the component’s stateful config to true, and assign a value for the stateId config. The initState method is invoked inside the Component’s constructor. The saveState method is invoked when one of the events listed in the Components stateEvents config fires.
The stateful config allows you to turn on state awareness on a per-component basis. This is how you activate the feature only for those components where it makes sense based on your application’s functional requirements.
The stateId config is needed because it allows the Ext JS Framework to map the values being stored, or retrieved, to a specific component. This is how the Framework knows that width = 400 and height = 250 should be applied to the “Earnings” Panel in your app and not to the “Growth” Panel, for example.
What happens inside the initState and saveState methods? The following diagram will give you an idea.
Assuming that the Component has a valid stateful and stateId configs, the initState method delegates the retrieval of the values for the stateful properties to the Ext.state.Manager global object. It does so by invoking the State Manager’s get method, passing in the stateId value. A single instance of the State Manager is available to all components that need it.
As the diagram indicates, the State Manager in turn delegates down to the designated State Provider the task of retrieving or saving the stateful values from and to the external storage source.
Ext JS currently offers two choices of external storage sources, cookies and LocalStorage, available through the Ext.state.CookieProvider and Ext.state.LocalStorageProvider Classes. These providers are very convenient when you want to preserve component state across user sessions on the same computer or mobile device.
Maintaining State Across User Sessions and Devices in Ext JS
In many cases you also want to preserve state for the same user from different computers or mobile devices. This feature is very valuable in large applications with complex user interfaces, as I mentioned at the beginning of this article.
Cross-device state persistence requires that you save state to a location from where it can be shared across devices. One approach that you can take to accomplish this is to send and pull state from a database on the server. However, Ext JS doesn’t offer a built-in State Provider for this case.
You can solve this challenge by rolling out your own State Provider that implements the Ext.state.Provider Class. This is the third option that I drew in the diagram above.
What you want is a State Provider that sends requests to an HTTP endpoint that in turn saves or retrieves state data from a database. This is precisely what we will do in this article. Let’s get started with our example.
Creating an Ext JS Project
We will start by creating the Ext JS project for our example. Go ahead and pick a root directory for the project on your workstation, then create a directories hierarchy like this:
This is a pretty common Ext JS project structure where you find the app, model, store and view directories, with the exception of the util directory, which I will explain in a few minutes.
The next step is to create the file that hosts the Ext JS application. Let’s name it grid-stateful-server-csharp-backend.html and place it in the project’s root directory.
As it is customary, we will place the references to the Ext JS libraries and our application’s main file, in the host file:
<!DOCTYPE HTML > <html> <head> <title></title> <link href="../../../Lib/extjs/5.1.0/packages/ext-theme-neptune/build/resources/ext-theme-neptune-all.css" rel="stylesheet" /> <script src="../../../Lib/extjs/5.1.0/ext-all.js"></script> <script src="../../../Lib/extjs/5.1.0/packages/ext-theme-neptune/build/ext-theme-neptune.js"></script> <script src="app.js"></script> </head> <body> </body> </html>
Make sure that the css and script references to the Ext JS library in the head section of the html document are pointing to the correct directories in your development workstation. The references above are for my development environment.
Now we will create the app.js file in the root directory of the project:
Let’s open app.js and type the following Ext JS 5 application definition:
Ext.application({ name: 'App', models: ['ModelCar'], stores: ['ModelCars'], views: ['Viewport'] });
In the application definition we declared the ModelCar Model, ModelCars Store and the Viewpot View. Let’s work on these components next.
Creating the Ext JS Model
Now we are going to create an Ext JS Model to represent a model car. First we need to create the ModelCar.js file in the model directory of the project:
Here’s the model definition:
Ext.define('App.model.ModelCar', { extend: 'Ext.data.Model', fields: [ { name: 'id', type: 'int' }, { name: 'category', type: 'string' }, { name: 'name', type: 'string' }, { name: 'vendor', type: 'string' } ] });
The ModelCar’s fields include the id, category, name and vendor of a model car.
Creating the Ext JS Store
The ModelCars Ext JS Store will feed data to the grid. Let’s create the ModelCars.js file in the store directory of the application:
This is how we will define the store in the file:
Ext.define('App.store.ModelCars', { extend: 'Ext.data.Store', model: 'App.model.ModelCar', sorters: ['name'], data: [ { 'id': 1, 'category': 'Trucks and Buses', 'name': '1940 Ford Pickup Truck', 'vendor': 'Motor City Art Classics' }, { 'id': 2, 'category': 'Trucks and Buses', 'name': '1957 Chevy Pickup', 'vendor': 'Gearbox Collectibles' }, { 'id': 3, 'category': 'Classic Cars', 'name': '1972 Alfa Romeo GTA', 'vendor': 'Motor City Art Classics' }, { 'id': 4, 'category': 'Motorcycles', 'name': '2003 Harley-Davidson Eagle Drag Bike', 'vendor': 'Studio M Art Models' }, { 'id': 5, 'category': 'Motorcycles', 'name': '1996 Moto Guzzi 1100i', 'vendor': 'Motor City Art Classics' }, { 'id': 6, 'category': 'Classic Cars', 'name': '1952 Alpine Renault 1300', 'vendor': 'Studio M Art Models' }, { 'id': 7, 'category': 'Classic Cars', 'name': '1993 Mazda RX-7', 'vendor': 'Motor City Art Classics' }, { 'id': 9, 'category': 'Classic Cars', 'name': '1965 Aston Martin DB5', 'vendor': 'Motor City Art Classics' }, { 'id': 10, 'category': 'Classic Cars', 'name': '1998 Chrysler Plymouth Prowler', 'vendor': 'Unimax Art Galleries' }, { 'id': 11, 'category': 'Trucks and Buses', 'name': '1926 Ford Fire Engine', 'vendor': 'Studio M Art Models' }, { 'id': 12, 'category': 'Trucks and Buses', 'name': '1962 Volkswagen Microbus', 'vendor': 'Unimax Art Galleries' }, { 'id': 13, 'category': 'Trucks and Buses', 'name': '1980’s GM Manhattan Express', 'vendor': 'Motor City Art Classics' }, { 'id': 14, 'category': 'Motorcycles', 'name': '1997 BMW F650 ST', 'vendor': 'Gearbox Collectibles' }, { 'id': 15, 'category': 'Motorcycles', 'name': '1974 Ducati 350 Mk3 Desmo', 'vendor': 'Motor City Art Classics' }, { 'id': 16, 'category': 'Motorcycles', 'name': '2002 Yamaha YZR M1', 'vendor': 'Motor City Art Classics' } ] });
For the purposes of this example we are hard-coding a number of records in the Store using its data config. In a real-world application you will probably use a proxy to pull data from the server.
Creating the Viewport
The mission of the Viewport in this simple example is to host the Model Cars Grid. We will create its file, Viewport.js, in the view directory:
Let’s type the following code in the file in order to define the viewport:
Ext.define('App.view.Viewport', { extend: 'Ext.container.Viewport', requires: ['Ext.grid.Panel'], style: 'padding:25px', layout: 'vbox', items: [ { xtype: 'gridpanel', stateful: true, stateId:'model-cars-grid', width: 650, height: 350, title: 'Ext JS Grid - Stateful - Server', store: 'ModelCars', columns: [ { text: 'Id', hidden: true, dataIndex: 'id', stateId: 'col-id' }, { text: 'Name', sortable: true, dataIndex: 'name', flex: 3, stateId: 'col-name' }, { text: 'Vendor', sortable: true, dataIndex: 'vendor', flex: 2, stateId: 'col-vendor' }, { text: 'Category', sortable: true, dataIndex: 'category', flex: 2, stateId: 'col-category' } ] } ] });
The Viewport hosts the Model Cars Grid. Pay special attention to the Grid’s stateful and stateId configs. As we discussed earlier, they are set to turn on the state awareness features of the Grid.
Creating the State Provider
One of the most interesting parts of this tutorial is creating a custom State Provider that will allow us to interchange state data with the server. Let’s begin by creating the HttpStateProvider.js file in the project’s util directory:
Now, type the following State Provider implementation in the file:
Ext.define('App.util.HttpStateProvider', { extend: 'Ext.state.Provider', requires: ['Ext.state.Provider', 'Ext.Ajax'], alias: 'util.HttpProvider', config: { userId: null, url: null, stateRestoredCallback: null }, constructor: function (config) { if (!config.userId) { throw 'App.util.HttpStateProvider: Missing userId'; } if (!config.url) { throw 'App.util.HttpStateProvider: Missing url'; } this.initConfig(config); var me = this; me.restoreState(); me.callParent(arguments); }, set: function (name, value) { var me = this; if (typeof value == 'undefined' || value === null) { me.clear(name); return; } me.saveStateForKey(name, value); me.callParent(arguments); }, // private restoreState: function () { var me = this, callback = me.getStateRestoredCallback(); Ext.Ajax.request({ url: me.getUrl(), method: 'GET', params: { userId: me.getUserId(), }, success: function (response, options) { var result = JSON.parse(response.responseText.trim()); for (var property in result) { if (result.hasOwnProperty(property)) { me.state[property] = me.decodeValue(result[property]); } } if (callback) { callback();} }, failure: function () { console.log('App.util.HttpStateProvider: restoreState failed', arguments); if (callback) { callback(); } } }); }, // private clear: function (name) { var me = this; me.clearStateForKey(name); me.callParent(arguments); }, // private saveStateForKey: function (key, value) { var me = this; Ext.Ajax.request({ url: me.getUrl(), method: 'POST', params: { userId: me.getUserId(), key: key, value: me.encodeValue(value) }, failure: function () { console.log('App.util.HttpStateProvider: saveStateForKey failed', arguments); } }); }, // private clearStateForKey: function (key) { var me = this; Ext.Ajax.request({ url: me.getUrl(), method: 'DELETE', params: { userId: me.getUserId(), key: key }, failure: function () { console.log('App.util.HttpStateProvider: clearStateForKey failed', arguments); } }); } });
Let’s review this code in detail. First, note how we use the extend config to define that the Class’s parent is Ext.state.Provider.
Next, we define the userId, url and stateRestoredCallback properties inside the config object. We will use them to store the id of the person using the application, the url of the server endpoint that will receive the state data, and the callback method that we will invoke after the state data is received from the server.
The reason why we are saving the user’s id is that we want to save state for each user of the app. This means that upon application launch we need to authenticate the user through a logon screen or other means. This topic is outside the scope of this article. If you are curious about how to do this you can refer to the Adding a Login Screen to a Sencha Touch Application articles that I wrote some time ago. While it is a Sencha Touch implementation, you can easily port it to Ext JS.
Why the stateRestoredCallback callback? We use this callback because we are going to talk to the server asynchronously through Ajax calls that return their results via callback functions.
Moving on with our walkthrough of the HttpStateProvider’s implementation, the next stop is the Class’s constructor. In it, we first check that the required userId and url configurations are being fed to the constructor. Then, we call the Class’s initConfig and restoreState methods prior to executing the constructor of the parent Class by invoking the callParent method:
constructor: function (config) { if (!config.userId) { throw 'App.util.HttpStateProvider: Missing userId'; } if (!config.url) { throw 'App.util.HttpStateProvider: Missing url'; } this.initConfig(config); var me = this; me.restoreState(); me.callParent(arguments); },
The restoreState method is where we retrieve the state from the server. We will review its implementation in a minute.
Right after the constructor comes the set method, which takes a name and a value as arguments. The value argument is the state data, the name argument is the key that identifies to which component the state belongs:
set: function (name, value) { var me = this; if (typeof value == 'undefined' || value === null) { me.clear(name); return; } me.saveStateForKey(name, value); me.callParent(arguments); },
This method delegates the actual saving of the state to the saveStateForKey method, which we will review shortly. Note that if the state value passed is not defined or null, we invoke the clear method, which will ask the server to clear the saved state for the given key.
As I said earlier, the restoreState method is where we retrieve the state data from the server:
restoreState: function () { var me = this, callback = me.getStateRestoredCallback(); Ext.Ajax.request({ url: me.getUrl(), method: 'GET', params: { userId: me.getUserId(), }, success: function (response, options) { var result = JSON.parse(response.responseText.trim()); for (var property in result) { if (result.hasOwnProperty(property)) { me.state[property] = me.decodeValue(result[property]); } } if (callback) { callback();} }, failure: function () { console.log('App.util.HttpStateProvider: restoreState failed', arguments); if (callback) { callback(); } } }); },
We pull the state data from the server through an Ajax request. The request that sends the user’s id over to the server so the server can pull the user’s state records from the database.
In the success callback of the Ajax request, we deserialize the state data sent by the server and save it into the Providers’ state object. The data sent by the server is a dictionary where each entry is a key-value pair with the stateId and state data for a state-aware component in the application. We assign each of these entries as a property to the state object of the provider.
Once we saved the server data into the state object, we invoke the method’s callback to signal that fresh state data is ready for consumption.
Moving on, in the clear method we attempt to clear the state for the supplied key by invoking the clearStateForKey method:
clear: function (name) { var me = this; me.clearStateForKey(name); me.callParent(arguments); },
In the saveStateForKey method we send a POST request to the server. The request carries the state data to be saved, the identifier of the component submitting its state, and the id of the application’s user:
saveStateForKey: function (key, value) { var me = this; Ext.Ajax.request({ url: me.getUrl(), method: 'POST', params: { userId: me.getUserId(), key: key, value: me.encodeValue(value) }, failure: function () { console.log('App.util.HttpStateProvider: saveStateForKey failed', arguments); } }); },
Finally, in the clearStateForKey method we send a DELETE request to the server with the user’s id and the stateId of the component whose state we want to remove from the database:
clearStateForKey: function (key) { var me = this; Ext.Ajax.request({ url: me.getUrl(), method: 'DELETE', params: { userId: me.getUserId(), key: key }, failure: function () { console.log('App.util.HttpStateProvider: clearStateForKey failed', arguments); } }); }
Setting Up the Ext JS Application to Use the Custom State Provider
Earlier in this article we talked about how the global State Manager (Ext.state.Manager) object delegates to the State Provider, in this case the provider we just created, the saving and retrieval of state data. This means that at some point during the application’s launch process we need to make the application’s State Manager aware of the State Provider that it needs to use.
We can accomplish this inside the application’s launch function. Let’s go back to the app.js file and modify the launch function as shown in the following code:
Ext.application({ name: 'App', requires: ['App.util.HttpStateProvider'], models: ['ModelCar'], stores: ['ModelCars'], views: ['Viewport'], launch: function () { var v; Ext.state.Manager.setProvider(new App.util.HttpStateProvider({ userId: 'test user', url: '../../../api/StatefulGridExample', stateRestoredCallback: function () { if (!v) { v = Ext.create('App.view.Viewport'); } }})); } });
In the launch function we call the State Manager’s setProvider method, passing a new instance of the HttpStateProvider Class.
Notice the values of the userId, url and satateRestoredCallback configs of the Provider. The user’s id is hard-coded because user authentication is outside of the scope of this tutorial. The url value is the url of the server endpoint that we will create in a few minutes.
The stateRestoredCallback is an inline function where we simply create the application’s viewport. You need to be aware that this means that the viewport will be created after the restore state Ajax call to the server returns. As a consequence, there will be a delay that can be significant before the application’s user interface is functional.
At this point our work on the Ext JS application’s side is completed and we can jump to the server side.
Creating the Application State Table in the Database
Let’s start our work on the server side by creating the database table that will store state data for the application.
I chose MySQL as the database technology for this example. You can use your favorite database engine as long as you make sure that the table where the state data goes has the correct structure. What we are aiming for is a table where you can record the user’s id, the state key and the state data values sent by the State Provider that we just coded.
Here’s the table definition that I used for my MySQL environment:
CREATE TABLE `app_state` ( `id` int(11) NOT NULL AUTO_INCREMENT, `state_user_id` varchar(45) NOT NULL, `state_key` varchar(45) NOT NULL, `state_value` varchar(4000) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `id_UNIQUE` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
Creating the Server-Side StateData Class
I chose C# as the server-side language for this example. The first piece of C# code that we will write is a server-side Class that will represent a unit of state data sent from the Ext JS State Provider.
Let’s go ahead and create the StateData.cs file in the project’s root directory:
This is the Class’s definition that you need to type in the file:
public class StateData { public string UserId { get; set; } public string Key { get; set; } public string Value { get; set; } }
The Class’s structure is very similar to the table structure that we will use in the database, as you might expect. It also matches the structure of the state data sent by the State Provider in the Ext JS application.
Let’s move on to creating the server endpoint so you can see how all the pieces fit together.
Creating the Server-Side Controller
We are going to create a RESTful endpoint using a ASP.NET Web API Controller. First we need to create the file for the Controller, which we will name StatefulGridExampleController.cs.
In the file we are going to create an empty ASP.NET Web API Controller, with a variable that will hold the connection string to our MySQL database:
public class StatefulGridExampleController : ApiController { private string connString = "server=127.0.0.1;uid=[USER];pwd=[PASSWORD];database=extjs-training;"; }
Saving State
The first Controller method will handle requests to save state to the database. These POST requests originate in the saveStateForKey method of the HttpStateProvider instance in the Ext JS application. Let’s add a POST handler to the server-side Controller using the following code:
public Dictionary<string, string> Post(StateData stateData) { int count = 0; using (MySqlConnection cn = new MySqlConnection(connString)) { using (MySqlCommand cmd = new MySqlCommand()) { cmd.CommandText = string.Format("SELECT COUNT(id) FROM `extjs-training`.`app_state` where state_user_id = '{0}'", stateData.UserId); cmd.CommandType = System.Data.CommandType.Text; cmd.Connection = cn; cn.Open(); object countObject = cmd.ExecuteScalar(); count = int.Parse(countObject.ToString()); if (count > 0) { cmd.CommandText = string.Format("update `extjs-training`.`app_state` set state_value = '{0}' where state_user_id = '{1}' and state_key = '{2}'", stateData.Value, stateData.UserId, stateData.Key); } else { cmd.CommandText = string.Format("insert into `extjs-training`.`app_state` (state_user_id, state_key, state_value) values ('{0}', '{1}', '{2}')", stateData.UserId, stateData.Key, stateData.Value); } cmd.CommandType = System.Data.CommandType.Text; cmd.ExecuteNonQuery(); cn.Close(); } } Dictionary<string, string> result = new Dictionary<string, string>(); result.Add("success", "true"); return result; }
The Post method is fed an instance of the StateData Class, which contains the state information sent from the Ext JS application. The process of parsing out the state data carried byt the HTTP request and creating the StateData instance is handled by .NET for us.
Inside the Post method, we check if the state table contains an entry for the user and state key combination sent from the Ext JS app. If this is the case, we update the entry with the new state data. If not, we create an entry for the given user and state key.
After saving the state data, we return a success response back to the State Provider in the Ext JS application.
Retrieving State Data
The next Controller method that we need to create will handle requests to get state from the database. These requests originate in the restoreState method of the HttpStateProvider instance in the Ext JS application.
If you review the restoreState method, you will notice that state retrieval happens on a per-user basis, meaning that all the state records that exist for the given user are retrieved together. Let’s add the Controller method using the following code:
public Dictionary<string, string> Get(string userId) { Dictionary<string, string> stateData = new Dictionary<string, string>(); using (MySqlConnection cn = new MySqlConnection(connString)) { using (MySqlCommand cmd = new MySqlCommand()) { cmd.CommandText = "SELECT state_key, state_value FROM `extjs-training`.`app_state` WHERE state_user_id = '" + userId + "'"; cmd.CommandType = System.Data.CommandType.Text; cmd.Connection = cn; cn.Open(); MySqlDataReader reader = cmd.ExecuteReader(); while (reader.Read()) { stateData.Add(reader.GetString("state_key"), reader.GetString("state_value")); } cn.Close(); } } return stateData; }
In the Get method, we pull the state data for the provided userId value from the database and package it as a dictionary of key-value pairs that is sent in JSON format back to the Ext JS application.
Deleting State Data
Finally, we will create the Controller method that will handle requests to delete the state data for a given combination of user id and state key. These requests originate in the clearStateForKey method of the HttpStateProvider instance used in the Ext JS application.
The Ajax requests to delete state data use the DELETE Http verb. In the Controller, we will create the delete method like so:
public Dictionary<string, string> Delete(string userId, string key) { using (MySqlConnection cn = new MySqlConnection(connString)) { using (MySqlCommand cmd = new MySqlCommand()) { cmd.CommandText = string.Format("delete FROM `extjs-training`.`app_state` WHERE state_user_id ='{0}' and state_key = '{1}'", userId, key); cmd.CommandType = System.Data.CommandType.Text; cmd.Connection = cn; cn.Open(); cmd.ExecuteNonQuery(); cn.Close(); } } Dictionary<string, string> result = new Dictionary<string, string>(); result.Add("success", "true"); return result; }
Inside the method, we execute a database command to delete the state record that matches the user id and state key passed in.
Testing the Ext JS State Provider
At this point the server side is ready and we can go ahead and test the application. You can perform the test by executing the following steps:
- Launch the application in your favorite browser and make changes to the Model Cars Grid’s sort order, columns order and columns visibility.
- Close your browser.
- Open a new browser session and launch the application again. When the grid is rendered, it should show the same sort order and column properties that you set in the prior session.
- If you are hosting the application on a shared web server, launch the application from a different browser, or from a different computer or mobile device. When the grid is rendered, it should again show the same sort order and column properties that you set in the prior session.
Summary and Next Steps
In this article we reviewed the concepts that make possible state-aware components in Ext JS. We discussed why this feature is important, and learned how to create a custom State Provider that allows you to save the state of your Ext JS application’s components on the server side.
You can continue learning about Ext JS in the Ext JS tutorials section of my blog MiamiCoder.com. Check it out here: Ext JS tutorials by MiamiCoder.
Make sure to sign up for my newsletter so you can be among the first to know when my next tutorial is available.
Download the Source Code
Download the source code for this tutorial: Custom Ext JS State Provider source code.
Stay Tuned
Don’t miss out on the updates! Make sure to sign up for my newsletter so you can get MiamiCoder’s new articles and updates sent free to your inbox.
The post Creating a Custom Ext JS State Provider appeared first on MiamiCoder.