ExtJs Grid has a lot of features built in. Some examples are the Grouped Grid and the Checkbox Selection.

However, grouping these two together poses a bit of a challenge.

After searching on the forums, this post guided me into a nice, working example.

The Grouping Check column has been created as a feature.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Ext.define('App.GroupedCheckboxFeature', {
extend: 'Ext.grid.feature.Grouping',
alias: 'feature.groupedcheckbox',
/** @property */
targetCls: 'group-checkbox',
/** @property */
checkDataIndex: 'isChecked',
startCollapsed: true,
constructor: function (config) {
config.groupHeaderTpl = ['<div class="group-header"><input class="' + this.targetCls + '" {[values.record.get("' + this.checkDataIndex + '") ? "checked" : ""]} type="checkbox"/><span class="group-title">{name}</span></div>'];
this.callParent(arguments);
},
init: function (grid) {
var store = grid.getStore();
if (store) {
store.on('update', this.onStoreUpdate, this);
}
this.callParent(arguments);
},
setupRowData: function (record, idx, rowValues) {
this.callParent(arguments);
// Ext JS 6 vs Ext JS 5.1.1 vs Ext JS 5.1.0-
var groupInfo = this.groupRenderInfo || this.metaGroupCache || this.groupInfo;
groupInfo.record = this.getParentRecord(record.get(this.getGroupField()));
},
/**
* This method will only run once... on the initial load of the view... this
* is so we can check the store for the grouped item's children... if they're
* all checked, then we need to set the private variable to checked
*/
checkAllGroups: function (groupName) {
var store = this.view.getStore();
var groupField = this.getGroupField();
if (store) {
var groups = store.getGroups();
if (groups) {
groups.each(function (groupRec) {
var allChecked = true;
var groupKey = groupRec.getGroupKey();
var checkGroup = true;
if (groupName) {
if (groupKey !== groupName) {
checkGroup = false;
}
}
if (checkGroup) {
groupRec.each(function (rec) {
allChecked = rec.get(this.checkDataIndex);
groupName = rec.get(groupField);
if (allChecked === false) {
return false;
}
}, this);
this.updateParentRecord(groupName, allChecked);
}
}, this);
}
}
},
updateParentRecord: function (groupName, checked) {
var parentRecord = this.getParentRecord(groupName);
if (parentRecord) {
parentRecord.set(this.checkDataIndex, checked);
this.refreshView();
}
},
getParentRecord: function (groupName) {
var parentRecord;
var metaGroup;
// For Ext JS 6 and 5.1.1
if (this.getMetaGroup) {
metaGroup = this.getMetaGroup(groupName);
}
// For Ext JS 5.1-
else {
metaGroup = this.groupCache[groupName];
}
if (metaGroup) {
parentRecord = metaGroup.placeholder;
}
return parentRecord;
},
/**
* TODO: This might break... we're using a private variable here... but this
* is the only way we can refresh the view without breaking any sort of
* scrolling... I'm not sure how to only refresh the group header itself, so
* I'm keeping the groupName as a param passing in... might be able to figure
* this out later
* @param {String} groupName
*/
refreshView: function (groupName) {
var view = this.view;
if (view) {
view.refreshView();
}
},
onStoreUpdate: function (store, record, operation, modifiedFieldNames, details, eOpts) {
var grid = this.grid;
if (!this.updatingRecords && grid && record) {
var groupName = record.get(this.getGroupField());
this.checkAllGroups(groupName);
grid.setSelection(record);
this.refreshView(groupName);
}
},
onGroupClick: function (grid, node, group, event, eOpts) {
if (event && grid) {
var target = event.getTarget('.' + this.targetCls);
var store = grid.getStore();
var groupRecord = this.getRecordGroup(event.record);
if (target && store && groupRecord) {
var checked = target.checked;
this.updatingRecords = true;
groupRecord.each(function (rec, index) {
rec.beginEdit();
rec.set(this.checkDataIndex, checked);
rec.endEdit(true);
}, this);
this.updatingRecords = false;
this.updateParentRecord(group, checked);
} else {
this.callParent(arguments);
}
}
}
});
Ext.define('App.GroupedCheckboxFeature', { extend: 'Ext.grid.feature.Grouping', alias: 'feature.groupedcheckbox', /** @property */ targetCls: 'group-checkbox', /** @property */ checkDataIndex: 'isChecked', startCollapsed: true, constructor: function (config) { config.groupHeaderTpl = ['<div class="group-header"><input class="' + this.targetCls + '" {[values.record.get("' + this.checkDataIndex + '") ? "checked" : ""]} type="checkbox"/><span class="group-title">{name}</span></div>']; this.callParent(arguments); }, init: function (grid) { var store = grid.getStore(); if (store) { store.on('update', this.onStoreUpdate, this); } this.callParent(arguments); }, setupRowData: function (record, idx, rowValues) { this.callParent(arguments); // Ext JS 6 vs Ext JS 5.1.1 vs Ext JS 5.1.0- var groupInfo = this.groupRenderInfo || this.metaGroupCache || this.groupInfo; groupInfo.record = this.getParentRecord(record.get(this.getGroupField())); }, /** * This method will only run once... on the initial load of the view... this * is so we can check the store for the grouped item's children... if they're * all checked, then we need to set the private variable to checked */ checkAllGroups: function (groupName) { var store = this.view.getStore(); var groupField = this.getGroupField(); if (store) { var groups = store.getGroups(); if (groups) { groups.each(function (groupRec) { var allChecked = true; var groupKey = groupRec.getGroupKey(); var checkGroup = true; if (groupName) { if (groupKey !== groupName) { checkGroup = false; } } if (checkGroup) { groupRec.each(function (rec) { allChecked = rec.get(this.checkDataIndex); groupName = rec.get(groupField); if (allChecked === false) { return false; } }, this); this.updateParentRecord(groupName, allChecked); } }, this); } } }, updateParentRecord: function (groupName, checked) { var parentRecord = this.getParentRecord(groupName); if (parentRecord) { parentRecord.set(this.checkDataIndex, checked); this.refreshView(); } }, getParentRecord: function (groupName) { var parentRecord; var metaGroup; // For Ext JS 6 and 5.1.1 if (this.getMetaGroup) { metaGroup = this.getMetaGroup(groupName); } // For Ext JS 5.1- else { metaGroup = this.groupCache[groupName]; } if (metaGroup) { parentRecord = metaGroup.placeholder; } return parentRecord; }, /** * TODO: This might break... we're using a private variable here... but this * is the only way we can refresh the view without breaking any sort of * scrolling... I'm not sure how to only refresh the group header itself, so * I'm keeping the groupName as a param passing in... might be able to figure * this out later * @param {String} groupName */ refreshView: function (groupName) { var view = this.view; if (view) { view.refreshView(); } }, onStoreUpdate: function (store, record, operation, modifiedFieldNames, details, eOpts) { var grid = this.grid; if (!this.updatingRecords && grid && record) { var groupName = record.get(this.getGroupField()); this.checkAllGroups(groupName); grid.setSelection(record); this.refreshView(groupName); } }, onGroupClick: function (grid, node, group, event, eOpts) { if (event && grid) { var target = event.getTarget('.' + this.targetCls); var store = grid.getStore(); var groupRecord = this.getRecordGroup(event.record); if (target && store && groupRecord) { var checked = target.checked; this.updatingRecords = true; groupRecord.each(function (rec, index) { rec.beginEdit(); rec.set(this.checkDataIndex, checked); rec.endEdit(true); }, this); this.updatingRecords = false; this.updateParentRecord(group, checked); } else { this.callParent(arguments); } } } });
Ext.define('App.GroupedCheckboxFeature', {
	extend: 'Ext.grid.feature.Grouping',
	alias: 'feature.groupedcheckbox',

	/** @property */
	targetCls: 'group-checkbox',
	/** @property */
	checkDataIndex: 'isChecked',
	startCollapsed: true,

	constructor: function (config) {
		config.groupHeaderTpl = ['<div class="group-header"><input class="' + this.targetCls + '" {[values.record.get("' + this.checkDataIndex + '") ? "checked" : ""]} type="checkbox"/><span class="group-title">{name}</span></div>'];
		this.callParent(arguments);
	},

	init: function (grid) {
		var store = grid.getStore();
		if (store) {
			store.on('update', this.onStoreUpdate, this);
		}
		this.callParent(arguments);
	},

	setupRowData: function (record, idx, rowValues) {
		this.callParent(arguments);
		// Ext JS 6 vs Ext JS 5.1.1 vs Ext JS 5.1.0-
		var groupInfo = this.groupRenderInfo || this.metaGroupCache || this.groupInfo;
		groupInfo.record = this.getParentRecord(record.get(this.getGroupField()));
	},

	/**
	 * This method will only run once... on the initial load of the view... this
	 * is so we can check the store for the grouped item's children... if they're
	 * all checked, then we need to set the private variable to checked
	 */
	checkAllGroups: function (groupName) {
		var store = this.view.getStore();
		var groupField = this.getGroupField();
		if (store) {
			var groups = store.getGroups();
			if (groups) {
				groups.each(function (groupRec) {
					var allChecked = true;
					var groupKey = groupRec.getGroupKey();
					var checkGroup = true;
					if (groupName) {
						if (groupKey !== groupName) {
							checkGroup = false;
						}
					}
					if (checkGroup) {
						groupRec.each(function (rec) {
							allChecked = rec.get(this.checkDataIndex);
							groupName = rec.get(groupField);
							if (allChecked === false) {
								return false;
							}
						}, this);
						this.updateParentRecord(groupName, allChecked);
					}
				}, this);
			}
		}
	},

	updateParentRecord: function (groupName, checked) {
		var parentRecord = this.getParentRecord(groupName);
		if (parentRecord) {
			parentRecord.set(this.checkDataIndex, checked);
			this.refreshView();
		}
	},

	getParentRecord: function (groupName) {
		var parentRecord;
		var metaGroup;
		// For Ext JS 6 and 5.1.1
		if (this.getMetaGroup) {
			metaGroup = this.getMetaGroup(groupName);
		}
		// For Ext JS 5.1-
		else {
			metaGroup = this.groupCache[groupName];
		}
		if (metaGroup) {
			parentRecord = metaGroup.placeholder;
		}
		return parentRecord;
	},

	/**
	 * TODO: This might break... we're using a private variable here... but this
	 * is the only way we can refresh the view without breaking any sort of
	 * scrolling... I'm not sure how to only refresh the group header itself, so
	 * I'm keeping the groupName as a param passing in... might be able to figure
	 * this out later
	 * @param {String} groupName
	 */
	refreshView: function (groupName) {
		var view = this.view;
		if (view) {
			view.refreshView();
		}
	},

	onStoreUpdate: function (store, record, operation, modifiedFieldNames, details, eOpts) {
		var grid = this.grid;
		if (!this.updatingRecords && grid && record) {
			var groupName = record.get(this.getGroupField());
			this.checkAllGroups(groupName);
			grid.setSelection(record);
			this.refreshView(groupName);
		}
	},

	onGroupClick: function (grid, node, group, event, eOpts) {
		if (event && grid) {
			var target = event.getTarget('.' + this.targetCls);
			var store = grid.getStore();
			var groupRecord = this.getRecordGroup(event.record);
			if (target && store && groupRecord) {
				var checked = target.checked;
				this.updatingRecords = true;
				groupRecord.each(function (rec, index) {
					rec.beginEdit();
					rec.set(this.checkDataIndex, checked);
					rec.endEdit(true);
				}, this);
				this.updatingRecords = false;
				this.updateParentRecord(group, checked);
			} else {
				this.callParent(arguments);
			}
		}
	}
});

The Grid itself is a regular grid.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Ext.define('App.view.Main', {
extend: 'Ext.container.Viewport'
, items: [{
xtype: 'grid'
, title: 'ExtJs - Grouped Grid Checkbox Column'
, titleAlign: 'center'
, viewConfig: {
markDirty: false
}
, features: [{
ftype: 'groupedcheckbox',
enableGroupingMenu: false,
hideGroupHeader: true,
//startCollapsed: true // produces strange bug
}]
, store: {
fields: ['name', 'department', 'isChecked']
, groupField: 'department'
, proxy: {
type: 'rest'
, url: 'data/store.json'
, reader: {
type: 'json'
, rootProperty: 'values'
}
}
, autoLoad: true
}
, columns: {
defaults: {
menuDisabled: true
, scrollable: true
, hideable: false
, draggable: false
}
, items: [{
xtype: 'checkcolumn'
, headerCheckbox: true
, text: ''
, dataIndex: 'isChecked'
, width: 50
}, {
text: 'Name'
, dataIndex: 'name'
, flex: 1
}, {
text: 'Department'
, dataIndex: 'department'
, flex: 1
}]
}
}]
});
Ext.define('App.view.Main', { extend: 'Ext.container.Viewport' , items: [{ xtype: 'grid' , title: 'ExtJs - Grouped Grid Checkbox Column' , titleAlign: 'center' , viewConfig: { markDirty: false } , features: [{ ftype: 'groupedcheckbox', enableGroupingMenu: false, hideGroupHeader: true, //startCollapsed: true // produces strange bug }] , store: { fields: ['name', 'department', 'isChecked'] , groupField: 'department' , proxy: { type: 'rest' , url: 'data/store.json' , reader: { type: 'json' , rootProperty: 'values' } } , autoLoad: true } , columns: { defaults: { menuDisabled: true , scrollable: true , hideable: false , draggable: false } , items: [{ xtype: 'checkcolumn' , headerCheckbox: true , text: '' , dataIndex: 'isChecked' , width: 50 }, { text: 'Name' , dataIndex: 'name' , flex: 1 }, { text: 'Department' , dataIndex: 'department' , flex: 1 }] } }] });
Ext.define('App.view.Main', {
	extend: 'Ext.container.Viewport'

	, items: [{
		xtype: 'grid'
		, title: 'ExtJs - Grouped Grid Checkbox Column'
		, titleAlign: 'center'

		, viewConfig: {
			markDirty: false
		}

		, features: [{
			ftype: 'groupedcheckbox',
			enableGroupingMenu: false,
			hideGroupHeader: true,
			//startCollapsed: true // produces strange bug
		}]

		, store: {
			fields: ['name', 'department', 'isChecked']
			, groupField: 'department'

			, proxy: {
				type: 'rest'
				, url: 'data/store.json'
				, reader: {
					type: 'json'
					, rootProperty: 'values'
				}
			}

			, autoLoad: true
		}

		, columns: {
			defaults: {
				menuDisabled: true
				, scrollable: true
				, hideable: false
				, draggable: false
			}
			, items: [{
				xtype: 'checkcolumn'
				, headerCheckbox: true
				, text: ''
				, dataIndex: 'isChecked'
				, width: 50
			}, {
				text: 'Name'
				, dataIndex: 'name'
				, flex: 1
			}, {
				text: 'Department'
				, dataIndex: 'department'
				, flex: 1
			}]
		}
	}]


});

But although it worked perfectly, the styling looked a bit off and that’s because it uses the standard HTML checkbox.

I had to hunt around for trying to customize it to look like an ExtJs checkbox.

Finally, with the help of this Stackoverflow solution, I was able to solve it!

I also did a bit of customization to the GroupHeaderTpl from the original post to make the group header texts align correctly (as seen above in the GroupedCheckboxFeature ).

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
.group-header {
display: flex;
align-items: center;
vertical-align: middle;
}
.group-title {
font-weight: bold;
}
input[type="checkbox"] {
width: 15px;
height: 15px;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
margin-right: 10px;
background-color: #ffffff;
outline: 0;
border: 1px solid #919191;
border-radius: 3px;
display: inline-block;
-webkit-box-shadow: none !important;
-moz-box-shadow: none !important;
box-shadow: none !important;
}
input[type="checkbox"]:focus {
outline: none;
border: none !important;
-webkit-box-shadow: none !important;
-moz-box-shadow: none !important;
box-shadow: none !important;
}
input[type="checkbox"]:checked {
outline: none;
border: none !important;
text-align: center;
line-height: 15px;
}
input[type="checkbox"]:checked::after {
content: "\e613";
font: 18px/1 "ExtJS";
color: #919191;
}
.group-header { display: flex; align-items: center; vertical-align: middle; } .group-title { font-weight: bold; } input[type="checkbox"] { width: 15px; height: 15px; -webkit-appearance: none; -moz-appearance: none; appearance: none; margin-right: 10px; background-color: #ffffff; outline: 0; border: 1px solid #919191; border-radius: 3px; display: inline-block; -webkit-box-shadow: none !important; -moz-box-shadow: none !important; box-shadow: none !important; } input[type="checkbox"]:focus { outline: none; border: none !important; -webkit-box-shadow: none !important; -moz-box-shadow: none !important; box-shadow: none !important; } input[type="checkbox"]:checked { outline: none; border: none !important; text-align: center; line-height: 15px; } input[type="checkbox"]:checked::after { content: "\e613"; font: 18px/1 "ExtJS"; color: #919191; }
.group-header {
	display: flex;
	align-items: center;
	vertical-align: middle;
}

.group-title {
	font-weight: bold;
}

input[type="checkbox"] {
	width: 15px;
	height: 15px;
	-webkit-appearance: none;
	-moz-appearance: none;
	appearance: none;
	margin-right: 10px;
	background-color: #ffffff;
	outline: 0;
	border: 1px solid #919191;
	border-radius: 3px;
	display: inline-block;
	-webkit-box-shadow: none !important;
	-moz-box-shadow: none !important;
	box-shadow: none !important;
}

input[type="checkbox"]:focus {
	outline: none;
	border: none !important;
	-webkit-box-shadow: none !important;
	-moz-box-shadow: none !important;
	box-shadow: none !important;
}

input[type="checkbox"]:checked {
	outline: none;
	border: none !important;
	text-align: center;
	line-height: 15px;
}

input[type="checkbox"]:checked::after {
	content: "\e613";
	font: 18px/1 "ExtJS";
	color: #919191;
}