Digital Deception Digital Deception - 3 months ago 23
Dart Question

Dart JS Library, how to pass callback functions

We are trying to wrap the D3 (v4) line generator class with a Dart wrapper with https://pub.dartlang.org/packages/js. We've followed https://github.com/google/chartjs.dart/ but are having problems passing through functions.

Our wrapper looks like this:

@JS('d3')
library d3;

import 'dart:js';
import "package:js/js.dart";


@JS('line')
class Line {
external Line();
external String call (List<List<num>> data);
external Line x(Function func);
external Line y(Function func);
}


We've currently managed to get it working using:

Line line = new Line();
String path = line([[10, 20], [200, 250]]);


However we would like to set a function to access the x and y values.
We've tried using:

Line line = new Line();
line.x(allowInterop((d) { return d[0]+10;}));
line([[10, 20], [200, 250]]);


This produces a stack trace:

JSFunction._apply (dart:js:1300)
#1 JSFunction.call (dart:js:1290)
#2 CardinalLine.path (package:dials/app_component.dart:42:16)
#3 _View_AppComponent2.detectChangesInternal (package:dials/app_component.template.dart:189:49)
#4 AppView.detectChanges (package:angular2/src/core/linker/view.dart:247:10)
#5 DebugAppView.detectChanges (package:angular2/src/core/linker/view.dart:367:26)
#6 AppView.detectContentChildrenChanges (package:angular2/src/core/linker/view.dart:264:31)
#7 _View_AppComponent0.detectChangesInternal (package:dials/app_component.template.dart:118:10)
#8 AppView.detectChanges (package:angular2/src/core/linker/view.dart:247:10)
#9 DebugAppView.detectChanges (package:angular2/src/core/linker/view.dart:367:26)
#10 AppView.detectViewChildrenChanges (package:angular2/src/core/linker/view.dart:270:28)
#11 AppView.detectChangesInternal (package:angular2/src/core/linker/view.dart:259:10)
#12 AppView.detectChanges (package:angular2/src/core/linker/view.dart:247:10)
#13 DebugAppView.detectChanges (package:angular2/src/core/linker/view.dart:367:26)
#14 ViewRef_.detectChanges (package:angular2/src/core/linker/view_ref.dart:123:16)
#15 ApplicationRef_.tick.<anonymous closure> (package:angular2/src/core/application_ref.dart:443:63)
#16 List.forEach (dart:core-patch/growable_array.dart:254)
#17 ApplicationRef_.tick (package:angular2/src/core/application_ref.dart:443:32)
#18 ApplicationRef_._loadComponent (package:angular2/src/core/application_ref.dart:414:10)
#19 ApplicationRef_.bootstrap.<anonymous closure> (package:angular2/src/core/application_ref.dart:401:12)
#20 ApplicationRef_.run.<anonymous closure> (package:angular2/src/core/application_ref.dart:366:26)


This appears to occur while it is trying to pass the arguments to something:

ORIGINAL EXCEPTION: V8 Exception(anonymous function) @ VM3533:1
VM3533:1 ORIGINAL STACKTRACE:(anonymous function) @ VM3533:1


Where VM3533 is:

(function() {
var func = this;
return function() {
return func(Array.prototype.slice.apply(arguments));
}
;
}
)


The arguments passed are:

[[MutationRecord], MutationObserver]


Where the Mutation Record consists of:

0: MutationRecord
addedNodes: NodeList[0]
attributeName: "hidden"
attributeNamespace: null
nextSibling: null
oldValue: null
previousSibling: null
removedNodes: NodeList[0]
target: div
type: "attributes"
__proto__: MutationRecord
length: 1
__proto__: Array[0]


We've tried lots of variations, including following the chartjs example, but they all end with the same stack-trace. What is the right way to do this?

Answer

So it turns out that the function was being properly attached and called, the problem lay in the fact that I had defined the x method to be called with only one variable d.

This is how most D3 accessor functions are defined in JavaScript, you only care about the piece of data currently being modified. However D3 always passes three variables, the piece of data d, the index i, and the entire dataset data. JavaScript doesn't care that the function doesn't take the extra arguments, it just calls it with the one argument. However Dart does care, and will throw a wobbly that the function can't be applied due to an overload of arguments. This was the exception happening in VM3533.

The exception thrown is annoyingly vague, and hard to see without debugging the JavaScript.

So the correct solution is to ensure when you define a function that will be called from JavaScript that it has the exact amount of arguments that it will be called with in this context.

In this case:

Line line = new Line();
line.x(
    allowInterop(
        (List<num> d, num i, List<List<num>> data) {
            return d[0]+10;
        }
    )
);
return line([[10, 20], [30, 40]]);
Comments