├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── README.md ├── demo └── src │ └── index.js ├── nwb.config.js ├── package.json ├── react-lever.png ├── src ├── Lever.js ├── LeverProvider.js ├── context.js ├── index.js └── useLever.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 2 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@medipass/react-medipass" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /demo/dist 3 | /es 4 | /lib 5 | /node_modules 6 | /umd 7 | npm-debug.log* 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | node_js: 5 | - 8 6 | 7 | before_install: 8 | - npm install codecov.io coveralls 9 | 10 | after_success: 11 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js 12 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 13 | 14 | branches: 15 | only: 16 | - master 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | [Node.js](http://nodejs.org/) >= v4 must be installed. 4 | 5 | ## Installation 6 | 7 | - Running `npm install` in the component's root directory will install everything you need for development. 8 | 9 | ## Demo Development Server 10 | 11 | - `npm start` will run a development server with the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading. 12 | 13 | ## Running Tests 14 | 15 | - `npm test` will run the tests once. 16 | 17 | - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`. 18 | 19 | - `npm run test:watch` will run the tests on every change. 20 | 21 | ## Building 22 | 23 | - `npm run build` will build the component for publishing to npm and also bundle the demo app. 24 | 25 | - `npm run clean` will delete built resources. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Lever 2 | 3 | > A library to conditionally render React components based on feature toggles. 4 | 5 |

6 | 7 | ## Table of Contents 8 | 9 | - [React Lever](#react-lever) 10 | - [Table of Contents](#table-of-contents) 11 | - [Installation](#installation) 12 | - [Usage](#usage) 13 | - [Using Components](#using-components) 14 | - [Using Hooks](#using-hooks) 15 | - [`` props](#leverprovider-props) 16 | - [isDev](#isdev) 17 | - [features](#features) 18 | - [`` props](#lever-props) 19 | - [feature](#feature) 20 | - [either](#either) 21 | - [enabled](#enabled) 22 | - [devOnly](#devonly) 23 | - [`useLever(feature[, options])`](#useleverfeature-options) 24 | - [feature](#feature-1) 25 | - [options](#options) 26 | - [License](#license) 27 | 28 | ## Installation 29 | 30 | ``` 31 | npm install react-lever --save 32 | ``` 33 | 34 | or install with [Yarn](https://yarnpkg.com) if you prefer: 35 | 36 | ``` 37 | yarn add react-lever 38 | ``` 39 | - [React Lever](#react-lever) 40 | - [Table of Contents](#table-of-contents) 41 | - [Installation](#installation) 42 | - [Usage](#usage) 43 | - [Using Components](#using-components) 44 | - [Using Hooks](#using-hooks) 45 | - [`` props](#leverprovider-props) 46 | - [isDev](#isdev) 47 | - [features](#features) 48 | - [`` props](#lever-props) 49 | - [feature](#feature) 50 | - [either](#either) 51 | - [disabled](#disabled) 52 | - [forceEnabled](#forceenabled) 53 | - [devOnly](#devonly) 54 | - [`useLever(feature[, options])`](#useleverfeature-options) 55 | - [feature](#feature-1) 56 | - [options](#options) 57 | - [License](#license) 58 | ## Usage 59 | 60 | ### Using Components 61 | 62 | Wrap your application in a ``, and your features in a `` like so: 63 | 64 | ```jsx 65 | import React, { Component, Fragment } from 'react'; 66 | import ReactDOM from 'react-dom'; 67 | import Lever, { LeverProvider } from 'react-lever'; 68 | 69 | function AnimalPhotos() { 70 | return ( 71 | 72 |

Photos of animals

73 | 74 | {/* This will render as enabled=true */} 75 | 76 | 77 | 78 | 79 | {/* This will not render as enabled=false */} 80 | 81 | 82 | 83 | 84 | {/* This will not render as cats is disabled (all feature have to be enabled) */} 85 | 86 | 87 | 88 | 89 | 90 | {/* This will render as dogs is enabled (at least one feature has to be enabled) */} 91 | 92 | 93 | 94 | 95 | 96 | {/* This will render as enabled=false */} 97 | 98 | You can't see mah catz! 99 | 100 | 101 | {/* This will render as cats & koalas are disabled (all features have to be disabled) */} 102 | 103 | You can't see mah catz or koalaz! 104 | 105 | 106 | {/* This will render as cats is disabled (at least one feature has to be disabled) */} 107 | 108 | I may have a dog and I may have a cat 109 | 110 | 111 | {/* This will render as enabled=true, but will only render if in a development environment as devOnly=true. */} 112 | 113 | 114 | 115 |
116 | ) 117 | } 118 | 119 | const features = { 120 | dogs: { enabled: true }, 121 | cats: { enabled: false }, 122 | parrots: { enabled: true, devOnly: true }, 123 | koalas: { enabled: false }, 124 | }; 125 | 126 | ReactDOM.render( 127 | 128 | 129 | , 130 | document.querySelector('#root') 131 | ); 132 | ``` 133 | 134 | ### Using Hooks 135 | 136 | React Lever also supports React Hooks 137 | 138 | ```jsx 139 | import { useLever } from 'react-lever'; 140 | 141 | function AnimalPhotos() { 142 | const isDogPhotosEnabled = useLever('dogs'); 143 | const isCatPhotosEnabled = useLever('cats'); 144 | const isDogAndCatPhotosEnabled = useLever(['cats', 'dogs']); 145 | const isDogOrCatPhotosEnabled = useLever(['cats', 'dogs'], { either: true }); 146 | const isCatPhotosDisabled = useLever('cats', { disabled: true }); 147 | const isCatAndKoalaPhotosDisabled = useLever(['cats', 'koalas'], { disabled: true }); 148 | const isCatOrDogPhotosDisabled = useLever(['cats', 'koalas'], { disabled: true, either: true }); 149 | const isParrotPhotosEnabled = useLever('parrot'); 150 | return ( 151 | 152 |

Photos of animals

153 | {isDogPhotosEnabled && } 154 | {isCatPhotosEnabled && } 155 | {isDogAndCatPhotosEnabled && ( 156 | 157 | 158 | 159 | 160 | )} 161 | {isDogOrCatPhotosEnabled && ( 162 | 163 | 164 | 165 | 166 | )} 167 | {isCatPhotosDisabled && `You can't see mah catz!`} 168 | {isCatAndKoalaPhotosDisabled && `You can't see mah catz or koalaz!`} 169 | {isCatOrDogPhotosDisabled && `I may not have a dog and/or I may not have a cat`} 170 | {isParrotPhotosEnabled && } 171 |
172 | ) 173 | } 174 | ``` 175 | 176 | ## `` props 177 | 178 | ### isDev 179 | 180 | > `boolean` | Optional 181 | 182 | Is the app in a development environment? 183 | 184 | If `false`, and a feature is flagged with `enabled` and `devOnly` attributes as `true`, then the feature will not render. 185 | 186 | ### features 187 | 188 | > `{ [feature]: { enabled: boolean, devOnly: boolean } }` | Required 189 | 190 | The global features of the application. 191 | 192 | ## `` props 193 | 194 | ### feature 195 | 196 | > `string | Array` | Required 197 | 198 | The key (or name) of the feature. 199 | 200 | ### either 201 | 202 | > `boolean` | Default: `false` 203 | 204 | If the `feature` prop is an array & either of the features are enabled, then render the children. 205 | 206 | ### disabled 207 | 208 | > `boolean` | Default: `false` 209 | 210 | If the feature is disabled, then the feature will render. 211 | 212 | ### forceEnabled 213 | 214 | > `boolean` | Optional 215 | 216 | If `true`, then the feature will render. This prop overrides the `enabled` flag in the ``'s features. 217 | 218 | ### devOnly 219 | 220 | > `boolean` | Optional 221 | 222 | If `true`, then the feature is available to the development environment only (as specified in ``'s `isDev` prop). This prop overrides the `devOnly` flag in the ``'s features. 223 | 224 | ## `useLever(feature[, options])` 225 | 226 | ### feature 227 | 228 | > `string` | Required 229 | 230 | The key (or name) of the feature. 231 | 232 | ### options 233 | 234 | > `Object{ disabled, forceEnabled, either, devOnly }` | Optional 235 | 236 | ## License 237 | 238 | MIT © [Medipass Solutions Pty. Ltd.](https://github.com/medipass) 239 | 240 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import Lever, { useLever, LeverProvider } from '../../src'; 5 | 6 | function LeverDemoComponents() { 7 | return ( 8 |
9 | 10 |
Feature 1
11 |
12 | 13 |
Feature 2
14 |
15 | 16 |
Feature 3
17 |
18 | 19 |
Feature 4
20 |
21 | 22 |
Feature 5
23 |
24 | 25 |
Feature 1 and Feature 2
26 |
27 | 28 |
Feature 1 or Feature 2
29 |
30 | 31 |
Feature 2 is disabled
32 |
33 | 34 |
Feature 2 and 4 is disabled
35 |
36 | 37 |
Feature 2 or 3 is disabled
38 |
39 |
40 | ); 41 | } 42 | 43 | function LeverDemoHooks() { 44 | const isFeature1Enabled = useLever('feature1'); 45 | const isFeature2Enabled = useLever('feature2'); 46 | const isFeature3Enabled = useLever('feature3'); 47 | const isFeature4Enabled = useLever('feature4', { forceEnabled: true }); 48 | const isFeature5Enabled = useLever('feature5', { forceEnabled: true, devOnly: true }); 49 | const isFeature1And2Enabled = useLever(['feature1', 'feature2']); 50 | const isFeature1Or2Enabled = useLever(['feature1', 'feature2'], { either: true }); 51 | const isFeature2Disabled = useLever('feature2', { disabled: true }); 52 | const isFeature2and4Disabled = useLever(['feature2', 'feature4'], { disabled: true }); 53 | const isFeature2or3Disabled = useLever(['feature2', 'feature3'], { disabled: true, either: true }); 54 | return ( 55 |
56 | {isFeature1Enabled &&
Feature 1
} 57 | {isFeature2Enabled &&
Feature 2
} 58 | {isFeature3Enabled &&
Feature 3
} 59 | {isFeature4Enabled &&
Feature 4
} 60 | {isFeature5Enabled &&
Feature 5
} 61 | {isFeature1And2Enabled &&
Feature 1 and Feature 2
} 62 | {isFeature1Or2Enabled &&
Feature 1 or Feature 2
} 63 | {isFeature2Disabled &&
Feature 2 is disabled
} 64 | {isFeature2and4Disabled &&
Feature 2 and 4 is disabled
} 65 | {isFeature2or3Disabled &&
Feature 2 or 3 is disabled
} 66 |
67 | ); 68 | } 69 | 70 | const features = { 71 | feature1: { enabled: true }, 72 | feature2: { enabled: false }, 73 | feature3: { enabled: true, devOnly: true }, 74 | feature4: { enabled: false }, 75 | feature5: { enabled: false } 76 | }; 77 | render( 78 | 79 |

Components

80 | 81 |

Hooks

82 | 83 |
, 84 | document.querySelector('#demo') 85 | ); 86 | -------------------------------------------------------------------------------- /nwb.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'react-component', 3 | npm: { 4 | esModules: true, 5 | umd: { 6 | global: 'Lever', 7 | externals: { 8 | react: 'React' 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-lever", 3 | "version": "2.0.0", 4 | "description": "Minimalistic feature toggling for React", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "files": [ 8 | "css", 9 | "es", 10 | "lib", 11 | "umd" 12 | ], 13 | "scripts": { 14 | "build": "nwb build-react-component", 15 | "clean": "nwb clean-module && nwb clean-demo", 16 | "start": "nwb serve-react-demo", 17 | "lint": "eslint src/", 18 | "prepublish": "npm run build" 19 | }, 20 | "dependencies": { 21 | "lodash": "4.17.11" 22 | }, 23 | "peerDependencies": { 24 | "react": ">=16.8.0", 25 | "react-dom": ">=16.8.0" 26 | }, 27 | "devDependencies": { 28 | "@medipass/eslint-config-react-medipass": "8.3.6", 29 | "babel-eslint": "9.0.0", 30 | "eslint": "5.5.0", 31 | "eslint-plugin-import": "2.14.0", 32 | "eslint-plugin-prettier": "2.6.2", 33 | "nwb": "0.23.0", 34 | "prop-types": "15.6.2", 35 | "react": "16.8.6", 36 | "react-dom": "16.8.6" 37 | }, 38 | "author": "Jake Moxey", 39 | "homepage": "https://medipass.com.au", 40 | "license": "MIT", 41 | "repository": "", 42 | "keywords": [ 43 | "react-component" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /react-lever.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medipass/react-lever/9d9532db86166c9e566c3b7126e70280b60fb49e/react-lever.png -------------------------------------------------------------------------------- /src/Lever.js: -------------------------------------------------------------------------------- 1 | import useLever from './useLever'; 2 | 3 | export default function Lever(props) { 4 | const { children, feature, ...options } = props; 5 | const isEnabled = useLever(feature, options); 6 | if (isEnabled) return children; 7 | return null; 8 | } 9 | -------------------------------------------------------------------------------- /src/LeverProvider.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import _isEqual from 'lodash/isEqual'; 4 | 5 | import Context from './context'; 6 | 7 | export default class LeverProvider extends PureComponent { 8 | static propTypes = { 9 | children: PropTypes.node.isRequired, 10 | isDev: PropTypes.bool, 11 | features: PropTypes.object.isRequired 12 | }; 13 | 14 | static defaultProps = { 15 | isDev: false 16 | }; 17 | 18 | state = { 19 | isDev: this.props.isDev, 20 | features: this.props.features 21 | }; 22 | 23 | static getDerivedStateFromProps = (nextProps, prevState) => { 24 | if (!_isEqual(nextProps.features, prevState.features)) { 25 | return { 26 | features: nextProps.features 27 | }; 28 | } 29 | return null; 30 | }; 31 | 32 | render = () => { 33 | const { children } = this.props; 34 | return {children}; 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default React.createContext({ isDev: false, features: {} }); 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Lever'; 2 | export { default as LeverProvider } from './LeverProvider'; 3 | export { default as useLever } from './useLever'; 4 | -------------------------------------------------------------------------------- /src/useLever.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _get from 'lodash/get'; 3 | 4 | import Context from './context'; 5 | 6 | export default function useLever(feature, options = {}) { 7 | const { features: allFeatures, isDev } = React.useContext(Context); 8 | const { devOnly, disabled, forceEnabled } = options; 9 | 10 | let features = [feature]; 11 | if (Array.isArray(feature)) { 12 | features = feature; 13 | } 14 | 15 | const isEnabledSet = features.map(feature => { 16 | // If the feature is enabled, then we should render it. 17 | let isEnabled = forceEnabled || _get(allFeatures, `[${feature}].enabled`); 18 | if (disabled) { 19 | isEnabled = !isEnabled; 20 | } 21 | 22 | // If feature is a 'dev only' feature, and the environment is not a development environment, don't render the button. 23 | if ((devOnly || _get(allFeatures, `[${feature}].devOnly`)) && !isDev) { 24 | isEnabled = false; 25 | } 26 | 27 | return isEnabled; 28 | }); 29 | const isAllEnabled = !isEnabledSet.some(isEnabled => !isEnabled); 30 | const isAtLeastOneEnabled = isEnabledSet.some(isEnabled => isEnabled); 31 | 32 | if (options.either) { 33 | return isAtLeastOneEnabled; 34 | } 35 | return isAllEnabled; 36 | } 37 | --------------------------------------------------------------------------------