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.
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.
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);
});
The easiest way to add a custom event to an individual component on your diagram is to use the
event
special attribute.
event
- adds an event
of the specified name to the subelement's model; to be triggered when JointJS detects a pointerdown
event (mousedown/touchstart DOM event) on the subelement.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
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:
elementView
- sets the ElementView to use for rendering Element models on this paper.
Defaults to
joint.dia.ElementView
.linkView
- sets the LinkView to use for rendering Link models. Defaults to
joint.dia.LinkView
.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
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.