I have F# experience, but am new to Fable (and Javascript, npm, webpack, etc.). In my app, I'm trying to call an external Javascript library called cardsJS in order to display playing cards. However, this library requires JQuery, so I'm not sure how to call it from Fable/F#.
To be specific, one of the Javascript functions exposed by the library is:
// Gets the ID of the card, e.g. "KS" for the king of spades.
cid: function (card) {
var s = card.attr('src');
return s.substring(s.length - 6, s.length - 4);
},
In cardsJS, a card is just an HTML img element, so I tried to import the cid function in my Fable code via:
type ICardsJS =
abstract member cid : HTMLImageElement -> string
let iCardsJS : ICardsJS = importDefault "cardsJS/cards.js"
However, when I call iCardsJS.cid(card) this way, I get a Javascript runtime error:
Uncaught TypeError: card.attr is not a function
at Object.cid (cards.js:30:26)
The problem here is that the library apparently expects a JQuery object, rather than the raw DOM element, so it can call the object's attr function. So I guess my F# interface should actually look something like this instead:
type ICardsJS =
abstract member cid : JQueryThing??? -> string
If I was writing in raw Javascript, I could wrap the DOM element as a JQuery object, like this: $(card). How can I accomplish the same thing in Fable? I'm sure I need to import JQuery into my F# code somehow, but I don't know the right incantation. The only lead I've found so far is this issue, which suggests something like:
let jq : obj = importDefault "jquery"
let jqCard = jq $ card
But that doesn't compile in Fable 3:
error FSHARP: Value restriction. The value 'jqCard' has been inferred to have generic type
val jqCard : obj
I think the problem there is that jq has the wrong type (obj instead of some sort of IJQuery interface) and the compiler thinks that $ is from Fable.Core.JsInterop:
///<summary>
/// Destructure and apply a tuple to an arbitrary value.
/// E.g. `myFn $ (arg1, arg2)` in JS becomes `myFn(arg1, arg2)`
///</summary>
val ($) : callee: obj -> args: obj -> 'a
Related
I'm relatively new to JS and i have come across this scenario, where i need to identify if a method defined in a JS object follows a particular interface.
Let me explain with the help of an example:
let logger1 = {
log: function(message, meta){
//logging functionality
}
};
let logger2 = {
log: function(level, message, meta){
//logging functionality
}
}
There are 2 logger objects (logger1 or logger2), which has its own logging functionalities. They can get passed from the client to a library, that the client consumes. Library expects the log method to be of a particular interface, which is already mentioned in the library documentation. Inside the library, how would we verify if the logger object passed in has the 'log' method that matches the interface, the library is expecting ?
let Library = function(logger){
//Here I can verify if the 'log' function is defined. But how do i make sure the log
//function matches the interface that the library is expecting ?
//Let's say the interface that the library is expecting is function(level,message,meta),
//which is satisfied only by logger2.
if(logger && typeof(logger.log) === 'function'){
this.logger = logger;
}
else{
this.logger = console;
}
}
let lib1 = new Library(logger1);
let lib2 = new Library(logger2);
You cannot really. A Javascript function can omit parameters that it doesn't use, so the function may have fewer parameters and still be compatible. The function may define more parameters which will become undefined if not passed, and hence still be compatible. It may internally use arguments and hence not define any parameters, and still be compatible. It may name any of its parameters any way it wants, and still be compatible, so even inspecting the parameter names won't help you at all.
The only thing you can do it use something like Typescript and add compile-time checks.
Hmm, tricky one.
We would just determine the interface first and then the implementation. In this case we would code logger1 to have the signature: ( message, meta ) and logger2 ( message, meta, level ) .
That way we can call either function with eg. ( 'test', 'extra', 1 ) and logger1 will just ignore the level argument.
If you work with a system that has to juggle alot of different argumented versions, it might be better to just use an object as the only argument every time: function( config ) and just add extra properties when needed.
That way you can also force the interface to stay the same while also being extendable in some way.
There is a property length property for function which gives number of arguments it holds
Example:
let logger1 = {
log: function(message, meta){
//logging functionality
}
};
console.log(logger1.log.length); // 2
let logger2 = {
log: function(level, message, meta){
//logging functionality
}
}
console.log(logger2.log.length); // 3
Edit: Here is a minimal project that illustrates my issue. You can see the error described by serving it to the browser: pub get and then either pub serve (dartium) or pub build --mode=debug (other browsers).
How can I access an arbitrary JavaScript property from Dart through a JsObjectImpl? I am using the ace.js library with an interop to Dart that I've adapted from a typescript interface, and the method I am calling returns a plain javascript object with key-value pairs.
Dart gives me a JsObjectImpl, which cannot be casted to a Map or a JsObject, both of which have [] accessors. It confusingly seems to inherit from the deprecated JSObject (note the 's' is capitalized in the latter) which does not have the [] accessor, so I can't get the data out.
Some error messages:
When attempting a cast from JsObjectImpl to JsObject:
ORIGINAL EXCEPTION: type 'JSObjectImpl' is not a subtype of type 'JsObject' of 'obj' where
JSObjectImpl is from dart:js
JsObject is from dart:js. I get a similar message when using Map as well.
Looking at the object in the debugger, I can frustratingly see the property in JS view but not in the Dart object: The 4: Object is the data I want.
Ok, this was a fun one, happy holidays :)
It looks like Map is not a supported auto-conversion for package:js. So a couple of things:
Filed https://github.com/dart-lang/sdk/issues/28194
Sent your a PR introducing a workaround
For interested parties, we can use the browser-native Object.keys:
#JS()
library example;
import 'package:js/js.dart';
/// A workaround to converting an object from JS to a Dart Map.
Map jsToMap(jsObject) {
return new Map.fromIterable(
_getKeysOfObject(jsObject),
value: (key) => getProperty(jsObject, key),
);
}
// Both of these interfaces exist to call `Object.keys` from Dart.
//
// But you don't use them directly. Just see `jsToMap`.
#JS('Object.keys')
external List<String> _getKeysOfObject(jsObject);
And call it once we have an arbitrary JavaScript object:
var properties = jsToMap(toy.getData());
print(properties);
I had to modify #matanlurey solution so it works on dart 2.12 and is recursive.
import 'dart:js';
/// A workaround to deep-converting an object from JS to a Dart Object.
Object jsToDart(jsObject) {
if (jsObject is JsArray || jsObject is Iterable) {
return jsObject.map(jsToDart).toList();
}
if (jsObject is JsObject) {
return Map.fromIterable(
getObjectKeys(jsObject),
value: (key) => jsToDart(jsObject[key]),
);
}
return jsObject;
}
List<String> getObjectKeys(JsObject object) => context['Object']
.callMethod('getOwnPropertyNames', [object])
.toList()
.cast<String>();
So I'm building a node module for use with node-webkit that creates a new object and exports it. Standard fare. But since Node has no access to the nw-gui module of node-webkit, I'm just passing it in as a parameter to the constructor. Something like this:
function Example(gui) {
this.gui = gui; //Save for later
}
Example.prototype.createExampleMenu = function() {
return new this.gui.Menu();
}
exports.example = Example;
Works great. But I'm trying to modify .prototype methods of node-webkit's inner modules, like Menu and MenuItem. Is the only way to modify those methods (or add new ones) in the constructor itself? If I try to add new prototype methods outside, it (obviously) fails since this.gui hasn't been set. Basically, I'm trying to make it nicer to add new prototype methods to node-webkit modules without doing it in the constructor. Anyone?
I'm in no way an expert but from what I understand of the implementation of node-webkit from reading its source code, I doubt you can modify any of the objects defined in nw.gui.
If you look at the implementation of Node's standard require function in a running node-webkit instance, you'll find:
function (name) {
if (name == 'nw.gui')
return nwDispatcher.requireNwGui();
return global.require(name);
}
which means that requires of nw.gui are very special indeed.
Rather than requiring JavaScript code, this returns an internal binary object that only appears to be a required library.
Looking a little deeper, we find the nwDispatcher.nwGui.Menu is defined as:
function Menu(option) {
if (typeof option != 'object')
option = { type: 'contextmenu' };
if (option.type != 'contextmenu' && option.type != 'menubar')
throw new String('Invalid menu type: ' + option.type);
this.type = option.type;
v8_util.setHiddenValue(this, 'items', []);
nw.allocateObject(this, option);
}
which calls methods of the nw object, which is an object that is not available outside of this function, (i.e. the function acts as a closure over it.)
Further inspection of the various prototype methods of nw.gui.Menu shows that each call refers (internally) to this nw object to handle method dispatch to internally defined functions (written in C++).
So, rather than a group of standard JavaScript prototypical objects, the nw.gui module calls internal binary functions within the node-webkit runtime which are not exposed via its defined API.
UPDATE
From the node-webkit wiki:
Do not change UI types' prototype.
Store and retrieve Google Dart objects in JavaScript library containers
In a Dart application I am using an external JavaScript library to do various matrix calculations.
The specific functionality of the library is not important, what it's important is that I need to store and retrieve Dart object that I put in the matrix.
Dart Class - Lets image i have a dart object that which has a parameter called name
MyDartClass mydc = new MyDartClass(something, something);
mydc.name;
// Everything works as planned
Storing
matrix = js.context.matrix
matrix.cell(1,1).store("thing", new MyDartClass(something, something));
Retrieving
matrix.cell(1,1).has_object_of_type("thing");
// true
MyDartClass mydc = matrix.cell(1,1).retrieve("thing");
Do something with the object
mydc.name;
// Exception: The null object does not have a getter 'name'.
// NoSuchMethodError : method not found: 'name'
// Receiver: null
// Arguments: []
Does the library really work?
Yes it does. I have done the exact same thing in pure javascript many times and there are plenty of test to test the behaviour ( in Javascript )
Is Dart Broken?
When I try to use a javascriptified Hash to do the same behavoiur it works like a charm.
var options = js.map({ 'dart' : new MyDartclass(something, something));
var y = options["dart"];
js.context.console.log(y.name);
// Name is printed
What do you get out from the retrieve?
It seems that I get some kind of Dart Proxy
MyDartClass mydc = matrix.cell(1,1). retrieve("thing");
js.context.console.log(mydc);
DartProxy {id: "dart-ref-20", port: DartSendPortSync}
id: "dart-ref-20"
port: DartSendPortSync
__proto__: DartProxy
I belive that the lib stores the objects, deep down, in a hash map. But it seems like when I retrieve the object into the Dart I get something, but not in a way that I can work with it. So i need help since I don't know how to make it work.
Do I need to de-proxify the object?
Perhaps it IS a Dart bug when you try to retrieve objects from hashes inside objects
Perhaps I missunderstod everything that this is not suppose to work.
Passing and retrieving Dart objects inside the same scope is working. There's the following test case in the tests of js-interop to proove it :
test('retrieve same dart Object', () {
final date = new DateTime.now();
js.context.dartDate = date;
expect(js.context.dartDate, equals(date));
});
However there seems to be an issue with multiple scopes (and multiple event loops as well). There is no way to retain a dart object for now. So your dart object reference goes away at the end of scope. Here's a simple test case that fails :
test('retrieve same dart Object', () {
final date = new DateTime.now();
js.scoped(() {
js.context.dartDate = date;
});
js.scoped(() {
expect(js.context.dartDate, equals(date));
});
});
Please file an issue.
I'm building an AIR desktop application. At one point the application loads a popup window (an MXML component based on s:Window), which contains an mx:HTML component which loads a local (in the application directory) html file, blank.html. The relevant elements in blank.html are:
<script src="jw/jwplayer.js"/> <!--JW Player's JS-based Embedder-->
...
<div id="jwtarget" /> <!-- the target that the embedder will use -->
Since the parameters I want to use are determined at runtime, I use the domWindow property to invoke the method which loads the player. Here's an example that works:
private function injectPlayer():void {
var playerVars:Object = {};
playerVars.flashplayer = "jw/player.swf";
playerVars.file = "http://www.archive.org/download/meet_john_doe_ipod/meet_john_doe_512kb.mp4";
playerVars.height = 360;
playerVars.width = 640;
try { // attempt to invoke the js function
htmlComponent.domWindow.jwplayer("jwtarget").setup(playerVars);
} catch(e:Error) {}
}
which is called when the page finishes loading by:
<mx:HTML id="htmlComponent" location="assets/blank.html" complete="injectPlayer()" />
That all works fine.
Now to the question. I need to be able to pass a more complex playerVars Object to the function, but I don't seem to be getting the syntax correct. Here's the simplest example I've been attempting:
private function injectPlayer():void {
var playerVars:Object = {};
//playerVars.flashplayer = "jw/player.swf";
playerVars.file = "http://www.archive.org/download/meet_john_doe_ipod/meet_john_doe_512kb.mp4";
playerVars.height = 360;
playerVars.width = 640;
playerVars.modes = [{"type":"flash","src":"jw/player.swf"}];
try { // attempt to invoke the js function
htmlComponent.domWindow.jwplayer("jwtarget").setup(playerVars);
} catch(e:Error) {}
}
This code should create the exact same thing as the above code, but it fails to execute. I assume I need to change the syntax in some way to allow the array of Objects (modes) to be passed properly as a parameter to the js function.
I've tried various things, like passing the modes as a String, or putting the whole thing through JSON.stringify() first, but to no avail. Anyone know the correct way for constructing a complex object for a parameter?
Other details, if you haven't inferred them by now: Flex 4.5.1 is the SDK I'm building with, including the AIR 3.0 extensions (which means targeting FP11).
Update:
Another configuration I tried, which does work:
playerVars.modes = {"type":"flash", "src":"jw/player.swf"};
However, this still doesn't solve the problem that I should be able to pass an Array of Objects in the modes property. But at least this way loads the video player.
More Update:
So, I found this little section of code from jwplayer.js where I suspected the player loading was failing:
if (typeof parsedConfig.modes == "string") {
_modes = _playerDefaults();
_modes[0].src = parsedConfig.modes;
} else if (parsedConfig.modes instanceof Array) { // I suspect this was eval'd as false
_modes = parsedConfig.modes;
} else if (typeof parsedConfig.modes == "object" && parsedConfig.modes.type) {
_modes = [parsedConfig.modes];
}
And to test my suspicion I added the following function to my blank.html:
<script type="text/javascript">
var instanceOfArrayTest = function(arr) {
return arr instanceof Array;
}
</script>
And in my ActionScript code tried the following:
trace([1,2,3] is Array); // true
trace(htmlComponent.domWindow.instanceOfArrayTest([1,2,3])); // false!!!!
So, it seems that the problem is that ActionScript is not passing AS3 Array objects as JS Array objects!
Try doing this instead:
playerVars.modes = [{type:"flash",src:"jw/player.swf"}];
Unlike the call() method of the ExternalInterface class, the mx:HTML does not automatically convert AS3 classes to corresponding JS classes when they are passed as parameters to a JS function. Instead, the HTML Control maintains an environment where methods and properties native to the AS3 classes are preserved and made accessible to JS directly.
If a JS function requires a JS Array object, one must create the JS Array explicitly using the JavaScript Window object to access the JS Array constructor. The HTML Control provides access to this with it's domWindow property. Otherwise, there is no way to "cast" an AS3 Array to a JS Array.
Here's a basic example:
var JSArray:Function = htmlComponent.domWindow.Array;
htmlComponent.domWindow.instanceOfArrayTest( JSArray(1,2,3) ); // true
And for the more complex example using the config parameter for JW Player:
playerVars.modes = JSArray({"type":"flash","src":"jw/player.swf"},{"type":"html5"});
which creates a JS Array of two Objects.
For more info on the JavaScript environment in the HTML Control, check out the JavaScript in AIR section of Adobe's Developing AIR Applications with Flex.