MVC architectural pattern is introduced in ExtJS from version 4. The 5-th version added the support of MVVC. These patterns gave a birth to so called Fat Stupid and Ugly Controllers. FSUC and God View Objects makes the code highly complicated.
All the business logic must be developed in models (records), stores and view-models. If your logic belongs to single record, put it there. In case it manipulates multiple instances, put it in the store. The other code must be presented in the model-views.
Let’t develop a simple application which will show users in a grid and let us make presents for users in single and batch modes.
Our user model will be able to make a present to a single user.
Ext.define('MvcDemo.model.User', { extend: 'Ext.data.Model', fields: [{ name: 'userId', type: 'string' }, { name: 'firstName', type: 'string' }, { name: 'lastName', type: 'string' }, { name: 'presents', type: 'auto', defaultValue: [] }], makePresent: function () { Ext.Ajax.request({ url: 'makePresent.json', params: { userId: this.get('userId') }, success: function (response, opts) { var obj = Ext.decode(response.responseText); this.set( 'presents', Ext.Array.merge( this.get('presents'), obj.presents ) ); }, failure: function (response, opts) { console.log('server-side failure with status code ' + response.status); }, scope: this }); return this; }, getFullName: function() { return this.get('firstName') + ' ' + this.get('lastName'); } });
The store will have a method to make presents in batch mode.
Ext.define('MvcDemo.store.Users', { extend: 'Ext.data.Store', model: 'MvcDemo.model.User', proxy: { type: 'ajax', url: 'getUsers.json', reader: { type: 'json', rootProperty: 'users' } }, autoLoad: true, makePresents: function () { var userIds = []; this.each(function (record) { userIds.push(record.get('userId')) }, this); Ext.Ajax.request({ url: 'makePresents.json', params: { userIds: userIds }, success: function (response, opts) { var obj = Ext.decode(response.responseText); Ext.Array.each(obj.presents, function (row) { this.addPresent(row.userId, row.present); }, this); }, failure: function (response, opts) { console.log('server-side failure with status code ' + response.status); }, scope: this }); return this; }, addPresent: function (userId, presents) { var user = this.findRecord('userId', userId); if (user) { user.set( 'presents', Ext.Array.merge( user.get('presents'), presents ) ); } } });
I also would like to draw your attention on the toolbar of the grid. It does not know anything about parent element.
Ext.define('MvcDemo.view.user.toolbar.Top', { extend: 'Ext.toolbar.Toolbar', requires: [ 'MvcDemo.view.user.toolbar.TopController' ], initComponent: function () { this.items = [ this.getMakePresentsButton() ] this.callParent(); }, getMakePresentsButton: function () { if (!this.makePresentsButton) { this.makePresentsButton = Ext.create('Ext.button.Button', { text: "Make Presents", handler: 'onMakePresentsButtonClick' }); } return this.makePresentsButton; } });
The communication between the toolbar and the parent grid is produced via the events in the controller.
Ext.define('MvcDemo.view.user.toolbar.TopController', { extend: 'Ext.app.ViewController', alias: 'controller.MvcDemo_view_user_toolbar_TopController', onMakePresentsButtonClick: function (grid, rowIndex, colIndex, item, e, record) { this.fireViewEvent('MakePresentsButtonClick'); } });
As you can see I am not using inter-controllers communication and strongly suggest you to minimize the usage of this technology.
In the column model of the grid the renderers and other helping function are moved to separate methods of the class to avoid monster configurations which are not so readable.
Ext.define('MvcDemo.view.user.Grid', { extend: 'Ext.grid.Panel', requires: [ 'MvcDemo.view.user.GridController', 'MvcDemo.store.Users', 'MvcDemo.view.user.toolbar.Top' ], controller: 'MvcDemo_view_user_GridController', title: 'Action Column Demo', initComponent: function () { this.store = Ext.create('MvcDemo.store.Users'); this.dockedItems = [ this.getTopToolbar() ]; this.columns = this.getTheColumnModel(); this.callParent(); }, getTopToolbar: function () { if (!this.topToolbar) { this.topToolbar = Ext.create('MvcDemo.view.user.toolbar.Top', { listeners: { 'MakePresentsButtonClick': 'onMakePresentsButtonClick' } }); } return this.topToolbar; }, getTheColumnModel: function () { var colModel = [{ xtype: 'actioncolumn', width: 40, items: [{ glyph: 'xf06b@FontAwesome', handler: 'onPresentIconClick' }, { glyph: 'xf06e@FontAwesome', isActionDisabled: this.isViewPresentsIconDisabled, handler: 'onViewPresentsIconClick' }] }, { text: "First Name", dataIndex: 'firstName' }, { text: "Last Name", dataIndex: 'lastName' }, { text: "Presents", dataIndex: 'presents', renderer: this.presentsRenderer, flex: 1 }]; return colModel; }, presentsRenderer: function (value) { return value.join(','); }, isViewPresentsIconDisabled: function (view, rowIndex, colIndex, items, record) { return record.get('presents').length == 0; } });
And the thin grid controller.
Ext.define('MvcDemo.view.user.GridController', { extend: 'Ext.app.ViewController', alias: 'controller.MvcDemo_view_user_GridController', onPresentIconClick: function (grid, rowIndex, colIndex, item, e, record) { record.makePresent(); }, onViewPresentsIconClick: function(grid, rowIndex, colIndex, item, e, record) { Ext.Msg.alert( "Presents of " + record.getFullName(), record.get('presents').join(' ') ); }, onMakePresentsButtonClick: function () { this.getView().getStore().makePresents(); } });