From 2f56bd3571d9c5fb689a09d21cfb3813f5b70e89 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 12 Nov 2021 00:28:13 -0500 Subject: [PATCH] feat(utils): replace ext lib `assign-deep` by local `deepMerge` util --- packages/common/package.json | 1 - .../src/extensions/slickCellRangeDecorator.ts | 6 +- .../src/extensions/slickCellRangeSelector.ts | 6 +- packages/common/src/index.ts | 5 +- .../src/services/__tests__/utilities.spec.ts | 117 ++++++++++++++---- packages/common/src/services/domUtilities.ts | 4 +- packages/common/src/services/utilities.ts | 72 +++++++---- .../common/typings/assign-deep/index.d.ts | 3 - packages/composite-editor-component/README.md | 3 - .../composite-editor-component/package.json | 3 +- .../src/slick-composite-editor.component.ts | 6 +- .../typings/assign-deep/index.d.ts | 3 - yarn.lock | 12 -- 13 files changed, 146 insertions(+), 95 deletions(-) delete mode 100644 packages/common/typings/assign-deep/index.d.ts delete mode 100644 packages/composite-editor-component/typings/assign-deep/index.d.ts diff --git a/packages/common/package.json b/packages/common/package.json index 522206ad5..ee5d35731 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -71,7 +71,6 @@ "not dead" ], "dependencies": { - "assign-deep": "^1.0.1", "dequal": "^2.0.2", "dompurify": "^2.3.3", "flatpickr": "^4.6.9", diff --git a/packages/common/src/extensions/slickCellRangeDecorator.ts b/packages/common/src/extensions/slickCellRangeDecorator.ts index 7c1d2d9e5..be38967aa 100644 --- a/packages/common/src/extensions/slickCellRangeDecorator.ts +++ b/packages/common/src/extensions/slickCellRangeDecorator.ts @@ -1,8 +1,6 @@ -import * as assign_ from 'assign-deep'; -const assign = (assign_ as any)['default'] || assign_; - import { CellRange, CellRangeDecoratorOption, CSSStyleDeclarationWritable, SlickGrid } from '../interfaces/index'; import { createDomElement } from '../services/domUtilities'; +import { deepMerge } from '../services/utilities'; /** * Displays an overlay on top of a given cell range. @@ -26,7 +24,7 @@ export class SlickCellRangeDecorator { pluginName = 'CellRangeDecorator'; constructor(grid: SlickGrid, options?: Partial) { - this._addonOptions = assign({}, this._defaults, options); + this._addonOptions = deepMerge(this._defaults, options); this._grid = grid; } diff --git a/packages/common/src/extensions/slickCellRangeSelector.ts b/packages/common/src/extensions/slickCellRangeSelector.ts index e76ca2d5b..761e6dc57 100644 --- a/packages/common/src/extensions/slickCellRangeSelector.ts +++ b/packages/common/src/extensions/slickCellRangeSelector.ts @@ -1,9 +1,7 @@ -import * as assign_ from 'assign-deep'; -const assign = (assign_ as any)['default'] || assign_; - import { emptyElement, getHtmlElementOffset, } from '../services/domUtilities'; import { CellRange, CellRangeSelectorOption, DOMMouseEvent, DragPosition, DragRange, GridOption, OnScrollEventArgs, SlickEventHandler, SlickGrid, SlickNamespace } from '../interfaces/index'; import { SlickCellRangeDecorator } from './index'; +import { deepMerge } from '../services/utilities'; // using external SlickGrid JS libraries declare const Slick: SlickNamespace; @@ -40,7 +38,7 @@ export class SlickCellRangeSelector { constructor(options?: Partial) { this._eventHandler = new Slick.EventHandler(); - this._addonOptions = assign({}, this._defaults, options); + this._addonOptions = deepMerge(this._defaults, options); } get addonOptions() { diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 97f628025..ef4bafc24 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -3,8 +3,7 @@ import * as BackendUtilities from './services/backendUtility.service'; import * as Observers from './services/observers'; import * as ServiceUtilities from './services/utilities'; import * as SortUtilities from './sortComparers/sortUtilities'; -import * as assign_ from 'assign-deep'; -const deepAssign = (assign_ as any)['default'] || assign_; +import { deepMerge } from './services/utilities'; // Public classes. export * from './constants'; @@ -31,6 +30,6 @@ export * from './sortComparers/sortComparers.index'; export * from './services/index'; export { Enums } from './enums/enums.index'; -const Utilities = { ...BackendUtilities, ...Observers, ...ServiceUtilities, ...SortUtilities, deepAssign }; +const Utilities = { ...BackendUtilities, ...Observers, ...ServiceUtilities, ...SortUtilities, deepAssign: deepMerge }; export { Utilities }; export { SlickgridConfig } from './slickgrid-config'; diff --git a/packages/common/src/services/__tests__/utilities.spec.ts b/packages/common/src/services/__tests__/utilities.spec.ts index 0d03cc9ac..c66c44b01 100644 --- a/packages/common/src/services/__tests__/utilities.spec.ts +++ b/packages/common/src/services/__tests__/utilities.spec.ts @@ -16,6 +16,7 @@ import { unflattenParentChildArrayToTree, decimalFormatted, deepCopy, + deepMerge, emptyObject, findItemInHierarchicalStructure, findItemInTreeStructure, @@ -31,7 +32,6 @@ import { mapOperatorByFieldType, mapOperatorToShorthandDesignation, mapOperatorType, - mergeDeep, parseBoolean, parseUtcDate, removeAccentFromText, @@ -459,6 +459,95 @@ describe('Service/Utilies', () => { }); }); + describe('deepMerge method', () => { + it('should return undefined when both inputs are undefined', () => { + const obj1 = undefined; + const obj2 = null; + const output = deepMerge(obj1, obj2); + expect(output).toEqual(undefined); + }); + + it('should merge object even when 1st input is undefined because 2nd input is an object', () => { + const input1 = undefined; + const input2 = { firstName: 'John' }; + const output = deepMerge(input1, input2); + expect(output).toEqual({ firstName: 'John' }); + }); + + it('should merge object even when 1st input is undefined because 2nd input is an object', () => { + const input1 = { firstName: 'John' }; + const input2 = undefined; + const output = deepMerge(input1, input2); + expect(output).toEqual({ firstName: 'John' }); + }); + + it('should provide empty object as input and expect output object to include 2nd object', () => { + const input1 = {}; + const input2 = { firstName: 'John' }; + const output = deepMerge(input1, input2); + expect(output).toEqual({ firstName: 'John' }); + }); + + it('should provide filled object and return same object when 2nd object is also an object', () => { + const input1 = { firstName: 'Jane' }; + const input2 = { firstName: { name: 'John' } }; + const output = deepMerge(input1, input2); + expect(output).toEqual({ firstName: { name: 'John' } }); + }); + + it('should provide input object with undefined property and expect output object to return merged object from 2nd object when that one is filled', () => { + const input1 = { firstName: undefined }; + const input2 = { firstName: {} }; + const output = deepMerge(input1, input2); + expect(output).toEqual({ firstName: {} }); + }); + + it('should provide input object with undefined property and expect output object to return merged object from 2nd object when that one is filled', () => { + const input1 = { firstName: { name: 'John' } }; + const input2 = { firstName: undefined }; + const output = deepMerge(input1, input2); + expect(output).toEqual({ firstName: undefined }); + }); + + it('should merge 2 objects and expect objects to be merged with both side', () => { + const input1 = { a: 1, b: 1, c: { x: 1, y: 1 }, d: [1, 1] }; + const input2 = { b: 2, c: { y: 2, z: 2 }, d: [2, 2], e: 2 }; + + const output = deepMerge(input1, input2); + expect(output).toEqual({ + a: 1, b: 2, c: { x: 1, y: 2, z: 2 }, + d: [1, 1, 2, 2], + e: 2 + }); + }); + + it('should merge 3 objects and expect objects to be merged with both side', () => { + const input1 = { a: 1, b: 1, c: { x: 1, y: 1 }, d: [1, 1] }; + const input2 = { b: 2, c: { y: 2, z: 2 } }; + const input3 = { d: [2, 2], e: 2 }; + + const output = deepMerge(input1, input2, input3); + expect(output).toEqual({ + a: 1, b: 2, c: { x: 1, y: 2, z: 2 }, + d: [1, 1, 2, 2], + e: 2 + }); + }); + + it('should merge 3 objects, by calling deepMerge 2 times, and expect objects to be merged with both side', () => { + const input1 = { a: 1, b: 1, c: { x: 1, y: 1 }, d: [1, 1] }; + const input2 = { b: 2, c: { y: 2, z: 2 } }; + const input3 = { d: [2, 2], e: 2 }; + + const output = deepMerge(deepMerge(input1, input2), input3); + expect(output).toEqual({ + a: 1, b: 2, c: { x: 1, y: 2, z: 2 }, + d: [1, 1, 2, 2], + e: 2 + }); + }); + }); + describe('emptyObject method', () => { it('should empty all object properties', () => { const obj = { firstName: 'John', address: { zip: 123456, streetNumber: '123 Belleville Blvd' } }; @@ -1202,32 +1291,6 @@ describe('Service/Utilies', () => { }); }); - describe('mergeDeep method', () => { - it('should have undefined object when input object is also undefined', () => { - const inputObj = undefined; - mergeDeep(inputObj, { firstName: 'John' }); - expect(inputObj).toEqual(undefined); - }); - - it('should provide empty object as input and expect output object to include 2nd object', () => { - const inputObj = {}; - mergeDeep(inputObj, { firstName: 'John' }); - expect(inputObj).toEqual({ firstName: 'John' }); - }); - - it('should provide filled object and return same object when 2nd object is also an object', () => { - const inputObj = { firstName: 'Jane' }; - mergeDeep(inputObj, { firstName: { name: 'John' } }); - expect(inputObj).toEqual({ firstName: 'Jane' }); - }); - - it('should provide input object with undefined property and expect output object to return merged object from 2nd object when that one is filled', () => { - const inputObj = { firstName: undefined }; - mergeDeep(inputObj, { firstName: {} }); - expect(inputObj).toEqual({ firstName: {} }); - }); - }); - describe('parseBoolean method', () => { it('should return false when input value is not parseable to a boolean', () => { const output = parseBoolean('abc'); diff --git a/packages/common/src/services/domUtilities.ts b/packages/common/src/services/domUtilities.ts index ae614afb5..f2ea8e509 100644 --- a/packages/common/src/services/domUtilities.ts +++ b/packages/common/src/services/domUtilities.ts @@ -4,7 +4,7 @@ const DOMPurify = DOMPurify_; // patch to fix rollup to work import { InferType, SearchTerm } from '../enums/index'; import { Column, GridOption, HtmlElementPosition, SelectOption, SlickGrid, } from '../interfaces/index'; import { TranslaterService } from './translater.service'; -import { mergeDeep } from './utilities'; +import { deepMerge } from './utilities'; /** * Create the HTML DOM Element for a Select Editor or Filter, this is specific to these 2 types only and the unit tests are directly under them @@ -164,7 +164,7 @@ export function createDomElement { const elmValue = (elementOptions as any)[elmOptionKey]; if (typeof elmValue === 'object') { - mergeDeep(elm[elmOptionKey as K], elmValue); + deepMerge(elm[elmOptionKey as K], elmValue); } else { elm[elmOptionKey as K] = (elementOptions as any)[elmOptionKey]; } diff --git a/packages/common/src/services/utilities.ts b/packages/common/src/services/utilities.ts index 6c184c3bc..150c7b669 100644 --- a/packages/common/src/services/utilities.ts +++ b/packages/common/src/services/utilities.ts @@ -251,6 +251,51 @@ export function deepCopy(objectOrArray: any | any[]): any | any[] { return objectOrArray; } +/** + * Performs a deep merge of objects and returns new object, it does not modify the source object, objects (immutable) and merges arrays via concatenation. + * Also, if first argument is undefined/null but next argument is an object then it will proceed and output will be an object + * @param {...object} objects - Objects to merge + * @returns {object} New object with merged key/values + */ +export function deepMerge(target: any, ...sources: any[]): any { + if (!sources.length) { + return target; + } + const source = sources.shift(); + + // when target is not an object but source is an object, then we'll assign as object + target = (!isObject(target) && isObject(source)) ? {} : target; + + if (isObject(target) && isObject(source)) { + for (const prop in source) { + if (source.hasOwnProperty(prop)) { + if (prop in target) { + // handling merging of two properties with equal names + if (typeof target[prop] !== 'object') { + target[prop] = source[prop]; + } else { + if (typeof source[prop] !== 'object') { + target[prop] = source[prop]; + } else { + if (target[prop].concat && source[prop].concat) { + // two arrays get concatenated + target[prop] = target[prop].concat(source[prop]); + } else { + // two objects get merged recursively + target[prop] = deepMerge(target[prop], source[prop]); + } + } + } + } else { + // new properties get added to target + target[prop] = source[prop]; + } + } + } + } + return deepMerge(target, ...sources); +} + /** * Empty an object properties by looping through them all and deleting them * @param obj - input object @@ -835,33 +880,6 @@ export function mapOperatorByFieldType(fieldType: typeof FieldType[keyof typeof return map; } -/** - * Deep merge two objects. - * @param {*} target - * @param {*} ...sources - */ -export function mergeDeep(target: any, ...sources: any[]): any { - if (!sources.length) { - return target; - } - const source = sources.shift(); - - if (isObject(target) && isObject(source)) { - for (const key in source) { - if (isObject(source[key])) { - if (!target[key]) { - Object.assign(target, { [key]: {} }); - } - mergeDeep(target[key], source[key]); - } else { - Object.assign(target, { [key]: source[key] }); - } - } - } - - return mergeDeep(target, ...sources); -} - /** * Takes an object and allow to provide a property key to omit from the original object * @param {Object} obj - input object diff --git a/packages/common/typings/assign-deep/index.d.ts b/packages/common/typings/assign-deep/index.d.ts deleted file mode 100644 index dac09deac..000000000 --- a/packages/common/typings/assign-deep/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare module 'assign-deep' { - export default function assign(target: any, ...args: any[]): any; -} \ No newline at end of file diff --git a/packages/composite-editor-component/README.md b/packages/composite-editor-component/README.md index 71d5e0b8d..137d65c84 100644 --- a/packages/composite-editor-component/README.md +++ b/packages/composite-editor-component/README.md @@ -22,8 +22,5 @@ Vanilla Bundle implementation of a Composite Editor Modal Window which can do th ### Internal Dependencies - [@slickgrid-universal/common](https://github.com/ghiscoding/slickgrid-universal/tree/master/packages/common) -### External Dependencies -- [assign-deep](https://github.com/jonschlinkert/assign-deep) to deeply assign Object properties/values - ### Installation Follow the instruction provided in the main [README](https://github.com/ghiscoding/slickgrid-universal#installation). diff --git a/packages/composite-editor-component/package.json b/packages/composite-editor-component/package.json index 08051c612..4c34e1592 100644 --- a/packages/composite-editor-component/package.json +++ b/packages/composite-editor-component/package.json @@ -48,8 +48,7 @@ "not dead" ], "dependencies": { - "@slickgrid-universal/common": "^0.19.0", - "assign-deep": "^1.0.1" + "@slickgrid-universal/common": "^0.19.0" }, "devDependencies": { "cross-env": "^7.0.3", diff --git a/packages/composite-editor-component/src/slick-composite-editor.component.ts b/packages/composite-editor-component/src/slick-composite-editor.component.ts index d95cefde1..37acf3edb 100644 --- a/packages/composite-editor-component/src/slick-composite-editor.component.ts +++ b/packages/composite-editor-component/src/slick-composite-editor.component.ts @@ -1,6 +1,3 @@ -import * as assign_ from 'assign-deep'; -const assign = (assign_ as any)['default'] || assign_; - import { BindingEventService, Column, @@ -13,6 +10,7 @@ import { createDomElement, CurrentRowSelection, deepCopy, + deepMerge, DOMEvent, Editor, EditorValidationResult, @@ -218,7 +216,7 @@ export class SlickCompositeEditorComponent implements ExternalResource { this._formValues = { ...this._formValues, [columnId]: newValue }; } - this._formValues = assign({}, this._itemDataContext, this._formValues); + this._formValues = deepMerge({}, this._itemDataContext, this._formValues); } /** diff --git a/packages/composite-editor-component/typings/assign-deep/index.d.ts b/packages/composite-editor-component/typings/assign-deep/index.d.ts deleted file mode 100644 index dac09deac..000000000 --- a/packages/composite-editor-component/typings/assign-deep/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare module 'assign-deep' { - export default function assign(target: any, ...args: any[]): any; -} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index b06e1649f..6ba3bc31d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2629,23 +2629,11 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= -assign-deep@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/assign-deep/-/assign-deep-1.0.1.tgz#b6d21d74e2f28bf6592e4c0c541bed6ab59c5f27" - integrity sha512-CSXAX79mibneEYfqLT5FEmkqR5WXF+xDRjgQQuVf6wSCXCYU8/vHttPidNar7wJ5BFmKAo8Wei0rCtzb+M/yeA== - dependencies: - assign-symbols "^2.0.2" - assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= -assign-symbols@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-2.0.2.tgz#0fb9191dd9d617042746ecfc354f3a3d768a0c98" - integrity sha512-9sBQUQZMKFKcO/C3Bo6Rx4CQany0R0UeVcefNGRRdW2vbmaMOhV1sbmlXcQLcD56juLXbSGTBm0GGuvmrAF8pA== - astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"