This is a simple enough question (TLDR;) :
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.
But I include alot of my debug process, hence the length :
don't be frightened by it though it's something anyone could read in a minute and half.
Context :
- a table made in mat table 2
- started using material 2's selection model
selection
to select table items upon click - added ctrl-click (add remove to selection when you Ctrl- click)
- tried to add shift-click support as well (Shift- click would add/remove all the items between click and the last added/removed item)
What fails :
the 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).
The Code :
Tackling the Shift-select :
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.
Debugging :
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.
A Deeper-seeded issue :
let's console.log our selection (console.log(this.selection.selected);
)
- then click the first Itemof our mat-table
- shift-click the fourth
- ctrl click the fith
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.
Shift-Click up? :
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.
What about 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.
Do carbon copies really fail? :
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.
Which versions of Angular, Material, OS, TypeScript, browsers? :
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"
}
}