Why does Angular Mat Table 2 selection model fail?
In particular why does it fail with a carbon copy of an object passed to either it's select() or toggle() methods.
don't be frightened by it though it's something anyone could read in a minute and half.
selection to select table items upon clickthe items added by the Shift- click method to the selection are in the selection array, yet do not show up as selected visually, independent of following clicks/selections (which will produce the same results E.G. : keeping the current visual faults in selection while keeping a faultless selection array. Yes one would think the click of the "bug-free" type afterwards would at least fix the faulty ones if the whole array is correct in the console.log, but no).
html :
...
</div>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="pinnedColumnsWSelect"></mat-header-row>
<mat-row *matRowDef="let row; columns: pinnedColumnsWSelect;"
class="noselect"
[ngClass]="{ 'selected': selection.isSelected(row)}"
(click)="addToSelection(row, $event, false)"></mat-row>
</mat-table>
...
(false is for, did the click originate from the checkbox element at the start of the line or from just clicking on the rest of the line?)
ts :
addToSelection(row, event, checkbox){
if(event.shiftKey && this.previous !== -1 && this.previous !== row.numberOfRow) {
if(this.previous > row.numberOfRow){
for(let previous = this.previous - 1; previous >= row.numberOfRow; previous-- ){
this.selection.toggle(this.originalDataSet.filter(x => x['numberOfRow'] === previous)[0] as object[]);
}
}else{
for(let previous = this.previous + 1; previous <= row.numberOfRow; previous++ ){
this.selection.toggle(this.originalDataSet.filter(x => x['numberOfRow'] === previous)[0] as object[]);
}
}
} else if(event.ctrlKey || checkbox) {
this.selection.toggle(row);
} else {
if(this.originalDataSet){
if(this.selection.selected.length === 1 && this.selection.selected[0] === row) {
this.selection.clear();
} else {
this.selection.clear();
this.selection.select(row);
}
}
}
this.previous = row.numberOfRow;
}
As you can probably deduce from above I first check if the Shift key was held down. If it was, I apply my selection which is currently causing issues, if Shift isn't being held but Ctrl is, I add to selection (this works), and lastly I'm in a case where neither keys are being held down, I simply clear the selection and set the new item as sole selected item.
As you may also conclude from reading my above code, what I'm trying to do in the "shift held down" part is obtain the corresponding row for each line to be selected and pass that to selection's toggle function.
Having previously console.logged row and noticed that row was indeed the entire current row of the table I deduced that I could emulate the correct objects being passed to the method "toggle" between points A and B.
My material table accepts an array of objects as it's dataset. Each object corresponds to a row, each object's keys corresponds to the table's headers. so far so good.
Pulling the right row from the array by myself (with a filter where I match the numberOfRow, my unique identifier which happens to count rows (0 , 1, 2, 3, ect...)) should give me the same thing.
console logging the two give me the same thing :
const y = this.originalDataSet.filter(x => x['numberOfRow'] === previous)[0];
console.log('filtered item ', y, ' row ', row, ' equal ', y === row, y == row );
however the nightmare begins at the two declaring being not equal to one another.
now for === fair enough, but for ==, why??
how this happens, I have no idea :
filtered item {numberOfRow: 2, nCommande: "4500131111", nLigne: "00010", nEcheance: "0001", id: {…}, …} row {numberOfRow: 2, nCommande: "4500131111", nLigne: "00010", nEcheance: "0001", id: {…}, …} equal false false

This is something I legitimately have never ever seen before in javascript. The two objects are IDENTICAL I checked manually 15 times now by opening all the nodes. yet == fails.
but hold that thought, mesmerizing though it is, I have something even more mesmerizing for you.
let's console.log our selection (console.log(this.selection.selected);)
what would you expect happens ?
that none end up selected because selection is in an incorrect format? I'd love that that only the first end up selected because selection's array is incorrect beyond that point? this would also make sense that all 5 items be correctly selected? one can dream
well no :

ok let's look at the log :
(5) [{…}, {…}, {…}, {…}, {…}]
0 : {numberOfRow: 0, nCommande: "2284595", nLigne: "1", nEcheance: "0", id: {…}, …}
1 : {numberOfRow: 1, nCommande: "2284595", nLigne: "2", nEcheance: "0", id: {…}, …}
2 : {numberOfRow: 2, nCommande: "4500131111", nLigne: "00010", nEcheance: "0001", id: {…}, …}
3 : {numberOfRow: 3, nCommande: "4500131111", nLigne: "00020", nEcheance: "0001", id: {…}, …}
4 : {numberOfRow: 4, nCommande: "4500634818", nLigne: "00010", nEcheance: "0001", id: {…}, …}
length : 5
__proto__:Array(0)
I'm confused.
you correctly selected array item 0 and 4 using this array but for everything in between, no-go, even though 4's selection (item 5) came last.
how?
this pattern of "distinguishing" between "faulty" and "correct" where the human eye can't, continues as you stack on shift selections and ctrl selections ad infinitum. If your this.selection.selected is 100000 items long it will still not have selected in the actual visual representation all those items added in it's array with shift and have correctly selected all those added with ctrl.
let's mix it up because that's how we get something that finally diverges from the beaten path.
let's try deselecting with shift
If I do one more shit click intending to add three more items and one more control click for the item below that then shift click back UP to the second item :

nothing diverging from our current mess, at least visually :
(9) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
0 : {numberOfRow: 0, nCommande: "2284595", nLigne: "1", nEcheance: "0", id: {…}, …}
1 : {numberOfRow: 1, nCommande: "2284595", nLigne: "2", nEcheance: "0", id: {…}, …}
2 : {numberOfRow: 2, nCommande: "4500131111", nLigne: "00010", nEcheance: "0001", id: {…}, …}
3 : {numberOfRow: 3, nCommande: "4500131111", nLigne: "00020", nEcheance: "0001", id: {…}, …}
4 : {numberOfRow: 4, nCommande: "4500634818", nLigne: "00010", nEcheance: "0001", id: {…}, …}
5 : {numberOfRow: 5, nCommande: "4500634818", nLigne: "00020", nEcheance: "0001", id: {…}, …}
6 : {numberOfRow: 6, nCommande: "4500634818", nLigne: "00030", nEcheance: "0001", id: {…}, …}
7 : {numberOfRow: 7, nCommande: "4500634818", nLigne: "00040", nEcheance: "0001", id: {…}, …}
8 : {numberOfRow: 8, nCommande: "4500634818", nLigne: "00050", nEcheance: "0001", id: {…}, …}
length : 9
(4) [{…}, {…}, {…}, {…}]
0 : {numberOfRow: 0, nCommande: "2284595", nLigne: "1", nEcheance: "0", id: {…}, …}
1 : {numberOfRow: 4, nCommande: "4500634818", nLigne: "00010", nEcheance: "0001", id: {…}, …}
2 : {numberOfRow: 8, nCommande: "4500634818", nLigne: "00050", nEcheance: "0001", id: {…}, …}
3 : {numberOfRow: 4, nCommande: "4500634818", nLigne: "00010", nEcheance: "0001", id: {…}, …}
length : 4
well this is unexpected.
why did it keep item 4 instead of deselecting it like the rest and on top of that ADDED IT AGAIN?
this makes me think that there is another array that's being used for comparisons I'm not aware of.
onChange ? :Furthermore
onChange results are exactly as expected :
ngOnInit() {
this.selection.onChange.subscribe(x=> {
console.log(x);
});
}
for shift-click from first item to sixth and back again :
{source: SelectionModel, added: Array(1), removed: Array(0)}
{source: SelectionModel, added: Array(1), removed: Array(0)}
{source: SelectionModel, added: Array(1), removed: Array(0)}
{source: SelectionModel, added: Array(1), removed: Array(0)}
{source: SelectionModel, added: Array(1), removed: Array(0)}
{source: SelectionModel, added: Array(1), removed: Array(0)}
{source: SelectionModel, added: Array(0), removed: Array(1)}
{source: SelectionModel, added: Array(0), removed: Array(1)}
{source: SelectionModel, added: Array(0), removed: Array(1)}
{source: SelectionModel, added: Array(0), removed: Array(1)}
{source: SelectionModel, added: Array(1), removed: Array(0)}
I check the contents they indicate being exactly as they should be (the right item numbers in the right order).
yet the visual does not follow suit.
another experiment :
I mentioned in the intro that a carbon copy of the object would be refused : this is true.
If I do this to our so far functional ctrl code it ceases to function (this is the imported underscorejs library btw, it is a shallow clone, but it does not omit underlings, it uses their memory reference) :
} else if(event.ctrlKey || checkbox) {
const bb = _.clone(row);
this.selection.toggle(bb);
}
same thing happens with this approach (this is a deep clone) :
} else if(event.ctrlKey || checkbox) {
const bb = jQuery.extend(true, {}, row);
this.selection.toggle(bb);
}
the rows are no longer visually selected with ctrl-click yet the console logged selection array and all other aspects of selection continue to be faultless.
windows pro 10 64bit chrome
{
"name": "web.ui",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"ng": "ng",
"start": "ng serve --aot",
"build": "ng b --prod",
"test": "ng test",
"lint": "ng lint"
},
"private": true,
"dependencies": {
"@angular/animations": "^6.0.3",
"@angular/cdk": "^6.1.0",
"@angular/common": "^6.0.3",
"@angular/compiler": "^6.0.3",
"@angular/core": "^6.0.3",
"@angular/forms": "^6.0.3",
"@angular/http": "^6.0.3",
"@angular/material": "^6.1.0",
"@angular/platform-browser": "^6.0.3",
"@angular/platform-browser-dynamic": "^6.0.3",
"@angular/router": "^6.0.3",
"@types/underscore": "^1.8.7",
"angular-font-awesome": "^3.1.2",
"bootstrap": "^4.0.0",
"classlist.js": "^1.1.20150312",
"core-js": "^2.5.3",
"file-saver": "^1.3.8",
"font-awesome": "^4.7.0",
"jquery": "^3.3.1",
"lodash": "^4.17.5",
"ng2-ion-range-slider": "^2.0.0",
"ngx-bootstrap": "^3.0.0",
"ngx-dropzone-wrapper": "^6.1.0",
"rxjs": "^6.2.0",
"rxjs-compat": "^6.0.0-rc.0",
"typescript": "2.7.2",
"underscore": "^1.8.3",
"web-animations-js": "^2.3.1",
"zone.js": "^0.8.20"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.6.5",
"@angular/cli": "^6.0.5",
"@angular/compiler-cli": "^6.0.3",
"@angular/language-service": "^6.0.3",
"@types/jasmine": "^2.8.6",
"@types/jasminewd2": "~2.0.3",
"@types/node": "~10.1.3",
"codelyzer": "^4.2.1",
"postcss-modules": "^1.1.0",
"protractor": "~5.3.0",
"ts-node": "~6.0.5",
"tslint": "~5.10.0"
}
}
Ok I figured it out and this will seem to some of you like it was an unknown in the problem I gave and so you couldn't have guessed it because I didn't give you my full components and to the rest of you it will seem obvious, because you know mat table well enough and you could tell from the naming of my variable something was amiss,
I have a set of four local vars for the four steps of the treatment of my object between when it arrives from API and when it shows up visually in the mat-table.
// [ 1 ] this first gets the api object and will serve as a store
originalDataSet:Array<object>;
// [ 2 ] rendered after filters
filteredDataSet:Array<object>;
// [ 3 ] sclice step (needs to be second to last due to dual slider for size)
dataSlice = [];
// [ 4 ] final. what actually is queried by Mat Table
@ViewChild(MatSort) sort: MatSort;
dataSource: MatTableDataSource<any>;
the rows (objects) are indeed the same between 1 and 4 but alot of them have gone missing since.
this does not answer why javascript thinks two copies of an object aren't the same but it does answer why the object I was feeding my toggle() methods was being considered not the same as row
now with this.dataSource.filteredData I'm literally iterating over the same array as my mat table is.
and this does not cause defects with switching pages and shift-clicking for example.
(apart from not selecting the items from page one it can no longer iterate over since they aren't in the present list, which is what I wanted to avoid at the start but I'll take that bug over this one)
If someone has an answer as to why js can't see a copy of an object as equal to itself I'd love to have an answer.
If someone can see how this object could fail to meet mat table's selection model's criteria and how I could have fooled it, I would also love to know.
I'm willing to mark you as answer and give a 50 point bounty.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With