I have a small list with some items that can be reordered (see stackblitz). Internally that list is implemented as a map. Conveniently, Angular provides the pipe keyvalue, which allows an easy way to iterate over maps like this:
*ngFor="let item of doc.items | keyvalue:sortHash"
You can provide a function, sortHash that will take care of sorting the list.
I want to use cdkDropList
in order to provide DnD ordering to the list. This is trivial when using arrays:
(cdkDropListDropped)="dropItem($event, doc.items, doc)
You have to simply pass a function to cdkDropListDropped
that will take care of moving the item within the array. Angular provides a built-in function moveItemInArray
that does so:
import { moveItemInArray } from '@angular/cdk/drag-drop';
...
async dropItem(event: CdkDragDrop<string[]>, list: any, doc: any) {
moveItemInArray(list, event.previousIndex, event.currentIndex);
}
This works as expected with arrays, but in my case, I rely on maps where the order is defined by a property "order", see my data structure:
doc = {
meta: {
text: 'title',
...
},
items: {
SEC_000000: {
meta: {
text: 'Episode I - The Phantom Menace',
order: '0',
...
},
},
SEC_111111: {
meta: {
text: 'Episode II - Attack of the Clones',
order: '1',
...
},
},
SEC_222222: {
meta: {
text: 'Episode III - Revenge of the Sith',
order: '2',
...
},
},
},
};
hence my dropItem
function is a bit different, it
- transforms my map (
doc.items
) into an array - then uses the built-in
moveItemInArray
function to effectively move the item inside the array - then updates the "order" property of all items, and finally
- transforms the array back into a map
The sorting function works as expected, but the UI does not update when DnD.
Here is a stackblitz with a simplified sample code. What am I missing here?
CodePudding user response:
The answer lies deep in the Angular core.
You are using the KeyValuePipe
. Source here.
The part of the pipe we are interested in is this:
const differChanges: KeyValueChanges<K, V>|null = this.differ.diff(input as any);
const compareFnChanged = compareFn !== this.compareFn;
if (differChanges || compareFnChanged) {
this.keyValues.sort(compareFn);
this.compareFn = compareFn;
}
If the differ finds a difference in the input object, the sort function is run.
You can console log differChanges
and see that it consistently returns null
after the view is initialized. Why? We need to look at the differ code:
// Add the record or a given key to the list of changes only when the value has actually changed
private _maybeAddToChanges(record: KeyValueChangeRecord_<K, V>, newValue: any): void {
if (!Object.is(newValue, record.currentValue)) {
record.previousValue = record.currentValue;
record.currentValue = newValue;
this._addToChanges(record);
}
}
The differ is using Object.is(newValue, record.currentValue)
to determine if the value has changed. The value of the object to be diffed in this case is an object itself, and Object.is()
does not evaluate deep equality.
So you have at least two options:
- Write your own keyvalue pipe that works the way you want it to.
- Use a different data structure to hold your movie info
I've created a working StackBlitz with a custom brute-force resorting strategy keyvalue
pipe.