Tuesday, May 7, 2013

Wrapping JavaScript Objects with Callbacks in Dart

‹prev | My Chain | next›

Encouraged by my progress with the Dart type wrapper from js-interop, I continue working with it today. The purpose of js-interop is to allow Dart developers to call JavaScript code from Dart. It also allows Dart developers to call Dart code from JavaScript, which is very handy with callbacks.

For simple JavaScript calls, the basic js-interop is often enough:
    // ...
    _ace = js.context.ace.edit(_editor_el);
    _ace.renderer.onResize();
    _ace.focus();
The above code creates a instance of the ACE code editor (from a JavaScript library) and calls two method on it. Thanks to Dart's similarity to JavaScript and the magic of js-interop, those method calls look identical to what they look like in JavaScript.

But sometimes, things get complicated:
    _ace.getSession().on('change',new js.Callback.many((e,a)=>this.resetUpdateTimer()));
At times like this, it would be nice to completely wrap the JavaScript object in a Dart wrapper. This is the point of the js_wrapping library in js-interop.

A JavaScript wrapper class in Dart extends the TypedProxy class from the js_wrapping library. As I found yesterday, there is a bit of convention that needs to be followed to setup a TypedProxy subclass. Once that is done, it is a simple matter of pointing Dart methods to the corresponding JavaScript method in the wrapped JavaScript object:
import 'package:js/js.dart' as js;
import 'package:js/js_wrapping.dart' as jsw;

class Ace extends jsw.TypedProxy {
  // proxy setup here...
  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();
}
Today, I need to add associated classes. I have the main Ace class working fairly well, but now I need a class to represent the AceSession, which is available from the session getter of the main Ace class. The getter can be defined as:
class Ace extends jsw.TypedProxy {
  // ...
  AceSession get session => AceSession.cast($unsafe.getSession());
}
This means that I need to define an AceSession class along with a cast() static method:
class AceSession extends jsw.TypedProxy {
  static AceSession cast(js.Proxy proxy) =>
    proxy == null ? null : new AceSession.fromProxy(proxy);
  AceSession.fromProxy(js.Proxy proxy) : super.fromProxy(proxy);
}
Thanks to some insights from Alexandre Ardhuin, the maintainer of js-interop, I know that the return value from any js-interop will be one of four things:
  • null
  • Dart primitives
  • DOM element
  • Proxy
The value returned from the call to getSession() in Ace is not a DOM element and will not be a Dart primitive either. It is either going to be a proxy for a JavaScript object (a session prototype) or null. This is why the cast() method handles both a null or returns a nicely encapsulated JavaScript object constructed as an AceSession.

With that, I can define some setters for the session:
class AceSession extends jsw.TypedProxy {
  static AceSession cast(js.Proxy proxy) =>
    proxy == null ? null : new AceSession.fromProxy(proxy);
  AceSession.fromProxy(js.Proxy proxy) : super.fromProxy(proxy);

  set mode(String m) => $unsafe.setMode(m);
  set useWrapMode(bool b) => $unsafe.setUseWrapMode(b);
  set useSoftTabs(bool b) => $unsafe.setUseSoftTabs(b);
  set tabSize(int size) => $unsafe.setTabSize(size);
}
And can use them like regular, beautiful Dart methods:
      _ace.session
        ..mode = "ace/mode/javascript"
        ..useWrapMode = true
        ..useSoftTabs = true
        ..tabSize = 2;
What I don't know about is callbacks. I make extensive use of the on-change callback in the ICE Code Editor, doing things like:
    _ace.session.on('change',new js.Callback.many((e,a)=>this.resetUpdateTimer()));
I believe that the Dart way of exposing this would be with an onChange stream that ought to look like:
    _ace.session.onChange.listen((e)=> this.resetUpdateTimer());
To support this in the AceSession class, I need an onChange getter method that returns a Stream. I think the quickest way to accomplish that is with a StreamController:
class AceSession extends jsw.TypedProxy {
  // ...
  StreamController _onChange;
  get onChange {
    if (_onChange != null) return _onChange.stream;

    _onChange = new StreamController();
    return _onChange.stream;
  }
}
Now, I need the JavaScript session to callback into my Dart code and, whenever that happens, I need to add the event to the stream controller:
class AceSession extends jsw.TypedProxy {
  // ...
  Stream _onChange;
  get onChange {
    if (_onChange != null) return _onChange.stream;

    _onChange = new StreamController();
    $unsafe.on('change', new js.Callback.many((e,a){
      _onChange.add(e);
    }));
    return _onChange.stream;
  }
}
The js.Callback is js-interop's way of allowing JavaScript to call Dart code—in this case it needs to call the Dart code via a JavaScript callback. Since many changes will take place, it needs to call this callback many times (not just once), hence the js.Callback.many() instead of js.Callback.once().

With that, I have all of my current Ace interaction completely wrapped inside of Dart objects. I can update the code many times and, thanks to my shiny new Dart onChange stream, I get to see the updates as they happen:



To be sure, the js-wrapper code that comprises the Ace and AceSession classes is not the prettiest Dart code in the world. But a little self-contained ugliness is more than that worth it to be able to interact with these JavaScript objects everywhere -- as if they were pure, awesome Dart.

I really like this stuff.

Day #744

No comments:

Post a Comment