From de8ab2731853c2963ba8b9a6783fbde4db8656cf Mon Sep 17 00:00:00 2001 From: Aaron Peltz Date: Tue, 22 Aug 2017 16:31:51 -0700 Subject: [PATCH 1/4] deepMerge adds optional options argument to default export; adds deepmerge to options --- README.md | 10 ++++++++++ package.json | 1 + rollup.config.js | 6 ++++-- src/index.js | 24 +++++++++++++++++++++--- test/index.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 81 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 247051b..4a36d5a 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Creates a higher order component that provides values from configuration as prop - `namespace` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)?** If provided, exposes `defaults` under a `namespace` - `defaults` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)?** An object containing default configuration values +- `options` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)?** An object containing options for resolving configuration values (optional, default `{}`) **Examples** @@ -80,6 +81,15 @@ export default configure({ ); ``` +```javascript +let configure = preconf('users', { user1: { first: 'john' } }, { deepMerge: true }); +export default configure({ + user1: { last: 'smith' } +})( ({ user1 }) => + Full Name: {`${props.user1.first} ${props.user1.last}`} +); +``` + Returns **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** [configure()](#configure) #### configure diff --git a/package.json b/package.json index 6eba6ec..7eaa7b8 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "preact": "*" }, "dependencies": { + "deepmerge": "^1.5.0", "dlv": "^1.1.0" } } diff --git a/rollup.config.js b/rollup.config.js index 7eee98c..2c0a43e 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -4,11 +4,13 @@ export default { useStrict: false, external: [ 'preact', - 'dlv' + 'dlv', + 'deepmerge' ], globals: { preact: 'preact', - dlv: 'dlv' + dlv: 'dlv', + deepmerge: 'deepmerge' }, plugins: [ buble({ diff --git a/src/index.js b/src/index.js index 3684bc6..f4e2775 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,11 @@ import { h } from 'preact'; import delve from 'dlv'; +import merge from 'deepmerge'; /** Creates a higher order component that provides values from configuration as props. * @param {String} [namespace] If provided, exposes `defaults` under a `namespace` * @param {Object} [defaults] An object containing default configuration values + * @param {Object} [options] An object containing options for resolving configuration values * @returns {Function} [configure()](#configure) * * @example @@ -23,8 +25,16 @@ import delve from 'dlv'; * })( ({ url }) => * * ); + * + * @example + * let configure = preconf('users', { user1: { first: 'john' } }, { deepMerge: true }); + * export default configure({ + * user1: { last: 'smith' } + * })( ({ user1 }) => + * Full Name: {`${props.user1.first} ${props.user1.last}`} + * ); */ -export default function preconf(namespace, defaults) { +export default function preconf(namespace, defaults, options={}) { if (namespace) defaults = { [namespace]: defaults }; /** Creates a Higher Order Component that provides configuration as props. @@ -43,8 +53,16 @@ export default function preconf(namespace, defaults) { for (let key in keys) { let path = keys[key]; if (isArray) key = path.split('.').pop(); - if (typeof props[key]==='undefined' || props[key]===null) { - props[key] = delve(context, 'config.'+path, delve(defaults, path)); + + let inheritedVal = delve(context, 'config.'+path); + let defaultVal = delve(defaults, path); + + if (options.deepMerge && inheritedVal && defaultVal && typeof inheritedVal === 'object' && typeof defaultVal === 'object') { + props[key] = merge(defaultVal, inheritedVal); + } + + else if (typeof props[key]==='undefined' || props[key]===null) { + props[key] = inheritedVal || defaultVal; } } return h(Child, props); diff --git a/test/index.js b/test/index.js index 84425d4..4f64a84 100644 --- a/test/index.js +++ b/test/index.js @@ -118,5 +118,50 @@ describe('preconf', () => { }); }); }); + + describe('opts.deepMerge', () => { + let defaults = { + a: 'b', + c: { d: 'e', f: { g: 'h' } } + }; + + let Child = spy( () => null ); + let opts = { deepMerge: true }; + let configure = preconf(null, defaults, opts); + + function test(config, selector) { + Child.reset(); + let Wrapped = configure(selector)(Child); + rndr(); + } + + it('should pass defaults', () => { + test(undefined, ['a', 'c']); + expect(Child).to.have.been.calledWithMatch({ a: 'b', c: { d: 'e', f: { g: 'h' } } }); + }); + + it('should pass provided config values', () => { + let config = { c: 'override', e: 'f' }; + + test(config, ['a', 'c', 'e']); + expect(Child).to.have.been.calledWithMatch({ a: 'b', c: 'override', e: 'f' }); + + }); + + it('should deep merge config values', () => { + let config = { c: { f: { g: 'override' } }, e: 'f' }; + + test(config, ['a', 'c', 'e']); + expect(Child).to.have.been.calledWithMatch({ a: 'b', c: { d: 'e', f: { g: 'override' } }, e: 'f' }); + + config = { c: { d: 'override' }, e: 'f' }; + + test(config, ['a', 'c', 'e']); + expect(Child).to.have.been.calledWithMatch({ a: 'b', c: { d: 'override', f: { g: 'h' } }, e: 'f' }); + + }); + + }); + }); }); From fd7531dd723073d4d6c625682c134b5907a305e7 Mon Sep 17 00:00:00 2001 From: Aaron Peltz Date: Fri, 8 Sep 2017 16:19:37 -0700 Subject: [PATCH 2/4] sets deepMerge option to true by default; updates readme --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++---- src/index.js | 12 ++++++------ test/index.js | 37 ++++++++++++++++++++++++++++--------- 3 files changed, 80 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 4a36d5a..619af56 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ Preconf is just 400 bytes and works well with [preact-context-provider](https:// ## Usage +Install: `npm i preconf` + ```js import preconf from 'preconf'; import Provider from 'preact-context-provider'; @@ -44,6 +46,45 @@ render( // hello, Stan ``` +**Collisions** + +In the event that a configuration field is present as a mergeable object in both `context.config` and the default configuration object provided to `preconf`, the resulting prop passed to the enhanced component will be a merged object. + +Default behavior: + +```js +const defaults = { name: { first: 'Bob', } }; +let configure = preconf(null, defaults); + +let FooWithDefaults = configure('name')(Foo) + +render( + + + +) +// Foo receives prop `name` as merged object: { first: 'Bob', last: 'Jones'} +``` + +To prevent the default behavior and override the provided default mergeable object with the mergeable object from `context.config`, `deepMerge` in the `options` parameter can be set to `false`. + +Override with value from `context.config`: + +```js +const defaults = { name: { first: 'Bob', } }; +// Additional options parameter passed with deepMerge set to false +let configure = preconf(null, defaults, { deepMerge: false }); + +let FooWithDefaults = configure('name')(Foo) + +render( + + + +) +// Foo receives prop `name` as unmerged object: { last: 'Jones'} +``` + ## API @@ -57,6 +98,7 @@ Creates a higher order component that provides values from configuration as prop - `namespace` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)?** If provided, exposes `defaults` under a `namespace` - `defaults` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)?** An object containing default configuration values - `options` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)?** An object containing options for resolving configuration values (optional, default `{}`) + - `options.deepMerge` (optional, default `true`) **Examples** @@ -82,11 +124,11 @@ export default configure({ ``` ```javascript -let configure = preconf('users', { user1: { first: 'john' } }, { deepMerge: true }); +let configure = preconf('locations', { headquarters: { country: 'Germany' } }); export default configure({ - user1: { last: 'smith' } -})( ({ user1 }) => - Full Name: {`${props.user1.first} ${props.user1.last}`} + headquarters: { city: 'Hamburg' } +})( ({ headquarters }) => + Location: {`${props.headquarters.city}, ${props.headquarters.country}`} ); ``` diff --git a/src/index.js b/src/index.js index f4e2775..3024fac 100644 --- a/src/index.js +++ b/src/index.js @@ -27,14 +27,14 @@ import merge from 'deepmerge'; * ); * * @example - * let configure = preconf('users', { user1: { first: 'john' } }, { deepMerge: true }); + * let configure = preconf('locations', { headquarters: { country: 'Germany' } }); * export default configure({ - * user1: { last: 'smith' } - * })( ({ user1 }) => - * Full Name: {`${props.user1.first} ${props.user1.last}`} + * headquarters: { city: 'Hamburg' } + * })( ({ headquarters }) => + * Location: {`${props.headquarters.city}, ${props.headquarters.country}`} * ); */ -export default function preconf(namespace, defaults, options={}) { +export default function preconf(namespace, defaults, { deepMerge=true }={}) { if (namespace) defaults = { [namespace]: defaults }; /** Creates a Higher Order Component that provides configuration as props. @@ -57,7 +57,7 @@ export default function preconf(namespace, defaults, options={}) { let inheritedVal = delve(context, 'config.'+path); let defaultVal = delve(defaults, path); - if (options.deepMerge && inheritedVal && defaultVal && typeof inheritedVal === 'object' && typeof defaultVal === 'object') { + if (deepMerge && inheritedVal && defaultVal && typeof inheritedVal === 'object' && typeof defaultVal === 'object') { props[key] = merge(defaultVal, inheritedVal); } diff --git a/test/index.js b/test/index.js index 4f64a84..2ad6944 100644 --- a/test/index.js +++ b/test/index.js @@ -126,38 +126,57 @@ describe('preconf', () => { }; let Child = spy( () => null ); - let opts = { deepMerge: true }; - let configure = preconf(null, defaults, opts); + // let opts = { deepMerge: true }; + // let configure = preconf(null, defaults, opts); - function test(config, selector) { + function test(config, selector, opts) { Child.reset(); - let Wrapped = configure(selector)(Child); + let Wrapped = preconf(null, defaults, opts)(selector)(Child); rndr(); } it('should pass defaults', () => { test(undefined, ['a', 'c']); - expect(Child).to.have.been.calledWithMatch({ a: 'b', c: { d: 'e', f: { g: 'h' } } }); + delete Child.args[0][0].children; + expect(Child).to.have.been.calledWith(defaults); }); it('should pass provided config values', () => { let config = { c: 'override', e: 'f' }; test(config, ['a', 'c', 'e']); - expect(Child).to.have.been.calledWithMatch({ a: 'b', c: 'override', e: 'f' }); + delete Child.args[0][0].children; + expect(Child).to.have.been.calledWith({ a: 'b', c: 'override', e: 'f' }); }); - it('should deep merge config values', () => { + it('should deep merge config values by default', () => { let config = { c: { f: { g: 'override' } }, e: 'f' }; test(config, ['a', 'c', 'e']); - expect(Child).to.have.been.calledWithMatch({ a: 'b', c: { d: 'e', f: { g: 'override' } }, e: 'f' }); + delete Child.args[0][0].children; + expect(Child).to.have.been.calledWith({ a: 'b', c: { d: 'e', f: { g: 'override' } }, e: 'f' }); config = { c: { d: 'override' }, e: 'f' }; test(config, ['a', 'c', 'e']); - expect(Child).to.have.been.calledWithMatch({ a: 'b', c: { d: 'override', f: { g: 'h' } }, e: 'f' }); + delete Child.args[0][0].children; + expect(Child).to.have.been.calledWith({ a: 'b', c: { d: 'override', f: { g: 'h' } }, e: 'f' }); + + }); + + it('should not deep merge config values when deep merge option set to false', () => { + let config = { c: { f: { g: 'override' } }, e: 'f' }; + + test(config, ['a', 'c', 'e'], { deepMerge: false }); + delete Child.args[0][0].children; + expect(Child).to.have.been.calledWith({ ...defaults, ...config }); + + config = { c: { d: 'override' }, e: 'f' }; + + test(config, ['a', 'c', 'e'], { deepMerge: false }); + delete Child.args[0][0].children; + expect(Child).to.have.been.calledWith({ ...defaults, ...config }); }); From 910476ea366cca5a1d00becf884d908368920910 Mon Sep 17 00:00:00 2001 From: Aaron Peltz Date: Thu, 8 Feb 2018 15:17:41 -0800 Subject: [PATCH 3/4] deepMerge accepts array of keys for mergeProps option; adds yieldToContext option --- README.md | 67 ++++++++++++++++++++++++++++++++------- src/index.js | 17 ++++++---- test/index.js | 86 +++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 147 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 619af56..9401732 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ render( **Collisions** -In the event that a configuration field is present as a mergeable object in both `context.config` and the default configuration object provided to `preconf`, the resulting prop passed to the enhanced component will be a merged object. +In the event that a configuration field is present as a mergeable object in both `context.config` and the default configuration object provided to `preconf`, the resulting prop passed to the enhanced component will be a merged object, with the configuration value from `context` overriding the value from the default configuration for keys that cannot be merged. Default behavior: @@ -66,14 +66,35 @@ render( // Foo receives prop `name` as merged object: { first: 'Bob', last: 'Jones'} ``` -To prevent the default behavior and override the provided default mergeable object with the mergeable object from `context.config`, `deepMerge` in the `options` parameter can be set to `false`. +To prevent the default behavior and allow values in the default mergeable object to take precedence over values from the mergeable object from `context.config`, `yieldToContext` in the `options` parameter can be set to `false`. + +Override value from `context.config` with value from default: + +```js +const defaults = { name: { first: 'Bob', } }; +// Additional options parameter passed with yieldToContext set to false +let configure = preconf(null, defaults, { yieldToContext: false }); + +let FooWithDefaults = configure('name')(Foo) + +render( + + + +) +// Foo receives prop `name` as unmerged object: { first: 'Bob'} +``` + +To prevent the default behavior and prevent deep-merging mergeable objects, `mergeProps` in the `options` parameter can be set to `false`. + +_Note: precedence will be given to the object in `context` unless `yieldToContext` in `options` is set to `false`._ Override with value from `context.config`: ```js const defaults = { name: { first: 'Bob', } }; -// Additional options parameter passed with deepMerge set to false -let configure = preconf(null, defaults, { deepMerge: false }); +// Additional options parameter passed with mergeProps set to false +let configure = preconf(null, defaults, { mergeProps: false }); let FooWithDefaults = configure('name')(Foo) @@ -85,6 +106,29 @@ render( // Foo receives prop `name` as unmerged object: { last: 'Jones'} ``` +To prevent the default behavior and override only specific top-level keys, `mergeProps` in the `options` parameter can be set to an `Array` of key names, where only the keys present in the `mergeProps` `Array` will be merged. + +_Note: Selective key-merging only applies to top-level keys; nested keys of the same name are merged._ + +_Note: Can be used in combination with `yieldToContext` to determine precedence in merging select top-level keys._ + +Override only specific top-level keys: + +```js +const defaults = { name: { first: 'Bob', }, location: { city: 'Hamburg' } }; +// Additional options parameter passed with mergeProps set to an Array of keys to be merged +let configure = preconf(null, defaults, { mergeProps: ['location'] }); + +let FooWithDefaults = configure('name, location')(Foo) + +render( + + + +) +// Foo receives prop `name` as unmerged object: { last: 'Jones'}, and prop `location` as merged object: { country: 'Germany', city: 'Hamburg' } +``` + ## API @@ -95,10 +139,11 @@ Creates a higher order component that provides values from configuration as prop **Parameters** -- `namespace` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)?** If provided, exposes `defaults` under a `namespace` -- `defaults` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)?** An object containing default configuration values -- `options` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)?** An object containing options for resolving configuration values (optional, default `{}`) - - `options.deepMerge` (optional, default `true`) +- `namespace` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** If provided, exposes `defaults` under a `namespace` +- `defaults` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)?** An object containing default configuration values +- `options` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)?** An object containing options for resolving configuration values (optional, default `{}`) + - `options.mergeProps` **([Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean) \| [Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array))?** A boolean indicating whether props should be merged, or an array indicating which keys in props should be merged (optional, default `true`) + - `options.yieldToContext` **[Boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)?** A boolean where false indicates that the default values should override those from context, else values in context take precedence (optional, default `true`) **Examples** @@ -132,7 +177,7 @@ export default configure({ ); ``` -Returns **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** [configure()](#configure) +Returns **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** [configure()](#configure) #### configure @@ -140,6 +185,6 @@ Creates a Higher Order Component that provides configuration as props. **Parameters** -- `keys` **([Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) \| [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)<[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)>)** An object where the keys are prop names to pass down and values are dot-notated keypaths corresponding to values in configuration. If a string or array, prop names are inferred from configuration keys. +- `keys` **([Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object) \| [Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)>)** An object where the keys are prop names to pass down and values are dot-notated keypaths corresponding to values in configuration. If a string or array, prop names are inferred from configuration keys. -Returns **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** configureComponent(Component) -> Component +Returns **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** configureComponent(Component) -> Component diff --git a/src/index.js b/src/index.js index 3024fac..6a844a2 100644 --- a/src/index.js +++ b/src/index.js @@ -3,9 +3,11 @@ import delve from 'dlv'; import merge from 'deepmerge'; /** Creates a higher order component that provides values from configuration as props. - * @param {String} [namespace] If provided, exposes `defaults` under a `namespace` - * @param {Object} [defaults] An object containing default configuration values - * @param {Object} [options] An object containing options for resolving configuration values + * @param {String} [namespace] If provided, exposes `defaults` under a `namespace` + * @param {Object} [defaults] An object containing default configuration values + * @param {Object} [options] An object containing options for resolving configuration values + * @param {Boolean|Array} [options.mergeProps] A boolean indicating whether props should be merged, or an array indicating which keys in props should be merged + * @param {Boolean} [options.yieldToContext] A boolean where false indicates that the default values should override those from context, else values in context take precedence * @returns {Function} [configure()](#configure) * * @example @@ -34,7 +36,8 @@ import merge from 'deepmerge'; * Location: {`${props.headquarters.city}, ${props.headquarters.country}`} * ); */ -export default function preconf(namespace, defaults, { deepMerge=true }={}) { +export default function preconf(namespace, defaults, { mergeProps=true, yieldToContext=true }={}) { + if (namespace) defaults = { [namespace]: defaults }; /** Creates a Higher Order Component that provides configuration as props. @@ -57,12 +60,14 @@ export default function preconf(namespace, defaults, { deepMerge=true }={}) { let inheritedVal = delve(context, 'config.'+path); let defaultVal = delve(defaults, path); + let deepMerge = Array.isArray(mergeProps) ? ~mergeProps.indexOf(path) : mergeProps; + if (deepMerge && inheritedVal && defaultVal && typeof inheritedVal === 'object' && typeof defaultVal === 'object') { - props[key] = merge(defaultVal, inheritedVal); + props[key] = yieldToContext ? merge(defaultVal, inheritedVal) : merge(inheritedVal, defaultVal); } else if (typeof props[key]==='undefined' || props[key]===null) { - props[key] = inheritedVal || defaultVal; + props[key] = yieldToContext ? inheritedVal || defaultVal : defaultVal || inheritedVal; } } return h(Child, props); diff --git a/test/index.js b/test/index.js index 2ad6944..c35e8d4 100644 --- a/test/index.js +++ b/test/index.js @@ -119,15 +119,13 @@ describe('preconf', () => { }); }); - describe('opts.deepMerge', () => { + describe('opts.mergeProps', () => { let defaults = { a: 'b', c: { d: 'e', f: { g: 'h' } } }; let Child = spy( () => null ); - // let opts = { deepMerge: true }; - // let configure = preconf(null, defaults, opts); function test(config, selector, opts) { Child.reset(); @@ -165,21 +163,97 @@ describe('preconf', () => { }); - it('should not deep merge config values when deep merge option set to false', () => { + it('should not deep merge config values when merge props option set to false', () => { let config = { c: { f: { g: 'override' } }, e: 'f' }; - test(config, ['a', 'c', 'e'], { deepMerge: false }); + test(config, ['a', 'c', 'e'], { mergeProps: false }); delete Child.args[0][0].children; expect(Child).to.have.been.calledWith({ ...defaults, ...config }); config = { c: { d: 'override' }, e: 'f' }; - test(config, ['a', 'c', 'e'], { deepMerge: false }); + test(config, ['a', 'c', 'e'], { mergeProps: false }); delete Child.args[0][0].children; expect(Child).to.have.been.calledWith({ ...defaults, ...config }); }); + it('should merge only select keys when merge props opt is provided Array of keys to merge', () => { + let config = { a: 'override', c: { f: { g: 'override' } }, e: 'f' }; + + test(config, ['a', 'c', 'e'], { mergeProps: ['c'] }); + delete Child.args[0][0].children; + expect(Child).to.have.been.calledWith({ a: 'override', c: { d: 'e', f: { g: 'override' } }, e: 'f' }); + + config = { c: { d: 'override' }, e: 'f' }; + + test(config, ['a', 'c', 'e'], { mergeProps: [] }); + delete Child.args[0][0].children; + expect(Child).to.have.been.calledWith({ ...defaults, ...config }); + + }); + + }); + + describe('opts.yieldToContext', () => { + let defaults = { + a: 'b', + c: { d: 'e', f: { g: 'h' } } + }; + + let Child = spy( () => null ); + + function test(config, selector, opts) { + Child.reset(); + let Wrapped = preconf(null, defaults, opts)(selector)(Child); + rndr(); + } + + it('should override config from context when default config given precedence', () => { + let config = { c: { f: { g: 'override' } }, e: 'f' }; + + test(config, ['a', 'c', 'e'], { yieldToContext: false }); + delete Child.args[0][0].children; + expect(Child).to.have.been.calledWith({ a: 'b', c: { d: 'e', f: { g: 'h' } }, e: 'f' }); + + config = { a: 'override', c: { d: 'override' }, e: 'f' }; + + test(config, ['a', 'c', 'e'], { yieldToContext: false }); + delete Child.args[0][0].children; + expect(Child).to.have.been.calledWith({ a: 'b', c: { d: 'e', f: { g: 'h' } }, e: 'f' }); + + }); + + it('should not override default configs when merge props option set to false', () => { + let config = { c: { f: { g: 'override' } }, e: 'f' }; + + test(config, ['a', 'c', 'e'], { mergeProps: false, yieldToContext: false }); + delete Child.args[0][0].children; + expect(Child).to.have.been.calledWith({ ...config, ...defaults }); + + config = { c: { d: 'override' }, e: 'f' }; + + test(config, ['a', 'c', 'e'], { mergeProps: false, yieldToContext: false }); + delete Child.args[0][0].children; + expect(Child).to.have.been.calledWith({ ...config, ...defaults }); + + }); + + it('should merge only select keys when merge props opt is provided Array of keys to merge', () => { + let config = { a: 'override', c: { f: { g: 'override' } }, e: 'f' }; + + test(config, ['a', 'c', 'e'], { mergeProps: ['c'], yieldToContext: false }); + delete Child.args[0][0].children; + expect(Child).to.have.been.calledWith({ a: 'b', c: { d: 'e', f: { g: 'h' } }, e: 'f' }); + + config = { c: { d: 'override' }, e: 'f' }; + + test(config, ['a', 'c', 'e'], { mergeProps: [], yieldToContext: false }); + delete Child.args[0][0].children; + expect(Child).to.have.been.calledWith({ ...config, ...defaults }); + + }); + }); }); From 8bbf895ca4ed76168217c5df4dc4bfe5bd5f7744 Mon Sep 17 00:00:00 2001 From: Aaron Peltz Date: Sat, 3 Mar 2018 13:20:05 -0800 Subject: [PATCH 4/4] deepMerge additional test cases for deep merge; corrects function docblock example --- README.md | 34 +++++++++++------------ src/index.js | 20 ++++++------- test/index.js | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 9401732..9df08fc 100644 --- a/README.md +++ b/README.md @@ -66,14 +66,16 @@ render( // Foo receives prop `name` as merged object: { first: 'Bob', last: 'Jones'} ``` -To prevent the default behavior and allow values in the default mergeable object to take precedence over values from the mergeable object from `context.config`, `yieldToContext` in the `options` parameter can be set to `false`. +To prevent the default behavior and prevent deep-merging mergeable objects, `mergeProps` in the `options` parameter can be set to `false`. -Override value from `context.config` with value from default: +_Note: precedence will be given to the object in `context` unless `yieldToContext` in `options` is set to `false`._ + +Override with value from `context.config`: ```js const defaults = { name: { first: 'Bob', } }; -// Additional options parameter passed with yieldToContext set to false -let configure = preconf(null, defaults, { yieldToContext: false }); +// Additional options parameter passed with mergeProps set to false +let configure = preconf(null, defaults, { mergeProps: false }); let FooWithDefaults = configure('name')(Foo) @@ -82,19 +84,17 @@ render( ) -// Foo receives prop `name` as unmerged object: { first: 'Bob'} +// Foo receives prop `name` as unmerged object: { last: 'Jones'} ``` -To prevent the default behavior and prevent deep-merging mergeable objects, `mergeProps` in the `options` parameter can be set to `false`. - -_Note: precedence will be given to the object in `context` unless `yieldToContext` in `options` is set to `false`._ +To prevent the default behavior and allow values in the default mergeable object to take precedence over values from the mergeable object from `context.config`, `yieldToContext` in the `options` parameter can be set to `false`. -Override with value from `context.config`: +Override value from `context.config` with value from default: ```js const defaults = { name: { first: 'Bob', } }; -// Additional options parameter passed with mergeProps set to false -let configure = preconf(null, defaults, { mergeProps: false }); +// Additional options parameter passed with yieldToContext set to false +let configure = preconf(null, defaults, { mergeProps: false, yieldToContext: false }); let FooWithDefaults = configure('name')(Foo) @@ -103,7 +103,7 @@ render( ) -// Foo receives prop `name` as unmerged object: { last: 'Jones'} +// Foo receives prop `name` as unmerged object: { first: 'Bob'} ``` To prevent the default behavior and override only specific top-level keys, `mergeProps` in the `options` parameter can be set to an `Array` of key names, where only the keys present in the `mergeProps` `Array` will be merged. @@ -169,11 +169,11 @@ export default configure({ ``` ```javascript -let configure = preconf('locations', { headquarters: { country: 'Germany' } }); -export default configure({ - headquarters: { city: 'Hamburg' } -})( ({ headquarters }) => - Location: {`${props.headquarters.city}, ${props.headquarters.country}`} +// context.config = { location: { city: 'Hamburg' }} + +let configure = preconf(null, { location: { country: 'Germany' } }); +export default configure('location')( (props) => + Location: {`${props.location.city}, ${props.location.country}`} ); ``` diff --git a/src/index.js b/src/index.js index 6a844a2..4a2c413 100644 --- a/src/index.js +++ b/src/index.js @@ -3,11 +3,11 @@ import delve from 'dlv'; import merge from 'deepmerge'; /** Creates a higher order component that provides values from configuration as props. - * @param {String} [namespace] If provided, exposes `defaults` under a `namespace` - * @param {Object} [defaults] An object containing default configuration values - * @param {Object} [options] An object containing options for resolving configuration values - * @param {Boolean|Array} [options.mergeProps] A boolean indicating whether props should be merged, or an array indicating which keys in props should be merged - * @param {Boolean} [options.yieldToContext] A boolean where false indicates that the default values should override those from context, else values in context take precedence + * @param {String} [namespace] If provided, exposes `defaults` under a `namespace` + * @param {Object} [defaults] An object containing default configuration values + * @param {Object} [options] An object containing options for resolving configuration values + * @param {Boolean|Array} [options.mergeProps] A boolean indicating whether props should be merged, or an array indicating which keys in props should be merged + * @param {Boolean} [options.yieldToContext] A boolean where false indicates that the default values should override those from context, else values in context take precedence * @returns {Function} [configure()](#configure) * * @example @@ -29,11 +29,11 @@ import merge from 'deepmerge'; * ); * * @example - * let configure = preconf('locations', { headquarters: { country: 'Germany' } }); - * export default configure({ - * headquarters: { city: 'Hamburg' } - * })( ({ headquarters }) => - * Location: {`${props.headquarters.city}, ${props.headquarters.country}`} + * // context.config = { location: { city: 'Hamburg' }} + * + * let configure = preconf(null, { location: { country: 'Germany' } }); + * export default configure('location')( (props) => + * Location: {`${props.location.city}, ${props.location.country}`} * ); */ export default function preconf(namespace, defaults, { mergeProps=true, yieldToContext=true }={}) { diff --git a/test/index.js b/test/index.js index c35e8d4..a81a051 100644 --- a/test/index.js +++ b/test/index.js @@ -256,5 +256,82 @@ describe('preconf', () => { }); + describe('namespacing', () => { + + it('should pass the correct props when no namespace is provided - prop name specified as string', () => { + let Child = spy( () => null ); + let configure = preconf(null, { headquarters: { city: 'Hamburg' } }); + let Wrapped = configure('headquarters')(Child); + + let config = { headquarters: { country: 'Germany' } }; + rndr(); + + delete Child.args[0][0].children; + expect(Child).to.have.been.calledWith({ headquarters: { city: 'Hamburg', country: 'Germany' } }); + }); + + it('should pass the correct props when no namespace is provided - prop name specified as object', () => { + let Child = spy( () => null ); + let configure = preconf(null, { headquarters: { city: 'Hamburg' } }); + let Wrapped = configure({ location: 'headquarters' })(Child); + + let config = { headquarters: { country: 'Germany' } }; + rndr(); + + delete Child.args[0][0].children; + expect(Child).to.have.been.calledWith({ location: { city: 'Hamburg', country: 'Germany' } }); + }); + + it('should pass the correct props for provided namespace', () => { + let Child = spy( () => null ); + let configure = preconf('hq', { headquarters: { city: 'Hamburg' } }); + let Wrapped = configure({ location: 'hq.headquarters' })(Child); + + let config = { headquarters: { country: 'Germany' } }; + rndr(); + + delete Child.args[0][0].children; + expect(Child).to.have.been.calledWith({ location: { city: 'Hamburg' } }); + }); + + it('should pass the correct props for conflicting namespace - opts === default', () => { + let Child = spy( () => null ); + let configure = preconf('hq', { headquarters: { city: 'Hamburg' } }); + let Wrapped = configure({ location: 'hq.headquarters' })(Child); + + let config = { hq: { headquarters: { country: 'Germany' } } }; + rndr(); + + delete Child.args[0][0].children; + expect(Child).to.have.been.calledWith({ location: { city: 'Hamburg', country: 'Germany' } }); + }); + + it('should pass the correct props for conflicting namespace - opts === { mergeProps: false }', () => { + let Child = spy( () => null ); + let configure = preconf('hq', { headquarters: { city: 'Hamburg' } }, { mergeProps: false }); + let Wrapped = configure({ location: 'hq.headquarters' })(Child); + + let config = { hq: { headquarters: { country: 'Germany' } } }; + rndr(); + + delete Child.args[0][0].children; + expect(Child).to.have.been.calledWith({ location: { country: 'Germany' } }); + }); + + it('should pass the correct props for conflicting namespace - opts === { mergeProps: false, yieldToContext: false }', () => { + let Child = spy( () => null ); + let configure = preconf('hq', { headquarters: { city: 'Hamburg' } }, { mergeProps: false, yieldToContext: false }); + let Wrapped = configure({ location: 'hq.headquarters' })(Child); + + let config = { hq: { headquarters: { country: 'Germany' } } }; + rndr(); + + delete Child.args[0][0].children; + expect(Child).to.have.been.calledWith({ location: { city: 'Hamburg' } }); + }); + + + }); + }); });