Many diagramming applications deal with the idea of elements with ports. Ports are often displayed as circles inside diagram elements. They are not only used 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 not then point to an element as a whole, but to a certain port instead.
JointJS has built-in support for elements with ports, linking between ports, and a facility for defining what connections are allowed. This is useful if you, for example, want to restrict linking between input and output ports, or between certain ports of different elements. This tutorial takes you through the process from the beginning, and shows you how you can achieve all that.
To add ports to elements, we can pass a ports definition as an option to a constructor, or utilize the Port API provided by JointJS. Both methods allow us to add ports to any arbitrary shape we can imagine.
In the following example, we define a port with some basic
configuration, and instantiate a
standard.Rectangle
shape. Notice we provide a magnet
attribute with a value of true
to our port definition. This allows our port to become a source/target of a link during reconnection, and provide that
nice UI interaction. That's why when we click and drag a port, JointJS automatically creates a link coming out of that
port. Cool, right?
When drawing a link from an active magnet, the default link is joint.dia.Link
. As this link contains
linkTools
, you may
wish to change the link which is created. You can achieve this via the
defaultLink
paper option as shown in
this example. To prevent the link being dropped in a blank paper area, use the
linkPinning
option.
JointJS source code: ports-basic.js
new joint.dia.Paper({
// Other Paper options
defaultLink: () => new joint.shapes.standard.Link(),
linkPinning: false
});
var port = {
label: {
position: {
name: 'left'
},
markup: [{
tagName: 'text',
selector: 'label'
}]
},
attrs: {
portBody: {
magnet: true,
width: 16,
height: 16,
x: -8,
y: -8,
fill: '#03071E'
},
label: {
text: 'port'
}
},
markup: [{
tagName: 'rect',
selector: 'portBody'
}]
};
var model = new joint.shapes.standard.Rectangle({
position: { x: 275, y: 50 },
size: { width: 90, height: 90 },
attrs: {
body: {
fill: '#8ECAE6'
}
},
ports: {
items: [ port ] // add a port in constructor
}
});
model.addPort(port); // add a port using Port API
What if we want a little more control over our ports? Expanding from our previous example, we will introduce the idea of groups. Groups can provide more structure, allow us to separate ports into the catagories we want, and influence the layout of our ports.
In the following example, we will cover a common use case, the idea of input and output ports. To get started, we create a separate port definition for each group, and again add some basic configuration. This allows us to create groups with similar properties, but differences in visual presentation.
When we are satisfied with our individual port definitions, we can then define our port groups in the
element constructor. You are free to name the groups how you like depending on your specific use case.
As we are creating input and output ports, we name our port groups 'in'
and 'out'
respectively.
Lastly, using the Port API, we use the addPorts()
method to add an array of ports while
also providing a custom label for each one.
JointJS source code: ports-basic-groups.js
var portsIn = {
position: {
name: 'left'
},
attrs: {
portBody: {
magnet: true,
r: 10,
fill: '#023047',
stroke: '#023047'
}
},
label: {
position: {
name: 'left',
args: { y: 6 }
},
markup: [{
tagName: 'text',
selector: 'label',
className: 'label-text'
}]
},
markup: [{
tagName: 'circle',
selector: 'portBody'
}]
};
var portsOut = {
position: {
name: 'right'
},
attrs: {
portBody: {
magnet: true,
r: 10,
fill: '#E6A502',
stroke:'#023047'
}
},
label: {
position: {
name: 'right',
args: { y: 6 }
},
markup: [{
// Markup
}]
},
markup: [{
// Markup
}]
};
var model = new joint.shapes.standard.Rectangle({
position: { x: 275, y: 50 },
size: { width: 90, height: 90 },
attrs: {
body: {
fill: '#8ECAE6',
},
label: {
text: 'Model',
fontSize: 16,
y: -10
}
},
ports: {
groups: {
'in': portsIn,
'out': portsOut
}
}
});
model.addPorts([
{
group: 'in',
attrs: { label: { text: 'in1' }}
},
{
group: 'in',
attrs: { label: { text: 'in2' }}
},
{
group: 'out',
attrs: { label: { text: 'out' }}
}
]);
If you are already familiar with the prop()
method for Elements and Links, JointJS also provides a similar method for working with ports.
portProp()
allows users to set
presentation attributes or custom data properties on an element port.
const port = {
id: 'custom-port-id', // set a custom ID
label: {
markup: [{
tagName: 'text',
selector: 'label'
}]
},
attrs: {
portBody: {
magnet: true,
width: 16,
height: 16,
x: -8,
y: -8,
fill: '#03071E'
},
label: {
text: 'port'
}
},
markup: [{
tagName: 'rect',
selector: 'portBody'
}]
};
const element = new joint.shapes.standard.Rectangle({
position: { x: 275, y: 50 },
size: { width: 90, height: 90 },
attrs: {
body: {
fill: '#8ECAE6'
}
},
ports: {
items: [ port ]
}
});
element.portProp('custom-port-id', 'attrs/portBody', { r: 8, fill: 'darkslateblue' });
const portId = element.getPorts()[0].id;
element.portProp(portId, 'custom', { testAttribute: true });
console.log(element.portProp(portId, 'custom')); // {testAttribute: true}
element.portProp(portId, 'attrs/portBody/fill', 'tomato');
Ports aren't so interesting all by themselves, so let's look at linking elements with ports. In the diagram that follows, we create two elements much like the previous examples. We can observe which ports are connected to which, because JointJS stores information about ports in the link models themselves once the links are created via the UI. Try connecting the ports yourself, and you should see a message displaying the linking information.
JointJS source code: ports-links.js
Now that we know how to create elements with ports, and get the linking information, we should look
at another common pattern, and that's how to restrict certain connections. JointJS doesn't limit you,
and allows you to restrict or permit any connections you like depending on your use case. JointJS
simply allows you to define a function that returns true
if a connection between a source
and target magnet of a certain element is allowed, or false
otherwise.
You can also mark certain magnets as 'passive'
. That means JointJS will treat the magnets in a
way that doesn't permit them becoming a source of a link. For further information, please see the list of options
that you can pass to joint.dia.Paper
in the
API reference page,
especially the two related functions: validateConnection()
and validateMagnet()
.
As we have discussed input and output ports throughout this tutorial, we will continue with that theme. In the following example, we have restricted linking from input ports, linking to output ports, and also linking from output to input ports of the same element. All of this simply means we can link from output to input ports as long as it's a different element.
JointJS source code: ports-restrictions.js
You can see some example restrictions in the following code snippet:
new joint.dia.Paper({
// Other Paper options
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 output ports
return magnetT && magnetT.getAttribute('port-group') === 'in';
},
validateMagnet: function(cellView, magnet) {
// Note that this is the default behaviour. It is shown for reference purposes.
// Disable linking interaction for magnets marked as passive
return magnet.getAttribute('magnet') !== 'passive';
}
});
To improve user experience, you might want to enable
link snapping. This means that while
the user is dragging a link, it searches for the closest port in a 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.
JointJS source code: ports-link-snapping.js
new joint.dia.Paper({
// Other Paper options
validateConnection: function(cellViewS, magnetS, cellViewT, magnetT, end, linkView) {
// Prevent loop linking
return (magnetS !== magnetT);
},
// Enable link snapping within 20px lookup radius
snapLinks: { radius: 20 }
});
Another way to make the user's life easier is to indicate which magnets are available to connect to while
dragging a link. To achieve this, all you have to do is enable the
markAvailable
option on the
paper, and add some CSS rules in your stylesheet to target the classes shown in the code snippet below.
new joint.dia.Paper({
// Other Paper options
// Enable mark available for cells & magnets
markAvailable: true
});
/* port styling */
.available-magnet {
fill: #5DA271;
}
/* element styling */
.available-cell rect {
stroke-dasharray: 5, 2;
}
Using the paper highlighting option,
JointJS also allows us to customize highlighting behaviour relating to a given interaction. In the code snippet
below, magnetAvailability
creates a custom CSS class we can target in our stylesheets, and
elementAvailability
is used to give our element a nice accent color upon availability. You can also
utilize your own custom highlighters in this manner.
var paper = new joint.dia.Paper({
// Other paper options
highlighting: {
'magnetAvailability': {
name: 'addClass',
options: {
className: 'custom-available-magnet'
}
},
'elementAvailability': {
name: 'stroke',
options: {
padding: 20,
attrs: {
'stroke-width': 3,
'stroke': '#ED6A5A'
}
}
}
}
});
JointJS source code: ports-mark-available.js
JointJS source code: ports.css
We have already used Port Layouts briefly in this tutorial.
In linking elements with ports, and several other sections, we aligned our port groups
to the 'left'
and 'right'
of our standard.Rectangle
element. This functionality is
very convenient , but did you know JointJS provides even more layout options? Let's explore an example.
In the diagram below, we create a standard.Ellipse
element. The layout we were using before doesn't make much
sense for a circular shape if we have more than one port, so we will use a radial layout for our ports instead. In our port
definition, notice we now use the 'ellipseSpreads'
layout to evenly spread our input ports along the ellipse.
One important note about port layouts is that they can only be defined at group
level, so it's something to
be aware of when you are creating port definitions.
JointJS source code: ports-layout.js
var ellipsePortsIn = {
position: {
name: 'ellipseSpread',
args: {
dx: 1,
dy: 1,
dr: 1,
startAngle: 220,
step: 50,
compensateRotation: false
}
},
// Other attributes
};
That's all we will cover in this tutorial. Hopefully, we highlighted some information that is useful for you. If you would like to explore any of the features mentioned here in more detail, you can find extra information in our JointJS documentation.