Skip to content

Ext JS Complete Project — Building an Enterprise Dashboard from Scratch

DodaTech Updated 2026-06-28 13 min read

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

  1. 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.

  2. Overcomplicating the router - Start with hash-based routing. Add pushState only after verifying that the server supports URL rewriting.

  3. Forgetting error handling - Server calls can fail. Always add failure callbacks to store.load(), form.submit(), and store.sync().

  4. Not testing with production build - Development builds include all source files. Production builds may differ due to optimizations. Test the production build before deploying.

  5. Ignoring loading states - Data loading from APIs takes time. Show load masks on grids and forms during data operations.

Practice Questions

  1. What is the role of the Application class in an Ext JS project?
  2. How does the Router controller connect URL hashes to views?
  3. What is the purpose of the stores array in a controller?
  4. How do you handle server-side errors in form submissions?
  5. 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

How do I handle authentication in an Ext JS application?

Create a login controller that redirects to a login form. Store the auth token in a global variable. Add a before guard on all routes that checks authentication. Use an Ajax interceptor to handle 401 responses.

What is the recommended build tool for Ext JS production builds?

Sencha Cmd is the official build tool. It compiles SCSS, minifies JS, generates a manifest, and produces optimized builds. For smaller projects, you can use Webpack with the Sencha Webpack plugin.

How do I optimize initial load time?

Use dynamic loading (require only needed classes), enable caching with cache.manifest or service workers, compress images, enable gzip on the server, and use the production build which creates a single optimized JS bundle.

Can I use npm packages with Ext JS?

Yes. Sencha Cmd supports npm integration. Install packages with npm and require them in your code. Ext JS 7+ has improved npm/Webpack integration for modern toolchains.

How do I debug an Ext JS application?

Use browser DevTools. The Sencha Inspector tool provides component tree inspection, store data viewing, and performance profiling. Enable Ext.Logger for detailed logging in development mode.

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