Updating react-table values after Dragging and Dropping a row in React Redux - react-redux

I´ve accomplished the react drag and drop functionality into my project so i can reorder a row in a react table´s list. The problem is i have a column named 'Sequence', witch shows me the order of the elements, that i can´t update its values.
Example:
before (the rows are draggable):
Sequence | Name
1 Jack
2 Angel
after ( i need to update the values of Sequence wherea i change their position after dropping a specific draggable row, in this case i dragged Jack at the first position and dropped it at the second position) :
Sequence | Name
1 Angel
2 Jack
React/Redux it´s allowing me to change the index order of this array of elements, without getting the 'A state mutation was detected between dispatches' error message, but is not allowing me to update the Sequence values with a new order values.
This is what i have tried so far:
// within the parent class component
// item is an array of objects from child
UpdateSequence(startIndex, endIndex, item) {
// the state.Data is already an array of object
const result = this.state.Data;
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
// this is working without the mutation state error
this.setState({ Data: result })
let positionDiff = 0;
let direction = null;
let newIndex = 0;
positionDiff = endIndex - startIndex;
if (startIndex > endIndex) {
direction = "up";
}
else if (startIndex < endIndex) {
direction = "down";
}
if (positionDiff !== 0) {
for (var x = 0; x <= Math.abs(positionDiff); x++) {
if (x === 0) {
newIndex = startIndex + positionDiff - x;
this.setState(prevState => ({
Data: {
...prevState.Data,
[prevState.Data[newIndex].Sequence]: Data[newIndex].Sequence + positionDiff
},
}));
}
else {
if (direction === "down") {
newIndex = startIndex + positionDiff - x;
this.setState(prevState => ({
Data: {
...prevState.Data,
[prevState.Data[newIndex].Sequence]: Data[newIndex].Sequence - 1
},
}));
}
else if (direction === "up") {
Data= startIndex + positionDiff + x;
this.setState(prevState => ({
Data: {
...prevState.Data,
[prevState.Data[newIndex].Sequence]: Data[newIndex].Sequence + 1
},
}));
}
}
}
// so when i call save action i am stepping into the 'A state mutation was detected between dispatches' error message.
this.props.actions.saveSequence(this.state.Data)
.then(() => {
this.props.actions.loadData();
})
.catch(error => {
toastr['error'](error, 'error....');
})
}
Calling the action 'saveSequence' whenever i try to update the element of the array, 'Sequence', i am getting the 'A state mutation was detected between dispatches' error message.
Any help will be greatfull! Thank you!
note: The logic applied to reorder the Sequence is ok.

While I don't know redux particularly well, I am noticing that you are directly modifying state, which seems like a likely culprit.
const result = this.state.Data;
const [removed] = result.splice(startIndex, 1);
splice is a destructive method that modifies its input, and its input is a reference to something in this.state.
To demonstrate:
> state = {Data: [1,2,3]}
{ Data: [ 1, 2, 3 ] }
> result = state.Data.splice(0,1)
[ 1 ]
> state
{ Data: [ 2, 3 ] }
Notice that state has been modified. This might be what Redux is detecting, and a general React no-no.
To avoid modifying state, the easy way out is to clone the data you are looking to modify
const result = this.state.Data.slice()
Note that this does a shallow copy, so if Data has non-primitive values, you have to watch out for doing destructive edits on those values too. (Look up deep vs shallow copy if you want to find out more.) However, since you are only reordering things, I believe you're safe.

Well, i figured it out changing this part of code:
//code....
const result = item;
const [removed] = result.splice(startIndex, 1);
// i created a new empty copy of the const 'removed', called 'copy' and update the Sequence property of the array like this below. (this code with the sequence number is just a sample of what i came up to fix it )
let copy;
copy = {
...removed,
Sequence: 1000,
};
result.splice(endIndex, 0, copy);
After i didn´t setState for it, so i commented this line:
// this.setState({ Data: result })
//...code
and the end of it was putting the result to the save action as a parameter , and not the state.
this.props.actions.saveSequence(result)
Works and now i have i fully drag and drop functionality saving the new order sequence into the database with no more 'A state mutation was detected between dispatches' error message!

Related

Resetting/Restart Rxjs Stream on form submit

I have an input field which when submitted makes a http call and then plots a graph. When I click on any node of graph, same http call is made and results are appended to the previous results and graph is updated. It is working fine till here. I am using scan operator to update my resultset. Now, what I want is to reset the resultset (ie - return new original response) whenever I am submitting the input form and append to resultset when graph node is clicked. Any ideas on how this can be achieved? Mainly how can I reset this stream on form submit? Or how can I show new data on form submit and updated data on node click
Here linkingDetailsByAccount$ makes the http call and gets the data from the server.
this.linkingDetailsByAccountSubject.next(account);
Same code is called on node click as well as on form submit which then activates my stream.
graph$ = this.linkingDetailsByAccount$.pipe(
pluck('graph'),
scan((linkedDetails, adjacency) => {
const { nodes: linkedNodes = [], edges: linkedEdges = [] } = linkedDetails;
const { nodes: newNodes = [], edges: newEdges = [] } = adjacency;
const updatedNodes = differenceBy(newNodes, linkedNodes, 'id');
const updatedEdges = differenceWith(
newEdges,
linkedEdges,
(newEdge: VisEdge, existingEdge: VisEdge) => newEdge.from === existingEdge.to
);
const allNodes = [...linkedNodes, ...updatedNodes];
const allEdges = [...linkedEdges, ...updatedEdges];
return {
nodes: allNodes,
edges: allEdges
};
}, {} as NodesEdges)
);
Appreciate any inputs on this.
Thanks,
Vatsal
Edit: Updated answer when I received more details from OP.
How I would do it is turn it into a mini Redux like state manager.
So the scan operator should take in functions or event objects.
First you want to store the first initial state from the initial HTTP call you make. You will use this object to reset your state on form submission.
Then create a graphEvents subject.
interface UpdateGraphEvent {
type: 'Update';
account: any;
}
interface ResetGraphEvent {
type: 'Reset';
account: any;
}
type GraphEvent = UpdateGraphEvent | ResetGraphEvent;
this.graphEvents$ = new Subject<GraphEvent>();
Then you can use your new graphEvents$ subject to replace uses of linkingDetailsByAccountSubject.
// When you want to update with new data.
this.graphEvent$.next({type: 'Update', account: account});
// when you want to reset with initial data.
this.graphEvent$.next({type: 'Reset', account: this.initialAccount});
Then use it in your stream.
graph$ = this.graphEvent$.pipe(
pluck('graph'),
scan((linkedDetails, event: GraphEvent) => {
if (event.type === 'Reset') {
return {
nodes: event.account.nodes,
edges: event.account.edges,
}
}
const { nodes: linkedNodes = [], edges: linkedEdges = [] } = linkedDetails;
const { nodes: newNodes = [], edges: newEdges = [] } = event.account;
const updatedNodes = differenceBy(newNodes, linkedNodes, 'id');
const updatedEdges = differenceWith(
newEdges,
linkedEdges,
(newEdge: VisEdge, existingEdge: VisEdge) => newEdge.from === existingEdge.to
);
const allNodes = [...linkedNodes, ...updatedNodes];
const allEdges = [...linkedEdges, ...updatedEdges];
return {
nodes: allNodes,
edges: allEdges
};
}, {} as NodesEdges)
);
The graphEvent$ will be a Subject that emits those events (GraphEvent).

dc.js charting nested data arrays

Problem description
I have a large data array with a structure similar to the following and seek to create a chart that will display the timerecords' changesets by hour that the changeset was created.
[ // array of records
{
id: 1,
name: 'some record',
in_datetime: '2019-10-24T08:15:00.000000',
out_datetime: '2019-10-24T10:15:00.000000',
hours: 2,
tasks: ["some task name", "another task"],
changesets: [ //array of changesets within a record
{
id: 1001,
created_at: '2019-10-24T09:37:00.000000'
},
...
]
},
...
]
No matter how I have tried to create the dimension/write reduction functions I can't get the correct values out of the data table.
const changeReduceAdd = (p, v) => {
v.changesets.forEach(change => {
let cHour = timeBand[change.created_hour]
if (showByChanges) {
p[cHour] = (p[cHour] || 0) + (change.num_changes || 0)
} else {
p[cHour] = (p[cHour] || 0) + 1 //this is 1 changeset
}
})
return p
}
const changeReduceRemove = (p, v) => {
v.changesets.forEach(change => {
let cHour = timeBand[change.created_hour]
if (showByChanges) {
p[cHour] = (p[cHour] || 0) - (change.num_changes || 0)
} else {
p[cHour] = (p[cHour] || 0) - 1 //this is 1 changeset
}
})
return p
}
const changeReduceInit = () => {
return {}
}
//next create the array dimension of changesets by hour
//goal: show changesets or num_changes grouped by their created_hour
let changeDim = ndx.dimension(r => r.changesets.map(c => timeBand[c.created_hour]), true)
let changeGroup = changeDim.group().reduce(changeReduceAdd, changeReduceRemove, changeReduceInit)
let changeChart = dc.barChart('#changeset-hour-chart')
.dimension(changeDim)
.keyAccessor(d => d.key)
.valueAccessor(d => d.value[d.key])
.group(changeGroup)
jsfiddle and debugging notes
The main problem I'm having is I want the changesets/created_hour chart, but in every dimension I have tried, where the keys appear correct, the values are significantly higher than the expected.
The values in the "8AM" category give value 5, when there are really only 3 changesets which I marked created_hour: 8:
There are a lot of solutions to the "tag dimension" problem, and you happen to have chosen two of the best.
Either
the custom reduction, or
the array/tag flag parameter to the dimension constructor
would do the trick.
Combining the two techniques is what got you into trouble. I didn't try to figure what exactly was going on, but you were somehow summing the counts of the hours.
Simple solution: use the built-in tag dimension feature
Use the tag/dimension flag and default reduceCount:
let changeDim = ndx.dimension(r => r.changesets.map(c => timeBand[c.created_hour]), true)
let changeGroup = changeDim.group(); // no reduce, defaults to reduceCount
let changeChart = dc.barChart('#changeset-hour-chart')
.dimension(changeDim)
.keyAccessor(d => d.key)
.valueAccessor(d => d.value) // no [d.key], value is count
.group(changeGroup)
fork of your fiddle
Manual, pre-1.4 groupAll version
You also have a groupAll solution in your code. This solution was necessary before array/tag dimensions were introduced in crossfilter 1.4.
Out of curiosity, I tried enabling it, and it also works once you transform from the groupAll result into group results:
function groupall_map_to_group(groupall) {
return {
all: () => Object.entries(groupall.value())
.map(([key,value])=>({key,value}))
}
}
let changeGroup = ndx.groupAll().reduce(changeReduceAdd, changeReduceRemove, changeReduceInit)
let changeChart = dc.barChart('#changeset-hour-chart')
.dimension({}) // filtering not implemented
.keyAccessor(d => d.key)
.valueAccessor(d => d.value) // [d.key]
.group(groupall_map_to_group(changeGroup))
.x(dc.d3.scaleBand().domain(timeBand))
.xUnits(dc.units.ordinal)
.elasticY(true)
.render()
crossfilter 1.3 version

How to make this RxJs code more elegant? The code recording mouse hover time on a specific area

I want to record mouse hover time on a specific area such as a 'div' container box area , by using RxJs.
const element = document.querySelector('#some-div');
let totalHoverTime = 0;
const INTERVAL = 100;
const mousePos = {
x: -1,
y: -1
};
const accumulate = () => {
const x = mousePos.x;
const y = mousePos.y;
const divx1 = element.offsetLeft;
const divy1 = element.offsetTop;
const divx2 = element.offsetLeft + element.offsetWidth;
const divy2 = element.offsetTop + element.offsetHeight;
// out of area
if (x < divx1 || x > divx2 || y < divy1 || y > divy2) {
console.log('out')
} else {
// in area
console.log('in')
totalHoverTime += INTERVAL;
}
};
const accumulateTimer = rx.interval(INTERVAL);
accumulateTimer.subscribe(() => {
accumulate();
});
rx
.fromEvent(element, 'mousemove')
.pipe(rxOp.debounce(() => rx.timer(INTERVAL)))
.subscribe((e: MouseEvent) => {
mousePos.x = e.clientX;
mousePos.y = e.clientY;
});
I'm not very familiar with rxjs, I think this code may can be more elegant to implement.
Optimized code
Thank you very much for your answers. #hugo #der_berni
const element = document.body;
const INTERVAL = 2000;
const withinBounds = ({ x, y }: { x: number; y: number }) => {
const divx1 = element.offsetLeft;
const divy1 = element.offsetTop;
const divx2 = element.offsetLeft + element.offsetWidth;
const divy2 = element.offsetTop + element.offsetHeight;
const outOfBounds = x < divx1 || x > divx2 || y < divy1 || y > divy2;
if (outOfBounds) {
// out of area
console.log('out');
} else {
// in area
console.log('in');
}
return !outOfBounds;
};
const mousePositions = rx
.fromEvent(document, 'mousemove')
.pipe(rxOp.throttleTime(200))
.pipe(rxOp.map((e: MouseEvent) => ({ x: e.pageX, y: e.pageY })));
const mousePositionIsValid = mousePositions
.pipe(rxOp.map(withinBounds))
.pipe(rxOp.distinctUntilChanged());
const hoverTimer = mousePositionIsValid.pipe(rxOp.switchMap(valid => (valid ? accumulateTimer : rx.empty())));
const totalHoverTime = hoverTimer.pipe(rxOp.scan((x, y) => x + INTERVAL, -500)); // The first time mouse moves in, this will be triggered once, so it is set to -500, and the first time it comes in is 0ms.
totalHoverTime.subscribe(hoverTime => {
console.log('totalHoverTime is:', hoverTime);
});
Finally, I found that I still need to use mousemove event combined timer to implement this function. When the mouse is already hovering above the div on page load, the mouseenter event will never triggerd in my page seemly. Maybe only in jsfiddle can be no problem.
I' also only started using RxJS recently, so there might be a better way to solve your problem.
However, a huge improvement over your approach would already be to chain the observables and use the switchMap operator. One thing to keep in mind when working with rxjs is, that you want to avoid manual subscriptions, because you will have to keep track of them and unsubscribe yourself to prevent leaks. When using operators like switchMap, these keep track of the subscriptions to inner observables, and also automatically unsubscribe.
Following code snippet should solve your problem:
Rx.Observable.fromEvent(element, 'mouseenter') // returns Observable<Event>
.map(() => Date.now()) // transform to Observable<number>
.switchMap((startTime) => { // switches to new inner observable
return Rx.Observable.fromEvent(button, 'mouseleave')
// When the observable from mouseleave emmits, calculate the hover time
.map(() => Date.now() - startTime);
})
.subscribe((hoverTime) => {console.log(hoverTime)});
If you want to try it out, see this jsFiddle: https://jsfiddle.net/derberni/hLgw1yvj/3/
EDIT:
Even if your div is very large, and the mouse might never leave it and trigger the mouseleave event, this can be solved with rxjs. You just have to change when the observable emits, and for how long you let it emit before you complete it. The WHEN can be adapted, so that it emits in a set interval, and the UNTIL can be set with the rxjs function takeUntil. takeUntil receives an observable as an argument, and takes values from the source observable, until the 'argument' observable emits.
Check out this code and fiddle, which updates the hover time in 1s steps and when the mouseleave event triggers: https://jsfiddle.net/derberni/3cky0g4e/
let div = document.querySelector('.hover-target');
let text = document.querySelector('.hover-time');
Rx.Observable.fromEvent(div, 'mouseenter')
.map(() => Date.now())
.switchMap((startTime) => {
return Rx.Observable.merge(
Rx.Observable.interval(1000),
Rx.Observable.fromEvent(div, 'mouseleave')
)
.takeUntil(Rx.Observable.fromEvent(div, 'mouseleave'))
.map(() => Date.now() - startTime);
})
//.takeUntil(Rx.Observable.fromEvent(div, 'mouseleave'))
.subscribe((hoverTime) => {
text.innerHTML = "Hover time: " + hoverTime + "ms"
});
At least in the fiddle this works also when the mouse is already hovering above the div on page load, because then the mouseenter event is also triggered.
Point-free
The simplest thing: replace (x => f(x)) with simply f. It's equivalent and will read better in most cases. This:
accumulateTimer.subscribe(() => {
accumulate();
});
Becomes:
accumulateTimer.subscribe(accumulate);
Fat functions:
The accumulate function could be broken down into:
const accumulate = () => {
const x = mousePos.x;
const y = mousePos.y;
if (withinBounds(x, y)) {
totalHoverTime += INTERVAL;
}
};
const withinBounds = ({x, y}) => {
const divx1 = element.offsetLeft;
const divy1 = element.offsetTop;
const divx2 = element.offsetLeft + element.offsetWidth;
const divy2 = element.offsetTop + element.offsetHeight;
const outOfBounds = x < divx1 || x > divx2 || y < divy1 || y > divy2;
if (outOfBounds) {
// out of area
console.log('out')
} else {
// in area
console.log('in')
}
return !outOfBounds;
};
See how we separated withinBounds which is pretty big but performs a simple definite task, purely functionally (no side-effect, one input gives the same output) -- ignoring the debug calls that is. Now we don't have to think so hard about it and we can focus on accumulate.
Avoid side-effects & compose
The most glaring issue is the whole loop relying on a side effect on mousePos:
const mousePositions = rx
.fromEvent(element, 'mousemove')
.pipe(rxOp.debounce(() => rx.timer(INTERVAL)))
//.subscribe((e: MouseEvent) => {
// mousePos.x = e.clientX;
// mousePos.y = e.clientY;
//});
.map((e: MouseEvent) => ({ x: e.clientX, y: e.clientY )));
Don't subscribe and save the value, it breaks the idea of flow behind rxjs. Use the return value, Luke. More specifically, pipe further to refine it until you reach the desired data. Above, we have a stream that emits the mouse positions alone.
// Will emit true when the mouse enters and false when it leaves:
const mousePositionIsValid = mousePositions
.map(withinBounds)
.distinctUntilChanged();
// Fires every INTERVAL, only when mouse is within bounds:
const hoverTimer = mousePositionIsValid
.switchMap(valid => valid ? accumulateTimer : rx.empty())
(edited with switchMap as suggested by #der_berni)
You wrote a function named "accumulate". Whenever you say "accumulate", reduce (and the likes) should come to mind. Reduce emits a single aggregate value when the stream completes. Here we use scan to get a new updated value each time the underlying stream emits:
// For each element produced by hoverTimer, add INTERVAL
const totalHoverTime = hoverTimer.scan((x, y) => x + INTERVAL, 0);
Note that it doesn't add to global each time, but every value it emits is the previous one + INTERVAL. So you can subscribe to that to get your total hover time.

Angularjs - optimizing the watcher of ngrepeat to watch only certain properties of collection

In my angularJS application I have a collection (array) of rather large objects. I need to bind to this collection in various places (e.g. to display the property: name of the contained objects) - binding is essential, as these names might change.
As the normal ngRepeat would observe the whole collection by strict equality comparison I am concerned about application speed (we are talking about objects with thousends of properties or more) - I actually just need to observe general changes in the collection (like length, changes of the single references in case two elements are flipped and some specific properties like the mentioned .name property)
I am thinking about using the following approach (basically creating a custom copy of the collection and manually bind to the original collection.
My question:
Is the described approach better than watching the original collection (by equality - as it is my understanding the ngRepeater does) or is there some better approach (e.g. defining some kind of compare callback in a watch statement to check only for changes in certain properties,...)
<script>
function QuickTestController($scope) {
// simulate data from a service
var serviceCollection = [], counter = 0,
generateElement = function() {
var element = { name:'name' + ++counter };
//var element = { name:'name' };
for (var j = 0 ; j < 5 ; j++) element['property' + j] = j;
return element;
};
for (var i = 0 ; i < 5 ; i++) {
serviceCollection.push( generateElement() );
}
// in the view controller we could either bind to the service collection directly (which should internally use a watchCollection and watch every single element for equality)
$scope.viewCollection = serviceCollection;
// watching equality of collection
/*
$scope.$watch('_viewCollectionObserve', function(newValue, oldValue) {
console.log('watch: ', newValue, oldValue);
}, true);
*/
// or we could create our own watchCollection / watch structure and watch only those properties we are interested in
$scope._viewCollectionObserve = serviceCollection;
var viewCollectionManual = [],
rebuildViewCollection = function() {
viewCollectionManual = [];
for (var i = 0, length = serviceCollection.length ; i < length ; i++) {
viewCollectionManual.push( {name:serviceCollection[i].name } );
}
console.log('- rebuildViewCollection - ');
$scope.viewCollection2 = viewCollectionManual;
},
watchCollectionProperties = [],
unregisterWatchCollection = function() {},
rebuildWatchCollectionProperties = function() {
watchCollectionProperties = [];
for (var i = 0, length = serviceCollection.length ; i < length ; i++) {
watchCollectionProperties.push('_viewCollectionObserve[' + i + ']'); // watch for ref changes
watchCollectionProperties.push('_viewCollectionObserve[' + i + '].name'); // watch for changes in specific properties
}
unregisterWatchCollection();
var watchString = '[' + watchCollectionProperties.join(',') + ']';
unregisterWatchCollection = $scope.$watchCollection(watchString, function(newValues, oldValues) {
console.log('watchCollection: ', newValues, oldValues);
rebuildViewCollection();
});
};
$scope.$watch('_viewCollectionObserve.length', function(newValue, oldValue) { // watch add / remove elements to / from collection
console.log('watch / length: ', newValue, oldValue);
rebuildWatchCollectionProperties();
});
// rebuildViewCollection();
rebuildWatchCollectionProperties();
// click handler ---
$scope.changName = function() { serviceCollection[0].name += '1'; };
$scope.changeSomeProperty = function() { serviceCollection[0].property0 += 1; };
$scope.removeElement = function() { serviceCollection.splice(0, 1); };
$scope.addElement = function() { serviceCollection.push( generateElement() ); };
$scope.switchElement = function() {
var temp = serviceCollection[0];
serviceCollection[0] = serviceCollection[1];
serviceCollection[1] = temp;
};
// will of course not react to this (this is desired behaviour!)
$scope.removeCollection = function() { serviceCollection = []; };
}
</script>
<div data-ng-controller="QuickTestController">
<ul>
<li data-ng-repeat="element in viewCollection">{{element.name}} {{element}}</li>
</ul>
<hr>
<ul>
<li data-ng-repeat="element in viewCollection2">{{element.name}} {{element}}</li>
</ul>
<button data-ng-click="changName()">changName</button>
<button data-ng-click="changeSomeProperty()">changeSomeProperty</button>
<button data-ng-click="removeElement()">removeElement</button>
<button data-ng-click="addElement()">addElement</button>
<button data-ng-click="switchElement()">switchElement</button>
<hr>
<button data-ng-click="removeCollection()">removeCollection (see comment)</button>
</div>
Any help / opinions would be greatly appreciated - please note that I tried to create a fiddle to demonstrate my approach but failed :-(
(I know that benchmarking might be a possible solution to test my approach, but I´d rather know the opinion of the angularjs pros in here)
thanks,
matthias
I think what you're looking for is bindonce, which is a high performance binding directive that lets you bind a property or expression once in AngularJS, just as what its name suggests.
One thing you can also try is a 'track by' expression. If you have a property that is unique for each object in the collection, you can pass that to your repeat expression.
<div ng-repeat="item in items track by item.id"></div>
I think Angular will then just watch that property on each of your items. So this should improve performance, but I don't know how much.

Do not automatically expand all subgrid rows when clicking grouping column's expand icon

After grouping, is there a way for expand/collapse icon of current row not automatically expand/collapse all of the subgrid's rows? Just leave it alone as it was.
var parmColumnName = 'Model';
$('#test').jqGrid('groupingGroupBy'),
parmColumnName,
{
groupCollapse: true,
groupField: ['name']
}
);
//Original setup after playing around with it. (See X5 under BMW)
//Collapse the grouped Make
//Then Expand the grouped Make (All of the model are expanded by default, I do not want it to change and I want it to look like the original snapshot above)
I find your question very interesting, but the solution for the problem is not easy. In my opinion the source code of two jqGrid methods groupingRender and especially groupingToggle should be changed. The solution which I suggest you can see on the demo. The demo overwrites the original code of groupingRender and groupingToggle methods. More full description of my suggestions you will find below.
First of all I want to describe the problem in my words. You used the words "the subgrid's rows" in the text of your question which bring misunderstanding. What you use is multilevel grouping. The first problem in my opinion is the behavior of groupCollapse: true option. In case of multilevel grouping jqGrid collapse currently only data instead of all grouping headers till the top level. The demo uses 3-level grouping and the option groupCollapse: true. It dysplays
instead of intuitively expected
Another problem which you formulate in your question is the current behavior of expending. The problem is that if the user have collapsed the nodes to that all looks compact, like on the last picture which I posted, end then the user expand some node jqGrid expand all children grouping headers of the node till the data. So if one expand for example only "test1" node then all its children nodes will be expanded instead of expending only the next grouping level.
To fix the first problem (opened sub-grouping headers in spite of groupCollapse: true) I changed one line of groupingRender method from
str += "<tr id=\""+hid+"\" role=\"row\" class= \"ui-widget-content jqgroup ui-row-"+$t.p.direction+" "+clid+"\"><td style=\"padding-left:"+(n.idx * 12) + "px;"+"\" colspan=\""+colspans+"\">"+icon+$.jgrid.template(grp.groupText[n.idx], gv, n.cnt, n.summary)+"</td></tr>";
to
str += "<tr id=\""+hid+"\"" +(grp.groupCollapse && n.idx>0 ? " style=\"display:none;\" " : " ") + "role=\"row\" class= \"ui-widget-content jqgroup ui-row-"+$t.p.direction+" "+clid+"\"><td style=\"padding-left:"+(n.idx * 12) + "px;"+"\" colspan=\""+colspans+"\">"+icon+$.jgrid.template(grp.groupText[n.idx], gv, n.cnt, n.summary)+"</td></tr>";
The main problem which you asked was a little more difficult. Below you can find the fixed version of
$.jgrid.extend({
groupingToggle : function(hid){
this.each(function(){
var $t = this,
grp = $t.p.groupingView,
strpos = hid.split('_'),
uidpos,
//uid = hid.substring(0,strpos+1),
num = parseInt(strpos[strpos.length-2], 10);
strpos.splice(strpos.length-2,2);
var uid = strpos.join("_"),
minus = grp.minusicon,
plus = grp.plusicon,
tar = $("#"+$.jgrid.jqID(hid)),
r = tar.length ? tar[0].nextSibling : null,
tarspan = $("#"+$.jgrid.jqID(hid)+" span."+"tree-wrap-"+$t.p.direction),
getGroupingLevelFromClass = function (className) {
var nums = $.map(className.split(" "), function (item) {
if (item.substring(0, uid.length + 1) === uid + "_") {
return parseInt(item.substring(uid.length + 1), 10);
}
});
return nums.length > 0 ? nums[0] : undefined;
},
itemGroupingLevel,
collapsed = false, tspan;
if( tarspan.hasClass(minus) ) {
if(grp.showSummaryOnHide) {
if(r){
while(r) {
if($(r).hasClass('jqfoot') ) {
var lv = parseInt($(r).attr("jqfootlevel"),10);
if( lv <= num) {
break;
}
}
$(r).hide();
r = r.nextSibling;
}
}
} else {
if(r){
while(r) {
itemGroupingLevel = getGroupingLevelFromClass(r.className);
if (itemGroupingLevel !== undefined && itemGroupingLevel <= num) {
break;
}
$(r).hide();
r = r.nextSibling;
}
}
}
tarspan.removeClass(minus).addClass(plus);
collapsed = true;
} else {
if(r){
var showData = undefined;
while(r) {
itemGroupingLevel = getGroupingLevelFromClass(r.className);
if (showData === undefined) {
showData = itemGroupingLevel === undefined; // if the first row after the opening group is data row then show the data rows
}
if (itemGroupingLevel !== undefined) {
if (itemGroupingLevel <= num) {
break;// next item of the same lever are found
} else if (itemGroupingLevel === num + 1) {
$(r).show().find(">td>span."+"tree-wrap-"+$t.p.direction).removeClass(minus).addClass(plus);
}
} else if (showData) {
$(r).show();
}
r = r.nextSibling;
}
}
tarspan.removeClass(plus).addClass(minus);
}
$($t).triggerHandler("jqGridGroupingClickGroup", [hid , collapsed]);
if( $.isFunction($t.p.onClickGroup)) { $t.p.onClickGroup.call($t, hid , collapsed); }
});
return false;
},
});
The demo demonstrates the results of all changes which I suggest. I'll post the changes as pull request to trirand. I hope that the changes will be included in the main code of jqGrid.
UPDATED: I posted the pull request with the changes which I suggested above.
UPDATED 2: My pull request was merged with the main code of jqGrid. The new 4.5.4 version of jqGrid published today includes the changed. The new demo uses jqGrid 4.5.4 and it works like you expect. So to fix the problem which you described in your question you need just update jqGrid.

Resources