Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.

Commit 2fdcd4b

Browse files
authored
[RFC] feat(handleRef): add an util for handling passed refs (#459)
* chore(package): correct peer dependencies * docs(CHANGELOG): add entry * docs(ComponentExample): fix types * feat(handleRef): add a new util * feat(Ref): add willUnmount(), export component on top level * docs(handleRef): add JSDoc * test(handleRef|Ref): add tests * docs(CHANGELOG): add entry * improve description * apply changes * remove useless empty lines
1 parent 723f829 commit 2fdcd4b

File tree

10 files changed

+187
-11
lines changed

10 files changed

+187
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
4242
- Add `iconPosition` property to `Input` component @mnajdova ([#442](https://github.com/stardust-ui/react/pull/442))
4343
- Add `color`, `inverted` and `renderContent` props and `content` slot to `Segment` component @Bugaa92 ([#389](https://github.com/stardust-ui/react/pull/389))
4444
- Add focus trap behavior to `Popup` @kuzhelov ([#457](https://github.com/stardust-ui/react/pull/457))
45+
- Export `Ref` component and add `handleRef` util @layershifter ([#459](https://github.com/stardust-ui/react/pull/459))
4546

4647
### Documentation
4748
- Add all missing component descriptions and improve those existing @levithomason ([#400](https://github.com/stardust-ui/react/pull/400))
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React from 'react'
2+
import { Button, Grid, Ref, Segment } from '@stardust-ui/react'
3+
4+
class RefExampleRef extends React.Component {
5+
state = { isMounted: false }
6+
7+
createdRef = React.createRef<HTMLButtonElement>()
8+
functionalRef = null
9+
10+
handleRef = node => (this.functionalRef = node)
11+
12+
componentDidMount() {
13+
this.setState({ isMounted: true })
14+
}
15+
16+
render() {
17+
const { isMounted } = this.state
18+
19+
return (
20+
<Grid columns={2}>
21+
<Segment>
22+
<Ref innerRef={this.handleRef}>
23+
<Button primary>With functional ref</Button>
24+
</Ref>
25+
<Ref innerRef={this.createdRef}>
26+
<Button>
27+
With <code>createRef()</code>
28+
</Button>
29+
</Ref>
30+
</Segment>
31+
32+
{isMounted && (
33+
<code style={{ margin: 10 }}>
34+
<pre>
35+
{JSON.stringify(
36+
{
37+
nodeName: this.functionalRef.nodeName,
38+
nodeType: this.functionalRef.nodeType,
39+
textContent: this.functionalRef.textContent,
40+
},
41+
null,
42+
2,
43+
)}
44+
</pre>
45+
<pre>
46+
{JSON.stringify(
47+
{
48+
nodeName: this.createdRef.current.nodeName,
49+
nodeType: this.createdRef.current.nodeType,
50+
textContent: this.createdRef.current.textContent,
51+
},
52+
null,
53+
2,
54+
)}
55+
</pre>
56+
</code>
57+
)}
58+
</Grid>
59+
)
60+
}
61+
}
62+
63+
export default RefExampleRef
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import * as React from 'react'
2+
3+
import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample'
4+
import ExampleSection from 'docs/src/components/ComponentDoc/ExampleSection'
5+
6+
const RefTypesExamples = () => (
7+
<ExampleSection title="Types">
8+
<ComponentExample
9+
title="Ref"
10+
description={
11+
<span>
12+
A component exposes the <code>innerRef</code> prop that always returns the DOM node of
13+
both functional and class component children.
14+
</span>
15+
}
16+
examplePath="components/Ref/Types/RefExampleRef"
17+
/>
18+
</ExampleSection>
19+
)
20+
21+
export default RefTypesExamples
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as React from 'react'
2+
import Types from './Types'
3+
4+
const RefExamples: React.SFC = () => (
5+
<div>
6+
<Types />
7+
</div>
8+
)
9+
10+
export default RefExamples

src/components/Ref/Ref.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
import * as PropTypes from 'prop-types'
2-
import * as _ from 'lodash'
3-
import { Children, Component } from 'react'
2+
import * as React from 'react'
43
import { findDOMNode } from 'react-dom'
5-
import { ReactChildren } from 'utils'
4+
5+
import { ReactChildren } from '../../../types/utils'
6+
import { handleRef } from '../../lib'
67

78
export interface RefProps {
89
children?: ReactChildren
9-
innerRef?: (ref: HTMLElement) => void
10+
innerRef?: React.Ref<any>
1011
}
1112

1213
/**
1314
* This component exposes a callback prop that always returns the DOM node of both functional and class component
1415
* children.
1516
*/
16-
export default class Ref extends Component<RefProps> {
17+
export default class Ref extends React.Component<RefProps> {
1718
static propTypes = {
1819
/**
1920
* Used to set content when using childrenApi - internal only
@@ -22,18 +23,22 @@ export default class Ref extends Component<RefProps> {
2223
children: PropTypes.element,
2324

2425
/**
25-
* Called when componentDidMount.
26+
* Called when a child component will be mounted or updated.
2627
*
2728
* @param {HTMLElement} node - Referred node.
2829
*/
29-
innerRef: PropTypes.func,
30+
innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
3031
}
3132

3233
componentDidMount() {
33-
_.invoke(this.props, 'innerRef', findDOMNode(this))
34+
handleRef(this.props.innerRef, findDOMNode(this))
35+
}
36+
37+
componentWillUnmount() {
38+
handleRef(this.props.innerRef, null)
3439
}
3540

3641
render() {
37-
return this.props.children && Children.only(this.props.children)
42+
return this.props.children && React.Children.only(this.props.children)
3843
}
3944
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export {
8686
RadioGroupItemProps,
8787
} from './components/RadioGroup/RadioGroupItem'
8888

89+
export { default as Ref, RefProps } from './components/Ref/Ref'
8990
export { default as Segment, SegmentProps } from './components/Segment/Segment'
9091

9192
export { default as Status, StatusPropsWithDefaults, StatusProps } from './components/Status/Status'

src/lib/handleRef.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as React from 'react'
2+
3+
/**
4+
* The function that correctly handles passing refs.
5+
*
6+
* @param ref An ref object or function
7+
* @param node A node that should be passed by ref
8+
*/
9+
const handleRef = <N>(ref: React.Ref<N>, node: N) => {
10+
if (process.env.NODE_ENV !== 'production') {
11+
if (typeof ref === 'string') {
12+
throw new Error(
13+
'We do not support refs as string, this is a legacy API and will be likely to be removed in one of the future releases of React.',
14+
)
15+
}
16+
}
17+
18+
if (typeof ref === 'function') {
19+
ref(node)
20+
return
21+
}
22+
23+
if (typeof ref === 'object') {
24+
// @ts-ignore The `current` property is defined as readonly, however it's a valid way because
25+
// `ref` is a mutable object
26+
ref.current = node
27+
}
28+
}
29+
30+
export default handleRef

src/lib/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export { default as getElementType } from './getElementType'
1515
export { default as getUnhandledProps } from './getUnhandledProps'
1616
export { default as mergeThemes } from './mergeThemes'
1717
export { default as renderComponent, RenderResultConfig } from './renderComponent'
18+
19+
export { default as handleRef } from './handleRef'
1820
export {
1921
htmlImageProps,
2022
htmlInputAttrs,

test/specs/components/Ref/Ref-test.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import * as React from 'react'
21
import { shallow, mount } from 'enzyme'
2+
import * as React from 'react'
33

4-
import { CompositeClass, CompositeFunction, DOMClass, DOMFunction } from './fixtures'
54
import Ref from 'src/components/Ref/Ref'
5+
import { CompositeClass, CompositeFunction, DOMClass, DOMFunction } from './fixtures'
66

77
const testInnerRef = Component => {
88
const innerRef = jest.fn()
@@ -42,5 +42,20 @@ describe('Ref', () => {
4242
it('returns node from a class component', () => {
4343
testInnerRef(CompositeClass)
4444
})
45+
46+
it('returns "null" after unmount', () => {
47+
const innerRef = jest.fn()
48+
const wrapper = mount(
49+
<Ref innerRef={innerRef}>
50+
<CompositeClass />
51+
</Ref>,
52+
)
53+
54+
innerRef.mockClear()
55+
wrapper.unmount()
56+
57+
expect(innerRef).toHaveBeenCalledTimes(1)
58+
expect(innerRef).toHaveBeenCalledWith(null)
59+
})
4560
})
4661
})

test/specs/lib/handleRef-test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as React from 'react'
2+
import handleRef from 'src/lib/handleRef'
3+
4+
describe('handleRef', () => {
5+
it('throws an error when "ref" is string', () => {
6+
const node = document.createElement('div')
7+
8+
expect(() => {
9+
handleRef('ref', node)
10+
}).toThrowError()
11+
})
12+
13+
it('calls with node when "ref" is function', () => {
14+
const ref = jest.fn()
15+
const node = document.createElement('div')
16+
17+
handleRef(ref, node)
18+
expect(ref).toBeCalledWith(node)
19+
})
20+
21+
it('assigns to "current" when "ref" is object', () => {
22+
const ref = React.createRef<HTMLDivElement>()
23+
const node = document.createElement('div')
24+
25+
handleRef(ref, node)
26+
expect(ref.current).toBe(node)
27+
})
28+
})

0 commit comments

Comments
 (0)