Skip to content

Commit

Permalink
feat(Ref): add component
Browse files Browse the repository at this point in the history
  • Loading branch information
layershifter committed Jul 24, 2017
1 parent 292c595 commit 5307f11
Show file tree
Hide file tree
Showing 12 changed files with 235 additions and 3 deletions.
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
export { default as Confirm, ConfirmProps } from './dist/commonjs/addons/Confirm';
export { default as Portal, PortalProps } from './dist/commonjs/addons/Portal';
export { default as Radio, RadioProps } from './dist/commonjs/addons/Radio';
export { default as Ref, RefProps } from './dist/commonjs/addons/Ref';
export { default as Select, SelectProps } from './dist/commonjs/addons/Select';
export { default as TextArea, TextAreaProps, TextAreaOnChangeData } from './dist/commonjs/addons/TextArea';

Expand Down
22 changes: 22 additions & 0 deletions src/addons/Ref/Ref.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as React from 'react';

export interface RefProps {
[key: string]: any;

/** Primary content. */
children: React.ReactNode;

/**
* Called when componentDidMount.
*
* @param {HTMLElement} node - Referred node.
*/
innerRef: (node: HTMLElement) => void;
}

type withRefComponent = <P>(component: React.ReactType) => React.StatelessComponent<P>;

declare const Ref: React.ComponentClass<RefProps>;

export default Ref;
export const withRef: withRefComponent;
41 changes: 41 additions & 0 deletions src/addons/Ref/Ref.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import PropTypes from 'prop-types'
import { Children, Component } from 'react'
import { findDOMNode } from 'react-dom'

import { TYPES } from '../../lib/META'

/**
* This component exposes a callback prop that always returns the DOM node of both functional and class component
* children.
*/
export default class Ref extends Component {
static propTypes = {
/** Primary content. */
children: PropTypes.element.isRequired,

/**
* Called when componentDidMount.
*
* @param {HTMLElement} node - Referred node.
*/
innerRef: PropTypes.func.isRequired,
}

static _meta = {
name: 'Ref',
type: TYPES.ADDON,
}

componentDidMount() {
const { innerRef } = this.props
const node = findDOMNode(this)

innerRef(node)
}

render() {
const { children } = this.props

return Children.only(children)
}
}
1 change: 1 addition & 0 deletions src/addons/Ref/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default, RefProps } from './Ref';
1 change: 1 addition & 0 deletions src/addons/Ref/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default from './Ref'
28 changes: 28 additions & 0 deletions src/addons/Ref/withRef.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import PropTypes from 'prop-types'
import React, { Component } from 'react'

import Ref from './Ref'

const withRef = Child => class extends Component {
static propTypes = {
/**
* Called when componentDidMount.
*
* @param {HTMLElement} node - Referred node.
*/
innerRef: PropTypes.func.isRequired,
}

render() {
const { innerRef, ...rest } = this.props

if (typeof Child === 'string') return <Child {...rest} ref={innerRef} />
return (
<Ref innerRef={innerRef}>
<Child {...rest} />
</Ref>
)
}
}

export default withRef
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
export { default as Confirm } from './addons/Confirm'
export { default as Portal } from './addons/Portal'
export { default as Radio } from './addons/Radio'
export { default as Ref } from './addons/Ref'
export { default as Select } from './addons/Select'
export { default as TextArea } from './addons/TextArea'

Expand Down
6 changes: 5 additions & 1 deletion src/lib/getElementType.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import withRef from 'src/addons/Ref/withRef'

/**
* Returns a createElement() type based on the props of the Component.
* Useful for calculating what type a component should render as.
Expand All @@ -7,7 +9,7 @@
* @param {function} [getDefault] A function that returns a default element type.
* @returns {string|function} A ReactElement type
*/
function getElementType(Component, props, getDefault) {
export const computeElementType = (Component, props, getDefault) => {
const { defaultProps = {} } = Component

// ----------------------------------------
Expand All @@ -34,4 +36,6 @@ function getElementType(Component, props, getDefault) {
return defaultProps.as || 'div'
}

const getElementType = (...args) => withRef(computeElementType(...args))

export default getElementType
4 changes: 2 additions & 2 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export {
} from './debug'

export * from './factories'
export { default as getUnhandledProps } from './getUnhandledProps'
export { default as getElementType } from './getElementType'
export getUnhandledProps from './getUnhandledProps'
export getElementType from './getElementType'

export {
htmlInputAttrs,
Expand Down
60 changes: 60 additions & 0 deletions test/specs/addons/Ref/Ref-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import _ from 'lodash'
import React from 'react'

import Ref from 'src/addons/Ref/Ref'
import * as common from 'test/specs/commonTests'
import { sandbox } from 'test/utils'
import { CompositeClass, CompositeFunction, DOMClass, DOMFunction } from './fixtures'

const nodeMount = (Component, innerRef) => (
mount(
<Ref innerRef={innerRef}>
<Component />
</Ref>
)
.find('#node')
.getDOMNode()
)

describe('Ref', () => {
common.hasValidTypings(Ref, null, {
requiredProps: {
children: <div />,
innerRef: _.noop,
},
})

describe('innerRef', () => {
it('returns node from a functional component with DOM node', () => {
const innerRef = sandbox.spy()
const node = nodeMount(DOMFunction, innerRef)

innerRef.should.have.been.calledOnce()
innerRef.should.have.been.calledWithMatch(node)
})

it('returns node from a functional component', () => {
const innerRef = sandbox.spy()
const node = nodeMount(CompositeFunction, innerRef)

innerRef.should.have.been.calledOnce()
innerRef.should.have.been.calledWithMatch(node)
})

it('returns node from a class component with DOM node', () => {
const innerRef = sandbox.spy()
const node = nodeMount(DOMClass, innerRef)

innerRef.should.have.been.calledOnce()
innerRef.should.have.been.calledWithMatch(node)
})

it('returns node from a class component', () => {
const innerRef = sandbox.spy()
const node = nodeMount(CompositeClass, innerRef)

innerRef.should.have.been.calledOnce()
innerRef.should.have.been.calledWithMatch(node)
})
})
})
18 changes: 18 additions & 0 deletions test/specs/addons/Ref/fixtures.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* eslint-disable react/no-multi-comp */
import React, { Component } from 'react'

export const DOMFunction = (props) => <div {...props} id='node' />

export const CompositeFunction = (props) => <DOMFunction {...props} />

export class DOMClass extends Component {
render() {
return <div {...this.props} id='node' />
}
}

export class CompositeClass extends Component {
render() {
return <DOMClass {...this.props} />
}
}
55 changes: 55 additions & 0 deletions test/specs/lib/getElementType-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import faker from 'faker'
import _ from 'lodash'
import React from 'react'

import Ref from 'src/addons/Ref'
import getElementType, { computeElementType } from 'src/lib/getElementType'

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', () => {
const defaultProps = { as: 'button' }

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

describe('getElementType', () => {
it('returns a withRef HOC when "as" is a string', () => {
const ElementType = getElementType({}, { as: 'button' })
const wrapper = shallow(<ElementType innerRef={_.noop} />)

wrapper.should.have.tagName('button')
wrapper.should.have.type('button')
})

it('returns a withRef HOC when "as" is a component', () => {
const Component = () => <div />
const ElementType = getElementType({}, { as: Component })
const wrapper = shallow(<ElementType innerRef={_.noop} />)

wrapper.should.have.tagName('div')
wrapper.should.have.type(Ref)
})
})

0 comments on commit 5307f11

Please sign in to comment.