Skip to content

Commit 84b0e46

Browse files
authored
Keep plain children support (#91)
* feat: add back plain children support * feat: add example to story * fix: add correct types * test: improve tests
1 parent 5f34b3a commit 84b0e46

File tree

8 files changed

+106
-43
lines changed

8 files changed

+106
-43
lines changed

README.md

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ or NPM:
3030
npm install react-intersection-observer --save
3131
```
3232

33-
> ️You also want to add the [intersection-observer](https://www.npmjs.com/package/react-intersection-observer) polyfill for full browser support. Check out adding the [polyfill](#polyfill) for details about how you can include it.
33+
> ️ You also want to add the [intersection-observer](https://www.npmjs.com/package/react-intersection-observer) polyfill for full browser support. Check out adding the [polyfill](#polyfill) for details about how you can include it.
3434
3535
## Usage
3636

@@ -40,7 +40,7 @@ To use the `Observer`, you pass it a function. It will be called whenever the st
4040
In addition to the `inView` prop, children also receives a `ref` that should be set on the containing DOM element.
4141
This is the element that the IntersectionObserver will monitor.
4242

43-
```js
43+
```jsx
4444
import Observer from 'react-intersection-observer'
4545

4646
const Component = () => (
@@ -56,19 +56,40 @@ const Component = () => (
5656
export default Component
5757
```
5858

59+
### Plain children
60+
61+
You can pass any element to the `<Observer />`, and it will handle creating the wrapping DOM element.
62+
Add a handler to the `onChange` method, and control the state in your own component.
63+
It will pass any extra props to the HTML element, allowing you set the `className`, `style`, etc.
64+
65+
```jsx
66+
import Observer from 'react-intersection-observer'
67+
68+
const Component = () => (
69+
<Observer tag="div" onChange={inView => console.log('Inview:', inView)}>
70+
<h2>Plain children are always rendered. Use onChange to monitor state.</h2>
71+
</Observer>
72+
)
73+
74+
export default Component
75+
```
76+
77+
> ⚠️ When rendering a plain child, make sure you keep your HTML output semantic.
78+
> Change the `tag` to match the context, and add a `className` to style the `<Observer />`.
79+
5980
## Props
6081

6182
The **`<Observer />`** accepts the following props:
6283

63-
| Name | Type | Default | Required | Description |
64-
| --------------- | ----------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
65-
| **children** | ({inView, ref}) => Node | | true | Children expects a function that recieves an object contain an `inView` boolean and `ref` that should be assigned to the element root. |
66-
| **onChange** | (inView) => void | | false | Call this function whenever the in view state changes |
67-
| **root** | HTMLElement | | false | The HTMLElement that is used as the viewport for checking visibility of the target. Defaults to the browser viewport if not specified or if null. |
68-
| **rootId** | String | | false | Unique identifier for the root element - This is used to identify the IntersectionObserver instance, so it can be reused. If you defined a root element, without adding an id, it will create a new instance for all components. |
69-
| **rootMargin** | String | '0px' | false | Margin around the root. Can have values similar to the CSS margin property, e.g. "10px 20px 30px 40px" (top, right, bottom, left). |
70-
| **threshold** | Number | 0 | false | Number between 0 and 1 indicating the the percentage that should be visible before triggering. Can also be an array of numbers, to create multiple trigger points. |
71-
| **triggerOnce** | Bool | false | false | Only trigger this method once |
84+
| Name | Type | Default | Required | Description |
85+
| --------------- | ------------------------------------------ | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
86+
| **children** | ({inView, ref}) => React.Node / React.Node | | true | Children expects a function that receives an object contain an `inView` boolean and `ref` that should be assigned to the element root. Alternately pass a plain child, to have the `<Observer />` deal with the wrapping element. |
87+
| **onChange** | (inView) => void | | false | Call this function whenever the in view state changes |
88+
| **root** | HTMLElement | | false | The HTMLElement that is used as the viewport for checking visibility of the target. Defaults to the browser viewport if not specified or if null. |
89+
| **rootId** | String | | false | Unique identifier for the root element - This is used to identify the IntersectionObserver instance, so it can be reused. If you defined a root element, without adding an id, it will create a new instance for all components. |
90+
| **rootMargin** | String | '0px' | false | Margin around the root. Can have values similar to the CSS margin property, e.g. "10px 20px 30px 40px" (top, right, bottom, left). |
91+
| **threshold** | Number | 0 | false | Number between 0 and 1 indicating the the percentage that should be visible before triggering. Can also be an array of numbers, to create multiple trigger points. |
92+
| **triggerOnce** | Bool | false | false | Only trigger this method once |
7293

7394
## Usage in other projects
7495

index.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export interface RenderProps {
77

88
export interface IntersectionObserverProps {
99
/** Children expects a function that recieves an object contain an `inView` boolean and `ref` that should be assigned to the element root. */
10-
children?: (fields: RenderProps) => React.ReactNode
10+
children?: React.ReactNode | ((fields: RenderProps) => React.ReactNode)
1111

1212
/**
1313
* The `HTMLElement` that is used as the viewport for checking visibility of
@@ -31,6 +31,12 @@ export interface IntersectionObserverProps {
3131
*/
3232
rootMargin?: string
3333

34+
/**
35+
* Element tag to use for the wrapping component
36+
* @default `'div'`
37+
*/
38+
tag?: string
39+
3440
/** Number between 0 and 1 indicating the the percentage that should be
3541
* visible before triggering. Can also be an array of numbers, to create
3642
* multiple trigger points.

src/index.js

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,19 @@ import { observe, unobserve } from './intersection'
44
import invariant from 'invariant'
55

66
type Props = {
7-
/** Children expects a function that recieves an object contain an `inView` boolean and `ref` that should be assigned to the element root. */
8-
children?: ({
9-
inView: boolean,
10-
ref: (node: ?HTMLElement) => void,
11-
}) => React.Node,
7+
/** Children expects a function that receives an object contain an `inView` boolean and `ref` that should be assigned to the element root. */
8+
children?:
9+
| (({
10+
inView: boolean,
11+
ref: (node: ?HTMLElement) => void,
12+
}) => React.Node)
13+
| React.Node,
1214
/** @deprecated replace render with children */
1315
render?: ({
1416
inView: boolean,
1517
ref: (node: ?HTMLElement) => void,
1618
}) => React.Node,
17-
/** @deprecated */
19+
/** Element tag to use for the wrapping element when rendering a plain React.Node. Defaults to 'div' */
1820
tag?: string,
1921
/** Number between 0 and 1 indicating the the percentage that should be visible before triggering. Can also be an array of numbers, to create multiple trigger points. */
2022
threshold?: number | Array<number>,
@@ -61,11 +63,6 @@ class Observer extends React.Component<Props, State> {
6163
`react-intersection-observer: "render" is deprecated, and should be replaced with "children"`,
6264
this.node,
6365
)
64-
} else if (typeof this.props.children !== 'function') {
65-
console.warn(
66-
`react-intersection-observer: plain "children" is deprecated. You should convert it to a function that handles the "ref" manually.`,
67-
this.node,
68-
)
6966
}
7067
invariant(
7168
this.node,

src/intersection.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ export function unobserve(element: ?HTMLElement) {
7878
: observer
7979

8080
if (observerInstance) {
81-
// $FlowFixMe - the interface in bom.js is wrong. Spec should accept the element.
8281
observerInstance.unobserve(element)
8382
}
8483

@@ -147,9 +146,7 @@ function onChange(changes) {
147146
}
148147

149148
instance.visible = inView
150-
if (instance.callback) {
151-
instance.callback(inView)
152-
}
149+
instance.callback(inView)
153150
}
154151
})
155152
}

stories/Observer.story.js

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,11 @@ storiesOf('Intersection Observer', module)
4141
</Observer>
4242
</ScrollWrapper>
4343
))
44-
.add('Render prop', () => (
44+
.add('Plain children', () => (
4545
<ScrollWrapper>
46-
<Observer
47-
onChange={action('Render Observer inview')}
48-
render={({ inView, ref }) => (
49-
<Header ref={ref}>
50-
Header is inside viewport: {inView.toString()}
51-
</Header>
52-
)}
53-
/>
46+
<Observer onChange={action('Child Observer inview')}>
47+
<Header>Plain children</Header>
48+
</Observer>
5449
</ScrollWrapper>
5550
))
5651
.add('Taller then viewport', () => (

tests/Observer.test.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ it('Should render <Observer />', () => {
1919
)
2020
})
2121

22+
it('should render plain children', () => {
23+
const wrapper = mount(<Observer>inner</Observer>)
24+
expect(wrapper).toMatchSnapshot()
25+
})
26+
it('should render with tag', () => {
27+
const wrapper = mount(<Observer tag="span">inner</Observer>)
28+
expect(wrapper).toMatchSnapshot()
29+
})
30+
it('should render with className', () => {
31+
const wrapper = mount(<Observer className="inner-class">inner</Observer>)
32+
expect(wrapper).toMatchSnapshot()
33+
})
34+
2235
it('Should render <Observer /> inview', () => {
2336
const callback = jest.fn(plainChild)
2437
const wrapper = mount(<Observer>{callback}</Observer>)
@@ -166,13 +179,6 @@ describe('deprecated methods', () => {
166179
afterEach(() => {
167180
global.console.warn.mockReset()
168181
})
169-
it('should render plain children', () => {
170-
mount(<Observer>inner</Observer>)
171-
expect(global.console.warn).toHaveBeenCalledWith(
172-
'react-intersection-observer: plain "children" is deprecated. You should convert it to a function that handles the "ref" manually.',
173-
expect.any(Object),
174-
)
175-
})
176182
it('should render using "render"', () => {
177183
mount(<Observer render={plainChild} />)
178184
expect(global.console.warn).toHaveBeenCalledWith(

tests/__snapshots__/Observer.test.js.snap

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,40 @@ exports[`Should render <Observer /> render when in view 1`] = `
2323
</div>
2424
</Observer>
2525
`;
26+
27+
exports[`should render plain children 1`] = `
28+
<Observer
29+
threshold={0}
30+
triggerOnce={false}
31+
>
32+
<div>
33+
inner
34+
</div>
35+
</Observer>
36+
`;
37+
38+
exports[`should render with className 1`] = `
39+
<Observer
40+
className="inner-class"
41+
threshold={0}
42+
triggerOnce={false}
43+
>
44+
<div
45+
className="inner-class"
46+
>
47+
inner
48+
</div>
49+
</Observer>
50+
`;
51+
52+
exports[`should render with tag 1`] = `
53+
<Observer
54+
tag="span"
55+
threshold={0}
56+
triggerOnce={false}
57+
>
58+
<span>
59+
inner
60+
</span>
61+
</Observer>
62+
`;

tests/intersection.test.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ it('should unobserve', () => {
9191
unobserve(el)
9292
})
9393

94+
it('should only unobserve if it gets an element', () => {
95+
unobserve()
96+
})
97+
9498
it('should keep observer when unobserve with multiple elements', () => {
9599
observe(el, jest.fn())
96100
observe({ el: 'htmlElement2' }, jest.fn())

0 commit comments

Comments
 (0)