How to have a checkbox in an ng-grid grouping - angularjs

Is it possible to have a check box in an ng-grid grouping header?
When that grouping check box is clicked, all the checkboxes of the rows under that particular group should be selected.
The other group rows should remain un-selected.
Right now, when I have my gridOptions as follows, I see checkboxes for all rows, and in the table header. When a column header is dragged to use it for grouping, the grouped rows-header do not have checkboxes.
Can anybody help?
$scope.gridOptions = {
data: 'myData',
showGroupPanel: true,
showSelectionCheckbox: true,
jqueryUITheme: true,
enableCellSelection: true,
enableRowSelection: true,
enableCellEdit: false,
pinSelectionCheckbox: false,
selectWithCheckboxOnly: true
};

In order to make it, you have first to override the default aggregateTemplate :
$scope.gridOptions = {
data: 'myData',
showGroupPanel: true,
showSelectionCheckbox: true,
jqueryUITheme: true,
enableRowSelection: true,
aggregateTemplate: "<div ng-init=\"initAggregateGroup(row)\" ng-style=\"rowStyle(row)\" style=\"top: 0px; height: 48px; left: 0px; cursor:pointer\" class=\"ngAggregate\">" +
" <span class=\"ngAggregateText\" ng-click=\"expandGroupChilds($event, row)\">{{row.label CUSTOM_FILTERS}} ({{row.totalChildren()}} {{AggItemsLabel}})</span>" +
"<input class=\"ngSelectionHeader\" type=\"checkbox\" ng-show=\"multiSelect\" ng-checked=\"row.status.isChecked\" ng-model=\"row.status.isChecked\" ng-change=\"toggleGroupSelectedChilds(row)\" /></div>"
};
Then you can add those functions in your controller:
$scope.expandAll = function($event){
$scope.isAllCollapsed = !$scope.isAllCollapsed;
_.each($scope.ngGridOptions.ngGrid.rowFactory.aggCache, function(row){
row.toggleExpand();
});
};
$scope.initAggregateGroup = function(group){
group.status = {
isChecked: false
};
};
$scope.toggleGroupSelectedChilds = function(group){
_.each(group.children, function(entry){
$scope.ngGridOptions.selectItem(entry.rowIndex, group.status.isChecked);
});
};
$scope.expandGroupChilds = function($event, group){
$event.stopPropagation();
$event.preventDefault();
group.toggleExpand();
};

I found this question while desperately seeking an answer to the same problem, and while Eric's answer pointed me in the right direction, it wasn't working as expected*.
Here's what I came to in the end, after a couple of days of playing around with the contents of the ngRow and ngAggregate objects.
My aggregateTemplate is pretty similar to Eric's, but there are a few differences. First, I've added the expand/collapse icon thing back in (span class="{{row.aggClass()}}></span>"). I've also moved the ng-click="expandGroupChildren($event, row)" to the containing div, and added ng-click="$event.stopPropagation()" to the checkbox. This means that clicking anywhere but the checkbox will expand/collapse the group. Finally, I've renamed some functions.
var aggregateTemplate = '<div ng-init="initAggregateGroup(row)" ng-style="rowStyle(row)" style="top: 0; height: 48px; left: 0; cursor:pointer" class="ngAggregate" ng-click="expandGroupChildren($event, row)">' +
'<span class="{{row.aggClass()}}"></span>' +
'<input class="ngSelectionHeader" type="checkbox" ng-show="multiSelect" ng-checked="row.status.isChecked" ng-model="row.status.isChecked" ng-change="setGroupSelection(row)" ng-click="$event.stopPropagation()" />' +
'<span class="ngAggregateText">{{row.label CUSTOM_FILTERS}} ({{row.totalChildren()}} {{AggItemsLabel}})</span>' +
'</div>';
But the function code is where I really diverge.
First, this just doesn't work for me:
$scope.gridOptions.selectItem(entry.rowIndex, group.status.isChecked)
Instead, in this version we call this:
row.selectionProvider.setSelection (row, group.status.isChecked)
Since it takes the row itself instead of an index, it just works, no matter how tangled the indices might get.
This version also works with nested groups. When you group by, for instance, City > Age, and you get a group Minnesota containing a group 53, clicking on the group header for 53 will select all 53-year-olds who live in Minesota, but not 53-year-olds in other cities or Minnesotans of other ages. Clicking the group header for Minnesota, on the other hand, will select everyone in Minnesota, and the group header checkboxes for every sub-group. Likewise, if we only have 53-year-olds in the the Minnesota group, then clicking the 53 checkbox will also tick the Minnesota checkbox.
And that brings me to the final change. With a watcher per group (I don't generally like watchers and try to avoid them, but sometimes they're a necessary evil), we keep track of the selection within each group and automatically tick the box in the group header when every row is selected. Just like the select-all checkbox at the top of the grid.
So here's the code:
angular.extend ($scope, {
initAggregateGroup : initAggregateGroup,
expandGroupChildren : expandGroupChildren,
setGroupSelection : setGroupSelection
});
function initAggregateGroup (group) {
group.status = {
isChecked: getGroupSelection (group)
};
$scope.$watch (
function () {
return getGroupSelection (group)
},
function (isSelected) {
setGroupSelection (group, isSelected);
}
)
}
function expandGroupChildren ($event, group) {
$event.stopPropagation ();
$event.preventDefault ();
group.toggleExpand ();
}
function setGroupSelection (group, isSelected) {
// Sets the field when called by the watcher (in which case the field needs to be updated)
// or during recursion (in which case the objects inside aggChildren don't have it set at all)
if (_.isBoolean (isSelected)) {
group.status = { isChecked : isSelected }
}
// Protects against infinite digest loops caused by the watcher above
if (group.status.isChecked === getGroupSelection (group)) { return }
_.forEach (group.children, function (row) {
// children: ngRow objects that represent actual data rows
row.selectionProvider.setSelection (row, group.status.isChecked);
});
_.forEach (group.aggChildren, function (subGroup) {
// aggChildren: ngAggregate objects that represent groups
setGroupSelection (subGroup, group.status.isChecked);
});
}
function getGroupSelection (group) {
if (group.children.length > 0) {
return _.every (group.children, 'selected');
}
if (group.aggChildren.length > 0) {
return _.every (group.aggChildren, getGroupSelection);
}
return false;
}
*Clicking the checkbox in the aggregateTemplate would select a seemingly random collection of rows from all across the grid (seemingly random, because it was consistent throughout separate sessions, for the same data).
I think the problem (at least for me in ngGrid 2.0.12) was that ngGrid wasn't properly mapping the rowIndex field to the right row in its model. I think this was because the rows were rearranged for the grouping as well as the sorting, and the internal mapping hadn't kept up.

Related

ng-dropdown-multiselect, does max items break UI

Using this very cool AngularJS control but having a problem with limiting the maximum number of items that can be selected.
http://dotansimha.github.io/angularjs-dropdown-multiselect/docs.
The documentation is vague at best although there are a lot of properties and features described. But after several hours of trying combinations of settings I almost got it to work as desired.
I want the dropdown button to display a custom label and show the count of the selected items. The problem is I can't seem to find any way to have this functionally and also limit the number of selected items the user can click on.
This said, I have not tried to see if it is possible to capture the click event and force the behavior I desire because the control describes the "smartButtonMaxItems" as a property that is intended to do just this, limit the number of selected items.
The JS
app.controller('AlphabeticController', ['$scope', '$window', function ($scope, $window){
var _this = this; // insure correct scope within callBacks to this controller
this.name = "AlphabeticController";
_this.names = [
{ 'pk': 1, 'userName': 'Priscila Gail Hane' },
{ 'pk': 2, 'userName': 'Milford Frank-Duell' },
{ 'pk': 3, 'userName': 'Wilson Albanese' },
{ 'pk': 4, 'userName': 'Aileen Hudec' }
];
// init the selected names collection
_this.oSelectedNames = [];
// create default alphabetic list of names
this.init = function () {
// sort names with local characgters (accent inorged)
_this.oSortedNames = angular.copy(_this.names);
_this.oSortedNames.sort(function (a, b) {
return a.userName.strength - b.userName.strength || a.userName.localeCompare(b.userName);
});
};
this.projectSettings = {
displayProp: 'userName',
idProp: 'pk',
externalIdProp: '',
styleActive: true,
showCheckAll: true,
showUncheckAll: true,
scrollable: true,
enableSearch: true,
keyboardControls: true,
dynamicTitle: true,
smartButtonMaxItems: 0,
smartButtonTextConverter: function (itemText, originalItem) {
return _this.oSelectedNames.length;
}
};
this.projectText = {
buttonDefaultText: _this.oSelectedNames.length + ' Selected Names',
selectionCount: 0,
selectionOf: 0,
searchPlaceholder: 'enter name',
dynamicButtonTextSuffix: ' Selected Names'
};
}]);
The HTML
<div ng-controller="AlphabeticController as AC" ng-init="AC.init()">
<div ng-dropdown-multiselect=""
options="AC.oSortedNames"
selected-model="AC.oSelectedNames"
translation-texts="AC.projectText"
extra-settings="AC.projectSettings"
search-filter="">
</div>
</div>
With the value of zero (0) set on the "smartButtonMaxItems" property the dropdown button works correctly but there is no upper limit to the number of items that can be selected. Whereas, with the "smartButtonMaxItems" set to a value smaller than the names array the control limits the number of items that can be selected by the button no longer shows the count and rather shows the names them selves.
Demo on Fiddler
To limit the selection i have added selectionLimit: 2 to this.projectSettings .
Also i have customized the selection message in the below fiddler . Hope this helps you
Here's Updated fiddler

Persist selected rows the correct way

I'm trying to make use of ngGrid.
The list represents a bunch of items which the users then can select (or not). This selection should be persisted so when the user comes back the grid shows the same selected items as last time. I made a plunker
However I've run into a bit of a problem using ngGrid.
I'm using afterSelectionChange to save selection changes to the grid.
$scope.gridOptions = {
data: 'myData',
showSelectionCheckbox: true,
afterSelectionChange: function(rowItem, event) {
// $http... save selection state
}
};
Which is fine. However when I want to select rows programmatically when the page loads all hell breaks loose. The below code is supposed to select the row with the name Enos, and it does. But it triggers afterSelectionChange 4 times.
$scope.$on('ngGridEventData', function() {
angular.forEach($scope.myData, function(data, index) {
if (data.name == 'Enos') {
$scope.gridOptions.selectItem(index, true);
}
});
});
That can't be intended. I made a plunker.
How to persist selected rows using ngGrid?
Don't know why this is firing 4 Times, but it does not happen when you use selectedItems:
$scope.gridOptions = {
data: 'myData',
showSelectionCheckbox: true,
selectedItems:$scope.output
};
Not really an answer, but maybe it helps you.
Forked Plunker
Update
Found out some more:
The event ngGridEventData is fired 2 times:
On Initialization AND after selectItem by a watcher.
Also afterSelectionChange is fired 2 time. The rowItem from first call is a clone (from cache?) the second one is noClone.
This sums up to 4!
So by taking init out of ngGridEventdata and replacing it with a timeout as well as only pushing rowitems when they are a clone (why?) resolves this issue.
$scope.gridOptions = {
data: 'myData',
showSelectionCheckbox: true,
afterSelectionChange: function(rowItem, event) {
if (rowItem.isClone) {
$scope.output.push({
name: rowItem.entity.name,
selected: rowItem.selected
});
$scope.num++;
}
}
};
setTimeout(function() {
angular.forEach($scope.myData, function(data, index) {
if (data.name == 'Enos') {
$scope.gridOptions.selectItem(index, true);
}
});
})
I know this is still not an answer and smells like a bug to me, but here is another forked Plunker anyhow.
Of course you now have to find a way how to splice items out of the array when they are unselected. Good Luck!
I figured it out - or sort of.
Using beforeSelectionChange instead of afterSelectionChange I get the expected behavior.
The documentation is lagging some information.

ng-grid cellTemplate and grouping: can I change the template for the 'grouping' row

I have been checking out ng-grid, and changed the columnDefs to use a different cellTemplate for certain columns. This template doesn't show the actual value but looks it up in another object, in order to make it more readable for an end-user (basically a foreign key id that is translated to something a human can make sense of).
When I enable showGroupPanel: true in the gridOptions, you can drag one column to the 'grouping' bar, and the results will be grouped.
When I do this with one of the columns for which I edited the template, it doesn't use my cellTemplate, and shows the id again.
Is there a way to make sure that, also in the header of a group, I can use my own template, instead of ng-grid simply showing the values?
[UPDATE]
Some code (simplified to 1 column to only show the essence) might make it easier to understand (any code errors are typos, this is no copy-paste of my real code):
HTML:
<div ng-grid="gridOptions" class="gridStyle"></div>
JS:
$scope.gridOptions = {
data: 'data', enableSorting: true, showFilter: true, multiSelect: false,
showGroupPanel: true,
columnDefs: 'colDefs',
};
$scope.colDefs = [];
$scope.topicid_fkvalues = { 1: "Languages", 2: "Mathematics" };
var colDef1 = { 'field': 'Topic' };
colDef1.cellTemplate = '<label ng-class="\'colt\' + col.index" class="ng-pristine ng-valid colt3" style="width: 90%" >{{topicid_fkvalues[ COL_FIELD ]}} ({{COL_FIELD}})</label>';
$scope.colDefs.push( colDef1 );
$scope.data = [ { topicid: 1 }, { topicid: 2 }, { topicid: 1 } ]
So when I drag this column into the grouping bar, I would like to see the description also in the goup header, instead of only the id...
[UPDATE 2] SOLUTION
I think I found a good solution for this.
It turns out I didn't need a cellTemplate, just a filter.
So, if you define a new filter on your module, something like this:
app.filter(
'translateForeignKey',
function() {
return function( fk, fkValues ) {
return fkValues[ fk ];
}
;
}
);
You could replace this:
var colDef1 = { 'field': 'Topic' };
colDef1.cellTemplate = '<label ng-class="\'colt\' + col.index" class="ng-pristine ng-valid colt3" style="width: 90%" >{{topicid_fkvalues[ COL_FIELD ]}} ({{COL_FIELD}})</label>';
by the following
var colDef1 = { 'field': 'Topic' };
colDef1.cellFilter = 'translateForeignKey: topicid_fkvalues';
which will be adding the filter with as the second argument the $scope.topicid_fkvalues parameter (and this filter will also be used in 'aggregate' view). So if you have different columns with different translations, you could still use the same filter but use multiple scope variables for the translation.
Hope this makes sense, and helps some people out who might have the same question...
SOLUTION
I think I found a good solution for this.
It turns out I didn't need a cellTemplate, just a filter.
So, if you define a new filter on your module, something like this:
app.filter(
'translateForeignKey',
function() {
return function( fk, fkValues ) {
return fkValues[ fk ];
}
;
}
);
You could replace this:
var colDef1 = { 'field': 'Topic' };
colDef1.cellTemplate = '<label ng-class="\'colt\' + col.index" class="ng-pristine ng-valid colt3" style="width: 90%" >{{topicid_fkvalues[ COL_FIELD ]}} ({{COL_FIELD}})</label>';
by the following
var colDef1 = { 'field': 'Topic' };
colDef1.cellFilter = 'translateForeignKey: topicid_fkvalues';
which will be adding the filter with as the second argument the $scope.topicid_fkvalues parameter (and this filter will also be used in 'aggregate' view). So if you have different columns with different translations, you could still use the same filter but use multiple scope variables for the translation.

ExtJs combo loses selected value on store page load

I have an ExtJS 4.1 combo box with a JsonStore and queryMode: 'remote', with paging and filtering, as such:
...
queryMode: 'remote',
allowBlank: true,
forceSelection: true,
autoSelect: false,
pageSize: 25,
typeAhead: true,
minChars: 2,
...
When I load my form with a saved value in this combo box, I load the store passing the saved value as a query (filtering) parameter, to make sure that the selected value is definitely within the returned records, and then I set that value as the combo selected value as such:
mycombo.getStore().load({
params: {
query: displayField
},
scope: {
field: combo,
valueField: valueField,
displayField: displayField
},
callback: function(records, operation, success) {
this.field.setValue(this.valueField);
}
});
So far, so good, the above works fine. The problem is, that if the user then clicks on the dropdown arrow to select another value for the combo, the 1st page of the store is loaded, erasing all previously selected values, and even if nothing is selected, the previously selected value is lost.
This problem is generic, and is quite similar to this question:
ExtJS paged combo with remote JSON store. Display selected value with paging
and can be summarized as such:
In an ExtJS combo box with a remote store and paging, selected values are lost when the loaded page changes.
I tried setting clearOnPageLoad: false for the store, but then each time a new page is loaded, the records are appended to the end of the list. I would have expected this parameter to cache the loaded pages and still show me the correct page while moving back and forth.
So, any ideas on how to keep the selected value while moving between pages? I suppose I could create a record with the selected value manually and append it to the store on each page load until a new value is selected, but this sounds like too much effort for something so basic.
We ended up contacting Sencha support since we have a paid license. This is the answer we got back:
Ext.override(Ext.form.field.ComboBox, {
onLoad: function() {
var me = this,
value = me.value;
if (me.ignoreSelection > 0) {
--me.ignoreSelection;
}
if (me.rawQuery) {
me.rawQuery = false;
me.syncSelection();
if (me.picker && !me.picker.getSelectionModel().hasSelection()) {
me.doAutoSelect();
}
}
else {
if (me.value || me.value === 0) {
if (me.pageSize === 0) { // added for paging; do not execute on page change
me.setValue(me.value);
}
} else {
if (me.store.getCount()) {
me.doAutoSelect();
} else {
me.setValue(me.value);
}
}
}
}
});
Had the same problem, and 'pruneRemoved: false' didn't work (it seems to be used only in grids). This is the solution:
Ext.override(Ext.form.field.ComboBox,{
// lastSelection is searched for records
// (together with store's records which are searched in the parent call)
findRecord: function(field, value) {
var foundRec = null;
Ext.each(this.lastSelection, function(rec) {
if (rec.get(field) === value) {
foundRec = rec;
return false; // stop 'each' loop
}
});
if (foundRec) {
return foundRec;
} else {
return this.callParent(arguments);
}
}
});
Hope it doesn't have negative side effects. I've tested it a bit and it seems OK.
I am experiencing this issue in extjs 6.0.1.
I discovered a work around that might be helpful for others.
I used override for onLoad to add the selected record from the combo to the store prior to calling the base onLoad.
This works because if the selected record is in the page being viewed, the combo is smart enough to not clear the selection. In other words, the reason the selection is being cleared as you page is because the selected record is not in the page you are viewing.
onLoad: function (store, records, success)
{
var selection = this.getSelection();
if (selection)
{
records.unshift(selection);
store.insert(0, records);
}
this.callParent(arguments);
}

How do I set a Ext Grid Filter Default?

I have a working sort-able grid using the ext 3.4 grid filter plugin. I would like to default the active column to filter true values. User who needs the inactive records could remove the filter. How do I specify a default filter column and value?
Thanks in advance!
colModel: new Ext.grid.ColumnModel({
defaults: {
sortable: true
// How do I specify a default filter value
//
// Only show active records unless the user changes the filter...
},
columns: [{
dataIndex:'f_uid',
id:'f_uid',
header:'ID',
hidden:true
}, {
dataIndex:'f_name',
id:'f_name',
header:'Name',
}, {
xtype:'booleancolumn',
dataIndex:'f_active',
id:'f_active',
header:'Active',
filterable:true,
trueText:'Active',
falseText:'Inactive'
}]
I realise this is an old question but it took me a while to find a solution, therefore I thought I would share.
1) The filter can be set using the value property in the filter.
filter: {
type: 'LIST',
value: ['VALUE TO FILTER']
}
2) In order to initially filter the data use the filterBy() method in the store. This could be defined in the onRender event handler.
this.getStore().load({
scope:this,
callback: function() {
// filter the store
this.getStore().filterBy(function(record, id) {
// true will display the record, false will not
return record.data.DATA_TO_FILTER == 'VALUE TO FILTER ';
});
}
});
The answer was in the Filter.js source code. The filter object within the column definition can be used to configure the default behavior.
}, {
xtype:'booleancolumn',
dataIndex:'f_active',
id:'f_active',
header:'Active',
trueText:'Active',
falseText:'Inactive',
filterable:true,
filter: {
value:1, // 0 is false, 1 is true
active:true // turn on the filter
}
}
I have encountered the same problem and I found that #John's answer is right, I can make it work with the sample http://dev.sencha.com/deploy/ext-4.0.0/examples/grid-filtering/grid-filter-local.html, for the grid-filter-local.js, just add the code like:
grid.getStore().load({
scope:this,
callback: function() {
// filter the store
grid.getStore().filterBy(function(record, id) {
// true will display the record, false will not
return record.data.size === 'small';
});
}
});
before the original code store.load(), and wipe off the store.load().
Then it will only show the record with size equals 'small' at the first load of the web page. Cheers!
I've made a universal helper class that allows you to set any default values in column definition.
https://gist.github.com/Eccenux/ea7332159d5c54823ad7
This should work with both remote and static stores. Note that this also works with filterbar plugin.
So your column item is something like:
{
header: 'Filename',
dataIndex: 'fileName',
filter: {
type: 'string',
// filename that starts with current year
value: Ext.Date.format(new Date(), 'Y'),
active:true
}
},
And then in your window component you just add something like:
initComponent: function() {
this.callParent();
// apply default filters from grid to store
var grid = this.down('grid');
var defaultFilters = Ext.create('Ext.ux.grid.DefaultFilters');
defaultFilters.apply(grid);
},

Resources