6

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?

  • Why are you trying to access with the `viewType` . Since inside your shadow dom the canvas id is `canvas` use something like this.`document.querySelector('flt-platform-view').shadowRoot.querySelector('#canvas');` – Abhilash Chandran Feb 18 '20 at 09:46
  • Ah, ok. Thx. I change and edit this. – Vitaliy Vostrikov Feb 18 '20 at 10:03
  • I think I just use iframe element but I want know how to do it properly in flutter web. – Vitaliy Vostrikov Feb 18 '20 at 10:04
  • 1
    Iframe is not a good solution in my humble opinion. Especially when flutter rebuilds the `HTMLElementView` widget it might even re-render the iframe altogether For e.g when a resize is happening. Also communication between the main app and Iframe could be another paint point. I will try this issue later today. – Abhilash Chandran Feb 18 '20 at 10:16

1 Answers1

9

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

Spatz
  • 18,640
  • 7
  • 62
  • 66
  • Thx! Can I pass xml to viewer without `fetch` operation? – Vitaliy Vostrikov May 22 '20 at 10:29
  • 1
    I added an example. – Spatz May 23 '20 at 06:15
  • Excellent answer! Is there a way of sending back a message from JS to the Dart side? – nbloqs Jun 18 '20 at 01:22
  • CustomEvent event maybe: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent – Vitaliy Vostrikov Jun 18 '20 at 11:51
  • 1
    @nbloqs [here](https://stackoverflow.com/a/61306049/1891712) is example with PayPal feedback to flutter, but to work with a large number of methods you have to create wrapper with a [package:js](https://pub.dev/packages/js) (not a dart:js library). – Spatz Jun 18 '20 at 12:14
  • Thanks! I will try and if I can't figure it out, I will post a question. – nbloqs Jun 18 '20 at 14:00
  • Thanks again. By looking into the PayPal example I can send and receive data, but calling methods, although I can do it with functions defined in the Flutter for web's index.html, I am not able to call methods inside the IFrame's srcdoc. Is there a way to do that? – nbloqs Jun 19 '20 at 00:19
  • 1
    @nbloqs I added some suggestions to my answer. – Spatz Jun 19 '20 at 12:39
  • @Spatz Thanks a lot! – nbloqs Jun 19 '20 at 13:52
  • The name 'platformViewRegistry' is being referenced through the prefix 'ui', but it isn't defined in any of the libraries imported using that prefix. Try correcting the prefix or importing the library that defines 'platformViewRegistry'. – gowthaman C Nov 26 '20 at 11:15
  • @gowthaman C did you miss this line ``// ignore:undefined_prefixed_name``? – Spatz Nov 26 '20 at 12:49