Ext JS Complete Project — Building an Enterprise Dashboard from Scratch
In this tutorial, you will learn about Ext JS Complete Project. We cover key concepts, practical examples, and best practices to help you master this topic.
Build a complete Ext JS enterprise dashboard application from scratch — plan the architecture, implement MVC modules, create data grids with full CRUD, design interactive charts, add routing and theming, and prepare for production deployment.
What You'll Learn
- Planning an Ext JS application architecture
- Implementing MVC with multiple modules
- Building CRUD interfaces with real API integration
- Adding charts, routing, and theming
- Deployment and production build optimization
Why It Matters
Building a complete application ties together all Ext JS concepts — components, data package, MVC, routing, theming, and extensions — into a working project. This lesson walks through the entire Process so you can replicate it for any enterprise application.
Real-World Use
A customer analytics dashboard with user management (CRUD grid), sales analytics (charts), order tracking (data-driven views), role-based permissions, and a custom branded theme — exactly the pattern used in CRM, ERP, and admin panel projects.
Application Architecture
flowchart TD
A[Application] --> B[Main Viewport]
B --> C[Sidebar Nav]
B --> D[Content Area]
D --> E[Dashboard]
D --> F[Users Module]
D --> G[Analytics Module]
D --> H[Orders Module]
F --> I[Grid View]
F --> J[Form View]
G --> K[Charts View]
H --> L[Grid View]
H --> M[Detail View]
A --> N[Router]
N --> F
N --> G
N --> H
style A fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px
Project Structure
enterprise-dashboard/
app/
controller/
Dashboard.js
Users.js
Analytics.js
Orders.js
Router.js
model/
User.js
Product.js
Order.js
Analytics.js
store/
Users.js
Products.js
Orders.js
Analytics.js
view/
Main.js
dashboard/
Dashboard.js
KpiCard.js
users/
Grid.js
Form.js
analytics/
Charts.js
orders/
Grid.js
Detail.js
common/
Header.js
Sidebar.js
Application.js
app.js
resources/
images/
css/
fonts/
index.html
build.xml
Main Viewport
// app/view/Main.js
Ext.define('Enterprise.view.Main', {
extend: 'Ext.container.Viewport',
layout: 'border',
items: [{
region: 'north',
xtype: 'appheader',
height: 56
}, {
region: 'west',
xtype: 'appsidebar',
width: 240,
collapsible: true,
split: true
}, {
region: 'center',
xtype: 'container',
itemId: 'contentPanel',
layout: 'card',
activeItem: 0
}]
});
// app/view/common/Header.js
Ext.define('Enterprise.view.common.Header', {
extend: 'Ext.container.Container',
alias: 'widget.appheader',
layout: 'hbox',
padding: '0 20',
style: { backgroundColor: '#1a237e', color: '#fff' },
items: [{
xtype: 'component',
html: '<h2 style="margin:0;color:#fff">Enterprise Dashboard</h2>',
flex: 1
}, {
xtype: 'button',
text: 'Dark Mode',
action: 'toggleTheme',
iconCls: 'x-fa fa-moon'
}, {
xtype: 'button',
text: 'Logout',
iconCls: 'x-fa fa-sign-out',
margin: '0 0 0 10'
}]
});
// app/view/common/Sidebar.js
Ext.define('Enterprise.view.common.Sidebar', {
extend: 'Ext.tree.Panel',
alias: 'widget.appsidebar',
title: 'Navigation',
rootVisible: false,
store: {
type: 'tree',
root: {
expanded: true,
children: [
{ text: 'Dashboard', iconCls: 'x-fa fa-tachometer', route: 'dashboard', leaf: true },
{ text: 'Users', iconCls: 'x-fa fa-users', route: 'users', leaf: true },
{ text: 'Analytics', iconCls: 'x-fa fa-bar-chart', route: 'analytics', leaf: true },
{ text: 'Orders', iconCls: 'x-fa fa-shopping-cart', route: 'orders', leaf: true }
]
}
}
});
Expected output: A border layout viewport with a dark blue header, collapsible sidebar navigation, and a card layout content area that switches between modules.
Dashboard Module
// app/view/dashboard/Dashboard.js
Ext.define('Enterprise.view.dashboard.Dashboard', {
extend: 'Ext.container.Container',
alias: 'widget.dashboardview',
layout: 'vbox',
bodyPadding: 20,
items: [{
xtype: 'container',
layout: 'hbox',
height: 120,
defaults: { flex: 1, margin: '0 10 0 0' },
items: [
{ xtype: 'kpiview', title: 'Total Users', value: '1,247', icon: 'fa-users', color: '#4a90d9' },
{ xtype: 'kpiview', title: 'Revenue', value: '$84,500', icon: 'fa-dollar', color: '#4caf50' },
{ xtype: 'kpiview', title: 'Orders', value: '3,892', icon: 'fa-shopping-cart', color: '#ff9800' },
{ xtype: 'kpiview', title: 'Growth', value: '+12.5%', icon: 'fa-arrow-up', color: '#9c27b0' }
]
}, {
xtype: 'container',
layout: 'hbox',
flex: 1,
defaults: { flex: 1, margin: '10 10 0 0' },
items: [
{ xtype: 'chart', /* revenue chart config */ },
{ xtype: 'grid', /* recent orders grid config */ }
]
}]
});
// app/view/dashboard/KpiCard.js
Ext.define('Enterprise.view.dashboard.KpiCard', {
extend: 'Ext.panel.Panel',
alias: 'widget.kpiview',
bodyPadding: 20,
layout: 'vbox',
items: [{
xtype: 'component',
style: { fontSize: '24px', color: '#666' }
}, {
xtype: 'component',
style: { fontSize: '32px', fontWeight: 'bold', margin: '10 0' }
}, {
xtype: 'component',
style: { fontSize: '14px', color: '#999' }
}],
initComponent: function() {
this.items[0].html = '<i class="x-fa ' + (this.icon || '') + '"></i>';
this.items[1].html = this.value;
this.items[2].html = this.title;
this.callParent();
}
});
Expected output: The dashboard shows four KPI cards in a row (users, revenue, orders, growth) and a second row with a chart and recent orders grid.
Users Module (Full CRUD)
// app/model/User.js
Ext.define('Enterprise.model.User', {
extend: 'Ext.data.Model',
fields: [
{ name: 'id', type: 'int' },
{ name: 'name', type: 'string' },
{ name: 'email', type: 'string' },
{ name: 'role', type: 'string' },
{ name: 'status', type: 'string' },
{ name: 'createdAt', type: 'date', dateFormat: 'Y-m-d' }
],
validators: {
name: { type: 'presence', message: 'Name is required' },
email: [{ type: 'presence' }, { type: 'email' }],
role: { type: 'presence' }
},
proxy: {
type: 'ajax',
url: '/api/users',
reader: { type: 'json', rootProperty: 'data', totalProperty: 'total' },
writer: { type: 'json', writeAllFields: true }
}
});
// app/store/Users.js
Ext.define('Enterprise.store.Users', {
extend: 'Ext.data.Store',
alias: 'store.users',
model: 'Enterprise.model.User',
autoLoad: true,
pageSize: 25,
sorters: [{ property: 'createdAt', direction: 'DESC' }]
});
// app/view/users/Grid.js
Ext.define('Enterprise.view.users.Grid', {
extend: 'Ext.grid.Panel',
alias: 'widget.usersgrid',
store: { type: 'users' },
reference: 'usersGrid',
columns: [
{ text: 'ID', dataIndex: 'id', width: 60 },
{ text: 'Name', dataIndex: 'name', flex: 1 },
{ text: 'Email', dataIndex: 'email', flex: 2 },
{ text: 'Role', dataIndex: 'role', width: 120,
renderer: function(v) { return '<span class="badge badge-' + v + '">' + v + '</span>'; }
},
{ text: 'Status', dataIndex: 'status', width: 100,
renderer: function(v) { return v === 'active' ? 'Active' : 'Inactive'; }
},
{ text: 'Created', dataIndex: 'createdAt', width: 120, xtype: 'datecolumn', format: 'Y-m-d' },
{ xtype: 'actioncolumn', width: 80,
items: [
{ iconCls: 'x-fa fa-pencil', tooltip: 'Edit', handler: 'onEditUser' },
{ iconCls: 'x-fa fa-trash', tooltip: 'Delete', handler: 'onDeleteUser' }
]
}
],
tbar: [
{ text: 'Add User', iconCls: 'x-fa fa-plus', handler: 'onAddUser' },
'->',
{ xtype: 'textfield', emptyText: 'Search users...', width: 200, listeners: { change: 'onSearch' } }
],
bbar: { xtype: 'pagingtoolbar', displayInfo: true }
});
// app/view/users/Form.js
Ext.define('Enterprise.view.users.Form', {
extend: 'Ext.window.Window',
alias: 'widget.userform',
title: 'User',
width: 450,
modal: true,
layout: 'fit',
items: [{
xtype: 'form',
reference: 'userForm',
bodyPadding: 15,
defaults: { anchor: '100%', labelWidth: 100 },
items: [
{ xtype: 'textfield', fieldLabel: 'Name', name: 'name', allowBlank: false },
{ xtype: 'textfield', fieldLabel: 'Email', name: 'email', vtype: 'email' },
{ xtype: 'combobox', fieldLabel: 'Role', name: 'role', store: ['Admin', 'Editor', 'Viewer'], queryMode: 'local' },
{ xtype: 'combobox', fieldLabel: 'Status', name: 'status', store: ['active', 'inactive'], queryMode: 'local' }
]
}],
buttons: [
{ text: 'Save', handler: 'onSaveUser' },
{ text: 'Cancel', handler: function(btn) { btn.up('window').close(); } }
]
});
// app/controller/Users.js
Ext.define('Enterprise.controller.Users', {
extend: 'Ext.app.Controller',
stores: ['Users'],
refs: [
{ ref: 'usersGrid', selector: 'usersgrid' },
{ ref: 'userForm', selector: 'userform' }
],
control: {
'usersgrid': { itemdblclick: 'onEditUser' },
'usersgrid button[handler=onAddUser]': { click: 'onAddUser' },
'usersgrid button[handler=onEditUser]': { click: 'onEditUserFromAction' }
},
routes: {
'users': 'showUsers'
},
showUsers: function() {
var content = Ext.getCmp('contentPanel');
content.removeAll(true);
content.add({ xtype: 'usersgrid' });
},
onAddUser: function() {
var form = Ext.create('Enterprise.view.users.Form');
form.getViewModel().set('record', null);
form.show();
},
onEditUser: function(grid, record) {
var form = Ext.create('Enterprise.view.users.Form');
form.getForm().loadRecord(record);
form.show();
},
onSaveUser: function(btn) {
var win = btn.up('window');
var form = win.down('form').getForm();
if (form.isValid()) {
var record = form.getRecord() || Ext.create('Enterprise.model.User');
form.updateRecord(record);
this.getUsersStore().add(record);
this.getUsersStore().sync();
win.close();
}
}
});
Expected output: The Users module provides full CRUD with a paginated grid, inline search, add/edit form in a modal window, and delete confirmation. All changes sync to the server via the Model's proxy.
Analytics Module
// app/view/analytics/Charts.js
Ext.define('Enterprise.view.analytics.Charts', {
extend: 'Ext.container.Container',
alias: 'widget.analyticsview',
layout: 'vbox',
bodyPadding: 20,
items: [{
xtype: 'container',
layout: 'hbox',
height: 400,
defaults: { flex: 1, margin: '0 10 0 0' },
items: [{
xtype: 'cartesian',
title: 'Monthly Revenue',
store: { type: 'analytics', /* with revenue data */ },
axes: [
{ type: 'category', position: 'bottom', fields: ['month'] },
{ type: 'numeric', position: 'left', fields: ['revenue'], title: 'Revenue ($)' }
],
series: [{ type: 'bar', xField: 'month', yField: 'revenue' }],
interactions: [{ type: 'itemhighlight' }]
}, {
xtype: 'cartesian',
title: 'User Growth',
store: { type: 'analytics' },
axes: [
{ type: 'category', position: 'bottom', fields: ['month'] },
{ type: 'numeric', position: 'left', fields: ['users'], title: 'Users' }
],
series: [{ type: 'line', xField: 'month', yField: 'users', smooth: true }]
}]
}, {
xtype: 'container',
layout: 'hbox',
flex: 1,
defaults: { flex: 1, margin: '10 10 0 0' },
items: [{
xtype: 'polar',
title: 'Revenue by Category',
store: { type: 'analytics' },
series: [{ type: 'pie', angleField: 'amount', labelField: 'category', donut: 30 }],
legend: { docked: 'right' }
}, {
xtype: 'cartesian',
title: 'Orders by Status',
store: { type: 'analytics' },
axes: [
{ type: 'category', position: 'bottom', fields: ['status'] },
{ type: 'numeric', position: 'left', fields: ['count'] }
],
series: [{ type: 'bar', xField: 'status', yField: 'count', style: { fill: '#4caf50' } }]
}]
}]
});
// app/controller/Analytics.js
Ext.define('Enterprise.controller.Analytics', {
extend: 'Ext.app.Controller',
stores: ['Analytics'],
routes: {
'analytics': 'showAnalytics'
},
showAnalytics: function() {
var content = Ext.getCmp('contentPanel');
content.removeAll(true);
content.add({ xtype: 'analyticsview' });
this.getAnalyticsStore().load();
}
});
Expected output: The Analytics module shows four charts — revenue bar chart, user growth line chart, revenue by category donut chart, and orders by status bar chart — all bound to the Analytics store.
Routing Integration
// app/controller/Router.js
Ext.define('Enterprise.controller.Router', {
extend: 'Ext.app.Controller',
refs: [{
ref: 'sidebar',
selector: 'appsidebar'
}],
control: {
'appsidebar': {
itemclick: 'onNavClick'
}
},
init: function() {
// Listen to history changes for active nav highlighting
Ext.History.on('change', function(token) {
var sidebar = this.getSidebar();
if (sidebar) {
sidebar.getStore().getRoot().cascadeBy(function(node) {
node.set('cls', node.get('route') === token ? 'active-nav' : '');
});
}
}, this);
},
onNavClick: function(tree, record) {
var route = record.get('route');
if (route) {
this.redirectTo(route);
}
},
routes: {
'dashboard': 'onDashboard',
'users': 'onUsers',
'analytics': 'onAnalytics',
'orders': 'onOrders',
// Default route
'': 'onDashboard',
// Catch-all
'*path': 'onNotFound'
},
onDashboard: function() {
var content = Ext.getCmp('contentPanel');
content.removeAll(true);
content.add({ xtype: 'dashboardview' });
},
onUsers: function() {
this.getController('Users').showUsers();
},
onAnalytics: function() {
this.getController('Analytics').showAnalytics();
},
onOrders: function() {
// Similar pattern
},
onNotFound: function() {
Ext.Msg.alert('Not Found', 'The requested page was not found.');
}
});
Expected output: Clicking sidebar items navigates via routes. The URL hash updates (e.g., #users). Browser back/forward works. The active navigation item is highlighted.
Theming
// sass/var/all.scss
$base-color: #1a237e;
$base-highlight-color: #283593;
$base-light-color: #e8eaf6;
$font-family: 'Inter', 'Segoe UI', sans-serif;
$panel-header-background-color: $base-color;
$panel-header-color: #ffffff;
$grid-header-background-color: $base-light-color;
$button-default-background-color: $base-color;
$button-default-color: #ffffff;
$form-field-focus-border-color: $base-color;
// app/controller/Theme.js
Ext.define('Enterprise.controller.Theme', {
extend: 'Ext.app.Controller',
control: {
'button[action=toggleTheme]': {
click: 'onToggleTheme'
}
},
onToggleTheme: function() {
var isDark = Ext.getDoc().hasCls('dark-theme');
Ext.getDoc().toggleCls('dark-theme');
Ext.getDoc().toggleCls('light-theme');
// Update CSS variables
var vars = isDark ? this.getLightVars() : this.getDarkVars();
Ext.Object.each(vars, function(key, value) {
Ext.getDoc().setStyle('--' + key, value);
});
localStorage.setItem('theme', isDark ? 'light' : 'dark');
},
getDarkVars: function() {
return {
'background-color': '#121212',
'text-color': '#e0e0e0',
'surface-color': '#1e1e1e',
'border-color': '#333'
};
},
getLightVars: function() {
return {
'background-color': '#ffffff',
'text-color': '#212121',
'surface-color': '#f5f5f5',
'border-color': '#e0e0e0'
};
}
});
Expected output: Light/dark theme toggle updates CSS variables and saves preference. The entire application follows the custom brand theme.
Application Bootstrap
// app/Application.js
Ext.define('Enterprise.Application', {
extend: 'Ext.app.Application',
name: 'Enterprise',
appFolder: 'app',
controllers: [
'Router',
'Users',
'Analytics',
'Orders',
'Dashboard',
'Theme'
],
stores: [
'Users',
'Analytics',
'Orders',
'Products'
],
launch: function() {
// Restore theme preference
var savedTheme = localStorage.getItem('theme') || 'light';
Ext.getDoc().addCls(savedTheme + '-theme');
// Create main viewport
Ext.create('Enterprise.view.Main');
// Navigate to initial route
var token = window.location.hash.replace('#', '') || 'dashboard';
this.getController('Router').redirectTo(token);
},
onAppUpdate: function() {
Ext.Msg.confirm('Update Available', 'A new version is available. Reload?', function(btn) {
if (btn === 'yes') window.location.reload();
});
}
});
// app.js
Ext.application({
name: 'Enterprise',
appFolder: 'app',
extend: 'Enterprise.Application',
requires: ['Enterprise.Application']
});
Expected output: The application boots, loads all controllers and stores, restores the saved theme, creates the viewport, and navigates to the initial route based on the URL hash.
Production Build
# Development build with Sencha Cmd
sencha app build development
# Production build (minified, optimized)
sencha app build production
# The production build outputs to build/production/Enterprise/
# Includes:
# - bootstrap.js (loader)
# - app.js (minified application code)
# - resources/ (CSS, images, fonts)
# - index.html
# Deploy to web server
cp -r build/production/Enterprise/* /var/www/html/enterprise/
# Nginx configuration for pushState routes
# location /enterprise {
# try_files $uri $uri/ /enterprise/index.html;
# }
Expected output: The production build produces optimized, minified files. The application runs from a single page with pushState or hash URL support.
Common Mistakes
Not planning the store structure first - Store and Model design should happen before views. Changing the data layer after views are built requires updating columns, forms, and controllers.
Overcomplicating the router - Start with hash-based routing. Add pushState only after verifying that the server supports URL rewriting.
Forgetting error handling - Server calls can fail. Always add failure callbacks to store.load(), form.submit(), and store.sync().
Not testing with production build - Development builds include all source files. Production builds may differ due to optimizations. Test the production build before deploying.
Ignoring loading states - Data loading from APIs takes time. Show load masks on grids and forms during data operations.
Practice Questions
- What is the role of the Application class in an Ext JS project?
- How does the Router controller connect URL hashes to views?
- What is the purpose of the stores array in a controller?
- How do you handle server-side errors in form submissions?
- What does the production build optimize compared to development?
Challenge: Extend the enterprise dashboard with a new Reports module (grid with report list, chart preview, export to CSV), add role-based permissions (hide admin features from viewers), implement a notification system (poll server for alerts, show in a badge on the header), and add keyboard shortcuts (Ctrl+N for new user, Ctrl+F for search focus).
FAQ
Mini Project
Build the complete enterprise dashboard as specified in this lesson: full MVC structure with Router, Users (CRUD), Analytics (charts), Orders (grid with detail view), Dashboard (KPI cards + recent data), theme toggle (light/dark with persistence), production build, and deployment configuration. This project serves as a template you can adapt for any enterprise Ext JS application.
What's Next
You have completed the Ext JS guide. Explore other frameworks or continue building real projects with the complete enterprise dashboard as your template.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro