Monday, May 6, 2013

Getting Started with JavaScript Object Wrapping in Dart

‹prev | My Chain | next›

Up today, I would to experiment with code boundaries in Dart. At its heart, the ICE Code Editor, is responsible for communicating between two pieces: the actual editor and the preview layer that displays the contents of the code from the editor.

Even in the JavaScript version, this involved a fair amount of mixing editor and preview work as evidenced by the content setter:
Editor.prototype.setContent = function(data) {
  var that = this;
  if (!this.handle_change) {
    this.handle_change = function() {
      that.resetUpdateTimer();
    };
  }

  this.editor.getSession().removeListener('change', this.handle_change);
  this.editor.setValue(data, -1);
  this.editor.getSession().setUndoManager(new UndoManager());
  this.editor.getSession().on('change', this.handle_change);
  // Let the browser render elements before preview so that preview
  // size can be properly determined
  setTimeout(function(){that.updatePreview();},50);
};
Whenever the content of the editor changed (e.g. when a new project was opened), there was editor work like actually changing the content of the editor and starting a new undo-manager. But there was just as much work setting up the change handler to updated the preview layer as edits were made.

This barely works in the Dart version and already much of the underlying JavaScript implementation is bleeding through:
  set content(String data) {
    if (!_waitForAce.isCompleted) {
      editorReady.then((_) => this.content = data);
      return;
    }

    this._ace.setValue(data, -1);
    this._ace.focus();
    this.updatePreview();
    _ace.getSession().on('change',new js.Callback.many((e,a)=>this.updatePreview()));
  }
Alexandre Ardhuin, the primary maintainer of js-interop, had suggestion for a different approach. He suggested that I investigate the undocumented (except for a pull request) type-wrapper class. Instead of keeping the underlying JavaScript nature of an object front-and-center whenever and wherever it is used, the js-wrapper allows developers to wrap the entire JavaScript object inside a Dart wrapper.

I do not know if I will be able to push everything that I need from the ACE code editor JavaScript library into this wrapper tonight. But I would like to get started and hopefully develop a feel for it.

The ACE JavaScript library has a code entry point of ace.edit(). I am going to mimic that in a Dart js-wrapper as Ace.edit(). That will be a static (class) method in the Ace wrapper:
import 'package:js/js.dart' as js;
import 'package:js/js_wrapping.dart' as jsw;


class Ace extends jsw.TypedProxy {
  static Ace edit(dynamic el) => Ace.cast(js.context.ace.edit(el));
  // ...
}
There is a lot going on there. It is somewhat ugly, but hopefully the ugliness will be self-contained, allowing the code that uses this Ace class to be nothing but pretty Dart.

The "normal" js.dart package is imported with the usual js prefix. The new type-wrapper library is importer with the jsw prefix. In order for this wrapping to work, I have to make Ace a sub-class of TypedProxy from the js-wrapper library.

I then make my static edit() method return the JavaScript ace.edit() result—but cast into this typed proxy wrapper. The cast() method is not something from the js-wrapper library. Rather it seems to be an early convention from the library. I implement it as:
class Ace extends jsw.TypedProxy {
  static Ace edit(dynamic el) => Ace.cast(js.context['ace'].edit(el));

  static Ace cast(js.Proxy proxy) =>
    proxy == null ? null : new Ace.fromProxy(proxy);

  Ace.fromProxy(js.Proxy proxy) : super.fromProxy(proxy);
  // ...
}
The actual work of TypedProxy would appear to come from that fromProxy() class method.

Next up, I need to replace the old direct JavaScript method calls with wrapped Dart equivalents:
      _ace
        ..setTheme("ace/theme/chrome")
        ..setFontSize('18px')
        ..setPrintMarginColumn(false)
        ..setDisplayIndentGuides(false);

      _ace.getSession()
        ..setMode("ace/mode/javascript")
        ..setUseWrapMode(true)
        ..setUseSoftTabs(true)
        ..setTabSize(2);
The pure Dart version of this would look like:
      _ace
        ..theme = "ace/theme/chrome"
        ..fontSize ='18px'
        ..printMarginColumn = false 
        ..displayIndentGuides = false;

      _ace.session
        ..mode = "ace/mode/javascript"
        ..useWrapMode true 
        ..useSoftTabs = true
        ..tabSize = 2;
I get most of the way there by defining the various setters and getters in my Ace proxy class:
class Ace extends jsw.TypedProxy {
  static Ace edit(dynamic el) => Ace.cast(js.context['ace'].edit(el));

  static Ace cast(js.Proxy proxy) =>
    proxy == null ? null : new Ace.fromProxy(proxy);

  Ace.fromProxy(js.Proxy proxy) : super.fromProxy(proxy);

  set fontSize(String size) => $unsafe.setFontSize(size);
  set theme(String theme) => $unsafe.setTheme(theme);
  set printMarginColumn(bool b) => $unsafe.setPrintMarginColumn(b);
  set displayIndentGuides(bool b) => $unsafe.setDisplayIndentGuides(b);

  set value(String content) => $unsafe.setValue(content, -1);
  String get value => $unsafe.getValue();
  void focus() => $unsafe.focus();

  EditSession get session => $unsafe.getSession();
}
After the initial strangeness of the cast() and fromProxy(), the rest of this is pretty straight forward. Each setter or getter points to an unsafe (i.e. JavaScript) equivalent.

With that, I have a pure (as far as the calling context is concerned) Dart object that masks all of the ugliness of the underlying JavaScript. And, yes it works with no errors:



I still need to proxy the ACE session class (and a few others like the undo manager). But this seems very promising.


Day #743

No comments:

Post a Comment