Ext JS MVC Architecture — Models, Views, Controllers, and Application Structure
In this tutorial, you will learn about Ext JS MVC Architecture. We cover key concepts, practical examples, and best practices to help you master this topic.
Ext JS MVC architecture separates application logic into Models (data), Views (UI components), and Controllers (business logic), providing organized code structure for large-scale enterprise applications.
What You'll Learn
- MVC folder structure and naming conventions
- Defining Controllers with refs and control
- View-Controller communication via events
- Application class and app initialization
- Best practices for large MVC projects
Why It Matters
Without a clear architecture, Ext JS applications become unmanageable as they grow. Controllers contain event handlers, Views remain pure component configurations, and Models define data contracts — each layer has a single responsibility and can be tested independently.
Real-World Use
An enterprise CRM with 100+ screens where each module (Contacts, Deals, Reports) has its own Controller, multiple View classes, and shared Model definitions — allowing teams to work on separate modules without merge conflicts.
MVC Architecture
flowchart LR
A[Application] --> B[Controllers]
B --> C[Views]
B --> D[Models]
B --> E[Stores]
C --> F[Components]
D --> G[Data Fields]
E --> H[Data Sources]
B -.-> I[Event Flow]
I --> J[View Events]
I --> K[Store Events]
I --> L[Controller Actions]
style A fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px
Folder Structure
myapp/
app/
controller/
Main.js
Users.js
Products.js
model/
User.js
Product.js
store/
Users.js
Products.js
view/
Main.js
users/
List.js
Form.js
products/
Grid.js
Form.js
Application.js
app.js
index.html
Application Definition
// app/Application.js
Ext.define('MyApp.Application', {
extend: 'Ext.app.Application',
name: 'MyApp',
appFolder: 'app',
controllers: [
'Main',
'Users',
'Products'
],
stores: [
'Users',
'Products'
],
launch: function() {
Ext.create('MyApp.view.Main', {
renderTo: Ext.getBody()
});
}
});
// app.js - Entry point
Ext.application({
name: 'MyApp',
appFolder: 'app',
controllers: ['Main', 'Users', 'Products'],
autoCreateViewport: false,
launch: function() {
Ext.create('MyApp.view.Main', {
renderTo: Ext.getBody()
});
}
});
Expected output: The application loads all controllers and stores, then creates the main view. The Application class manages the lifecycle and Dependency Injection.
Controller Definition
// app/controller/Users.js
Ext.define('MyApp.controller.Users', {
extend: 'Ext.app.Controller',
refs: [{
ref: 'usersGrid',
selector: 'usersgrid'
}, {
ref: 'userForm',
selector: 'userform'
}, {
ref: 'addButton',
selector: 'usersgrid button[action=add]'
}],
control: {
'usersgrid': {
itemdblclick: 'onEditUser',
itemcontextmenu: 'onUserContextMenu'
},
'usersgrid button[action=add]': {
click: 'onAddUser'
},
'userform button[action=save]': {
click: 'onSaveUser'
},
'userform button[action=cancel]': {
click: 'onCancelEdit'
}
},
stores: ['Users'],
init: function() {
this.getUsersStore().load();
console.log('Users controller initialized');
},
onAddUser: function() {
var form = Ext.create('MyApp.view.users.Form');
form.show();
},
onEditUser: function(grid, record) {
var form = Ext.create('MyApp.view.users.Form');
form.getForm().loadRecord(record);
form.show();
},
onUserContextMenu: function(grid, record, item, index, event) {
event.stopEvent();
},
onSaveUser: function(btn) {
var form = btn.up('userform').getForm();
if (form.isValid()) {
form.updateRecord(form.getRecord());
this.getUsersStore().sync();
btn.up('window').close();
}
},
onCancelEdit: function(btn) {
btn.up('window').close();
}
});
Expected output: The controller listens for events from views using CSS-like selectors. Double-clicking a user grid row opens the edit form. Clicking Add opens a blank form. Save writes to the store and syncs to server.
View Definition with xtype
// app/view/users/Grid.js
Ext.define('MyApp.view.users.Grid', {
extend: 'Ext.grid.Panel',
alias: 'widget.usersgrid',
title: 'Users',
store: 'Users',
columns: [
{ text: 'ID', dataIndex: 'id', width: 50 },
{ text: 'Name', dataIndex: 'name', flex: 1 },
{ text: 'Email', dataIndex: 'email', flex: 2 },
{ text: 'Status', dataIndex: 'status', width: 100,
renderer: function(v) { return v === 'active' ? 'Active' : 'Inactive'; }
}
],
tbar: [{
text: 'Add User',
action: 'add',
iconCls: 'x-fa fa-plus'
}, {
text: 'Delete',
action: 'delete',
disabled: true,
iconCls: 'x-fa fa-trash'
}],
dockedItems: [{
xtype: 'pagingtoolbar',
dock: 'bottom',
store: 'Users',
displayInfo: true
}]
});
// app/view/users/Form.js
Ext.define('MyApp.view.users.Form', {
extend: 'Ext.window.Window',
alias: 'widget.userform',
title: 'User Details',
width: 450,
modal: true,
layout: 'fit',
items: [{
xtype: 'form',
bodyPadding: 15,
defaults: { anchor: '100%' },
items: [{
xtype: 'textfield',
fieldLabel: 'Name',
name: 'name',
allowBlank: false
}, {
xtype: 'textfield',
fieldLabel: 'Email',
name: 'email',
vtype: 'email'
}, {
xtype: 'combobox',
fieldLabel: 'Status',
name: 'status',
store: ['active', 'inactive']
}]
}],
buttons: [{
text: 'Save',
action: 'save'
}, {
text: 'Cancel',
action: 'cancel'
}]
});
Expected output: The grid and form views are defined as Ext JS classes with aliases. The controller references them using widget selectors. Views remain pure component configurations with no business logic.
View-Controller Event Flow
// Option 1: Controller listens (recommended)
Ext.define('MyApp.controller.Orders', {
extend: 'Ext.app.Controller',
refs: [{ ref: 'ordersGrid', selector: 'ordersgrid' }],
control: {
'ordersgrid': {
select: 'onOrderSelect',
itemclick: 'onOrderClick'
}
},
onOrderSelect: function(grid, record) {
this.loadOrderDetails(record.get('id'));
},
loadOrderDetails: function(orderId) {
console.log('Load order:', orderId);
}
});
// Option 2: View fires custom events
Ext.define('MyApp.view.orders.Grid', {
extend: 'Ext.grid.Panel',
alias: 'widget.ordersgrid',
listeners: {
itemdblclick: function(grid, record) {
grid.fireEvent('orderSelected', grid, record);
}
}
});
Expected output: The controller-centered approach keeps views stupid (no logic) and controllers smart (all logic). Custom events add semantic meaning like orderSelected instead of raw itemdblclick.
Store Management in MVC
// app/store/Users.js
Ext.define('MyApp.store.Users', {
extend: 'Ext.data.Store',
alias: 'store.users',
model: 'MyApp.model.User',
autoLoad: false,
pageSize: 25,
proxy: {
type: 'ajax',
url: '/api/users',
reader: { type: 'json', rootProperty: 'data', totalProperty: 'total' },
writer: { type: 'json', writeAllFields: true }
}
});
// Controller accessing stores
Ext.define('MyApp.controller.Users', {
extend: 'Ext.app.Controller',
stores: ['Users'],
init: function() {
var store = this.getUsersStore();
store.load({
params: { status: 'active' },
callback: function(records) {
console.log('Loaded ' + records.length + ' users');
}
});
}
});
Expected output: Stores are declared in the controller's stores array, which auto-generates a getter method. The controller initiates data loading and reacts to store events.
Application Lifecycle
Ext.define('MyApp.Application', {
extend: 'Ext.app.Application',
name: 'MyApp',
controllers: ['Users', 'Products', 'Orders'],
init: function() {
console.log('App initializing');
},
launch: function() {
Ext.create('MyApp.view.Viewport', {
renderTo: Ext.getBody()
});
console.log('App launched');
},
onAppUpdate: function() {
Ext.Msg.confirm('Update Available', 'Reload for the latest version?', function(btn) {
if (btn === 'yes') {
window.location.reload();
}
});
}
});
Expected output: The application lifecycle runs init first, then creates controllers and stores, then calls launch. onAppUpdate provides a hook for service worker updates.
Common Mistakes
Putting business logic in views - Views should be pure component configurations. All event handlers and data manipulation belong in controllers.
Not using refs for component lookups - Using this.getUsersGrid() is cleaner than Ext.ComponentQuery.query('usersgrid'). Refs are defined once and reused.
Creating circular controller dependencies - Controller A references Controller B's store, and B references A's. Break the cycle by sharing a common store or using the application-level stores.
Loading data in view constructors - Views should not call store.load(). Controllers initiate data loading after views are created and configured.
Defining the same store in multiple controllers - Register a store once (in Application.js or one controller) and reference it in others using the store alias.
Practice Questions
- What is the purpose of the refs array in a controller?
- How does the control config map events to handler methods?
- What is the difference between init and launch in the Application class?
- How do you access a store from a controller?
- Why should views not contain business logic?
Challenge: Build a complete MVC module for order management with: an Orders controller with CRUD handlers, a Grid view with action column buttons, a Form view in a window for create/edit, a Model with validators, a Store with server proxy, proper refs and control mappings, and integration with the main application.
FAQ
Mini Project
Build a full MVC product management module: Product model with fields and validators, Product store with Ajax proxy and pagination, Product grid view with column renderers and action buttons, Product form view (window with form panel), Product controller with refs, control mappings, and CRUD handlers, integration with the main application class, and a main viewport with border layout hosting the grid.
What's Next
MVC structures your code. Learn how Ext JS Routing adds URL-based navigation with browser history support and deep-linking to specific views.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro