Events

This is the second article of the intermediate section of the JointJS tutorial. Return to special attributes. See index of basic and intermediate articles.

JointJS offers several different ways to enable user interaction. Events are triggered on your paper, on individual element/link views.

Built-in Paper Events

Paper automatically triggers several built-in events upon user interaction. These include pointerdown, double click and right click events, as well as link connection or cell highlighting events. You can find the full list in the Paper documentation. Reacting to any of these events is as simple as adding a listener on your paper; this is the easiest way to detect one type of interaction on all instances of a component.

For example, the event 'link:pointerdblclick' is triggered when a double click is detected on any link in the diagram. A corresponding event for elements ('element:pointerdblclick'), cells in general ('cell:pointerdblclick'), and for blank areas on the paper ('blank:pointerdblclick') is provided as well, Here, the cell event listener unhides an element with a message whenever an Element or Link is clicked. The more specific element and link event listeners change the color of their model's stroke. Finally, the blank event listener hides the message element and changes the color of the Paper background:

paper.on('blank:pointerdblclick', function() {
    resetAll(this);

    info.attr('body/visibility', 'hidden');
    info.attr('label/visibility', 'hidden');

    this.drawBackground({
        color: 'orange'
    });
});

paper.on('element:pointerdblclick', function(elementView) {
    resetAll(this);

    var currentElement = elementView.model;
    currentElement.attr('body/stroke', 'orange');
});

paper.on('link:pointerdblclick', function(linkView) {
    resetAll(this);

    var currentLink = linkView.model;
    currentLink.attr('line/stroke', 'orange');
    currentLink.label(0, {
        attrs: {
            body: {
                stroke: 'orange'
            }
        }
    });
});

paper.on('cell:pointerdblclick', function(cellView) {
    var isElement = cellView.model.isElement();
    var message = (isElement ? 'Element' : 'Link') + ' clicked';
    info.attr('label/text', message);

    info.attr('body/visibility', 'visible');
    info.attr('label/visibility', 'visible');
});

function resetAll(paper) {
    paper.drawBackground({
        color: 'white'
    });

    var elements = paper.model.getElements();
    for (var i = 0, ii = elements.length; i < ii; i++) {
        var currentElement = elements[i];
        currentElement.attr('body/stroke', 'black');
    }

    var links = paper.model.getLinks();
    for (var j = 0, jj = links.length; j < jj; j++) {
        var currentLink = links[j];
        currentLink.attr('line/stroke', 'black');
        currentLink.label(0, {
            attrs: {
                body: {
                    stroke: 'black'
                }
            }
        });
    }
}

JointJS source code: events-paper-events.js

The event listener callback functions have the signature callback([cellView,] eventObject, eventX, eventY) (cellView is not provided for 'blank:…' events, for obvious reasons). Inside the listeners, the paper itself is available as this. The eventObject can be used to, for example, stopPropagation(). The x and y coordinates can be useful when placing markers at sites of user interaction. We have not used these three parameters in the above example, but you will encounter them frequently in JointJS event listeners.

Note that all of these events are also triggered on the individual cellView (element or link view) the user interacted with. Therefore, you could achieve the same functionality as above by adding a listener on every elementView and linkView separately. While this approach has its uses, we recommend using paper listeners; having a single listener cover all interaction options is better practice in JavaScript than having dozens of listeners for individual views.

Built-in Graph Events

The Graph model comes with several built-in events as well. These events react to changes in the cell model's properties, including position, size, attributes, and status of JointJS transitions. JointJS documentation contains a generic list of Graph events you can react to, alongside more specific lists for Element events and Link events. To react to these events, add a listener on your Graph model.

In the following example, the event 'change:position' is triggered on the element when it is moved; we determine the new position of the element's center and write it into its text label. The event 'change:target' is triggered on the link when its target is moved; we use that event to write the new target position into the link label.

graph.on('change:position', function(cell) {
    var center = cell.getBBox().center();
    var label = center.toString();
    cell.attr('label/text', label);
});

graph.on('change:target', function(cell) {
    var target = new g.Point(cell.target());
    var label = target.toString();
    cell.label(0, {
        attrs: {
            label: {
                text: label
            }
        }
    });
});

JointJS source code: events-graph-events.js

The callback functions of the cell graph change events have the signature callback(cell, newValue). Inside the listeners, the graph itself is available as this. In our example, we were able to assume that the received cell model is of the type Element and Link, respectively, because Link objects do not trigger 'change:position' events, and Element objects do not trigger 'change:target'. Keep in mind that some Graph events can be triggered on both data types (e.g. 'change:attrs'). However, depending on what you are trying to do, you might need to check the actual cell data type first.

Other graph event listeners are provided with different parameters. The generic 'graph:change' event listeners receive only the changed cell but not the new value (callback(cell)). The 'graph:add' event listeners receive the added cell and the updated cells array (callback(cell, newCells)), whereas the 'graph:remove' event listeners receive the removed cell and the original cells array (callback(cell, oldCells)).

Similarly to built-in paper events, these events are also triggered on the individual cell (element or link model) the user interacted with. Therefore, you could achieve the same functionality as above by adding a listener on every element and link separately. While this approach has its uses, we recommend using graph listeners; having a single listener cover all interaction options is better practice in JavaScript than having dozens of listeners for individual models.

The graph can also react on changes in its own properties. For example, calling graph.set('property', true) would trigger 'change:property' on the graph). Event listeners for graph attribute changes receive a reference to the graph and the new value as their parameters (callback(graph, newValue)).

Beware that due to backwards-compatibility considerations, this can lead to confusion if we are careless in choosing custom graph property names! If we named our custom graph property position instead of property, the triggered event would be identified as 'change:position', and would thus be captured by the event listener in our example - but the callback would have to contend with an unexpected set of arguments. To avoid such name collisions, we strongly recommend adopting a naming convention for custom graph properties - e.g. starting their variable names with the word graph (i.e. graphProperty and graphPosition). If that is not an option, and you need to support custom graph attributes, you can make yourself safe by asserting that the cell parameter is not actually a Graph:

graph.on('change:position', function(cell) {
    if (cell instanceof joint.dia.Graph) return;
    var center = cell.getBBox().center();
    var label = center.toString();
    cell.attr('label/text', label);
});

Subelement Event Attribute

The easiest way to add a custom event to an individual component on your diagram is to use the event special attribute.

This special attribute is useful for creating elements with custom tool subelements (e.g. a minimize button). Then, you can simply attach a listener on your paper that gets called when your custom event is detected.

In the following example, we define a custom element type with a button subelement. We attach a custom event 'element:button:pointerdown' to the button and listen for it. When the button is pressed, the element's body and label are hidden (minimized), and the symbol in the button is changed to indicate that the element can now be unminimized:

var CustomElement = joint.dia.Element.define('examples.CustomElement', {
    attrs: {
        body: {
            width: 'calc(w)',
            height: 'calc(h)',
            strokeWidth: 2,
            stroke: 'black',
            fill: 'white'
        },
        label: {
            textVerticalAnchor: 'middle',
            textAnchor: 'middle',
            x: 'calc(0.5*w)',
            y: 'calc(0.5*h)',
            fontSize: 14,
            fill: 'black'
        },
        button: {
            cursor: 'pointer',
            ref: 'buttonLabel',
            width: 'calc(1.5*w)',
            height: 'calc(1.5*h)',
            x: 'calc(x-calc(0.25*w))',
            y: 'calc(y-calc(0.25*h))'
        },
        buttonLabel: {
            pointerEvents: 'none',
            x: 'calc(w)',
            y: 0,
            textAnchor: 'middle',
            textVerticalAnchor: 'middle'
        }
    }
}, {
    markup: [{
        tagName: 'rect',
        selector: 'body',
    }, {
        tagName: 'text',
        selector: 'label'
    }, {
        tagName: 'rect',
        selector: 'button'
    }, {
        tagName: 'text',
        selector: 'buttonLabel'
    }]
});

var element = new CustomElement();
element.position(250, 30);
element.resize(100, 40);
element.attr({
    label: {
        pointerEvents: 'none',
        visibility: 'visible',
        text: 'Element'
    },
    body: {
        cursor: 'default',
        visibility: 'visible'
    },
    button: {
        event: 'element:button:pointerdown',
        fill: 'orange',
        stroke: 'black',
        strokeWidth: 2
    },
    buttonLabel: {
        text: '_', // fullwidth underscore
        fill: 'black',
        fontSize: 8,
        fontWeight: 'bold'
    }
});
element.addTo(graph);

paper.on('element:button:pointerdown', function(elementView, evt) {
    evt.stopPropagation(); // stop any further actions with the element view (e.g. dragging)

    var model = elementView.model;

    if (model.attr('body/visibility') === 'visible') {
        model.attr('body/visibility', 'hidden');
        model.attr('label/visibility', 'hidden');
        model.attr('buttonLabel/text', '+'); // fullwidth plus

    } else {
        model.attr('body/visibility', 'visible');
        model.attr('label/visibility', 'visible');
        model.attr('buttonLabel/text', '_'); // fullwidth underscore
    }
});

JointJS source code: events-event-attribute.js

Custom View Events

For more advanced event customization, we need to delve into custom views. That is an advanced topic with many powerful options; here we will concentrate on extending our View objects with custom events.

The Paper object has two options that determine the views used to render diagram components:

We will use these two options to provide extended versions of the default ElementView and LinkView. The example removes double-clicked components:

var paper = new joint.dia.Paper({
    el: document.getElementById('paper-custom-view-events'),
    model: graph,
    width: 600,
    height: 100,
    gridSize: 1,
    background: {
        color: 'white'
    },
    interactive: false, // disable default interaction (e.g. dragging)
    elementView: joint.dia.ElementView.extend({
        pointerdblclick: function(evt, x, y) {
            this.model.remove();
        }
    }),
    linkView: joint.dia.LinkView.extend({
        pointerdblclick: function(evt, x, y) {
            this.model.remove();
        }
    })
});

JointJS source code: events-custom-view-events.js

Custom View Event Propagation

You can maintain recognition of the event by the built-in paper mechanism if you notify the CellView and Paper about the event. For example, in our custom ElementView pointerdblclick handler, we would include this:

joint.dia.CellView.prototype.pointerdblclick.apply(this, arguments);
this.notify('element:pointerdblclick', evt, x, y);

This can be useful if you need to maintain default event handling behavior. The following example integrates showing a message element on Cell click from the paper events demo with removing the components from the previous example:

var paper = new joint.dia.Paper({
    el: document.getElementById('paper-custom-view-events'),
    model: graph,
    width: 600,
    height: 100,
    gridSize: 1,
    background: {
        color: 'white'
    },
    interactive: false, // disable default interaction (e.g. dragging)
    elementView: joint.dia.ElementView.extend({
        pointerdblclick: function(evt, x, y) {
            joint.dia.CellView.prototype.pointerdblclick.apply(this, arguments);
            this.notify('element:pointerdblclick', evt, x, y);
            this.model.remove();
        }
    }),
    linkView: joint.dia.LinkView.extend({
        pointerdblclick: function(evt, x, y) {
            joint.dia.CellView.prototype.pointerdblclick.apply(this, arguments);
            this.notify('link:pointerdblclick', evt, x, y);
            this.model.remove();
        }
    })
});

paper.on('cell:pointerdblclick', function(cellView) {
    var isElement = cellView.model.isElement();
    var message = (isElement ? 'Element' : 'Link') + ' removed';
    info.attr('label/text', message);

    info.attr('body/visibility', 'visible');
    info.attr('label/visibility', 'visible');
});

JointJS source code: events-custom-view-events-propagation.js

In the next section of the intermediate tutorial, we will learn about data serialization.