Sunday, October 28, 2007

Flash & Javascript :How to communicate with Objects (or why JSON should be renamed ESON)

Flash allows to create rich web interface : very cool effects, 3D animations,data-driven websites. Actionscript,language based on ECMAScript, offers a lot of power to Flash applications.
Here, we will be using Actionscript 3 to see how we can communicate with Javascript objects thru the literal notation and DOM !

Creating a pseudo-div in Flash


I am sorry as I cannot put any flash animations here but first, we will see how to create a textField in actionScript 3. Even if it is very different from the so-called div html elements, I will call this textField a div. Here we go with a function that will create a basic textField:
function createDiv(y:Number,x:Number,height:Number,width:Number):TextField {
     var div:TextField=new TextField();
     div.y=y;
     div.x=x;
     div.height=height;
     div.width=width;
     div.background=true;
     div.backgroundColor='0xFF0000';
     addChild(div);
     return div;
}
As you can see this function takes several parameters and create a textField that we place on the scene thru the addCild function.
This is how we will use it :
var div=createDiv(50,50,50,50);
For those of you that knows javascript this must be quite familiar.
Basically we just create a square that is at the top 50 and the left 50 from the movie clip with a height and width of 50.
We also add a default color to see it!

Talking to Javascript from Flash


Communicating with javascript from Flash is very easy in actionscript 3!
You first need to import the class that contains the function that will do so:
import flash.external.ExternalInterface;
This will load the API necessary to communicate with external entities.
Importing this give us access to the following function:
ExternalInterface.addCallback(javascriptFunctionName, actionscriptFunctionName);
This addCallback method takes 2 parameters:
The name of the function that javascript will use to call flash
the name of the actionscript function that will be used.
Most of the time the 2 parameters will have the same name!
Let's see an example:
(just place the code in the action panel of an empty flash movie)
import flash.external.ExternalInterface;

ExternalInterface.addCallback("calledByJS", calledByJS);

function calledByJS(str:String):void {
 var div=createDiv(50,50,50,50);
 div.htmlText=str;
}
So when javascript will use the calledByJS function on a flash movie passing a string, it will call the actionscript function with the same name and put the text into the textField.
But let's see how we can call this function from javascript!

Calling Actionscript functions from Javascript


The first thing javascript needs to do is to use the function on the right movie clip!
You may have several clips on your page but perhaps only one of them will have the calledByJS function.
So we need to create a javascript function that will find the movie clip object in the page:
function getFlashMovie(objectId,embedId) {
  return (window[objectId]) ? window[objectId] 
              : document[embedId];
}
Unfortunately the code that you may find in the actionscript documentation will not work under safari mac. This version should work in IE,FireFox and Safari Windows/Mac.
You should pass the id of the object tag and the id of the embed tag.
Let's see the code to add a flash movie in an html page so that you may understand:

<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0" 
width="250px" height="250px" id="swf2">
<param name="movie" value="FlashWithESON.swf">
<embed src="FlashWithESON.swf" width="250px" height="250px" id="swf" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer">
</object>

The important thing to notice is the id set on the object tag and the id set on the embed tag.
you have swf and swf2 and that is what we pass in the function getFlashMovie:
var movie=getFlashMovie('swf2','swf');
And now, we can call the function calledByJS on the movie object!
movie.calledByJS('Hi from Javascript');
This code shall display the text into the textField that we have set thru actionscript in our flash movie!

Interacting with several functions with one function


We have seen that we need to specify the function that javascript will be able to call and that's fine if you only have one or two functions.
But this might be cumbersome to specify the function each time we need too:
import flash.external.ExternalInterface;

ExternalInterface.addCallback("calledByJS", calledByJS);

function calledByJS(str:String):void {
 var div=createDiv(50,50,50,50);
 div.htmlText=str;
}
ExternalInterface.addCallback("calledByJS2", calledByJS2);

function calledByJS2(str:String):void {
 var div=createDiv(50,50,50,50);
 div.htmlText=str;
}
ExternalInterface.addCallback("calledByJS3", calledByJS3);

function calledByJS3(str:String):void {
 var div=createDiv(50,50,50,50);
 div.htmlText=str;
}
You see that it is rather painfull to have to write this addCallback all the time.
Couldn't it be nice if we could build the function on the fly ? Creating one function that will be in charge of calling the final function ?
A function that will work like a factory where you put it in something raw (like a string!) and get something finished (like a function!)
This is what we are going to do:
import flash.external.ExternalInterface;

ExternalInterface.addCallback("apply", functionFactory);

function functionFactory(func:String,obj:String):void {
 this[func](obj);
}
Alright, here you see that the function that javascript will call is different from the one actionscript will really call.
Why ?
Well, if you put your self in a javascript programmer, this may be weird to write 'calledByJS' all the time!! After he/she is using javascript, no need to remember this every time!
We have seen that javascript will need to find the movie clip and call the function on the movie object so for the javascript programmer, this may make more sense to write this:
var movie=getFlashMovie('swf2','swf');
movie.apply('calledByJS','Hi from Javascript');
This could be 'call' or 'execute' or something like that!
On the other hand, this will not make that much sense on an actionscript programmer point of view!
That's why we gave an other name to the function that will describe what will be the end result:
ExternalInterface.addCallback("apply", functionFactory);

function functionFactory(func:String,obj:String):void {
        this[func](obj);
}
So the functionFactory says it all : this is a function that is in charge of building, creating,calling other function.
In actionscript, we cannot just 'eval' the string to turn it into a function so we call the function as a method of the this object (the scene) and then put the parameter into it!
We are done!
We have a factory that will launch any functions called by javascript without needing us to call ExternalInterface.addCallback every time!
But I know what you are thinking :
My function is not going to need only one parameter everytime so I will have to call the ExternalInterface anyway...
That's where objects comes in!

Passing arguments thru ESON

ESON is just a name that I give to JSON that stands for JavaScript Object Notation.
In fact, we can use this notation and send it to actionscript from javascript and we can send actionscript notation to send it to javascript too!
So this is not JSON but ESON, ECMAScript Object Notation.
So now, we will change the functionFactory so that it accepts an object, it is easy:
ExternalInterface.addCallback("apply", functionFactory);

function functionFactory(func:String,obj:Object):void {
 this[func](obj);
}
We have just change the type of obj from String to Object!
Let's say that you have build a nice actionscript API that gives hook to javascript to modify the look and feel of the application and the text too so you have several functions dedicated to communicating with javascript, one of them being:
function modifyDiv(obj:Object):void {
 
 var style=obj.style;
 div.y=parseInt(style['top']);
 div.x=parseInt(style['left']);
 div.width=parseInt(style['width');
 div.height=parseInt(style['height']);
 div.backgroundColor=style['backgroundColor'].replace(/#/,'0x');
 div.textColor=style['color'].replace(/#/,'0x');
 div.htmlText=obj['text'];
}
The modifyDiv will be called by javascript to change the appearance of the textField thru a javascript object!
Let's see the javascript side coding:
function getFlashMovie(objectId,embedId) {
  return (window[objectId]) ? window[objectId] 
              : document[embedId];
}

//wrapper that memoize the result
// i have put an alert so that you can see that
//next call to the function will return the second alert
function callFlash(func,obj) {
    alert('first call');
    var movie=getFlashMovie('swf','swf2');
    this.callFlash=function() { alert('other calls'); movie.apply(func,obj); };
    return movie.apply(func,obj); 
} 

function changeDiv() {
 var elm={style:{
                      top:'20px',
                      left:'60px',
                      width:'200px',
                      height:'80px',
                      'background-color':'#OOOOFF',
                      color:'#FF0000'
                 },
                 text:'hi from javascript! lets change your look !'
        };
   callFlash('modifyDiv',elm);
}
so as you can see we create a literal object in javascript that define the new style of the textField and the new text. Then we pass the object to the modifyDiv function define in actionscript!
Let's look back to the modifyDiv actionscript function:
function modifyDiv(obj:Object):void {
 
 var style=obj.style;
 div.y=parseInt(style['top']);
 div.x=parseInt(style['left']);
 div.width=parseInt(style['width');
 div.height=parseInt(style['height']);
 div.backgroundColor=style['backgroundColor'].replace(/#/,'0x');
 div.textColor=style['color'].replace(/#/,'0x');
 div.htmlText=obj['text'];
}
This is quite an ugly function I know but for our purpose, it will be fine! As you can see it accepts an object and then apply each element to the textField.
But why I didn't send an integer directly instead of sending the value with pixels?
Well, I guess that you have found that in fact I am simulating sending css properties from javascript to flash!
Here we have hardcoded the setting but perhaps we could create a function that will loop thru the javascript object and set the value automaticly!
So instead of creating a javascript object and putting the text into javascript, couldn't we do something else?

Passing DOM objects to Actionscript


Well, I have written this ugly actionscript function only for that purpose!
Let's see the javascript side code as the actionscript doesn't need any change:
function wrapper() {
   var elm=document.getElementById('text');
   callFlash('modifyDiv',{style:elm.style,text:elm.innerHTML});
}
As it is a very basic example, here is the xhtml that goes with it (the style is defined in the xhtml which is not a good habit but for now, forgive me!):
<div id="text" style="top:20px;left:60px;width:200px;height:80px;background-color:#OOOOFF;color:#FF0000">
<p>let's try to send some text to flash!<p>
</div>

As you can see, the javascript is fairly simple!
And now, we can manipulate the look and feel and contents of a Flash animation with properties that relate to xhtml and css!
You could build an application that will define some property of the look in a css file, the contents in a xhtml document and you will have a full SE friendly application!!
The css and xhtml will be used when javascript is off otherwise, the cool flash animation will do its wow job!

Putting the pieces together


First let's put the actionscript code that you can put on the action panel of an emply fla:
 //code to let javascript call actionscript functions

 import flash.external.ExternalInterface;

 ExternalInterface.addCallback("apply", functionFactory);

 function functionFactory(func:String,obj:String):void {
        this[func](obj);
 }

 //code related to our API(dirty example)

 var div=createDiv(50,50,50,50);
 function modifyDiv(obj:Object):void {
 
   var style=obj.style;
   div.y=parseInt(style['top']);
   div.x=parseInt(style['left']);
   div.width=parseInt(style['width']);
   div.height=parseInt(style['height']);
   div.backgroundColor=style['backgroundColor'].replace(/#/,'0x');
   div.textColor=style['color'].replace(/#/,'0x');
   div.htmlText=obj['text'];
 }
 function createDiv(y:Number,x:Number,height:Number,width:Number):TextField {
     var div:TextField=new TextField();
     div.y=y;
     div.x=x;
     div.height=height;
     div.width=width;
     div.background=true;
     div.backgroundColor='0xFF0000';
     addChild(div);
     return div;
 }

Now, let's see the javascript code:
//find the movie clip we need
function getFlashMovie(objectId,embedId) {
  return (window[objectId]) ? window[objectId] 
              : document[embedId];
}
//wrapp into one function : callFlash
function callFlash(func,obj) {
    var movie=getFlashMovie('swf','swf2');
    this.callFlash=function() { movie.apply(func,obj); };
    return movie.apply(func,obj); 
} 

//execute the modification to actionscript textField here:
function changeDiv() {
   var elm=document.getElementById('text');
   callFlash('modifyDiv',{style:elm.style,text:elm.innerHTML});
}


And in the end, the html you can find in the body:
<div id="text" style="top:20px;left:60px;width:200px;height:80px;background-color:#OOOOFF;color:#FF0000">
<p>let's try to send some text to flash!<p>
</div>
<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0" 
width="250px" height="250px" id="swf2">
<param name="movie" value="FlashWithESON.swf">
<embed src="FlashWithESON.swf" width="250px" height="250px" id="swf" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer">
</object>
In fact we should clean this code and load the flash animation thru swfObject in order to allow the switch between html and flash when necessary.

Conclusion


As you can see it is very easy to communicate with javascript thru ESON.
You can now create applications that gives hook to javascript with only one actionscript function, acting like a Factory, that will call the function and pass the object to it.
Using objects allow us to pass as many parameters as we want without having to redefine our main function, therefore allowing to ease maintainability of your code!
We have seen a cross-browser way to find a movie clip with javascript and how to call actionscript functions from their! There is no real standard about the name of the function javascript should use as this is pretty new in the ESON world (perhaps other bodies are doing this and if so they should give us their tips!) but it could be nice to specify a standard so that javascript could interact directly with Flash thru one function that will take the actionscript function name as a first parameter and a literal object as a second!
We didn't see how to call javascript from actionscript but you will see that there is a lot of good surprises awaiting us!
We might see in an other entry how to interact with css thru actionscript (not with the getStyle actionscript function...) so that you can offer your users to edit in real time the css thru tools like firebug and see the change in flash directly!
I hope you enjoy this article!

7 comments:

pixeline said...

thanks Shiriru for this article,
that was very insightful.
Will you write one about passing an object from flash to javascript?

for instance, i would like to know how to call a javascript object's method from within javascript.
ex:

(in js)
var myDiv.update=function(){
this.innerHTML="i'm a fool!";
}

(in AS)
Externalinterface.call("myDiv.update","you're not a fool!");


i tried that and it didn't work, i guess i am indeed a fool :)

shiriru said...

Thank you for your comment, pixeline!

>Will you write one about passing an object from flash to javascript?

Indeed!
In fact, we will see how we can manipulate the DOM from within Flash and it might answer to your question to some extends!

As for your problem,
it is not a JS<=>AS problem but rather a context problem.
You will meet exactly the same problem with the following javascript code:

function execute(f) {
var args=[];
for(var i=1;i< arguments.length;i++) {
args.push(arguments[i]);
}
return f(args);
}
window.onload=function() {

myDiv=document.getElementById('text');
myDiv.update=function(txt) {
this.innerHTML=txt;
}
//doesn't work
execute(myDiv.update,'Hi');

}

The above code is I guess what you want to do with AS as an intermediary.
But as you can see the above code is not going to work because when you pass the myDiv's method, you are loosing the context of the 'this' keyword.
Therefore, you need to bind the function to its initial context:


function execute(f,context) {
if(arguments.length==1) {
return f();
}
var args=[];
for(var i=2;i< arguments.length;i++) {
args.push(arguments[i]);
}
return f.apply(context,args);
}

window.onload=function() {

myDiv=document.getElementById('text');
myDiv.update=function(txt) {
this.innerHTML=txt;
}
execute(myDiv.update,myDiv,'Hi');

}

Here you can see that we specify the context in which the method should be called, we bind the method to it's original object.

Therefore, you have exactly the same problem when trying to send a javascript method from JS to AS,
you are just loosing the context.

But we will see how you can directly get all the good points of JS within your AS files with a one-liner!
this is a dirty solution but easy to implement to debug.

pixeline said...

cool, you've got talent as a teacher!

i look forward to reading you!


all the best,

pixeline

shiriru said...

Thank you for your kind comment!

Looking forward to reading your comments,questions, concerns!

A bientot!

paisible said...

excellent article, et comme pixeline a dit, tu as une tres bonne capacite a enseigner!

Merci.

Anonymous said...

I realize this article is a little old, but still valid for people who found their way here.

This is a great article, and spurred off great ideas for future reference. However, I found a minor bug that would prevent objects from being passed as arguments to your AS methods. The AS method "functionFactory" arg "obj" shouldn't be "String" but "Object".

So, it should read:
function functionFactory(funct:String, obj:Object) { this[funct](obj); }

Hope it helps.

Anonymous said...

At least I found a sample that works!
Thanks...