Skip to content

Instantly share code, notes, and snippets.

@webdevwilson
Created July 22, 2012 19:38
Show Gist options
  • Save webdevwilson/3160834 to your computer and use it in GitHub Desktop.
Save webdevwilson/3160834 to your computer and use it in GitHub Desktop.
The js.dart library provides simple synchronous JS invocation from Dart in a browser setting (Dartium or via dart2js).
// Copyright 2012, the Dart project authors. All rights reserved.
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials provided
// with the distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// The js.dart library provides simple synchronous JS invocation from
// Dart in a browser setting (Dartium or via dart2js).
//
// Methods
// (1) js.invoke invokes a JS method (specified by string) on a
// list of serializable parameters in the DOM window's JS context and
// returns a deserialized result. E.g.:
//
// #import('js.dart', prefix: 'js');
// #import('dart:html');
// void main() {
// final div = new DivElement();
// div.innerHTML = js.invoke('decodeURI', ['%C3%A0%20la%20carte']);
// document.body.nodes.add(div);
// }
//
// (2) js.eval evaluates an arbitrary string in the DOM window's JS context
// and returns a deserialized result. E.g.:
//
// #import('js.dart', prefix: 'js');
// main() {
// js.eval('window.alert("Hello World")');
// }
//
// Allowable Data Types
// Parameters and return values be valid JSON types: String,
// num (int/double), bool, List (of JSON), or Map (of JSON).
//
// To ease marshalling of user types, we provide additional convenience
// mechanisms. First, we provide a js.callback method to allow wrapped
// callback functions to be used as parameters. E.g.,
//
// js.invoke('function (f) { setTimeout(f, 0) }', js.callback(f));
//
//
// Second, we provide an abstract Serializable base class is provided
// that converts the user type into JSON on demand. E.g.,
//
// class LatLng extends Serializable {
// final num _lat;
// final num _lng;
// LatLng(this._lat, this._lng);
//
// serialize() => encode(
// 'function (lat, lng) { return new LatLng(lat, lng); }',
// [_lat, _lng]);
// }
//
// The user must define the serialize method to encode the user type.
//
// Implementation Overview
//
// The APIs above are implemented via the DOM's synchronous dispatchEvent
// mechanism. All calls to JS are converted to
// 1. Serialize parameters to a JSON string.
// 2. Encode function string and parameters as a DOM Event and
// dispatch to JS.
// 3. In JS, handle DOM Event:
// (a) Deserialize the JSON string into JS objects.
// (b) Evaluate the function string into a JS function.
// (c) Invoke the JS function.
// (d) Send the result back to Dart via a dispatchEvent in the
// reverse direction. Note, although Dart is blocked on stack,
// it will still handle this Event synchronously.
// 4. Deserialize the returned result (if any) to Dart and return it.
//
// Caveats
// - When compiling to JS, invoked JS code may conflict with generated JS.
// - It is slow. It channels through the DOM.
// - It is limited to JSON serializable data for parameters and return values.
// - It does not support easy invocation from JS to Dart (only Dart to JS).
#library('js');
#import('dart:html');
#import('dart:json');
// Tags in JSON sent to JS.
final String _DESERIALIZER = '_js_dart_deserialize_function';
final String _ARGUMENTS = '_js_dart_deserialize_arguments';
final String _CALLBACK = '_js_dart_callback';
// Tags in JSON returned from JS.
final String _TYPEID = '_js_dart_type';
// JS support code.
final String _jscode = '''
(function () {
function createEvent(name, data) {
var event = document.createEvent('TextEvent');
event.initTextEvent(name, false, false, window, data);
return event;
}
function dispatch(name, args) {
try {
var args = Array.prototype.slice.call(args);
return eval('('+name+')').apply(this, args);
} catch(e) {
console.log('Dispatch Error: ' + e);
}
}
function callback(id) {
var f = function() {
var data = JSON.stringify({'id': id, 'arguments': serialize(arguments)});
window.dispatchEvent(createEvent('js-dart-callback', data));
}
return f;
}
function deserialize(obj) {
if (obj && typeof(obj) == 'object') {
if ('$_DESERIALIZER' in obj) {
var f = obj['$_DESERIALIZER'];
var args = deserialize(obj['$_ARGUMENTS']);
return dispatch(f, args);
} else if ('$_CALLBACK' in obj) {
var id = obj['$_CALLBACK'];
return callback(id);
}
for (var key in obj) {
obj[key] = deserialize(obj[key]);
}
}
return obj;
}
var serializedTypes = {};
var serializedNames = {};
window.serializedNames = serializedNames;
function name(obj) {
// TODO(vsm): Make this portable.
return obj.constructor.name;
}
function serialize(obj) {
if (typeof(obj) == 'object') {
var n = name(obj);
if (n in serializedTypes) {
var map = serializedTypes[n];
var result = { '$_TYPEID': serializedNames[n] };
for (var key in map) {
var value = dispatch(map[key], [obj]);
result[key] = serialize(value);
}
return result;
}
for (var key in obj) {
obj[key] = serialize(obj[key]);
}
}
return obj;
}
function handleInvoke(e) {
var data = deserialize(JSON.parse(e.data));
try {
var name = data.method;
var args = data.arguments;
var ret = dispatch(name, args);
var result = serialize(JSON.stringify({ 'return': ret }));
window.dispatchEvent(createEvent('js-dart-result', result));
} catch (e) {
window.console.error('js.dart: ' + e);
var error = JSON.stringify({ 'error': e });
window.dispatchEvent(createEvent('js-dart-result', error));
}
}
function registerType(e) {
var data = JSON.parse(e.data);
var id = eval(data.type).prototype.constructor.name;
var map = data.fields;
serializedTypes[id] = map;
serializedNames[id] = data.type;
}
window.addEventListener('js-dart-invoke', handleInvoke, false);
window.addEventListener('js-dart-register', registerType, false);
})();
''';
Event _createEvent(String name, final data) {
// TODO(vsm): Use TextEvent constructor when that's available.
TextEvent event = document.$dom_createEvent('TextEvent');
event.initTextEvent(name, false, false, window, data);
return event;
}
Event _createMethodEvent(String methodName, List arguments) {
final data = { 'method': methodName, 'arguments': arguments };
return _createEvent('js-dart-invoke', JSON.stringify(data));
}
Event _createRegistrationEvent(String typeName, Map<String,String> fields) {
final data = { 'type': typeName, 'fields': fields };
return _createEvent('js-dart-register', JSON.stringify(data));
}
var _result;
void _resultHandler(TextEvent e) {
_result = _construct(JSON.parse(e.data));
}
/**
* Invoke the given function in JavaScript in the same window with the
* given arguments, and return the result. All arguments (and the
* result) must be valid JSON types or extend the Serializable class
* below.
*/
invoke(String methodName, List arguments) {
_initialize();
if (arguments is! List) {
throw new Exception('Invalid arguments: $arguments');
}
final methodEvent = _createMethodEvent(methodName, arguments);
_result = null;
window.$dom_dispatchEvent(methodEvent);
if (null == _result || _result is! Map) {
throw new Exception('Invalid result invoking: $methodName($arguments): $_result.');
} else if (_result.containsKey('error')) {
throw new Exception(_result['error']);
}
return _result['return'];
}
/**
* Evaluate the given expression and return the result.
*/
eval(String command) {
return invoke('eval', [command]);
}
// TODO(vsm): Fix this memory leak. Alternatively, use a better
// abstraction (e.g., futures).
Map<int, Function> _callbacks;
int _callbackCounter = 0;
/**
* Return a JSON serializable version of f that can be invoked from JS
* as a callback.
*/
Map callback(Function f) {
int id = _callbackCounter++;
_callbacks[id] = f;
var result = {};
result[_CALLBACK] = id;
return result;
}
_callbackHandler(TextEvent e) {
var data = _construct(JSON.parse(e.data));
int id = data['id'];
var arguments = data['arguments'];
Function f = _callbacks[id];
switch (arguments.length) {
case 0: return f();
case 1: return f(arguments['0']);
case 2: return f(arguments['0'], arguments['1']);
case 3: return f(arguments['0'], arguments['1'], arguments['2']);
default: throw new Exception('Unsupported number of arguments');
}
}
bool _initialized = false;
void _initialize() {
if (_initialized) {
return;
}
_initialized = true;
_callbacks = new Map<int, Function>();
_constructors = new Map<String, Function>();
window.on['js-dart-result'].add(_resultHandler, false);
window.on['js-dart-callback'].add(_callbackHandler, false);
final script = new ScriptElement();
script.type = 'text/javascript';
script.innerHTML = _jscode;
document.body.nodes.add(script);
}
Map<String, Function> _constructors;
_construct(var obj) {
if (obj is Map) {
obj.forEach((key, value) {
obj[key] = _construct(value);
});
if (obj.containsKey(_TYPEID)) {
String id = obj[_TYPEID];
Function constructor = _constructors[id];
return constructor(obj);
}
} else if (obj is List) {
for (int i = 0; i < obj.length; ++i) {
obj[i] = _construct(obj[i]);
}
}
return obj;
}
/**
* Register a JS type via its JavaScript [name] as serializable
* when passed to Dart.
*
* [fields] declares how the JS object is converted to a map
* representation. Each key is a field name, and the value is the
* text of a JS function that takes a JS object and returns this
* field's value. (I.e. value is a projection function for that
* field.)
*
* [constructor] says how to go from a map representation to a Dart object. It
* takes a map arg, expects to see certain fields, and uses their values to
* construct an object.
*/
void register(String name, Map<String, String> fields,
Function constructor) {
_initialize();
final registerEvent = _createRegistrationEvent(name, fields);
window.$dom_dispatchEvent(registerEvent);
_constructors[name] = constructor;
}
// TODO(vsm): Eliminate once we have a Map mixin.
class _SerializableMap implements Map<String, Object> {
// Just throw. Everything required to keep the JSON library happy is implemented in Serializable below.
_fail() { throw new NotImplementedException(); }
operator [](key) => _fail();
operator []=(key, value) => _fail();
clear() => _fail();
containsKey(key) => _fail();
containsValue(value) => _fail();
forEach(f) => _fail();
getKeys() => _fail();
getValues() => _fail();
isEmpty() => _fail();
get length() => _fail();
putIfAbsent(key, ifAbsent) => _fail();
remove(key) => _fail();
}
/**
* An abstract base type for Dart types that automatically serialize
* to JavaScript via JSON.
*/
class Serializable extends _SerializableMap {
void forEach(void f(String key, Object value)) {
// Forward forEach to operate on the serialized copy instead.
serialize().forEach(f);
}
/**
* A helper for subclasses to encode as JSON.
*
* [jsDeserializer] defines (as a String) a JS function that,
* given [arguments], will create a JS object.
*/
Map<String, Object> encode(String jsDeserializer, List arguments) {
var result = {};
result[_DESERIALIZER] = jsDeserializer;
result[_ARGUMENTS] = arguments;
return result;
}
/**
* Create a legal JSON Map from this object.
*/
abstract Map<String, Object> serialize();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment