Thursday, July 15, 2010

How to add, edit or delete a record from an ArrayStore

CAUTION: Copy and Pasters be warned. This is a tutorial and the code provided here is for EXPLANATORY purposes ONLY! DO NOT use this code in a live environment!

VERSION 1.2 - I've fortunately learned more as I've gone along, and have updated the code here to reflect my optimisations. You won't notice anything (unless you remember it verbatim from the previous version), just no that the code has been improved!

Ok this will be my first tutorial for, well basically anything really, so please be patient as I tend to waffle.

ExtJS came to my attention in June, 2010 and I have been tinkering with it for a couple of weeks. When I first started I was impressed with the UI components but then when I went to build something from scratch I was dumbfounded by the lack of decent tutorials out there.

ExtJS comes with a fantastic API documentation viewer which I have opened constantly, and a pretty decent set of examples to look at, but there is a MASSIVE gap when it comes to demonstrating something to actually explaining it. This is what I hope to achieve with these tutorials.

Scenario

You have a grid which is connected to an ArrayStore. You want your users to be able to add new records, edit or delete existing records in a grid but don't want to push the data server side first in order to re-render the page with the new data in the ArrayStore.

Details

I typically like to get the user to add content to the client side controls first then when the user clicks 'save', I'll push the entire data store back to the server. That way, the user can modify existing records, delete existing records and add new records to the data set and we don't have to worry about what records to update, delete and add, you just get the entire data set, once, after all mods have been done.

This also allows a sense of 'undo' in that you can just reload the data store if you wish to undo every single change a user has done if they want to (ie. revert).

In this scenario we have a list of users that we're going to hook up to some type of user administration system.

The grid will load on the page with an ArrayStore. Don't worry about loading data from ther server at this point, this tutorial is simply to explain how to add a new record. Populating the store would probably come from an JsonStore whereby the dev will specify a URL to retrieve the initial dataset.

Code

A live example of this code can be found at: http://www.thebestdevs.com/extjs/index.html

<html lang="en">
<head>
<title>Add a Record to ArrayStore Tutorial</title>

<link rel="stylesheet" type="text/css" href="scripts/extjs/resources/css/ext-all.css" />
<link rel="stylesheet" type="text/css" href="styles/main.css" />

<script type="text/javascript" src="scripts/extjs/adapter/ext/ext-base.js"></script>
<script type="text/javascript" src="scripts/extjs/ext-all.js"></script>
<script type="text/javascript">

Ext.BLANK_IMAGE_URL = "scripts/extjs/resources/images/default/s.gif";

var userWindow = null;

Ext.onReady(function() {
var store = new Ext.data.ArrayStore({
fields: [
'userID'
, 'username'
, 'password'
, 'active'
]
, data: [
[1, 'user1', 'pass1', 'Yes'],
[2, 'user2', 'pass2', 'Yes'],
[3, 'user3', 'pass3', 'Yes'],
[4, 'user4', 'pass4', 'Yes'],
[5, 'user5', 'pass5', 'Yes'],
[6, 'user6', 'pass6', 'Yes'],
[7, 'user7', 'pass7', 'Yes'],
[8, 'user8', 'pass8', 'Yes'],
[9, 'user9', 'pass9', 'Yes'],
]
});

new Ext.grid.GridPanel({
store: store
, border: false
, id: 'usersGrid'
, colModel: new Ext.grid.ColumnModel({
defaults: {
width: 400
, sortable: true
}
, columns: [{
id: 'userID'
, header: 'userID'
, dataIndex: 'userID'
, hidden: true
}, {
header: 'Username'
, sortable: true
, dataIndex: 'username'
}, {
header: 'Password'
, sortable: true
, dataIndex: 'password'
}, {
header: 'Active'
, sortable: true
, dataIndex: 'active'
}]
})
, viewConfig: {
forceFit: true
}
, sm: new Ext.grid.RowSelectionModel({
singleSelect: false
})
, autoWidth: true
, autoHeight: true
, frame: false
, iconCls: 'icon-grid'
, renderTo: 'tutorialGrid'
, tbar: [{
text: 'Add New User'
, handler: function(b, e) {
ShowUserDetailsWindow();
}
}, {
text: 'Edit Selected User'
, handler: function(b, e) {
EditSelectedUser(Ext.getCmp('usersGrid'));
}
, disabled: true
, id: 'btnEditUser'
}, {
text: 'Delete Selected User'
, handler: function(b, e) {
DeleteSelectedUsers();
}
, disabled: true
, id: 'btnDelUser'
}]
, listeners: {
rowdblclick: function(grid, rowIndex, evt) {
EditSelectedUser(grid);
}
, rowclick: function(grid, rowIndex, evt) {
var editButton = Ext.getCmp('btnEditUser');
var delButton = Ext.getCmp('btnDelUser');
var selections = Ext.getCmp('usersGrid').getSelectionModel().getSelections();
var enableButtons = true;

if (selections.length == 1) {
editButton.setText('Edit Selected User');
delButton.setText('Delete Selected User');
} else if (selections.length == 0) {
enableButtons = false;
} else {
editButton.setText('Edit First Selected User');
delButton.setText('Delete Selected Users');
}

if (enableButtons) {
editButton.enable();
delButton.enable();
} else {
editButton.disable();
delButton.disable();
}
}
}
});
});

function ShowUserDetailsWindow() {
if (userWindow == null) {
userWindow = new Ext.Window({
layout: 'fit'
, width: 275
, height: 200
, closeAction: 'hide'
, plain: true
, title: 'User Details Dialog'
, buttons: [{
text: 'Save'
, handler: function() {
SaveUser();
}
}, {
text: 'Cancel'
, handler: function() {
userWindow.hide();
}
}]
, items: [{
border: false
, layout: 'form'
, defaultType: 'textfield'
, bodyStyle: 'padding: 10px'
, items: [{
fieldLabel: 'User ID'
, id: 'txtUserID'
, name: 'txtUserID'
}, {
fieldLabel: 'Username'
, id: 'txtUsername'
, name: 'txtUsername'
}, {
fieldLabel: 'Password'
, id: 'txtPassword'
, name: 'txtPassword'
}, {
fieldLabel: 'Active'
, xtype: 'checkbox'
, id: 'cbxActive'
, name: 'cbxActive'
}]
}]
});
}

userWindow.show(this);
}

function SaveUser() {
var userID = Ext.getCmp('txtUserID').getValue();
var username = Ext.getCmp('txtUsername').getValue();
var password = Ext.getCmp('txtPassword').getValue();
var active = Ext.getCmp('cbxActive').getValue() == true ? 'Yes' : 'No';

var userRecord = GetUserRecord(userID);
if (userRecord != null) {
userRecord.beginEdit();
userRecord.set('username', username);
userRecord.set('password', password);
userRecord.set('active', active);
userRecord.endEdit();
userRecord.commit(false);
} else {
var store = Ext.getCmp('usersGrid').getStore();
store.add(new store.recordType({
userID: userID
, username: username
, password: password
, active: active
}));
}

userWindow.hide();
ClearFields();
}

function ClearFields() {
Ext.getCmp('txtUserID').setValue('');
Ext.getCmp('txtUsername').setValue('');
Ext.getCmp('txtPassword').setValue('');
Ext.getCmp('cbxActive').setValue(false);
}

function GetUserRecord(userID) {
var store = Ext.getCmp('usersGrid').getStore();
var recordIndex = store.find('userID', userID);
if (recordIndex > -1) {
return store.getAt(recordIndex);
} else {
return null;
}
}

function EditSelectedUser(grid) {
var selectedUser = grid.getSelectionModel().getSelected();

ShowUserDetailsWindow();
Ext.getCmp('txtUserID').setValue(selectedUser.data.userID);
Ext.getCmp('txtUsername').setValue(selectedUser.data.username);
Ext.getCmp('txtPassword').setValue(selectedUser.data.password);
Ext.getCmp('cbxActive').setValue(selectedUser.data.active == 'Yes' ? true : false);
}

function DeleteSelectedUsers() {
Ext.Msg.show({
title: 'Delete Confirmation'
, msg: 'Are you sure you wish to delete the selected users?'
, buttons: Ext.Msg.YESNO
, icon: Ext.Msg.QUESTION
, fn: function(btnID, text, opt) {
if (btnID == 'yes') {
var grid = Ext.getCmp('usersGrid');
var selectedUsers = grid.getSelectionModel().getSelections();

for (var i = 0; i < selectedUsers.length; i++) {
grid.store.remove(selectedUsers[i]);
}
}
}
});
}

</script>
</head>
<body>
<div id='tutorialGrid'>

</div>
</body>
</html>

These tags simply stand as a placeholder for the grid (I hate it when tutorials renderTo: document.body :P) and close up the HTML document.

The main script

Ok let's go examine the functions within the main script. Here they are and a subsequent explanation (usually these details are included as comments in the code but I've extracted them here for you).


Ext.onReady


This is executed when ExtJS determines that all the components in the DOM are available for use. It is also where I've decided to declare the ArrayStore and GridPanel. Typically these could be declared somewhere else but I've left them here for the tutorial.


ShowUserDetailsWindow()


This method is responsible for showing the User Details Dialog, and if it doesn't exist (ie. first time it's been requested) then it will create the actual window, then show it.


SaveUser()


This method is responsible for taking the user details entered in the User Details Dialog and saving them to the ArrayStore. If the record doesn't exist (indicated by the userID not being found in the ArrayStore), a new record is created and appended to the ArrayStore. If it exists (indicated by the userID being found in the ArrayStore) then the record data is updated with the details from the User Details Dialog.


ClearFields()


This is a utility method designed to reset the form. I was looking for a generic reset() method for the FormPanel but haven't found one as yet (doesn't mean it doesn't exist, just that I haven't found it yet :P).


GetUserRecord(userID)


This method looks through the ArrayStore for a record with the specified userID. If one is found, the record is returned otherwise NULL is returned.


EditSelectedUser(grid)


This method is responsible for rendering the selected users' data in the User Details Dialog and then showing the dialog.


DeleteSelectedUsers()


This method is responsible for confirming the deletion request (in case it was an accidental click) and if confirmed, for every selected user, removing it from the ArrayStore.

The gory details

Ok so let's delve into the gory details of each method.

The ArrayStore


var store = new Ext.data.ArrayStore({
fields: [
'userID'
, 'username'
, 'password'
, 'active'
]
, data: [
[1, 'user1', 'pass1', 'Yes'],
[2, 'user2', 'pass2', 'Yes'],
[3, 'user3', 'pass3', 'Yes'],
[4, 'user4', 'pass4', 'Yes'],
[5, 'user5', 'pass5', 'Yes'],
[6, 'user6', 'pass6', 'Yes'],
[7, 'user7', 'pass7', 'Yes'],
[8, 'user8', 'pass8', 'Yes'],
[9, 'user9', 'pass9', 'Yes'],
]
});


In it's simplicity, the ArrayStore is a object that contains data and metadata. The metadata (fields array) defines the name and position of each data item within the data array. The data defines the actual data and must match the definitions of the fields metadata. If there is an incongruency, ExtJS typically ignores it and tries its best to work with the data it has. Just put the correct data in the correct positions and you'll be right!

Typically the JsonStore is my store of choice as it allows me to remotely load the initial dataset, plus, I can reload the data at any time without having to rebuild the entire page.

Anyhow, this example is using the ArrayStore. In my travels, people go, just figure it out with another store or whatever object and it pisses me off. I'll give you an example with the JsonStore in another tutorial.

The GridPanel

Let's go through this in chunks...


new Ext.grid.GridPanel({
store: store


We're defining the store as the previously created ArrayStore


, border: false
, id: 'usersGrid'


We've assigned an ID to the grid for future referencing


, colModel: new Ext.grid.ColumnModel({
defaults: {
width: 400
, sortable: true
}
, columns: [{
id: 'userID'
, header: 'userID'
, dataIndex: 'userID'
, hidden: true
}, {
header: 'Username'
, sortable: true
, dataIndex: 'username'
}, {
header: 'Password'
, sortable: true
, dataIndex: 'password'
}, {
header: 'Active'
, sortable: true
, dataIndex: 'active'
}]
})


The column model defines what columns are to be used in rendering the grid, whether they are visible or not, what fields should be displayed in the column (ie. the data field mapping), and a whole bunch more (see http://www.sencha.com/deploy/dev/docs/?class=Ext.grid.ColumnModel for all the options).

In this instance we're defining all the fields that we specified in the ArrayStore. You may or may not want to do this as there is no requirement that you define them if you don't want them.


, viewConfig: {
forceFit: true
}


The View Config represents the GridView (see http://www.sencha.com/deploy/dev/docs/?class=Ext.grid.GridView). At present, this looks to be fairly under utilised by the community, even the examples are fairly brief when it comes to the GridView. So for our example, we're also going to be brief as we truely don't need anything more than just this option.


, sm: new Ext.grid.RowSelectionModel({
singleSelect: false
})


Ah the selection model. The Selection Model, in this case a Row Selection Model (see http://www.sencha.com/deploy/dev/docs/?class=Ext.grid.RowSelectionModel) is used because it's easy, simple and doesn't require any additional configuration.

In this instance the singleSelect is set to false meaning we want to be able to select multiple items instead of just one item at a time, so for us, we can select multiple rows (or users as they be) in order to delete many users simultaneously.


, autoWidth: true
, autoHeight: true
, frame: false
, iconCls: 'icon-grid'


I'm pretty sure these don't need any explanation. Though these can be read on the API (see http://www.sencha.com/deploy/dev/docs/?class=Ext.grid.GridPanel).


, renderTo: 'tutorialGrid'


We're going to render the grid to a DIV called 'tutorialGrid'. It doesn't have to be a div, just something that can be a container. Typically though, a DIV is the safest option.


, tbar: [{
text: 'Add New User'
, handler: function(b, e) {
ShowUserDetailsWindow();
}


The Add New User button simply calls ShowUserDetailsWindow() which brings up the User Details Dialog (or creates it if it doesn't already exist then shows it).


}, {
text: 'Edit Selected User'
, handler: function(b, e) {
EditSelectedUser(Ext.getCmp('usersGrid'));
}
, disabled: true
, id: 'btnEditUser'


The Edit Selected User button has a few tricks to it. Firstly, when rendered the first time it's disabled, meaning that we don't want the user to be able to click Edit without having selected a user first. Why? Because the function that is called ASSUMES that a user is selected. If there isn't a selected user, the functions don't have a contingency plan to deal with the lack of a selection, so to be safe, we disable the button.

Since the only way (that I know of) to select a user in the grid is to physically click on it (I've tried to tab to it but it just tabs over it as I'm assuming that the control doesn't handle the tab stop), we can safely assume (assume!) that the only way to select a user is to click the grid. We have hooked up the CLICK event listener on the Grid to handle some additional functionality to handle enabling the button.

The click handler for the button itself calls the EditSelectedUser() function, passing it a reference to the 'usersGrid'. As we'll see in the EditSelectedUser() function later on, the reference to the grid is to allow for multiple entry points, one being the Grid's Double Click event handler (which already has a reference to the grid).


}, {
text: 'Delete Selected User'
, handler: function(b, e) {
DeleteSelectedUsers();
}
, disabled: true
, id: 'btnDelUser'


Just like the Edit Selected User button, the Delete Selected User button is also disabled by default. The same rules apply to this as does the Edit Selected User button, we can safely 'assume' that the only way to select an item in the user grid is through the grid's click event. This even has additional functionality to handle enabling the button after the event has been fired.

The click handler for the button simply calls the DeleteSelectedUsers() function to handle the removal of the selected user(s).


}]
, listeners: {
rowdblclick: function(grid, rowIndex, evt) {
EditSelectedUser(grid);
}


Ok, double clicking the grid...what would you expect to have happen? Well I expect that double clicking anything 'opens' it. For us in this context, 'opening' a user means to view the details in the User Details Dialog. So for this event handler we simply call EditSelectedUser() and pass in the reference to the grid we got for free as part of the event handler signature.


, rowclick: function(grid, rowIndex, evt) {
var editButton = Ext.getCmp('btnEditUser');
var delButton = Ext.getCmp('btnDelUser');
var selections = Ext.getCmp('usersGrid').getSelectionModel().getSelections();
var enableButtons = true;

if (selections.length == 1) {
editButton.setText('Edit Selected User');
delButton.setText('Delete Selected User');
} else if (selections.length == 0) {
enableButtons = false;
} else {
editButton.setText('Edit First Selected User');
delButton.setText('Delete Selected Users');
}

if (enableButtons) {
editButton.enable();
delButton.enable();
} else {
editButton.disable();
delButton.disable();
}
}


Alrighty, as promised, the row click handler has some additional functionality to handle enabling the buttons, but it also does something extra.

I'm a grammer nazi (google is gonna kill me for referencing that word!) and I ABHOR when developers do not use the correct pluralisation. A single selected user is a 'user' not a 'users'! So the additional logic simply counts how many users were selected and adjusts the buttons' text appropriately.

I'm certainly not a MASTER at grammer but I still like things to read well. :)

For the Edit button, the pluralisation text isn't the same as the Delete button as delete can handle multiple users whereas edit can only handle a single user hence the wording for the edit button.

In any case, since (AFAIK) you can not 'de-select' a grid (ie. once a row is selected, nothing can deselect it other than clicking on another row - which fires the rowclick event anyhow), I don't need to handle the event of deselection which is why the edit and delete buttons will always only be enabled rather than becoming disabled in the future.

Edit: After posting this on the Sencha Forums, Animal pointed me in the right direction with the column deselection. Apparently ExtJS is even more standards compliant (UI wise) than I previously thought. This is an AWESOME thing which basically means that CTRL-Clicking a row will deselect it for you. This is a common feature found in windows ListView controls and ExtJS have appropriately replicated this functionality. I just didn't think to try it (another assumption on my part :P).

I have therefore modified my code to reflect this new information. The rowclick function now detects the number of 'selections' or selected rows that have been made, and based on that number, decides how to control the Edit and Delete buttons.


}
});


The ShowUserDetailsWindow() function

As stated before, this function simply handles the creation (if it didn't previously exist) of the User Details Dialog and it's subsequent 'showing'.


function ShowUserDetailsWindow() {


We have a global (ewwwww) variable called userWindow which is the placeholder for the object after creation. We know nothing hasn't been created before if the variable is NULL.


if (userWindow == null) {
userWindow = new Ext.Window({
layout: 'fit'
, width: 275
, height: 200
, closeAction: 'hide'
, plain: true
, title: 'User Details Dialog'


Basic details for setting up a window (see http://www.sencha.com/deploy/dev/docs/?class=Ext.Window).

, buttons: [{
text: 'Save'
, handler: function() {
SaveUser();


After the user feels they have finished and want to save the user, this click event simply calls the SaveUser() function.


}
}, {
text: 'Cancel'
, handler: function() {
userWindow.hide();


If the user wants to abort we simply hide the window. We don't need to handle the clearing of the form fields here because the user may not have actually changed anything (so there may not be anything in the fields) so we'll leave that cleanup process to later on (I'm a bloke, it's what we do :P).


}
}]
, items: [{
border: false
, layout: 'form'
, defaultType: 'textfield'
, bodyStyle: 'padding: 10px'
, items: [{
fieldLabel: 'User ID'
, id: 'txtUserID'
, name: 'txtUserID'
}, {
fieldLabel: 'Username'
, id: 'txtUsername'
, name: 'txtUsername'
}, {
fieldLabel: 'Password'
, id: 'txtPassword'
, name: 'txtPassword'
}, {
fieldLabel: 'Active'
, xtype: 'checkbox'
, id: 'cbxActive'
, name: 'cbxActive'
}]
}]
});
}


Stock standard form fields catering for the User Object that we've created in the ArrayStore. This form is missing validation and other obvious features that a normal form will have, but it's for demonstrational purposes only :P


userWindow.show(this);
}


At the end, the userWindow object is gonna have SOMETHING in it (whether it's the newly created Ext.Window object or the previously created Ext.Window object), and we can safely 'assume' that it's an Ext.Window object (unless something/someone else has screwed us over), so we can simply just call the show() function to get the window on the screen.

The SaveUser() function

This function is designed to do one of two things based on whether the user details entered are those of a user that already exists in the ArrayStore (ie. this user exists so we're going to update their details) or those of a user that doesn't exist (ie. we need to create a new record and insert it).


function SaveUser() {
var userID = Ext.getCmp('txtUserID').getValue();
var username = Ext.getCmp('txtUsername').getValue();
var password = Ext.getCmp('txtPassword').getValue();
var active = Ext.getCmp('cbxActive').getValue() == true ? 'Yes' : 'No';


Firstly we retrieve the details entered by the user in the User Details Dialog. For the active value we need to convert the checked nature from checked = yes, unchecked = no.


var userRecord = GetUserRecord(userID);


Next we retrieve the user record from the ArrayStore. This function simply check whether the users' ID is found in the ArrayStore. If it is, then since it's a primary key, we know that this is a user that we're going to want to update.

Otherwise it will return NULL and we'll know it's a user that we want to insert (forgive the SQL parlance).


if (userRecord != null) {
userRecord.beginEdit();
userRecord.set('username', username);
userRecord.set('password', password);
userRecord.set('active', active);
userRecord.endEdit();
userRecord.commit(false);


Ok let's go through the API documentation for a moment...According to the Ext.data.Record (see http://www.sencha.com/deploy/dev/docs/?class=Ext.data.Record) documentation, the functions beginEdit() and endEdit() designed to put an existing record into 'edit' mode. During this time, "no events (eg.. the update event) are relayed to the containing store", so edit mode acts like a transaction ensuring that the edit will go through before anything else happens to that record.

The commit() function "Commits all changes made to the Record since either creation, or the last commit operation". So I'm going to assume that the safest way (if not the only way) to update an existing record is by getting the record, entering it into 'edit' mode, make modifications to its' data (it's not specified whether an entity tracking mechanism has been employed or whether it's just assumed that when the endEdit() function is called that the state of the record is 'dirty' causing the standard validation functions to be applied before updating the actual store), call the endEdit() function to specify that all changes that are going to be made to the record have been made, then call commit() to actually save the record back to the store.

According to the documentation, the 'silent' parameter, when set to true, causes the store to fire the 'update' event. This allows anyone who has subscribed to the update event to be notified that the store has been updated.

The grid automatically subscribes to the 'update' event for you during creation of the grid, so updating the store automatically causes the grid to update and redraw itself with the latest data available to the store. So for us, we get a free 'update' call to the grid done on our behalf.

That's all you have to do to update a single records worth of data! Seems like a lot, but it's actually doing a lot of work in the background for you to ensure data integrity, something that'd take you a shit load of effort to implement for each data store you create!


} else {
var store = Ext.getCmp('usersGrid').getStore();
store.add(new store.recordType({
userID: userID
, username: username
, password: password
, active: active
}));


So we found a user record after all huh? Well we are going to assume that the business rules state that finding a record means that you want to update that record, so here, we will apply the user changes to the record and update the store accordingly.

Firstly we grab the store. I could have just used the reference Ext.getCmp('usersGrid') over and over, but I feel that any time you have to make a call to the same function to retrieve the same data, you should store the data in a variable and use that variable over and over again rather than using the CPU cycles to get the same data, over and over again. Ext.getCmp() is a function that does a lot of looping, and to make this call over and over each time to get the same data is bad from an optimisation POV.

Secondly we create a new store.recordType object. Let's look at this further.

According to the documentation (see http://www.sencha.com/deploy/dev/docs/?class=Ext.data.ArrayStore), the ArrayStore.recordType property represents the Record constructor used by the Reader for the ArrayStore.

This function actually represents the Ext.data.Record.create function, so according to the documentation for create, it takes an "Array of Field definition objects".

Now, along with the name of the field, a whole bunch of additional parameters can be passed but for our purposes, we only want to identify the name of the field so we'll just pass those through, along with the values that we want the new record set to.

So now we have a new Ext.data.Record object, fully instantiated with the data from the User Details Dialog. According to the documentation, ArrayStore.add() simply takes an array of Record objects. Well, here's one we prepared earlier!

We take our new Record object, through it into the Add() function of the ArrayStore and presto, our new record is structured correctly, instantiated and is now apart of the ArrayStore. According to the documentation, the Add() function fires the add event, which the GridPanel automatically subscribes to during its creation. The GridPanel treats the add event as a modification to its store and causes that same free update event to be fired automatically for us, so once again we get a free update to the grid with no additional logic.


}

userWindow.hide();
ClearFields();
}


At this point we hide the User Details Dialog and call the ClearFields() utility method.

The ClearFields() function

As stated above, the ClearFields() function simply sets the values for the fields within the User Details Dialog to empty strings.

In the active checkbox case, setting it to false unchecks the checkbox which is the default state for the checkbox.


function ClearFields() {
Ext.getCmp('txtUserID').setValue('');
Ext.getCmp('txtUsername').setValue('');
Ext.getCmp('txtPassword').setValue('');
Ext.getCmp('cbxActive').setValue(false);
}


The GetUserRecord() function

The ArrayStore object exposes a hand function called find() which allows us to look through a store and look for a record with the property that we specified matching the value that we specify. It will return only the first match (typically good enough to know that at least one item (usually only one would exist) does exist within the store).


function GetUserRecord(userID) {
var store = Ext.getCmp('usersGrid').getStore();
var recordIndex = store.find('userID', userID);


We get an index back from the find() function. This index represents the array index position the items sits at within the ArrayStore. Typically knowing this value is not important, only that the value is > -1. -1 means that it was not found. Why? Because arrays are 0-based and returning a value of 0 means that the first element of the array is the one you're looking for instead of 'no matches found', so -1 is returned instead :)


if (recordIndex > -1) {
return store.getAt(recordIndex);


At this point we simply retrieve the actual record and fire it back to the caller


} else {
return null;


Otherwise we return null so that we can test against it.


}
}


The EditSelectedUser() function

This method is designed to open the User Details Dialog and set the form values to their respective record values.


function EditSelectedUser(grid) {
var selectedUser = grid.getSelectionModel().getSelected();


We retrieve the selected user by querying the Selection Model. The Selection Model has a few functions that are handy including the getSelected() and getSelections() functions. If you are expecting multiple selections then you call getSelections(). It will return an array of record objects that have been selected within the grid (or whatever other object you are using). If you are expecting a single one (or just want one from a mutliselect grid) then you call getSelected(). This will return a single (the first) record that has been selected.


ShowUserDetailsWindow();
Ext.getCmp('txtUserID').setValue(selectedUser.data.userID);
Ext.getCmp('txtUsername').setValue(selectedUser.data.username);
Ext.getCmp('txtPassword').setValue(selectedUser.data.password);
Ext.getCmp('cbxActive').setValue(selectedUser.data.active == 'Yes' ? true : false);
}


Lastly we bring up the User Details Dialog (or create it if doesn't already exist) and populate the form fields with the data from the record object.

The 'data' property is a json representation of the data in the store. I'm not sure if this is the best way to retrieve the data from the record, but as yet, I haven't read any other way.

The DeleteSelectedUsers() function

This function asks the user whether they are sure (because you know users, the just click around like there's no tomorrow and do other funky things to our perfect systems :P) that they want to delete the selected users (one or more), and only if the user selects the 'yes' button will the action be exectued.

The delete action involves grabbing an array of selected users (whether it's 0, 1 or more) and physically removing them from the ArrayStore.

According to the documentation, calling the remove() function fires the remove event. Anything subscribed to this event will be told that the ArrayStore has been updated, and this (for us) means fortunately that we get yet another free update call performed for us against the usersGrid. So once again the grid will automatically be updated by the removal call. Sweet huh!


function DeleteSelectedUsers() {
Ext.Msg.show({
title: 'Delete Confirmation'
, msg: 'Are you sure you wish to delete the selected users?'
, buttons: Ext.Msg.YESNO
, icon: Ext.Msg.QUESTION
, fn: function(btnID, text, opt) {


Ask the user whether they are sure.


if (btnID == 'yes') {


Only if they click 'yes'.


var grid = Ext.getCmp('usersGrid');
var selectedUsers = grid.getSelectionModel().getSelections();


Retrieve the selected users from the Selection Model.


for (var i = 0; i < selectedUsers.length; i++) {
grid.store.remove(selectedUsers[i]);


Kill'em baby! Give us that free update call on the grid!


}
}
}
});
}


Conclusion

Manipulating controls such as the GridPanel are the bread and butter of all web apps. These simple processes occur so often that I believe the process of handling these changes should be built into the controls themselves rather than having all these extraneous functions that would be tailored to work with different controls.

Although the processes are fairly straight forward, it took a while of googling forums and tutorials in order to find out the basic details. This should not have to happen. I hope that this (and my other tutorials) shed some light on the why, as there are plenty of pages on the how (however disperse they are).

Summary

For me, what started as a pain in the ass due to lack of documentation, has resulted in me spending the time to build a working example and a run down of the functions within for how to add, edit and delete records from an ArrayStore/Grid.

My biggest gripe with ExtJS is not that it's freakn' HUGE (the BASE file itself is 662KB, full deploy (min files only) is 6.34MB) or that the memory footprint is HUGE (causes my browser to run at ~500MB (at the time of this writing)) or even the funky licensing issues, it's that the documentation explaining WHY things are done they way they are done is severely lacking IMO.

The API and example pages that ExtJS provide show HOW to do things but they don't explain why they are done and therefore cause a whole lot of frustration that is not necessary.

Sencha have got a great thing going, they are providing a framework that is consistent, fairly easy to work with (when you understand what you're doing) and most importantly, is cross-browser compatible (for the most part).

When I'm developing a web app, I'd rather be focused upon the components and their usage rather than their construction and the uniformity that ExtJS provides out-of-the-box.

I'd love to hear from you if you feel that something could be done better. I do hope you realise that this code is not without a major refactorisation, so any critisims about it will simply be ignored and you'll be flamed badly :P

1 comment:

  1. Great tutorial! It helped to understand a little bit of this framework.
    I agree with you concerning the lack of a manual. It would be nice to have a community wiki, with the hope it will be the begin of an unofficial manual.

    ReplyDelete