svgjointjsrappidforeignobject

JointJS how to create a HTML select (drop down) inside a shape object


I am looking for best practises or advice in how to create interactive elements within Shapes (elements) using the JointJS (Rappid) library.

This is what I have done currently:

enter image description here

The code

 const shape = new joint.shapes.basic.Generic({
        type: 'shape',
        attrs: {},
        markup: [
            {
                tagName: 'foreignObject',
                selector: 'foreignObject',
                attributes: {
                    x: '10',
                    y: '10',
                    width: '60',
                    height: '100',
                },
                children: [
                    {
                        tagName: 'div',
                        namespaceURI: 'http://www.w3.org/1999/xhtml',
                        selector: 'content',
                        style: {
                            display: 'flex',
                            flexDirection: 'column',
                            alignItems: 'center',
                            justifyContent: 'center',
                        },

                        children: [
                            {
                                tagName: 'select',
                                selector: 'select',
                                style: {
                                    'z-index': 1,
                                },
                                children: [
                                    {
                                        tagName: 'option',
                                        value: 'test1',
                                        textContent: 'test 1',
                                    },
                                    {
                                        tagName: 'option',
                                        value: 'test2',
                                        textContent: 'test 2',
                                    },
                                ],
                            },
                        ],
                    },
                ],
            },
        ],
    });

You can see there is a shape rendered and it has a select element in it by using a foreignObject (https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject). However, using foreignObject is troublesome due to that they do not play well in browsers and the drop down you see here doesn't actually work.

I could start hacking around and get it to work but this is the point of the question, is there a cleaner way in achieving this?

I need something like this

https://resources.jointjs.com/tutorial/html-elements

But the html library is now decrepitated and JointJS wants you to use markup JSON instead.

Lastly, I have seen this post which doesn't fill me much with confidence...

https://groups.google.com/g/jointjs/c/-yXXlnreq6M

But I am hoping as this is from 5 years ago, it is outdated and we don't need to have to do the workarounds as it suggests?


Solution

  • HTML can be added inside foreignObject, but as you mentioned there can be some issues in browsers. One example of using foreignObject can be found here.

    The code demonstrates how to create a text input with foreignObject.

    var paper = new joint.dia.Paper({
        el: document.getElementById('paper'),
        width: 650,
        height: 400,
        gridSize: 10,
        model: graph,
        guard: function(evt) {
            return evt.target instanceof HTMLInputElement;
        }
    });
    
    paper.on('blank:pointerdown cell:pointerdown', function() {
        document.activeElement.blur();
    });
    
    var Example = joint.dia.Element.define('example.ForeignObject', {
        attrs: {
            body: {
                refWidth: '100%',
                refHeight: '100%',
                stroke: '#333333',
                fill: '#ffffff',
                strokeWidth: 2
            },
            foreignObject: {
                refWidth: '100%',
                refHeight: '100%'
            }
        }
    }, {
        markup: [{
            tagName: 'rect',
            selector: 'body'
        }, {
            tagName: 'foreignObject',
            selector: 'foreignObject',
            attributes: {
                'overflow': 'hidden'
            },
            children: [{
                tagName: 'div',
                namespaceURI: 'http://www.w3.org/1999/xhtml',
                selector: 'content',
                style: {
                    fontSize: 14,
                    width: '100%',
                    height: '100%',
                    position: 'static',
                    backgroundColor: 'transparent',
                    textAlign: 'center',
                    margin: 0,
                    padding: '0px 10px',
                    boxSizing: 'border-box',
                    display: 'flex',
                    flexDirection: 'column',
                    alignItems: 'center',
                    justifyContent: 'center'
                },
                children: [{
                    tagName: 'span',
                    textContent: 'First Name'
                }, {
                    tagName: 'input',
                    selector: 'firstname',
                    attributes: {
                        'type': 'input',
                        'name': 'firstname'
                    },
                    style: {
                        position: 'static',
                        width: '100%'
                    }
                }, {
                    tagName: 'span',
                    textContent: 'Last Name'
                }, {
                    tagName: 'input',
                    selector: 'lastname',
                    attributes: {
                        'type': 'input',
                        'name': 'lastname'
                    },
                    style: {
                        position: 'static',
                        width: '100%'
                    }
                }]
            }]
        }]
    }, {
        attributes: {
            value: {
                set: function(text, _, node) {
                    if ('value' in node) node.value = text;
                }
            }
        }
    });
    
    joint.shapes.example.ForeignObjectView = joint.dia.ElementView.extend({
    
        events: {
            'change input': 'onInputChange'
        },
    
        onInputChange: function(evt) {
            var input = evt.target;
            this.model.attr(input.name + '/value', input.value);
        }
    });
    

    The article that you linked is deprecated, but there is an updated version of creating elements with a HTML face here. You can see some select fields demonstrated there, but it is a little more involved.