Working with Ports

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.

Creating elements with ports

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' }}
    }
]);

Setting port properties

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

Linking restrictions

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 }
});

Marking available magnets

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

Port layouts

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.