Saturday, May 4, 2013

A Necessary Evil/Good in Dart: IFrame Message Passing

‹prev | My Chain | next›

I converted a bunch of the method from the old JavaScript version of the into the ICE Code Editor into Dart. The similarities between the two languages make this fairly pleasant.

I did take time to convert a few JavaScript properties and getter/setter methods like getValue() to Dart. I was even able to move things like the creation of the preview element (holds the visualization of the editor code) out of the constructor and into the getter:
  Element get preview_el {
    if (_preview_el != null) return _preview_el;

    _preview_el = new DivElement();

    if (!this.edit_only) {
      this.el.children.add(_preview_el);
    }

    return _preview_el;
  }
I like this code because it is fairly easy to read and it encapsulates all of the create-unless-exists-and-return functionality that is needed for the preview element.

Best of all, I can use this getter as if it were a regular old property, not worrying about the underlying create-unless-exists-and-return functionality. As fast as the calling context is concerned, preview_el is just another property:
createPreviewIframe() {
    var iframe = new IFrameElement();
    // ...
    this.preview_el.children.add( iframe );

    return iframe;
  }
Actually, now that I think about it, that is not the “best of all” part about using Dart instead of JavaScript. What is really best of all is that the object constructor is now just this:
  Editor(this._el, {this.edit_only:false, this.autoupdate:true, this.title}) {
    this._startAce();
    this._applyStyles();
  }
The JavaScript version that does the same thing looked like:
function Editor(el, options) {
  this.el = el;

  if (typeof(options) != "object") options = {};
  this.edit_only = options.hasOwnProperty('edit_only') ?
    !!options.edit_only : false;
  this.autoupdate = options.hasOwnProperty('autoupdate') ?
    !!options.autoupdate : true;
  this.title = options.title;

  this.preview_el = options.preview_el || this.createPreviewElement();
  this.editor_el = this.createEditorElement();
  this.editor = this.initializeAce();
  this.applyStyles();
}
Plus there is no longer a need for createPreviewElement() or createEditorElement()—they are just part of the preview_el and editor_el getters.

It's not all sunshine and daisies in the land of Dart, however. The whole point of the ICE Code Editor is the visualization layer that demonstrates what the code written looks like. This is the kind of thing that is great for 3D Game Programming for Kids. Unfortunately, this is the kind of thing that will not work out-of-the-box in Dart.

The problem is that I need to write the contents of the editor to the document of an iframe. That is, if the content of the editor looks something like:
<body></body>
<script src="http://gamingJS.com/Three.js"></script>
<script src="http://gamingJS.com/ChromeFixes.js"></script>
<script>
  // Cool game code here...
</script>
Then this is what should be used as the content of the preview iframe's document. In JavaScript, this is done by writing to iframe.contentWindow.document. Dart, however, is a bit more restrictive in what can be done to iframes. Well, OK, a lot more restrictive. Messages can be sent, but no other communication is possible. And it is certainly not possible to write over the contents of an iframe—even one that I just created.

On the whole, I have to admit that this is probably a good thing for the future of the web. But in my immediate world, it pretty much sucks. Writing the web page content to the iframe's srcdoc (e.g. iframe.srcdoc = _ace.getValue()) works, but only in Chrome -- even when compiled with dart2js (shouldn't Dart fix HTML5 incompatibilities?).

My only recourse would seem to be to do what Dart wants me to do: communicate between frames with postMessage(). So, in the ICE Editor class, I change the createPreviewIframe() method to introduce a src setting:
  createPreviewIframe() {
    var iframe = new IFrameElement();
    // ...
    iframe.src = 'packages/ice_code_editor/html/preview_frame.html';
    this.preview_el.children.add( iframe );

    return iframe;
  }
I will get to the contents of preview_frame.html in a moment. First, I tell the updatePreview() method that, instead of overwriting the content of the preview iframe, it needs to postMessage() to the iframe:
  updatePreview() {
    if (this.edit_only) return;

    this.removePreview();
    var iframe = this.createPreviewIframe();

    var wait = new Duration(milliseconds: 900);
    new Timer(wait, (){
      iframe.contentWindow.postMessage(_ace.getValue(), window.location.href);
    });
  }
The timer delay allows the preview_frame.html time to load. The 900ms is way too high, but I needed something that was guaranteed to work regardless of what else was happening and did not want to fiddle with it. I will definitely need something more robust than this eventually.

Now, as for the content of preview_frame.html, it needs an event listener for messages. In fact, that is all that it needs. It needs to listen for messages and replace the document with the payload of the message being sent. This does the trick:
<body></body>
<script>
window.addEventListener("message", receiveMessage, false);

function receiveMessage(event) {

  //document.open();
  document.write(event.data);
  document.close();

  document.body.style.margin = '0';
}
</script>
I find that I have to comment out the document.open() in order to get Internet Explorer to work with the dart2js code. I have no idea why, but with it commented out, it works in Chrome, FireFox, and even Internet Explorer:



That is something that I never even tried in the JavaScript version of the ICE Code Editor. Mostly, it is not worth the effort because of IE's lack of WebGL. Too many of the examples in 3D Game Programming for Kids would simply not work without WebGL. That, and cross-browser support being a pain, made Internet Explorer far more trouble than it would have been worth.

But, thanks to Dart, I can get IE support for free. And kids whose parents insist on IE will actually be able to code a significant portion of the book. Wins all around—thanks to Dart.


Day #741

No comments:

Post a Comment