├── .gitignore
├── README.md
├── bower.json
├── index.js
├── package.json
└── test
├── component.js
└── index.html
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-component-visibility
2 |
3 | A mixin for determining whether a component is visible to the user or not.
4 |
5 | Versions below v1.0.0 use the React namespace, v1.0.0 and later use ReactDOM
6 | instead, which means if you're using an older version of React, you may
7 | need to handpick the version you want to use.
8 |
9 | ## What is this?
10 |
11 | This mixin is for running React components in the browser (it has a hard
12 | dependency on `window` and `document`), listening to `scroll` and `resize`
13 | events to check whether these have made components visible to the user. If
14 | so, magic happens and the component's `componentVisibilityChanged` function
15 | to notify the component that a visibility change occurred.
16 |
17 | In addition to the event handler, a state change is triggered for a value
18 | called `visible`, so you usually don't even need to implement your own
19 | `componentVisibilityChanged` handler, you can simply rely on the fact that
20 | **if** the component becomes visible, or goes from visible to no longer
21 | visible (based on scroll, resize, or window minimize), `render()`, and
22 | subsequent `componentDidUpdate` will get triggered.
23 |
24 | Nice and easy.
25 |
26 | ## This mixin has a stupidly simple API
27 |
28 |
29 | The mixin takes care of registering and dropping event listeners for scroll
30 | and window resizing. However, because some times you only need "trigger once,
31 | then stop listening", there are two functions you can call if you need more
32 | control than the mixin provides:
33 |
34 | - `enableVisibilityHandling([checkNow])` (built in)
35 |
36 | Call as `this.enableVisibilityHandling()`, with an optional `true` as argument
37 | to both enable visibiilty handling and immediately do a visibiity check.
38 |
39 | - `disableVisibilityHandling()` (built in)
40 |
41 | Call as `this.disableVisibilityHandling()` to turn off event listening for
42 | this component.
43 |
44 | And then for convenience, so you don't need to mess with visibility change
45 | checks in `componentDidUpdate()`, there is an optional function that your
46 | component can implement, which will then be used to notify it of any
47 | changes to the component visibility:
48 |
49 | - `componentVisibilityChanged()` (optional)
50 |
51 | This function, if you add it to your component yourself, gets called
52 | automatically after binding a visibility change in the component's state,
53 | so that you can trigger custom logic. No argument comes into this function,
54 | since the `this.state.visible` value will already reflect the currect value,
55 | and the old value was simply `!visible`.
56 |
57 | ### Rate limiting the scroll handling
58 |
59 | By default, the mixin does rate limiting to prevent event saturation (onscroll
60 | refires very fast), set such that when a scroll event is handled, it won't
61 | listen for and act on new events until 25 milliseconds later. You can change
62 | the delay by calling the rate limit function with the number of milliseconds
63 | you want the interval to be instead:
64 |
65 | ```
66 | ...
67 | componentDidMount: function() {
68 | ...
69 | this.setComponentVisibilityRateLimit(ms);
70 | ...
71 | },
72 | ...
73 | ```
74 |
75 | ## An example
76 |
77 | Using the mixin is pretty straight forward.
78 |
79 | ### In the browser:
80 |
81 | ```
82 |
83 | ...
84 |
100 | ```
101 |
102 | ### In the browser, AMD style:
103 |
104 | Bind `react-component-visibility/index.js` in your require config,
105 | and then simply require it in like everything else:
106 |
107 | ```
108 | define(
109 | ['React', 'ComponentVisibilityMixin'],
110 |
111 | function(R, CVM) {
112 | var MyComponent = R.createClass({
113 | ...
114 | mixins = [ CVM ];
115 | ...
116 | componentVisibilityChanged: function() {
117 | var visible = this.state.visible;
118 | ...
119 | },
120 | ...
121 | });
122 | }
123 | );
124 | ```
125 |
126 | ### In node.js
127 |
128 | Like every other node package:
129 |
130 | ```
131 | var React = require("react");
132 | var CVM = require("react-component-visibility");
133 | var MyComponent = React.createClass({
134 | ...
135 | mixins = [ CVM ];
136 | ...
137 | componentVisibilityChanged: function() {
138 | var visible = this.state.visible;
139 | ...
140 | },
141 | ...
142 | });
143 |
144 | module.exports = MyComponent;
145 | ```
146 |
147 | ## How to install
148 |
149 | Simply use `npm`:
150 |
151 | ```
152 | $> npm install react-component-visibility --save
153 | ```
154 |
155 | and you're off to the races.
156 |
157 | ## I think you forgot something
158 |
159 | I very well might have! Hit up the [issue tracker](https://github.com/Pomax/react-component-visibility/issues) and we can discuss that.
160 |
161 | -- [Pomax](http://twitter.com/TheRealPomax)
162 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-component-visibility",
3 | "main": "index.js",
4 | "version": "0.0.6",
5 | "homepage": "https://github.com/Pomax/react-component-visibility",
6 | "authors": [
7 | "Pomax "
8 | ],
9 | "description": "A mixin for determining whether a component is visible to the user or not.",
10 | "moduleType": [
11 | "amd",
12 | "globals",
13 | "node"
14 | ],
15 | "keywords": [
16 | "React",
17 | "visible",
18 | "visibility",
19 | "mixin"
20 | ],
21 | "dependencies": {
22 | "react": "~0.13"
23 | },
24 | "license": "MIT",
25 | "ignore": [
26 | "**/.*",
27 | "node_modules",
28 | "bower_components",
29 | "test",
30 | "tests"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | var React = (typeof window !== 'undefined' && window.React) ? window.React : require('react');
3 | var ReactDOM = (typeof window !== 'undefined' && window.ReactDOM) ? window.ReactDOM : require('react-dom');
4 |
5 | var RATE_LIMIT = 25;
6 |
7 | var ComponentVisibilityMixin = {
8 | setComponentVisibilityRateLimit: function(milliseconds) {
9 | RATE_LIMIT = milliseconds;
10 | },
11 |
12 | getInitialState: function() {
13 | return { visible: false };
14 | },
15 |
16 | componentDidMount: function() {
17 | this.enableVisibilityHandling(true);
18 | },
19 |
20 | componentWillUnmount: function() {
21 | this.disableVisibilityHandling();
22 | },
23 |
24 | /**
25 | * Check whether a component is in view based on its DOM node,
26 | * checking for both vertical and horizontal in-view-ness, as
27 | * well as whether or not it's invisible due to CSS rules based
28 | * on opacity:0 or visibility:hidden.
29 | */
30 | checkComponentVisibility: function() {
31 | var domnode = this._dom_node,
32 | gcs = window.getComputedStyle(domnode, false),
33 | dims = domnode.getBoundingClientRect(),
34 | h = window.innerHeight,
35 | w = window.innerWidth,
36 | // are we vertically visible?
37 | topVisible = 0 <= dims.top && dims.top <= h,
38 | bottomVisible = 0 <= dims.bottom && dims.bottom <= h,
39 | verticallyVisible = topVisible || bottomVisible,
40 | // also, are we horizontally visible?
41 | leftVisible = 0 <= dims.left && dims.left <= w,
42 | rightVisible = 0 <= dims.right && dims.right <= w,
43 | horizontallyVisible = leftVisible || rightVisible,
44 | // we're only visible if both of those are true.
45 | visible = horizontallyVisible && verticallyVisible;
46 |
47 | // but let's be fair: if we're opacity: 0 or
48 | // visibility: hidden, or browser window is minimized we're not visible at all.
49 | if(visible) {
50 | var isDocHidden = document.hidden;
51 | var isElementNotDisplayed = (gcs.getPropertyValue("display") === "none");
52 | var elementHasZeroOpacity = (gcs.getPropertyValue("opacity") === 0);
53 | var isElementHidden = (gcs.getPropertyValue("visibility") === "hidden");
54 | visible = visible && !(
55 | isDocHidden || isElementNotDisplayed || elementHasZeroOpacity || isElementHidden
56 | );
57 | }
58 |
59 | // at this point, if our visibility is not what we expected,
60 | // update our state so that we can trigger whatever needs to
61 | // happen.
62 | if(visible !== this.state.visible) {
63 | // set State first:
64 | this.setState({ visible: visible },
65 | // then notify the component the value was changed:
66 | function() {
67 | if (this.componentVisibilityChanged) {
68 | this.componentVisibilityChanged();
69 | }
70 | });
71 | }
72 | },
73 |
74 | /**
75 | * This can be called to manually turn on visibility handling, if at
76 | * some point it got turned off. Call this without arguments to turn
77 | * listening on, or with argument "true" to turn listening on and
78 | * immediately check whether this element is already visible or not.
79 | */
80 | enableVisibilityHandling: function(checkNow) {
81 | if (typeof window === "undefined") {
82 | return console.error("This environment lacks 'window' support.");
83 | }
84 |
85 | if (typeof document === "undefined") {
86 | return console.error("This environment lacks 'document' support.");
87 | }
88 |
89 | if (!this._dom_node) {
90 | this._dom_node = ReactDOM.findDOMNode(this);
91 | }
92 | var domnode = this._dom_node;
93 |
94 | this._rcv_fn = function() {
95 | if(this._rcv_lock) {
96 | this._rcv_schedule = true;
97 | return;
98 | }
99 | this._rcv_lock = true;
100 | this.checkComponentVisibility();
101 | this._rcv_timeout = setTimeout(function() {
102 | this._rcv_lock = false;
103 | if (this._rcv_schedule) {
104 | this._rcv_schedule = false;
105 | this.checkComponentVisibility();
106 | }
107 | }.bind(this), RATE_LIMIT);
108 | }.bind(this);
109 |
110 | /* Adding scroll listeners to all element's parents */
111 | while (domnode.nodeName !== 'BODY' && domnode.parentElement) {
112 | domnode = domnode.parentElement;
113 | domnode.addEventListener("scroll", this._rcv_fn);
114 | }
115 | /* Adding listeners to page events */
116 | document.addEventListener("visibilitychange", this._rcv_fn);
117 | document.addEventListener("scroll", this._rcv_fn);
118 | window.addEventListener("resize", this._rcv_fn);
119 |
120 | if (checkNow) { this._rcv_fn(); }
121 | },
122 |
123 | /**
124 | * This can be called to manually turn off visibility handling. This
125 | * is particularly handy when you're running it on a lot of components
126 | * and you only really need to do something once, like loading in
127 | * static assets on first-time-in-view-ness (that's a word, right?).
128 | */
129 | disableVisibilityHandling: function() {
130 | clearTimeout(this._rcv_timeout);
131 | if (this._rcv_fn) {
132 | var domnode = this._dom_node;
133 |
134 | while (domnode.nodeName !== 'BODY' && domnode.parentElement) {
135 | domnode = domnode.parentElement;
136 | domnode.removeEventListener("scroll", this._rcv_fn);
137 | }
138 |
139 | document.removeEventListener("visibilitychange", this._rcv_fn);
140 | document.removeEventListener("scroll", this._rcv_fn);
141 | window.removeEventListener("resize", this._rcv_fn);
142 | this._rcv_fn = false;
143 | }
144 | }
145 | };
146 |
147 | if(typeof module !== "undefined") {
148 | module.exports = ComponentVisibilityMixin;
149 | }
150 |
151 | else if (typeof define !== "undefined") {
152 | define(function() {
153 | return ComponentVisibilityMixin;
154 | });
155 | }
156 |
157 | else if (typeof window !== "undefined") {
158 | window.ComponentVisibilityMixin = ComponentVisibilityMixin;
159 | }
160 |
161 | }());
162 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-component-visibility",
3 | "version": "2.1.0",
4 | "description": "A mixin for determining whether a component is visible to the user or not.",
5 | "main": "index.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/Pomax/react-component-visibility.git"
9 | },
10 | "keywords": [
11 | "React",
12 | "visible",
13 | "visibility",
14 | "mixin",
15 | "react-component"
16 | ],
17 | "author": "Pomax",
18 | "license": "MIT",
19 | "bugs": {
20 | "url": "https://github.com/Pomax/react-component-visibility/issues"
21 | },
22 | "scripts": {
23 | "test": "mocha"
24 | },
25 | "homepage": "https://github.com/Pomax/react-component-visibility",
26 | "devDependencies": {
27 | "chai": "^3.2.0",
28 | "jsdom": "^3.1.2",
29 | "react": "^15.0.2",
30 | "react-dom": "^15.0.2"
31 | },
32 | "peerDependencies": {
33 | "react": "^15.0.2",
34 | "react-dom": "^15.0.2"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/test/component.js:
--------------------------------------------------------------------------------
1 | var assert = require('chai').assert;
2 | var jsdom = require('jsdom');
3 | var mixin = require('../');
4 | var React;
5 | var ReactDOM;
6 |
7 | before(function () {
8 | React = require('react');
9 | ReactDOM = require('react-dom');
10 |
11 | global.document = jsdom.jsdom('');
12 | global.window = document.parentWindow;
13 | });
14 |
15 | function fireEvent(type) {
16 | // NOTE: initEvent is deprecated
17 | // TODO: Replace with `new window.Event()` when jsdom supports it
18 | var event = document.createEvent(type);
19 | event.initEvent(type, false, false);
20 | if (type == 'resize') {
21 | window.dispatchEvent(event);
22 | } else {
23 | document.dispatchEvent(event);
24 | }
25 | }
26 |
27 | function wait(done) {
28 | // Wait for at least RATE_LIMIT (default 25)
29 | return setTimeout(function () {
30 | done();
31 | }, 30);
32 | }
33 |
34 | describe('react-component-visibility', function () {
35 | var component;
36 | var element;
37 |
38 | beforeEach(function () {
39 | component = React.createClass({
40 | mixins: [mixin],
41 |
42 | render: function () {
43 | return React.createElement('div', {}, 'hello');
44 | }
45 | });
46 | element = ReactDOM.render(React.createElement(component), document.body);
47 | });
48 |
49 | function testEvent(type) {
50 | describe(type, function () {
51 | it('should trigger checkComponentVisibility', function (done) {
52 | element.checkComponentVisibility = function () {
53 | done();
54 | };
55 | fireEvent(type);
56 | })
57 |
58 | it('should not trigger checkComponentVisibility if disabled', function (done) {
59 | element.disableVisibilityHandling();
60 | element.checkComponentVisibility = function () {
61 | done(new Error('should not run'));
62 | };
63 | fireEvent(type);
64 | wait(done);
65 | });
66 |
67 | it('should not trigger checkComponentVisibility if unmounted', function (done) {
68 | // fire event to trigger rate limit
69 | fireEvent(type);
70 | ReactDOM.unmountComponentAtNode(document.body);
71 | element.checkComponentVisibility = function () {
72 | done(new Error('should not run'));
73 | };
74 | fireEvent(type);
75 | wait(done);
76 | });
77 | });
78 | }
79 |
80 | testEvent('resize');
81 | testEvent('scroll');
82 | testEvent('visibilitychange');
83 | });
84 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Test
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
36 |
37 |
38 |
--------------------------------------------------------------------------------