JointJS: Dynamic Markup

by Roman Bruckner

HTML

<html>

  <head>
    <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jointjs/3.6.3/joint.css" />
  </head>

  <body>
    <!-- content -->
    <div id="paper"></div>

    <!-- dependencies -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.4.0/backbone.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jointjs/3.7.5/joint.js"></script>

  </body>

</html>

JavaScript

const { dia, util, shapes: defaultShapes } = joint;

const List = dia.Element.define('List', {
    size: { width: 155, height: 0 },
    attrs: {
        body: {
            width: 'calc(w)',
            height: 'calc(h)',
            fill: '#E0E0E0',
            rx: '10',
            ry: '10',
        },
        label: {
            html: 'List',
            style: {
                margin: '5px',
                fontSize: '25px'
            }
        },
        bodyHTML: {
            width: 'calc(w)',
            height: 'calc(h)',
        },
    },
    items: []
}, {

    initialize: function() {
        dia.Element.prototype.initialize.apply(this, arguments);
        this.on('change', this.onChange, this);
        this.buildMarkup();
    },

    onChange: function(_, opt) {
        // make sure the markup is not changed from the outside
        if (opt.list !== this.id && this.hasChanged('markup')) {
            throw new Error('Markup can not be modified.');
        }
        // when the items has changed, rebuild the markup
        if (this.hasChanged('items')) {
            this.buildMarkup(opt);
        }
    },

    buildMarkup: function(opt) {

        let items = this.get('items');
        if (!Array.isArray(items)) {
            items = [];
        }

        const itemsMarkup = items.map((item) => this.buildItemMarkup(item));

        const markup = util.svg/* xml */`
            <rect @selector="body" />
            <foreignObject @selector="bodyHTML">
                <div @selector="label" xmlns="http://www.w3.org/1999/xhtml"></div>
                <div @selector="itemsWrap" style="padding: 5px;" xmlns="http://www.w3.org/1999/xhtml"></div>
            </foreignObject>
        `;

        const [, bodyHTML] = markup;
        bodyHTML.children[1].children = itemsMarkup;

        const flags = { list: this.id, dry: true, ...opt };
        this.set('markup', markup, flags);
        const { width } = this.size();
        const height = items.length * 40...