flutter-webdart-js-interop

Use js library in flutter web


I need widget with bpmn.js view: https://github.com/bpmn-io/bpmn-js

Used HtmlElementView:

    // ignore: undefined_prefixed_name
    ui.platformViewRegistry
        .registerViewFactory('bpmn_view', (int viewId) => element);

    return Column(
      children: <Widget>[
        Expanded(
            child: HtmlElementView(key: UniqueKey(), viewType: "bpmn_view")),
      ],
    );

With js:

    const html = '''
    <div id="canvas">canvas</div>
    <script>
      (function () {
        window.addEventListener('view_bpmn', function (e) {
           var bpmnJS = new BpmnJS({
               container: "#canvas"
           });

           bpmnJS.importXML(e.details);
         }, false);
      }());
    </script>
    ''';

    element.setInnerHtml(html,
        validator: NodeValidatorBuilder.common()..allowElement('script'));

enter image description here

But I get error when it execute:

VM4761 bpmn-viewer.development.js:18864 Uncaught TypeError: Cannot read property 'appendChild' of null
    at Viewer.BaseViewer.attachTo (VM4761 bpmn-viewer.development.js:18864)
    at Viewer.BaseViewer._init (VM4761 bpmn-viewer.development.js:18911)
    at Viewer.BaseViewer (VM4761 bpmn-viewer.development.js:18454)
    at new Viewer (VM4761 bpmn-viewer.development.js:19082)
    at <anonymous>:3:25
    at main.dart:185
    at future.dart:316
    at internalCallback (isolate_helper.dart:50)

And I can't set selector for BpmnJS like:

 var bpmnJS = new BpmnJS({
               container: "document.querySelector('flt-platform-view').shadowRoot.querySelector('#canvas')";
           });

How can I make it work?


Solution

  • Since BpmnJS container parameter accepts DOMElement type value, we can pass querySelector's result directly:

        _element = html.DivElement()
          ..id = 'canvas'
          ..append(html.ScriptElement()
            ..text = """
            const canvas = document.querySelector("flt-platform-view").shadowRoot.querySelector("#canvas");
            const viewer = new BpmnJS({ container: canvas });
            """);
    
        // ignore: undefined_prefixed_name
        ui.platformViewRegistry
            .registerViewFactory('bpmn-view', (int viewId) => _element);
    

    BpmnJS module should be attached to index.html file (in your project's top-level web folder):

    <!DOCTYPE html>
    <head>
      <title>BpmnJS Demo</title>
      <script defer src="main.dart.js" type="application/javascript"></script>
      <script src="https://unpkg.com/bpmn-js@6.4.2/dist/bpmn-navigated-viewer.development.js"></script>
    </head>
    ...
    
    

    Here is full code:

    import 'dart:ui' as ui;
    import 'package:universal_html/html.dart' as html;
    import 'package:flutter/material.dart';
    
    class BpmnDemo extends StatefulWidget {
      @override
      _BpmnDemoState createState() => _BpmnDemoState();
    }
    
    class _BpmnDemoState extends State<BpmnDemo> {
      html.DivElement _element;
    
      @override
      void initState() {
        super.initState();
    
        _element = html.DivElement()
          ..id = 'canvas'
          ..append(html.ScriptElement()
            ..text = """
            const canvas = document.querySelector("flt-platform-view").shadowRoot.querySelector("#canvas");
            const viewer = new BpmnJS({ container: canvas });
            const uri = "https://cdn.staticaly.com/gh/bpmn-io/bpmn-js-examples/dfceecba/url-viewer/resources/pizza-collaboration.bpmn";
            fetch(uri).then(res => res.text().then(xml => viewer.importXML(xml)));
            """);
    
        // ignore: undefined_prefixed_name
        ui.platformViewRegistry
            .registerViewFactory('bpmn-view', (int viewId) => _element);
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(),
          body: Center(
              child: HtmlElementView(key: UniqueKey(), viewType: "bpmn-view")),
        );
      }
    }
    
    

    UPDATE:

    This example shows how to load a diagram from dart code and uses dart:js library:

    import 'dart:ui' as ui;
    import 'dart:js' as js;
    import 'package:universal_html/html.dart' as html;
    import 'package:flutter/material.dart';
    
    class BpmnDemo extends StatefulWidget {
      @override
      _BpmnDemoState createState() => _BpmnDemoState();
    }
    
    class _BpmnDemoState extends State<BpmnDemo> {
      html.DivElement _element;
      js.JsObject _viewer;
    
      @override
      void initState() {
        super.initState();
        _element = html.DivElement();
        _viewer = js.JsObject(
          js.context['BpmnJS'],
          [
            js.JsObject.jsify({'container': _element})
          ],
        );
        // ignore: undefined_prefixed_name
        ui.platformViewRegistry.registerViewFactory('bpmn-view', (int viewId) => _element);
        loadDiagram('assets/pizza-collaboration.bpmn');
      }
    
      loadDiagram(String src) async {
        final bundle = DefaultAssetBundle.of(context);
        final xml = await bundle.loadString(src);
        _viewer.callMethod('importXML', [xml]);
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(),
          body: Center(child: HtmlElementView(key: UniqueKey(), viewType: "bpmn-view")),
        );
      }
    }
    
    

    UPDATE 2:

    Certain complications with calling methods from js library can arise when HtmlElementView uses IFrame element. In this case we can try two options:

    1. Store IFrame context on dart side and then use callMethod with saved context.
    2. Use postMessage method to communicate with IFrame
    import 'dart:ui' as ui;
    import 'dart:js' as js;
    import 'dart:html' as html;
    import 'package:flutter/material.dart';
    
    class IFrameDemoPage extends StatefulWidget {
      @override
      _IFrameDemoPageState createState() => _IFrameDemoPageState();
    }
    
    class _IFrameDemoPageState extends State<IFrameDemoPage> {
      html.IFrameElement _element;
      js.JsObject _connector;
    
      @override
      void initState() {
        super.initState();
    
        js.context["connect_content_to_flutter"] = (content) {
          _connector = content;
        };
    
        _element = html.IFrameElement()
          ..style.border = 'none'
          ..srcdoc = """
            <!DOCTYPE html>
              <head>
                <script>
                  // variant 1
                  parent.connect_content_to_flutter && parent.connect_content_to_flutter(window)
                  function hello(msg) {
                    alert(msg)
                  }
    
                  // variant 2
                  window.addEventListener("message", (message) => {
                    if (message.data.id === "test") {
                      alert(message.data.msg)
                    }
                  })
                </script>
              </head>
              <body>
                <h2>I'm IFrame</h2>
              </body>
            </html>
            """;
    
        // ignore:undefined_prefixed_name
        ui.platformViewRegistry.registerViewFactory(
          'example',
          (int viewId) => _element,
        );
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            actions: [
              IconButton(
                icon: Icon(Icons.filter_1),
                tooltip: 'Test with connector',
                onPressed: () {
                  _connector.callMethod('hello', ['Hello from first variant']);
                },
              ),
              IconButton(
                icon: Icon(Icons.filter_2),
                tooltip: 'Test with postMessage',
                onPressed: () {
                  _element.contentWindow.postMessage({
                    'id': 'test',
                    'msg': 'Hello from second variant',
                  }, "*");
                },
              )
            ],
          ),
          body: Container(
            child: HtmlElementView(viewType: 'example'),
          ),
        );
      }
    }