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',
targetCls: 'group-checkbox',
checkDataIndex: 'isChecked',
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);
var store = grid.getStore();
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();
var groups = store.getGroups();
groups.each(function (groupRec) {
var groupKey = groupRec.getGroupKey();
if (groupKey !== groupName) {
groupRec.each(function (rec) {
allChecked = rec.get(this.checkDataIndex);
groupName = rec.get(groupField);
if (allChecked === false) {
this.updateParentRecord(groupName, allChecked);
updateParentRecord: function (groupName, checked) {
var parentRecord = this.getParentRecord(groupName);
parentRecord.set(this.checkDataIndex, checked);
getParentRecord: function (groupName) {
// For Ext JS 6 and 5.1.1
metaGroup = this.getMetaGroup(groupName);
metaGroup = this.groupCache[groupName];
parentRecord = metaGroup.placeholder;
* 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
* @param {String} groupName
refreshView: function (groupName) {
onStoreUpdate: function (store, record, operation, modifiedFieldNames, details, eOpts) {
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) {
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.set(this.checkDataIndex, checked);
this.updatingRecords = false;
this.updateParentRecord(group, checked);
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.
Ext.define('App.view.Main', {
extend: 'Ext.container.Viewport'
, title: 'ExtJs - Grouped Grid Checkbox Column'
ftype: 'groupedcheckbox',
enableGroupingMenu: false,
//startCollapsed: true // produces strange bug
fields: ['name', 'department', 'isChecked']
, groupField: 'department'
, dataIndex: 'department'
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 ).
-webkit-appearance: none;
background-color: #ffffff;
border: 1px solid #919191;
-webkit-box-shadow: none !important;
-moz-box-shadow: none !important;
box-shadow: none !important;
input[type="checkbox"]:focus {
-webkit-box-shadow: none !important;
-moz-box-shadow: none !important;
box-shadow: none !important;
input[type="checkbox"]:checked {
input[type="checkbox"]:checked::after {
.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;
}