ABC News created an Adventr which allows viewers to answer a quiz in realtime. With our Realtime Player API, the page can be made to display instantly updated results, showing how other viewers have interacted or chosen. Play the Adventr below and see the page auto-update and react as you do.
If you want to peek under the hood on how this specific example was made, scroll down to read the tutorial.
C. Donald Trump
A. Chicken Wing – Pizza Combo
A. Seven
Adventr provides a powerful and easy-to-use JavaScript API that lets you do whatever you can imagine based on viewers’ engagement with your Adventr.
In this tutorial, I’ll show you how to use the Adventr Realtime Player API to show real-time quiz results as a viewer engages with an Adventr-powered quiz.
For the demo, we are simply saving the aggregate totals of each response, but you could also easily store responses along with user IDs to track each individual user’s responses in a quiz or survey.
We are going to listen for viewer choices and then save them to an external data source, and then display the aggregate results back to the viewer.
It might help to view the source of this page to follow along.
We start with a standard Adventr iframe embed and then add some divs to the HTML that will eventually display our questions and results (one div per question). They look like the following (with incrementing IDs (e.g. results_1
, results_2
, etc.):
<div class="results" id="results_1">
<h3 class="question"></h3>
<p class="correct-answer"><strong>A. Seven</strong></p>
<ul class="answers">
<li class="A">A <span class="count"></span></li>
<li class="B">B <span class="count"></span></li>
<li class="C">C <span class="count"></span></li>
<li class="D">D <span class="count"></span></li>
</ul>
</div>
Code language: HTML, XML (xml)
We have CSS that ensures these divs are hidden until they’re ready (look in the page source for the <style>
block).
For this example, we are using Airtable for our results data, since it provides an easy-to-use API for storing and retrieving data.
Note: For simplicity, we are accessing the Airtable API directly in javascript, however, this is not a recommended approach since it exposes our Airtable API Key. It would be better to handle the data retrieval and storage on your server-side so it’s more secure.
We are also using jQuery for Ajax requests and DOM manipulation, but you can easily use other libraries if you prefer.
Since we are only listening for user choices in the Adventr player, it’s overkill to use the player.js library. Instead, we start with a simple listener so we can trigger events based on choices within the Adventr. We are also going to listen for play
events and for responses to getCurrentClip
(which we use to identify where we are within the Adventr).
The following code is all you need to listen for choices from the Adventr player and can trigger any behavior you might imagine:
// this is the function called when message is received:
function receiveMessage(event) {
// first, determine if event is JSON
let received;
try {
received = JSON.parse(event.data);
} catch (e) {
received = {event: event.data}
}
// if the event is JSON, check that it's in the adventr/player.js format and if it's a choice
if (received.context === 'adventr' || received.context === 'player.js') {
if (received.event === 'choice') {
// a choice event happens every time there's a transition
updateAirTable(received.value, current_question);
}
else if (received.event === 'getCurrentClip') {
console.log("CLIP & CHOICES");
console.log(received.value);
// only advance the question number if there are multiple choices on the clip. this ensures we ignore intermediate auto-choose clips
if (received.value.choices.length > 1) {
current_question++;
}
}
else if (received.event === 'play') {
// the adventr has started, so let's obtain data about the first clip
getCurrentClip();
}
}
}
// Add the listener to the window:
window.addEventListener("message", receiveMessage, false);
Code language: JavaScript (javascript)
We have a helper function being called on the play event. This calls the Adventr API getCurrentClip method. Here’s that function:
// helper function for getting current clip and choices from adventr api
function getCurrentClip() {
let adventrPlayer = document.getElementById('adventrPlayer');
adventrPlayer.contentWindow.postMessage(
JSON.stringify({
context: 'adventr',
version: '4.0.0',
method: 'getCurrentClip'
}), '*'
);
}
Code language: JavaScript (javascript)
You’ll also see that when we receive a postMessage
that’s in the right format and has event=='choice'
, we pass the choice’s label to a function called updateAirTable()
. That function handles most of the rest of the behavior.
Before we define that function we define some variables we’ll be using:
let airtable_data = {};
let airtable_key = 'YOUR_PERSONAL_ACCESS_TOKEN_HERE';
// Note: it's insecure to directly use your a Personal Access Token
// in javascript like this (normally you'd want to implement a
// server-side version that hides your token).
// I'm only using this approach for this demo, and don't have any
// personal or other sensitive data in this table.
// And we'll declare a counter to define the current question:
let current_question = 0;
Code language: JavaScript (javascript)
We also need a function that obtains the state of the Airtable data when the page loads. This function uses Ajax to request the Airtable API and then copies the data into our airtable_data
variable.
function getAirTable() {
$.ajax ({
url: "https://api.airtable.com/v0/appDLZRNiYpkMsrgz/Table%201?maxRecords=3&view=Grid%20view",
beforeSend: function (xhr) {
xhr.setRequestHeader('Authorization', 'Bearer '+airtable_key);
},
success: function (data, textStatus, jqXHR) {
console.log (data);
airtable_data = data['records'];
},
error: function () {
}
});
}
// I'm using jQuery to call this function after the page is fully loaded:
$( document ).ready(function() {
getAirTable();
});
Code language: JavaScript (javascript)
Now, let’s come back to our main function of updateAirTable()
. Most of this function is handling the DOM manipulation to change the values and appearance of hidden HTML already on the page. Depending on your design, much of this could be very different.
I recommend viewing the source of the page to see the HTML that is being manipulated.
function updateAirTable(choice,question) {
console.log("UPDATE CALLED");
console.log(choice);
answer = choice.label;
answer_letter = answer.charAt(0); // first character of answer e.g. A, B, C, D
question_index = question-1; // airtable arrays start with 0 instead of 1
// only store data if it is in the right format e.g. "A. *", "B. *" -- this allows us to skip non-relevant choices
if (answer.charAt(1) == '.') {
// add 1 to the chosen answer
airtable_data[question_index]['fields'][answer_letter] += 1;
// find the html elment for this question
results_div = $('#results_'+question);
// put the question text into the html
results_div.find(".question").html(airtable_data[question_index]['fields']['Name']);
// find the most popular answer so we know how long to draw the bars
max_answer = Math.max(airtable_data[question_index]['fields']['A'], airtable_data[question_index]['fields']['B'], airtable_data[question_index]['fields']['C'], airtable_data[question_index]['fields']['D']);
// display the answer count for each
results_div.find("li.A .count").html(airtable_data[question_index]['fields']['A']);
results_div.find("li.B .count").html(airtable_data[question_index]['fields']['B']);
results_div.find("li.C .count").html(airtable_data[question_index]['fields']['C']);
results_div.find("li.D .count").html(airtable_data[question_index]['fields']['D']);
// calculate how wide to show each bar
results_div.find("li.A").css('width', airtable_data[question_index]['fields']['A']/max_answer*100 +'%');
results_div.find("li.B").css('width', airtable_data[question_index]['fields']['B']/max_answer*100 +'%');
results_div.find("li.C").css('width', airtable_data[question_index]['fields']['C']/max_answer*100 +'%');
results_div.find("li.D").css('width', airtable_data[question_index]['fields']['D']/max_answer*100 +'%');
// color the correct answer
results_div.find("li."+airtable_data[question_index]['fields']['Correct']).addClass("correct");
// color the answer chosen by the user
results_div.find("li."+answer_letter).addClass("mine");
// display a message to tell the user if they got it wrong or right
if (airtable_data[question_index]['fields']['Correct'] == answer_letter) {
results_div.find(".correct-answer").prepend("Great job! You got it right: ");
}
else {
results_div.find(".correct-answer").prepend("<span class='incorrect'>Oops, wrong choice!</span> The correct answer was: ");
}
// un-hide the html for this question
results_div.addClass('answered');
// hide the header so the questions are at the top
$('.header-detail').hide();
// remove the createdTime column from this airtable row so we can prepare to save it back to the database
delete airtable_data[question_index]['createdTime'];
// Note if you want to store the results for each individual, you could add a user ID or some other unique identifier and save each response to your database
// use the airable api to update this row
$.ajax ({
url: "https://api.airtable.com/v0/appDLZRNiYpkMsrgz/Table%201",
beforeSend: function (xhr) {
xhr.setRequestHeader('Authorization', 'Bearer '+airtable_key);
},
method: 'patch',
contentType: 'application/json',
data: JSON.stringify({records: [airtable_data[question_index]]}),
success: function (data, textStatus, jqXHR) {
//console.log("AIRTABLE RESULT");
//console.log (data);
},
error: function () {
// do something on error
}
});
}
// after each clip change, get data about next choice
getCurrentClip();
}
Code language: JavaScript (javascript)
As you can see, the majority of the code here is modifying the DOM and customizing how we display the data. The actual listening for choice events and doing something with those events is fairly simple.
We’d love to see what you create with the API. Please post in the forums if you have something you want to show off!