Persist selected rows the correct way - angularjs

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.

Related

How to prevent Kendo MultiSelect from losing values after editing in a grid template?

I have a grid that displays a comma-separated list of values, and it has an array that gets used in a template editor for the grid. (On the server, I transform the comma-separated list to an array for the Kendo multiselect AngularJS directive). I have almost everything working: display, edit, and adding values in the multiselect.
There's just one weird thing happening: after I add a value in the multiselect, click Save in the editor, and reopen the editor, the multiselect then only displays one of the most-recently entered values. I know that the values are there and going through the pipeline, because the values make it into the database. I can refresh the page, open the editor, and all the values display in the multiselect correctly, including the one I just added.
It's as if kendo "forgets" most of the values when I reopen the editor. How can this be prevented? Does the MultiSelect need to be rebound to the values? If so, how?
I have tried adding this onChange event, but it had no effect. I've added valuePrimitive to no effect. I tried specifying k-rebind, but it caused an error.
Here's the directive being used in the text/x-kendo-template:
<select kendo-multi-select
id="zipCode"
k-placeholder="'Enter zip codes...'"
style="width: 225px"
k-on-change="dataItem.dirty=true"
k-auto-bind="false"
k-min-length="3"
k-enforce-min-length="true"
k-data-source="options.zipCodeDataSource"
k-data-text-field="'text'"
k-filter="'startsWith'"
k-filtering="options.zipCodeFiltering"
k-no-data-template="'...'"
k-ng-model="dataItem.zipArray"
k-highlight-first="true" />
And this is the DataSource:
options.zipCodeDataSource = new kendo.data.DataSource({
severFiltering: true,
transport: {
read: {
url: serviceUrl + "ZipCode/Get",
type: "GET",
dataType: "json",
contentType: jsonType,
data: function (e) {
// get your widget.
let widget = $('#zipCode').data('kendoMultiSelect');
// get the text input
let filter = widget.input.val();
// what you return here will be in the query string
return {
filter: filter
};
}
},
},
schema: {
data: "data",
total: "Count",
model:
{
id: "text",
fields: {
text: { editable: false, defaultValue: 0 },
}
},
parse: function (response) {
return response;
}
},
error: function (e) {
}
});
If I display {{dataItem.zipArray}} in a <pre> all of the expected values are there.
I wonder if something needs to be added to the edit event handler in the kendo grid definition, but I'm not sure what it would be. I've had to do binding like that for the dropdownlist directive.
edit: function (e) {
if (e.model.marketId === 0) {
e.container.kendoWindow("title", "Add");
} else {
e.container.kendoWindow("title", "Edit");
}
// re-bind multi-select here??
// These two lines actually cause the multiselect to lose pre-existing items in dataItem.zipArray
// var multiselect = kendo.widgetInstance(angular.element('#zipCode'));
// multiselect.trigger('change');
}
...
Update:
This dojo demonstrates the issue.
Run the dojo
Edit the first record in the Contracts grid
Add a zip code such as 22250
Click Save
Then click Edit on the first row again
Only zip code 22250 is displayed in the editor
Also, I notice that if I change k-min-length="3" to k-min-length="1", then the issue goes away. But in the scenario I'm working on, it needs to be 3.
This seems to be an issue with kendo itself. Back then this issue was reported here.
Ok based on the reply from telerik, this issue has been fixed in 2017 R3 SP1 release (2017.3.1018). You can verify it by using this updated dojo example:
http://dojo.telerik.com/IREVAXar/3

Cannot detect row selection in ui.grid

I have an angular ui grid working with selection, I want to populate a form with the contents of the selected record. I see no way to either reference the selected record or detect that row selection has occurred. Any ideas?
With ui-grid the api is a little different than the ng-grid example Aidan posted.
Normally you'd listen to the rowSelectionChanged event, refer: http://ui-grid.info/docs/#/api/ui.grid.selection.api:PublicApi
This would look something like the following:
$scope.selectionChanged = function( rowChanged ) {
if( rowChanged.isSelected ){
$scope.targetRow = rowChanged;
}
};
gridOptions = {
// other stuff
onRegisterApi: function(gridApi) {
$scope.gridApi = gridApi;
$scope.gridApi.selection.on.rowSelectionChanged($scope, $scope.selectionChanged);
}
};
You should then be able to use $scope.targetRow as the target for your form. You'll probably need to turn off multiselect (check the selection gridOptions, or the selection tutorial), and deal with what happens when no row is selected.
In my application I tend to do the form as a popup (a modal or a separate page). When I do that it's much simpler, I just do a click event on the row itself, or (more commonly) a button on the row. Refer perhaps to http://ui-grid.info/docs/#/tutorial/305_appScope for an example. You can pass the row that was clicked upon into that click handler:
cellTemplate:'<button ng-click="grid.appScope.openModal( row.entity )">Edit</button>' }
You can then pass the entity (which would be an ngResource) into that modal, and once it edits it then it can just call save on the resource when it's done.
I did something similar to this in my (ng-grid based) tutorial series: https://technpol.wordpress.com/2013/11/09/angularjs-and-rails-4-crud-application-using-ng-boilerplate-and-twitter-bootstrap-3-tutorial-index/
It is customary to post what you have tried, please consider this in future. Here is a short sample that utilises the afterSelectionChange event for the ng-grid:
A grid with two divs to hold the selected data:
<div class='gridStyle' ng-grid='testGridOptions'></div>
<div>{{current.name}}</div>
<div>{{current.age}}</div>
some test data:
$scope.testData = [{name:'John', age:25},
{name:'Mary', age:30},
{name:'Fred', age:10},
{name:'Joan', age:20}];
configuration for the grid:
$scope.testGridOptions = {
data: 'testData',
multiSelect: false,
afterSelectionChange: function(data){
$scope.current = data.entity;
}
};
Hope this gets you started.
Aidan

How to have a checkbox in an ng-grid grouping

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.

show dynamic data using checkbox in extjs

I have a combo box in which the user selects a value, that value is passed to the checkbox data store and it is populated dynamically from database (oracle). I tried the code below. It seems that the selected parameter is being passed to checkbox and I can see the data being populated on the console. I just can't render the checkbox on form. The error I get is: typeError: this.items[0] is undefined.
testArray = new Array();
var testStore = new Ext.data.JsonStore({
proxy:new Ext.data.HttpProxy({
method:'GET',
prettyUrls:false,
url:'kiu.htm',
listeners:{
'loadexception':{
fn:test.form.data.loadException
}
}
}),
fields:["id", "display"],
reader:new Ext.data.JsonReader({
id:'id',
root:'results',
totalProperty:'totalCount',
fields:new Ext.data.Record.create([
{name:'id',type:'int'},
{name:'display',type:'string'}
])
}),
listeners:{
load: function(t, records, options, success) {
for(var i=0; i<records.length; i++) {
testArray.push({name:records[i].data.id, boxLabel: records[i].data.display});
alert(testArray[i].id);
}
}
}
});
{
xtype:'combo',
id:'comboid3',
store:combostore,
displayField:'display',
valueField:'id',
tabIndex:1,
loadingText:'Loading combo...',
listeners :{
select:function(event){
testStore.baseParams = {
"comboid":Ext.getCmp('comboid3').getValue()
};
testStore.load();
}
}
},
{
xtype:'checkboxgroup',
fieldLabel:'Check',
items:testArray
}
Help will be appreciated!
Specifying the Ext JS version is always going to be helpful. It appears this must be 2.x or 3.x and not the current version.
The issue is timing, load calls are asynchronous so by attempting to utilize testArray in this fashion you are likely referencing an empty array by the time it parses the items property of your checkboxgroup. You have a couple options around this... one is to grab a reference to the checkbox group and add the items into it, the other is to not put the checkbox group in the form at all until the call returns and then add it with the populated items array. In either case it is likely that you will need to look up a component reference to either the FormPanel or the CheckboxGroup from within the load handler function and call the 'add' method to add child items.

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

Resources