Skip to content

Commit

Permalink
feat(ElementType): new wrapper for all elements
Browse files Browse the repository at this point in the history
  • Loading branch information
layershifter committed Nov 8, 2017
1 parent 6ef5af5 commit 5633ae5
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 2 deletions.
3 changes: 1 addition & 2 deletions src/elements/Container/Container.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React from 'react'
import {
childrenUtils,
customPropTypes,
getElementType,
ElementType,
getUnhandledProps,
META,
SUI,
Expand Down Expand Up @@ -34,7 +34,6 @@ function Container(props) {
className,
)
const rest = getUnhandledProps(Container, props)
const ElementType = getElementType(Container, props)

return (
<ElementType {...rest} className={classes}>
Expand Down
42 changes: 42 additions & 0 deletions src/lib/ElementType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import PropTypes from 'prop-types'
import React, { Component } from 'react'

import Ref from '../addons/Ref'
import computeElementType from './computeElementType'
import * as customPropTypes from './customPropTypes'

export default class ElementType extends Component {
static propTypes = {
/** An element type to render as (string or function). */
as: customPropTypes.as,

/** A default element type to render as (string or function). */
defaultType: customPropTypes.as,

/** A function that returns a default element type. */
computeType: PropTypes.func,

/**
* Called when component did mount, returns an inner DOM node.
*
* @param {HTMLElement} node - Referred node.
*/
innerRef: PropTypes.func,
}

render() {
const { as, defaultType, computeType, innerRef, ...rest } = this.props
const Element = computeElementType({ as, defaultType, ...rest}, computeType)

// Heads up! We should handle common situations to gain performance.
// We don't need to <Ref> when Element is string or we deal with our component.
if(typeof Element === 'string') return <Element {...rest} ref={innerRef} />
if(Element._meta) return <Element {...rest} innerRef={innerRef} />

return (
<Ref innerRef={innerRef}>
<Element {...rest} />
</Ref>
)
}
}
36 changes: 36 additions & 0 deletions src/lib/computeElementType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Returns a createElement() type based on the passed props.
* Useful for calculating what type a component should render as.
*
* @param {object} props A ReactElement props object
* @param {function} [computeType] A function that returns a default element type.
* @returns {string|function} A ReactElement type
*/
const computeElementType = (props, computeType) => {
const { as, defaultType, href } = props

// ----------------------------------------
// user defined "as" element type

if (as && as !== defaultType) return props.as

// ----------------------------------------
// computed default element type

if (computeType) {
const computedDefault = computeType()
if (computedDefault) return computedDefault
}

// ----------------------------------------
// infer anchor links

if (href) return 'a'

// ----------------------------------------
// use defaultProp or 'div'

return defaultType || 'div'
}

export default computeElementType
1 change: 1 addition & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export {
debug,
makeDebugger,
} from './debug'
export ElementType from './ElementType'
export eventStack from './eventStack'

export * from './factories'
Expand Down
39 changes: 39 additions & 0 deletions test/specs/lib/ElementType-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react'

import Ref from 'src/addons/Ref'
import Container from 'src/elements/Container'
import { ElementType } from 'src/lib'
import { sandbox } from 'test/utils'

describe('ElementType', () => {
describe('children', () => {
it('adds ref to a string element', () => {
const innerRef = sandbox.spy()
const wrapper = mount(<ElementType as='a' innerRef={innerRef} />)

wrapper.childAt(0).type().should.equal('a')
innerRef.should.have.been.calledOnce('a')
innerRef.should.have.been.calledWithMatch({ tagName: 'A' })
})

it('adds innerRef to our element', () => {
const innerRef = () => {}
const wrapper = mount(<ElementType as={Container} innerRef={innerRef} />)
const child = wrapper.childAt(0)

child.type().should.equal(Container)
child.should.have.prop('innerRef', innerRef)
})

it('wraps with Ref a third-part element', () => {
const Element = () => <div />
const innerRef = () => {}
const wrapper = mount(<ElementType as={Element} innerRef={innerRef} />)
const child = wrapper.childAt(0)

child.type().should.equal(Ref)
child.should.have.prop('innerRef', innerRef)
child.childAt(0).type().should.equal(Element)
})
})
})
34 changes: 34 additions & 0 deletions test/specs/lib/computeElementType-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import faker from 'faker'
import computeElementType from 'src/lib/computeElementType'

describe('computeElementType', () => {
it('returns user defined "as" element type', () => {
computeElementType({ as: 'button' })
.should.equal('button')
})

it('returns computed default element type', () => {
computeElementType({}, () => 'button')
.should.equal('button')
})

it('returns default element type when compute failed', () => {
computeElementType({}, () => false)
.should.equal('div')
})

it('returns "a" when has a "href" prop', () => {
computeElementType({ href: faker.internet.url() })
.should.equal('a')
})

it('returns "as" from defaultProps', () => {
computeElementType({ defaultType: 'button' })
.should.equal('button')
})

it('returns default element type', () => {
computeElementType({})
.should.equal('div')
})
})

0 comments on commit 5633ae5

Please sign in to comment.