Skip to content

Commit

Permalink
feat(factories): allow deafultProps function
Browse files Browse the repository at this point in the history
  • Loading branch information
levithomason committed Sep 27, 2016
1 parent 15f89ce commit adac980
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 69 deletions.
53 changes: 27 additions & 26 deletions src/elements/Input/Input.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const htmlInputPropNames = [
// Limited HTML props
'autoFocus',
'checked',
'disabled',
// 'disabled', do not pass (duplicates SUI CSS opacity rule)
'form',
'max',
'maxLength',
Expand All @@ -40,7 +40,10 @@ export const htmlInputPropNames = [

/**
* An Input is a field used to elicit a response from a user
* @see Button
* @see Form
* @see Icon
* @see Label
*/
function Input(props) {
const {
Expand All @@ -59,6 +62,7 @@ function Input(props) {
labelPosition,
loading,
size,
type,
input,
transparent,
} = props
Expand Down Expand Up @@ -88,26 +92,28 @@ function Input(props) {
return <ElementType {...rest} className={classes}>{children}</ElementType>
}

const actionElement = Button.create(action, {
// all action components should have the button className
className: 'button',
})
const actionElement = Button.create(action, elProps => ({
className: cx(
// all action components should have the button className
!_.includes(elProps.className, 'button') && 'button',
),
}))
const iconElement = Icon.create(icon)
const labelElement = Label.create(label, {
const labelElement = Label.create(label, elProps => ({
className: cx(
// all label components should have the label className
'label',
!_.includes(elProps.className, 'label') && 'label',
// add 'left|right corner'
_.includes(labelPosition, 'corner') && labelPosition,
),
})
}))

return (
<ElementType {...rest} className={classes}>
{actionPosition === 'left' && actionElement}
{iconPosition === 'left' && iconElement}
{labelPosition !== 'right' && labelElement}
{createHTMLInput(input, inputProps)}
{createHTMLInput(input || type, inputProps)}
{actionPosition !== 'left' && actionElement}
{iconPosition !== 'left' && iconElement}
{labelPosition === 'right' && labelElement}
Expand All @@ -127,7 +133,7 @@ Input._meta = {
}

Input.defaultProps = {
input: 'text',
type: 'text',
}

Input.propTypes = {
Expand All @@ -152,7 +158,11 @@ Input.propTypes = {

/** Primary content. Used when there are multiple Labels or multiple Actions. */
children: customPropTypes.every([
customPropTypes.disallow(['action', 'icon', 'input', 'label']),
customPropTypes.disallow(['icon', 'input', 'label']),
customPropTypes.givenProps(
{ action: PropTypes.oneOfType([PropTypes.string, PropTypes.object, PropTypes.element]).isRequired },
customPropTypes.disallow(['action']),
),
PropTypes.node,
]),

Expand All @@ -176,11 +186,7 @@ Input.propTypes = {
PropTypes.bool,
customPropTypes.every([
customPropTypes.disallow(['children']),
PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
PropTypes.element,
]),
PropTypes.oneOfType([PropTypes.string, PropTypes.object, PropTypes.element]),
]),
]),

Expand All @@ -196,21 +202,13 @@ Input.propTypes = {
/** Shorthand prop for creating the HTML Input */
input: customPropTypes.every([
customPropTypes.disallow(['children']),
PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
PropTypes.element,
]),
PropTypes.oneOfType([PropTypes.string, PropTypes.object, PropTypes.element]),
]),

/** Optional Label to display along side the Input */
label: customPropTypes.every([
customPropTypes.disallow(['children']),
PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
PropTypes.element,
]),
PropTypes.oneOfType([PropTypes.string, PropTypes.object, PropTypes.element]),
]),

/** A Label can appear outside an Input on the left or right */
Expand All @@ -224,6 +222,9 @@ Input.propTypes = {

/** Transparent Input has no background */
transparent: PropTypes.bool,

/** The HTML input type */
type: PropTypes.string,
}

export default Input
34 changes: 25 additions & 9 deletions src/lib/factories.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,39 @@ const mergePropsAndClassName = (defaultProps, props) => {
* @param {function|string} Component A ReactClass or string
* @param {function} mapValueToProps A function that maps a primitive value to the Component props
* @param {string|object|function} val The value to create a ReactElement from
* @param {object} defaultProps Default props to add to the final ReactElement
* @param {object|function} [defaultProps] Default props object or function (called with regular props).
* @returns {function|null}
*/
export function createShorthand(Component, mapValueToProps, val, defaultProps = {}) {
// Clone ReactElements
export function createShorthand(Component, mapValueToProps, val, defaultProps) {
// short circuit for disabling shorthand
if (val === null) return null

let type
let usersProps = {}

if (isValidElement(val)) {
return React.cloneElement(val, mergePropsAndClassName(defaultProps, val.props))
type = 'element'
usersProps = val.props
} else if (_.isPlainObject(val)) {
type = 'props'
usersProps = val
} else if (_.isString(val) || _.isNumber(val)) {
type = 'literal'
usersProps = mapValueToProps(val)
}

// Create ReactElements from props objects
if (_.isPlainObject(val)) {
return <Component {...mergePropsAndClassName(defaultProps, val)} />
defaultProps = _.isFunction(defaultProps) ? defaultProps(usersProps) : defaultProps
const props = mergePropsAndClassName({ ...defaultProps }, usersProps)

// Clone ReactElements
if (type === 'element') {
return React.cloneElement(val, props)
}

// Create ReactElements from props objects
// Map values to props and create a ReactElement
if (_.isString(val) || _.isNumber(val)) {
return <Component {...mergePropsAndClassName(mapValueToProps(val), defaultProps)} />
if (type === 'props' || type === 'literal') {
return <Component {...props} />
}

// Otherwise null
Expand Down
10 changes: 6 additions & 4 deletions test/specs/collections/Breadcrumb/BreadcrumbDivider-test.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import cx from 'classnames'
import React from 'react'
import BreadcrumbDivider from 'src/collections/Breadcrumb/BreadcrumbDivider'

import * as common from 'test/specs/commonTests'
import BreadcrumbDivider from 'src/collections/Breadcrumb/BreadcrumbDivider'

describe('BreadcrumbDivider', () => {
common.isConformant(BreadcrumbDivider)
common.implementsIconProp(BreadcrumbDivider, {
requiredShorthandProps: {
className: 'divider',
},
shorthandDefaultProps: elProps => ({
className: cx(elProps.className, 'divider'),
}),
})
common.rendersChildren(BreadcrumbDivider)

Expand Down
64 changes: 46 additions & 18 deletions test/specs/commonTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,8 @@ export const implementsWidthProp = (Component, options = {}) => {
* @param {string|function} options.ShorthandComponent The component that should be rendered from the shorthand value.
* @param {function} options.mapValueToProps A function that maps a primitive value to the Component props
* @param {Object} [options.requiredProps={}] Props required to render the component.
* @param {Object} [options.requiredShorthandProps={}] Props required to render the shorthand component.
* @param {Object|function} [options.shorthandDefaultProps={}] Props required to render the shorthand component.
* @param {Object} [options.alwaysPresent] Whether or not the shorthand exists by default
*/
export const implementsShorthandProp = (Component, options = {}) => {
const { assertRequired } = commonTestHelpers('implementsShorthandProp', Component)
Expand All @@ -571,7 +572,8 @@ export const implementsShorthandProp = (Component, options = {}) => {
ShorthandComponent,
mapValueToProps,
requiredProps = {},
requiredShorthandProps = {},
shorthandDefaultProps = {},
alwaysPresent,
} = options

describe(`${propKey} shorthand prop (common)`, () => {
Expand All @@ -580,16 +582,18 @@ export const implementsShorthandProp = (Component, options = {}) => {
assertRequired(propKey, 'a `propKey`')
assertRequired(ShorthandComponent, 'a `ShorthandComponent`')

const name = typeof ShorthandComponent === 'string' ? ShorthandComponent : ShorthandComponent.name
const name = typeof ShorthandComponent === 'string'
? ShorthandComponent
: _.get(ShorthandComponent, '_meta.name') || ShorthandComponent.displayName || ShorthandComponent.name

const assertValidShorthand = (value) => {
const renderedShorthand = createShorthand(ShorthandComponent, mapValueToProps, value, requiredShorthandProps)
const renderedShorthand = createShorthand(ShorthandComponent, mapValueToProps, value, shorthandDefaultProps)
const element = createElement(Component, { ...requiredProps, [propKey]: value })

shallow(element).should.contain(renderedShorthand)
}

if (Component.defaultProps && Component.defaultProps[propKey]) {
if (alwaysPresent || Component.defaultProps && Component.defaultProps[propKey]) {
it(`has default ${name} when not defined`, () => {
shallow(<Component {...requiredProps} />)
.should.have.descendants(name)
Expand All @@ -603,10 +607,12 @@ export const implementsShorthandProp = (Component, options = {}) => {
})
}

it(`has no ${name} when null`, () => {
shallow(createElement(Component, { ...requiredProps, [propKey]: null }))
.should.not.have.descendants(ShorthandComponent)
})
if (!alwaysPresent) {
it(`has no ${name} when null`, () => {
shallow(createElement(Component, { ...requiredProps, [propKey]: null }))
.should.not.have.descendants(ShorthandComponent)
})
}

it(`renders a ${name} from strings`, () => {
consoleUtil.disableOnce()
Expand All @@ -625,7 +631,7 @@ export const implementsShorthandProp = (Component, options = {}) => {

it(`renders a ${name} from elements`, () => {
consoleUtil.disableOnce()
assertValidShorthand(<ShorthandComponent {...requiredShorthandProps} />)
assertValidShorthand(<ShorthandComponent />)
})
})
}
Expand All @@ -639,15 +645,15 @@ export const implementsShorthandProp = (Component, options = {}) => {
* @param {string|function} [options.ShorthandComponent] The component that should be rendered from the shorthand value.
* @param {function} [options.mapValueToProps] A function that maps a primitive value to the Component props
* @param {Object} [options.requiredProps={}] Props required to render the component.
* @param {Object} [options.requiredShorthandProps={}] Props required to render the shorthand component.
* @param {Object|function} [options.shorthandDefaultProps={}] Props required to render the shorthand component.
*/
export const implementsButtonProp = (Component, options = {}) => {
const opts = {
propKey: 'button',
ShorthandComponent: Button,
mapValueToProps: val => ({ content: val }),
requiredProps: {},
requiredShorthandProps: {},
shorthandDefaultProps: {},
...options,
}
implementsShorthandProp(Component, opts)
Expand All @@ -662,15 +668,37 @@ export const implementsButtonProp = (Component, options = {}) => {
* @param {string|function} [options.ShorthandComponent] The component that should be rendered from the shorthand value.
* @param {function} [options.mapValueToProps] A function that maps a primitive value to the Component props
* @param {Object} [options.requiredProps={}] Props required to render the component.
* @param {Object} [options.requiredShorthandProps={}] Props required to render the shorthand component.
* @param {Object|function} [options.shorthandDefaultProps={}] Props required to render the shorthand component.
*/
export const implementsIconProp = (Component, options = {}) => {
implementsShorthandProp(Component, {
propKey: 'icon',
ShorthandComponent: Icon,
mapValueToProps: val => ({ name: val }),
requiredProps: {},
requiredShorthandProps: {},
shorthandDefaultProps: {},
...options,
})
}

/**
* Assert that a Component correctly implements an HTML input shorthand prop.
*
* @param {function} Component The component to test.
* @param {object} [options={}]
* @param {string} [options.propKey='icon'] The name of the shorthand prop.
* @param {string|function} [options.ShorthandComponent] The component that should be rendered from the shorthand value.
* @param {function} [options.mapValueToProps] A function that maps a primitive value to the Component props
* @param {Object} [options.requiredProps={}] Props required to render the component.
* @param {Object|function} [options.shorthandDefaultProps={}] Props required to render the shorthand component.
*/
export const implementsHTMLInputProp = (Component, options = {}) => {
implementsShorthandProp(Component, {
propKey: 'input',
ShorthandComponent: 'input',
mapValueToProps: val => ({ type: val }),
requiredProps: {},
shorthandDefaultProps: {},
...options,
})
}
Expand All @@ -684,15 +712,15 @@ export const implementsIconProp = (Component, options = {}) => {
* @param {string|function} [options.ShorthandComponent] The component that should be rendered from the shorthand value.
* @param {function} [options.mapValueToProps] A function that maps a primitive value to the Component props
* @param {Object} [options.requiredProps={}] Props required to render the component.
* @param {Object} [options.requiredShorthandProps={}] Props required to render the shorthand component.
* @param {Object|function} [options.shorthandDefaultProps={}] Props required to render the shorthand component.
*/
export const implementsLabelProp = (Component, options = {}) => {
implementsShorthandProp(Component, {
propKey: 'label',
ShorthandComponent: Label,
mapValueToProps: val => ({ content: val }),
requiredProps: {},
requiredShorthandProps: {},
shorthandDefaultProps: {},
...options,
})
}
Expand All @@ -706,15 +734,15 @@ export const implementsLabelProp = (Component, options = {}) => {
* @param {string|function} [options.ShorthandComponent] The component that should be rendered from the shorthand value.
* @param {function} [options.mapValueToProps] A function that maps a primitive value to the Component props
* @param {Object} [options.requiredProps={}] Props required to render the component.
* @param {Object} [options.requiredShorthandProps={}] Props required to render the shorthand component.
* @param {Object|function} [options.shorthandDefaultProps={}] Props required to render the shorthand component.
*/
export const implementsImageProp = (Component, options = {}) => {
implementsShorthandProp(Component, {
propKey: 'image',
ShorthandComponent: Image,
mapValueToProps: val => ({ src: val }),
requiredProps: {},
requiredShorthandProps: {},
shorthandDefaultProps: {},
...options,
})
}
Expand Down
2 changes: 1 addition & 1 deletion test/specs/elements/Button/Button-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('Button', () => {
common.hasSubComponents(Button, [ButtonContent, ButtonGroup, ButtonOr])
common.implementsIconProp(Button)
common.implementsLabelProp(Button, {
requiredShorthandProps: {
shorthandDefaultProps: {
basic: true,
pointing: 'left',
},
Expand Down
2 changes: 0 additions & 2 deletions test/specs/elements/Icon/Icon-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import * as common from 'test/specs/commonTests'
describe('Icon', () => {
common.isConformant(Icon)
common.hasSubComponents(Icon, [IconGroup])
common.implementsIconProp(Icon)

common.propKeyOnlyToClassName(Icon, 'bordered')
common.propKeyOnlyToClassName(Icon, 'circular')
Expand All @@ -15,7 +14,6 @@ describe('Icon', () => {
common.propKeyOnlyToClassName(Icon, 'disabled')
common.propKeyOnlyToClassName(Icon, 'fitted')
common.propKeyAndValueToClassName(Icon, 'flipped')
common.propKeyOnlyToClassName(Icon, 'icon')
common.propKeyOnlyToClassName(Icon, 'inverted')
common.propValueOnlyToClassName(Icon, 'name')
common.propKeyOnlyToClassName(Icon, 'link')
Expand Down
Loading

0 comments on commit adac980

Please sign in to comment.