Cell Namespace

A simple, but important aspect of working with JointJS is to ensure that JointJS knows where to look for built-in and custom shapes. In order to achieve this, it's a requirement to tell JointJS where to read cell view definitions. Failure to do so will result in an error in our application. Built-in shapes are usually located in the joint.shapes namespace, so this is a common namespace to use. It's possible to add custom shapes to this namespace, or alternatively, you may like to use a different namespace completely. The choice is yours, but you need to state the namespace at the outset when using JointJS.

Let's begin by creating a simple, custom Rectangle definition which extends joint.dia.Element.

class Rectangle extends joint.dia.Element {
    defaults() {
        return {
            ...super.defaults,
            type: 'Rectangle',
            position: { x: 10, y: 10 },
            attrs: {
                body: {
                    width: 100,
                    height: 70
                }
            }
        };
    }

    preinitialize() {
        this.markup = joint.util.svg/* xml */ `
            <rect @selector="body" />
        `;
    }
}

We will also declare a variable which will contain our shapes, and act as our cell namespace.

// Built-in JointJS shapes and our custom Rectangle are added
const namespace = { ...joint.shapes, Rectangle };

If you want a little more organization and nesting in your cell namespace, you can define a type using dot notation, and structure your shape definitions how you would like.

class Rectangle extends joint.dia.Element {
    defaults() {
        return {
            ...
            type: 'custom.Rectangle',
            ...
        };
    }
    ...
}

const namespace = { ...joint.shapes, custom: { Rectangle }};

Now that we have created a cell namespace, how do we tell JointJS which namespace to use? There are 2 important options to be aware of when creating your diagrams. The first is the graph option cellNamespace, and the second is the paper option cellViewNamespace. In the following example, for a cell of type 'standard.Rectangle', the graph looks up the 'joint.shapes.standard.Rectangle' path to find the correct constructor. If you don't plan on creating custom shapes, or playing around with namespaces, the following setup should be fine for your application.

const namespace = joint.shapes;

const graph = new joint.dia.Graph({}, { cellNamespace: namespace });

const paper = new joint.dia.Paper({
    ...
    cellViewNamespace: namespace
    ...
});

graph.fromJSON({
    cells: [
        {
            type: 'standard.Rectangle',
            size: { width: 80, height: 50 },
            position: { x: 10, y: 10 }
        }
    ]
});

A More Detailed Look

With the intention of strengthening this concept in our minds, let's define another shape, so that we are more familiar with this process. Below, we create a class RectangleTwoLabels with a type property of 'custom.RectangleTwoLabels'. JointJS will now expect that our custom RectangleTwoLabels element will be located within the custom namespace.

As we want our custom namespace to be at the same level of nesting as built-in JointJS shapes, we will structure our cell namespace accordingly. First, we declare a namespace variable, then using the spread operator, ensure that namespace contains all of the properties of joint.shapes. These properties correspond to shape namespaces such as standard.

Afterwards, we also place our new custom namespace which contains our custom shape definition RectangleTwoLabels alongside our built-in shapes. As a result, standard and custom are both defined at the same level in our namespace object. Lastly, we make sure that namespace is set as the value of our cellNamespace and cellViewNamespace options respectively.

class RectangleTwoLabels extends joint.shapes.standard.Rectangle {
    defaults() {
        return {
            ...super.defaults,
            type: 'custom.RectangleTwoLabels'
        };
    }

    preinitialize() {
        this.markup = joint.util.svg/* xml */ `
            <rect @selector="body" />
            <text @selector="label" />
            <text @selector="labelSecondary" />
        `;
    }
}

const namespace = { ...joint.shapes, custom: { RectangleTwoLabels }};

const graph = new joint.dia.Graph({}, { cellNamespace: namespace });

new joint.dia.Paper({
    ...
    cellViewNamespace: namespace
    ...
});

With the objective of defining our custom namespace at the same nesting level of standard taken care of, it's now possible to add cells to our graph with the confidence that we shouldn't run into any errors regarding cell namespaces.

graph.fromJSON({
    cells: [
        {
            type: 'standard.Rectangle',
            size: { width: 100, height: 60 },
            position: { x: 50, y: 50 },
            attrs: { body: { fill: '#C9ECF5' }, label: { text: 'standard.Rectangle', textWrap: { width: 'calc(w-10)' }}}
        },
        {
            type: 'custom.RectangleTwoLabels',
            size: { width: 140, height: 80 },
            position: { x: 200, y: 30 },
            attrs: {
                body: {
                    fill: '#F5BDB0'
                },
                label: {
                    text: 'custom.RectangleTwoLabels',
                    textWrap: { width: 'calc(w-10)' }
                },
                labelSecondary: {
                    text: 'SecondaryLabel',
                    x: 'calc(w/2)',
                    y: 'calc(h+15)',
                    textAnchor: 'middle',
                    textVerticalAnchor: 'middle',
                    fontSize: 14
                }
            }
        },
    ]
});

JointJS source code: cell-namespace.js

Discovering your cell namespaces are not organized correctly should result in a common JointJS error. If you see the dreaded Uncaught Error: dia.ElementView: markup required appearing in your console, it's likely your namespace is not set up correctly, and JointJS cannot find the correct shape.

Don't Forget Custom Views!

Last but not least, the topics covered so far also apply to our custom views. Placing a custom view in the correct location is necessary, because the JointJS paper will search for any model types with a suffix of 'View' in our provided namespace.

In this snippet, we create a simple rectangle shape with text input. We also define a custom view that on user input, sets the input value on the model, and also logs the value to the console. This time around, we choose joint.shapes as our cellNamespace and cellViewNamespace values, and 'example.RectangleInput' as the type for our custom element. Those things combined mean JointJS assumes our custom element & view will be located at 'joint.shapes.example.RectangleInput' and 'joint.shapes.example.RectangleInputView' respectively.

const namespace = joint.shapes;

const graph = new joint.dia.Graph({}, { cellNamespace: namespace });

const paper = new joint.dia.Paper({
    ...
    cellViewNamespace: namespace,
    ...
});

class RectangleInput extends joint.dia.Element {
    defaults() {
        return {
            ...super.defaults,
            type: 'example.RectangleInput',
            attrs: {
                foreignObject: {
                    width: 'calc(w)',
                    height: 'calc(h)'
                }
            }
        };
    }

    preinitialize() {
        this.markup = joint.util.svg/* xml */`
            <foreignObject @selector="foreignObject">
                <div
                    xmlns="http:www.w3.org/1999/xhtml"
                    style="background:white;border:1px solid black;height:100%;display:flex;justify-content:center;align-items:center;"
                >
                    <input
                        placeholder="Type something"
                    />
                </div>
            </foreignObject>
        `;
    }
}

const RectangleInputView = joint.dia.ElementView.extend({

    events: {
        // Name of event + CSS selector : custom view method name
        'input input': 'onInput'
    },

    onInput: function(evt) {
        console.log('Input Value:', evt.target.value);
        this.model.attr('name/props/value', evt.target.value);
    }
});

Object.assign(namespace, {
    example: {
        RectangleInput,
        RectangleInputView
    }
});

const rectangleInput = new RectangleInput();
rectangleInput.position(10, 10);
rectangleInput.resize(200, 120);
rectangleInput.addTo(graph);

Quick Validation Tips

If you are experimenting with cell namespaces, you may like to perform some quick validation, or double-check exactly what type values you are working with. Taking advantage of the prop() method on both Elements & Links allows you to quickly access the type value, so it can be useful to keep track of where your shapes are located.

const rect = new Rectangle({
    size: { width: 80, height: 50 },
    position: { x: 10, y: 10 }
});

console.log(rect.prop('type')); // standard.Rectangle

A concise way to check if your namespaces are set up correctly is to overwrite the graph via graph.toJSON() passing it the value returned from graph.toJSON(). If no error occurs, you can be more confident that your namespaces are organized correctly.

graph.fromJSON(graph.toJSON());

That's all we will cover in this tutorial. Thanks for staying with us if you got this far, and we hope you will have more confidence when working with cell namespaces in JointJS. If you would like to explore any of the features mentioned here in more detail, you can find more information in our JointJS documentation.