diff --git a/src/lib/components/Flowbite/Flowbite.tsx b/src/lib/components/Flowbite/Flowbite.tsx index 10c528f3f..3a2dee4b5 100644 --- a/src/lib/components/Flowbite/Flowbite.tsx +++ b/src/lib/components/Flowbite/Flowbite.tsx @@ -22,7 +22,7 @@ export const Flowbite: FC = ({ children, theme = {} }) => { const { theme: customTheme = {}, dark, usePreferences = true } = theme; const [mode, setMode, toggleMode] = useThemeMode(usePreferences); - const mergedTheme = mergeDeep(defaultTheme, customTheme); + const mergedTheme = mergeDeep(defaultTheme, customTheme); useEffect(() => { if (dark) { diff --git a/src/lib/helpers/mergeDeep.spec.ts b/src/lib/helpers/mergeDeep.spec.ts index 0cb00a1c2..80086bd35 100644 --- a/src/lib/helpers/mergeDeep.spec.ts +++ b/src/lib/helpers/mergeDeep.spec.ts @@ -1,37 +1,73 @@ import { describe, expect, it } from 'vitest'; import { mergeDeep } from './mergeDeep'; -describe.concurrent('Helper / mergeDeep (Deeply merge two objects)', () => { - it('should use the overriding value given an identical key in both inputs', () => { - const defaultTheme = { - base: 'base', - content: { - base: 'content', - }, - flush: { - off: 'no-flush', - on: 'flush', - }, +describe('Helper / mergeDeep (Deeply merge two objects)', () => { + it('should merge keys that do not exist in target', () => { + const target = {}; + const source = { foo: 'bar' }; + + const result = mergeDeep(target, source); + + expect(result).to.deep.equal({ foo: 'bar' }); + }); + + it('should merge keys that do not exist in source', () => { + const target = { foo: 'bar' }; + const source = {}; + + const result = mergeDeep(target, source); + + expect(result).to.deep.equal({ foo: 'bar' }); + }); + + it('should override target key if source key is identical', () => { + const target = { foo: { bar: 'baz' } }; + const source = { foo: { bar: 'foobar' } }; + + const result = mergeDeep(target, source); + + expect(result).to.deep.equal({ foo: { bar: 'foobar' } }); + }); + + it('should merge keys and do not mutate target and source', () => { + const target = { + foo: { a: 1, b: { c: 2, f: { g: 3 } } }, + baz: 5, }; - const overrides = { - content: { - base: 'new-content', - }, - flush: { - off: 'new-no-flush', - on: 'new-flush', - }, + + const source = { + foo: { b: { c: 3, d: 3 } }, + bar: { a: 1 }, }; - expect(mergeDeep(defaultTheme, overrides)).toEqual({ - base: 'base', - content: { - base: 'new-content', - }, - flush: { - off: 'new-no-flush', - on: 'new-flush', - }, + const result = mergeDeep(target, source); + + expect(result).to.deep.equal({ foo: { a: 1, b: { c: 3, d: 3, f: { g: 3 } } }, baz: 5, bar: { a: 1 } }); + + expect(target).to.deep.equal({ + foo: { a: 1, b: { c: 2, f: { g: 3 } } }, + baz: 5, + }); + + expect(source).to.deep.equal({ + foo: { b: { c: 3, d: 3 } }, + bar: { a: 1 }, + }); + + result.foo.b.c = 97; + result.baz = 98; + result.bar.a = 99; + + expect(result).to.deep.equal({ foo: { a: 1, b: { c: 97, d: 3, f: { g: 3 } } }, baz: 98, bar: { a: 99 } }); + + expect(target).to.deep.equal({ + foo: { a: 1, b: { c: 2, f: { g: 3 } } }, + baz: 5, + }); + + expect(source).to.deep.equal({ + foo: { b: { c: 3, d: 3 } }, + bar: { a: 1 }, }); }); }); diff --git a/src/lib/helpers/mergeDeep.ts b/src/lib/helpers/mergeDeep.ts index 9ee24c74e..9fcc92c3a 100644 --- a/src/lib/helpers/mergeDeep.ts +++ b/src/lib/helpers/mergeDeep.ts @@ -1,39 +1,48 @@ -// source: https://stackoverflow.com/questions/27936772/how-to-deep-merge-instead-of-shallow-merge - -import { DeepPartial } from '../components'; - /** - * Simple object check. + * Check if provided parameter is plain object * @param item - * @returns {boolean} + * @returns boolean */ -export function isObject(item: unknown) { - return item && typeof item === 'object' && !Array.isArray(item); +function isObject(item: unknown): item is Record { + return item !== null && typeof item === 'object' && item.constructor === Object; +} + +function cloneDeep(source: T) { + if (!isObject(source)) { + return source; + } + + const output = { ...source }; + + Object.keys(source).forEach((key) => { + (output as Record)[key] = cloneDeep(source[key]); + }); + + return output; } /** - * Deep merge two objects with deep copy of the target object. - * @param target - * @param ...sources + * Merge and deep copy the values of all of the enumerable own properties of target object from source object to a new object + * @param target The target object to get properties from. + * @param source The source object from which to copy properties. + * @return A new merged and deep copied object. */ -export function mergeDeep>(target: T, ...sources: DeepPartial[]): T { - if (!sources.length) return target; - const source = sources.shift(); - const output = { ...target }; - - if (isObject(target) && isObject(source)) { - for (const key in source) { - if (isObject(source[key])) { - if (!target[key]) Object.assign(output, { [key]: {} }); - (output[key] as Record) = mergeDeep( - target[key] as Record, - source[key] as Record, - ); +export function mergeDeep(target: T, source: S): T & S { + if (isObject(source) && Object.keys(source).length === 0) { + return cloneDeep({ ...target, ...source }); + } + + let output = { ...target, ...source }; + + if (isObject(source) && isObject(target)) { + Object.keys(source).forEach((key) => { + if (isObject(source[key]) && key in target && isObject(target[key])) { + (output as Record)[key] = mergeDeep(target[key] as object, source[key] as object); } else { - Object.assign(output, { [key]: source[key] }); + (output as Record)[key] = isObject(source[key]) ? cloneDeep(source[key]) : source[key]; } - } + }); } - return mergeDeep(output, ...sources); + return output; }