Tuesday, September 10, 2013

The Power of JavaScript (in Dart)


I was able to generate JavaScript code from Dart last night, but was not quite able to use it to register event handlers. Tonight, I hope to do just that, so that I can finally generate and test keyboard events with data in Dart.

This is all because, at present, there is no way to add character data to custom built keyboard events in Dart. Actually, it turns out that there is no way to do so in JavaScript (Chrome/WebKit) either, but there are ways around this thanks to JavaScript's weak typing. Dart may not be statically typed, but it is strongly typed. The end result being that there is no way to trick it into seeing a vanilla event as a keyboard event as is possible in JavaScript.

The code that I am dynamically creating to listen for events in JavaScript is as follows:
  static void createJsListener() {
    js.context.eval('''
function _dart_key_event_x(target_classname, cb) {
  //var target = document.getElementsByClassName(target_classname)[0];
  target = document;

  target.addEventListener("keydown", function(event) {
    console.log("js event " + event);
    cb(event);
  });
}''');
  }
I am using the js-interop top-level context property to access JavaScript's eval(). This is admittedly pretty awful, but I cannot dynamically create functions with JavaScript's function keyword. So I place my dynamic JavaScript code inside a string, which js-interop is quite good at proxying into JavaScript. Actually, I will likely have to make that more dynamic in the future so that I can operate on HTML elements besides document, but I keep it simple here.

Once the _dart_key_event_x() JavaScript function is eval'd into existence, I can access it like any other top-level JavaScript function or property via Dart js-interop's context property:
js.context._dart_key_event_x(temp_classname, js.context.dartCallback);
Here, I am calling it with a temporary CSS class name that I will use to uniquely identify the HTML element to the JavaScript code. Currently that does nothing in the _dart_key_event_x() JavaScript function since I am only listening for events on document. I will need to supply a unique identifier because js-interop cannot pass object data back and forth—just primitives (that's not 100% true, but close enough for my purposes here). Anyhow, that first parameter is not even used yet as I am just listening on document.

The second parameter that is suppltied to _dart_key_event_x() is a callback. What is interesting here is that I am supplying a Dart callback for JavaScript to call. To make that work, I need to use the Callback class in js-interop:
    var dartCallback = new js.Callback.many((event) {
     //
    });
At the risk of doing the usual computer programming book thing, I make a big leap and put that together such that the callback puts a decorated version of the event onto a stream:
  static Stream<KeyEventX> onKeyDown(EventTarget target) {
    var _controller = new StreamController.broadcast();

    var temp_classname = "dart_id-${new Random().nextInt(1000)}";

    createJsListener();
    var dartCallback = new js.Callback.many((event) {
      _controller.add(new KeyEventX(event));
    });

    js.context._dart_key_event_x(temp_classname, dartCallback);

    return _controller.stream;
  }
And that almost works, but I keep getting js-interop proxy errors when I try to retain a reference to that JavaScript event inside KeyEventX (the class decorating the JavaScript event):
Caught Proxy js-ref-10 has been invalidated.
  package:js/js.dart 1037:22
  package:js/js.dart 1027:20
Proxy.noSuchMethod
  package:ctrl_alt_foo/key_event_x.dart 98:38
KeyEventX.keyCode
  ../test.dart 27:34
main.<fn>.<fn>
  dart:async 
I do manage to get this working by extracting the values that I need from the event directly in the KeyEventX constructor:
class KeyEventX implements KeyEvent {
  int keyCode;

  KeyEventX(KeyboardEvent parent) {
    keyCode = parent.keyCode;
  }

  bool isKey(String char) {
    return KeyIdentifier.keyCodeFor(char) == keyCode;
  }
  // ...
}
With that, I actually have one of my keyboard tests passing:
  test("can listen for key events", (){
    KeyboardEventStreamX.onKeyDown(document).listen(expectAsync1((e) {
      expect(e.isKey('A'), true);
    }));

    type('A');
  });
I can create and dispatch a keyboard event with character data attached and subscribe to an event that sees that same data come back through. All thanks to the power of JavaScript. In Dart.


Day #870

No comments:

Post a Comment