If you have been working with JointJS for any amount of time, you are probably familiar with presentation attributes.
These are the attributes which contribute to the visual aspect of our shape, and are mostly defined through
attrs
objects. While presentation attributes provide us with great functionality, occasionally we might want
to create our own attributes to help us work with our shape. This is where custom attributes come in useful. Maybe we
want a boolean to represent a state, a number for our threshold value, or just provide some attributes which represent our
specific problem, all of these could be possible use cases for a custom attribute.
To work with presentation attributes, we usually use the
attr()
method. However, when working with custom attributes, we recommend working with prop()
and set()
.
These methods will help you store custom data on the model, while at the same time also providing a nice separation between
custom and presentation attributes if needed.
Prop is used to set attributes on the element model. It can be used to set both custom and presentation attributes, and it also provides support for nesting making it very flexible. When setting an attribute, the first parameter is an object or string representation of our path, and when not using an object, the second parameter will be the value we wish to set. Prop will merge the properties you want to set with existing ones already present in the Cell.
element.prop('attrs/body/stroke', '#FFF'); // Set presentation attribute
element.prop('data', 10); // Set custom attribute with string
element.prop({ data: 10 }); // Set custom attribute with object
// Output from element.toJSON();
{
"type": "standard.Rectangle",
"position": { "x": 0, "y": 0 },
"size": { "width": 1, "height": 1 },
"angle": 0,
"id": "19cf14e6-cb78-4c32-b9a1-4256dc53776f",
"data": 10, // Our custom attribute
"attrs": {
"body": {
"stroke": "#FFF" // Our presentation attribute
}
}
}
element.prop('data/count', 10); // Set nested custom attribute with string
element.prop({ data: { count: 10 }}); // Set nested custom attribute with object
element.prop('data': { count: 10 }); // This also creates a nested custom attribute
{
"type": "standard.Rectangle",
"position": { "x": 0, "y": 0 },
"size": { "width": 1, "height": 1 },
"angle": 0,
"id": "19cf14e6-cb78-4c32-b9a1-4256dc53776f",
"data": {
"count": 10 // Our nested custom attribute
},
"attrs": {}
}
prop()
not only provides support for nested objects, but for nested arrays too.
element.prop('mylist/0/data/0/value', 50); // Set custom attribute as nested array
// Output from element.toJSON();
{
"type": "standard.Rectangle",
"position": { "x": 0, "y": 0 },
"size": { "width": 1, "height": 1 },
"angle": 0,
"id": "9adab5e5-cebe-419f-8d62-39cce5486d0d",
"mylist": [
{
"data": [
{
"value": 50 // Our nested custom attribute
}
]
}
],
"attrs": {}
}
Set is a method provided by backbone.js, and similarly to prop()
, it can be used to create custom data
attributes on the element model. Like prop()
, when setting an attribute, the first parameter can be an object
or string, but set()
doesn't provide nesting capability in the form of a string. That means any path representation
is considered to be one attribute. Again, when not using an object, the second parameter is the value we wish to set. Another
difference to take note of is that set()
will override attributes, while prop()
merges them.
element.set('data', 10); // Set custom attribute with string
element.set({ data: 10 }); // Set custom attribute with object
// Output from element.toJSON();
{
"type": "standard.Rectangle",
"position": { "x": 0, "y": 0 },
"size": { "width": 1, "height": 1 },
"angle": 0,
"id": "c0a6696d-1857-4fb7-892d-409433f84d29",
"data": 10, // Our custom attribute
"attrs": {}
}
element.set('data/count', 10); // We try to set a nested custom property using set()
// The output produced will not be nested as is the case when using prop()
{
"type": "standard.Rectangle",
"position": { "x": 0, "y": 0 },
"size": { "width": 1, "height": 1 },
"angle": 0,
"id": "afdc42ce-5b10-45e5-832e-afcf2a221314",
"data/count": 10, // Note the important difference here
"attrs": {}
}
We do provide some extra functionality when using prop, and that is to enable rewrite mode. To
enable rewrite mode, we simply use { rewrite: true }
as the 3rd argument in our prop() method.
This will replace the value referenced by the path with the new one. This differs from the default behaviour which is to
merge our properties.
rect.prop('custom/state/isCollapsed', true);
rect.prop('custom/state', { isActive: false });
// Output from element.toJSON();
// We can see our attributes have been merged
{
"type": "standard.Rectangle",
"position": { "x": 0, "y": 0 },
"size": { "width": 1, "height": 1 },
"angle": 0,
"id": "b1c02090-e46a-4d90-a5dc-5096f1559b9f",
"custom": {
"state": {
"isCollapsed": true,
"isActive": false
}
},
"attrs": {}
}
rect.prop('custom/state/isCollapsed', true);
rect.prop('custom/state', { isActive: false }, { rewrite: true });
// We can see our attributes have been overwritten
{
"type": "standard.Rectangle",
"position": { "x": 0, "y": 0 },
"size": { "width": 1, "height": 1 },
"angle": 0,
"id": "b1c02090-e46a-4d90-a5dc-5096f1559b9f",
"custom": {
"state": {
"isActive": false
}
},
"attrs": {}
}
Both of these methods function similarly, but there are a few small differences to be aware of. Internally, attr()
implements prop()
to process our attributes. Afterwards, the method places the presentation attributes within
the attrs
object. Separating attributes in this manner also provides our model with a nice semantic
and organizational divide between our custom and presentation properties.
In the following example, you can see both attr() and prop() in action. It would be possible to set both of these
attributes using prop(), but as mentioned above, both these methods achieve what we want in our example. We see that nice
separation between attributes, because after attr() implements prop(), it also
prepends our path with 'attrs'. This means we find our presentation attributes in the attrs
object.
element.attr('body/strokeWidth', 2);
element.prop('isCollapsed', true);
// Output from element.toJSON();
{
"type": "standard.Rectangle",
"position": { "x": 0, "y": 0 },
"size": { "width": 1, "height": 1 },
"angle": 0,
"id": "edafa2ac-27e6-4fbc-951a-aa3f9512c741",
"isCollapsed": true,
"attrs": {
"body": {
"strokeWidth": 2
}
}
}
Another important note to mention when talking about prop()
and attr()
is that when changing the model,
some useful information is passed along with the change event in JointJS. propertyPath
, propertyValue
,
and propertyPathArray
are all values which can be accessed when updating the model. This can prove useful if for some
reason you need to listen to a specific attribute change. Note that it is not possible to access these values in this manner
when using set()
.
graph.on('change', (cell, opt) => {
if ('attrs' in cell.changed) {
console.log(opt.propertyPathArray, 'was changed');
// --> ['attrs', 'body', 'fill'] 'was changed'
}
if ('isInteractive' in cell.changed) {
console.log(opt.propertyPathArray, 'was changed');
// --> ['isInteractive'] 'was changed'
}
});
element.attr('body/fill', 'cornflowerblue');
element.prop('isInteractive', true);
Thanks for reading this far. As you can see, custom attributes open up a new world of functionality within our shapes, and
don't get in the way of our presentation attributes which is nice. The most important take away is that
prop()
and set()
are the right set of tools to work with custom attributes. If you would like
to explore any of the features mentioned here in more detail, you can find our full JointJS documentation
here.