Search code examples
javascriptjavagwtcytoscape.jsjsni

JSNI script fails, but script succeeds when ran in Chrome Devtools console


I am workin on utilizing an external javascript library called cytoscape.js through JSNI using test data. When I run the script how I want to through JSNI in my java class, it fails to produce the graph. However, when I run it through the Chrome Devtools console, it works correctly.

All of the values pass to the cytoscape.js library seemingly correctly. Currently, the JSNI code fails a test performed by the Javascript library that the console code is able to pass.

Here is the JSNI code I am using:

public static native void cytoscape() /*-{
        var cy = $wnd.cy = $wnd.cytoscape({container: $wnd.document.getElementById('cy'),
            elements: $wnd.glyElements, 
            style: [ { selector: 'node', style: { 'background-color': '#666', 'label': 'data(id)' } }, 
                { selector: 'edge', style: { 'width': 3, 'line-color': '#ccc', 'target-arrow-color': '#ccc', 'target-arrow-shape': 'triangle' } } ],
            layout: { name: 'grid', rows: 1 } });
    }-*/;

The "$wnd." is used to get the correct scope.

Here is the console code:

var cy = window.cy = cytoscape({ 
            container: document.getElementById('cy'),
            elements: glyElements, 
            style: [ { selector: 'node', style: { 'background-color': '#666', 'label': 'data(id)' } }, { selector: 'edge', style: { 'width': 3, 'line-color': '#ccc', 'target-arrow-color': '#ccc', 'target-arrow-shape': 'triangle' } } ], 
            layout: { name: 'grid', rows: 1 } 
        });

The elements I am using in both cases are stored in a .js file and I have made sure they are accessible in both cases. Like I mentioned, Devtools debug shows me the correct values are passed into the cytoscape.js library.

The JSNI code fails this return statement in the cytoscape.js library:

var plainObject = function plainObject(obj) {
    return obj != null && _typeof(obj) === typeofobj && !array(obj) && obj.constructor === Object;
  };

If I comment out "&& obj.constructor === Object;" on line 145 of cytoscape.js, the code runs correctly with my JSNI script. This leads me to believe the cytoscape object is being created differently through JSNI than the console. Still, Dev tools lists the console and JSNI method as providing identical objects. How can I update my JSNI code to make it compliant with this check?

Ideally, JSNI should be usable in this case. I am not sure why passing the values in through JSNI causes this if statement to fail when running it through console passes the if statement.


Solution

  • The problem is that Object isn't quite the same as Object, and checking instanceof almost certainly does a lot more than cytoscape's author thinks it does - isn't JS grand?

    The cytoscape.js project's instanceof Object test is not just a test for "is this thing a plain Object rather than an instance of some other type", but actually tests "is this thing an instance of the Object class in my window instance, and not from any other iframe/window".

    GWT's default linker evaluates your code in an iframe to prevent accidentally leaking global variables and confusing JS loaded into the same page, or letting other JS on the same page overwrite or otherwise mess with GWT's code.

    There are a few ways to fix this, aside from trying to correct cytoscape's code to something a bit less rigid and inflexible.

    • Create instances of Object from the outer page: To do this, you have to directly create objects, rather than using the {...} syntax, which defaults to whatever window you happen to be executing in. Something like this:
           public static native void cytoscape() /*-{
               var obj = new $wnd.Object();
               obj.container = $wnd.document.getElementById('cy');//can also be simply $doc.getElementById('cy')
               obj.elements = $wnd.glyElements;
               // Note that you may have to repeat this for each of these nested objects, depending
               // on how picky the library is being on otherwise identically structured code...
               obj.style = [ { selector: 'node', style: { 'background-color': '#666', 'label': 'data(id)' } }, 
                       { selector: 'edge', style: { 'width': 3, 'line-color': '#ccc', 'target-arrow-color': '#ccc',
       'target-arrow-shape': 'triangle' } } ];
               obj.layout = { name: 'grid', rows: 1 };
               var cy = $wnd.cy = $wnd.cytoscape(obj);
           }-*/; 
    
    • Load cytoscape.js into the same iframe as your GWT code executes in. Note that it might not actually work there, and this cause other issues, but you can try this using ScriptInjector to insert the script content into your GWT app instead of referencing it in your .html page directly.