Use js library in flutter web

Issue

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'),
      ),
    );
  }
}

Answered By – Spatz

Answer Checked By – Senaida (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published.