WORKING WITH PORTS Many diagramming applications deal with elements with ports. Ports are usually displayed as circles inside diagram elements and are used not only as "sticky" points for connected links but they also further structure the linking information. It is common that certain elements have lists of input and output ports. A link might then point not to the element as a whole but to a certain port instead.
JointJS has a built-in support for elements with ports, linking between ports and a facility for defining what connections are allowed and what not. This is useful if you, for example, want to restrict linking in between input ports, or output ports or between a certain port of an element A and a certain port of an element B. This tutorial shows you how you can do all that.
Creating elements with ports The easiest way to start with elements with ports is using the joint.shapes.devs plugin. Search for joint.shapes.devs.js file. This plugin defines one important shape, the joint.shapes.devs.Model*. You can just instantiate that shape and pass the inPorts and outPorts arrays as parameters. You can further set the coloring of the ports and label for your element as you can see in the example below. Moreover, JointJS takes care of preparing the view and the magnets** for UI interaction. That's why you can already click and drag a port and JointJS automatically creates a link coming out of that port.
JointJS and the joint.shapes.devs.Model also makes it easy to change ports. Simply set the inPorts/outPorts arrays of your element:
element.set('inPorts', ['newIn1', 'newIn2', 'newIn3']); element.set('outPorts', ['newOut1', 'newOut2']); *DEVS is an abbreviation for Discrete EVent System specification and is a formalism for modeling and analyzing general systems. This formalism uses two types of models (Atomic and Coupled) both having a set of input and output ports.
**Magnets in JointJS are SVG sub-elements that serve as sticky points for links. If you use the joint.shapes.devs plugin, you don't have to define your magnets yourself, instead the joint.shapes.devs.Model shape does it for you.
(function() {
var graph = new joint.dia.Graph; var paper = new joint.dia.Paper({ el: $('#paper-create'), width: 650, height: 200, gridSize: 1, model: graph });
var m1 = new joint.shapes.devs.Model({ position: { x: 50, y: 50 }, size: { width: 90, height: 90 }, inPorts: ['in1','in2'], outPorts: ['out'], ports: { groups: { 'in': { attrs: { '.port-body': { fill: '#16A085' } } }, 'out': { attrs: { '.port-body': { fill: '#E74C3C' } } } } }, attrs: { '.label': { text: 'Model', 'ref-x': .5, 'ref-y': .2 }, rect: { fill: '#2ECC71' } } }); graph.addCell(m1);
}()); Linking elements with ports Now when you have your elements with ports created, you can start observing what port is connected with a link to what other port. This is easy to do thanks to JointJS storing the information about ports in the link models themselves once the links are created via the UI. The following example shows you how you can get the linking information. Try to connect a port of one element to another port of another element.
(function() {
var graph = new joint.dia.Graph; var paper = new joint.dia.Paper({ el: $('#paper-link'), width: 650, height: 200, gridSize: 1, model: graph });
var m1 = new joint.shapes.devs.Model({ position: { x: 50, y: 50 }, size: { width: 90, height: 90 }, inPorts: ['in1','in2'], outPorts: ['out'], ports: { groups: { 'in': { attrs: { '.port-body': { fill: '#16A085' } } }, 'out': { attrs: { '.port-body': { fill: '#E74C3C' } } } } }, attrs: { '.label': { text: 'Model', 'ref-x': .5, 'ref-y': .2 }, rect: { fill: '#2ECC71' } } }); graph.addCell(m1);
var m2 = m1.clone().translate(300, 0).attr('.label/text', 'Model 2'); graph.addCell(m2);
graph.on('change:source change:target', function(link) { var sourcePort = link.get('source').port; var sourceId = link.get('source').id; var targetPort = link.get('target').port; var targetId = link.get('target').id;
var m = [ 'The port <b>' + sourcePort, '</b> of element with ID <b>' + sourceId, '</b> is connected to port <b>' + targetPort, '</b> of elemnt with ID <b>' + targetId + '</b>' ].join(''); out(m); });
function out(m) { $('#paper-link-out').html(m); }
}()); Linking restrictions Now you know how to create elements with ports and how to get the linking information. Another practical functionality related to elements with ports and their links is restricting certain connections. Say you want links to never start in input ports and never end in output ports. This is the most usual case. However, all kinds of restrictions are possible and application specific. JointJS doesn't limit you. Instead, it allows you to define a function that simply returns true if a connection between a source magnet of a source element and a target magnet of a target element is allowed, and false otherwise. If the connection is not allowed JointJS does not connect the magnets (and associated ports). Furthermore, you can mark certain magnets as "passive" in which case JointJS treats these magnets in a way that they can never become a source of a link. For further information, please see the list of options that you can pass to the joint.dia.Paper in the API reference page, especially the two related functions: validateConnection() and validateMagnet().
(function() {
var graph = new joint.dia.Graph; var paper = new joint.dia.Paper({ el: $('#paper-restrict'), width: 650, height: 200, gridSize: 1, model: graph, defaultLink: new joint.dia.Link({ attrs: { '.marker-target': { d: 'M 10 0 L 0 5 L 10 10 z' } } }), validateConnection: function(cellViewS, magnetS, cellViewT, magnetT, end, linkView) { // Prevent linking from input ports. if (magnetS && magnetS.getAttribute('port-group') === 'in') return false; // Prevent linking from output ports to input ports within one element. if (cellViewS === cellViewT) return false; // Prevent linking to input ports. return magnetT && magnetT.getAttribute('port-group') === 'in'; }, validateMagnet: function(cellView, magnet) { // Note that this is the default behaviour. Just showing it here for reference. // Disable linking interaction for magnets marked as passive (see below `.inPorts circle`). return magnet.getAttribute('magnet') !== 'passive'; } });
var m1 = new joint.shapes.devs.Model({ position: { x: 50, y: 50 }, size: { width: 90, height: 90 }, inPorts: ['in1','in2'], outPorts: ['out'], ports: { groups: { 'in': { attrs: { '.port-body': { fill: '#16A085', magnet: 'passive' } } }, 'out': { attrs: { '.port-body': { fill: '#E74C3C' } } } } }, attrs: { '.label': { text: 'Model', 'ref-x': .5, 'ref-y': .2 }, rect: { fill: '#2ECC71' } } }); graph.addCell(m1);
var m2 = m1.clone(); m2.translate(300, 0); graph.addCell(m2); m2.attr('.label/text', 'Model 2');
}()); Link snapping To improve user experience little bit you might want to enable the link snapping. While the user is dragging a link, it searches for the closest port in the given radius. Once a suitable port is found (it meets requirements specified in validateConnection()) the link automatically connects to it. You can try this functionality in the example below.
(function() {
var graph = new joint.dia.Graph; var paper = new joint.dia.Paper({ el: $('#paper-link-snapping'), width: 650, height: 200, gridSize: 1, model: graph, defaultLink: new joint.dia.Link({ attrs: { '.marker-target': { d: 'M 10 0 L 0 5 L 10 10 z' } } }), validateConnection: function(cellViewS, magnetS, cellViewT, magnetT, end, linkView) { // Prevent loop linking return (magnetS !== magnetT); }, // Enable link snapping within 75px lookup radius snapLinks: { radius: 75 } });
var m1 = new joint.shapes.devs.Model({ position: { x: 50, y: 50 }, size: { width: 90, height: 90 }, inPorts: ['in1','in2'], outPorts: ['out'], ports: { groups: { 'in': { attrs: { '.port-body': { fill: '#16A085', magnet: 'passive' } } }, 'out': { attrs: { '.port-body': { fill: '#E74C3C' } } } } }, attrs: { '.label': { text: 'Model', 'ref-x': .5, 'ref-y': .2 }, rect: { fill: '#2ECC71' } } }); graph.addCell(m1);
var m2 = m1.clone(); m2.translate(300, 0); graph.addCell(m2); m2.attr('.label/text', 'Model 2');
})(); Marking available magnets Another way how to make user's life easier can be to offer him all magnets he can connect to while he is dragging a link. To achieve this you have to enable markAvailable option on the paper and add some css rules into your stylesheet like in the example bellow.
.available-magnet { fill: yellow; }
.available-cell rect { stroke-dasharray: 5, 2; } (function() {
var graph = new joint.dia.Graph; var paper = new joint.dia.Paper({ el: $('#paper-mark-available'), width: 650, height: 200, gridSize: 1, model: graph, defaultLink: new joint.dia.Link({ attrs: { '.marker-target': { d: 'M 10 0 L 0 5 L 10 10 z' } } }), validateConnection: function(cellViewS, magnetS, cellViewT, magnetT, end, linkView) { if (magnetS && magnetS.getAttribute('port-group') === 'in') return false; if (cellViewS === cellViewT) return false; return magnetT && magnetT.getAttribute('port-group') === 'in'; }, markAvailable: true });
var m1 = new joint.shapes.devs.Model({ position: { x: 50, y: 50 }, size: { width: 90, height: 90 }, inPorts: ['in1','in2'], outPorts: ['out'], ports: { groups: { 'in': { attrs: { '.port-body': { fill: '#16A085', magnet: 'passive' } } }, 'out': { attrs: { '.port-body': { fill: '#E74C3C' } } } } }, attrs: { '.label': { text: 'Model', 'ref-x': .5, 'ref-y': .2 }, rect: { fill: '#2ECC71' } } }).addTo(graph);
var m2 = m1.clone().translate(300, 0).attr('.label/text', 'Model 2').addTo(graph);
})();
|