Skip to content

Ext JS Extensions and Plugins — Custom Components, Mixins, and Reusable Logic

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Ext JS Extensions and Plugins. We cover key concepts, practical examples, and best practices to help you master this topic.

Ext JS extensions allow you to build custom components, create reusable plugins, share behavior with mixins, and extend existing classes — enabling code reuse and consistent functionality across applications.

What You'll Learn

  • Extending Ext JS components
  • Creating custom plugins and mixins
  • Building reusable custom xtypes
  • Packaging and distributing extensions
  • Integration patterns for extensions

Why It Matters

Enterprise applications often need custom UI patterns — specialized grids, validated form fields, or business-specific components. Extensions encapsulate this logic in reusable packages that can be shared across teams and projects.

Real-World Use

A reusable file upload panel with drag-and-drop, progress tracking, and thumbnail preview, packaged as an extension and used across multiple enterprise applications. A grid plugin that adds Excel-like column summaries with custom aggregation functions.

Extension Architecture

flowchart TD
    A[Extension Types] --> B[Extend Component]
    A --> C[Create Plugin]
    A --> D[Mixin]
    A --> E[Custom Xtype]
    B --> F[Ext.grid.Panel]
    B --> G[Ext.form.field.Text]
    C --> H[Plugin]
    H --> I[Before/After Hooks]
    H --> J[Events]
    D --> K[Reusable Logic]
    E --> L[New Component Class]
    style A fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px

Extending a Component

// Custom Number Field with currency formatting
Ext.define('MyApp.form.field.Currency', {
  extend: 'Ext.form.field.Number',
  alias: 'widget.currencyfield',
  // Default config
  step: 0.01,
  minValue: 0,
  hideTrigger: true,
  // Custom config
  currencySymbol: '$',
  // Override methods
  rawToValue: function(rawValue) {
    // Remove currency symbol and commas before parse
    var cleaned = String(rawValue).replace(/[^0-9.-]/g, '');
    return parseFloat(cleaned) || 0;
  },
  valueToRaw: function(value) {
    // Format with currency symbol
    return this.currencySymbol + Ext.util.Format.number(value || 0, '0,000.00');
  },
  // Custom method
  setCurrencySymbol: function(symbol) {
    this.currencySymbol = symbol;
    this.setRawValue(this.valueToRaw(this.getValue()));
  }
});

// Usage
{
  xtype: 'currencyfield',
  fieldLabel: 'Price',
  currencySymbol: '$',
  name: 'price'
}

Expected output: The currency field accepts numeric input but displays formatted values like $1,234.56. The rawToValue/valueToRaw methods handle the conversion transparently.

Grid Extension with Actions

Ext.define('MyApp.grid.ActionGrid', {
  extend: 'Ext.grid.Panel',
  alias: 'widget.actiongrid',
  // Default config
  viewConfig: {
    stripeRows: true,
    enableTextSelection: true
  },
  initComponent: function() {
    var me = this;
    // Add action column if configured
    if (me.enableActions !== false) {
      me.columns = me.columns || [];
      me.columns.push({
        xtype: 'actioncolumn',
        width: me.actionWidth || 100,
        items: me.actionItems || [
          { iconCls: 'x-fa fa-pencil', tooltip: 'Edit', handler: function(grid, rowIndex) {
            var record = grid.getStore().getAt(rowIndex);
            me.fireEvent('editAction', grid, record, rowIndex);
          }},
          { iconCls: 'x-fa fa-trash', tooltip: 'Delete', handler: function(grid, rowIndex) {
            var record = grid.getStore().getAt(rowIndex);
            me.fireEvent('deleteAction', grid, record, rowIndex);
          }}
        ]
      });
    }
    // Add paging toolbar if enabled
    if (me.showPaging !== false) {
      me.dockedItems = me.dockedItems || [];
      me.dockedItems.push({
        xtype: 'pagingtoolbar',
        dock: 'bottom',
        store: me.store,
        displayInfo: true
      });
    }
    me.callParent();
  }
});

// Usage
{
  xtype: 'actiongrid',
  title: 'Users',
  store: 'Users',
  enableActions: true,
  actionWidth: 80,
  actionItems: [
    { iconCls: 'x-fa fa-eye', tooltip: 'View', handler: function(grid, rowIndex) {
      console.log('View:', grid.getStore().getAt(rowIndex).get('name'));
    }}
  ],
  columns: [
    { text: 'Name', dataIndex: 'name', flex: 1 },
    { text: 'Email', dataIndex: 'email', flex: 2 }
  ],
  listeners: {
    editAction: function(grid, record) {
      console.log('Edit:', record.get('name'));
    },
    deleteAction: function(grid, record) {
      grid.getStore().remove(record);
    }
  }
}

Expected output: The ActionGrid automatically adds an action column with edit/delete buttons and a paging toolbar. Users can override action items and listen to custom editAction and deleteAction events.

Creating a Plugin

Ext.define('MyApp.plugin.FormStateSave', {
  extend: 'Ext.plugin.Abstract',
  alias: 'plugin.formstatesave',
  // Plugin config
  saveOnChange: true,
  autoRestore: true,
  stateKey: null,
  init: function(form) {
    this.form = form;
    // Auto-restore saved state
    if (this.autoRestore) {
      this.restoreState();
    }
    // Save on field change
    if (this.saveOnChange) {
      form.getForm().getFields().each(function(field) {
        field.on('change', this.saveState, this);
      }, this);
    }
    // Save on form submit success
    form.on('submitsuccess', function() {
      this.clearState();
    }, this);
  },
  saveState: function() {
    if (this.stateKey) {
      var values = this.form.getForm().getValues();
      localStorage.setItem('formstate-' + this.stateKey, Ext.JSON.encode(values));
      this.form.setTitle(this.form.title + ' *'); // Indicate unsaved
    }
  },
  restoreState: function() {
    if (this.stateKey) {
      var saved = localStorage.getItem('formstate-' + this.stateKey);
      if (saved) {
        this.form.getForm().setValues(Ext.JSON.decode(saved));
        this.form.setTitle(this.form.title + ' (restored)');
      }
    }
  },
  clearState: function() {
    if (this.stateKey) {
      localStorage.removeItem('formstate-' + this.stateKey);
    }
  },
  destroy: function() {
    this.clearState();
    this.callParent();
  }
});

// Usage
Ext.create('Ext.form.Panel', {
  title: 'Product Form',
  plugins: [{
    ptype: 'formstatesave',
    stateKey: 'product-form'
  }],
  items: [/* fields */]
});

Expected output: The FormStateSave plugin automatically saves form field values to localStorage as the user types. Refreshing the page restores the saved state. Successful submission clears the saved state.

Using Mixins

// Mixin: Add dirty tracking to any component
Ext.define('MyApp.mixin.DirtyTracking', {
  extend: 'Ext.Mixin',
  mixinConfig: {
    id: 'dirtyTracking',
    after: {
      initComponent: 'initDirtyTracking'
    }
  },
  // Config
  dirty: false,
  originalData: null,
  initDirtyTracking: function() {
    this.originalData = this.getState();
    this.on('change', this.onDirtyChange, this);
  },
  onDirtyChange: function() {
    var current = this.getState();
    var isDirty = Ext.encode(current) !== Ext.encode(this.originalData);
    this.setDirty(isDirty);
    this.fireEvent('dirtystatechange', this, isDirty);
  },
  setDirty: function(value) {
    this.dirty = value;
    if (this.el) {
      this.el[value ? 'addCls' : 'removeCls']('dirty-indicator');
    }
  },
  resetDirty: function() {
    this.originalData = this.getState();
    this.setDirty(false);
  },
  getState: function() {
    // Override in component
    return {};
  }
});

// Apply mixin to form panel
Ext.define('MyApp.form.DirtyForm', {
  extend: 'Ext.form.Panel',
  alias: 'widget.dirtyform',
  mixins: ['MyApp.mixin.DirtyTracking'],
  getState: function() {
    return this.getForm().getValues();
  }
});

// Apply mixin to grid panel
Ext.define('MyApp.grid.DirtyGrid', {
  extend: 'Ext.grid.Panel',
  alias: 'widget.dirtygrid',
  mixins: ['MyApp.mixin.DirtyTracking'],
  getState: function() {
    return this.getStore().getRange().map(function(r) { return r.getData(); });
  }
});

Expected output: The DirtyTracking mixin adds dirty state tracking to any component. Forms and grids that use the mixin fire dirtystatechange events and show a visual dirty indicator.

Custom Xtype Component

Ext.define('MyApp.panel.CollapsibleCard', {
  extend: 'Ext.panel.Panel',
  alias: 'widget.infocard',
  layout: 'fit',
  bodyPadding: 15,
  // Custom configs
  iconCls: null,
  cardColor: '#4a90d9',
  collapsible: true,
  collapsed: false,
  animCollapse: true,
  initComponent: function() {
    var me = this;
    // Add icon to title
    if (me.iconCls) {
      me.title = '<i class="' + me.iconCls + '" style="margin-right:8px"></i>' + (me.title || '');
    }
    // Apply card color to header
    me.header = {
      style: {
        backgroundColor: me.cardColor,
        color: '#ffffff'
      }
    };
    // Add a tool for refresh
    me.tools = [{
      type: 'refresh',
      handler: function() {
        me.fireEvent('cardRefresh', me);
      }
    }];
    me.callParent();
  },
  // Custom method
  setCardColor: function(color) {
    this.cardColor = color;
    if (this.header) {
      this.header.el.setStyle('background-color', color);
    }
  }
});

// Usage
{
  xtype: 'infocard',
  title: 'Revenue Summary',
  iconCls: 'x-fa fa-dollar',
  cardColor: '#4caf50',
  width: 300,
  height: 200,
  html: '<h2>$125,000</h2><p>Total revenue this quarter</p>',
  listeners: {
    cardRefresh: function(card) {
      console.log('Refresh:', card.title);
    }
  }
}

Expected output: The InfoCard is a reusable panel with colored header, icon support, collapsible body, refresh tool, and custom events. It encapsulates a common dashboard card pattern.

Plugin for Column Summaries

Ext.define('MyApp.plugin.GridSummary', {
  extend: 'Ext.plugin.Abstract',
  alias: 'plugin.gridsummary',
  config: {
    // Summary row config
    position: 'bottom', // 'bottom' or 'top'
    summaryText: 'Summary',
    // Summary functions per column
    summaries: {}
  },
  init: function(grid) {
    grid.on('afterrender', this.addSummaryRow, this);
    grid.getStore().on('datachanged', this.onDataChange, this);
  },
  addSummaryRow: function(grid) {
    this.summaryRow = grid.addDocked({
      xtype: 'toolbar',
      dock: this.getPosition(),
      cls: 'grid-summary-row',
      style: { fontWeight: 'bold', backgroundColor: '#f5f5f5' },
      items: this.buildSummaryItems(grid)
    });
  },
  buildSummaryItems: function(grid) {
    var items = [];
    var summaries = this.getSummaries();
    grid.getColumns().each(function(col) {
      var dataIndex = col.dataIndex;
      if (summaries[dataIndex]) {
        items.push({
          xtype: 'tbtext',
          text: summaries[dataIndex].label + ': ' + this.calculateSummary(grid, dataIndex, summaries[dataIndex]),
          margin: '0 10 0 0'
        });
      }
    }, this);
    return items.length ? items : [{ xtype: 'tbtext', text: this.getSummaryText() }];
  },
  calculateSummary: function(grid, field, config) {
    var store = grid.getStore();
    var values = store.getRange().map(function(r) { return r.get(field); });
    switch (config.type) {
      case 'sum': return Ext.util.Format.number(values.reduce(function(a, b) { return a + b; }, 0), '0,000.00');
      case 'avg': return Ext.util.Format.number(values.reduce(function(a, b) { return a + b; }, 0) / values.length, '0,000.00');
      case 'count': return values.length;
      case 'min': return Math.min.apply(null, values);
      case 'max': return Math.max.apply(null, values);
      default: return '';
    }
  },
  onDataChange: function(store) {
    if (this.summaryRow) {
      var grid = this.summaryRow.up();
      if (grid) {
        this.summaryRow.removeAll(true);
        this.summaryRow.add(this.buildSummaryItems(grid));
      }
    }
  }
});

// Usage
{
  xtype: 'grid',
  plugins: [{
    ptype: 'gridsummary',
    summaries: {
      'price': { label: 'Total', type: 'sum' },
      'quantity': { label: 'Count', type: 'count' },
      'rating': { label: 'Avg', type: 'avg' }
    }
  }]
}

Expected output: The GridSummary plugin adds a summary row at the bottom of the grid showing sum, count, average, min, or max for specified columns. The summary updates automatically when store data changes.

Common Mistakes

  1. Not calling callParent() in overridden methods - Overriding initComponent, beforeRender, or other lifecycle methods without callParent() breaks the component's internal setup.

  2. Creating plugin instances instead of configs - In the plugins array, use { ptype: 'name' } configs, not Ext.create('Plugin'). Ext JS instantiates plugins lazily from ptype.

  3. Modifying Prototype instead of instance - Directly changing a class's prototype affects all instances. Use instance-level configs or override methods properly.

  4. Not cleaning up plugin resources on destroy - Plugins should unbind listeners and remove DOM elements in a destroy method to prevent memory leaks.

  5. Over-abstracting simple components - Not every reusable pattern needs a full extension. Sometimes a simple Factory function or config object is sufficient.

Practice Questions

  1. What is the difference between extending a component and creating a plugin?
  2. How do you register a custom xtype for a new component?
  3. What is a mixin and when would you use one?
  4. How do plugins hook into a component's lifecycle?
  5. What is the purpose of the alias configuration in extensions?

Challenge: Build a complete custom extension library with: a StatusGrid extension that adds status indicators and quick filters, a PhoneField extension that formats phone numbers as the user types, a plugin that adds keyboard shortcut support to any grid, a mixin that provides undo/redo capability, and a custom xtype for a date range picker panel.

FAQ

What is the difference between extend and override in Ext JS?

extend creates a subclass that inherits and can add new behavior. override modifies the original class directly, affecting all existing and future instances. Use extend for custom components, override for bug fixes.

Can I distribute Ext JS extensions as packages?

Yes. Sencha Cmd package format (packages/local/) allows distribution. Package includes SCSS, JavaScript, resources, and a package.json manifest for dependency management.

How do plugins differ from mixins?

Plugins are instances added to a single component (has-a relationship). Mixins share behavior across classes (is-a relationship). Use plugins for optional features, mixins for required capabilities.

Can I extend a component from a third-party extension?

Yes. Ext JS's class system supports multi-level inheritance. Extend from any class, including custom extensions, as long as the parent class is loaded first.

How do I ensure backward compatibility for my extensions?

Follow semantic versioning. Mark deprecated methods with Ext.deprecate. Provide migration guides. Add config defaults for new options so existing code continues working.

Mini Project

Build a reusable extension library for an order management system: an OrderGrid extension with built-in status colors, quick filters, and action buttons, a CurrencyField extension with configurable currency symbol and formatting, an AutoSave plugin that saves form data to localStorage periodically, a DirtyTracking mixin for forms and grids, and an AddressField Composite component combining street, city, state, and zip fields into one xtype.

What's Next

Extensions let you build reusable components. Learn how Ext JS Complete Project brings everything together in a full application build from start to finish.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro