Skip to content

Commit

Permalink
feat(utils): replace slick-core extend utils with node-extend (#1277)
Browse files Browse the repository at this point in the history
* feat(utils): replace slick-core extend utils with `node-extend`
the `extend` function that was added to SlickGrid core utils seems to be causing problems when Angular-Slickgrid are published on external website as per this issue: ghiscoding/Angular-Slickgrid#1334
- the node-extend came from this popular npm package: https://github.com/justmoon/node-extend/blob/v1.2.0/test/index.js
the reason, I reimplemented it in here is because the npm package is using old CJS syntax and is not ESM friendly, moving the code in here would be better with newer syntax and less CJS code, also less external dependencies

* chore: replace all previous slickcore extend with node-extend
  • Loading branch information
ghiscoding committed Dec 21, 2023
1 parent abe256a commit 3439118
Show file tree
Hide file tree
Showing 13 changed files with 930 additions and 241 deletions.
125 changes: 0 additions & 125 deletions packages/common/src/core/__tests__/slickCore.spec.ts
Expand Up @@ -415,32 +415,6 @@ describe('SlickCore file', () => {
});

describe('Utils', () => {
describe('isPlainObject', () => {
it('should be falsy when object contains prototype methods', () => {
const l = console.log;
const obj = {
method: () => l("method in obj")
};
const obj2: any = { hello: 'world' };
obj2.__proto__ = obj;

expect(Utils.isPlainObject(obj2)).toBeFalsy();
});

it('should be truthy when object does not contains any prototype', () => {
const obj2: any = { hello: 'world' };
obj2.__proto__ = null;

expect(Utils.isPlainObject(obj2)).toBeTruthy();
});

it('should be truthy when object is a regular object without methods', () => {
const obj2 = { hello: 'world' };

expect(Utils.isPlainObject(obj2)).toBeTruthy();
});
});

describe('storage() function', () => {
it('should be able to store an object and retrieve it later', () => {
const div = document.createElement('div');
Expand Down Expand Up @@ -489,105 +463,6 @@ describe('SlickCore file', () => {
});
});

describe('extend() function', () => {
it('should be able to make a perfect deep copy of an object', () => {
const callback = () => console.log('hello');
const obj1 = { hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'], callback } };
const obj2 = Utils.extend(true, {}, obj1, { another: 'prop' });

expect(obj2).toEqual({ hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'], callback }, another: 'prop' });
});

it('should be able to make a perfect deep copy of an object that includes String() and Boolean() constructors', () => {
const callback = () => console.log('hello');
const obj1 = { hello: { sender: 'me', target: String(123), valid: Boolean(null) }, deeper: { children: ['abc', 'cde'], callback } };
const obj2 = Utils.extend(true, {}, obj1, { another: 'prop' });

expect(obj2).toEqual({ hello: { sender: 'me', target: '123', valid: false }, deeper: { children: ['abc', 'cde'], callback }, another: 'prop' });
});

it('should be able to make a deep copy of an object and changing new object prop should not affect input object', () => {
const obj1 = { hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'] } };
const obj2 = Utils.extend(true, {}, obj1, { another: 'prop' });
obj2.hello.target = 'mum';

expect(obj1).toEqual({ hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'] } });
expect(obj2).toEqual({ hello: { sender: 'me', target: 'mum' }, deeper: { children: ['abc', 'cde'] }, another: 'prop' });
});

it('should assume an extended object when passing true boolean but ommitting empty object as target, so changing output object will impact input object as well', () => {
const obj1 = { hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'] } };
const obj2 = Utils.extend(true, obj1, { another: 'prop' });
obj2.hello.target = 'mum';

expect(obj1).toEqual({ hello: { sender: 'me', target: 'mum' }, deeper: { children: ['abc', 'cde'] }, another: 'prop' });
expect(obj2).toEqual({ hello: { sender: 'me', target: 'mum' }, deeper: { children: ['abc', 'cde'] }, another: 'prop' });
});

it('should assume an extended object when ommitting true boolean, so changing output object will impact input object as well', () => {
const obj1 = { hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'] } };
const obj2 = Utils.extend(obj1, { another: { age: 20 } });
obj2.hello.target = 'mum';

expect(obj1).toEqual({ hello: { sender: 'me', target: 'mum' }, deeper: { children: ['abc', 'cde'] }, another: { age: 20 } });
expect(obj2).toEqual({ hello: { sender: 'me', target: 'mum' }, deeper: { children: ['abc', 'cde'] }, another: { age: 20 } });
});

it('should return same object when passing input object twice', () => {
const obj1 = { hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'] } };
const obj2 = Utils.extend(true, {}, obj1, obj1);

expect(obj2).toEqual(obj1);
});

it('should do a deep copy of an array of objects with properties having objects and changing object property should not affect original object', () => {
const obj1 = { firstName: 'John', lastName: 'Doe', address: { zip: 123456 } };
const obj2 = { firstName: 'Jane', lastName: 'Doe', address: { zip: 222222 } };
const arr1 = [obj1, obj2];
const arr2 = Utils.extend(true, [], arr1);
arr2[0].address.zip = 888888;
arr2[1].address.zip = 999999;

expect(arr1[0].address.zip).toBe(123456);
expect(arr1[1].address.zip).toBe(222222);
expect(arr2[0].address.zip).toBe(888888);
expect(arr2[1].address.zip).toBe(999999);
});

it('should return same object when passing only a single object', () => {
expect(Utils.extend({ hello: 'world' })).toEqual({ hello: 'world' });
});

it('should expect Symbol to be converted to Object', () => {
const sym1 = Symbol("foo");
const sym2 = Symbol("bar");

expect(Utils.extend(sym1, sym2, { hello: 'world' })).toEqual({ hello: 'world' });
});

it('should be able to make a copy of an object with prototype', () => {
const l = console.log;
const method = () => l("method in obj");
const obj = {
method
};
const obj2: any = { hello: 'world' };
obj2.__proto__ = obj;

const obj1 = { hello: { sender: 'me', target: 'world' }, deeper: { children: ['abc', 'cde'] } };
const obj3 = Utils.extend(obj1, obj2);

expect(obj3).toEqual({ hello: 'world', deeper: { children: ['abc', 'cde'] }, method });
});
});

describe('noop() function', () => {
it('should return empty function', () => {
expect(typeof Utils.noop).toBe('function');
expect(Utils.noop()).toBeUndefined();
});
});

describe('height() function', () => {
it('should return null when calling without a valid element', () => {
const result = Utils.height(null as any);
Expand Down
84 changes: 0 additions & 84 deletions packages/common/src/core/slickCore.ts
Expand Up @@ -7,8 +7,6 @@
* @module Core
* @namespace Slick
*/
import { isDefined } from '@slickgrid-universal/utils';

import { MergeTypes } from '../enums/index';
import type { CSSStyleDeclarationWritable, EditController } from '../interfaces';

Expand Down Expand Up @@ -557,13 +555,6 @@ export class SlickEditorLock {
}

export class Utils {
// jQuery's extend
private static getProto = Object.getPrototypeOf;
private static class2type: any = {};
private static toString = Utils.class2type.toString;
private static hasOwn = Utils.class2type.hasOwnProperty;
private static fnToString = Utils.hasOwn.toString;
private static ObjectFunctionString = Utils.fnToString.call(Object);
public static storage = {
// https://stackoverflow.com/questions/29222027/vanilla-alternative-to-jquery-data-function-any-native-javascript-alternati
_storage: new WeakMap(),
Expand All @@ -589,81 +580,6 @@ export class Utils {
}
};

public static isFunction(obj: any) {
return typeof obj === 'function' && typeof obj.nodeType !== 'number' && typeof obj.item !== 'function';
}

public static isPlainObject(obj: any) {
if (!obj || Utils.toString.call(obj) !== '[object Object]') {
return false;
}

const proto = Utils.getProto(obj);
if (!proto) {
return true;
}
const Ctor = Utils.hasOwn.call(proto, 'constructor') && proto.constructor;
return typeof Ctor === 'function' && Utils.fnToString.call(Ctor) === Utils.ObjectFunctionString;
}

public static extend<T = any>(...args: any[]): T {
// eslint-disable-next-line one-var
let options, name, src, copy, copyIsArray, clone;
let target = args[0];
let i = 1;
let deep = false;
const length = args.length;

if (target === true) {
deep = target;
target = args[i] || {};
i++;
} else {
target = target || {};
}
if (typeof target !== 'object' && !Utils.isFunction(target)) {
target = {}; // Symbol and others will be converted to Object
}
if (length === 1) {
return args[0];
}
/* istanbul ignore if */
if (i === length) {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-this-alias
target = this;
i--;
}
for (; i < length; i++) {
if (isDefined(options = args[i])) {
for (name in options) {
copy = options[name];
/* istanbul ignore if */
if (name === '__proto__' || target === copy) {
continue;
}
if (deep && copy && (Utils.isPlainObject(copy) || (copyIsArray = Array.isArray(copy)))) {
src = target[name];
if (copyIsArray && !Array.isArray(src)) {
clone = [];
} else if (!copyIsArray && !Utils.isPlainObject(src)) {
clone = {};
} else {
clone = src;
}
copyIsArray = false;
target[name] = Utils.extend(deep, clone, copy);
} else if (copy !== undefined) {
target[name] = copy;
}
}
}
}
return target as T;
}

public static noop() { }

public static height(el: HTMLElement, value?: number | string): number | void {
if (!el) {
return;
Expand Down
9 changes: 4 additions & 5 deletions packages/common/src/core/slickDataview.ts
@@ -1,6 +1,6 @@
/* eslint-disable no-new-func */
/* eslint-disable no-bitwise */
import { isDefined } from '@slickgrid-universal/utils';
import { extend, isDefined } from '@slickgrid-universal/utils';

import { SlickGroupItemMetadataProvider } from '../extensions/slickGroupItemMetadataProvider';
import type {
Expand All @@ -26,7 +26,6 @@ import {
SlickGroup,
SlickGroupTotals,
SlickNonDataItem,
Utils,
} from './slickCore';
import type { SlickGrid } from './slickGrid';

Expand Down Expand Up @@ -137,7 +136,7 @@ export class SlickDataView<TData extends SlickDataItem = any> implements CustomD
this.onSelectedRowIdsChanged = new SlickEvent<OnSelectedRowIdsChangedEventArgs>('onSelectedRowIdsChanged', externalPubSub);
this.onSetItemsCalled = new SlickEvent<OnSetItemsCalledEventArgs>('onSetItemsCalled', externalPubSub);

this._options = Utils.extend(true, {}, this.defaults, options);
this._options = extend(true, {}, this.defaults, options);
}

/**
Expand Down Expand Up @@ -394,7 +393,7 @@ export class SlickDataView<TData extends SlickDataItem = any> implements CustomD
this.groupingInfos = ((groupingInfo instanceof Array) ? groupingInfo : [groupingInfo]) as any;

for (let i = 0; i < this.groupingInfos.length; i++) {
const gi = this.groupingInfos[i] = Utils.extend(true, {}, this.groupingInfoDefaults, this.groupingInfos[i]);
const gi = this.groupingInfos[i] = extend(true, {}, this.groupingInfoDefaults, this.groupingInfos[i]);
gi.getterIsAFn = typeof gi.getter === 'function';

// pre-compile accumulator loops
Expand Down Expand Up @@ -1319,7 +1318,7 @@ export class SlickDataView<TData extends SlickDataItem = any> implements CustomD
return;
}

const previousPagingInfo = Utils.extend(true, {}, this.getPagingInfo());
const previousPagingInfo = extend(true, {}, this.getPagingInfo());

const countBefore = this.rows.length;
const totalRowsBefore = this.totalRows;
Expand Down
12 changes: 6 additions & 6 deletions packages/common/src/core/slickGrid.ts
Expand Up @@ -2,7 +2,7 @@
import Sortable, { SortableEvent } from 'sortablejs';
import DOMPurify from 'dompurify';
import { BindingEventService } from '@slickgrid-universal/binding';
import { createDomElement, emptyElement, getInnerSize, getOffset, insertAfterElement, isDefined, isPrimitiveOrHTML } from '@slickgrid-universal/utils';
import { createDomElement, emptyElement, extend, getInnerSize, getOffset, insertAfterElement, isDefined, isPrimitiveOrHTML } from '@slickgrid-universal/utils';

import {
type BasePubSub,
Expand Down Expand Up @@ -588,7 +588,7 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
if (!options) { this._options = {} as O; }
Utils.applyDefaults(this._options, this._defaults);
} else {
this._options = Utils.extend<O>(true, {}, this._defaults, options);
this._options = extend<O>(true, {}, this._defaults, options);
}
this.scrollThrottle = this.actionThrottle(this.render.bind(this), this._options.scrollRenderThrottling as number);
this.maxSupportedCssHeight = this.maxSupportedCssHeight || this.getMaxSupportedCssHeight();
Expand Down Expand Up @@ -3002,7 +3002,7 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
if (this._options.mixinDefaults) {
Utils.applyDefaults(m, this._columnDefaults);
} else {
m = this.columns[i] = Utils.extend({}, this._columnDefaults, m);
m = this.columns[i] = extend({}, this._columnDefaults, m);
}

this.columnsById[m.id] = i;
Expand Down Expand Up @@ -3076,8 +3076,8 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
this.handleScroll(); // trigger scroll to realign column headers as well
}

const originalOptions = Utils.extend(true, {}, this._options);
this._options = Utils.extend(this._options, newOptions);
const originalOptions = extend(true, {}, this._options);
this._options = extend(this._options, newOptions);
this.trigger(this.onSetOptions, { optionsBefore: originalOptions, optionsAfter: this._options });

this.internal_setOptions(suppressRender, suppressColumnSet, suppressSetOverflow);
Expand Down Expand Up @@ -4339,7 +4339,7 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
// add new rows & missing cells in existing rows
if (this.lastRenderedScrollLeft !== this.scrollLeft) {
if (this.hasFrozenRows) {
const renderedFrozenRows = Utils.extend(true, {}, rendered);
const renderedFrozenRows = extend(true, {}, rendered);

if (this._options.frozenBottom) {
renderedFrozenRows.top = this.actualFrozenRow;
Expand Down
@@ -1,6 +1,6 @@
import { createDomElement } from '@slickgrid-universal/utils';
import { createDomElement, extend } from '@slickgrid-universal/utils';

import { SlickEventHandler, Utils as SlickUtils, type SlickDataView, SlickGroup, type SlickGrid } from '../core/index';
import { SlickEventHandler, type SlickDataView, SlickGroup, type SlickGrid } from '../core/index';
import type {
Column,
DOMEvent,
Expand Down Expand Up @@ -42,7 +42,7 @@ export class SlickGroupItemMetadataProvider {

constructor(inputOptions?: GroupItemMetadataProviderOption) {
this._eventHandler = new SlickEventHandler();
this._options = SlickUtils.extend<GroupItemMetadataProviderOption>(true, {}, this._defaults, inputOptions);
this._options = extend<GroupItemMetadataProviderOption>(true, {}, this._defaults, inputOptions);
}

/** Getter of the SlickGrid Event Handler */
Expand Down
5 changes: 2 additions & 3 deletions packages/common/src/services/filter.service.ts
@@ -1,7 +1,6 @@
import { BasePubSubService } from '@slickgrid-universal/event-pub-sub';
import { deepCopy, stripTags } from '@slickgrid-universal/utils';
import { deepCopy, extend, stripTags } from '@slickgrid-universal/utils';
import { dequal } from 'dequal/lite';
import { Utils as SlickUtils } from '../core/index';

import { Constants } from '../constants';
import { FilterConditions, getParsedSearchTermsByFieldType } from './../filter-conditions/index';
Expand Down Expand Up @@ -1168,7 +1167,7 @@ export class FilterService {

// event might have been created as a CustomEvent (e.g. CompoundDateFilter), without being a valid SlickEventData,
// if so we will create a new SlickEventData and merge it with that CustomEvent to avoid having SlickGrid errors
const eventData = ((event && typeof (event as any).isPropagationStopped !== 'function') ? SlickUtils.extend({}, new SlickEventData(), event) : event);
const eventData = ((event && typeof (event as any).isPropagationStopped !== 'function') ? extend({}, new SlickEventData(), event) : event);

// trigger an event only if Filters changed or if ENTER key was pressed
const eventKey = (event as KeyboardEvent)?.key;
Expand Down

0 comments on commit 3439118

Please sign in to comment.