In the previous chapter, we looked at the Sencha MVC architecture and also listed the various framework classes which map to the model-view-controller. In this chapter we will take a step-by-step approach to create a functional application in ExtJS as well as Sencha Touch, using the MVC architecture and the framework classes related to it. For the sake of completeness and illustration of the concepts, we will be taking up an application requirement and implementing it in ExtJS and Sencha Touch. Though most of the features are common in ExtJS and Touch, there are features such as profiles which are unique to Touch. In this chapter, we will cover the common functionality across the two frameworks and visit the specific features in the subsequent chapters.
As a requirement, we will be creating an application, which would:
The following screenshot shows the layout of the page we would like to have when the application is run:
The following screenshot depicts the Edit User panel, which would show the field values populated from the selected user's record and allow the user to modify them:
After clicking on Save, the modified user's information will appear on the user list.
Let us delve deeper into the application that we want to create in ExtJS and Touch. First, let us identify the different views we would like to have in our application. Deciding about the number of views is totally based on the granularity at which you would like to work on. For example, in this application we can define the panel's top title bar as one view that can show a Title and a Refresh button on the top of a panel to which it is added. Similarly, the Save and Cancel buttons on the Edit User panel can be encapsulated inside the ButtonBar view and can be used on the panels where we need to show those two buttons. If tomorrow, one more button such as the Help button needs to be added to it so that we start seeing three buttons in all the places in the application, then making that change would be a lot faster and cleaner. However, if we take a different level of granularity, then we will have a panel with the title bar showing the panel title and a Refresh icon. Also, we would have an Edit User panel with the Save and Cancel buttons. So, is there a rule to decide when I should have the Save and Cancel buttons added to the panel, directly, and when I should wrap them into a ButtonBar view and add it to the panel? Well, there is no pre-defined rule or recommendation from Sencha. It totally depends upon how you want to model your application to achieve the goals, such as re-usability, agility (incorporating changes quickly), maintainability, and testability. So, if you see that the Save and Cancel buttons would appear in multiple places in your application, then that tells us that it would be better to create a ButtonBar view and use it across the panels in the application.
For simplicity, we will be defining the following views:
Now, let us list out the models. Going by the same goals, we will define the following models:
Since we will be showing a list of users and departments, we will define the following stores:
The store would load the data from a data source. So, let us define the following datafiles which we will be using in the application to load the data in the different stores:
users.json
This datafile contains a list of users. Information, such as name
, email
, and the department
code is returned for a user id
, which identifies a user uniquely.
{ success: true, users: [ {id: 1, name: 'Sunil', email: '[email protected]', department:'FIN'}, {id: 2, name: 'Sujit', email: '[email protected]', department:'FIN'}, {id: 3, name: 'Alok', email: '[email protected]', department:'DEV'}, {id: 4, name: 'Pradeep', email: '[email protected]', department:'SAL'}, {id: 5, name: 'Ajit', email: '[email protected]', department:'DEV'} ] }
The users
field in the JSON contains the users list. For each user, it contains the id
, name
, email
, and department
code. The success
property is used to report the application's success/failure, and the true
property indicates success.
departments.json
This datafile contains a list of departments with their code
, name
, and location
.
{ success: true, departments: [ {code: 'FIN', name: 'Finance', location: 'Hyderabad'}, {code: 'SAL', name: 'Sales', location: 'Hyderabad'}, {code: 'DEV', name: 'Development', location: 'Agra'} ] }
The departments
field in the JSON contains the department list. For each department, it contains code
, name
, and location
. The success
property is used to report the application's success/failure, and the true
property indicates success.
One last thing we are left with is the controller. Again, the question is whether we should have one controller or multiple controllers? I generally recommend having one controller per entity in the application. In our application, we have got two entities – User and Department. So, we will have the following controllers:
Now that we have identified all the pieces of our application, let us see how each one of them needs to be implemented in ExtJS and Touch and understand how they need to be assembled to create a functional application, which meets the requirements that we have outlined earlier.
In ExtJS, we will be dealing with the following classes:
Ext.app.Application
: This is the application classExt.app.Controller
: This class provides the controller functionalityExt.container.Container
, Ext.Component
: This class and its sub-classes are used for providing viewsExt.data.Model
: This class helps us represent a model which the Ext.data.Store
class can understandExt.data.Store
: This class contains a collection of Ext.data.Model
type objects and is used on the components to show a list of recordsIn Sencha MVC architecture, folder structure is very important as the underlying class loading uses the pre-defined rules, related to the folder structure, to load the classes automatically for us, on demand. More about the class loading and how it works will be covered in the next chapter.
Create a folder named extjsapp
under WebContent
in the SenchaArchitectureBook
project, which we created in Chapter 1, Sencha MVC Architecture, and add the following files and directories:
app
: This is the main application directory. This will have the model
, view
, controller
, and store
directoriesmodel
User.js
Department
.js
store
Users
.js
Departments
.js
view
userList.js
userEdit.js
departmentList.js
Viewport.js
controller
Users.js
Departments.js
data
: This contains the JSON datafilesextjs-4.1.0-rc1
: This contains the ExtJS frameworkapp.js
: This contains the entry point code for the applicationindex.html
: This is the HTML for the applicationOnce created, the folder structure should look like the following screenshot:
Let us define the different models for our application. We will have the following models:
Save the following code inside the appmodelUser.js
file:
Ext.define('AM.model.User', { extend: 'Ext.data.Model', fields: ['id', 'name', 'email','department'], });
The code that we just used defines a User
model, which represents a user in the application. AM.model
in the class name is important as it is used by the loader to identify the file, which contains the class definition and loads the same. AM
is the name of the application, which acts as a namespace. This has been explained, in detail, in the later part of this section.
Similar to the models, now let us define the stores. In the application, we will have the following stores:
Save the following code inside the appstoreUsers.js
file:
Ext.define('AM.store.Users', { extend: 'Ext.data.Store', model: 'AM.model.User', autoLoad: true, //loads data as soon as the store is initialized proxy: { type: 'ajax', api: { read: 'data/users.json' }, reader: { type: 'json', root: 'users', successProperty: 'success' } }, filterUsersByDepartment: function(deptCode) { this.clearFilter(); this.filter([{ property: 'department', value: deptCode }]); }, refresh: function() { this.clearFilter(); } });
The code that we just used defines a store, which keeps a collection of User
models, indicated by the model
property. The proxy
property contains the information about the AJAX proxy that the store will use to call the specified read URL data/user.json
to load the users. Reader
is configured based on the users.json
file where the root
must be set to the config in the users.json
file, which contains the user records, which is users
.
The autoLoad: true
property will tell the framework to load the data into the store as soon as the store is instantiated.
The filterUsersByDepartment
method is a public method, which filters the store using the specified department code. This method is called as part of the handling of the selection of a specific department from the Departments List view.
The refresh
method is a public method, which clears all the filters applied on the store data and shows all the records in the store. This method is called as part of the handling of the Refresh button click on the Users List view.
Save the following code inside the appstoreDepartments.js
file:
Ext.define('AM.store.Departments', { extend: 'Ext.data.Store', model: 'AM.model.Department', autoLoad: true, proxy: { type: 'ajax', api: { read: 'data/departments.json' }, reader: { type: 'json', root: 'departments', successProperty: 'success' } } });
The code that we just used defines store, which would contain the departments
data. Every entry in it will be a Department
model.
The application will have the following views:
Save the following code inside the appviewuserList.js
file:
Ext.define('AM.view.user.List' ,{ extend: 'Ext.grid.Panel', alias : 'widget.userlist', title : 'All Users', store: 'Users', columns: [ {header: 'Name', dataIndex: 'name', flex: 1}, {header: 'Email', dataIndex: 'email', flex: 1} ], tools:[{ type:'refresh', tooltip: 'Refresh', handler: function(){ var pnl = this.up('userlist'), pnl.getStore().refresh(); pnl.setTitle('All Users'), } }], filterUsersByDepartment: function(deptCode) { this.getStore().filterUsersByDepartment(deptCode); } });
The code that we just used defines the User List view, which extends a grid panel. The Users
store is associated with it. The grid will show two columns Name
and Email
, and has a toolbar with the Refresh button. Since, the refresh logic is private to the User List view, the handler is registered in the view class where it is refreshes the store and sets the title correctly.
The filterUsersByDepartment
method is a public method, which filters the store using the specified department code. This method is called as part of the handling of the selection of a specific department from the Departments List view. This relies on the store's filterUsersByDepartment
entity to accomplish the task.
userlist
has been mentioned as an alias, which can be used as xtype
for the User List view.
Save the following code inside the appviewuserEdit.js
file:
Ext.define('AM.view.user.Edit', { extend: 'Ext.window.Window', alias : 'widget.useredit', requires: ['Ext.form.Panel'], title : 'Edit User', layout: 'fit', autoShow: true, height: 120, width: 280, initComponent: function() { this.items = [ { xtype: 'form', padding: '5 5 0 5', border: false, style: 'background-color: #fff;', items: [ { xtype: 'textfield', name : 'name', fieldLabel: 'Name' }, { xtype: 'textfield', name : 'email', fieldLabel: 'Email' } ] } ]; this.buttons = [ { text: 'Save', action: 'save' }, { text: 'Cancel', action: 'cancel' } ]; this.callParent(arguments); } });
The code that we just used defines the Edit User view. This extends the Window
and has a form panel, which contains two text fields and two buttons.
The name
property on the text fields has been specified and their value must match with the field name on the User
model. This will be helpful in loading the data from the model into the form.
useredit
has been mentioned as an alias, which can be used as xtype
for the Edit User view.
Save the following code inside the appviewdepartmentList.js
file:
Ext.define('AM.view.department.List' ,{ extend: 'Ext.grid.Panel', alias : 'widget.departmentlist', title : 'Departments', store: 'Departments', columns: [ {header: 'Name', dataIndex: 'name', flex: 1}, {header: 'Location', dataIndex: 'location', flex: 1} ] });
In the code we just used, we defined the Department List view, which extends a grid panel and uses the Departments
store. It has two columns, Name
and Location
. departmentlist
has been mentioned as an alias, which can be used as xtype
for the Department List view.
Save the following code inside the appviewViewport.js
file:
Ext.define('AM.view.Viewport', { extend: 'Ext.container.Viewport', requires: ['AM.view.department.List', 'AM.view.user.List'], layout: 'border', config: { items: [{ region: 'west', width: 200, xtype: 'departmentlist' }, { region: 'center', xtype: 'userlist' }] } });
In the application, we will have the following controllers:
Save the following code inside the appcontrollerUsers.js
file:
Ext.define('AM.controller.Users', { extend: 'Ext.app.Controller', config: { stores: ['Users'], models: ['User'], views: ['user.Edit', 'user.List'], refs: [{ ref: 'usersList', selector: 'userlist' }] }, init: function(app) { this.control({ 'userlist dataview': { itemdblclick: this.editUser }, 'useredit button[action=save]': { click: this.updateUser }, 'useredit button[action=cancel]': { click: this.cancelEditUser } }); app.on('departmentselected', function(app, model) { this.getUsersStore().filterUsersByDepartment(model.get('code')); this.getUsersList().setTitle(model.get('name') + ' Users'), }, this); }, editUser: function(grid, record) { var edit = Ext.create('AM.view.user.Edit').show(); edit.down('form').loadRecord(record); }, updateUser: function(button) { var win = button.up('window'), form = win.down('form'), record = form.getRecord(), values = form.getValues(); record.set(values); win.close(); }, cancelEditUser: function(button) { var win = button.up('window'), win.close(); } });
In the code that we just used, we defined the Users
controller, which manages the userlist
and edituser
views. It also uses the Users
store and the User
model. All these are listed in a controller using the views
, stores
, and models
properties.
The init
method in a controller class is called by the framework to initialize the controller, even before the application is launched. The typical task that we do here is registering the handlers for the different view elements. This is done by calling the control
method of the controller, where we specify the xtype
-based selectors to identify the view component (for example, useredit button[action=save]
identifies the Save button on the Edit User window) and register their event handlers.
The controller also handles the departmentselected
event in the application, which is fired by the Departments
controller when a particular department is selected. Let us see what the Departments
controller looks like.
Save the following code inside the appcontrollerDepartments.js
file:
Ext.define('AM.controller.Departments', { extend: 'Ext.app.Controller', config: { stores: ['Departments'], models: ['Department'], views: ['department.List'] }, init: function() { this.control({ 'departmentlist': { itemclick: this.showDepartmentUser } }); }, showDepartmentUser: function(grid, model, itemEl, idx, e, eOpts) { var app = this.application; app.fireEvent('departmentselected', app, model); } });
The code that we just used defines the Departments
controller, which manages the department-related view, model, and the store. It fires the departmentselected
event on the application when a department is selected.
Save the following code inside the app.js
file:
Ext.application({ name: 'AM', //application name that becomes the namespace // automatically create an instance of AM.view.Viewport autoCreateViewport: true, controllers: [ 'Users', 'Departments' ] });
In the code that we just used, we are initializing the application. The name:'AM'
property is a very important property. It creates an automatic namespace for our application. Due to this, we defined all the classes starting with AM
, for example, AM.model.User
. Once specified, if the framework comes across with any class name starting with AM
, it will look for that class in the app
folder. For example, when it encounters AM.model.User
, it will look for the User.js
file inside the appmodel
folder. Similarly, for AM.view.user.List
it will look for the List.js
file inside the appviewuser
folder.
The autoCreateViewport:true
property will tell the framework to automatically look for the Viewport.js
file inside the appview
folder and create the viewport using it.
An application is a collection of controllers, which internally manage one or more views to accomplish the overall requirement. All the controllers, which constitute the application, need to be specified as part of the controllers
property. A fully qualified name, such as AM.controller.Users,
is not required as the framework will automatically look for the Users
controller inside the appcontroller
folder.
Save the following code inside the
index.html
file:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title id="page-title">Account Manager</title> <link rel="stylesheet" type="text/css" href="extjs-4.1.0-rc1/resources/css/ext-all.css"> <script type="text/javascript" src="extjs-4.1.0-rc1/ext-debug.js"></script> <script type="text/javascript" src="app.js"></script> </head> <body> </body> </html>
Publish the application to Apache Tomcat,
Start the server, and access http://<host>:<port>/SenchaArchitectureBook/extjsapp/index.html
in the browser. You shall see the following screen:
Selecting a particular department from the Departments list shows the users from the selected department, as shown in the following screenshot:
Double-click on the user record. This will bring up the Edit User view with the values populated from the selected user, as shown in the following screenshot:
Make some change to the user information, say, Name, and click on Save. We shall see the updated information in the users list, as shown in the following screenshot:
Click on the Refresh icon on the users list. This will reload all the users in the users list.
For the MVC architecture, as ExtJS and Touch have a lot of things in common, the overall approach would remain similar for Touch as well. In Sencha Touch, we will be dealing with the following classes:
Ext.app.Application
: This is the application classExt.app.Controller
: This class provides the controller functionalityExt.Container
, Ext.Component
: These classes and their sub-classes are used for providing viewsExt.data.Model
: This class helps us represent a model which the Ext.data.Store
class can understandExt.data.Store
: This class contains a collection of Ext.data.Model
type objects and is used on the components to show a list of recordsCreate a folder touchapp
under WebContent
in the SenchaArchitectureBook
project, which we created in Chapter 1, Sencha MVC Architecture, and add the following files and directories:
app
: This is the main application directory. It will have a model, view, controller, and store directories:model
User.js
Department.js
store
Users.js
Departments.js
view
userList.js
userEdit.js
departmentList.js
controller
Users.js
Departments.js
data
: This contains the JSON datafiles.sencha-touch-2.0.0
: This contains the ExtJS framework.app.js
: This contains the entry point code for the application.index.html
: This is the HTML for the application.Once created, the folder structure should look like the following screenshot:
We will have the following models:
In the application we will have the following stores:
Save the following code inside the appstoreUsers.js
file:
Ext.define('AM.store.Users', { extend: 'Ext.data.Store', config: { autoLoad: true, model: 'AM.model.User', storeId: 'Users', proxy: { type: 'ajax', api: { read: 'data/users.json' }, reader: { type: 'json', rootProperty: 'users' } } }, filterUsersByDepartment: function(deptCode) { this.clearFilter(); this.filter([{ property: 'department', value: deptCode }]); }, refresh: function() { this.clearFilter(); } });
The code that we just used defines the Users
store, which contains the collection of User
models.
Save the following code inside the appstoreDepartments.js
file:
Ext.define('AM.store.Departments', { extend: 'Ext.data.Store', model: 'AM.model.Department', config: { autoLoad: true, model: 'AM.model.Department', storeId: 'Departments', proxy: { type: 'ajax', api: { read: 'data/departments.json' }, reader: { type: 'json', rootProperty: 'departments' } } } });
The code that we just used defines the Departments
store, which contains the collection of Department
models.
The application will have the following views:
Save the following code inside the appviewuserList.js
file:
Ext.define('AM.view.user.List' ,{ extend: 'Ext.Container', alias : 'widget.userlist', config: { ui: 'light', layout: { type: 'fit' }, items: [ { xtype: 'toolbar', docked: 'top', title: 'All Users', defaults: { iconMask: true }, items: [{ xtype: 'spacer' }, { iconCls: 'refresh', ui: 'confirm', handler: function(){ this.up('userlist').down('list').getStore().refresh(); this.up('toolbar').setTitle('All Users'), } }] }, { xtype: 'list', height: '100%', ui: 'round', itemTpl: [ '<div style="float:left;">{name}</div>', '<div style="float:left;position:absolute;padding-left:150px;">{email}</div>' ], store: 'Users', onItemDisclosure: true } ] } });
The code that we just used defines the User List view by extending the Container
class. It shows a list of users with the disclosure option (refer to the Sencha Touch API documentation of Ext.dataview.List
for more detail) and a toolbar on the top with the Title and the Refresh button. The list uses the Users
store to load data. Similar to the ExtJS-based app, the handler for the Refresh button is implemented inside the view as the logic is local to the view.
The userlist
alias will act as xtype
for this view.
Save the following code inside the appviewuserEdit.js
file:
Ext.define('AM.view.user.Edit', { extend: 'Ext.form.Panel', alias : 'widget.useredit', config: { ui: 'light', items: [ { xtype: 'titlebar', docked: 'top', title: 'Edit User' }, { xtype: 'textfield', label: 'Name', name: 'name', labelWidth: '50%', required: true }, { xtype: 'textfield', label: 'Email', name: 'email', labelWidth: '50%', required: true }, { xtype: 'toolbar', docked: 'bottom', items: [{ xtype: 'button', margin: 10, align: 'left', ui: 'confirm', action: 'save', text: 'Save' }, { xtype: 'spacer' }, { xtype: 'button', margin: 10, align: 'right', ui: 'decline', action: 'cancel', text: 'Cancel' }] } ] } });
The code that we just used defines the Edit User view by extending the form panel. It contains two text fields and a bottom toolbar with Save and Cancel buttons.
The useredit
alias will be used as xtype
for this view.
Save the following code inside the appviewdepartmentList.js
file:
Ext.define('AM.view.department.List' ,{ extend: 'Ext.Container', alias : 'widget.departmentlist', config: { ui: 'light', layout: { type: 'fit' }, items: [ { xtype: 'titlebar', docked: 'top', title: 'Departments' }, { xtype: 'list', height: '100%', ui: 'round', itemTpl: [ '<div style="float:left;">{name}</div>', '<div style="float:left;position:absolute;padding-left:150px;">{location}</div>' ], store: 'Departments', onItemDisclosure: false } ] } });
The code that we just used defines the Department List view by extending the Container
class. It shows a list of departments without the disclosure option. The list uses the Departments
store to load data.
The departmentlist
alias will act as the xtype
for this view.
In the application, we will have the following controllers:
Save the following code inside the appcontrollerUsers.js
file:
Ext.define('AM.controller.Users', { extend: 'Ext.app.Controller', config: { stores: ['Users'], models: ['User'], views: ['user.Edit', 'user.List'], refs: { usersPanel: 'userlist' } }, init: function(app) { this.control({ 'userlist list': { disclose: this.editUser }, 'useredit button[action=save]': { tap: this.updateUser }, 'useredit button[action=cancel]': { tap: this.cancelEditUser } }); app.on('departmentselected', function(app, model) { this.getUsersStore().filterUsersByDepartment(model.get('code')); this.getUsersPanel().down('toolbar').setTitle(model.get('name') + ' Users'), }, this); }, editUser: function(view, model, t, index, e, eOpts) { var edit = Ext.create('AM.view.user.Edit'), Ext.Viewport.add(edit); edit.setRecord(model); Ext.Viewport.setActiveItem(edit); }, updateUser: function(button, e ,eOpts) { var form = button.up('formpanel'), var record = form.getRecord(), values = form.getValues(); record.set(values); this.getUsersStore().sync(); Ext.Viewport.setActiveItem(0); }, cancelEditUser: function(button, e ,eOpts) { Ext.Viewport.setActiveItem(0); }, showUsersList: function() { var list = Ext.create('AM.view.user.List'), Ext.Viewport.add(list); }, getUsersStore: function() { return this.getUsersPanel().down('list').getStore(); } });
Save the following code inside the appcontrollerDepartments.js
file:
Ext.define('AM.controller.Departments', { extend: 'Ext.app.Controller', config: { stores: ['Departments'], models: ['Department'], views: ['department.List'] } init: function() { this.control({ 'departmentlist list': { itemtap: this.showDepartmentUser } }); }, showDepartmentUser: function(view, idx, t, model, e, eOpts) { var app = this.initialConfig.application; app.fireEvent('departmentselected', app, model); } });
The code that we just used defines the Departments
controller.
Save the following code inside the app.js
file:
Ext.application({ name: 'AM', // dependencies controllers: ['Users', 'Departments'], // launch application launch: function() { var config = { layout: 'fit', items: [{ xtype: 'departmentlist', docked: 'left', width: 400 }, { xtype: 'userlist' }] }; Ext.Viewport.add(config); } });
In the last line, we added the panel, with department list
and user list
, to the viewport. Since the viewport concept is not very straightforward on touch devices, the framework wraps those differences and provides us with static methods, such as add
, to add content to it.
Save the following code inside the index.html
file:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title>Account Manager</title> <!-- Sencha Touch specific files --> <link rel="stylesheet" type="text/css" href="sencha-touch-2.0.0/resources/css/sencha-touch.css"> <script type="text/javascript" src="sencha-touch-2.0.0/sencha-touch-all-debug.js"></script> <!-- Application specific files --> <script type="text/javascript" src="app.js"></script> </head> <body> </body> </html>
Publish the application to Apache Tomcat, Start the server, and access http://<host>:<port>/SenchaArchitectureBook/touchapp/index.html
in the Webkit browser. You should see the following screen:
Selecting a particular department from the Departments list shows the users from the selected department, as shown in the following screenshot:
Double-click on the User Record button. This will bring up the Edit User view with the values populated from the selected user, as shown in the following screenshot:
Make some change to the user information, say, Name, and click on Save. We should see the updated information in the users list, as shown in the following screenshot:
Click on the Refresh icon on the users list. This will reload all the users in the users list.
18.188.57.172