Discrete Matrix Walkthrough - RedwoodAdmin/RedwoodFramework GitHub Wiki
This walkthrough focuses on the main experiment page of the Discrete Matrix example and explains the Javascript and HTML code in detail. The experiment can be downloaded by clicking the 'Discrete Matrix' link on this page.
Redwood.controller("SubjectCtrl", ["$rootScope", "$scope", "RedwoodSubject", function($rootScope, $scope, rs) {
Redwood
is the AngularJS application that is created by the redwood framework. The line above adds a controller to this application which will contain all the experiment code. AngularJS uses the Model-View-Controller (MVC) pattern where the controller is responsible for managing what is displayed in the view (defined by the html).
The above controller definition tells Angular that it needs three things (called dependencies), $rootScope
, $scope
, and RedwoodSubject
. $rootScope and $scope are built-in objects that angular provides, and these form the 'model' part of the MVC pattern. $scope is used to contain the current state of the experiment and its properties are directly accessible in the view. Redwood creates some properties on $rootScope which are also directly accessible in the view. RedwoodSubject
is the redwood subject library which is in the form of an angular service. The full names of these dependencies are specified as string values, while the function parameters can be used to specify aliases for these services, such as rs
for RedwoodSubject
in this example.
rs.on_load(function() {
The on_load
function provides an entry point to the experiment. The given function is the first function to be called once the framework has loaded the current period.
rs.config.pairs.forEach(function(pair, index) {
var userIndex = pair.indexOf(parseInt(rs.user_id));
if(userIndex > -1) {
$scope.pair_index = index;
$scope.user_index = userIndex;
$scope.partner_id = pair[($scope.user_index + 1) % 2].toString();
}
});
$scope.matrix = $scope.user_index === 0 ? rs.config.matrix : transpose(rs.config.matrix);
$scope.round = 0;
All fields defined in the config.csv file are available via rs.config
. The above code goes through the array of pairs to find the position of the current subject. It then sets a number of properties on the $scope object for use in the view and subsequent functions.
As will be shown later, any $scope property (such as $scope.round
) can be referenced in the view html and whenever its value is changed, AngularJS will automatically update the displayed value in the view.
rs.synchronizationBarrier('on_load').then(function() {
rs.trigger("next_round");
});
});
rs.synchronizationBarrier
provides a means of waiting for other subjects to reach the same point before continuing. Each synchronization barrier requires a unique id, in this case 'on_load'. (If, for instance, a synchronization barrier is required for each round of a period, then include the round number in the id to ensure that each id is definitely unique within that period.) The .then
function is used to provide the code that should be called once all subjects are at the same place. In this case it triggers a message, "next_round".
Redwood uses a messaging architecture where all state changes should be conducted by first sending a message to the server and then only performing the required actions in response to that message. This allows the framework to recover the experiment in the case of a page refresh.
rs.trigger
is used to send a message to the server which can then be responded to using rs.on()
as we will see later.
$scope.onSelection = function(selection) {
if($scope.inputsEnabled) {
$scope.selection = selection;
}
};
$scope.confirm = function() {
if(!angular.isNullOrUndefined($scope.selection)) {
$scope.inputsEnabled = false;
rs.trigger("action", $scope.selection);
} else {
alert("Please select an action.");
}
};
The above blocks define two functions on $scope which are used by the view to capture user input. Note that the first function onSelection
sets a $scope property without triggering a message. In this case it is on purpose because we don't mind losing the current selection if the page refreshed. However, as soon as the user confirms their selection, we need to make sure that this isn't lost and so trigger a message "action". Another thing to notice is that we disable user input $scope.inputsEnabled = false;
as soon as a selection is successfully confirmed, so that we prevent the possibility of a duplicate confirmation.
rs.on("action", function(value) {
$scope.inputsEnabled = false;
$scope.selection = value;
$scope.action = value;
rs.synchronizationBarrier('round_' + $scope.round, [$scope.partner_id]).then(function() {
allocateRewards($scope.action, $scope.partnerAction);
rs.trigger("next_round");
});
});
This is the handler for the 'action' message triggered in the previous block. For this experiment we only support a single submission so note that $scope.inputsEnabled = false;
is repeated here to ensure that user input is disabled. This is important since $scope.confirm
won't be called during a page recovery because it wasn't triggered by a message, it was triggered by the user.
Also note the use of the round number in the synchronizationBarrier id to ensure that it is always unique.
rs.recv("action", function(sender, value) {
if(sender == $scope.partner_id) {
$scope.partnerAction = value;
}
});
rs.on()
is used for messages that sent by the current user. To receive messages from other users, use rs.recv
as above. This block sets $scope.partnerAction
which will then be updated in the view by Angular.
rs.on("next_round", function() {
$scope.round++;
$scope.rounds = $.isArray(rs.config.rounds) ? rs.config.rounds[$scope.pair_index] : rs.config.rounds;
$scope.prevAction = $scope.action;
$scope.prevPartnerAction = $scope.partnerAction;
if($scope.round > $scope.rounds) {
rs.next_period(5);
} else {
$scope.inputsEnabled = true;
}
});
The block above handles the 'next_round' message by checking how many rounds are specified in the config and then either advancing to the next round or period. rs.next_period
is used to advance the current subject to the next period. The subject will then wait at the beginning of the next period for all subjects to advance to that period. rs.next_period
also accepts a delay parameter which delays advancing the period by a number of seconds, in this case 5.
var allocateRewards = function(ai, aj) {
$scope.reward = $scope.matrix[ai - 1][aj - 1][0];
rs.add_points($scope.reward);
$scope.partnerReward = $scope.matrix[ai - 1][aj - 1][1];
};
This function allocates points to the subject according to the rewards matrix in the config file. rs.add_points
is used to allocate points to the current subject.
var transpose = function(matrix) { //transpose a 2x2 matrix
var transposed = [[[], []], [[], []]];
for(var i = 0; i < 4; i++){
var row = Math.floor(i/2);
var column = i % 2;
transposed[column][row] = [matrix[row][column][1], matrix[row][column][0]];
}
return transposed;
};
}]);
This last function is simply a helper function to transpose a rewards matrix so that each opposing subject has an inverse matrix.
Redwood.filter("action", function() {
var actions = {
1: "A",
2: "B"
};
return function(value) {
return actions[value];
};
});
AngularJS provides a feature called 'filters'. A filter is simply a function that takes a value and returns a corresponding value. The power of filters is that they can be used directly from the view to achieve things like changing units of measure, localizing dates, etc. This particular filter just converts a number 1 or 2 into its corresponding label 'A' or 'B'.
The 'view' for the experiment is defined in html and is described below.
{% load verbatim %}
This directive is for the Django application to load the verbatim plugin which is needed later.
<!DOCTYPE HTML>
This the HTML5 document header and should be included in all views.
<html ng-app="Redwood">
The ng-app attribute is used to bootstrap the AngularJS application and everything within this tag is then considered by Angular to be part of the 'Redwood' application. Angular then goes through the contents of this tag and processes all other Angular attributes.
<head>
<title>Start</title>
<script type="text/javascript" src="{{ STATIC_URL }}framework/js/lib/jquery/jquery.min.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}framework/js/lib/bootstrap/bootstrap-3.1.1.min.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}framework/js/lib/angular/angular-1.2.16.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}framework/js/redwoodCore.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}framework/js/redwoodHelpers.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}framework/js/redwoodSubject.js"></script>
<link type="text/css" rel="stylesheet" href="{{ STATIC_URL }}framework/css/bootstrap-3.1.1.min.css"></link>
<script type="text/javascript">
{{ js }}
</script>
<style type="text/css">
{{ css }}
</style>
</head>
This is the html header block where all scripts and css should be included. The set of scripts included above are all required by the framework and should be included in all subject pages. Any additional scripts can be added here as well.
The {{ }}
delimiters in this context are processed by the Django application on the server and are replaced with their actual values where STATIC_URL
is the path to the static folder in the redwood repository. {{ js }}
and {{ css }}
are replaced with the contents of the JavaScript and CSS that was saved for this page.
{% verbatim %}
This tells the Django application that all subsequent text should not be processed for {{ }}
delimiters. This is important since AngularJS uses the exact same delimiters and will cause errors on the server if they are not ignored.
<body ng-controller="SubjectCtrl">
The ng-controller
attribute tells Angular that the contents of this tag are controlled by the 'SubjectCtrl' controller allowing us to access the properties on that controller's $scope.
<div class="navbar navbar-fixed-top container">
<div class="navbar navbar-default" style="margin-bottom: 0;">
<div class="navbar-brand" href="#">Economics Experiment</div>
<ul class="nav navbar-nav">
<li class="active">
<a>User ID: <span>{{$root.user_id}}</span></a>
</li>
</ul>
<div class="navbar-right">
<div class="navbar-text">Period: <span>{{$root.period}}</span></div>
<div class="navbar-text">Round: <span>{{round}} / {{rounds}}</span></div>
<div class="navbar-text">Total Reward: <span>{{$root.totalPoints}}</span></div>
</div>
</div>
</div>
This is the header bar for the experiment and displays a number of state variables from the controller. Any property on the $scope can be displayed in the view using the syntax {{propertyName}}
. All variables are assumed to be on $scope and can therefore be referenced directly without prefixing with $scope.
For instance {{round}} / {{rounds}}
displays the current round out of the total number of rounds. The values will be automatically updated when the controller changes the $scope.round property. $root.totalPoints
specifies that this property is not on $scope but is instead on $rootScope. In fact this specification is not necessary unless the property exists on both $scope and $rootScope. If not specified, Angular will actually resolve it regardless of which $scope it is on.
<div class="container">
The main container for the experiment content. class="container"
specifies a bootstrap class for this element. Most classes in this view are bootstrap classes.
<key-press key-code="38" callback="onSelection(1)"></key-press>
<key-press key-code="40" callback="onSelection(2)"></key-press>
<key-press key-code="13" callback="confirm()"></key-press>
Redwood defines a keypress a directive for easily including keyboard functionality in experiments. It can be used as shown above where key-code
specifies which keyboard button to react to and callback
specifies a function on the $scope to be called when that key is pressed.
<div class="row">
<div class="col-lg-2"></div>
<div class="col-lg-10">
<div class="row">
<div class="col-lg-5">
<div class="row">
<div class="col-lg-12">
<table id="input-matrix" class="table table-bordered table-condensed">
<tr style="height:40px;">
<td></td>
<td ng-class="{'selected': prevPartnerAction == 1}">A</td>
<td ng-class="{'selected': prevPartnerAction == 2}">B</td>
The above two elements are the table cells to display the opponent's action. They need to be highlighted blue according to the selected action and for this we can use the ng-class
attribute. The first cell will be given the class 'selected' is the opponent's action is 1 (A), while the second cell will be given the 'selected' class if the opponent's action is 2 (B). The actual styling for the selected class (the blue background) is defined in the CSS for this page.
</tr>
<tr style="height:40px;">
<td class="input-cell" ng-class="{'selected': selection == 1}">
<span class="disabled" ng-hide="inputsEnabled">A</span>
<a ng-click="onSelection(1)" ng-show="inputsEnabled" class="input-link" href>A</a>
</td>
The <td>
element above is for the user to enter their selection. ng-click
can be used to call a function on the scope in response to a click event on the element. In this case we call onSelection
with an argument of 1 to indicate that selection 'A' has been clicked.
However, we only want them to be able to change their selection if inputs are enabled. ng-show
and ng-hide
can be used to show and hide elements based on a true/false expression (consisting of properties on the $scope). In this case all we use is inputsEnabled
to decide whether to show a clickable link or just to show a grayed out <span>
.
<td ng-class="{'selected': prevPartnerAction == 1 || selection == 1, 'double-selected': prevPartnerAction == 1 && selection == 1}">
Multiple classes can be applied using ng-class
as seen above. In this case we apply the 'double-selected' class to darken the shading if the user actions intersect on this cell.
<strong>{{matrix[0][0][0]}}</strong>, <span>{{matrix[0][0][1]}}</span>
</td>
<td ng-class="{'selected': prevPartnerAction == 2 || selection == 1, 'double-selected': prevPartnerAction == 2 && selection == 1}">
<strong>{{matrix[0][1][0]}}</strong>, <span>{{matrix[0][1][1]}}</span>
</td>
</tr>
<tr style="height:40px;">
<td class="input-cell" ng-class="{'selected': selection == 2}">
<span class="disabled" ng-hide="inputsEnabled">B</span>
<a ng-click="onSelection(2)" ng-show="inputsEnabled" class="input-link" href>B</a>
</td>
<td ng-class="{'selected': prevPartnerAction == 1 || selection == 2, 'double-selected': prevPartnerAction == 1 && selection == 2}">
<strong>{{matrix[1][0][0]}}</strong>, <span>{{matrix[1][0][1]}}</span>
</td>
<td ng-class="{'selected': prevPartnerAction == 2 || selection == 2, 'double-selected': prevPartnerAction == 2 && selection == 2}">
<strong>{{matrix[1][1][0]}}</strong>, <span>{{matrix[1][1][1]}}</span>
</td>
</tr>
</table>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<button ng-click="confirm()" ng-disabled="!inputsEnabled" class="btn btn-success">Ready</button>
ng-disabled
works similarly to ng-hide
except that the element is disabled instead of being hidden. HTML only allows certain elements to be disabled, such as buttons and inputs (text-boxes, etc.).
</div>
</div>
</div>
<div class="col-lg-2"></div>
<div class="col-lg-5" style="border-left: 1px solid #eee;">
<p>Previous Round:</p>
<table id="results-table" class="table table-bordered table-condensed">
<tr>
<th></th>
<th>Action</th>
<th>Reward</th>
</tr>
<tr style="font-weight:bold;">
<td>You</td>
<td>{{prevAction | action}}</td>
The pipe operator '|', is used to apply an Angular filter. We added the 'action' filter previously in the JavaScript code and we now use it to Display 'A' or 'B' to the user instead of '1' or '2'.
<td>{{reward}}</td>
</tr>
<tr>
<td>Counterpart</td>
<td>{{prevPartnerAction | action}}</td>
<td>{{partnerReward}}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
<div class="row">
<hr style="margin-top:20px;"/>
<div class="col-lg-12" id="footer">
</div>
</div>
</div>
</body>
{% endverbatim %}
This is just to close the verbatim
block that we defined for Django.
</html>