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.

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.

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

.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;
}