Tutorial: Embedding an Activity iframe inside an LMS - GeneralizedLearningUtilities/SuperGLU GitHub Wiki

A common case where the functionality of the SuperGLU library is to allow many different iframes to be embedded as activities into a single learning system, such as a learning management system (LMS). This shows a two-frame example of this functionality, which can be quickly adapted by someone with some basic skills in HTML and JavaScript. An example of this kind of functionality is shown in the reference implementation example Simple Parent-Child HTML.

This tutorial covers a few topics:

Skills Needed: Basic HTML and JavaScript proficiency

Hardware Needed for Tutorial: None

Hardware Needed for Real-Life Use: A web server able to host static files for HTML and JavaScript (e.g., even a DropBox 'public' folder which can host an HTML file will work)

Running and Understanding the Example

This example can be tested easily. First, download the source code from the GIT repository. Since this example depends only on JavaScript, it can be run directly on any machine with a modern browser, without any additional installation.

  1. Download the source code as a zip file or through a git pull.
  2. Navigate to /SuperGLU/Examples/SimpleParentChildHTML
  3. Open "ParentPage.html"

When you open this page, it will have three main states, from the standpoint of the Parent page. The smaller frame seen in the page is ChildPage.html, which was opened with certain URL parameters. These states are each associated with a specific message:

  1. Loaded Message - Waiting for the child page activity to be Loaded
  2. Heartbeat Messages - Monitoring that the child page has not failed or broken during the activity, by monitoring a heartbeat message.
  3. Completed Message - Receiving a message that the child page activity is Completed and has some score.

State 1: Loading Child Activity Frame

You should now see a page that looks like: Waiting for Activity to Finish

State 2: Activity in Progress and Waiting for Completed Score

There is actually a loading screen that is shown before this, but it typically goes so quickly that you will not see it. It looks as followed (may look slightly different from your version): Waiting to Load

State 3: Activity Completed and Score Submitted

You can change the score and press the button, which will submit the score. The page should then look like: Activity Done and Score Displayed

You have now submitted a score from the internal frame, based on your behavior. I hope you gave yourself a good score, because this will go on your permanent record! Just kidding. Actually, because the ParentPage.html page has no server associated with it, this score is not recorded. In a real life application, the LMS or other learning environment would log this score in their database for later display and analysis.

Clicking the button calls:

var onClickTheOnlyButton = function(){
    var score = calculateScore();
    SIMPLE_ACTIVITY_SERVICE.sendCompletedMessage(score);
    HEARTBEAT_SERVICE.stop();
};

var calculateScore = function(){
    return parseFloat(document.getElementById("MyScore").value);
};

Two services are called during this function (SIMPLE_ACTIVITY_SERVICE and HEARTBEAT_SERVICE). One sends a score and one shuts off the heartbeat for the activity, indicating that it is done. The SIMPLE_ACTIVITY_SERVICE is also responsible for sending the Loaded message.

Messages Sent by the Child Frame

The child frame is responsible for sending three types of messages, as noted earlier. These are each essential for working properly with the parent frame.

Sending a Loaded Message

First, the child page needs to send a Loaded message so that the parent knows that the child activity is working and that the student can start on it. This is done by:

window.onload = function(){
    SIMPLE_ACTIVITY_SERVICE.sendLoadedMessage();
};

Under the hood, this sends a simple message that looks like:

  var msg = SuperGLU.Messaging.Message(FRAME_NAME, LOADED_VERB, window.location.href, true);
  self.sendMessage(msg);

This message is in the form:

  • actor (str): FRAME_NAME is the name of the current frame or gateway (e.g., whatever you want this page to be referred to as).
  • verb (str): The LOADED_VERB is the text "Loaded"
  • object (str): The URL of the child page
  • result (bool): Returns true that the page has loaded. Alternatively, could return false if it failed to load.

In practice, this should only be called after your activity content (not just the page) is loaded. After all, if something could still fail to load and stop a user from trying your content, you want the parent page to be able to help the user go somewhere else. So try to put this at the end of the window.onload function and any parts of your page that need to load for it to work right.

Sending a Heartbeat Message

From the standpoint of a parent page letting many different activities try to run on it, there are important issues related to detecting if a page has frozen or is otherwise having problems. A heartbeat is a regular signal that is sent out by a service that lets other services know that it is still alive. Services to do this are provided and can be used as-is. The heartbeat starts after the window is fully loaded, by: HEARTBEAT_SERVICE.start();. Turning off the heartbeat is good practice to do if the page fails or if it is completed. While the parent page should respond to other signals first, it is a good fallback. HEARTBEAT_SERVICE.stop(); stops the heartbeat.

Submitting a Score

The last message type sends a score so that it can be recorded. This is done using a function on a simple custom service for the frame, the SimpleActivityService class. It has a function:

self.sendCompletedMessage = function sendCompletedMessage(score){
    var actor = getParameterByName('student'),
        itemId = window.location.protocol + window.location.pathname,
        context = {};
        context['classId'] = getParameterByName('class');
        context['groupId'] = getParameterByName('group');
        context['referrerId'] = getParameterByName('referrerId');
    var msg;
    msg = SuperGLU.Messaging.Message(actor, COMPLETED_VERB, itemId, 
                                     score, Messaging.INFORM_ACT, context);
    self.sendMessage(msg);

};

This function creates a message with the score (SuperGLU.Messaging.Message), and then sends it (self.sendMessage). Each message is in the form Message(actor, verb, object, result, speechAct, context). This is called the Completed message. All messages will be named after their verb, which is the second parameter. You will notice that the parameters for the Completed message are:

  • actor (str): The name or id for the student
  • verb (str): "Completed", because we are saying the student completed this activity
  • object (str): The activity that the student completed. In this case, it is some itemId, like a problem number.
  • result (float): The score for the activity. Typically this is chosen to range between 0 and 1 for convenience.
  • speechAct (str): "Inform", because we are informing anyone who reads this message about this event.
  • context (mapping): A mapping in the form {'key' : value} that contains extra information relevant to what occurred. In this example, we have a classId (e.g., a unique id for the students' classroom), a groupId (e.g., their school, perhaps), and a referrerId (e.g., a unique id for the system that hosts the parent page). Any extra information that might be useful can be stored in the context object.

The item identifier could be anything appropriate to link to that activity (i.e., not just an href). We have the extra context fields in a dictionary to include all sorts of other useful data. Other popular things to send along things like the browser type (JavaScript: navigator.UserAgent) or a session unique ID. Timestamps are already added automatically to every message, so you know when the message was created. It is the responsibility of destination services to record when they received those messages, if that is important.

In this example, we have taken much of the information from the URL parameters. These parameters are contained in the weblink, such as "MyLink?urlParam1=Value1&urlParam2=Value2" (where the URL parameters are urlParam1 and urlParam2, whose values are Value1 and Value2, respectively). This is an easy way for the parent page to pass some information to the child page, such as a student's name or the number of a problem that the child page should load. In this example, these parameters are either used as one of the main parts of the message (actor, verb, object, result) or as extra context data for the message. This is a good practice to follow, in case you want to look at this data in a log file data later on.

Setting up a Real-Life Child Activity

It is not hard to adapt this to other systems. A key part of using this in real life is to identify the specific needs for the page that will act as the parent. This includes:

  1. Libraries: Integrating HTML and JS from ChildWindow.html with your own page
  2. URL Parameters: What data they will pass to your page to use?
  3. Messages and Triggers: Are they looking for the three types of messages outlined here, or are there more? What events will cause the Loaded and Completed messages to be sent?
  4. Frame Name: What name is the parent page using to refer to your frame?

Libraries and Integration

In general, the reference implementation is meant to be fairly straightforward to drop in to an existing HTML page that you control. The following elements should be paid attention to.

  • JavaScript: You should be able to copy and paste the JavaScript imports and <script> declarations into any existing HTML page. Assuming that you are running your HTML page on a server, you will need to host the JavaScript files and update your paths to those files so that they load properly.
  • HTML: You can also safely delete or omit any of the existing HTML in the <body> tags.
  • onClickTheOnlyButton: You will want to remove the onClickTheOnlyButton function and replace it with your own function and associated event that calls this (e.g., when an activity completes)
  • calculateScore: This function will need to be replaced with a function that calculates an actual score for the learner, based on their performance on the activity. This should return a float value in [0,1], or null if no valid score could be calculated.

URL Parameters

You will need URL parameters that, at a minimum, give you a unique ID or other name for the student. This is required to fill in the actor for the Completed message. You will need to find the parameter name used for the student and use it to replace the "student" string in the following line:

 actor = getParameterByName('student')

You will also want to add any extra URL parameters that might be important to record as context to the message, such as the lines:

context['classId'] = getParameterByName('class');`.  

Likewise, you will want to delete any lines that store context taken using getParameterByName that will never be part of your child activity URL. Finally, if you are already using libraries that can retrieve URL parameters (e.g., jquery and others), you may wish to use those instead of the simple getParameterByName function provided.

Messages and Triggers

The three message types (Loaded, Heartbeat, and Completed) are noted above. However, the generic messaging format can send a variety of messages that other services may wish to transmit or consume. For those messages, you will need to consult system/service-specific documentation.

In either case, for your Child frame, you will need to hook in the correct times for the Loaded and Completed messages to be sent. Using the linked reference implementation, you can continue to send this at the end of the window.onload function, in many cases.

Frame Name

The Parent page needs to know what to call the child page (and vice versa). By default for this reference implementation, these are called MainPostingGateway for the Parent frame and "ActivityFrame" for the Child frame. While this is not absolutely necessary, depending on the Parent configuration, this should be considered as an important consideration to ensure that messages are processed properly by the parent.

The standard practice for this implementation is to assign frame names using a top-down approach, where the top parent frame assigns its own name using window.name (e.g., window.name = "Some Parent Name" and assigns a name to each child iframe that is opened inside of it (e.g., `<iframe name = "Child 1 Name" src="www.child1frame.com"/>). These names are then intended to be used to initialize the PostMessageGateway objects.

The PostMessageGateway on each page is the actual gateway where each service sends and receives its messages. The PostMessageGatewayStub on each page represents the gateway on the other page that it cannot directly contact. So then, the parent page knows about a PostMessageGatewayStub whose ID is "ActivityFrame". When a message arrives from a frame and the postMessage senderId is marked as "ActivityFrame", it then knows where that message came from. By naming the iframe "ActivityFrame" when it is created, the parent knows what to call the PostMessageGatewayStub instance that is a placeholder for that frame (because it named it). Likewise, the child frame knows to name its PostMessageGateway instance for that frame (because it can check its own frame name). This process is expected to occur only once, when the frames are created, after which the names are expected to remain the same. With that said, the names for PostMessageGateway objects do not need to be the same as their frame names, so long as the stubs and their actual counterparts have the same name. This is merely a convenience to make the system work with minimal configuration.

Testing the Child Frame

Assuming that your child frame implements the reference implementation that you are testing on, you should be able to modify the parent frame to point to your child frame. This can be done by altering the parent frame link to in ParentPage.html, by changing the line:

 childFrameElement.src = "ChildWindow.html?student=StudentId&class=ClassId&group=GroupId&referrerId=ReferrerGUID";

You can change the URL parameters and the child window link to any weblink. This can be done regardless of if the ParentPage.html is opened on a local machine or is hosted on a remote server. However, you will typically want your child page to be hosted on a real server so that it can be reached by whatever LMS wants to use it. For example, you might instead replace this with:

childFrameElement.src = "www.SomeExampleWebsite.com/ChildWindow.html?p1=123456&p2=234567&p3=34567" + 
                        "&p4=45678&p5=56789&p6=67890&p7=Fred";

If you were implementing your child page example on a server that used different URL parameters. Obviously, this assumes that you have modified the reference implementations to match these parameters, but those changes should be very small, relatively speaking.

Rationale for this Example

A common issue for learning management systems (LMS) and other learning environments is that it is hard to embed arbitrary web resources whose scores and behavior feeds back into the larger record of the learner's progress. The original solutions to this problem developed standards such as the ADL SCORM and IMS Global Common Cartridge formats, which essentially allowed an LMS to act as a "web player" for the object. Given the relative non-standardization browsers for JavaScript and HTML in the early 2000's, it could be argued that approach made sense at that time. Unfortunately, these approaches struggled significantly to accommodate adaptive or highly-dynamic content meaningfully (e.g., intelligent tutoring systems and simulations).

However, by leveraging HTML5 postMessage functionality, it is now possible to embed essentially any web-based resource into an LMS with relatively little modification to the HTML and typically no modification to server-side code for the learning resource. In this example, the Parent represents an LMS page that opens some arbitrary child activity in an iframe embedded into its main site. This Parent frame waits for certain messages from the child activity to determine if the learner is still working on the activity and their score when they are done.

This requires only one type of Message Gateway: an HTML5 postMessage gateway. The postMessage protocol allows sending JavaScript object messages between a browser frames that live on different web sites (e.g., cross-domain communication). In the past, such communication was not possible due to security concerns. However, this protocol allows accepting messages only from special sites (i.e., a whitelist) and also does not allow any website to directly view or run code on a different website page (i.e., messages are just data, no code).

⚠️ **GitHub.com Fallback** ⚠️