Custom Attributes

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.

The prop() method

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": {}
}

The set() method

Set is a method provided by mvc.Model, 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": {}
}

Overwriting attributes with prop()

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": {}
}

Relationship between prop() and attr() methods

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.