Ext JS Extensions and Plugins — Custom Components, Mixins, and Reusable Logic
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
Not calling callParent() in overridden methods - Overriding initComponent, beforeRender, or other lifecycle methods without callParent() breaks the component's internal setup.
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.
Modifying Prototype instead of instance - Directly changing a class's prototype affects all instances. Use instance-level configs or override methods properly.
Not cleaning up plugin resources on destroy - Plugins should unbind listeners and remove DOM elements in a destroy method to prevent memory leaks.
Over-abstracting simple components - Not every reusable pattern needs a full extension. Sometimes a simple Factory function or config object is sufficient.
Practice Questions
- What is the difference between extending a component and creating a plugin?
- How do you register a custom xtype for a new component?
- What is a mixin and when would you use one?
- How do plugins hook into a component's lifecycle?
- 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
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