├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── index.js └── toggleAware.js └── test ├── mocha.opts ├── setup.js └── toggleAware-test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-1" 5 | ], 6 | "plugins": [ 7 | "transform-react-jsx" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | 5 | "ecmaFeatures": { 6 | "modules": true 7 | }, 8 | 9 | "rules": { 10 | "indent": [2, 4, { "SwitchCase": 1, "VariableDeclarator": 1 }] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | scripts 3 | src 4 | test 5 | 6 | .babelrc 7 | .eslintrc 8 | .gitignore 9 | .travis.yml 10 | CONTRIBUTING 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.12' 4 | - '4.2' 5 | - '5.5' 6 | env: 7 | - CXX=g++-4.8 8 | addons: 9 | apt: 10 | sources: 11 | - ubuntu-toolchain-r-test 12 | packages: 13 | - g++-4.8 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2016 Nicholas Clawson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-toggle-aware [![Build Status](https://travis-ci.org/azuqua/react-toggle-aware.svg?branch=master)](https://travis-ci.org/azuqua/react-toggle-aware) 2 | 3 | A tiny higher order component to track toggle state. 4 | 5 | ### Example 6 | ```js 7 | import { Component } from 'react'; 8 | import { toggleAware } from 'react-toggle-aware'; 9 | 10 | @toggleAware({ // same as default options 11 | onDelay: 0, 12 | offDelay: 0, 13 | handler: 'onToggle', 14 | key: 'isToggled' 15 | }) 16 | class CustomComponent extends Component { 17 | 18 | render() { 19 | // props will include the toggle handler 20 | let { isToggled, className, ...props } = this.props; 21 | 22 | if (isToggled) className += ' on'; 23 | 24 | return ( 25 |
26 | 27 |
28 | ) 29 | } 30 | }; 31 | ``` 32 | 33 | ### API 34 | 35 | ##### As a decorator 36 | ```js 37 | @toggleAware(options) 38 | export default class Test extends React.Component { 39 | /* your code */ 40 | } 41 | ``` 42 | 43 | ##### As a function 44 | 45 | ```js 46 | class Test extends React.Component { 47 | /* your code */ 48 | } 49 | 50 | export default toggleAware(options)(Test); 51 | ``` 52 | #### Options 53 | 54 | ##### `onDelay` defaults to `0` 55 | Time in `ms` to wait before setting the `active` status to `true`. 56 | 57 | ##### `offDelay` defaults to `0` 58 | Time in `ms` to wait before setting the `active` status to `false`. 59 | 60 | ##### `handler` defaults to `'onToggle'` 61 | Property name to expose the `handler` as. 62 | 63 | ##### `key` defaults to `'isToggled'` 64 | Property name to expose the `active` status as. 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-toggle-aware", 3 | "version": "0.2.0", 4 | "description": "A tiny higher order component to track toggle state.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "rm -rf lib && babel src --out-dir lib", 8 | "lint": "eslint src", 9 | "pretest": "npm run lint", 10 | "test": "mocha test", 11 | "prepublish": "in-publish && npm run test && npm run build || not-in-publish", 12 | "publish:major": "npm version major && npm publish", 13 | "publish:minor": "npm version minor && npm publish", 14 | "publish:patch": "npm version patch && npm publish", 15 | "postpublish": "git push origin master --tags" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/azuqua/react-toggle-aware" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/azuqua/react-toggle-aware/issues" 23 | }, 24 | "keywords": [ 25 | "react", 26 | "decorator", 27 | "mouse", 28 | "event", 29 | "listener", 30 | "event", 31 | "toggle", 32 | "aware", 33 | "click" 34 | ], 35 | "author": { 36 | "name": "Nicholas Clawson", 37 | "email": "nickclaw@gmail.com", 38 | "url": "nickclaw.com" 39 | }, 40 | "license": "MIT", 41 | "devDependencies": { 42 | "babel": "^6.3.26", 43 | "babel-cli": "^6.4.5", 44 | "babel-eslint": "^4.1.8", 45 | "babel-plugin-transform-react-jsx": "^6.5.0", 46 | "babel-preset-es2015": "^6.3.13", 47 | "babel-preset-stage-1": "^6.3.13", 48 | "babel-register": "^6.4.3", 49 | "chai": "^3.5.0", 50 | "eslint": "^1.10.3", 51 | "eslint-config-airbnb": "^4.0.0", 52 | "eslint-plugin-react": "^3.16.1", 53 | "in-publish": "^2.0.0", 54 | "jsdom": "^3.1.2", 55 | "mocha": "^2.4.5", 56 | "react": "^0.14.7", 57 | "react-addons-test-utils": "^0.14.7", 58 | "sinon": "^1.17.3", 59 | "sinon-chai": "^2.8.0" 60 | }, 61 | "dependencies": { 62 | "hoist-non-react-statics": "^1.0.5", 63 | "react-display-name": "^0.1.1" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { toggleAware } from './toggleAware'; 2 | -------------------------------------------------------------------------------- /src/toggleAware.js: -------------------------------------------------------------------------------- 1 | import { createElement, Component } from 'react'; 2 | import hoist from 'hoist-non-react-statics'; 3 | import getDisplayName from 'react-display-name'; 4 | 5 | export const toggleAware = ({ 6 | onDelay = 0, 7 | offDelay = 0, 8 | handler = 'onToggle', 9 | key = 'isToggled', 10 | } = {}) => (OriginalComponent) => { 11 | // create higher order component to track state 12 | class WrappedComponent extends Component { 13 | 14 | static displayName = `ToggleAware(${getDisplayName(OriginalComponent)})`; 15 | 16 | constructor() { 17 | super(); 18 | 19 | this.timeout = null; 20 | this.goal = false; 21 | this.state = { 22 | active: false, 23 | }; 24 | } 25 | 26 | componentWillUnmount() { 27 | if (this.timeout) clearTimeout(this.timeout); 28 | } 29 | 30 | onToggle = (...args) => { 31 | if (this.props[handler]) this.props[handler](...args); 32 | if (this.timeout) clearTimeout(this.timeout); 33 | 34 | // eye on the prize 35 | const goal = this.goal = !this.goal; 36 | const delay = goal ? onDelay : offDelay; 37 | 38 | const commit = () => { 39 | this.timeout = null; 40 | this.setState({ active: goal }); 41 | }; 42 | 43 | 44 | if (delay) { 45 | this.timeout = setTimeout(commit, delay); 46 | } else { 47 | commit(); 48 | } 49 | }; 50 | 51 | render() { 52 | const props = { 53 | ...this.props, 54 | [key]: this.state.active, 55 | [handler]: this.onToggle, 56 | }; 57 | 58 | return createElement(OriginalComponent, props); 59 | } 60 | } 61 | 62 | return hoist(WrappedComponent, Component); 63 | }; 64 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require babel-register 2 | --require test/setup 3 | --check-leaks 4 | --throw-deprecation 5 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import { jsdom } from 'jsdom'; 2 | import chai from 'chai'; 3 | import sinon from 'sinon'; 4 | import sinonChai from "sinon-chai"; 5 | chai.use(sinonChai); 6 | 7 | global.expect = chai.expect; 8 | global.sinon = sinon; 9 | global.document = jsdom(''); 10 | global.window = document.parentWindow; 11 | global.navigator = {userAgent: 'node.js'}; 12 | -------------------------------------------------------------------------------- /test/toggleAware-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react-addons-test-utils'; 3 | import { toggleAware } from '../src/toggleAware'; 4 | 5 | describe('toggleAware decorator', function() { 6 | 7 | it('should be a function', function() { 8 | expect(toggleAware).to.be.instanceOf(Function); 9 | }); 10 | 11 | it('should optionally take an options object', function() { 12 | expect(() => toggleAware()).to.not.throw(); 13 | expect(() => toggleAware({})).to.not.throw(); 14 | }); 15 | 16 | it('should return a toggleAware component factory', function() { 17 | expect(toggleAware()).to.be.instanceOf(Function); 18 | }); 19 | }); 20 | 21 | describe('toggleAware component factory', function() { 22 | 23 | let factory = null; 24 | before(function() { 25 | factory = toggleAware(); 26 | }); 27 | 28 | it('should be a function', function() { 29 | expect(factory).to.be.instanceOf(Function); 30 | }); 31 | 32 | it('must accept one Component class', function() { 33 | class Test extends React.Component {}; 34 | expect(() => factory()).to.throw(); 35 | expect(() => factory(Test)).to.not.throw(); 36 | }); 37 | 38 | it('must return a WrappedComponent', function() { 39 | class Test extends React.Component {}; 40 | const res = factory(Test); 41 | 42 | // expect(res).to.be.instanceOf(React.Component); // TODO why doesn't this work?? 43 | expect(res.displayName).to.equal('ToggleAware(Test)'); 44 | }); 45 | }); 46 | 47 | describe('toggleAware Component', function() { 48 | 49 | class _Test extends React.Component { 50 | render() { 51 | props = this.props; 52 | return
; 53 | } 54 | } 55 | 56 | let props = null; 57 | beforeEach(function() { 58 | props = null; 59 | }); 60 | 61 | it('should use the default options if none are provided', function() { 62 | const Test = toggleAware()(_Test); 63 | TestUtils.renderIntoDocument(); 64 | 65 | // has default property names 66 | expect(props.isToggled).to.exist; 67 | expect(props.onToggle).to.be.instanceOf(Function); 68 | 69 | // starts inactive 70 | expect(props.isToggled).to.be.false; 71 | 72 | // syncronously becomes active 73 | props.onToggle(); 74 | expect(props.isToggled).to.be.true; 75 | 76 | // syncronously becomes inactive 77 | props.onToggle(); 78 | expect(props.isToggled).to.be.false; 79 | }); 80 | 81 | it('should honor the "onDelay" option', function(done) { 82 | const Test = toggleAware({ onDelay: 100 })(_Test); 83 | TestUtils.renderIntoDocument(); 84 | 85 | // starts inactive 86 | expect(props.isToggled).to.be.false; 87 | 88 | // asynchronously becomes active 89 | props.onToggle(); 90 | expect(props.isToggled).to.be.false; 91 | setTimeout(function() { 92 | expect(props.isToggled).to.be.true; 93 | done(); 94 | }, 300); 95 | }); 96 | 97 | it('should honor the "offDelay" option', function(done) { 98 | const Test = toggleAware({ offDelay: 100 })(_Test); 99 | TestUtils.renderIntoDocument(); 100 | 101 | // starts inactive 102 | expect(props.isToggled).to.be.false; 103 | 104 | // syncronously becomes active 105 | props.onToggle(); 106 | expect(props.isToggled).to.be.true; 107 | 108 | // asynchronously becomes inactive 109 | props.onToggle(); 110 | setTimeout(function() { 111 | expect(props.isToggled).to.be.false; 112 | done(); 113 | }, 300); 114 | }); 115 | 116 | it('should honor the "handler" property', function() { 117 | const Test = toggleAware({ handler: 'foo' })(_Test); 118 | TestUtils.renderIntoDocument(); 119 | 120 | expect(props.foo).to.be.instanceOf(Function); 121 | expect(props.onToggle).to.be.undefined; 122 | }); 123 | 124 | it('should allow name collisions of "handler" property name', function() { 125 | const Test = toggleAware()(_Test); 126 | const spy = sinon.spy(); 127 | TestUtils.renderIntoDocument(); 128 | props.onToggle(); 129 | expect(spy).to.be.called; 130 | expect(props.isToggled).to.be.true; 131 | }); 132 | 133 | it('should honor the "key" property', function() { 134 | const Test = toggleAware({ key: 'foo' })(_Test); 135 | TestUtils.renderIntoDocument(); 136 | 137 | expect(props.foo).to.be.false; 138 | expect(props.isToggled).to.be.undefined; 139 | }); 140 | }); 141 | --------------------------------------------------------------------------------