Search code examples
gwtgwt-jsinterop

Javascript module function in GWT with JsInterop


Hoping this is way easier than I'm making it - I'm a Java coder, some inner Javascript aspects are a tad unfamiliar to me.

Trying to embed the great CodeJar library inside a GWT panel. There's a pretty nice/simple example for CodeJar:

<script type="module">
  import {CodeJar} from './codejar.js'
  import {withLineNumbers} from './linenumbers.js';

  const editor = document.querySelector('.editor')

  const highlight = editor => {
    // highlight.js does not trim old tags,
    // let's do it by this hack.
    editor.textContent = editor.textContent
    hljs.highlightBlock(editor)
  }

  const jar = CodeJar(editor, withLineNumbers(highlight), {
    indentOn: /[(\[{]$/
  })

  jar.updateCode(localStorage.getItem('code'))
  jar.onUpdate(code => {
    localStorage.setItem('code', code)
  })
</script>

The module function itself looks like this:

export function CodeJar(editor, highlight, opt = {}) { ... }

'editor' is a Div reference, and 'highlight' is a callback library function for handling code highlighting.

What I'm battling with is the JsInterop markup and code to make Javascript modules work with GWT. The above has a few aspects which I'm battling with

  • replacing the "import" such that the javascript module code is available to GWT. Obvioulsy I can just import the js in my top level index.html, but as I understand it JS modules don't become part of the global namespace, they're only usable from the JS module that imports them. Which in my case, presumably needs to be the GWT code.
  • how to pass the callback function in when recoding the above in GWT
  • how to get my own 'jar' reference to do own text set/get (replacing the use of local storage)

Solution

  • To load the script and have it available for GWT consumption, you have (at least) 3 possibilities:

    • use a static import in a <script type=module>, and then assign the CodeJar function to a window property to make it available globally (that could be another global object than window actually)
    • use a dynamic import() from GWT, using JsInterop and possibly elemental2-promise
    • use Rollup/Webpack/whatever to turn the CodeJar module into a non-module script so you can use it differently

    Next, you need to create JsInterop bindings so you can call it from GWT; something like that (assuming you made CodeJar available globally as window.CodeJar, and using elemental2-dom for HTMLElement, but com.google.gwt.dom.client.Element would work just as well):

    @JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "?")
    interface CodeJar {
      @JsMethod(namespace = JsPackage.GLOBAL, name = "CodeJar")
      static native CodeJar newInstance(HTMLElement element, HighlightFn highlight);
      @JsMethod(namespace = JsPackage.GLOBAL, name = "CodeJar")
      static native CodeJar newInstance(HTMLElement element, HighlightFn highlight, Options opts);
    
      void updateOptions(Options options);
      void updateCode(String code);
      void onUpdate(UpdateFn cb);
      void destroy();
    }
    
    @JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
    class Options {
      public String tab;
      public JsRegExp indentOn;
      public boolean spellcheck;
      public boolean addClosing;
    }
    
    @JsFunction
    @FunctionalInterface
    interface HighlightFn {
      void highlight(HTMLElement e);
    }
    
    @JsFunction
    @FunctionalInterface
    interface UpdateFn {
      void onUpdate(String code);
    }
    

    With the above code, you should be able to create an editor using something like:

    CodeJar jar = CodeJar.newInstance(editor, MyHighlighter::highlight);
    

    If you use a dynamic import(), replace the static methods with instance ones in a @JsType interface representing the module received from the promise.