├── .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 | --------------------------------------------------------------------------------