Skip to content

Commit

Permalink
Add context passing and rename del prop to clear.
Browse files Browse the repository at this point in the history
Fixes issue #1 and issue #2.
  • Loading branch information
Ola Holmström committed Aug 24, 2015
1 parent 813f982 commit 5590349
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 24 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,8 @@
### 0.4.0 (2015-08-24)

* Add context passing of props for convenience.
* Rename `del` to `clear`.

### 0.3.1 (2015-01-23)

* Fix bug when there are multiple children och Variant.
Expand Down
86 changes: 82 additions & 4 deletions README.md
Expand Up @@ -24,7 +24,7 @@ or
bower install react-ab --save
```

## Example
## Examples

Using [Mixpanel](https://mixpanel.com/).

Expand Down Expand Up @@ -109,6 +109,56 @@ var App = React.createClass({
});
```

Server side with Express.js and using ES6:

```js
import express from "express";
import cookieParser from "cookie-parser";

import React from "react/addons";
import { Experiment, Variant } from "react-ab";

var cookie = {};

var App = React.createClass({
choice: function (experiment, variant, index) {
console.log(experiment, variant, index);
}

, render: function () {
return (
<div>
<Experiment {...this.props} onChoice={this.choice} name="tagline">
<Variant name="normal">
<h1> A A/B testing component for React </h1>
</Variant>
<Variant name="enterprise">
<h1> A vertically integrated React component </h1>
</Variant>
<Variant name="lies">
<h1> One weird React component that will increase your metrics by 100%! </h1>
</Variant>
</Experiment>
</div>
);
}
});

var app = express();

app.use(cookieParser());

app.get("/", function (req, res) {
res.send(React.renderToString(<App
get={(x) => req.cookies[x]}
set={(x, y) => res.cookie(x, y)}
clear={res.clearCookie}
/>));
});

app.listen(3000);
```

## API

### Experiment
Expand All @@ -132,10 +182,38 @@ usually from a cookie.
Random function, should return a number in the range [0, 1). The default uses
`crypto.getRandomValues()` when available and falls back on `Math.random`.

##### get, set and del
##### get

A function that takes an `experiment` and returns a `variant`.

##### set

A function that takes an `experiment` and `variant` and stores it.

##### clear

A function that clears/unsets an `experiment`.

#### Context

`get, set, clear, random` can also be set from `context`. If these props
exists they overwrite `context`.

##### randomExperiment

`random` function taken from `context`.

##### getExperiment

`get` function taken from `context`.

##### setExperiment

`set` function taken from `context`.

##### clearExperiment

Get, set and delete stored experiments. By default these functions use browser
cookies. When rendering server side these should be changed appropriately.
`clear` function taken from `context`.

#### Methods

Expand Down
2 changes: 1 addition & 1 deletion bower.json
Expand Up @@ -3,7 +3,7 @@
"main": [
"react-ab.js"
],
"version": "0.3.2",
"version": "0.4.0",
"homepage": "https://github.com/olahol/react-ab",
"description": "Simple isopmorphic A/B testing component for React.",
"keywords": [
Expand Down
4 changes: 2 additions & 2 deletions package.json
@@ -1,6 +1,6 @@
{
"name": "react-ab",
"version": "0.3.2",
"version": "0.4.0",
"description": "Simple isopmorphic A/B testing component for React.",
"main": "react-ab.js",
"dependencies": {
Expand All @@ -17,7 +17,7 @@
},
"scripts": {
"dev": "beefy dev.js --live"
, "test": "mocha"
, "test": "./node_modules/.bin/mocha"
, "coverage": "./node_modules/istanbul/lib/cli.js cover --print detail ./node_modules/mocha/bin/_mocha -- --ui bdd -R spec"
},
"repository": {
Expand Down
52 changes: 35 additions & 17 deletions react-ab.js
Expand Up @@ -22,7 +22,7 @@
}
};

var cookie = {
var browserCookie = {
get: function (name) {
var eq = name + "="
, ca = document.cookie.split(";")
Expand Down Expand Up @@ -54,8 +54,8 @@
document.cookie = [key, expires, path].join(";");
}

, del: function (name) {
this.set(name, "", -1);
, clear: function (name) {
browserCookie.set(name, "", -1);
}
};

Expand All @@ -75,16 +75,7 @@
});

exports.Experiment = React.createClass({
getDefaultProps: function () {
return {
get: cookie.get
, set: cookie.set
, del: cookie.del
, random: random
};
}

, getInitialState: function () {
getInitialState: function () {
return {
index: null
};
Expand All @@ -94,10 +85,37 @@
name: React.PropTypes.string.isRequired
, children: React.PropTypes.array.isRequired
, onChoice: React.PropTypes.func.isRequired
, random: React.PropTypes.func
, get: React.PropTypes.func
, set: React.PropTypes.func
, clear: React.PropTypes.func
}

, contextTypes: {
randomExperiment: React.PropTypes.func
, getExperiment: React.PropTypes.func
, setExperiment: React.PropTypes.func
, clearExperiment: React.PropTypes.func
}

, random: function () {
return this.props.random || this.context.randomExperiment || random;
}

, get: function () {
return this.props.get || this.context.getExperiment || browserCookie.get;
}

, set: function () {
return this.props.set || this.context.setExperiment || browserCookie.set;
}

, clear: function () {
return this.props.clear || this.context.clearExperiment || browserCookie.clear;
}

, componentWillMount: function () {
var variant = this.props.get(this.cookieName());
var variant = this.get()(this.cookieName());

for (var i = 0; i < this.props.children.length; i += 1) {
if (variant === this.props.children[i].props.name) {
Expand All @@ -111,10 +129,10 @@
}

, chooseVariant: function (fire) {
var index = Math.floor(this.props.random() * this.props.children.length)
var index = Math.floor(this.random()() * this.props.children.length)
, variant = this.props.children[index].props.name;

this.props.set(this.cookieName(), variant);
this.set()(this.cookieName(), variant);

this.setState({ index: index });
this.props.onChoice(this.props.name, variant, index, false);
Expand All @@ -134,7 +152,7 @@
}

, clearCookie: function () {
this.props.del(this.cookieName());
this.clear()(this.cookieName());
}

, render: function () {
Expand Down
47 changes: 47 additions & 0 deletions test/index.js
Expand Up @@ -99,4 +99,51 @@ describe("Experiment", function () {

assert.ok(ex);
});

it("should work with isomorphic", function () {
var variant1 = React.createElement(Variant, { name: "one" }, React.createElement("span", null, "one"));
var variant2 = React.createElement(Variant, { name: "two" }, React.createElement("span", null, "two"));
var cookie = {};
var html = React.renderToString(React.createElement(Experiment, {
name: "test"
, get: function (x) { return cookie[x]; }
, set: function (x, y) { cookie[x] = y; }
, clear: function (x) { delete cookie[x] }
, onChoice: function () { }
}, variant1, variant2));

assert.ok(/(one|two)/.test(html), "one or two should be in html");
assert.equal(Object.keys(cookie).length, 1, "there should be one key");
});

it("should work with isomorphic context", function () {
var cookie = {};

var App = React.createClass({
childContextTypes: {
getExperiment: React.PropTypes.func
, setExperiment: React.PropTypes.func
, clearExperiment: React.PropTypes.func
}

, getChildContext: function () {
return {
getExperiment: function (x) { return cookie[x]; }
, setExperiment: function (x, y) { cookie[x] = y; }
, clearExperiment: function (x) { delete cookie[x] }
};
}

, render: function () {
var variant1 = React.createElement(Variant, { name: "one" }, React.createElement("span", null, "one"));
var variant2 = React.createElement(Variant, { name: "two" }, React.createElement("span", null, "two"));

return React.createElement(Experiment, { name: "test" , onChoice: function () { } }, variant1, variant2);
}
});

var html = React.renderToString(React.createElement(App));
assert.ok(/(one|two)/.test(html), "one or two should be in html");
assert.equal(Object.keys(cookie).length, 1, "there should be one key");
});
});

0 comments on commit 5590349

Please sign in to comment.