projects/angular/components/ui-grid/src/managers/data-manager.ts
import assignWith from 'lodash-es/assignWith';
import cloneDeep from 'lodash-es/cloneDeep';
import difference from 'lodash-es/difference';
import get from 'lodash-es/get';
import isArray from 'lodash-es/isArray';
import isDate from 'lodash-es/isDate';
import isObject from 'lodash-es/isObject';
import objectHash from 'object-hash';
import { BehaviorSubject } from 'rxjs';
import { isDevMode } from '@angular/core';
import {
GridOptions,
IGridDataEntry,
} from '../models';
/**
* @ignore
* @internal
*/
const customPatch = (left: any, right: any): any => {
if (
!isArray(left) && !isArray(right) &&
!isDate(left) && !isDate(right) &&
isObject(left) && isObject(right)
) {
return assignWith(left, right, customPatch);
}
};
/**
* @ignore
* @internal
*/
type PropertyMap<T> = { [Key in keyof T]?: PropertyMap<T[Key]> };
type StringOrNumberKeyOf<T> = keyof T & (string | number);
/**
* The data manager increases rendering performance drastically updating the dataset.
* By updating the references directly (when having set useCache: true – default: false)
* * the renderer will update the invidivual cells rather than redraw the entire row
* * increasing / decreasing data count will result in less node insertion / removal
*
* @export
* @ignore
* @internal
*/
export class DataManager<T extends IGridDataEntry, K extends StringOrNumberKeyOf<T> = StringOrNumberKeyOf<T>> {
useCache: boolean;
idProperty: K;
get length() {
return this.data$.value.length;
}
pristine = true;
data$ = new BehaviorSubject<T[]>([]);
getProperty = get;
private _hashMap = new Map<string, string>();
constructor(options?: GridOptions<T>) {
this.useCache = options?.useCache ?? false;
this.idProperty = options?.idProperty as unknown as K ?? 'id';
}
forEach = (callbackfn: (value: T, index: number, array: T[]) => void) =>
this.data$.value.forEach(callbackfn);
some = (callbackfn: (value: T, index: number, array: T[]) => boolean) =>
this.data$.value.some(callbackfn);
every = (callbackfn: (value: T, index: number, array: T[]) => boolean) =>
this.data$.value.every(callbackfn);
indexOf(entry: T) {
return this.data$.value.indexOf(entry);
}
find = <R extends T[K]>(entryId: R) =>
this.data$.value.find(e => e[this.idProperty] === entryId);
get = (index: number) =>
this.data$.value[index];
patchRow(id: T[K], patch: PropertyMap<T>) {
const entry = this.data$.value.find(e => e[this.idProperty] === id);
if (!entry) {
if (isDevMode()) {
console.warn(`Could not patch entity ${this.idProperty}. Skipping patch...`);
}
return;
}
if (!this.useCache) {
const data = this.data$.value.slice(0);
const index = data.indexOf(entry);
assignWith(entry, patch, customPatch);
data.splice(index, 1, { ...entry });
this.data$.next(data);
return;
}
assignWith(entry, patch, customPatch);
this._hash(entry);
this._emit();
}
update(data: T[] | null) {
this.pristine = this.pristine &&
data == null;
data = cloneDeep(data ?? []);
if (!this.useCache) {
this._emit(data);
return;
}
if (data.some(e => e[this.idProperty] == null)) {
throw new Error(`The '${this.idProperty}' property is a missing in: ${JSON.stringify(data, null, 4)}`);
}
const cache = this.data$.value;
this._hashMap.clear();
data.forEach(entry => this._hash(entry));
if (!cache.length) {
this._emit(data);
return;
}
if (data.length < cache.length) {
cache.splice(data.length);
}
data.forEach((entry: T, idx: number) => {
if (idx < cache.length) {
difference<string>(
Object.keys(cache[idx]),
Object.keys(entry),
).forEach(prop => {
const key = prop as keyof T;
delete cache[idx][key];
});
Object.assign(cache[idx], entry);
} else {
cache.push(entry);
}
});
this._emit();
}
destroy() {
this._hashMap.clear();
this.data$.complete();
}
hashTrack = (_: number | undefined | null, entry: T) =>
this.useCache
? this._hashMap.get(`${entry[this.idProperty]}`)
: entry[this.idProperty];
private _hash = (entry: T) =>
this._hashMap.set(`${entry[this.idProperty]}`, objectHash(entry, {
algorithm: 'md5',
ignoreUnknown: true,
}));
private _emit(data?: T[]) {
this.data$.next([...(data ?? this.data$.value)]);
}
}