JavaScript & Storyline Two-Way Communication via iFrame

Why would I need to know about IFrames?

In a previous post, we looked at how to use JavaScript to access or change Articulate Storyline variables from JavaScript. Here’s a practical example of that functionality. In my DevLearn session, we find ourselves in a story. This is Uncle Tomsday’s ship. Some of the items are stolen (grey), we need to locate them while busting or trusting myths around gamification.

c2inside cabin

Storyline variables are controlling the states of these objects. For example, we already found the globe, that’s why it is in color. But, we still need to locate the hat, treasure chest, chair, etc. Those objects are in “grey” state (black and white version of the original image). When in the game we stumble upon the hat, for example, JavaScript from the game changes a Storyline variable, which then triggers the hat to be in normal state showing up in colors.

What you need for this happen is a Storyline variable, let’s say “HatOn.” HatOn can be a Boolean or a number. I usually use numbers because it’s less problematic than dealing with true/false. So, HatOn is set to 0. That means we have 0 number of hats.

We also have a trigger that changes the hat’s state to normal when the HatOn variable changes, under the condition that HatOn is bigger than 0. (Now, if someone decides later that we need to collect 3 hats, we would just change the condition to at least 3. That’s one of the reasons to use numbers instead true/false.)

From JavaScript, to change a Storyline variable, you need only a couple of lines:

var player = GetPlayer();

player.SetVar("HatOn",1);

These two lines make sure that you set the HatOn variable to 1, which triggers the change and the hat’s state is now normal.

Enters the iFrame as a Webobject

Now, if your JavaScript (or HTML5) game is embedded as a Webobject within Storyline, it will be actually placed in an iFrame in the published output. (If you don’t know much about iFrames, it’s basically a new browser window within your browser window. But you often can’t really tell without the borders.)

In my DevLearn session, this is what it looks like below. You can see the same Storyline framework, but inside, the game is actually a webobject. The game is created in Construct 2 and published in HTML5. (A webobject could be anything else like a website, a plugin or whatever using JavaScript to communicate. EVEN ANOTHER STORYLINE OUTPUT!)

c2inside

c2insideThe problem is that picking up items (such as the hat) happens in the game itself, and not in Storyline. As I mentioned, the game (webobject) is in its own iFrame within Storyline. What that means is that the game’s frame does not “see” Storyline or its variables. It’s like a browser in browser that shares nothing with the parent.

Parenting iFrames

So, if you put above code in the game’s JavaScript to change the Storyline variable, you might not get the result you’re expecting:

var player = GetPlayer();

player.SetVar("HatOn",1);

Why? Because the Storyline object we’re trying to reach is not in the current iFrame (browser), it’s in the parent iFrame. To fix this, you need to reach Storyline, which is sitting in the “parent frame” of the game. This is done by adding a “parent” in front:

var player = parent.GetPlayer();

player.SetVar("HatOn",1);

parentchild

Here’s a raw example how this works:

In the parent Storyline course, we have a rudimentary Mr. Smiley and his hat. His hat is grey (state). Within the same screen, we have a Webobject embedded (child frame). That is actually another Storyline project with one single button in it. When I click that button (representing finding the hat), it executes a JavaScript trigger changing the parent Storyline’s variable: HatOn from 0 to 1.

2016-11-05_9-16-22

CLICK THE IMAGE TO VIEW DEMO

Child Storyline setup: when user clicks on Button 1, the below JavaScript code executes. “p” is a JavaScript variable that becomes the “parent” frame’s Player object. (Don’t worry if you’re not sure what GetPlayer() is. This is a Storyline function that you must use before accessing variables. GetPlayer() is not a generic JavaScript, only works in Storyline.)

Once we have the “p” variable set, we can set and get Storyline variables. In this case, p.SetVar(“HatOn”,1) means that we’re setting the Storyline variable “HatOn” (it has to exist in Storyline as a variable) to 1. Since “p” is referring to the “parent” frame (parent.GetPlayer();), we’re setting the parent’s Storyline variable with Mr. Smiley.

2016-11-05_9-23-48

THIS CHALLENGE IS FOR GEEKS ONLY: what if in the above situation you would use

var p = GetPlayer(); 

p.SetVar("HatOn",1);

instead of p = parent.GetPlayer();, omitting the parent from the code?

Answer

Omitting the “parent” from the code ( parent.GetPlayer(); ) would actually access the child frame’s Storyline project instead of the parent’s. The child does not have a variable called HatOn, so nothing would change. However, the GetPlayer() function exists, since this is a Storyline project. If the webobject was just an HTML5 Game, for example, you would get a JavaScript error because GetPlayer() does not exist.

In the parent, a trigger is watching the HatOn variable. When it changes to 1, the trigger changes the state of the hat to normal, the hat becomes colorful.

2016-11-05_9-16-36

Why am I not using all this for real?

And by “all this” I mean adding JavaScript code in Storyline’s Execute JavaScript trigger…

After all this talk about adding JavaScript in Storyline trigger, your question is valid. The reason I’m not adding JavaScript code within Storyline, because I’m waiting for Storyline 3 (HINT! HINT!). I do hope the new version brings some flexibility of using JavaScript.

For now: what I do is actually adding the JavaScript code AFTER I publish the Storyline project. I’m adding the code in the story_content/user.js file (or you can use your own js file and import it into the story.html). Why? Because you can test JavaScript runtime only anyway (no JS can be run in preview). Also, and this would lead into the woods of JavaScript: I’m using functions.

Into the Woods of JavaScript: Functions

This part is not for the faint-hearted. Remember the code from above:

var p = GetPlayer();

p.SetVar("HatOn",1);

When you put this code into Storyline’s JavaScript trigger, this is what happens when you publish it (story_content/user.js):

function ExecuteScript(strId)
{
 switch (strId)
 {
 case "6GWTamWCmcQ":
 Script1();
 break;
 }
}

function Script1()
{
 var p= GetPlayer();
p.SetVar("HatOn",1);
}

Very smart move from Storyline (not so smart for users like me). What Storyline does is it wraps my two lines into a function ( function Script1() ). A function has a name: Script1. The name can be anything (with some restrictions), and it’s used to identify the function later when you want to use it. A function always has this: { }. Between them, is the familiar JavaScript code. So, basically, a function is like “folder” or “storage” that keeps lines of code together inside.

And here are the advantages:

  1. If you need to run these two lines from 20 different places in your Storyline, you would have to type this in 20 times. Now, this is just two lines of code but imagine 20 lines of code copy-pasted lots of times… Then you find a bug. Now, you have to find all those 20 copy-pastes and change them 20 times… Nightmare.Instead, you keep the code as a function, and call the function name from 20 different places. The function is accessible from anywhere and if you need to change the code or debug it, there’s only one place to worry about.
  2. A function is accessible not only from anywhere in the browser, but also from child frames. That means if you put these two lines into a function and name it DoMyStuff(), you can call the function from the webobject like parent.DoMyStuff(); from anywhere, anytime. Testing will be much easier.
  3. A function can (frankly, should) return a value at the end. That means you could have a function that creates a random number and returns the result for you to work with. The one below returns a random number between 1 and 6.
    function createRandom() 
    { 
    return Math.floor(Math.random() * 6) + 1;
    }  
  4. And finally, the BIGGEST advantage: arguments. A function can receive arguments, variables if like. Why is it a big deal?Let’s say you like the random number generator that randomly creates a number between 1 and 6:
    Math.floor(Math.random() * 6) + 1  

    Fantastic! But. What if you also want random numbers between 1 and 10 or 10 and 100? Not using a function would mean coding each of this on its own…

    Instead, you create a function:

    function randomIntFromInterval(min,max)
    {
        return Math.floor(Math.random()*(max-min+1)+min);
    }

    This function is expecting two values (we call them min and max), and then use those values in the random generator. Now you have a generic function to create all kinds of random numbers. You can do randomIntFromInterval(1,10) or randomIntFromInterval(10,100). The function simply replaces the min and max with the numbers you’re sending as arguments (stuff in brackets).

And that’s what Storyline is doing with your code. But, here’s the issue! A function name must be unique. Otherwise, the system would not know which one to run. Storyline’s workaround is that EVERY TIME you publish, it re-creates unique identifiers for each of your JavaScript triggers and calls the ExecuteScript(strId) function to run them when needed. This strId is the unique ID that each code snippet gets. The problem with this is that it changes every time you publish. Only Storyline knows what the IDs are (6GWTamWCmcQ). That means I can’t write code to access them.

Therefore, I don’t put JavaScript code in Storyline’s Execute JavaScript triggers. Instead, I place the code in the story_content/user.js file (where they would end up anyway) AFTER each publishing. If the next version of Storyline fixes this issue, I would be Mr. Smiley with the blue hat.

Finally: two-way communication

So far we’ve been looking at how to communicate from the webobject (child frame) to the parent. But what if the parent wants to tell something to the child? Let’s say the button in the child frame should be visible only when the parent’s frame Mr. Smiley’s hat is grey. You would set up a variable inside the child frame project, IsGrey, with initally set to 1. Then you would add a trigger watching the IsGrey variable and in case it changes to 0, you would set the Button’s state to hidden. If it changes to 1, you would set the state to normal. So far, so good.

If only the child frame’s button can change Mr. Smiley’s hat, than you’re in a good shape. No communication is needed between the parent to the child. Basically, you would hide the button after it is clicked.

But what if Mr. Smiley’s had can turn back to grey if I click on Mr. Smiley in the parent Storyline via setting the HatOn variable to 0? How would the child know that the HatOn variable is now 0?

There are two ways to accomplish that. One, is that the child is constantly asking the parent about the variable. Imagine running a query every split of a second asking about the change. While this is technically possible (and often warranted), it’s using a lot of resources. Not to mention that we know, nothing happens until I click Mr. Smiley.

So, it would be logical to tell the child Storyline about the change WHEN it happens. You would think there’s a “child” like “parent” for frames you can use. Not really. Googling “accessing variables in child iFrame from parent iFrame” opens up a world of Matrix.

My suggestion is to try the following:

window.frames[0].frameElement.contentWindow.doMyStuff();

Assuming:

  1. There’s only one webobject (iframe) in the browser. Otherwise, the frames[0], is not 0.
  2. There’s a function in the child frame, called function doMyStuff(). So, let’s say we’re telling the change about HatOn in the parent and want to set the child’s IsGrey variable to 1.In the child’s /story_content/user.js file, we add this function to the end:
    function doMyStuff()
    
    {
    
      var p = GetPlayer();
    
    p.SetVar("IsGrey",1);
    
    }

    Again, this is in the child’s Storyline story_content/user.js file. The parent calling the doMyStuff() function tells the child to set its variable, IsGrey, to 1. This would trigger the button to show up again in the child Storyline project.

    If you want to be even more foolproof, you can test, whether the variable was correctly set:

    function doMyStuff()
    
    {
    
     var p = GetPlayer();
    
    var returnValue = 0;
    
    p.SetVar("IsGrey",1);
    
    returnValue= p.GetVar("IsGrey");
    
    return returnValue;
    
    }

    The code not only sets the IsGrey variable to 1 but reads it afterwards and returns its value. This way, the parent code can check if the value is 1. If not, something went wrong. You can then try to set it again. If it does not work in three trials, you handle the problem.

    Note that all variable declared ( var p and var returnValue) are accessible within the function itself but not outside. You’re not wasting memory, they only live within the function. For example, in another function, you can’t refer to returnValue. It gets created when the function is called, and destroyed when it ends.

    On the other hand, if you know that you always use the player, it might not make sense to put it every single function. Rather than create a global variable in the /story_content/user.js like:

    var gPlayer = GetPlayer();

    If you place this code directly in the user.js file (not within a function), you can use that anywhere:

    function doMyStuff()
    
    {
    
    var returnValue = 0;
    
    gPlayer.SetVar("IsGrey",1);
    
    returnValue= gPlayer.GetVar("IsGrey");
    
    return returnValue;
    
    }

And again, this might all change in the future. It all depends what we’re getting from Articulate very soon…

About the Author olahzsolt@hotmail.com

Leave a Comment: