Experience API - not only for LRSes

Experience API (xAPI) is a specification made with e-learning in mind. It is generic in its form. For H5P, xAPI-support means xAPI-statements are generated when users interact with the different content types. E.g. for the Multiple Choice content type, three types of xAPI statement may be generated:

  • Attempted (The Multiple Choice question is displayed on screen)
  • Interacted (The user has selected an alternative)
  • Answered (The user finished the task. Statement contains the result, the user's answer, and the correct answer)

xAPI statements are traditionally sent to an LRS (Learning Record Store). The LRS typically aggregates the data and provides drill down reports, learning analytics etc. But xAPI can be utilized for so much more. Here on h5p.org, for example, we use the statements to send data to Google Analytics. In this way, we can e.g. analyze the percentage of the users that looked at all the slides in a Course Presentation.

In this post, we will experiment with how we may use xAPI to communicate between different elements on a web page. For this we have created the following proof-of-concept page:

Visit demo page

Did you visit the link above? If so, you may have noticed that each lesson displays an H5P. To be able to get the score from each lesson, we have added a custom JavaScript that among other things listens for statements sent. These statements are used when updating the total score.

Code walkthrough

The proof-of-concept is developed as a Drupal module, and consists mainly of JavaScript and CSS. The Drupal-specific code is only about including these files. The DOM structure is made manually using the Drupal wysiwyg and utilizing the shortcode support. Here is an example which contains one lesson:

<div class="h5p-lessons-result">
  <div class="h5p-lessons-progress">Progress</div>
  <div class="h5p-lessons-score">Score</div>
</div>

<div class="h5p-lessons" data-total-score="13">
  <div class="h5p-lesson active" data-h5p-content-id="16559">
    <div class="h5p-lesson-heading">Lesson 1</div>
    <div class="h5p-lesson-intro">Basic intro to H5P</div>
    <button class="h5p-lesson-run">Begin</button>
  </div>
</div>

<div class="h5p-lesson-content">
  <div class="h5p-lesson-h5p" data-h5p-content-id="16559">
    <div class="h5p-lesson-h5p-heading">Lesson 1<a href="javascript:void(0)" class="quit-lesson">Quit lesson</a></div>
    H5P shortcode here
  </div>
</div>

The page has three parts:

  • The results (progress and score): .h5p-lessons-result
  • The lessons overview: .h5p-lessons
  • The actual lessons including the H5Ps: .h5p-lesson-content

The H5Ps itself are initially hidden by using CSS.

The JavaScript code starts off by finding all H5Ps on the current page as seen below:

var numLessons = 0;

// Iterate all lessons:
$('.h5p-lesson-run').each(function () {
  var $self = $(this);
  var $lessonBox = $self.parent();
  numLessons++;

  // Get the H5P contentId
  var cid = $lessonBox.data('h5p-content-id');
  
  // Find the H5P class instance, by providing the content ID:
  getInstance(cid, function (instance) {
    
    // Create a new Lesson instance
    var lesson = new XAPIDemo.Lesson($lessonBox, $lessonOverlay, instance);

    // Handling clicks on the begin lesson button
    $self.click(function () {
      if (!$lessonBox.hasClass('locked')) {
        lesson.show();
      }
    });

    // Handle lesson beeing finished
    // If score is available, it is found in event.data 
    lesson.on('finished', function (event) {
      self.progressWidget.increment();
      if (event.data) {
        scoreWidget.increment(event.data);
      }

      // Enable the next lession
      enableNextLesson();
    });
  });
});

Below is the Lesson class, which mainly handles showing/hiding the lesson and acting on xAPI statements. 

XAPIDemo.Lesson = (function () {
  function Lesson($lessonBox, $lessonOverlay, instance) {
    var self = this;
    var contentId = $lessonBox.data('h5p-content-id');
    var $lesson = $('.h5p-lesson-h5p[data-h5p-content-id=' + contentId + ']');
    $lesson.attr('role', 'dialog');

    var $heading = $lesson.find('.h5p-lesson-h5p-heading');
    $heading.attr('tabindex', 0);
    
    // Lesson inherits the EventDispatcher class
    H5P.EventDispatcher.call(self);

    // Handles xAPI statements
    var handleXapi = function (event) {
      var stmt = event.data.statement;
      var isParent = (stmt.context.contextActivities.parent === undefined);

      if (isParent && stmt.result !== undefined && stmt.result.completion === true) {
        setTimeout(function () {
          self.hide(event.getScore());
        }, 2500);
      }
    };

    // Display H5P in overlay
    self.show = function () {
      $lesson.addClass('open');
      $lessonOverlay.addClass('open');

      $('.quit-lesson', $lesson).bind('click', function () {

        var confirmDialog = new H5P.ConfirmationDialog({headerText: 'Are you sure?', dialogText: 'If quiting this lesson, no score will be given.'});
        confirmDialog.appendTo($lesson.get(0));

        confirmDialog.on('confirmed', function () {
          self.hide();
        });

        confirmDialog.show();
      });

      // Listen to xAPI events!
      instance.on('xAPI', handleXapi);

      if (instance) {
        instance.trigger('resize');
      }

      $heading.focus();
    };

    // Hide the H5P
    self.hide = function (scoring) {
      $lesson.toggleClass('open');
      $lessonOverlay.toggleClass('open');
      instance.off('xAPI');
      $('.quit-lesson', $lesson).unbind('click');
      $lessonBox.addClass('done');
      self.trigger('finished', scoring);
    };
  }

  // Lesson inherits the EventDispatcher class
  Lesson.prototype = Object.create(H5P.EventDispatcher.prototype);
  Lesson.prototype.constructor = Lesson;

  return Lesson;
})();

Worth to mention here is the handling of xAPI statement (the handleXapi function), where we have to check for two things to be sure the H5P is completed:

  1. The statement's contextActivities has no parent. I.e if a statement is sent from a question inside a Question Set, that doesn't mean the Question Set is completed. When Question Set itself says it is completed we know all questions inside it are completed. In this case, the statement won't have any parent.
  2. The statement.result.completion must be true (i.e. use this instead of using the type of verb for the statement)

There are also JavaScript code for creating the score and progress widget. If you would like to get the complete picture, please look at the JavaScript file here and the CSS here

For further readings about xAPI and H5P, have a look here

Please leave a comment below if you have any other bright ideas on how to utilize xAPI! Also leave a comment if you would like to see an H5P version of the demo.