diff --git a/README.md b/README.md
index 0d6692c..17f23af 100644
--- a/README.md
+++ b/README.md
@@ -77,8 +77,8 @@ below) has changed.
/>
```
-Waypoints can have children, allowing you to track when a section of content
-enters or leaves the viewport.
+Waypoints can take a child, allowing you to track when a section of content
+enters or leaves the viewport. For details, see [Children](#children), below.
```jsx
@@ -88,42 +88,6 @@ enters or leaves the viewport.
```
-Note that this inserts a `
` wrapping the children passed in.
-If this is undesirable, you can use the `noWrapper={true}` prop,
-in which case you can pass a single React Class or DOM element
-which will be rendered directly. You cannot pass a stateless component,
-text node, or array of nodes when `noWrapper={true}`.
-
-```jsx
-
-
- This section will be rendered without an enclosing div.
-
-
-
-// INVALID:
-
-
-
- Oops, I cannot pass multiple children with noWrapper={true}!
-
-
-
-// INVALID:
-
- Oops, I must pass an element with noWrapper={true}!
-
-
-// INVALID:
-
-
- Oops, I cannot pass a stateless component with noWrapper={true}!
- (this is because stateless components do not accept refs)
-
-
-```
-
-
### Example: [JSFiddle Example][jsfiddle-example]
[jsfiddle-example]: http://jsfiddle.net/L4z5wcx0/7/
@@ -296,29 +260,34 @@ top boundary or the bottom boundary.
## Children
-If you don't pass children into your Waypoint, then you can think of the
+If you don't pass a child into your Waypoint, then you can think of the
waypoint as a line across the page. Whenever that line crosses a
[boundary](#offsets-and-boundaries), then the `onEnter` or `onLeave` callbacks
will be called.
-When children are passed, the waypoint's size will be determined by the
-size of a div wrapping the contained children (or, with `noWrapper={true}`,
-the size of the child element you pass). The `onEnter` callback will be called
-when *any* part of the children is visible in the viewport. The `onLeave`
-callback will be called when *all* children have exited the viewport.
-(Note that this is measured only on a single axis; strangely positioned elements
-may not work as expected).
-
-Deciding whether to pass children or not will depend on your use case. One
-example of when passing children is useful is for a scrollspy. Imagine if you
-want to fire a waypoint when a particularly long piece of content is visible
-onscreen. When the page loads, it is conceivable that both the top and bottom of
-this piece of content could lie outside of the boundaries, because the content
-is taller than the viewport. If you didn't pass children, and instead put the
-waypoint above or below the content, then you will not receive an `onEnter`
-callback (nor any other callback from this library). Instead, passing this long
-content as a child of the Waypoint would fire the `onEnter` callback when the
-page loads.
+If you do pass a child, it must be a single DOM Element (eg; a `
`)
+and *not* a Component Element (eg; `
`).
+
+The `onEnter` callback will be called when *any* part of the child is visible
+in the viewport. The `onLeave` callback will be called when *all* of the child
+has exited the viewport.
+
+(Note that this is measured only on a single axis. What this means is that for a
+Waypoint within a vertically scrolling parent, it could be off of the screen
+horizontally yet still fire an onEnter event, because it is within the vertical
+boundaries).
+
+Deciding whether to pass a child or not will depend on your use case. One
+example of when passing a child is useful is for a scrollspy
+(like [Bootstrap's](https://bootstrapdocs.com/v3.3.6/docs/javascript/#scrollspy)).
+Imagine if you want to fire a waypoint when a particularly long piece of content
+is visible onscreen. When the page loads, it is conceivable that both the top
+and bottom of this piece of content could lie outside of the boundaries,
+because the content is taller than the viewport. If you didn't pass a child,
+and instead put the waypoint above or below the content, then you will not
+receive an `onEnter` callback (nor any other callback from this library).
+Instead, passing this long content as a child of the Waypoint would fire the `onEnter`
+callback when the page loads.
## Containing elements and `scrollableAncestor`
diff --git a/spec/waypoint_spec.js b/spec/waypoint_spec.js
index 94fdd7a..1fde22b 100644
--- a/spec/waypoint_spec.js
+++ b/spec/waypoint_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable react/no-multi-comp */
import React from 'react';
import ReactDOM from 'react-dom';
import Waypoint from '../src/waypoint.jsx';
@@ -343,7 +344,7 @@ describe('
', function() {
describe('with children', () => {
beforeEach(() => {
this.childrenHeight = 80;
- this.props.children = [
+ this.props.children = React.createElement('div', {}, [
React.createElement('div', {
key: 1,
style: {
@@ -356,7 +357,7 @@ describe('', function() {
height: this.childrenHeight / 2,
}
}),
- ];
+ ]);
});
describe('when scrolling down far enough', () => {
@@ -853,68 +854,35 @@ describe('', function() {
});
});
- describe('when the Waypoint has children and is above the top', () => {
- beforeEach(() => {
- this.topSpacerHeight = 200;
- this.bottomSpacerHeight = 200;
- this.childrenHeight = 100;
- this.props.children = React.createElement('div', {
- style: {
- height: this.childrenHeight,
- }
- });
- this.scrollable = this.subject();
+ describe('when the Waypoint has children that are not DOM Elements', () => {
+ const errorMessage = 'You must wrap any Component Elements passed to Waypoint ' +
+ 'in a DOM Element (eg; a ).';
- // Because of how we detect when a Waypoint is scrolled past without any
- // scroll event fired when it was visible, we need to reset callback
- // spies.
- scrollNodeTo(this.scrollable, 400);
- this.props.onEnter.calls.reset();
- this.props.onLeave.calls.reset();
- scrollNodeTo(this.scrollable, 400);
- });
+ it('errors with a stateless component', () => {
+ const StatelessComponent = () => React.createElement('div');
+ this.props.children = React.createElement(StatelessComponent);
- it('does not call the onEnter handler', () => {
- expect(this.props.onEnter).not.toHaveBeenCalled();
+ expect(this.subject).toThrowError(errorMessage);
});
- it('does not call the onLeave handler', () => {
- expect(this.props.onLeave).not.toHaveBeenCalled();
- });
-
- describe('when scrolled back up just past the bottom', () => {
- beforeEach(() => {
- scrollNodeTo(this.scrollable, this.topSpacerHeight + 50);
- });
-
- it('calls the onEnter handler', () => {
- expect(this.props.onEnter).
- toHaveBeenCalledWith({
- currentPosition: Waypoint.inside,
- previousPosition: Waypoint.above,
- event: jasmine.any(Event),
- waypointTop: -40,
- waypointBottom: -40 + this.childrenHeight,
- viewportTop: this.margin,
- viewportBottom: this.margin + this.parentHeight,
- });
- });
+ it('errors with a class-based component', () => {
+ class ClassBasedComponent extends React.Component {
+ render() {
+ return React.createElement('div');
+ }
+ }
+ this.props.children = React.createElement(ClassBasedComponent);
- it('does not call the onLeave handler', () => {
- expect(this.props.onLeave).not.toHaveBeenCalled();
- });
+ expect(this.subject).toThrowError(errorMessage);
});
});
- describe('when noWrapper=true and child is above the top', () => {
+ describe('when the Waypoint has children and is above the top', () => {
beforeEach(() => {
this.topSpacerHeight = 200;
this.bottomSpacerHeight = 200;
this.childrenHeight = 100;
- this.childRefSpy = jasmine.createSpy('ref');
- this.props.noWrapper = true;
- this.props.children = React.createElement('section', {
- ref: this.childRefSpy,
+ this.props.children = React.createElement('div', {
style: {
height: this.childrenHeight,
}
@@ -930,14 +898,6 @@ describe('
', function() {
scrollNodeTo(this.scrollable, 400);
});
- it('calls the original ref handler', () => {
- expect(this.childRefSpy).toHaveBeenCalled();
- });
-
- it('does not have an extra div', () => {
- expect(this.scrollable.children[1].nodeName).toBe('SECTION');
- });
-
it('does not call the onEnter handler', () => {
expect(this.props.onEnter).not.toHaveBeenCalled();
});
diff --git a/src/waypoint.jsx b/src/waypoint.jsx
index 7628f8e..f2853ad 100644
--- a/src/waypoint.jsx
+++ b/src/waypoint.jsx
@@ -120,6 +120,35 @@ function computeOffsetPixels(offset, contextHeight) {
}
}
+/**
+ * When an element's type is a string, it represents a DOM node with that tag name
+ * https://facebook.github.io/react/blog/2015/12/18/react-components-elements-and-instances.html#dom-elements
+ *
+ * @param {React.element} Component
+ * @return {bool} Whether the component is a DOM Element
+ */
+function isDOMElement(Component) {
+ return (typeof Component.type === 'string');
+}
+
+
+/**
+ * Raise an error if "children" isn't a single DOM Element
+ *
+ * @param {React.element|null} children
+ * @return {undefined}
+ */
+function ensureChildrenIsSingleDOMElement(children) {
+ if (children) {
+ React.Children.only(children);
+
+ if (!isDOMElement(children)) {
+ throw new Error(
+ 'You must wrap any Component Elements passed to Waypoint in a DOM Element (eg; a ).'
+ );
+ }
+ }
+}
/**
* Calls a function when you scroll to the element.
@@ -132,6 +161,8 @@ export default class Waypoint extends React.Component {
}
componentWillMount() {
+ ensureChildrenIsSingleDOMElement(this.props.children);
+
if (this.props.scrollableParent) { // eslint-disable-line react/prop-types
throw new Error('The `scrollableParent` prop has changed name to `scrollableAncestor`.');
}
@@ -171,6 +202,10 @@ export default class Waypoint extends React.Component {
}, 0);
}
+ componentWillReceiveProps(nextProps) {
+ ensureChildrenIsSingleDOMElement(nextProps.children);
+ }
+
componentDidUpdate() {
if (!Waypoint.getWindow()) {
return;
@@ -352,30 +387,27 @@ export default class Waypoint extends React.Component {
* @return {Object}
*/
render() {
- if (this.props.noWrapper) {
- const child = React.Children.only(this.props.children);
- const ref = (node) => {
- this.refElement(node);
- if (this.props.children.ref) {
- this.props.children.ref(node);
- }
- };
- return React.cloneElement(child, { ref });
- }
+ const { children } = this.props;
- if (this.props.children) {
- return
{this.props.children}
;
+ if (!children) {
+ // We need an element that we can locate in the DOM to determine where it is
+ // rendered relative to the top of its context.
+ return
;
}
- // We need an element that we can locate in the DOM to determine where it is
- // rendered relative to the top of its context.
- return
;
+ const ref = (node) => {
+ this.refElement(node);
+ if (children.ref) {
+ children.ref(node);
+ }
+ };
+
+ return React.cloneElement(children, { ref });
}
}
Waypoint.propTypes = {
- children: PropTypes.node,
- noWrapper: PropTypes.bool,
+ children: PropTypes.element,
debug: PropTypes.bool,
onEnter: PropTypes.func,
onLeave: PropTypes.func,