From 5590349f9a4b37ac067c4cfbb91caaf0ee1553d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ola=20Holmstr=C3=B6m?= Date: Mon, 24 Aug 2015 21:39:35 +0200 Subject: [PATCH] Add context passing and rename `del` prop to `clear`. Fixes issue #1 and issue #2. --- CHANGELOG.md | 5 +++ README.md | 86 ++++++++++++++++++++++++++++++++++++++++++++++++--- bower.json | 2 +- package.json | 4 +-- react-ab.js | 52 +++++++++++++++++++++---------- test/index.js | 47 ++++++++++++++++++++++++++++ 6 files changed, 172 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0144bde..6a925c1 100644 --- a/CHANGELOG.md +++ b/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. diff --git a/README.md b/README.md index 23255b9..21be9a5 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ or bower install react-ab --save ``` -## Example +## Examples Using [Mixpanel](https://mixpanel.com/). @@ -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 ( +
+ + +

A A/B testing component for React

+
+ +

A vertically integrated React component

+
+ +

One weird React component that will increase your metrics by 100%!

+
+
+
+ ); + } +}); + +var app = express(); + +app.use(cookieParser()); + +app.get("/", function (req, res) { + res.send(React.renderToString( req.cookies[x]} + set={(x, y) => res.cookie(x, y)} + clear={res.clearCookie} + />)); +}); + +app.listen(3000); +``` + ## API ### Experiment @@ -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 diff --git a/bower.json b/bower.json index 24e38c9..7d84f48 100644 --- a/bower.json +++ b/bower.json @@ -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": [ diff --git a/package.json b/package.json index 13abf5d..8651366 100644 --- a/package.json +++ b/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": { @@ -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": { diff --git a/react-ab.js b/react-ab.js index cec6588..926a659 100644 --- a/react-ab.js +++ b/react-ab.js @@ -22,7 +22,7 @@ } }; - var cookie = { + var browserCookie = { get: function (name) { var eq = name + "=" , ca = document.cookie.split(";") @@ -54,8 +54,8 @@ document.cookie = [key, expires, path].join(";"); } - , del: function (name) { - this.set(name, "", -1); + , clear: function (name) { + browserCookie.set(name, "", -1); } }; @@ -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 }; @@ -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) { @@ -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); @@ -134,7 +152,7 @@ } , clearCookie: function () { - this.props.del(this.cookieName()); + this.clear()(this.cookieName()); } , render: function () { diff --git a/test/index.js b/test/index.js index 5fc199f..ccbc19e 100644 --- a/test/index.js +++ b/test/index.js @@ -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"); + }); });