diff --git a/package.json b/package.json index a0898973..b59f7c67 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,8 @@ ] }, "dependencies": { + "classnames": "^2.5.1", + "is-plain-object": "^5.0.0", "react-is": "^18.2.0" }, "devDependencies": { diff --git a/src/index.ts b/src/index.ts index 14eabae4..be376240 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ export { default as useEvent } from './hooks/useEvent'; export { default as useMergedState } from './hooks/useMergedState'; +export { default as mergeProps } from './mergeProps'; export { supportNodeRef, supportRef, useComposeRef } from './ref'; export { default as get } from './utils/get'; export { default as set } from './utils/set'; diff --git a/src/mergeProps.ts b/src/mergeProps.ts new file mode 100644 index 00000000..f6b0f889 --- /dev/null +++ b/src/mergeProps.ts @@ -0,0 +1,38 @@ +import classNames from 'classnames'; +import { isPlainObject } from 'is-plain-object'; + +function mergeClassNames(classNamesA: T, classNamesB: T): T { + const result = { ...classNamesA }; + Object.keys(classNamesB).forEach(key => { + result[key] = mergeClassName(classNamesA[key], classNamesB[key]); + }); + return result; +} + +function mergeClassName(classNameA?: any, classNameB?: any): string { + if (typeof classNameA === 'object' || typeof classNameB === 'object') { + return mergeClassNames(classNameA, classNameB); + } + + return classNames(classNameA, classNameB); +} + +export default function mergeProps(...list: T[]): T { + if (list.length > 2) { + return mergeProps(list[0], mergeProps(...list.slice(1))); + } + const result: T = { ...list[0] }; + list[1] && + Object.keys(list[1]).forEach(key => { + if (key === 'className') { + result[key] = classNames(result[key], list[1][key]); + } else if (key === 'classNames') { + result[key] = mergeClassNames(result[key], list[1][key]); + } else if (isPlainObject(list[1][key])) { + result[key] = mergeProps(result[key], list[1][key]); + } else { + result[key] = list[1][key] ?? result[key]; + } + }); + return result; +} diff --git a/tests/mergeProps.test.ts b/tests/mergeProps.test.ts new file mode 100644 index 00000000..af83fb63 --- /dev/null +++ b/tests/mergeProps.test.ts @@ -0,0 +1,104 @@ +import mergeProps from '../src/mergeProps'; + +test('merge className', () => { + expect(mergeProps({ className: 'foo' }, { className: 'bar' })).toEqual({ + className: 'foo bar', + }); +}); + +test('merge classNames', () => { + expect( + mergeProps( + { + classNames: { + body: 'bam', + footer: 'foo', + }, + }, + { + classNames: { + footer: 'bar', + header: 'boo', + }, + }, + ), + ).toEqual({ + classNames: { + body: 'bam', + footer: 'foo bar', + header: 'boo', + }, + }); +}); + +test('merge style', () => { + expect( + mergeProps( + { + style: { + background: '#000', + color: '#666', + }, + }, + { + style: { + background: '#fff', + }, + }, + ), + ).toEqual({ + style: { + background: '#fff', + color: '#666', + }, + }); +}); + +test('merge boolean prop', () => { + expect( + mergeProps( + { + disabled: true, + loading: false, + }, + { + disabled: false, + }, + ), + ).toEqual({ + disabled: false, + loading: false, + }); +}); + +test('merge number prop', () => { + expect( + mergeProps( + { + value: 1, + }, + { + value: 2, + }, + ), + ).toEqual({ + value: 2, + }); +}); + +test('merge non-plain object prop', () => { + const dateObj = new Date(); + const urlObj = new URL('https://example.com/'); + expect( + mergeProps( + { + value: dateObj, + }, + { + value: urlObj, + }, + ), + ).toEqual({ + value: urlObj, + }); +});