A/B Testing Your AngularJS Templates (Part 1)

If you’d like to A/B test your single page application templates, it is important to create a clean and scalable solution instead of cluttering your templates with conditionals, as such:

sign_up_form.haml
%sign-up-form
%input{type: 'email', name: 'email'}
%submit#red-button{ng_if: "variant == 'a'"}
%submit#blue-button{ng_if: "variant == 'b'"}

One clean alternative is to create separate templates per variant. The code is much easier to manage and track down variant specific bugs.

In this example, we’re using a Rails back-end and split, which will drive the experiment. On initial data fetch, our API controller will send back the variant value by setting it to the response header we’ll call “X-Variant”:

class ItemsController < ApplicationController
def index
variant = ab_test(:form_variant, 'a', 'b')
response.headers["X-Variant"] = variant
...
end
end

We’ll create an interceptor to capture this variant value from the HTTP response in our Angular code. The interceptor will look to see if the variant header is present and pass it to the service that will store the variant value:

angular.module 'my-app'
.config ($httpProvider) ->
$httpProvider.interceptors.push (VariantService) ->
response: (response) ->
if response.headers('X-Variant')
VariantService.set('form_variant', response.headers('X-Variant'))

VariantService simply encapsulates any key-value pair we pass to it:

variant.service.coffee
angular.module 'my-app'
.service 'VariantService', ->
props = {}

set: (key, value) ->
props[key] = value
get: (key) ->
props[key]

We’ll split our sign_up_form.haml into two separate templates:

sign_up_form_a.haml
%sign-up-form
%input{type: 'email', name: 'email'}
%submit#red-button
sign_up_form_b.haml
%sign-up-form
%input{type: 'email', name: 'email'}
%submit#blue-button

Now our form directive will fetch the template based on the variant value from the VariantService:

sign_up_form.directive.coffee
angular.module 'my-app'
.directive 'signUpForm', (VariantService, $compile, $templateRequest) ->
...
link: ($scope, $element) ->
variant = VariantService.get('form_variant')
templateUrl = "sign_up_form_#{variant}.html"
    $templateRequest(templateUrl).then (html) ->
template = angular.element(html)
$element.append(template)
$compile(template)($scope)

Instead of specifying the template or templateUrl in the directive’s definition, we request and compile the template in the link function. This approach is written about here.

Now on the initial data load request to the server, the API will send back a variant value that will determine which template (sign_up_form_a.haml or sign_up_form_b.haml) gets rendered.

Notes:

The example code here adheres to John Papa’s style guide: https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md