Skip to content

Commit

Permalink
feat(factories): support functions (#2790)
Browse files Browse the repository at this point in the history
  • Loading branch information
levithomason authored May 11, 2018
1 parent 8b52350 commit 6a22434
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 76 deletions.
48 changes: 30 additions & 18 deletions src/lib/factories.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,22 @@ export function createShorthand(Component, mapValueToProps, val, options = {}) {

const valIsString = _.isString(val)
const valIsNumber = _.isNumber(val)

const isReactElement = isValidElement(val)
const isPropsObject = _.isPlainObject(val)
const isPrimitiveValue = valIsString || valIsNumber || _.isArray(val)
const valIsFunction = _.isFunction(val)
const valIsReactElement = isValidElement(val)
const valIsPropsObject = _.isPlainObject(val)
const valIsPrimitiveValue = valIsString || valIsNumber || _.isArray(val)

// unhandled type return null
/* eslint-disable no-console */
if (!isReactElement && !isPropsObject && !isPrimitiveValue) {
if (!valIsFunction && !valIsReactElement && !valIsPropsObject && !valIsPrimitiveValue) {
if (process.env.NODE_ENV !== 'production') {
console.error([
'Shorthand value must be a string|number|array|object|ReactElement.',
' Use null|undefined|boolean for none',
` Received ${typeof val}.`,
].join(''))
console.error(
[
'Shorthand value must be a string|number|array|object|ReactElement|function.',
' Use null|undefined|boolean for none',
` Received ${typeof val}.`,
].join(''),
)
}
return null
}
Expand All @@ -51,21 +53,28 @@ export function createShorthand(Component, mapValueToProps, val, options = {}) {
const { defaultProps = {} } = options

// User's props
const usersProps = (isReactElement && val.props)
|| (isPropsObject && val)
|| (isPrimitiveValue && mapValueToProps(val))
const usersProps =
(valIsReactElement && val.props) ||
(valIsPropsObject && val) ||
(valIsPrimitiveValue && mapValueToProps(val))

// Override props
let { overrideProps = {} } = options
overrideProps = _.isFunction(overrideProps) ? overrideProps({ ...defaultProps, ...usersProps }) : overrideProps
overrideProps = _.isFunction(overrideProps)
? overrideProps({ ...defaultProps, ...usersProps })
: overrideProps

// Merge props
/* eslint-disable react/prop-types */
const props = { ...defaultProps, ...usersProps, ...overrideProps }

// Merge className
if (defaultProps.className || overrideProps.className || usersProps.className) {
const mergedClassesNames = cx(defaultProps.className, overrideProps.className, usersProps.className)
const mergedClassesNames = cx(
defaultProps.className,
overrideProps.className,
usersProps.className,
)
props.className = _.uniq(mergedClassesNames.split(' ')).join(' ')
}

Expand All @@ -91,17 +100,20 @@ export function createShorthand(Component, mapValueToProps, val, options = {}) {
props.key = val
}
}
/* eslint-enable react/prop-types */

// ----------------------------------------
// Create Element
// ----------------------------------------

// Clone ReactElements
if (isReactElement) return cloneElement(val, props)
if (valIsReactElement) return cloneElement(val, props)

// Create ReactElements from built up props
if (isPrimitiveValue || isPropsObject) return <Component {...props} />
if (valIsPrimitiveValue || valIsPropsObject) return <Component {...props} />

// Call functions with args similar to createElement()
if (valIsFunction) return val(Component, props, props.children)
/* eslint-enable react/prop-types */
}

// ============================================================
Expand Down
146 changes: 88 additions & 58 deletions test/specs/lib/factories-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ const itMergesClassNames = (classNameSource, extraClassName, shorthandConfig) =>
const defaultProps = { className: 'default' }
const overrideProps = { className: 'override' }

shallow(getShorthand({ defaultProps, overrideProps, ...shorthandConfig }))
.should.have.same.className(`default override ${extraClassName}`)
shallow(
getShorthand({ defaultProps, overrideProps, ...shorthandConfig }),
).should.have.same.className(`default override ${extraClassName}`)
})
}

Expand Down Expand Up @@ -170,61 +171,49 @@ describe('factories', () => {

describe('key', () => {
it('is not consumed', () => {
getShorthand({ value: { key: 123 } })
.props.should.have.property('key')
getShorthand({ value: { key: 123 } }).props.should.have.property('key')
})

describe('on an element', () => {
it('works with a string', () => {
getShorthand({ value: <div key='foo' /> })
.should.have.property('key', 'foo')
getShorthand({ value: <div key='foo' /> }).should.have.property('key', 'foo')
})

it('works with a number', () => {
getShorthand({ value: <div key={123} /> })
.should.have.property('key', '123')
getShorthand({ value: <div key={123} /> }).should.have.property('key', '123')
})

it('works with falsy values', () => {
getShorthand({ value: <div key={null} /> })
.should.have.property('key', 'null')
getShorthand({ value: <div key={null} /> }).should.have.property('key', 'null')

getShorthand({ value: <div key={0} /> })
.should.have.property('key', '0')
getShorthand({ value: <div key={0} /> }).should.have.property('key', '0')

getShorthand({ value: <div key='' /> })
.should.have.property('key', '')
getShorthand({ value: <div key='' /> }).should.have.property('key', '')
})
})

describe('on an object', () => {
it('works with a string', () => {
getShorthand({ value: { key: 'foo' } })
.should.have.property('key', 'foo')
getShorthand({ value: { key: 'foo' } }).should.have.property('key', 'foo')
})

it('works with a number', () => {
getShorthand({ value: { key: 123 } })
.should.have.property('key', '123')
getShorthand({ value: { key: 123 } }).should.have.property('key', '123')
})

it('works with falsy values', () => {
getShorthand({ value: { key: null } })
.should.have.property('key', 'null')
getShorthand({ value: { key: null } }).should.have.property('key', 'null')

getShorthand({ value: { key: 0 } })
.should.have.property('key', '0')
getShorthand({ value: { key: 0 } }).should.have.property('key', '0')

getShorthand({ value: { key: '' } })
.should.have.property('key', '')
getShorthand({ value: { key: '' } }).should.have.property('key', '')
})
})
})

describe('childKey', () => {
it('is consumed', () => {
getShorthand({ value: { childKey: 123 } })
.props.should.not.have.property('childKey')
getShorthand({ value: { childKey: 123 } }).props.should.not.have.property('childKey')
})

it('is called with the final `props` if it is a function', () => {
Expand All @@ -239,47 +228,37 @@ describe('factories', () => {

describe('on an element', () => {
it('works with a string', () => {
getShorthand({ value: <div childKey='foo' /> })
.should.have.property('key', 'foo')
getShorthand({ value: <div childKey='foo' /> }).should.have.property('key', 'foo')
})

it('works with a number', () => {
getShorthand({ value: <div childKey={123} /> })
.should.have.property('key', '123')
getShorthand({ value: <div childKey={123} /> }).should.have.property('key', '123')
})

it('works with falsy values', () => {
getShorthand({ value: <div childKey={null} /> })
.should.have.property('key', null)
getShorthand({ value: <div childKey={null} /> }).should.have.property('key', null)

getShorthand({ value: <div childKey={0} /> })
.should.have.property('key', '0')
getShorthand({ value: <div childKey={0} /> }).should.have.property('key', '0')

getShorthand({ value: <div childKey='' /> })
.should.have.property('key', '')
getShorthand({ value: <div childKey='' /> }).should.have.property('key', '')
})
})

describe('on an object', () => {
it('works with a string', () => {
getShorthand({ value: { childKey: 'foo' } })
.should.have.property('key', 'foo')
getShorthand({ value: { childKey: 'foo' } }).should.have.property('key', 'foo')
})

it('works with a number', () => {
getShorthand({ value: { childKey: 123 } })
.should.have.property('key', '123')
getShorthand({ value: { childKey: 123 } }).should.have.property('key', '123')
})

it('works with falsy values', () => {
getShorthand({ value: { childKey: null } })
.should.have.property('key', null)
getShorthand({ value: { childKey: null } }).should.have.property('key', null)

getShorthand({ value: { childKey: 0 } })
.should.have.property('key', '0')
getShorthand({ value: { childKey: 0 } }).should.have.property('key', '0')

getShorthand({ value: { childKey: '' } })
.should.have.property('key', '')
getShorthand({ value: { childKey: '' } }).should.have.property('key', '')
})
})
})
Expand Down Expand Up @@ -376,10 +355,14 @@ describe('factories', () => {
mapValueToProps: () => ({ className: 'mapped' }),
})

itAppliesProps('mapValueToProps', { 'data-prop': 'present' }, {
value: 'foo',
mapValueToProps: () => ({ 'data-prop': 'present' }),
})
itAppliesProps(
'mapValueToProps',
{ 'data-prop': 'present' },
{
value: 'foo',
mapValueToProps: () => ({ 'data-prop': 'present' }),
},
)

itOverridesDefaultProps(
'mapValueToProps',
Expand Down Expand Up @@ -419,6 +402,45 @@ describe('factories', () => {
})
})

describe('from a function', () => {
itReturnsAValidElement(() => <div />)
itDoesNotIncludePropsFromMapValueToProps(() => <div />)

it('is called once', () => {
const spy = sandbox.spy()

getShorthand({ value: spy })

spy.should.have.been.calledOnce()
})

it('is called with Component, props, children', () => {
const spy = sandbox.spy(() => <div />)

getShorthand({ Component: 'p', value: spy })

spy.should.have.been.calledWithExactly('p', {}, undefined)
})

it('receives defaultProps in its props argument', () => {
const spy = sandbox.spy(() => <div />)
const defaultProps = { defaults: true }

getShorthand({ Component: 'p', defaultProps, value: spy })

spy.should.have.been.calledWithExactly('p', defaultProps, undefined)
})

it('receives overrideProps in its props argument', () => {
const spy = sandbox.spy(() => <div />)
const overrideProps = { overrides: true }

getShorthand({ Component: 'p', overrideProps, value: spy })

spy.should.have.been.calledWithExactly('p', overrideProps, undefined)
})
})

describe('from an array', () => {
itReturnsAValidElement(['foo'])
itAppliesDefaultProps(['foo'])
Expand All @@ -427,10 +449,14 @@ describe('factories', () => {
mapValueToProps: () => ({ className: 'mapped' }),
})

itAppliesProps('mapValueToProps', { 'data-prop': 'present' }, {
value: ['foo'],
mapValueToProps: () => ({ 'data-prop': 'present' }),
})
itAppliesProps(
'mapValueToProps',
{ 'data-prop': 'present' },
{
value: ['foo'],
mapValueToProps: () => ({ 'data-prop': 'present' }),
},
)

itOverridesDefaultProps(
'mapValueToProps',
Expand All @@ -455,31 +481,35 @@ describe('factories', () => {
const overrideProps = { style: { right: 5 } }

shallow(getShorthand({ defaultProps, overrideProps, value: userProps }))
.should.have.prop('style').deep.equal({ left: 5, bottom: 5, right: 5 })
.should.have.prop('style')
.deep.equal({ left: 5, bottom: 5, right: 5 })
})

it('merges style prop and handles override by userProps', () => {
const defaultProps = { style: { left: 10, bottom: 5 } }
const userProps = { style: { bottom: 10 } }

shallow(getShorthand({ defaultProps, value: userProps }))
.should.have.prop('style').deep.equal({ left: 10, bottom: 10 })
.should.have.prop('style')
.deep.equal({ left: 10, bottom: 10 })
})

it('merges style prop and handles override by overrideProps', () => {
const userProps = { style: { bottom: 10, right: 5 } }
const overrideProps = { style: { right: 10 } }

shallow(getShorthand({ overrideProps, value: userProps }))
.should.have.prop('style').deep.equal({ bottom: 10, right: 10 })
.should.have.prop('style')
.deep.equal({ bottom: 10, right: 10 })
})

it('merges style prop from defaultProps and overrideProps', () => {
const defaultProps = { style: { left: 10, bottom: 5 } }
const overrideProps = { style: { bottom: 10 } }

shallow(getShorthand({ defaultProps, overrideProps, value: 'foo' }))
.should.have.prop('style').deep.equal({ left: 10, bottom: 10 })
.should.have.prop('style')
.deep.equal({ left: 10, bottom: 10 })
})
})
})
Expand Down

0 comments on commit 6a22434

Please sign in to comment.