Skip to main content
Version: 4.1

Custom special presentation attributes

Custom special presentation attributes allow you to define your own special presentation attributes that can be used inside your shape's attrs model attribute just like other special presentation attributes. They work as shortcuts that can set multiple native SVG attributes to the rendered SVGElement representing a subelement of your shape based on any of the following:

Defining custom special presentation attributesโ€‹

Custom special presentation attributes are defined inside a static attributes object property on a custom shape. The keys of this object are strings that uniquely identify the shape's custom special presentation attributes (in kebab-case), and values are objects with callback functions - set(), unset(), position() and offset().

The provided callback functions tell JointJS how to work with the custom special presentation attribute at different stages of the built-in process of applying SVG attributes to subelements' SVGElements - as such, each callback function is suitable for setting different kinds of SVG attributes. All of these callback functions are optional - you are free to only specify the ones that are relevant for your use case.

info

Inside the attributes context, the keys of custom special presentation attributes have to be addressed via kebab-case naming, because this is a lower-level API of JointJS and the kebab-case to camelCase translation functionality is not available.

For the same reason, you cannot set any special presentation attributes - nor rely on the calc() functionality - inside any custom special presentation attribute callback function. The presentation attributes assigned inside the callback functions are always treated as native SVG attributes (which are addressed via kebab-case naming), never as JointJS attributes.

set()โ€‹

[optional] A function called by JointJS for all presentation attributes whose value is not null. It allows you to set attributes of this subelement's SVGElement dynamically based on its reference bounding box (refBBox, explained below) - for example, width, height, d, points, rx, ry or transform.

The function is expected to return an object or a value specifying the value(s) to be assigned to this subelement's SVGElement:

  • object - If an object is returned, its keys are interpreted as names of attributes (in kebab-case) on this subelement's SVGElement, and its values are the values to be assigned to those attributes.
  • string | number - If a non-object value is returned, it is assigned to the attribute matching the name of this custom special presentation attribute on this subelement's SVGElement.
  • void - If nothing is returned, no value is assigned on this subelement's SVGElement.

The function is provided with the following arguments, which you may use in your logic:

  • value is the value which was provided to the custom special presentation attribute within the current subelement's attrs object.
  • refBBox is a rectangle describing the coordinates (in terms of x, y, width and height) within which this subelement's SVGElement is going to be rendered. If no ref attribute is specified on the current subelement (the default), it is the current Cell's computed bounding box (more efficient). Otherwise, it is the measured bounding box of the SVGElement referenced by the ref attribute (less efficient).
  • node is this subelement's SVGElement.
  • attrs is the current subelement's attrs object (where the keys are names of attributes in kebab-case and values are the values provided to these attributes).
  • cellView is the current CellView, for context. If you need to refer to a model attribute in your logic, you can find it inside the cellView.model.attributes object - see below for details.

If no set() callback is provided, the attributes of the subelement's SVGElement are not changed.

Example...

In this example, the 'shape' custom special presentation attribute sets or unsets the 'd' path attribute of the subelement's SVGElement based on the provided value ('rectangle', 'circle', 'ellipse', or 'rhombus') and the reference bounding box.

You can see it in action below.

static attributes = {
'shape': {
set: function(value, refBBox, _node, _attrs, _cellView) {
const {width: w, height: h} = refBBox;
const s = Math.min(w, h);
switch (value) {
case 'rectangle':
return { 'd': `M 0 0 h ${w} v ${h} h ${-w} Z` };
case 'circle':
return { 'd': `M ${w/2} ${h/2} m ${s/2} 0 a ${s/2} ${s/2} 0 1 0 ${-s} 0 a ${s/2} ${s/2} 0 1 0 ${s} 0` };
case 'ellipse':
return { 'd': `M ${w} ${h/2} a ${w/2} ${h/2} 0 1 0 ${-w} 0 a ${w/2} ${h/2} 0 1 0 ${w} 0` };
case 'rhombus':
return { 'd': `M 0 ${h/2} L ${w/2} 0 L ${w} ${h/2} L ${w/2} ${h} Z` };
}
throw new Error('Unknown shape value.');
},
unset: 'd'
}
}

unset()โ€‹

[optional] A function called by JointJS for all presentation attributes whose value is null. It allows you to undo attribute modifications performed by the set() callback.

For a given Cell instance, the unset() callback applies only to attributes whose values were set by a special presentation attribute. If the unset() callback mentions a presentation attribute that was also set explicitly on the Cell instance (e.g. via the attrs object or the cell.attr() method), that specific attribute is not removed.

note

The unset property does not need to be specified as a callback - you may provide a string or an array of strings as a value directly. All logic which applies to the unset() callback output applies to these directly-provided values as well.

The function is expected to return a string or an array of strings specifying the attribute(s) to be removed from this subelement's SVGElement:

  • string - If a string is returned, it is interpreted as the name of the attribute (in kebab-case) to be removed from this subelement's SVGElement (if present).
  • string[] - If an array of strings is returned, the provided strings are interpreted as names of attributes (in kebab-case) to be removed from this subelement's SVGElement (if present).

The function is provided with the following arguments, which you may use in your logic:

  • node is this subelement's SVGElement.
  • attrs is the current subelement's attrs object (where the keys are names of attributes in kebab-case and values are the values provided to these attributes).
  • cellView is the current CellView, for context. If you need to refer to a model attribute in your logic, you can find it inside the cellView.model.attributes object - see below for details.

If no unset() callback is provided (or if the provided callback returns null or void), JointJS removes the attribute matching the name of this custom special presentation attribute from this subelement's SVGElement (if present).

Example...

In this example, the 'line-style' custom special presentation attribute sets the 'stroke-dasharray' attribute of the subelement's SVGElement based on the provided value ('dashed', 'dotted', or 'none' as the default) and the attrs['stroke-width'] value. Conversely, when the attribute is unset, we need to make sure that this automatically-assigned value is unset as well.

You can see it in action below.

static attributes = {
'line-style': {
set: function(value, _refBBox, _node, attrs, _cellView) {
const n = attrs['stroke-width'] || 1;
switch (value) {
case 'dashed':
return { 'stroke-dasharray': `${4 * n},${2 * n}` }
case 'dotted':
return { 'stroke-dasharray': `${n},${n}` }
default:
return { 'stroke-dasharray': 'none' }
}
},
unset: 'stroke-dasharray'
}
}

position()โ€‹

[optional] A function called by JointJS after the set() callback is evaluated (and used to lock down the transformations of this subelement's SVGElement) for all presentation attributes. It allows you to adjust the x and y coordinates of the anchor of this subelement's SVGElement dynamically based on its reference bounding box (refBBox, explained below), which allows you to define your own relative positioning logic.

The function is expected to return an object with x and/or y properties specifying the offset (shift/displacement) of the anchor point of the subelement's SVGElement within the coordinate system of refBBox.

note

The anchor point is different for different SVGElement types:

  • Most often, it is the top-left corner of the element (e.g. <rect>).
  • For <circle> and <ellipse> elements, it is the center of the element.
  • For <text> elements, it is the starting point of the baseline.

The function is provided with the following arguments, which you may use in your logic:

  • value is the value which was provided to the custom special presentation attribute within the current subelement's attrs object.
  • refBBox is a rectangle describing the coordinates (in terms of x, y, width and height) within which this subelement's SVGElement is going to be rendered. If no ref attribute is specified on the current subelement (the default), it is the current Cell's computed bounding box (more efficient). Otherwise, it is the measured bounding box of the SVGElement referenced by the ref attribute (less efficient).
  • node is this subelement's SVGElement.
  • attrs is the current subelement's attrs object (where the keys are names of attributes in kebab-case and values are the values provided to these attributes).
  • cellView is the current CellView, for context. If you need to refer to a model attribute in your logic, you can find it inside the cellView.model.attributes object - see below for details.

If no position() callback is provided, the coordinates of the anchor of this subelement's SVGElement are not changed.

Example...

In this example, the 'placement' custom special presentation attribute positions the anchor point of subelement's SVGElement within refBBox based on the provided value.

note

For <rect> subelements, this means that the top-left point is positioned at the specified position within the refBBox, since the top-left point is the default anchor point. Meanwhile, for <circle> subelements, this means that the center point is positioned at the specified position within the refBBox, since the center point is the default anchor point.

If you need to adjust the anchor point as well - e.g. to position the center point of a <rect> at the center point of the refBBox - you can do so by applying an additional offset() callback.

static attributes = {
'placement': {
position: function(value, refBBox, _node, _attrs, _cellView) {
switch (value) {
case 'middle':
return {
x: refBBox.width / 2,
y: refBBox.height / 2,
}
case 'top':
return {
x: refBBox.width / 2,
y: 0
}
case 'right':
return {
x: refBBox.width,
y: refBBox.height / 2
}
case 'bottom':
return {
x: refBBox.width / 2,
y: refBBox.height
}
case 'left':
return {
x: 0,
y: refBBox.height / 2
}
}
throw new Error('Unknown placement value.');
}
}
}

offset()โ€‹

info

Only evaluated when the subelement's SVGElement is rendered (i.e. its bounding client rect's width and height are not 0).

JointJS needs to measure the size of the subelement's rendered SVGElement. For this reason, using the offset() callback can affect the performance of the application. It is recommended to use it only when necessary.

[optional] A function called by JointJS after the position() callback is evaluated for all presentation attributes. It allows you to adjust the x and y coordinates of the anchor of this subelement's SVGElement dynamically based on its own final measured bounding box (nodeBBox, explained below), which enables you to define your own horizontal/vertical alignment logic.

The function is expected to return an object with x and/or y properties specifying the offset (shift/displacement) of the anchor point of the subelement's SVGElement within the coordinate system of nodeBBox.

note

The anchor point is different for different SVGElement types:

  • Most often, it is the top-left corner of the element (e.g. <rect>).
  • For <circle> and <ellipse> elements, it is the center of the element.
  • For <text> elements, it is the starting point of the baseline.

The function is provided with the following arguments, which you may use in your logic:

  • value is the value which was provided to the custom special presentation attribute within the current subelement's attrs object.
  • nodeBBox is a rectangle describing the coordinates (in terms of x, y, width and height) of the final measured bounding box of this subelement's SVGElement.
  • node is this subelement's SVGElement.
  • attrs is the current subelement's attrs object (where the keys are names of attributes in kebab-case and values are the values provided to these attributes).
  • cellView is the current CellView, for context. If you need to refer to a model attribute in your logic, you can find it inside the cellView.model.attributes object - see below for details.

If no offset() callback is provided, the coordinates of the anchor of this subelement's SVGElement are not changed.

Example...

In this example, the 'alignment' custom special presentation attribute offsets the anchor point of subelement's SVGElement within its own measured bounding box based on the provided value. Effectively, a different point within the element is treated as the anchor point.

In a <rect> subelement, the top-left point is the anchor point. Providing middle as the value of the 'alignment' custom special presentation attribute would offset the top-left point such that the center point of the subelement ends up where the top-left point originally was.

note

To support SVGElement types with a different anchor point (e.g. <circle>), the x and y values returned by the callback would need to be adjusted.

static attributes = {
'alignment': {
offset: function(value, nodeBBox, node, _attrs, _cellView) {
const tagName = node.tagName.toUpperCase();
if (tagName === 'CIRCLE' || tagName === 'ELLIPSE' || tagName === 'TEXT') {
throw new Error('SVGElement types whose anchor point is not in top-left corner need different return values.');
}
switch (value) {
case 'middle':
return {
x: -nodeBBox.width / 2,
y: -nodeBBox.height / 2
}
case 'top':
return {
x: -nodeBBox.width / 2,
y: 0
}
case 'right':
return {
x: -nodeBBox.width,
y: -nodeBBox.height / 2
}
case 'bottom':
return {
x: -nodeBBox.width / 2,
y: -nodeBBox.height
}
case 'left':
return {
x: 0,
y: -nodeBBox.height / 2
}
}
throw new Error('Unknown alignment value.');
}
}
}

No callback functionsโ€‹

Not specifying any callback functions for a custom special presentation attribute is valid if the attribute is exclusively used to modify the behavior of other custom special presentation attribute(s). Empty custom special presentation attributes do nothing on their own.

Example...

In this example, the 'text-position' custom special presentation attribute relies on the value of the 'text-margin' custom special presentation attribute to calculate the correct text position, while 'text-margin' does nothing on its own (i.e. 'text-margin' does not set any attribute on the subelement's SVGElement):

static attributes = {
'text-margin': {},

'text-position': {
position: function(_val, refBBox, _node, attrs, _cellView) {
const textMargin = util.normalizeSides(attrs['text-margin']);
const { left, top, right, bottom } = textMargin;
const { width, height } = refBBox;
return {
x: width / 2 + left - right,
y: height / 2 + top - bottom
}
}
}
}

Referring to model attributesโ€‹

You can refer to values of model attributes inside all custom special presentation attributes callback functions via the cellView.model.get() function. This allows your callback logic to refer to any data stored on the cell - any custom data, as well as any built-in model attributes of Cell / Element / Link. For example:

static attributes = {
'my-fill': { set: function(/* ... */, cellView) { return { 'fill': cellView.model.get('color') }; }}
};

There is a caveat, however. When you reference a model attribute inside the logic of your custom special presentation attribute, JointJS does not automatically keep track of its changes. (By default, JointJS assumes that only changes in attrs and size model attributes are relevant for element presentation attributes. For link presentation attributes, the only relevant model attributes are assumed to be attrs, source, target, vertices, router and connector.) You need to tell JointJS that it needs to keep track of the (top-level) model attribute you are referencing as if it were another presentation attribute.

This is done via a custom cell view. You can add new presentationAttributes via the dia.ElementView.addPresentationAttributes() static method. In the provided object, the keys are the (top-level) model attributes you want JointJS to track, and values are flags which specify what JointJS should do if the value of the referenced model attribute changes. In this case, you should provide dia.ElementView.Flags.UPDATE, since that flag tels JointJS to re-evaluate presentation attributes.

Example...

In this example, the orderStatus model attribute is used to determine the fill color of the element:

import { dia, shapes, util } from '@joint/core';

const markup = util.svg/* xml */`
<rect @selector="body" cursor="pointer"/>
<text @selector="label"/>
`

class MyShape extends dia.Element {

preinitialize() {
this.markup = markup;
}

defaults() {
return {
...super.defaults,
type: 'MyShape',
size: { width: 120, height: 30 },
position: { x: 140, y: 50 },
attrs: {
body: {
automaticOrderStatusFill: true,
width: 'calc(w)',
height: 'calc(h)',
rx: 5,
ry: 5
},
label: {
textAnchor: 'middle',
textVerticalAnchor: 'middle',
x: 'calc(w/2)',
y: 'calc(h/2)',
fontSize: 14,
fill: 'white'
}
}
};
}

static attributes = {
'automatic-order-status-fill': {
set: function(_value, _refBBox, _node, _attrs, cellView) {
const orderStatus = cellView.model.prop('orderStatus');
switch (orderStatus) {
case 'pending':
return { 'fill': 'orange' }
case 'complete':
return { 'fill': 'green' }
case 'refunded':
return { 'fill': 'red' }
case 'processing':
return { 'fill': 'lightblue' }
case 'shipped':
return { 'fill': 'blue' }
}
throw new Error('Unknown orderStatus.');
},
unset: 'fill'
}
};
}

class MyShapeView extends dia.ElementView {

preinitialize() {
this.presentationAttributes = dia.ElementView.addPresentationAttributes({
'orderStatus': dia.ElementView.Flags.UPDATE
});
}
}

const namespace = {
...shapes,
MyShape,
MyShapeView
};

const graph = new dia.Graph({}, { cellNamespace: namespace });
const paper = new dia.Paper({
model: graph,
width: 400,
height: 200,
gridSize: 1,
interactive: true,
async: true,
frozen: false,
background: { color: '#F3F7F6' },
cellViewNamespace: namespace
});
document.getElementById('paper').appendChild(paper.el);

graph.fromJSON({
cells: [
{
type: 'MyShape',
},
{
type: 'MyShape',
position: { x: 140, y: 100 }
}
]
});

const [el0, el1] = graph.getElements();
el0.attr('label/text', 'Order #25183154').prop('orderStatus', 'pending'); // orange
el1.attr('label/text', 'Order #25183206').prop('orderStatus', 'complete'); // green

Exampleโ€‹

The following example shows two custom special presentation attributes in action:

  • 'line-style' sets or unsets the 'stroke-dasharray' attribute of the subelement's SVGElement based on the provided value ('dashed', 'dotted', or 'none' as the default) and the attrs['stroke-width'] value.
  • 'shape' sets or unsets the 'd' path attribute of the subelement's SVGElement based on the provided value ('rectangle', 'circle', 'ellipse', or 'rhombus') and the reference bounding box.