├── .babelrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── __tests__ ├── testAbTest.js ├── testExperimentClass.js ├── testNamespace.js ├── testParametrize.js └── utils │ └── experimentUtils.js ├── build.sh ├── build ├── index.js ├── webpack.config.js └── webpack.config.minified.js ├── dist ├── react-experiments.js └── react-experiments.min.js ├── examples ├── index.html ├── react-experiments.jpg └── styles.css ├── package.json └── src ├── Experiment.js ├── abtest.js ├── parametrize.js ├── parametrizeComponent.js ├── variationComponents.js └── withExperimentParams.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | 'presets': ['react', 'stage-0', 'es2015', 'jest'], 3 | 'plugins': [ 4 | "add-module-exports" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_store 2 | npm-debug.log 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "6.1" 5 | - "5.11" 6 | - "4.0" 7 | script: npm run-script test 8 | before_script: 9 | - npm install 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes in 5.1.0 2 | - Require latest version of planout 3 | 4 | # Changes in 4.1.0 5 | - Now transpile using babel 6 instead of babel 4 6 | 7 | # Changes in 3.0.1 8 | - Removed the ```shouldEnroll``` prop. If you want to use conditional enrollment then you should register the experimental input to the experiment you care about and then conditionally unenroll users in your PlanOut experiment definition (see here for more information: https://github.com/HubSpot/PlanOut.js/pull/15) 9 | 10 | # Changes in 3.0 11 | - Renamed experimentClass -> Experiment 12 | 13 | # Changes in 2.1 14 | - Added the ```parametrize``` function that takes in experiment information and a component and parametrizes the component with the experiment parameters as props. 15 | - Added the requirement to pass in an array of experiment parameters as props to the Parametrize component and removed the experimentName prop. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-experiments 2 | [![Build Status](https://travis-ci.org/rawls238/react-experiments.svg?branch=master)](https://travis-ci.org/rawls238/react-experiments) 3 | [![npm downloads](https://img.shields.io/npm/dm/react-experiments.svg?style=flat-square)](https://www.npmjs.com/package/react-experiments) 4 | 5 | 6 | ### This library is no longer actively maintained. ### 7 | 8 | react-experiments is a set of React components for implementing UI experiments. 9 | 10 | For additional context behind why we built this, check out the accompanying [blog post](http://product.hubspot.com/blog/product-experimentation-with-planout-and-react.js) 11 | 12 | # Installation 13 | 14 | ``` 15 | npm install react-experiments 16 | ``` 17 | 18 | # Usage 19 | 20 | **If you are using React 0.14 the latest supported version is 4.1.0** 21 | 22 | react-experiments was built to work with [PlanOut.js](https://www.github.com/HubSpot/PlanOut.js) and most of its constructs are inspired by the structure of PlanOut.js. This library will work out of the box if you pass it an instantiated PlanOut Namespace or Experiment class, but if you want to use your own methods of assigning experiment parameters and logging exposure then you can extend the base [experiment class](https://github.com/HubSpot/react-experiments/blob/master/src/Experiment.js) and pass that as the experiment class prop. 23 | 24 | ## Implementing a simple experiment 25 | 26 | This library serves as a way to declaratively implement UI experiments that are defined via PlanOut. The standard usage of this library is as follows: 27 | 28 | 1) Define experiment via PlanOut script / API. The PlanOut parameters that you set should map to the props on which you want to run an experiment. Let's use the [sample PlanOut.js experiment](https://github.com/HubSpot/PlanOut.js/blob/master/examples/sample_planout_es5.js#L41) as an example, which is effectively: 29 | 30 | ```js 31 | signupText = uniformChoice(choices=['Signup', 'Join now']) 32 | ``` 33 | 34 | 2) Wrap the component where you want to implement your UI experiment with the parametrize function provided by the library along with an instantiated experiment class and the specific parameter names that you want to parametrize the component with. As an example, 35 | 36 | ```js 37 | const Signup = parametrize(DummyExperiment, ['signupText'], React.createClass({ 38 | render() { 39 | return ( 40 |
41 | {this.props.signupText} 42 |
43 | ); 44 | } 45 | })); 46 | ``` 47 | 48 | Now you should be all set to run the sample experiment. The Signup component will render 'Sign up' or 'Join now' based on the randomized parameter assigned by PlanOut.js. 49 | 50 | To put it all together, 51 | 52 | ```js 53 | exp = new DummyExperiment({ id: 'this_is_the_user_id'}); 54 | 55 | let Signup = parametrize(exp, ['signupText'], React.createClass({ 56 | render() { 57 | return ( 58 |
59 | {this.props.signupText} 60 |
61 | ); 62 | } 63 | })); 64 | 65 | let Parent = React.createClass({ 66 | render() { 67 | ... 68 | 69 | } 70 | }); 71 | ``` 72 | 73 | 74 | ## Base Parametrize Component 75 | 76 | The implementation of all the components provided by this library are wrappers around a base ```Parametrize``` component. The ```Parametrize``` component allows for parametrizing a given component with experiment parameters. The following are the props that the ```Parametrize``` component takes: 77 | 78 | **experiment**: This is an instance of a PlanOut.js experiment/namespace class or the base Experiment class. [REQUIRED] 79 | 80 | 81 | **params**: This is the list of experiment parameters that you want to use to parametrize the component. They should correspond to the parameter names defined in your PlanOut script / experiment definition. [REQUIRED] 82 | 83 | [any arbitrary prop]: You can pass arbitrary props to the Parametrize component and they will be available via context.experimentProps in all descendants of the Parametrize component. 84 | 85 | ### Higher-order Parametrization Components 86 | 87 | There are two primary higher-order components to use for parametrization. 88 | 89 | **parametrize**: The ```parametrize``` function takes an instantiated experiment class, either an experiment name or a list of params, and a React component. It takes the given component and sets the deterministically and randomly assigned experiment parameters of the experiment class as props. 90 | 91 | ```parametrize(exp, ['signupText'], React.createClass({..}));``` 92 | 93 | **withExperimentParams**: The ```withExperimentParams``` function is used in combination with the base ```Parametrize``` component. It is useful when running an experiment with nested components, but generally the ```parametrize``` function should be preferred. 94 | 95 | 96 | ```js 97 | const Parent = createReactClass({ 98 | render() { 99 | return ( 100 | 101 | 102 | 103 | 104 | ); 105 | } 106 | }); 107 | 108 | const SignupHeader = withExperimentParams(createReactClass({ 109 | render() { 110 | return ( 111 |
112 | {this.props.signup_nav_text} 113 |
114 | ); 115 | } 116 | }); 117 | 118 | const SignupForm = withExperimentParams(createReactClass({ 119 | render() { 120 | return ( 121 |
122 | {this.props.signup_form_text} 123 |
124 | ); 125 | } 126 | }); 127 | ``` 128 | 129 | 130 | ## Running A/B Variation experiments: 131 | 132 | There are two common types of experimental parameters: 133 | 134 | 1) Parameters that correspond to parametrizations of existing variables and components. For instance, if one is running an experiment to test which shade of blue optimizes the click rate of the button, then the values to which your experiment parameters map would correspond to something such as the different hex codes for the different shades of blue. 135 | 136 | 2) "Branching" parameters where the parameter values correspond to different "variations" of the experiment. For instance, if one is testing two completely different user interfaces then it wouldn't make sense to parametrize every aspect that has changed, but rather to bin users into either 'Variation A' or 'Variation B'. 137 | 138 | While the core component of this library focuses on the first type of parameter, it also includes some convenience components built around the Parametrize component for running "branching" experiments using the ```ABTest``` component. 139 | 140 | ```javascript 141 | 142 | 143 | variation 1 144 | 145 | 146 | variation 2 147 | 148 | 149 | variation 3 150 | 151 | 152 | variation default 153 | 154 | 155 | ``` 156 | 157 | The ABTest component above branches off the value of ```this.props.experiment.get(this.props.on);```, ```TestNamespace.get('foo')``` in this case, and renders the When component where ```ABTest.props.experiment.get(ABTest.props.on) === ABTest.props.value```. If it doesn't find a corresponding When component to render then the Default component will render. This component makes implementing an experiment using "branching" parameters easy. 158 | 159 | The ABTest component takes the following as props: 160 | 161 | **experiment** - an instantiated PlanOut namespace/experiment class or a custom Experiment class. [REQUIRED] 162 | 163 | **on** - the parameter name to "branch" off [REQUIRED] 164 | 165 | ## Customized Experiment Components 166 | 167 | If you want to create your own experiment component you can extend the base Parametrize component. 168 | 169 | ## Logging 170 | 171 | react-experiments deals with logging experiment exposure. All the components provided by this library trigger an exposure log when the component is mounted. 172 | 173 | ## Development 174 | 175 | This project is written using ES6 and all build steps / transpilation are done by webpack. Be sure to run ```npm install``` to install the necessary development dependencies. In order to develop and test locally, it is necessary to simply run the ```build.sh``` shell script which will take care of transpiling to ES5 and running tests. 176 | 177 | To test API changes locally, open the examples/index.html file locally after building with your most recent changes. The index.html file contains a simple example of using this library paired with the [PlanOut.js sample experiment](https://github.com/HubSpot/PlanOut.js/blob/master/examples/sample_planout_es5.js). 178 | 179 | Please be sure to add tests to this project when making changes. This project uses [Jest](https://facebook.github.io/jest/) for running tests. Tests can be run either by building the project using build.sh or by using ```npm test```. 180 | -------------------------------------------------------------------------------- /__tests__/testAbTest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react-dom/test-utils'; 3 | import {DefaultExperiment, expInitializeObject, clearLogs, getLogLength} from './utils/experimentUtils'; 4 | import ReactExperiments from '../dist/react-experiments'; 5 | 6 | let exp; 7 | describe('Test experiment component', () => { 8 | 9 | beforeEach(() => { 10 | clearLogs(); 11 | exp = new DefaultExperiment(expInitializeObject); 12 | }); 13 | 14 | it('fetches the right value', () => { 15 | expect(exp.get('foo')).toEqual('Variation A'); 16 | }); 17 | 18 | it('renders only one, correct variation', () => { 19 | const experimentComponent = TestUtils.renderIntoDocument( 20 | 21 | 22 | 23 | foo 24 | 25 | 26 | 27 | 28 | foo 29 | 30 | 31 | 32 | ); 33 | 34 | //renders only one variation 35 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 36 | experimentComponent, 37 | 'experiment-variation-component' 38 | ).length).toBe(1); 39 | 40 | //renders the correct variation 41 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 42 | experimentComponent, 43 | 'variation-a' 44 | ).length).toBe(1); 45 | 46 | expect(getLogLength()).toEqual(1); 47 | }); 48 | 49 | it('renders the default variation when needed', () => { 50 | const experimentComponent = TestUtils.renderIntoDocument( 51 | 52 | 53 | foo 54 | 55 | 56 |
57 | Test 58 |
59 |
60 |
61 | ); 62 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 63 | experimentComponent, 64 | 'default-div' 65 | ).length).toBe(1); 66 | }); 67 | 68 | it('renders nothing with no default variation', () => { 69 | const experimentComponent = TestUtils.renderIntoDocument( 70 | 71 | 72 |
73 | test 74 |
75 |
76 |
77 | ); 78 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 79 | experimentComponent, 80 | 'foobar' 81 | ).length).toBe(0); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /__tests__/testExperimentClass.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createReactClass from 'create-react-class'; 3 | import TestUtils from 'react-dom/test-utils'; 4 | import PropTypes from 'prop-types'; 5 | import ReactExperiments from '../dist/react-experiments'; 6 | 7 | let logs = []; 8 | const paramKey = 'foo'; 9 | const paramVal = 'bar'; 10 | class Experiment extends ReactExperiments.Experiment { 11 | get(param) { 12 | if (param === paramKey) { 13 | return paramVal; 14 | } 15 | } 16 | 17 | logExposure(data) { 18 | logs.push(data); 19 | } 20 | 21 | getName() { 22 | return 'testExperiment'; 23 | } 24 | 25 | previouslyLogged() { 26 | return false; 27 | } 28 | } 29 | 30 | const exp = new Experiment(); 31 | describe('Test experiment', () => { 32 | beforeEach(() => { 33 | logs = []; 34 | }); 35 | 36 | it('should work with experiment Class', () => { 37 | const Comp = createReactClass({ 38 | contextTypes: { 39 | experimentParameters: PropTypes.object.isRequired 40 | }, 41 | 42 | render() { 43 | return ( 44 |
45 | Test 46 |
47 | ); 48 | } 49 | }); 50 | 51 | const parametrized = TestUtils.renderIntoDocument( 52 | 53 | 54 | 55 | ); 56 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 57 | parametrized, 58 | paramVal 59 | ).length).toBe(1); 60 | expect(logs.length).toEqual(1); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /__tests__/testNamespace.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createReactClass from 'create-react-class'; 3 | import TestUtils from 'react-dom/test-utils'; 4 | import {DefaultNamespace, DefaultEmptyNamespace, expInitializeObject, clearLogs, getLogLength} from './utils/experimentUtils'; 5 | import ReactExperiments from '../dist/react-experiments'; 6 | 7 | describe('Test that experiment component works with namespaces', () => { 8 | 9 | beforeEach(() => { 10 | clearLogs(); 11 | }); 12 | 13 | it('works when the user is enrolled in the namespace', () => { 14 | const namespace = new DefaultNamespace(expInitializeObject); 15 | const experimentComponent = TestUtils.renderIntoDocument( 16 | 17 | 18 | 19 | foo 20 | 21 | 22 | 23 | 24 | foo 25 | 26 | 27 | ); 28 | 29 | //renders only one variation 30 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 31 | experimentComponent, 32 | 'experiment-variation-component' 33 | ).length).toBe(1); 34 | 35 | //renders the correct variation 36 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 37 | experimentComponent, 38 | 'variation-b' 39 | ).length).toBe(1); 40 | 41 | expect(getLogLength()).toEqual(1); 42 | }); 43 | 44 | it('default component works when the user is not enrolled in a namespace', () => { 45 | const emptyNamespace = new DefaultEmptyNamespace(expInitializeObject); 46 | const experimentComponent = TestUtils.renderIntoDocument( 47 | 48 | 49 | 50 | foo 51 | 52 | 53 | 54 | 55 | foo 56 | 57 | 58 | 59 | 60 | rawr 61 | 62 | 63 | ); 64 | 65 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 66 | experimentComponent, 67 | 'experiment-variation-component' 68 | ).length).toBe(0); 69 | 70 | //renders the correct variation 71 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 72 | experimentComponent, 73 | 'default' 74 | ).length).toBe(1); 75 | 76 | expect(getLogLength()).toEqual(0); 77 | }); 78 | 79 | it('works when enrolled in the namespace with parametrize', () => { 80 | const namespace = new DefaultNamespace(expInitializeObject); 81 | let SampleComponent = createReactClass({ 82 | getClassName() { 83 | return this.props.foo || 'default' 84 | }, 85 | 86 | render() { 87 | return ( 88 |
89 | Test 90 |
91 | ); 92 | } 93 | }); 94 | SampleComponent = ReactExperiments.withExperimentParams(SampleComponent); 95 | const experimentComponent = TestUtils.renderIntoDocument( 96 | 97 | 98 | 99 | ); 100 | 101 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 102 | experimentComponent, 103 | 'Variation B' 104 | ).length).toBe(1); 105 | 106 | //renders the correct variation 107 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 108 | experimentComponent, 109 | 'default' 110 | ).length).toBe(0); 111 | 112 | expect(getLogLength()).toEqual(1); 113 | 114 | const experimentComponent2 = TestUtils.renderIntoDocument( 115 | 116 | 117 | 118 | ); 119 | 120 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 121 | experimentComponent2, 122 | 'Variation B' 123 | ).length).toBe(0); 124 | 125 | //renders the correct variation 126 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 127 | experimentComponent2, 128 | 'default' 129 | ).length).toBe(1); 130 | 131 | expect(getLogLength()).toEqual(1); 132 | }); 133 | 134 | 135 | it('default component works when the user is not enrolled in a namespace', () => { 136 | const emptyNamespace = new DefaultEmptyNamespace(expInitializeObject); 137 | let SampleComponent = createReactClass({ 138 | getClassName() { 139 | return this.props.foo || 'default' 140 | }, 141 | 142 | render() { 143 | return ( 144 |
145 | Test 146 |
147 | ); 148 | } 149 | }); 150 | SampleComponent = ReactExperiments.withExperimentParams(SampleComponent); 151 | const experimentComponent = TestUtils.renderIntoDocument( 152 | 153 | 154 | 155 | ); 156 | 157 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 158 | experimentComponent, 159 | 'Variation B' 160 | ).length).toBe(0); 161 | 162 | //renders the correct variation 163 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 164 | experimentComponent, 165 | 'default' 166 | ).length).toBe(1); 167 | 168 | expect(getLogLength()).toEqual(0); 169 | }); 170 | 171 | it('works when a user is enrolled in a different experiment in a namespace', () => { 172 | const namespace = new DefaultNamespace(expInitializeObject); 173 | let SampleComponent = createReactClass({ 174 | getClassName() { 175 | return this.props.foobar || 'default' 176 | }, 177 | 178 | render() { 179 | return ( 180 |
181 | Test 182 |
183 | ); 184 | } 185 | }); 186 | SampleComponent = ReactExperiments.withExperimentParams(SampleComponent); 187 | const experimentComponent = TestUtils.renderIntoDocument( 188 | 189 | 190 | 191 | ); 192 | 193 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 194 | experimentComponent, 195 | 'Variation B' 196 | ).length).toBe(0); 197 | 198 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 199 | experimentComponent, 200 | 'default' 201 | ).length).toBe(1); 202 | 203 | expect(getLogLength()).toEqual(0); 204 | }); 205 | }); 206 | -------------------------------------------------------------------------------- /__tests__/testParametrize.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createReactClass from 'create-react-class'; 3 | import PropTypes from 'prop-types'; 4 | import TestUtils from 'react-dom/test-utils'; 5 | import {DefaultExperiment, expInitializeObject, getLogLength, clearLogs} from './utils/experimentUtils'; 6 | import ReactExperiments from '../dist/react-experiments'; 7 | 8 | let exp; 9 | describe('Test parametrize component', () => { 10 | 11 | beforeEach(() => { 12 | clearLogs(); 13 | exp = new DefaultExperiment(expInitializeObject); 14 | }); 15 | 16 | it('should work with parametrize using props', () => { 17 | const ParametrizedComponent = createReactClass({ 18 | render() { 19 | return ( 20 | 21 | this.props.foo; 22 | 23 | ); 24 | } 25 | }); 26 | const ParametrizedComponentWithParams = ReactExperiments.withExperimentParams(ParametrizedComponent); 27 | const parametrized = TestUtils.renderIntoDocument( 28 | 29 | 30 | 31 | ); 32 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 33 | parametrized, 34 | 'Variation A' 35 | ).length).toBe(1); 36 | }); 37 | 38 | it('should work with parametrize using context', () => { 39 | const DoublyNestedComponent = createReactClass({ 40 | contextTypes: { 41 | experimentParameters: PropTypes.object.isRequired, 42 | }, 43 | 44 | render() { 45 | return ( 46 | 47 | foo 48 | 49 | ); 50 | } 51 | }); 52 | 53 | const NestedComponent = createReactClass({ 54 | contextTypes: { 55 | experimentParameters: PropTypes.object.isRequired, 56 | }, 57 | 58 | render() { 59 | return ( 60 | 61 | 62 | 63 | ); 64 | } 65 | }); 66 | 67 | const ParametrizedComponent = createReactClass({ 68 | render() { 69 | return ( 70 | 71 | 72 | 73 | ); 74 | } 75 | }); 76 | 77 | const parametrized = TestUtils.renderIntoDocument( 78 | 79 | 80 | 81 | ); 82 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 83 | parametrized, 84 | 'Variation A' 85 | ).length).toBe(1); 86 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 87 | parametrized, 88 | 'Num1' 89 | ).length).toBe(1); 90 | }); 91 | 92 | it('should work correctly after re-rendering', () => { 93 | const TestComponent = createReactClass({ 94 | contextTypes: { 95 | experimentParameters: PropTypes.object.isRequired, 96 | }, 97 | 98 | render() { 99 | if (!this.props.test) { 100 | return null; 101 | } 102 | return ( 103 |
104 | test2 105 |
106 | ); 107 | } 108 | }); 109 | 110 | const Parametrized = createReactClass({ 111 | render() { 112 | return ( 113 | 114 | 115 | 116 | ); 117 | } 118 | }); 119 | 120 | const RerenderingComponent = createReactClass({ 121 | getInitialState() { 122 | return { 123 | 'test': false 124 | } 125 | }, 126 | 127 | toggleTest() { 128 | this.setState({ 129 | test: !this.state.test 130 | }) 131 | }, 132 | 133 | render() { 134 | return ( 135 |
136 | 137 | 138 |
139 | ); 140 | } 141 | }); 142 | 143 | const experimentComponent = TestUtils.renderIntoDocument( 144 | 145 | ); 146 | 147 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 148 | experimentComponent, 149 | exp.get('foo') 150 | ).length).toBe(0); 151 | 152 | //click the button and the rendering component should have rendered 153 | const button = TestUtils.scryRenderedDOMComponentsWithClass(experimentComponent, 'input-button')[0]; 154 | TestUtils.Simulate.click(button); 155 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 156 | experimentComponent, 157 | exp.get('foo') 158 | ).length).toBe(1); 159 | 160 | //click it again and it should be gone 161 | TestUtils.Simulate.click(button); 162 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 163 | experimentComponent, 164 | exp.get('foo') 165 | ).length).toBe(0); 166 | }); 167 | 168 | it('should work with the withExperimentParams component', () => { 169 | const Buttons = createReactClass({ 170 | render() { 171 | return ( 172 |
173 |
174 | test 175 |
176 |
177 | testing2 178 |
179 |
180 | testing3 181 |
182 |
183 | ); 184 | } 185 | }); 186 | 187 | const ExpButton = ReactExperiments.withExperimentParams(Buttons); 188 | 189 | const otherVal = 'ha'; 190 | const parametrized = TestUtils.renderIntoDocument( 191 | 192 | 193 | 194 | ); 195 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 196 | parametrized, 197 | exp.get('foo') 198 | ).length).toBe(1); 199 | 200 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 201 | parametrized, 202 | exp.get('test2') 203 | ).length).toBe(1); 204 | 205 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 206 | parametrized, 207 | otherVal 208 | ).length).toBe(1); 209 | }); 210 | 211 | it('should work with higher order component and list of params', () => { 212 | const Buttons = ReactExperiments.parametrize(exp, ['foo', 'test2'], createReactClass({ 213 | render() { 214 | return ( 215 |
216 |
217 | test 218 |
219 |
220 | testing2 221 |
222 |
223 | testing3 224 |
225 |
226 | ); 227 | } 228 | })); 229 | 230 | const otherVal = 'ha'; 231 | const parametrized = TestUtils.renderIntoDocument( 232 | 233 | ); 234 | 235 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 236 | parametrized, 237 | exp.get('foo') 238 | ).length).toBe(1); 239 | 240 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 241 | parametrized, 242 | exp.get('test2') 243 | ).length).toBe(1); 244 | 245 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 246 | parametrized, 247 | otherVal 248 | ).length).toBe(1); 249 | }); 250 | 251 | it('should not log exposure if no valid params are passed', () => { 252 | const Test = ReactExperiments.parametrize(exp, ['foobar'], createReactClass({ 253 | render() { 254 | return ( 255 |
256 |
257 | testing2 258 |
259 |
260 | ); 261 | } 262 | })); 263 | const otherVal = 'ha'; 264 | const parametrized = TestUtils.renderIntoDocument( 265 | 266 | ); 267 | expect(getLogLength()).toEqual(0); 268 | expect(TestUtils.scryRenderedDOMComponentsWithClass( 269 | parametrized, 270 | exp.get('foo') 271 | ).length).toBe(0); 272 | }); 273 | }); 274 | -------------------------------------------------------------------------------- /__tests__/utils/experimentUtils.js: -------------------------------------------------------------------------------- 1 | import {Experiment, Namespace, Ops} from 'planout'; 2 | 3 | let globalLog = []; 4 | class DefaultExperiment extends Experiment { 5 | setup() { 6 | this.setName("SampleExperiment"); 7 | } 8 | 9 | assign(params, args) { 10 | params.set('foo', 11 | new Ops.Random.UniformChoice({ 12 | 'choices': ['Variation A', 'Variation B'], 13 | 'unit': args.id 14 | }) 15 | ); 16 | 17 | params.set('test2', 18 | new Ops.Random.UniformChoice({ 19 | 'choices': ['Num1', 'Num2'], 20 | 'unit': args.id 21 | }) 22 | ); 23 | } 24 | 25 | configureLogger() { 26 | return; 27 | } 28 | 29 | log(stuff) { 30 | globalLog.push(stuff); 31 | } 32 | 33 | getParamNames() { 34 | return this.getDefaultParamNames(); 35 | } 36 | 37 | previouslyLogged() { 38 | return this._exposureLogged; 39 | } 40 | }; 41 | 42 | class DefaultExperiment2 extends Experiment { 43 | setup() { 44 | this.setName('SampleExperiment2'); 45 | } 46 | 47 | assign(params, args) { 48 | params.set('foobar', 49 | new Ops.Random.UniformChoice({ 50 | 'choices': ['Variation A', 'Variation B'], 51 | 'unit': args.id 52 | }) 53 | ); 54 | } 55 | 56 | configureLogger() { 57 | return; 58 | } 59 | 60 | log(stuff) { 61 | globalLog.push(stuff); 62 | } 63 | 64 | getParamNames() { 65 | return this.getDefaultParamNames(); 66 | } 67 | 68 | previouslyLogged() { 69 | return this._exposureLogged; 70 | } 71 | } 72 | 73 | class DefaultNamespace extends Namespace.SimpleNamespace { 74 | 75 | setupDefaults() { 76 | this.numSegments = 100; 77 | } 78 | 79 | setup() { 80 | this.setName('MyNamespace'); 81 | this.setPrimaryUnit('id'); 82 | } 83 | 84 | setupExperiments() { 85 | this.addExperiment('SampleExperiment', DefaultExperiment, 50); 86 | this.addExperiment('SampleExperiment2', DefaultExperiment2, 50); 87 | } 88 | }; 89 | 90 | class DefaultEmptyNamespace extends Namespace.SimpleNamespace { 91 | 92 | setupDefaults() { 93 | this.numSegments = 100; 94 | } 95 | 96 | setup() { 97 | this.setName('MyNamespace'); 98 | this.setPrimaryUnit('id'); 99 | } 100 | 101 | setupExperiments() { 102 | return; 103 | } 104 | }; 105 | 106 | 107 | const expInitializeObject = { id: 2333 }; 108 | 109 | const clearLogs = () => { 110 | globalLog = []; 111 | } 112 | 113 | const getLogLength = () => { 114 | return globalLog.length; 115 | } 116 | 117 | export default { DefaultExperiment, DefaultEmptyNamespace, DefaultNamespace, expInitializeObject, clearLogs, getLogLength }; -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | node node_modules/webpack/bin/webpack --config build/webpack.config.js 2 | node node_modules/webpack/bin/webpack --config build/webpack.config.minified.js 3 | -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | import * as Variations from '../src/variationComponents'; 2 | import Experiment from '../src/Experiment'; 3 | import ABTest from '../src/abtest'; 4 | import Parametrize from '../src/parametrize'; 5 | import withExperimentParams from '../src/withExperimentParams'; 6 | import parametrize from '../src/parametrizeComponent'; 7 | 8 | export default { 9 | ABTest: ABTest, 10 | When: Variations.When, 11 | Default: Variations.Default, 12 | Experiment: Experiment, 13 | Parametrize: Parametrize, 14 | withExperimentParams: withExperimentParams, 15 | parametrize: parametrize 16 | }; 17 | -------------------------------------------------------------------------------- /build/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './build/index.js', 3 | module: { 4 | loaders: [ 5 | { 6 | test: /\.js$/, 7 | exclude: /node_modules/, 8 | loader: 'babel-loader', 9 | query: { 10 | cacheDirectory: true, 11 | presets: ['react', 'es2015', 'jest'], 12 | plugins: ['add-module-exports'] 13 | } 14 | }, 15 | ] 16 | }, 17 | output: { 18 | filename: './dist/react-experiments.js', 19 | libraryTarget: 'umd', 20 | library: 'ReactExperiments' 21 | }, 22 | externals: [ 23 | { 24 | "react": { 25 | root: "React", 26 | commonjs2: "react", 27 | commonjs: "react", 28 | amd: "react" 29 | } 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /build/webpack.config.minified.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | 3 | module.exports = { 4 | entry: './build/index.js', 5 | module: { 6 | loaders: [ 7 | { 8 | test: /\.js$/, 9 | exclude: /node_modules/, 10 | loader: 'babel-loader', 11 | query: { 12 | cacheDirectory: true, 13 | presets: ['react', 'es2015', 'jest'], 14 | plugins: ['add-module-exports'] 15 | } 16 | }, 17 | ] 18 | }, 19 | output: { 20 | filename: './dist/react-experiments.min.js', 21 | libraryTarget: 'umd', 22 | library: 'ReactExperiments' 23 | }, 24 | plugins: [ 25 | new webpack.optimize.UglifyJsPlugin({minimize: true}) 26 | ], 27 | externals: [ 28 | { 29 | "react": { 30 | root: "React", 31 | commonjs2: "react", 32 | commonjs: "react", 33 | amd: "react" 34 | } 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /dist/react-experiments.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(require("react")); 4 | else if(typeof define === 'function' && define.amd) 5 | define(["react"], factory); 6 | else if(typeof exports === 'object') 7 | exports["ReactExperiments"] = factory(require("react")); 8 | else 9 | root["ReactExperiments"] = factory(root["React"]); 10 | })(this, function(__WEBPACK_EXTERNAL_MODULE_2__) { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) 20 | /******/ return installedModules[moduleId].exports; 21 | 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ exports: {}, 25 | /******/ id: moduleId, 26 | /******/ loaded: false 27 | /******/ }; 28 | 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | 32 | /******/ // Flag the module as loaded 33 | /******/ module.loaded = true; 34 | 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | 39 | 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | 46 | /******/ // __webpack_public_path__ 47 | /******/ __webpack_require__.p = ""; 48 | 49 | /******/ // Load entry module and return exports 50 | /******/ return __webpack_require__(0); 51 | /******/ }) 52 | /************************************************************************/ 53 | /******/ ([ 54 | /* 0 */ 55 | /***/ (function(module, exports, __webpack_require__) { 56 | 57 | 'use strict'; 58 | 59 | Object.defineProperty(exports, "__esModule", { 60 | value: true 61 | }); 62 | 63 | var _variationComponents = __webpack_require__(1); 64 | 65 | var Variations = _interopRequireWildcard(_variationComponents); 66 | 67 | var _Experiment = __webpack_require__(16); 68 | 69 | var _Experiment2 = _interopRequireDefault(_Experiment); 70 | 71 | var _abtest = __webpack_require__(17); 72 | 73 | var _abtest2 = _interopRequireDefault(_abtest); 74 | 75 | var _parametrize = __webpack_require__(18); 76 | 77 | var _parametrize2 = _interopRequireDefault(_parametrize); 78 | 79 | var _withExperimentParams = __webpack_require__(19); 80 | 81 | var _withExperimentParams2 = _interopRequireDefault(_withExperimentParams); 82 | 83 | var _parametrizeComponent = __webpack_require__(20); 84 | 85 | var _parametrizeComponent2 = _interopRequireDefault(_parametrizeComponent); 86 | 87 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 88 | 89 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 90 | 91 | exports.default = { 92 | ABTest: _abtest2.default, 93 | When: Variations.When, 94 | Default: Variations.Default, 95 | Experiment: _Experiment2.default, 96 | Parametrize: _parametrize2.default, 97 | withExperimentParams: _withExperimentParams2.default, 98 | parametrize: _parametrizeComponent2.default 99 | }; 100 | module.exports = exports['default']; 101 | 102 | /***/ }), 103 | /* 1 */ 104 | /***/ (function(module, exports, __webpack_require__) { 105 | 106 | 'use strict'; 107 | 108 | Object.defineProperty(exports, "__esModule", { 109 | value: true 110 | }); 111 | exports.Default = exports.When = undefined; 112 | 113 | var _react = __webpack_require__(2); 114 | 115 | var _react2 = _interopRequireDefault(_react); 116 | 117 | var _propTypes = __webpack_require__(3); 118 | 119 | var _propTypes2 = _interopRequireDefault(_propTypes); 120 | 121 | var _createReactClass = __webpack_require__(13); 122 | 123 | var _createReactClass2 = _interopRequireDefault(_createReactClass); 124 | 125 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 126 | 127 | var When = exports.When = (0, _createReactClass2.default)({ 128 | displayName: 'When', 129 | getInitialState: function getInitialState() { 130 | return { 131 | shouldRender: false 132 | }; 133 | }, 134 | 135 | 136 | contextTypes: { 137 | experimentParameters: _propTypes2.default.object.isRequired, 138 | experimentProps: _propTypes2.default.object.isRequired 139 | }, 140 | 141 | componentWillUpdate: function componentWillUpdate(props, state) { 142 | if (state.shouldRender) { 143 | this.context.experimentProps.enrolledInVariation(); 144 | } 145 | }, 146 | componentDidMount: function componentDidMount() { 147 | this.shouldRenderVariation(); 148 | }, 149 | shouldRenderVariation: function shouldRenderVariation() { 150 | var value = this.props.value; 151 | var paramName = this.context.experimentProps.on; 152 | if (this.context.experimentParameters && this.context.experimentParameters[paramName] === value) { 153 | this.setState({ 154 | shouldRender: true 155 | }); 156 | } 157 | }, 158 | renderChildren: function renderChildren() { 159 | return _react2.default.Children.map(this.props.children, function (child) { 160 | if (_react2.default.isValidElement(child)) { 161 | return _react2.default.cloneElement(child, {}); 162 | } 163 | return child; 164 | }); 165 | }, 166 | render: function render() { 167 | if (!this.state.shouldRender) { 168 | return null; 169 | } 170 | 171 | return _react2.default.createElement( 172 | 'span', 173 | { className: 'experiment-variation-component' }, 174 | this.renderChildren() 175 | ); 176 | } 177 | }); 178 | 179 | var Default = exports.Default = (0, _createReactClass2.default)({ 180 | displayName: 'Default', 181 | 182 | contextTypes: { 183 | experimentProps: _propTypes2.default.object.isRequired 184 | }, 185 | 186 | render: function render() { 187 | if (this.context.experimentProps.hasRendered) { 188 | return null; 189 | } 190 | 191 | return _react2.default.createElement( 192 | 'span', 193 | null, 194 | this.props.children 195 | ); 196 | } 197 | }); 198 | 199 | /***/ }), 200 | /* 2 */ 201 | /***/ (function(module, exports) { 202 | 203 | module.exports = __WEBPACK_EXTERNAL_MODULE_2__; 204 | 205 | /***/ }), 206 | /* 3 */ 207 | /***/ (function(module, exports, __webpack_require__) { 208 | 209 | /* WEBPACK VAR INJECTION */(function(process) {/** 210 | * Copyright (c) 2013-present, Facebook, Inc. 211 | * 212 | * This source code is licensed under the MIT license found in the 213 | * LICENSE file in the root directory of this source tree. 214 | */ 215 | 216 | if (process.env.NODE_ENV !== 'production') { 217 | var REACT_ELEMENT_TYPE = (typeof Symbol === 'function' && 218 | Symbol.for && 219 | Symbol.for('react.element')) || 220 | 0xeac7; 221 | 222 | var isValidElement = function(object) { 223 | return typeof object === 'object' && 224 | object !== null && 225 | object.$$typeof === REACT_ELEMENT_TYPE; 226 | }; 227 | 228 | // By explicitly using `prop-types` you are opting into new development behavior. 229 | // http://fb.me/prop-types-in-prod 230 | var throwOnDirectAccess = true; 231 | module.exports = __webpack_require__(5)(isValidElement, throwOnDirectAccess); 232 | } else { 233 | // By explicitly using `prop-types` you are opting into new production behavior. 234 | // http://fb.me/prop-types-in-prod 235 | module.exports = __webpack_require__(12)(); 236 | } 237 | 238 | /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4))) 239 | 240 | /***/ }), 241 | /* 4 */ 242 | /***/ (function(module, exports) { 243 | 244 | // shim for using process in browser 245 | var process = module.exports = {}; 246 | 247 | // cached from whatever global is present so that test runners that stub it 248 | // don't break things. But we need to wrap it in a try catch in case it is 249 | // wrapped in strict mode code which doesn't define any globals. It's inside a 250 | // function because try/catches deoptimize in certain engines. 251 | 252 | var cachedSetTimeout; 253 | var cachedClearTimeout; 254 | 255 | function defaultSetTimout() { 256 | throw new Error('setTimeout has not been defined'); 257 | } 258 | function defaultClearTimeout () { 259 | throw new Error('clearTimeout has not been defined'); 260 | } 261 | (function () { 262 | try { 263 | if (typeof setTimeout === 'function') { 264 | cachedSetTimeout = setTimeout; 265 | } else { 266 | cachedSetTimeout = defaultSetTimout; 267 | } 268 | } catch (e) { 269 | cachedSetTimeout = defaultSetTimout; 270 | } 271 | try { 272 | if (typeof clearTimeout === 'function') { 273 | cachedClearTimeout = clearTimeout; 274 | } else { 275 | cachedClearTimeout = defaultClearTimeout; 276 | } 277 | } catch (e) { 278 | cachedClearTimeout = defaultClearTimeout; 279 | } 280 | } ()) 281 | function runTimeout(fun) { 282 | if (cachedSetTimeout === setTimeout) { 283 | //normal enviroments in sane situations 284 | return setTimeout(fun, 0); 285 | } 286 | // if setTimeout wasn't available but was latter defined 287 | if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { 288 | cachedSetTimeout = setTimeout; 289 | return setTimeout(fun, 0); 290 | } 291 | try { 292 | // when when somebody has screwed with setTimeout but no I.E. maddness 293 | return cachedSetTimeout(fun, 0); 294 | } catch(e){ 295 | try { 296 | // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally 297 | return cachedSetTimeout.call(null, fun, 0); 298 | } catch(e){ 299 | // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error 300 | return cachedSetTimeout.call(this, fun, 0); 301 | } 302 | } 303 | 304 | 305 | } 306 | function runClearTimeout(marker) { 307 | if (cachedClearTimeout === clearTimeout) { 308 | //normal enviroments in sane situations 309 | return clearTimeout(marker); 310 | } 311 | // if clearTimeout wasn't available but was latter defined 312 | if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { 313 | cachedClearTimeout = clearTimeout; 314 | return clearTimeout(marker); 315 | } 316 | try { 317 | // when when somebody has screwed with setTimeout but no I.E. maddness 318 | return cachedClearTimeout(marker); 319 | } catch (e){ 320 | try { 321 | // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally 322 | return cachedClearTimeout.call(null, marker); 323 | } catch (e){ 324 | // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. 325 | // Some versions of I.E. have different rules for clearTimeout vs setTimeout 326 | return cachedClearTimeout.call(this, marker); 327 | } 328 | } 329 | 330 | 331 | 332 | } 333 | var queue = []; 334 | var draining = false; 335 | var currentQueue; 336 | var queueIndex = -1; 337 | 338 | function cleanUpNextTick() { 339 | if (!draining || !currentQueue) { 340 | return; 341 | } 342 | draining = false; 343 | if (currentQueue.length) { 344 | queue = currentQueue.concat(queue); 345 | } else { 346 | queueIndex = -1; 347 | } 348 | if (queue.length) { 349 | drainQueue(); 350 | } 351 | } 352 | 353 | function drainQueue() { 354 | if (draining) { 355 | return; 356 | } 357 | var timeout = runTimeout(cleanUpNextTick); 358 | draining = true; 359 | 360 | var len = queue.length; 361 | while(len) { 362 | currentQueue = queue; 363 | queue = []; 364 | while (++queueIndex < len) { 365 | if (currentQueue) { 366 | currentQueue[queueIndex].run(); 367 | } 368 | } 369 | queueIndex = -1; 370 | len = queue.length; 371 | } 372 | currentQueue = null; 373 | draining = false; 374 | runClearTimeout(timeout); 375 | } 376 | 377 | process.nextTick = function (fun) { 378 | var args = new Array(arguments.length - 1); 379 | if (arguments.length > 1) { 380 | for (var i = 1; i < arguments.length; i++) { 381 | args[i - 1] = arguments[i]; 382 | } 383 | } 384 | queue.push(new Item(fun, args)); 385 | if (queue.length === 1 && !draining) { 386 | runTimeout(drainQueue); 387 | } 388 | }; 389 | 390 | // v8 likes predictible objects 391 | function Item(fun, array) { 392 | this.fun = fun; 393 | this.array = array; 394 | } 395 | Item.prototype.run = function () { 396 | this.fun.apply(null, this.array); 397 | }; 398 | process.title = 'browser'; 399 | process.browser = true; 400 | process.env = {}; 401 | process.argv = []; 402 | process.version = ''; // empty string to avoid regexp issues 403 | process.versions = {}; 404 | 405 | function noop() {} 406 | 407 | process.on = noop; 408 | process.addListener = noop; 409 | process.once = noop; 410 | process.off = noop; 411 | process.removeListener = noop; 412 | process.removeAllListeners = noop; 413 | process.emit = noop; 414 | process.prependListener = noop; 415 | process.prependOnceListener = noop; 416 | 417 | process.listeners = function (name) { return [] } 418 | 419 | process.binding = function (name) { 420 | throw new Error('process.binding is not supported'); 421 | }; 422 | 423 | process.cwd = function () { return '/' }; 424 | process.chdir = function (dir) { 425 | throw new Error('process.chdir is not supported'); 426 | }; 427 | process.umask = function() { return 0; }; 428 | 429 | 430 | /***/ }), 431 | /* 5 */ 432 | /***/ (function(module, exports, __webpack_require__) { 433 | 434 | /* WEBPACK VAR INJECTION */(function(process) {/** 435 | * Copyright (c) 2013-present, Facebook, Inc. 436 | * 437 | * This source code is licensed under the MIT license found in the 438 | * LICENSE file in the root directory of this source tree. 439 | */ 440 | 441 | 'use strict'; 442 | 443 | var emptyFunction = __webpack_require__(6); 444 | var invariant = __webpack_require__(7); 445 | var warning = __webpack_require__(8); 446 | var assign = __webpack_require__(9); 447 | 448 | var ReactPropTypesSecret = __webpack_require__(10); 449 | var checkPropTypes = __webpack_require__(11); 450 | 451 | module.exports = function(isValidElement, throwOnDirectAccess) { 452 | /* global Symbol */ 453 | var ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator; 454 | var FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec. 455 | 456 | /** 457 | * Returns the iterator method function contained on the iterable object. 458 | * 459 | * Be sure to invoke the function with the iterable as context: 460 | * 461 | * var iteratorFn = getIteratorFn(myIterable); 462 | * if (iteratorFn) { 463 | * var iterator = iteratorFn.call(myIterable); 464 | * ... 465 | * } 466 | * 467 | * @param {?object} maybeIterable 468 | * @return {?function} 469 | */ 470 | function getIteratorFn(maybeIterable) { 471 | var iteratorFn = maybeIterable && (ITERATOR_SYMBOL && maybeIterable[ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL]); 472 | if (typeof iteratorFn === 'function') { 473 | return iteratorFn; 474 | } 475 | } 476 | 477 | /** 478 | * Collection of methods that allow declaration and validation of props that are 479 | * supplied to React components. Example usage: 480 | * 481 | * var Props = require('ReactPropTypes'); 482 | * var MyArticle = React.createClass({ 483 | * propTypes: { 484 | * // An optional string prop named "description". 485 | * description: Props.string, 486 | * 487 | * // A required enum prop named "category". 488 | * category: Props.oneOf(['News','Photos']).isRequired, 489 | * 490 | * // A prop named "dialog" that requires an instance of Dialog. 491 | * dialog: Props.instanceOf(Dialog).isRequired 492 | * }, 493 | * render: function() { ... } 494 | * }); 495 | * 496 | * A more formal specification of how these methods are used: 497 | * 498 | * type := array|bool|func|object|number|string|oneOf([...])|instanceOf(...) 499 | * decl := ReactPropTypes.{type}(.isRequired)? 500 | * 501 | * Each and every declaration produces a function with the same signature. This 502 | * allows the creation of custom validation functions. For example: 503 | * 504 | * var MyLink = React.createClass({ 505 | * propTypes: { 506 | * // An optional string or URI prop named "href". 507 | * href: function(props, propName, componentName) { 508 | * var propValue = props[propName]; 509 | * if (propValue != null && typeof propValue !== 'string' && 510 | * !(propValue instanceof URI)) { 511 | * return new Error( 512 | * 'Expected a string or an URI for ' + propName + ' in ' + 513 | * componentName 514 | * ); 515 | * } 516 | * } 517 | * }, 518 | * render: function() {...} 519 | * }); 520 | * 521 | * @internal 522 | */ 523 | 524 | var ANONYMOUS = '<>'; 525 | 526 | // Important! 527 | // Keep this list in sync with production version in `./factoryWithThrowingShims.js`. 528 | var ReactPropTypes = { 529 | array: createPrimitiveTypeChecker('array'), 530 | bool: createPrimitiveTypeChecker('boolean'), 531 | func: createPrimitiveTypeChecker('function'), 532 | number: createPrimitiveTypeChecker('number'), 533 | object: createPrimitiveTypeChecker('object'), 534 | string: createPrimitiveTypeChecker('string'), 535 | symbol: createPrimitiveTypeChecker('symbol'), 536 | 537 | any: createAnyTypeChecker(), 538 | arrayOf: createArrayOfTypeChecker, 539 | element: createElementTypeChecker(), 540 | instanceOf: createInstanceTypeChecker, 541 | node: createNodeChecker(), 542 | objectOf: createObjectOfTypeChecker, 543 | oneOf: createEnumTypeChecker, 544 | oneOfType: createUnionTypeChecker, 545 | shape: createShapeTypeChecker, 546 | exact: createStrictShapeTypeChecker, 547 | }; 548 | 549 | /** 550 | * inlined Object.is polyfill to avoid requiring consumers ship their own 551 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is 552 | */ 553 | /*eslint-disable no-self-compare*/ 554 | function is(x, y) { 555 | // SameValue algorithm 556 | if (x === y) { 557 | // Steps 1-5, 7-10 558 | // Steps 6.b-6.e: +0 != -0 559 | return x !== 0 || 1 / x === 1 / y; 560 | } else { 561 | // Step 6.a: NaN == NaN 562 | return x !== x && y !== y; 563 | } 564 | } 565 | /*eslint-enable no-self-compare*/ 566 | 567 | /** 568 | * We use an Error-like object for backward compatibility as people may call 569 | * PropTypes directly and inspect their output. However, we don't use real 570 | * Errors anymore. We don't inspect their stack anyway, and creating them 571 | * is prohibitively expensive if they are created too often, such as what 572 | * happens in oneOfType() for any type before the one that matched. 573 | */ 574 | function PropTypeError(message) { 575 | this.message = message; 576 | this.stack = ''; 577 | } 578 | // Make `instanceof Error` still work for returned errors. 579 | PropTypeError.prototype = Error.prototype; 580 | 581 | function createChainableTypeChecker(validate) { 582 | if (process.env.NODE_ENV !== 'production') { 583 | var manualPropTypeCallCache = {}; 584 | var manualPropTypeWarningCount = 0; 585 | } 586 | function checkType(isRequired, props, propName, componentName, location, propFullName, secret) { 587 | componentName = componentName || ANONYMOUS; 588 | propFullName = propFullName || propName; 589 | 590 | if (secret !== ReactPropTypesSecret) { 591 | if (throwOnDirectAccess) { 592 | // New behavior only for users of `prop-types` package 593 | invariant( 594 | false, 595 | 'Calling PropTypes validators directly is not supported by the `prop-types` package. ' + 596 | 'Use `PropTypes.checkPropTypes()` to call them. ' + 597 | 'Read more at http://fb.me/use-check-prop-types' 598 | ); 599 | } else if (process.env.NODE_ENV !== 'production' && typeof console !== 'undefined') { 600 | // Old behavior for people using React.PropTypes 601 | var cacheKey = componentName + ':' + propName; 602 | if ( 603 | !manualPropTypeCallCache[cacheKey] && 604 | // Avoid spamming the console because they are often not actionable except for lib authors 605 | manualPropTypeWarningCount < 3 606 | ) { 607 | warning( 608 | false, 609 | 'You are manually calling a React.PropTypes validation ' + 610 | 'function for the `%s` prop on `%s`. This is deprecated ' + 611 | 'and will throw in the standalone `prop-types` package. ' + 612 | 'You may be seeing this warning due to a third-party PropTypes ' + 613 | 'library. See https://fb.me/react-warning-dont-call-proptypes ' + 'for details.', 614 | propFullName, 615 | componentName 616 | ); 617 | manualPropTypeCallCache[cacheKey] = true; 618 | manualPropTypeWarningCount++; 619 | } 620 | } 621 | } 622 | if (props[propName] == null) { 623 | if (isRequired) { 624 | if (props[propName] === null) { 625 | return new PropTypeError('The ' + location + ' `' + propFullName + '` is marked as required ' + ('in `' + componentName + '`, but its value is `null`.')); 626 | } 627 | return new PropTypeError('The ' + location + ' `' + propFullName + '` is marked as required in ' + ('`' + componentName + '`, but its value is `undefined`.')); 628 | } 629 | return null; 630 | } else { 631 | return validate(props, propName, componentName, location, propFullName); 632 | } 633 | } 634 | 635 | var chainedCheckType = checkType.bind(null, false); 636 | chainedCheckType.isRequired = checkType.bind(null, true); 637 | 638 | return chainedCheckType; 639 | } 640 | 641 | function createPrimitiveTypeChecker(expectedType) { 642 | function validate(props, propName, componentName, location, propFullName, secret) { 643 | var propValue = props[propName]; 644 | var propType = getPropType(propValue); 645 | if (propType !== expectedType) { 646 | // `propValue` being instance of, say, date/regexp, pass the 'object' 647 | // check, but we can offer a more precise error message here rather than 648 | // 'of type `object`'. 649 | var preciseType = getPreciseType(propValue); 650 | 651 | return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + preciseType + '` supplied to `' + componentName + '`, expected ') + ('`' + expectedType + '`.')); 652 | } 653 | return null; 654 | } 655 | return createChainableTypeChecker(validate); 656 | } 657 | 658 | function createAnyTypeChecker() { 659 | return createChainableTypeChecker(emptyFunction.thatReturnsNull); 660 | } 661 | 662 | function createArrayOfTypeChecker(typeChecker) { 663 | function validate(props, propName, componentName, location, propFullName) { 664 | if (typeof typeChecker !== 'function') { 665 | return new PropTypeError('Property `' + propFullName + '` of component `' + componentName + '` has invalid PropType notation inside arrayOf.'); 666 | } 667 | var propValue = props[propName]; 668 | if (!Array.isArray(propValue)) { 669 | var propType = getPropType(propValue); 670 | return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected an array.')); 671 | } 672 | for (var i = 0; i < propValue.length; i++) { 673 | var error = typeChecker(propValue, i, componentName, location, propFullName + '[' + i + ']', ReactPropTypesSecret); 674 | if (error instanceof Error) { 675 | return error; 676 | } 677 | } 678 | return null; 679 | } 680 | return createChainableTypeChecker(validate); 681 | } 682 | 683 | function createElementTypeChecker() { 684 | function validate(props, propName, componentName, location, propFullName) { 685 | var propValue = props[propName]; 686 | if (!isValidElement(propValue)) { 687 | var propType = getPropType(propValue); 688 | return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected a single ReactElement.')); 689 | } 690 | return null; 691 | } 692 | return createChainableTypeChecker(validate); 693 | } 694 | 695 | function createInstanceTypeChecker(expectedClass) { 696 | function validate(props, propName, componentName, location, propFullName) { 697 | if (!(props[propName] instanceof expectedClass)) { 698 | var expectedClassName = expectedClass.name || ANONYMOUS; 699 | var actualClassName = getClassName(props[propName]); 700 | return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + actualClassName + '` supplied to `' + componentName + '`, expected ') + ('instance of `' + expectedClassName + '`.')); 701 | } 702 | return null; 703 | } 704 | return createChainableTypeChecker(validate); 705 | } 706 | 707 | function createEnumTypeChecker(expectedValues) { 708 | if (!Array.isArray(expectedValues)) { 709 | process.env.NODE_ENV !== 'production' ? warning(false, 'Invalid argument supplied to oneOf, expected an instance of array.') : void 0; 710 | return emptyFunction.thatReturnsNull; 711 | } 712 | 713 | function validate(props, propName, componentName, location, propFullName) { 714 | var propValue = props[propName]; 715 | for (var i = 0; i < expectedValues.length; i++) { 716 | if (is(propValue, expectedValues[i])) { 717 | return null; 718 | } 719 | } 720 | 721 | var valuesString = JSON.stringify(expectedValues); 722 | return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of value `' + propValue + '` ' + ('supplied to `' + componentName + '`, expected one of ' + valuesString + '.')); 723 | } 724 | return createChainableTypeChecker(validate); 725 | } 726 | 727 | function createObjectOfTypeChecker(typeChecker) { 728 | function validate(props, propName, componentName, location, propFullName) { 729 | if (typeof typeChecker !== 'function') { 730 | return new PropTypeError('Property `' + propFullName + '` of component `' + componentName + '` has invalid PropType notation inside objectOf.'); 731 | } 732 | var propValue = props[propName]; 733 | var propType = getPropType(propValue); 734 | if (propType !== 'object') { 735 | return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected an object.')); 736 | } 737 | for (var key in propValue) { 738 | if (propValue.hasOwnProperty(key)) { 739 | var error = typeChecker(propValue, key, componentName, location, propFullName + '.' + key, ReactPropTypesSecret); 740 | if (error instanceof Error) { 741 | return error; 742 | } 743 | } 744 | } 745 | return null; 746 | } 747 | return createChainableTypeChecker(validate); 748 | } 749 | 750 | function createUnionTypeChecker(arrayOfTypeCheckers) { 751 | if (!Array.isArray(arrayOfTypeCheckers)) { 752 | process.env.NODE_ENV !== 'production' ? warning(false, 'Invalid argument supplied to oneOfType, expected an instance of array.') : void 0; 753 | return emptyFunction.thatReturnsNull; 754 | } 755 | 756 | for (var i = 0; i < arrayOfTypeCheckers.length; i++) { 757 | var checker = arrayOfTypeCheckers[i]; 758 | if (typeof checker !== 'function') { 759 | warning( 760 | false, 761 | 'Invalid argument supplied to oneOfType. Expected an array of check functions, but ' + 762 | 'received %s at index %s.', 763 | getPostfixForTypeWarning(checker), 764 | i 765 | ); 766 | return emptyFunction.thatReturnsNull; 767 | } 768 | } 769 | 770 | function validate(props, propName, componentName, location, propFullName) { 771 | for (var i = 0; i < arrayOfTypeCheckers.length; i++) { 772 | var checker = arrayOfTypeCheckers[i]; 773 | if (checker(props, propName, componentName, location, propFullName, ReactPropTypesSecret) == null) { 774 | return null; 775 | } 776 | } 777 | 778 | return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`.')); 779 | } 780 | return createChainableTypeChecker(validate); 781 | } 782 | 783 | function createNodeChecker() { 784 | function validate(props, propName, componentName, location, propFullName) { 785 | if (!isNode(props[propName])) { 786 | return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`, expected a ReactNode.')); 787 | } 788 | return null; 789 | } 790 | return createChainableTypeChecker(validate); 791 | } 792 | 793 | function createShapeTypeChecker(shapeTypes) { 794 | function validate(props, propName, componentName, location, propFullName) { 795 | var propValue = props[propName]; 796 | var propType = getPropType(propValue); 797 | if (propType !== 'object') { 798 | return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type `' + propType + '` ' + ('supplied to `' + componentName + '`, expected `object`.')); 799 | } 800 | for (var key in shapeTypes) { 801 | var checker = shapeTypes[key]; 802 | if (!checker) { 803 | continue; 804 | } 805 | var error = checker(propValue, key, componentName, location, propFullName + '.' + key, ReactPropTypesSecret); 806 | if (error) { 807 | return error; 808 | } 809 | } 810 | return null; 811 | } 812 | return createChainableTypeChecker(validate); 813 | } 814 | 815 | function createStrictShapeTypeChecker(shapeTypes) { 816 | function validate(props, propName, componentName, location, propFullName) { 817 | var propValue = props[propName]; 818 | var propType = getPropType(propValue); 819 | if (propType !== 'object') { 820 | return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type `' + propType + '` ' + ('supplied to `' + componentName + '`, expected `object`.')); 821 | } 822 | // We need to check all keys in case some are required but missing from 823 | // props. 824 | var allKeys = assign({}, props[propName], shapeTypes); 825 | for (var key in allKeys) { 826 | var checker = shapeTypes[key]; 827 | if (!checker) { 828 | return new PropTypeError( 829 | 'Invalid ' + location + ' `' + propFullName + '` key `' + key + '` supplied to `' + componentName + '`.' + 830 | '\nBad object: ' + JSON.stringify(props[propName], null, ' ') + 831 | '\nValid keys: ' + JSON.stringify(Object.keys(shapeTypes), null, ' ') 832 | ); 833 | } 834 | var error = checker(propValue, key, componentName, location, propFullName + '.' + key, ReactPropTypesSecret); 835 | if (error) { 836 | return error; 837 | } 838 | } 839 | return null; 840 | } 841 | 842 | return createChainableTypeChecker(validate); 843 | } 844 | 845 | function isNode(propValue) { 846 | switch (typeof propValue) { 847 | case 'number': 848 | case 'string': 849 | case 'undefined': 850 | return true; 851 | case 'boolean': 852 | return !propValue; 853 | case 'object': 854 | if (Array.isArray(propValue)) { 855 | return propValue.every(isNode); 856 | } 857 | if (propValue === null || isValidElement(propValue)) { 858 | return true; 859 | } 860 | 861 | var iteratorFn = getIteratorFn(propValue); 862 | if (iteratorFn) { 863 | var iterator = iteratorFn.call(propValue); 864 | var step; 865 | if (iteratorFn !== propValue.entries) { 866 | while (!(step = iterator.next()).done) { 867 | if (!isNode(step.value)) { 868 | return false; 869 | } 870 | } 871 | } else { 872 | // Iterator will provide entry [k,v] tuples rather than values. 873 | while (!(step = iterator.next()).done) { 874 | var entry = step.value; 875 | if (entry) { 876 | if (!isNode(entry[1])) { 877 | return false; 878 | } 879 | } 880 | } 881 | } 882 | } else { 883 | return false; 884 | } 885 | 886 | return true; 887 | default: 888 | return false; 889 | } 890 | } 891 | 892 | function isSymbol(propType, propValue) { 893 | // Native Symbol. 894 | if (propType === 'symbol') { 895 | return true; 896 | } 897 | 898 | // 19.4.3.5 Symbol.prototype[@@toStringTag] === 'Symbol' 899 | if (propValue['@@toStringTag'] === 'Symbol') { 900 | return true; 901 | } 902 | 903 | // Fallback for non-spec compliant Symbols which are polyfilled. 904 | if (typeof Symbol === 'function' && propValue instanceof Symbol) { 905 | return true; 906 | } 907 | 908 | return false; 909 | } 910 | 911 | // Equivalent of `typeof` but with special handling for array and regexp. 912 | function getPropType(propValue) { 913 | var propType = typeof propValue; 914 | if (Array.isArray(propValue)) { 915 | return 'array'; 916 | } 917 | if (propValue instanceof RegExp) { 918 | // Old webkits (at least until Android 4.0) return 'function' rather than 919 | // 'object' for typeof a RegExp. We'll normalize this here so that /bla/ 920 | // passes PropTypes.object. 921 | return 'object'; 922 | } 923 | if (isSymbol(propType, propValue)) { 924 | return 'symbol'; 925 | } 926 | return propType; 927 | } 928 | 929 | // This handles more types than `getPropType`. Only used for error messages. 930 | // See `createPrimitiveTypeChecker`. 931 | function getPreciseType(propValue) { 932 | if (typeof propValue === 'undefined' || propValue === null) { 933 | return '' + propValue; 934 | } 935 | var propType = getPropType(propValue); 936 | if (propType === 'object') { 937 | if (propValue instanceof Date) { 938 | return 'date'; 939 | } else if (propValue instanceof RegExp) { 940 | return 'regexp'; 941 | } 942 | } 943 | return propType; 944 | } 945 | 946 | // Returns a string that is postfixed to a warning about an invalid type. 947 | // For example, "undefined" or "of type array" 948 | function getPostfixForTypeWarning(value) { 949 | var type = getPreciseType(value); 950 | switch (type) { 951 | case 'array': 952 | case 'object': 953 | return 'an ' + type; 954 | case 'boolean': 955 | case 'date': 956 | case 'regexp': 957 | return 'a ' + type; 958 | default: 959 | return type; 960 | } 961 | } 962 | 963 | // Returns class name of the object, if any. 964 | function getClassName(propValue) { 965 | if (!propValue.constructor || !propValue.constructor.name) { 966 | return ANONYMOUS; 967 | } 968 | return propValue.constructor.name; 969 | } 970 | 971 | ReactPropTypes.checkPropTypes = checkPropTypes; 972 | ReactPropTypes.PropTypes = ReactPropTypes; 973 | 974 | return ReactPropTypes; 975 | }; 976 | 977 | /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4))) 978 | 979 | /***/ }), 980 | /* 6 */ 981 | /***/ (function(module, exports) { 982 | 983 | "use strict"; 984 | 985 | /** 986 | * Copyright (c) 2013-present, Facebook, Inc. 987 | * 988 | * This source code is licensed under the MIT license found in the 989 | * LICENSE file in the root directory of this source tree. 990 | * 991 | * 992 | */ 993 | 994 | function makeEmptyFunction(arg) { 995 | return function () { 996 | return arg; 997 | }; 998 | } 999 | 1000 | /** 1001 | * This function accepts and discards inputs; it has no side effects. This is 1002 | * primarily useful idiomatically for overridable function endpoints which 1003 | * always need to be callable, since JS lacks a null-call idiom ala Cocoa. 1004 | */ 1005 | var emptyFunction = function emptyFunction() {}; 1006 | 1007 | emptyFunction.thatReturns = makeEmptyFunction; 1008 | emptyFunction.thatReturnsFalse = makeEmptyFunction(false); 1009 | emptyFunction.thatReturnsTrue = makeEmptyFunction(true); 1010 | emptyFunction.thatReturnsNull = makeEmptyFunction(null); 1011 | emptyFunction.thatReturnsThis = function () { 1012 | return this; 1013 | }; 1014 | emptyFunction.thatReturnsArgument = function (arg) { 1015 | return arg; 1016 | }; 1017 | 1018 | module.exports = emptyFunction; 1019 | 1020 | /***/ }), 1021 | /* 7 */ 1022 | /***/ (function(module, exports, __webpack_require__) { 1023 | 1024 | /* WEBPACK VAR INJECTION */(function(process) {/** 1025 | * Copyright (c) 2013-present, Facebook, Inc. 1026 | * 1027 | * This source code is licensed under the MIT license found in the 1028 | * LICENSE file in the root directory of this source tree. 1029 | * 1030 | */ 1031 | 1032 | 'use strict'; 1033 | 1034 | /** 1035 | * Use invariant() to assert state which your program assumes to be true. 1036 | * 1037 | * Provide sprintf-style format (only %s is supported) and arguments 1038 | * to provide information about what broke and what you were 1039 | * expecting. 1040 | * 1041 | * The invariant message will be stripped in production, but the invariant 1042 | * will remain to ensure logic does not differ in production. 1043 | */ 1044 | 1045 | var validateFormat = function validateFormat(format) {}; 1046 | 1047 | if (process.env.NODE_ENV !== 'production') { 1048 | validateFormat = function validateFormat(format) { 1049 | if (format === undefined) { 1050 | throw new Error('invariant requires an error message argument'); 1051 | } 1052 | }; 1053 | } 1054 | 1055 | function invariant(condition, format, a, b, c, d, e, f) { 1056 | validateFormat(format); 1057 | 1058 | if (!condition) { 1059 | var error; 1060 | if (format === undefined) { 1061 | error = new Error('Minified exception occurred; use the non-minified dev environment ' + 'for the full error message and additional helpful warnings.'); 1062 | } else { 1063 | var args = [a, b, c, d, e, f]; 1064 | var argIndex = 0; 1065 | error = new Error(format.replace(/%s/g, function () { 1066 | return args[argIndex++]; 1067 | })); 1068 | error.name = 'Invariant Violation'; 1069 | } 1070 | 1071 | error.framesToPop = 1; // we don't care about invariant's own frame 1072 | throw error; 1073 | } 1074 | } 1075 | 1076 | module.exports = invariant; 1077 | /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4))) 1078 | 1079 | /***/ }), 1080 | /* 8 */ 1081 | /***/ (function(module, exports, __webpack_require__) { 1082 | 1083 | /* WEBPACK VAR INJECTION */(function(process) {/** 1084 | * Copyright (c) 2014-present, Facebook, Inc. 1085 | * 1086 | * This source code is licensed under the MIT license found in the 1087 | * LICENSE file in the root directory of this source tree. 1088 | * 1089 | */ 1090 | 1091 | 'use strict'; 1092 | 1093 | var emptyFunction = __webpack_require__(6); 1094 | 1095 | /** 1096 | * Similar to invariant but only logs a warning if the condition is not met. 1097 | * This can be used to log issues in development environments in critical 1098 | * paths. Removing the logging code for production environments will keep the 1099 | * same logic and follow the same code paths. 1100 | */ 1101 | 1102 | var warning = emptyFunction; 1103 | 1104 | if (process.env.NODE_ENV !== 'production') { 1105 | var printWarning = function printWarning(format) { 1106 | for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 1107 | args[_key - 1] = arguments[_key]; 1108 | } 1109 | 1110 | var argIndex = 0; 1111 | var message = 'Warning: ' + format.replace(/%s/g, function () { 1112 | return args[argIndex++]; 1113 | }); 1114 | if (typeof console !== 'undefined') { 1115 | console.error(message); 1116 | } 1117 | try { 1118 | // --- Welcome to debugging React --- 1119 | // This error was thrown as a convenience so that you can use this stack 1120 | // to find the callsite that caused this warning to fire. 1121 | throw new Error(message); 1122 | } catch (x) {} 1123 | }; 1124 | 1125 | warning = function warning(condition, format) { 1126 | if (format === undefined) { 1127 | throw new Error('`warning(condition, format, ...args)` requires a warning ' + 'message argument'); 1128 | } 1129 | 1130 | if (format.indexOf('Failed Composite propType: ') === 0) { 1131 | return; // Ignore CompositeComponent proptype check. 1132 | } 1133 | 1134 | if (!condition) { 1135 | for (var _len2 = arguments.length, args = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) { 1136 | args[_key2 - 2] = arguments[_key2]; 1137 | } 1138 | 1139 | printWarning.apply(undefined, [format].concat(args)); 1140 | } 1141 | }; 1142 | } 1143 | 1144 | module.exports = warning; 1145 | /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4))) 1146 | 1147 | /***/ }), 1148 | /* 9 */ 1149 | /***/ (function(module, exports) { 1150 | 1151 | /* 1152 | object-assign 1153 | (c) Sindre Sorhus 1154 | @license MIT 1155 | */ 1156 | 1157 | 'use strict'; 1158 | /* eslint-disable no-unused-vars */ 1159 | var getOwnPropertySymbols = Object.getOwnPropertySymbols; 1160 | var hasOwnProperty = Object.prototype.hasOwnProperty; 1161 | var propIsEnumerable = Object.prototype.propertyIsEnumerable; 1162 | 1163 | function toObject(val) { 1164 | if (val === null || val === undefined) { 1165 | throw new TypeError('Object.assign cannot be called with null or undefined'); 1166 | } 1167 | 1168 | return Object(val); 1169 | } 1170 | 1171 | function shouldUseNative() { 1172 | try { 1173 | if (!Object.assign) { 1174 | return false; 1175 | } 1176 | 1177 | // Detect buggy property enumeration order in older V8 versions. 1178 | 1179 | // https://bugs.chromium.org/p/v8/issues/detail?id=4118 1180 | var test1 = new String('abc'); // eslint-disable-line no-new-wrappers 1181 | test1[5] = 'de'; 1182 | if (Object.getOwnPropertyNames(test1)[0] === '5') { 1183 | return false; 1184 | } 1185 | 1186 | // https://bugs.chromium.org/p/v8/issues/detail?id=3056 1187 | var test2 = {}; 1188 | for (var i = 0; i < 10; i++) { 1189 | test2['_' + String.fromCharCode(i)] = i; 1190 | } 1191 | var order2 = Object.getOwnPropertyNames(test2).map(function (n) { 1192 | return test2[n]; 1193 | }); 1194 | if (order2.join('') !== '0123456789') { 1195 | return false; 1196 | } 1197 | 1198 | // https://bugs.chromium.org/p/v8/issues/detail?id=3056 1199 | var test3 = {}; 1200 | 'abcdefghijklmnopqrst'.split('').forEach(function (letter) { 1201 | test3[letter] = letter; 1202 | }); 1203 | if (Object.keys(Object.assign({}, test3)).join('') !== 1204 | 'abcdefghijklmnopqrst') { 1205 | return false; 1206 | } 1207 | 1208 | return true; 1209 | } catch (err) { 1210 | // We don't expect any of the above to throw, but better to be safe. 1211 | return false; 1212 | } 1213 | } 1214 | 1215 | module.exports = shouldUseNative() ? Object.assign : function (target, source) { 1216 | var from; 1217 | var to = toObject(target); 1218 | var symbols; 1219 | 1220 | for (var s = 1; s < arguments.length; s++) { 1221 | from = Object(arguments[s]); 1222 | 1223 | for (var key in from) { 1224 | if (hasOwnProperty.call(from, key)) { 1225 | to[key] = from[key]; 1226 | } 1227 | } 1228 | 1229 | if (getOwnPropertySymbols) { 1230 | symbols = getOwnPropertySymbols(from); 1231 | for (var i = 0; i < symbols.length; i++) { 1232 | if (propIsEnumerable.call(from, symbols[i])) { 1233 | to[symbols[i]] = from[symbols[i]]; 1234 | } 1235 | } 1236 | } 1237 | } 1238 | 1239 | return to; 1240 | }; 1241 | 1242 | 1243 | /***/ }), 1244 | /* 10 */ 1245 | /***/ (function(module, exports) { 1246 | 1247 | /** 1248 | * Copyright (c) 2013-present, Facebook, Inc. 1249 | * 1250 | * This source code is licensed under the MIT license found in the 1251 | * LICENSE file in the root directory of this source tree. 1252 | */ 1253 | 1254 | 'use strict'; 1255 | 1256 | var ReactPropTypesSecret = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED'; 1257 | 1258 | module.exports = ReactPropTypesSecret; 1259 | 1260 | 1261 | /***/ }), 1262 | /* 11 */ 1263 | /***/ (function(module, exports, __webpack_require__) { 1264 | 1265 | /* WEBPACK VAR INJECTION */(function(process) {/** 1266 | * Copyright (c) 2013-present, Facebook, Inc. 1267 | * 1268 | * This source code is licensed under the MIT license found in the 1269 | * LICENSE file in the root directory of this source tree. 1270 | */ 1271 | 1272 | 'use strict'; 1273 | 1274 | if (process.env.NODE_ENV !== 'production') { 1275 | var invariant = __webpack_require__(7); 1276 | var warning = __webpack_require__(8); 1277 | var ReactPropTypesSecret = __webpack_require__(10); 1278 | var loggedTypeFailures = {}; 1279 | } 1280 | 1281 | /** 1282 | * Assert that the values match with the type specs. 1283 | * Error messages are memorized and will only be shown once. 1284 | * 1285 | * @param {object} typeSpecs Map of name to a ReactPropType 1286 | * @param {object} values Runtime values that need to be type-checked 1287 | * @param {string} location e.g. "prop", "context", "child context" 1288 | * @param {string} componentName Name of the component for error messages. 1289 | * @param {?Function} getStack Returns the component stack. 1290 | * @private 1291 | */ 1292 | function checkPropTypes(typeSpecs, values, location, componentName, getStack) { 1293 | if (process.env.NODE_ENV !== 'production') { 1294 | for (var typeSpecName in typeSpecs) { 1295 | if (typeSpecs.hasOwnProperty(typeSpecName)) { 1296 | var error; 1297 | // Prop type validation may throw. In case they do, we don't want to 1298 | // fail the render phase where it didn't fail before. So we log it. 1299 | // After these have been cleaned up, we'll let them throw. 1300 | try { 1301 | // This is intentionally an invariant that gets caught. It's the same 1302 | // behavior as without this statement except with a better message. 1303 | invariant(typeof typeSpecs[typeSpecName] === 'function', '%s: %s type `%s` is invalid; it must be a function, usually from ' + 'the `prop-types` package, but received `%s`.', componentName || 'React class', location, typeSpecName, typeof typeSpecs[typeSpecName]); 1304 | error = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, ReactPropTypesSecret); 1305 | } catch (ex) { 1306 | error = ex; 1307 | } 1308 | warning(!error || error instanceof Error, '%s: type specification of %s `%s` is invalid; the type checker ' + 'function must return `null` or an `Error` but returned a %s. ' + 'You may have forgotten to pass an argument to the type checker ' + 'creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and ' + 'shape all require an argument).', componentName || 'React class', location, typeSpecName, typeof error); 1309 | if (error instanceof Error && !(error.message in loggedTypeFailures)) { 1310 | // Only monitor this failure once because there tends to be a lot of the 1311 | // same error. 1312 | loggedTypeFailures[error.message] = true; 1313 | 1314 | var stack = getStack ? getStack() : ''; 1315 | 1316 | warning(false, 'Failed %s type: %s%s', location, error.message, stack != null ? stack : ''); 1317 | } 1318 | } 1319 | } 1320 | } 1321 | } 1322 | 1323 | module.exports = checkPropTypes; 1324 | 1325 | /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4))) 1326 | 1327 | /***/ }), 1328 | /* 12 */ 1329 | /***/ (function(module, exports, __webpack_require__) { 1330 | 1331 | /** 1332 | * Copyright (c) 2013-present, Facebook, Inc. 1333 | * 1334 | * This source code is licensed under the MIT license found in the 1335 | * LICENSE file in the root directory of this source tree. 1336 | */ 1337 | 1338 | 'use strict'; 1339 | 1340 | var emptyFunction = __webpack_require__(6); 1341 | var invariant = __webpack_require__(7); 1342 | var ReactPropTypesSecret = __webpack_require__(10); 1343 | 1344 | module.exports = function() { 1345 | function shim(props, propName, componentName, location, propFullName, secret) { 1346 | if (secret === ReactPropTypesSecret) { 1347 | // It is still safe when called from React. 1348 | return; 1349 | } 1350 | invariant( 1351 | false, 1352 | 'Calling PropTypes validators directly is not supported by the `prop-types` package. ' + 1353 | 'Use PropTypes.checkPropTypes() to call them. ' + 1354 | 'Read more at http://fb.me/use-check-prop-types' 1355 | ); 1356 | }; 1357 | shim.isRequired = shim; 1358 | function getShim() { 1359 | return shim; 1360 | }; 1361 | // Important! 1362 | // Keep this list in sync with production version in `./factoryWithTypeCheckers.js`. 1363 | var ReactPropTypes = { 1364 | array: shim, 1365 | bool: shim, 1366 | func: shim, 1367 | number: shim, 1368 | object: shim, 1369 | string: shim, 1370 | symbol: shim, 1371 | 1372 | any: shim, 1373 | arrayOf: getShim, 1374 | element: shim, 1375 | instanceOf: getShim, 1376 | node: shim, 1377 | objectOf: getShim, 1378 | oneOf: getShim, 1379 | oneOfType: getShim, 1380 | shape: getShim, 1381 | exact: getShim 1382 | }; 1383 | 1384 | ReactPropTypes.checkPropTypes = emptyFunction; 1385 | ReactPropTypes.PropTypes = ReactPropTypes; 1386 | 1387 | return ReactPropTypes; 1388 | }; 1389 | 1390 | 1391 | /***/ }), 1392 | /* 13 */ 1393 | /***/ (function(module, exports, __webpack_require__) { 1394 | 1395 | /** 1396 | * Copyright (c) 2013-present, Facebook, Inc. 1397 | * 1398 | * This source code is licensed under the MIT license found in the 1399 | * LICENSE file in the root directory of this source tree. 1400 | * 1401 | */ 1402 | 1403 | 'use strict'; 1404 | 1405 | var React = __webpack_require__(2); 1406 | var factory = __webpack_require__(14); 1407 | 1408 | if (typeof React === 'undefined') { 1409 | throw Error( 1410 | 'create-react-class could not find the React object. If you are using script tags, ' + 1411 | 'make sure that React is being loaded before create-react-class.' 1412 | ); 1413 | } 1414 | 1415 | // Hack to grab NoopUpdateQueue from isomorphic React 1416 | var ReactNoopUpdateQueue = new React.Component().updater; 1417 | 1418 | module.exports = factory( 1419 | React.Component, 1420 | React.isValidElement, 1421 | ReactNoopUpdateQueue 1422 | ); 1423 | 1424 | 1425 | /***/ }), 1426 | /* 14 */ 1427 | /***/ (function(module, exports, __webpack_require__) { 1428 | 1429 | /* WEBPACK VAR INJECTION */(function(process) {/** 1430 | * Copyright (c) 2013-present, Facebook, Inc. 1431 | * 1432 | * This source code is licensed under the MIT license found in the 1433 | * LICENSE file in the root directory of this source tree. 1434 | * 1435 | */ 1436 | 1437 | 'use strict'; 1438 | 1439 | var _assign = __webpack_require__(9); 1440 | 1441 | var emptyObject = __webpack_require__(15); 1442 | var _invariant = __webpack_require__(7); 1443 | 1444 | if (process.env.NODE_ENV !== 'production') { 1445 | var warning = __webpack_require__(8); 1446 | } 1447 | 1448 | var MIXINS_KEY = 'mixins'; 1449 | 1450 | // Helper function to allow the creation of anonymous functions which do not 1451 | // have .name set to the name of the variable being assigned to. 1452 | function identity(fn) { 1453 | return fn; 1454 | } 1455 | 1456 | var ReactPropTypeLocationNames; 1457 | if (process.env.NODE_ENV !== 'production') { 1458 | ReactPropTypeLocationNames = { 1459 | prop: 'prop', 1460 | context: 'context', 1461 | childContext: 'child context' 1462 | }; 1463 | } else { 1464 | ReactPropTypeLocationNames = {}; 1465 | } 1466 | 1467 | function factory(ReactComponent, isValidElement, ReactNoopUpdateQueue) { 1468 | /** 1469 | * Policies that describe methods in `ReactClassInterface`. 1470 | */ 1471 | 1472 | var injectedMixins = []; 1473 | 1474 | /** 1475 | * Composite components are higher-level components that compose other composite 1476 | * or host components. 1477 | * 1478 | * To create a new type of `ReactClass`, pass a specification of 1479 | * your new class to `React.createClass`. The only requirement of your class 1480 | * specification is that you implement a `render` method. 1481 | * 1482 | * var MyComponent = React.createClass({ 1483 | * render: function() { 1484 | * return
Hello World
; 1485 | * } 1486 | * }); 1487 | * 1488 | * The class specification supports a specific protocol of methods that have 1489 | * special meaning (e.g. `render`). See `ReactClassInterface` for 1490 | * more the comprehensive protocol. Any other properties and methods in the 1491 | * class specification will be available on the prototype. 1492 | * 1493 | * @interface ReactClassInterface 1494 | * @internal 1495 | */ 1496 | var ReactClassInterface = { 1497 | /** 1498 | * An array of Mixin objects to include when defining your component. 1499 | * 1500 | * @type {array} 1501 | * @optional 1502 | */ 1503 | mixins: 'DEFINE_MANY', 1504 | 1505 | /** 1506 | * An object containing properties and methods that should be defined on 1507 | * the component's constructor instead of its prototype (static methods). 1508 | * 1509 | * @type {object} 1510 | * @optional 1511 | */ 1512 | statics: 'DEFINE_MANY', 1513 | 1514 | /** 1515 | * Definition of prop types for this component. 1516 | * 1517 | * @type {object} 1518 | * @optional 1519 | */ 1520 | propTypes: 'DEFINE_MANY', 1521 | 1522 | /** 1523 | * Definition of context types for this component. 1524 | * 1525 | * @type {object} 1526 | * @optional 1527 | */ 1528 | contextTypes: 'DEFINE_MANY', 1529 | 1530 | /** 1531 | * Definition of context types this component sets for its children. 1532 | * 1533 | * @type {object} 1534 | * @optional 1535 | */ 1536 | childContextTypes: 'DEFINE_MANY', 1537 | 1538 | // ==== Definition methods ==== 1539 | 1540 | /** 1541 | * Invoked when the component is mounted. Values in the mapping will be set on 1542 | * `this.props` if that prop is not specified (i.e. using an `in` check). 1543 | * 1544 | * This method is invoked before `getInitialState` and therefore cannot rely 1545 | * on `this.state` or use `this.setState`. 1546 | * 1547 | * @return {object} 1548 | * @optional 1549 | */ 1550 | getDefaultProps: 'DEFINE_MANY_MERGED', 1551 | 1552 | /** 1553 | * Invoked once before the component is mounted. The return value will be used 1554 | * as the initial value of `this.state`. 1555 | * 1556 | * getInitialState: function() { 1557 | * return { 1558 | * isOn: false, 1559 | * fooBaz: new BazFoo() 1560 | * } 1561 | * } 1562 | * 1563 | * @return {object} 1564 | * @optional 1565 | */ 1566 | getInitialState: 'DEFINE_MANY_MERGED', 1567 | 1568 | /** 1569 | * @return {object} 1570 | * @optional 1571 | */ 1572 | getChildContext: 'DEFINE_MANY_MERGED', 1573 | 1574 | /** 1575 | * Uses props from `this.props` and state from `this.state` to render the 1576 | * structure of the component. 1577 | * 1578 | * No guarantees are made about when or how often this method is invoked, so 1579 | * it must not have side effects. 1580 | * 1581 | * render: function() { 1582 | * var name = this.props.name; 1583 | * return
Hello, {name}!
; 1584 | * } 1585 | * 1586 | * @return {ReactComponent} 1587 | * @required 1588 | */ 1589 | render: 'DEFINE_ONCE', 1590 | 1591 | // ==== Delegate methods ==== 1592 | 1593 | /** 1594 | * Invoked when the component is initially created and about to be mounted. 1595 | * This may have side effects, but any external subscriptions or data created 1596 | * by this method must be cleaned up in `componentWillUnmount`. 1597 | * 1598 | * @optional 1599 | */ 1600 | componentWillMount: 'DEFINE_MANY', 1601 | 1602 | /** 1603 | * Invoked when the component has been mounted and has a DOM representation. 1604 | * However, there is no guarantee that the DOM node is in the document. 1605 | * 1606 | * Use this as an opportunity to operate on the DOM when the component has 1607 | * been mounted (initialized and rendered) for the first time. 1608 | * 1609 | * @param {DOMElement} rootNode DOM element representing the component. 1610 | * @optional 1611 | */ 1612 | componentDidMount: 'DEFINE_MANY', 1613 | 1614 | /** 1615 | * Invoked before the component receives new props. 1616 | * 1617 | * Use this as an opportunity to react to a prop transition by updating the 1618 | * state using `this.setState`. Current props are accessed via `this.props`. 1619 | * 1620 | * componentWillReceiveProps: function(nextProps, nextContext) { 1621 | * this.setState({ 1622 | * likesIncreasing: nextProps.likeCount > this.props.likeCount 1623 | * }); 1624 | * } 1625 | * 1626 | * NOTE: There is no equivalent `componentWillReceiveState`. An incoming prop 1627 | * transition may cause a state change, but the opposite is not true. If you 1628 | * need it, you are probably looking for `componentWillUpdate`. 1629 | * 1630 | * @param {object} nextProps 1631 | * @optional 1632 | */ 1633 | componentWillReceiveProps: 'DEFINE_MANY', 1634 | 1635 | /** 1636 | * Invoked while deciding if the component should be updated as a result of 1637 | * receiving new props, state and/or context. 1638 | * 1639 | * Use this as an opportunity to `return false` when you're certain that the 1640 | * transition to the new props/state/context will not require a component 1641 | * update. 1642 | * 1643 | * shouldComponentUpdate: function(nextProps, nextState, nextContext) { 1644 | * return !equal(nextProps, this.props) || 1645 | * !equal(nextState, this.state) || 1646 | * !equal(nextContext, this.context); 1647 | * } 1648 | * 1649 | * @param {object} nextProps 1650 | * @param {?object} nextState 1651 | * @param {?object} nextContext 1652 | * @return {boolean} True if the component should update. 1653 | * @optional 1654 | */ 1655 | shouldComponentUpdate: 'DEFINE_ONCE', 1656 | 1657 | /** 1658 | * Invoked when the component is about to update due to a transition from 1659 | * `this.props`, `this.state` and `this.context` to `nextProps`, `nextState` 1660 | * and `nextContext`. 1661 | * 1662 | * Use this as an opportunity to perform preparation before an update occurs. 1663 | * 1664 | * NOTE: You **cannot** use `this.setState()` in this method. 1665 | * 1666 | * @param {object} nextProps 1667 | * @param {?object} nextState 1668 | * @param {?object} nextContext 1669 | * @param {ReactReconcileTransaction} transaction 1670 | * @optional 1671 | */ 1672 | componentWillUpdate: 'DEFINE_MANY', 1673 | 1674 | /** 1675 | * Invoked when the component's DOM representation has been updated. 1676 | * 1677 | * Use this as an opportunity to operate on the DOM when the component has 1678 | * been updated. 1679 | * 1680 | * @param {object} prevProps 1681 | * @param {?object} prevState 1682 | * @param {?object} prevContext 1683 | * @param {DOMElement} rootNode DOM element representing the component. 1684 | * @optional 1685 | */ 1686 | componentDidUpdate: 'DEFINE_MANY', 1687 | 1688 | /** 1689 | * Invoked when the component is about to be removed from its parent and have 1690 | * its DOM representation destroyed. 1691 | * 1692 | * Use this as an opportunity to deallocate any external resources. 1693 | * 1694 | * NOTE: There is no `componentDidUnmount` since your component will have been 1695 | * destroyed by that point. 1696 | * 1697 | * @optional 1698 | */ 1699 | componentWillUnmount: 'DEFINE_MANY', 1700 | 1701 | /** 1702 | * Replacement for (deprecated) `componentWillMount`. 1703 | * 1704 | * @optional 1705 | */ 1706 | UNSAFE_componentWillMount: 'DEFINE_MANY', 1707 | 1708 | /** 1709 | * Replacement for (deprecated) `componentWillReceiveProps`. 1710 | * 1711 | * @optional 1712 | */ 1713 | UNSAFE_componentWillReceiveProps: 'DEFINE_MANY', 1714 | 1715 | /** 1716 | * Replacement for (deprecated) `componentWillUpdate`. 1717 | * 1718 | * @optional 1719 | */ 1720 | UNSAFE_componentWillUpdate: 'DEFINE_MANY', 1721 | 1722 | // ==== Advanced methods ==== 1723 | 1724 | /** 1725 | * Updates the component's currently mounted DOM representation. 1726 | * 1727 | * By default, this implements React's rendering and reconciliation algorithm. 1728 | * Sophisticated clients may wish to override this. 1729 | * 1730 | * @param {ReactReconcileTransaction} transaction 1731 | * @internal 1732 | * @overridable 1733 | */ 1734 | updateComponent: 'OVERRIDE_BASE' 1735 | }; 1736 | 1737 | /** 1738 | * Similar to ReactClassInterface but for static methods. 1739 | */ 1740 | var ReactClassStaticInterface = { 1741 | /** 1742 | * This method is invoked after a component is instantiated and when it 1743 | * receives new props. Return an object to update state in response to 1744 | * prop changes. Return null to indicate no change to state. 1745 | * 1746 | * If an object is returned, its keys will be merged into the existing state. 1747 | * 1748 | * @return {object || null} 1749 | * @optional 1750 | */ 1751 | getDerivedStateFromProps: 'DEFINE_MANY_MERGED' 1752 | }; 1753 | 1754 | /** 1755 | * Mapping from class specification keys to special processing functions. 1756 | * 1757 | * Although these are declared like instance properties in the specification 1758 | * when defining classes using `React.createClass`, they are actually static 1759 | * and are accessible on the constructor instead of the prototype. Despite 1760 | * being static, they must be defined outside of the "statics" key under 1761 | * which all other static methods are defined. 1762 | */ 1763 | var RESERVED_SPEC_KEYS = { 1764 | displayName: function(Constructor, displayName) { 1765 | Constructor.displayName = displayName; 1766 | }, 1767 | mixins: function(Constructor, mixins) { 1768 | if (mixins) { 1769 | for (var i = 0; i < mixins.length; i++) { 1770 | mixSpecIntoComponent(Constructor, mixins[i]); 1771 | } 1772 | } 1773 | }, 1774 | childContextTypes: function(Constructor, childContextTypes) { 1775 | if (process.env.NODE_ENV !== 'production') { 1776 | validateTypeDef(Constructor, childContextTypes, 'childContext'); 1777 | } 1778 | Constructor.childContextTypes = _assign( 1779 | {}, 1780 | Constructor.childContextTypes, 1781 | childContextTypes 1782 | ); 1783 | }, 1784 | contextTypes: function(Constructor, contextTypes) { 1785 | if (process.env.NODE_ENV !== 'production') { 1786 | validateTypeDef(Constructor, contextTypes, 'context'); 1787 | } 1788 | Constructor.contextTypes = _assign( 1789 | {}, 1790 | Constructor.contextTypes, 1791 | contextTypes 1792 | ); 1793 | }, 1794 | /** 1795 | * Special case getDefaultProps which should move into statics but requires 1796 | * automatic merging. 1797 | */ 1798 | getDefaultProps: function(Constructor, getDefaultProps) { 1799 | if (Constructor.getDefaultProps) { 1800 | Constructor.getDefaultProps = createMergedResultFunction( 1801 | Constructor.getDefaultProps, 1802 | getDefaultProps 1803 | ); 1804 | } else { 1805 | Constructor.getDefaultProps = getDefaultProps; 1806 | } 1807 | }, 1808 | propTypes: function(Constructor, propTypes) { 1809 | if (process.env.NODE_ENV !== 'production') { 1810 | validateTypeDef(Constructor, propTypes, 'prop'); 1811 | } 1812 | Constructor.propTypes = _assign({}, Constructor.propTypes, propTypes); 1813 | }, 1814 | statics: function(Constructor, statics) { 1815 | mixStaticSpecIntoComponent(Constructor, statics); 1816 | }, 1817 | autobind: function() {} 1818 | }; 1819 | 1820 | function validateTypeDef(Constructor, typeDef, location) { 1821 | for (var propName in typeDef) { 1822 | if (typeDef.hasOwnProperty(propName)) { 1823 | // use a warning instead of an _invariant so components 1824 | // don't show up in prod but only in __DEV__ 1825 | if (process.env.NODE_ENV !== 'production') { 1826 | warning( 1827 | typeof typeDef[propName] === 'function', 1828 | '%s: %s type `%s` is invalid; it must be a function, usually from ' + 1829 | 'React.PropTypes.', 1830 | Constructor.displayName || 'ReactClass', 1831 | ReactPropTypeLocationNames[location], 1832 | propName 1833 | ); 1834 | } 1835 | } 1836 | } 1837 | } 1838 | 1839 | function validateMethodOverride(isAlreadyDefined, name) { 1840 | var specPolicy = ReactClassInterface.hasOwnProperty(name) 1841 | ? ReactClassInterface[name] 1842 | : null; 1843 | 1844 | // Disallow overriding of base class methods unless explicitly allowed. 1845 | if (ReactClassMixin.hasOwnProperty(name)) { 1846 | _invariant( 1847 | specPolicy === 'OVERRIDE_BASE', 1848 | 'ReactClassInterface: You are attempting to override ' + 1849 | '`%s` from your class specification. Ensure that your method names ' + 1850 | 'do not overlap with React methods.', 1851 | name 1852 | ); 1853 | } 1854 | 1855 | // Disallow defining methods more than once unless explicitly allowed. 1856 | if (isAlreadyDefined) { 1857 | _invariant( 1858 | specPolicy === 'DEFINE_MANY' || specPolicy === 'DEFINE_MANY_MERGED', 1859 | 'ReactClassInterface: You are attempting to define ' + 1860 | '`%s` on your component more than once. This conflict may be due ' + 1861 | 'to a mixin.', 1862 | name 1863 | ); 1864 | } 1865 | } 1866 | 1867 | /** 1868 | * Mixin helper which handles policy validation and reserved 1869 | * specification keys when building React classes. 1870 | */ 1871 | function mixSpecIntoComponent(Constructor, spec) { 1872 | if (!spec) { 1873 | if (process.env.NODE_ENV !== 'production') { 1874 | var typeofSpec = typeof spec; 1875 | var isMixinValid = typeofSpec === 'object' && spec !== null; 1876 | 1877 | if (process.env.NODE_ENV !== 'production') { 1878 | warning( 1879 | isMixinValid, 1880 | "%s: You're attempting to include a mixin that is either null " + 1881 | 'or not an object. Check the mixins included by the component, ' + 1882 | 'as well as any mixins they include themselves. ' + 1883 | 'Expected object but got %s.', 1884 | Constructor.displayName || 'ReactClass', 1885 | spec === null ? null : typeofSpec 1886 | ); 1887 | } 1888 | } 1889 | 1890 | return; 1891 | } 1892 | 1893 | _invariant( 1894 | typeof spec !== 'function', 1895 | "ReactClass: You're attempting to " + 1896 | 'use a component class or function as a mixin. Instead, just use a ' + 1897 | 'regular object.' 1898 | ); 1899 | _invariant( 1900 | !isValidElement(spec), 1901 | "ReactClass: You're attempting to " + 1902 | 'use a component as a mixin. Instead, just use a regular object.' 1903 | ); 1904 | 1905 | var proto = Constructor.prototype; 1906 | var autoBindPairs = proto.__reactAutoBindPairs; 1907 | 1908 | // By handling mixins before any other properties, we ensure the same 1909 | // chaining order is applied to methods with DEFINE_MANY policy, whether 1910 | // mixins are listed before or after these methods in the spec. 1911 | if (spec.hasOwnProperty(MIXINS_KEY)) { 1912 | RESERVED_SPEC_KEYS.mixins(Constructor, spec.mixins); 1913 | } 1914 | 1915 | for (var name in spec) { 1916 | if (!spec.hasOwnProperty(name)) { 1917 | continue; 1918 | } 1919 | 1920 | if (name === MIXINS_KEY) { 1921 | // We have already handled mixins in a special case above. 1922 | continue; 1923 | } 1924 | 1925 | var property = spec[name]; 1926 | var isAlreadyDefined = proto.hasOwnProperty(name); 1927 | validateMethodOverride(isAlreadyDefined, name); 1928 | 1929 | if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) { 1930 | RESERVED_SPEC_KEYS[name](Constructor, property); 1931 | } else { 1932 | // Setup methods on prototype: 1933 | // The following member methods should not be automatically bound: 1934 | // 1. Expected ReactClass methods (in the "interface"). 1935 | // 2. Overridden methods (that were mixed in). 1936 | var isReactClassMethod = ReactClassInterface.hasOwnProperty(name); 1937 | var isFunction = typeof property === 'function'; 1938 | var shouldAutoBind = 1939 | isFunction && 1940 | !isReactClassMethod && 1941 | !isAlreadyDefined && 1942 | spec.autobind !== false; 1943 | 1944 | if (shouldAutoBind) { 1945 | autoBindPairs.push(name, property); 1946 | proto[name] = property; 1947 | } else { 1948 | if (isAlreadyDefined) { 1949 | var specPolicy = ReactClassInterface[name]; 1950 | 1951 | // These cases should already be caught by validateMethodOverride. 1952 | _invariant( 1953 | isReactClassMethod && 1954 | (specPolicy === 'DEFINE_MANY_MERGED' || 1955 | specPolicy === 'DEFINE_MANY'), 1956 | 'ReactClass: Unexpected spec policy %s for key %s ' + 1957 | 'when mixing in component specs.', 1958 | specPolicy, 1959 | name 1960 | ); 1961 | 1962 | // For methods which are defined more than once, call the existing 1963 | // methods before calling the new property, merging if appropriate. 1964 | if (specPolicy === 'DEFINE_MANY_MERGED') { 1965 | proto[name] = createMergedResultFunction(proto[name], property); 1966 | } else if (specPolicy === 'DEFINE_MANY') { 1967 | proto[name] = createChainedFunction(proto[name], property); 1968 | } 1969 | } else { 1970 | proto[name] = property; 1971 | if (process.env.NODE_ENV !== 'production') { 1972 | // Add verbose displayName to the function, which helps when looking 1973 | // at profiling tools. 1974 | if (typeof property === 'function' && spec.displayName) { 1975 | proto[name].displayName = spec.displayName + '_' + name; 1976 | } 1977 | } 1978 | } 1979 | } 1980 | } 1981 | } 1982 | } 1983 | 1984 | function mixStaticSpecIntoComponent(Constructor, statics) { 1985 | if (!statics) { 1986 | return; 1987 | } 1988 | 1989 | for (var name in statics) { 1990 | var property = statics[name]; 1991 | if (!statics.hasOwnProperty(name)) { 1992 | continue; 1993 | } 1994 | 1995 | var isReserved = name in RESERVED_SPEC_KEYS; 1996 | _invariant( 1997 | !isReserved, 1998 | 'ReactClass: You are attempting to define a reserved ' + 1999 | 'property, `%s`, that shouldn\'t be on the "statics" key. Define it ' + 2000 | 'as an instance property instead; it will still be accessible on the ' + 2001 | 'constructor.', 2002 | name 2003 | ); 2004 | 2005 | var isAlreadyDefined = name in Constructor; 2006 | if (isAlreadyDefined) { 2007 | var specPolicy = ReactClassStaticInterface.hasOwnProperty(name) 2008 | ? ReactClassStaticInterface[name] 2009 | : null; 2010 | 2011 | _invariant( 2012 | specPolicy === 'DEFINE_MANY_MERGED', 2013 | 'ReactClass: You are attempting to define ' + 2014 | '`%s` on your component more than once. This conflict may be ' + 2015 | 'due to a mixin.', 2016 | name 2017 | ); 2018 | 2019 | Constructor[name] = createMergedResultFunction(Constructor[name], property); 2020 | 2021 | return; 2022 | } 2023 | 2024 | Constructor[name] = property; 2025 | } 2026 | } 2027 | 2028 | /** 2029 | * Merge two objects, but throw if both contain the same key. 2030 | * 2031 | * @param {object} one The first object, which is mutated. 2032 | * @param {object} two The second object 2033 | * @return {object} one after it has been mutated to contain everything in two. 2034 | */ 2035 | function mergeIntoWithNoDuplicateKeys(one, two) { 2036 | _invariant( 2037 | one && two && typeof one === 'object' && typeof two === 'object', 2038 | 'mergeIntoWithNoDuplicateKeys(): Cannot merge non-objects.' 2039 | ); 2040 | 2041 | for (var key in two) { 2042 | if (two.hasOwnProperty(key)) { 2043 | _invariant( 2044 | one[key] === undefined, 2045 | 'mergeIntoWithNoDuplicateKeys(): ' + 2046 | 'Tried to merge two objects with the same key: `%s`. This conflict ' + 2047 | 'may be due to a mixin; in particular, this may be caused by two ' + 2048 | 'getInitialState() or getDefaultProps() methods returning objects ' + 2049 | 'with clashing keys.', 2050 | key 2051 | ); 2052 | one[key] = two[key]; 2053 | } 2054 | } 2055 | return one; 2056 | } 2057 | 2058 | /** 2059 | * Creates a function that invokes two functions and merges their return values. 2060 | * 2061 | * @param {function} one Function to invoke first. 2062 | * @param {function} two Function to invoke second. 2063 | * @return {function} Function that invokes the two argument functions. 2064 | * @private 2065 | */ 2066 | function createMergedResultFunction(one, two) { 2067 | return function mergedResult() { 2068 | var a = one.apply(this, arguments); 2069 | var b = two.apply(this, arguments); 2070 | if (a == null) { 2071 | return b; 2072 | } else if (b == null) { 2073 | return a; 2074 | } 2075 | var c = {}; 2076 | mergeIntoWithNoDuplicateKeys(c, a); 2077 | mergeIntoWithNoDuplicateKeys(c, b); 2078 | return c; 2079 | }; 2080 | } 2081 | 2082 | /** 2083 | * Creates a function that invokes two functions and ignores their return vales. 2084 | * 2085 | * @param {function} one Function to invoke first. 2086 | * @param {function} two Function to invoke second. 2087 | * @return {function} Function that invokes the two argument functions. 2088 | * @private 2089 | */ 2090 | function createChainedFunction(one, two) { 2091 | return function chainedFunction() { 2092 | one.apply(this, arguments); 2093 | two.apply(this, arguments); 2094 | }; 2095 | } 2096 | 2097 | /** 2098 | * Binds a method to the component. 2099 | * 2100 | * @param {object} component Component whose method is going to be bound. 2101 | * @param {function} method Method to be bound. 2102 | * @return {function} The bound method. 2103 | */ 2104 | function bindAutoBindMethod(component, method) { 2105 | var boundMethod = method.bind(component); 2106 | if (process.env.NODE_ENV !== 'production') { 2107 | boundMethod.__reactBoundContext = component; 2108 | boundMethod.__reactBoundMethod = method; 2109 | boundMethod.__reactBoundArguments = null; 2110 | var componentName = component.constructor.displayName; 2111 | var _bind = boundMethod.bind; 2112 | boundMethod.bind = function(newThis) { 2113 | for ( 2114 | var _len = arguments.length, 2115 | args = Array(_len > 1 ? _len - 1 : 0), 2116 | _key = 1; 2117 | _key < _len; 2118 | _key++ 2119 | ) { 2120 | args[_key - 1] = arguments[_key]; 2121 | } 2122 | 2123 | // User is trying to bind() an autobound method; we effectively will 2124 | // ignore the value of "this" that the user is trying to use, so 2125 | // let's warn. 2126 | if (newThis !== component && newThis !== null) { 2127 | if (process.env.NODE_ENV !== 'production') { 2128 | warning( 2129 | false, 2130 | 'bind(): React component methods may only be bound to the ' + 2131 | 'component instance. See %s', 2132 | componentName 2133 | ); 2134 | } 2135 | } else if (!args.length) { 2136 | if (process.env.NODE_ENV !== 'production') { 2137 | warning( 2138 | false, 2139 | 'bind(): You are binding a component method to the component. ' + 2140 | 'React does this for you automatically in a high-performance ' + 2141 | 'way, so you can safely remove this call. See %s', 2142 | componentName 2143 | ); 2144 | } 2145 | return boundMethod; 2146 | } 2147 | var reboundMethod = _bind.apply(boundMethod, arguments); 2148 | reboundMethod.__reactBoundContext = component; 2149 | reboundMethod.__reactBoundMethod = method; 2150 | reboundMethod.__reactBoundArguments = args; 2151 | return reboundMethod; 2152 | }; 2153 | } 2154 | return boundMethod; 2155 | } 2156 | 2157 | /** 2158 | * Binds all auto-bound methods in a component. 2159 | * 2160 | * @param {object} component Component whose method is going to be bound. 2161 | */ 2162 | function bindAutoBindMethods(component) { 2163 | var pairs = component.__reactAutoBindPairs; 2164 | for (var i = 0; i < pairs.length; i += 2) { 2165 | var autoBindKey = pairs[i]; 2166 | var method = pairs[i + 1]; 2167 | component[autoBindKey] = bindAutoBindMethod(component, method); 2168 | } 2169 | } 2170 | 2171 | var IsMountedPreMixin = { 2172 | componentDidMount: function() { 2173 | this.__isMounted = true; 2174 | } 2175 | }; 2176 | 2177 | var IsMountedPostMixin = { 2178 | componentWillUnmount: function() { 2179 | this.__isMounted = false; 2180 | } 2181 | }; 2182 | 2183 | /** 2184 | * Add more to the ReactClass base class. These are all legacy features and 2185 | * therefore not already part of the modern ReactComponent. 2186 | */ 2187 | var ReactClassMixin = { 2188 | /** 2189 | * TODO: This will be deprecated because state should always keep a consistent 2190 | * type signature and the only use case for this, is to avoid that. 2191 | */ 2192 | replaceState: function(newState, callback) { 2193 | this.updater.enqueueReplaceState(this, newState, callback); 2194 | }, 2195 | 2196 | /** 2197 | * Checks whether or not this composite component is mounted. 2198 | * @return {boolean} True if mounted, false otherwise. 2199 | * @protected 2200 | * @final 2201 | */ 2202 | isMounted: function() { 2203 | if (process.env.NODE_ENV !== 'production') { 2204 | warning( 2205 | this.__didWarnIsMounted, 2206 | '%s: isMounted is deprecated. Instead, make sure to clean up ' + 2207 | 'subscriptions and pending requests in componentWillUnmount to ' + 2208 | 'prevent memory leaks.', 2209 | (this.constructor && this.constructor.displayName) || 2210 | this.name || 2211 | 'Component' 2212 | ); 2213 | this.__didWarnIsMounted = true; 2214 | } 2215 | return !!this.__isMounted; 2216 | } 2217 | }; 2218 | 2219 | var ReactClassComponent = function() {}; 2220 | _assign( 2221 | ReactClassComponent.prototype, 2222 | ReactComponent.prototype, 2223 | ReactClassMixin 2224 | ); 2225 | 2226 | /** 2227 | * Creates a composite component class given a class specification. 2228 | * See https://facebook.github.io/react/docs/top-level-api.html#react.createclass 2229 | * 2230 | * @param {object} spec Class specification (which must define `render`). 2231 | * @return {function} Component constructor function. 2232 | * @public 2233 | */ 2234 | function createClass(spec) { 2235 | // To keep our warnings more understandable, we'll use a little hack here to 2236 | // ensure that Constructor.name !== 'Constructor'. This makes sure we don't 2237 | // unnecessarily identify a class without displayName as 'Constructor'. 2238 | var Constructor = identity(function(props, context, updater) { 2239 | // This constructor gets overridden by mocks. The argument is used 2240 | // by mocks to assert on what gets mounted. 2241 | 2242 | if (process.env.NODE_ENV !== 'production') { 2243 | warning( 2244 | this instanceof Constructor, 2245 | 'Something is calling a React component directly. Use a factory or ' + 2246 | 'JSX instead. See: https://fb.me/react-legacyfactory' 2247 | ); 2248 | } 2249 | 2250 | // Wire up auto-binding 2251 | if (this.__reactAutoBindPairs.length) { 2252 | bindAutoBindMethods(this); 2253 | } 2254 | 2255 | this.props = props; 2256 | this.context = context; 2257 | this.refs = emptyObject; 2258 | this.updater = updater || ReactNoopUpdateQueue; 2259 | 2260 | this.state = null; 2261 | 2262 | // ReactClasses doesn't have constructors. Instead, they use the 2263 | // getInitialState and componentWillMount methods for initialization. 2264 | 2265 | var initialState = this.getInitialState ? this.getInitialState() : null; 2266 | if (process.env.NODE_ENV !== 'production') { 2267 | // We allow auto-mocks to proceed as if they're returning null. 2268 | if ( 2269 | initialState === undefined && 2270 | this.getInitialState._isMockFunction 2271 | ) { 2272 | // This is probably bad practice. Consider warning here and 2273 | // deprecating this convenience. 2274 | initialState = null; 2275 | } 2276 | } 2277 | _invariant( 2278 | typeof initialState === 'object' && !Array.isArray(initialState), 2279 | '%s.getInitialState(): must return an object or null', 2280 | Constructor.displayName || 'ReactCompositeComponent' 2281 | ); 2282 | 2283 | this.state = initialState; 2284 | }); 2285 | Constructor.prototype = new ReactClassComponent(); 2286 | Constructor.prototype.constructor = Constructor; 2287 | Constructor.prototype.__reactAutoBindPairs = []; 2288 | 2289 | injectedMixins.forEach(mixSpecIntoComponent.bind(null, Constructor)); 2290 | 2291 | mixSpecIntoComponent(Constructor, IsMountedPreMixin); 2292 | mixSpecIntoComponent(Constructor, spec); 2293 | mixSpecIntoComponent(Constructor, IsMountedPostMixin); 2294 | 2295 | // Initialize the defaultProps property after all mixins have been merged. 2296 | if (Constructor.getDefaultProps) { 2297 | Constructor.defaultProps = Constructor.getDefaultProps(); 2298 | } 2299 | 2300 | if (process.env.NODE_ENV !== 'production') { 2301 | // This is a tag to indicate that the use of these method names is ok, 2302 | // since it's used with createClass. If it's not, then it's likely a 2303 | // mistake so we'll warn you to use the static property, property 2304 | // initializer or constructor respectively. 2305 | if (Constructor.getDefaultProps) { 2306 | Constructor.getDefaultProps.isReactClassApproved = {}; 2307 | } 2308 | if (Constructor.prototype.getInitialState) { 2309 | Constructor.prototype.getInitialState.isReactClassApproved = {}; 2310 | } 2311 | } 2312 | 2313 | _invariant( 2314 | Constructor.prototype.render, 2315 | 'createClass(...): Class specification must implement a `render` method.' 2316 | ); 2317 | 2318 | if (process.env.NODE_ENV !== 'production') { 2319 | warning( 2320 | !Constructor.prototype.componentShouldUpdate, 2321 | '%s has a method called ' + 2322 | 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + 2323 | 'The name is phrased as a question because the function is ' + 2324 | 'expected to return a value.', 2325 | spec.displayName || 'A component' 2326 | ); 2327 | warning( 2328 | !Constructor.prototype.componentWillRecieveProps, 2329 | '%s has a method called ' + 2330 | 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', 2331 | spec.displayName || 'A component' 2332 | ); 2333 | warning( 2334 | !Constructor.prototype.UNSAFE_componentWillRecieveProps, 2335 | '%s has a method called UNSAFE_componentWillRecieveProps(). ' + 2336 | 'Did you mean UNSAFE_componentWillReceiveProps()?', 2337 | spec.displayName || 'A component' 2338 | ); 2339 | } 2340 | 2341 | // Reduce time spent doing lookups by setting these on the prototype. 2342 | for (var methodName in ReactClassInterface) { 2343 | if (!Constructor.prototype[methodName]) { 2344 | Constructor.prototype[methodName] = null; 2345 | } 2346 | } 2347 | 2348 | return Constructor; 2349 | } 2350 | 2351 | return createClass; 2352 | } 2353 | 2354 | module.exports = factory; 2355 | 2356 | /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4))) 2357 | 2358 | /***/ }), 2359 | /* 15 */ 2360 | /***/ (function(module, exports, __webpack_require__) { 2361 | 2362 | /* WEBPACK VAR INJECTION */(function(process) {/** 2363 | * Copyright (c) 2013-present, Facebook, Inc. 2364 | * 2365 | * This source code is licensed under the MIT license found in the 2366 | * LICENSE file in the root directory of this source tree. 2367 | * 2368 | */ 2369 | 2370 | 'use strict'; 2371 | 2372 | var emptyObject = {}; 2373 | 2374 | if (process.env.NODE_ENV !== 'production') { 2375 | Object.freeze(emptyObject); 2376 | } 2377 | 2378 | module.exports = emptyObject; 2379 | /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4))) 2380 | 2381 | /***/ }), 2382 | /* 16 */ 2383 | /***/ (function(module, exports) { 2384 | 2385 | "use strict"; 2386 | 2387 | Object.defineProperty(exports, "__esModule", { 2388 | value: true 2389 | }); 2390 | 2391 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 2392 | 2393 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 2394 | 2395 | var Experiment = function () { 2396 | function Experiment() { 2397 | _classCallCheck(this, Experiment); 2398 | } 2399 | 2400 | _createClass(Experiment, [{ 2401 | key: "get", 2402 | value: function get(parameter) { 2403 | throw "IMPLEMENT get"; 2404 | } 2405 | }, { 2406 | key: "logExposure", 2407 | value: function logExposure(opts) { 2408 | throw "IMPLEMENT logExposure"; 2409 | } 2410 | }, { 2411 | key: "getName", 2412 | value: function getName() { 2413 | throw "IMPLEMENT getName"; 2414 | } 2415 | }, { 2416 | key: "previouslyLogged", 2417 | value: function previouslyLogged() { 2418 | throw "IMPLEMENT previouslyLogged"; 2419 | } 2420 | }, { 2421 | key: "shouldFetchExperimentParameter", 2422 | value: function shouldFetchExperimentParameter(name) { 2423 | throw "IMPLEMENT shouldFetchExperimentParameter"; 2424 | } 2425 | }]); 2426 | 2427 | return Experiment; 2428 | }(); 2429 | 2430 | exports.default = Experiment; 2431 | ; 2432 | module.exports = exports["default"]; 2433 | 2434 | /***/ }), 2435 | /* 17 */ 2436 | /***/ (function(module, exports, __webpack_require__) { 2437 | 2438 | 'use strict'; 2439 | 2440 | Object.defineProperty(exports, "__esModule", { 2441 | value: true 2442 | }); 2443 | 2444 | var _react = __webpack_require__(2); 2445 | 2446 | var _react2 = _interopRequireDefault(_react); 2447 | 2448 | var _createReactClass = __webpack_require__(13); 2449 | 2450 | var _createReactClass2 = _interopRequireDefault(_createReactClass); 2451 | 2452 | var _parametrize = __webpack_require__(18); 2453 | 2454 | var _parametrize2 = _interopRequireDefault(_parametrize); 2455 | 2456 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 2457 | 2458 | var ABTest = (0, _createReactClass2.default)({ 2459 | displayName: 'ABTest', 2460 | getInitialState: function getInitialState() { 2461 | return { 2462 | hasRendered: false 2463 | }; 2464 | }, 2465 | enrolledInVariation: function enrolledInVariation() { 2466 | if (!this.state.hasRendered) { 2467 | this.setState({ 2468 | hasRendered: true 2469 | }); 2470 | } 2471 | }, 2472 | renderExposedVariation: function renderExposedVariation() { 2473 | var _props = this.props, 2474 | on = _props.on, 2475 | experiment = _props.experiment; 2476 | 2477 | 2478 | if (!experiment) { 2479 | console.error("You must pass in an experiment instance as a prop"); 2480 | return null; 2481 | } else if (!on) { 2482 | console.error("You must pass an 'on' prop indicating what parameter you want to branch off"); 2483 | return null; 2484 | } 2485 | 2486 | return _react2.default.createElement( 2487 | _parametrize2.default, 2488 | { 2489 | experiment: experiment, 2490 | params: [on], 2491 | on: on, 2492 | enrolledInVariation: this.enrolledInVariation, 2493 | hasRendered: this.state.hasRendered }, 2494 | this.props.children 2495 | ); 2496 | }, 2497 | render: function render() { 2498 | return this.renderExposedVariation(); 2499 | } 2500 | }); 2501 | 2502 | exports.default = ABTest; 2503 | module.exports = exports['default']; 2504 | 2505 | /***/ }), 2506 | /* 18 */ 2507 | /***/ (function(module, exports, __webpack_require__) { 2508 | 2509 | 'use strict'; 2510 | 2511 | Object.defineProperty(exports, "__esModule", { 2512 | value: true 2513 | }); 2514 | 2515 | var _react = __webpack_require__(2); 2516 | 2517 | var _react2 = _interopRequireDefault(_react); 2518 | 2519 | var _propTypes = __webpack_require__(3); 2520 | 2521 | var _propTypes2 = _interopRequireDefault(_propTypes); 2522 | 2523 | var _createReactClass = __webpack_require__(13); 2524 | 2525 | var _createReactClass2 = _interopRequireDefault(_createReactClass); 2526 | 2527 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 2528 | 2529 | var Parametrize = (0, _createReactClass2.default)({ 2530 | displayName: 'Parametrize', 2531 | getInitialState: function getInitialState() { 2532 | return { 2533 | experimentParameters: null 2534 | }; 2535 | }, 2536 | 2537 | 2538 | childContextTypes: { 2539 | experimentParameters: _propTypes2.default.object, 2540 | experimentProps: _propTypes2.default.object.isRequired 2541 | }, 2542 | 2543 | getChildContext: function getChildContext() { 2544 | return { 2545 | experimentParameters: this.state.experimentParameters, 2546 | experimentProps: this.props 2547 | }; 2548 | }, 2549 | componentWillMount: function componentWillMount() { 2550 | this.fetchParameters(); 2551 | }, 2552 | fetchParameters: function fetchParameters() { 2553 | var _props = this.props, 2554 | experiment = _props.experiment, 2555 | params = _props.params; 2556 | 2557 | 2558 | if (!experiment || !experiment.get) { 2559 | console.error("You must pass in an experiment instance as a prop"); 2560 | return; 2561 | } else if (!params) { 2562 | console.error("You mass pass a list of params in as a prop"); 2563 | return; 2564 | } 2565 | 2566 | var paramsObj = {}; 2567 | for (var i = 0; i < params.length; i++) { 2568 | var param = params[i]; 2569 | var paramVal = experiment.get(param); 2570 | if (paramVal !== null && paramVal !== undefined) { 2571 | paramsObj[param] = paramVal; 2572 | } 2573 | } 2574 | 2575 | if (Object.keys(paramsObj).length !== 0 && experiment.previouslyLogged() === false) { 2576 | experiment.logExposure({ 2577 | params: params, 2578 | name: experiment.getName() 2579 | }); 2580 | } 2581 | 2582 | this.setState({ 2583 | experimentParameters: paramsObj 2584 | }); 2585 | }, 2586 | renderExperiment: function renderExperiment() { 2587 | var _this = this; 2588 | 2589 | if (!this.state.experimentParameters) { 2590 | return null; 2591 | } 2592 | 2593 | var passThrough = this.props._passThrough; 2594 | var renderedChildren = _react2.default.Children.map(this.props.children, function (child) { 2595 | if (passThrough) { 2596 | return _react2.default.cloneElement(child, _this.state.experimentParameters); 2597 | } else { 2598 | return _react2.default.cloneElement(child, {}); 2599 | } 2600 | }); 2601 | 2602 | return _react2.default.createElement( 2603 | 'span', 2604 | null, 2605 | renderedChildren 2606 | ); 2607 | }, 2608 | render: function render() { 2609 | return this.renderExperiment(); 2610 | } 2611 | }); 2612 | 2613 | exports.default = Parametrize; 2614 | module.exports = exports['default']; 2615 | 2616 | /***/ }), 2617 | /* 19 */ 2618 | /***/ (function(module, exports, __webpack_require__) { 2619 | 2620 | 'use strict'; 2621 | 2622 | Object.defineProperty(exports, "__esModule", { 2623 | value: true 2624 | }); 2625 | 2626 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 2627 | 2628 | var _react = __webpack_require__(2); 2629 | 2630 | var _react2 = _interopRequireDefault(_react); 2631 | 2632 | var _propTypes = __webpack_require__(3); 2633 | 2634 | var _propTypes2 = _interopRequireDefault(_propTypes); 2635 | 2636 | var _createReactClass = __webpack_require__(13); 2637 | 2638 | var _createReactClass2 = _interopRequireDefault(_createReactClass); 2639 | 2640 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 2641 | 2642 | exports.default = function (Component) { 2643 | return (0, _createReactClass2.default)({ 2644 | contextTypes: { 2645 | experimentParameters: _propTypes2.default.object.isRequired 2646 | }, 2647 | 2648 | render: function render() { 2649 | return _react2.default.createElement(Component, _extends({}, this.props, this.context.experimentParameters)); 2650 | } 2651 | }); 2652 | }; 2653 | 2654 | module.exports = exports['default']; 2655 | 2656 | /***/ }), 2657 | /* 20 */ 2658 | /***/ (function(module, exports, __webpack_require__) { 2659 | 2660 | 'use strict'; 2661 | 2662 | Object.defineProperty(exports, "__esModule", { 2663 | value: true 2664 | }); 2665 | 2666 | var _react = __webpack_require__(2); 2667 | 2668 | var _react2 = _interopRequireDefault(_react); 2669 | 2670 | var _createReactClass = __webpack_require__(13); 2671 | 2672 | var _createReactClass2 = _interopRequireDefault(_createReactClass); 2673 | 2674 | var _parametrize = __webpack_require__(18); 2675 | 2676 | var _parametrize2 = _interopRequireDefault(_parametrize); 2677 | 2678 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 2679 | 2680 | exports.default = function (experiment, experimentParams, Component) { 2681 | return (0, _createReactClass2.default)({ 2682 | render: function render() { 2683 | return _react2.default.createElement( 2684 | _parametrize2.default, 2685 | { experiment: experiment, params: experimentParams, _passThrough: true }, 2686 | _react2.default.createElement(Component, this.props) 2687 | ); 2688 | } 2689 | }); 2690 | }; 2691 | 2692 | module.exports = exports['default']; 2693 | 2694 | /***/ }) 2695 | /******/ ]) 2696 | }); 2697 | ; -------------------------------------------------------------------------------- /dist/react-experiments.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("react")):"function"==typeof define&&define.amd?define(["react"],t):"object"==typeof exports?exports.ReactExperiments=t(require("react")):e.ReactExperiments=t(e.React)}(this,function(e){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function o(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t.default=e,t}Object.defineProperty(t,"__esModule",{value:!0});var i=n(1),a=o(i),u=n(16),s=r(u),c=n(17),l=r(c),p=n(18),f=r(p),d=n(19),m=r(d),h=n(20),y=r(h);t.default={ABTest:l.default,When:a.When,Default:a.Default,Experiment:s.default,Parametrize:f.default,withExperimentParams:m.default,parametrize:y.default},e.exports=t.default},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0}),t.Default=t.When=void 0;var o=n(2),i=r(o),a=n(3),u=r(a),s=n(13),c=r(s);t.When=(0,c.default)({displayName:"When",getInitialState:function(){return{shouldRender:!1}},contextTypes:{experimentParameters:u.default.object.isRequired,experimentProps:u.default.object.isRequired},componentWillUpdate:function(e,t){t.shouldRender&&this.context.experimentProps.enrolledInVariation()},componentDidMount:function(){this.shouldRenderVariation()},shouldRenderVariation:function(){var e=this.props.value,t=this.context.experimentProps.on;this.context.experimentParameters&&this.context.experimentParameters[t]===e&&this.setState({shouldRender:!0})},renderChildren:function(){return i.default.Children.map(this.props.children,function(e){return i.default.isValidElement(e)?i.default.cloneElement(e,{}):e})},render:function(){return this.state.shouldRender?i.default.createElement("span",{className:"experiment-variation-component"},this.renderChildren()):null}}),t.Default=(0,c.default)({displayName:"Default",contextTypes:{experimentProps:u.default.object.isRequired},render:function(){return this.context.experimentProps.hasRendered?null:i.default.createElement("span",null,this.props.children)}})},function(t,n){t.exports=e},function(e,t,n){(function(t){if("production"!==t.env.NODE_ENV){var r="function"==typeof Symbol&&Symbol.for&&Symbol.for("react.element")||60103,o=function(e){return"object"==typeof e&&null!==e&&e.$$typeof===r},i=!0;e.exports=n(5)(o,i)}else e.exports=n(12)()}).call(t,n(4))},function(e,t){function n(){throw new Error("setTimeout has not been defined")}function r(){throw new Error("clearTimeout has not been defined")}function o(e){if(l===setTimeout)return setTimeout(e,0);if((l===n||!l)&&setTimeout)return l=setTimeout,setTimeout(e,0);try{return l(e,0)}catch(t){try{return l.call(null,e,0)}catch(t){return l.call(this,e,0)}}}function i(e){if(p===clearTimeout)return clearTimeout(e);if((p===r||!p)&&clearTimeout)return p=clearTimeout,clearTimeout(e);try{return p(e)}catch(t){try{return p.call(null,e)}catch(t){return p.call(this,e)}}}function a(){h&&d&&(h=!1,d.length?m=d.concat(m):y=-1,m.length&&u())}function u(){if(!h){var e=o(a);h=!0;for(var t=m.length;t;){for(d=m,m=[];++y1)for(var n=1;n1?t-1:0),r=1;r2?n-2:0),o=2;o1?u-1:0),l=1;l 2 | 3 | 4 | react-experiments example 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 91 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /examples/react-experiments.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rawls238/react-experiments/5aa82b75174057b1052ea1f802db5074519f0e03/examples/react-experiments.jpg -------------------------------------------------------------------------------- /examples/styles.css: -------------------------------------------------------------------------------- 1 | body {background: whitesmoke;color: #282828;font-family: 'Roboto', sans-serif;font-weight: 400;} 2 | img {max-width: 100%;} 3 | .wrap {width: 100%;max-width: 600px;margin: 0 auto;} 4 | .clearfix {clear: both;} 5 | .text-center {text-align: center;} 6 | .text-left {text-align: left;} 7 | .text-right {text-align: right;} 8 | .margin-top {margin-top: 20px;} 9 | .margin-bottom {margin-bottom: 20px;} 10 | .card {margin: 0 0 10px 0;padding: 20px;background: white;border: 1px solid gainsboro;border-radius: 4px;} 11 | .example {margin: 20px 0;} 12 | .logo {margin: 20px 0;padding-bottom: 40px;border-bottom: 1px solid gainsboro;} 13 | .user-id {color: #999;font-size: 13px;} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-experiments", 3 | "version": "5.1.0", 4 | "dependencies": { 5 | "babel-preset-es2015": "^6.9.0", 6 | "babel-preset-react": "^6.11.1", 7 | "babel-preset-stage-0": "^6.5.0", 8 | "create-react-class": "^15.6.3", 9 | "prop-types": "^15.6.1", 10 | "react": "^16.2.0" 11 | }, 12 | "devDependencies": { 13 | "babel-core": "^6.0.1", 14 | "babel-loader": "^6.0.0", 15 | "babel-plugin-add-module-exports": "0.2.1", 16 | "babel-preset-jest": "^13.0.0", 17 | "jest": "^22.4.2", 18 | "planout": "^5.0.0", 19 | "react-dom": "^16.2.0", 20 | "webpack": "^1.5.3" 21 | }, 22 | "description": "react-experiments is a Javascript library that assists in defining and managing UI experiments in React.", 23 | "main": "dist/react-experiments.js", 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/HubSpot/react-experiments" 27 | }, 28 | "keywords": [ 29 | "ab", 30 | "a/b", 31 | "split testing", 32 | "ui experiments", 33 | "experiments react", 34 | "react-component", 35 | "component" 36 | ], 37 | "authors": [ 38 | "Gustavo Vargas", 39 | "Guy Aridor", 40 | "Matthew Rheault" 41 | ], 42 | "jest": { 43 | "moduleFileExtensions": [ 44 | "js", 45 | "json", 46 | "es6" 47 | ], 48 | "unmockedModulePathPatterns": [ 49 | "dist/", 50 | "src/", 51 | "__tests__/utils", 52 | "node_modules/" 53 | ], 54 | "testPathIgnorePatterns": [ 55 | "/node_modules/", 56 | "/__tests__/utils/" 57 | ], 58 | "testURL": "http://localhost" 59 | }, 60 | "scripts": { 61 | "test": "sh build.sh && jest", 62 | "build": "sh build.sh" 63 | }, 64 | "license": "MIT" 65 | } 66 | -------------------------------------------------------------------------------- /src/Experiment.js: -------------------------------------------------------------------------------- 1 | export default class Experiment { 2 | get(parameter) { 3 | throw "IMPLEMENT get"; 4 | } 5 | 6 | logExposure(opts) { 7 | throw "IMPLEMENT logExposure"; 8 | } 9 | 10 | getName() { 11 | throw "IMPLEMENT getName"; 12 | } 13 | 14 | previouslyLogged() { 15 | throw "IMPLEMENT previouslyLogged"; 16 | } 17 | 18 | shouldFetchExperimentParameter(name) { 19 | throw "IMPLEMENT shouldFetchExperimentParameter"; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/abtest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createReactClass from 'create-react-class'; 3 | import Parametrize from './parametrize'; 4 | 5 | const ABTest = createReactClass({ 6 | getInitialState() { 7 | return { 8 | hasRendered: false 9 | }; 10 | }, 11 | 12 | enrolledInVariation() { 13 | if (!this.state.hasRendered) { 14 | this.setState({ 15 | hasRendered: true 16 | }); 17 | } 18 | }, 19 | 20 | renderExposedVariation() { 21 | const { on, experiment } = this.props; 22 | 23 | if (!experiment) { 24 | console.error("You must pass in an experiment instance as a prop"); 25 | return null; 26 | } else if (!on) { 27 | console.error("You must pass an 'on' prop indicating what parameter you want to branch off"); 28 | return null; 29 | } 30 | 31 | return ( 32 | 38 | 39 | {this.props.children} 40 | 41 | 42 | ); 43 | }, 44 | 45 | render() { 46 | return this.renderExposedVariation(); 47 | } 48 | }); 49 | 50 | export default ABTest; 51 | -------------------------------------------------------------------------------- /src/parametrize.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import createReactClass from 'create-react-class'; 4 | 5 | const Parametrize = createReactClass({ 6 | getInitialState() { 7 | return { 8 | experimentParameters: null 9 | }; 10 | }, 11 | 12 | childContextTypes: { 13 | experimentParameters: PropTypes.object, 14 | experimentProps: PropTypes.object.isRequired 15 | }, 16 | 17 | getChildContext() { 18 | return { 19 | experimentParameters: this.state.experimentParameters, 20 | experimentProps: this.props 21 | }; 22 | }, 23 | 24 | componentWillMount() { 25 | this.fetchParameters(); 26 | }, 27 | 28 | fetchParameters() { 29 | const { experiment, params } = this.props; 30 | 31 | if (!experiment || !experiment.get) { 32 | console.error("You must pass in an experiment instance as a prop"); 33 | return; 34 | } else if (!params) { 35 | console.error("You must pass a list of params in as a prop"); 36 | return; 37 | } 38 | 39 | let paramsObj = {}; 40 | for (let i = 0; i < params.length; i++) { 41 | const param = params[i]; 42 | const paramVal = experiment.get(param); 43 | if (paramVal !== null && paramVal !== undefined) { 44 | paramsObj[param] = paramVal; 45 | } 46 | } 47 | 48 | if (Object.keys(paramsObj).length !== 0 && experiment.previouslyLogged() === false) { 49 | experiment.logExposure({ 50 | params: params, 51 | name: experiment.getName() 52 | }); 53 | } 54 | 55 | this.setState({ 56 | experimentParameters: paramsObj 57 | }); 58 | }, 59 | 60 | renderExperiment() { 61 | if (!this.state.experimentParameters) { 62 | return null; 63 | } 64 | 65 | const passThrough = this.props._passThrough; 66 | const renderedChildren = React.Children.map(this.props.children, (child) => { 67 | if (passThrough) { 68 | return React.cloneElement(child, this.state.experimentParameters); 69 | } else { 70 | return React.cloneElement(child, {}); 71 | } 72 | }); 73 | 74 | return ( 75 | 76 | { renderedChildren } 77 | 78 | ); 79 | }, 80 | 81 | render() { 82 | return this.renderExperiment(); 83 | } 84 | }); 85 | 86 | export default Parametrize; 87 | -------------------------------------------------------------------------------- /src/parametrizeComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createReactClass from 'create-react-class'; 3 | import Parametrize from './parametrize'; 4 | 5 | export default (experiment, experimentParams, Component) => createReactClass({ 6 | render() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | }); -------------------------------------------------------------------------------- /src/variationComponents.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import createReactClass from 'create-react-class'; 4 | 5 | export const When = createReactClass({ 6 | getInitialState() { 7 | return { 8 | shouldRender: false 9 | } 10 | }, 11 | 12 | contextTypes: { 13 | experimentParameters: PropTypes.object.isRequired, 14 | experimentProps: PropTypes.object.isRequired 15 | }, 16 | 17 | componentWillUpdate(props, state) { 18 | if (state.shouldRender) { 19 | this.context.experimentProps.enrolledInVariation(); 20 | } 21 | }, 22 | 23 | componentDidMount() { 24 | this.shouldRenderVariation(); 25 | }, 26 | 27 | shouldRenderVariation() { 28 | const value = this.props.value; 29 | const paramName = this.context.experimentProps.on; 30 | if (this.context.experimentParameters && this.context.experimentParameters[paramName] === value) { 31 | this.setState({ 32 | shouldRender: true 33 | }); 34 | } 35 | }, 36 | 37 | renderChildren() { 38 | return React.Children.map(this.props.children, (child) => { 39 | if (React.isValidElement(child)) { 40 | return React.cloneElement(child, {}); 41 | } 42 | return child; 43 | }); 44 | }, 45 | 46 | render() { 47 | if (!this.state.shouldRender) { 48 | return null; 49 | } 50 | 51 | return ( 52 | 53 | {this.renderChildren()} 54 | 55 | ); 56 | } 57 | }); 58 | 59 | export const Default = createReactClass({ 60 | contextTypes: { 61 | experimentProps: PropTypes.object.isRequired 62 | }, 63 | 64 | render() { 65 | if (this.context.experimentProps.hasRendered) { 66 | return null; 67 | } 68 | 69 | return ( 70 | 71 | {this.props.children} 72 | 73 | ); 74 | } 75 | }); 76 | -------------------------------------------------------------------------------- /src/withExperimentParams.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import createReactClass from 'create-react-class'; 4 | 5 | export default (Component) => { 6 | return createReactClass({ 7 | contextTypes: { 8 | experimentParameters: PropTypes.object.isRequired 9 | }, 10 | 11 | render() { 12 | return ( 13 | 14 | ); 15 | } 16 | }); 17 | }; --------------------------------------------------------------------------------