JointJS+ Inspector

ui.Inspector

Inspector plugin creates an editor and viewer of cell model properties. It creates a two-way data-binding between the cell model and a generated HTML form with input fields. These input fields can be defined in a declarative fashion using a plain JavaScript object. Not only visual attributes can be configured for editing in the inspector but also custom data that will be set on the cell model! This all makes the inspector an extremely flexible and useful widget for use in applications.

Install

Include joint.ui.inspector.css and joint.ui.inspector.js files to your HTML:

<link rel="stylesheet" type="text/css" href="joint.ui.inspector.css"/>
<script src="joint.ui.inspector.js"></script>

Create an inspector

You can create the Inspector instance as follows:

var inspector = new joint.ui.Inspector(options).render();
inspector.$el.appendTo($('body'))

Usually the applications display only one inspector at the time, which is used for various application models and differs in the configuration of input fields only. For this common use-case we've introduced a static helper - joint.ui.Inspector.create(). It makes sure the previous instance (if there is any) is properly removed, it creates a new one and renders it into the DOM. It also keeps track of open/closed groups and restores them based on the last used state. It can be used as follows:

joint.ui.Inspector.create('#inspector', options);

For more information about the create method visit the Inspector API chapter.

paper.on('element:pointerdown', function(elementView) {
    // open the inspector when the user interacts with an element
    joint.ui.Inspector.create('#inspector', {
        cell: cellView.model,
        inputs: {
            attrs: {
                circle: {
                    fill: {
                        type: 'color-palette',
                        options: [
                            { content: '#FFFFFF' },
                            { content: '#FF0000' },
                            { content: '#00FF00' },
                            { content: '#0000FF' },
                            { content: '#000000' }
                        ],
                        label: 'Fill color',
                        group: 'presentation',
                        index: 1
                    },
                    stroke: {
                        type: 'color-palette',
                        options: [
                            { content: '#FFFFFF' },
                            { content: '#FF0000' },
                            { content: '#00FF00' },
                            { content: '#0000FF' },
                            { content: '#000000' }
                        ],
                        label: 'Outline color',
                        group: 'presentation',
                        index: 2
                    },
                    'stroke-width': {
                        type: 'range',
                        min: 0,
                        max: 50,
                        unit: 'px',
                        label: 'Outline thickness',
                        group: 'presentation',
                        index: 3
                    }
                },
                text: {
                    text: {
                        type: 'textarea',
                        label: 'Text',
                        group: 'text',
                        index: 1
                    },
                    'font-size': {
                        type: 'range',
                        min: 5,
                        max: 30,
                        label: 'Font size',
                        group: 'text',
                        index: 2
                    },
                    'font-family': {
                        type: 'select',
                        options: ['Arial', 'Times New Roman', 'Courier New'],
                        label: 'Font family',
                        group: 'text',
                        index: 3
                    }
                }
            }
        },
        groups: {
        presentation: {
            label: 'Presentation',
            index: 1
        },
        text: {
            label: 'Text',
            index: 2
        }
        }
    });
});

Configuration

There are two ways to create an instance of the inspector. The first option is to use the joint.ui.Inspector.create(container, options) static function (where container is a CSS selector of an HTML element on your page). The second option is to directly create an instance with new joint.ui.Inspector(options). The inspector can be configured by the options object with the following properties:

cellView joint.dia.CellView (Mandatory - alternative 1) An ElementView or LinkView which you want to inspect. (Mutually exclusive with cell option.)
cell Backbone.Model (Mandatory - alternative 2) An arbitrary Backbone model which you want to inspect (i.e. an Element, Link or Graph). (Mutually exclusive with the cellView option.)
inputs object An object that mimics the structure of a cell model. Instead of the final values, it contains definitions of input fields. (The input field is in charge of setting the user input on the specified cell model.) See below for further explanation.
groups object An object that contains group identifiers as keys and group options as values. Each group may contain any number of input fields in the inspector panel. The user can show/hide the whole group by clicking a toggle button. See below for further explanation.
live boolean

Should the Inspector update the cell properties immediately (in reaction to an "onchange" event triggered on a form input)? The default is true.

If you need to prevent the Inspector from updating cell properties immediately when the user leaves their corresponding input field (for example, if you need to update cell properties only in reaction to the user pressing an Update button somewhere in your application), set this option to false instead. You then need to call the `updateCell()` function manually when appropriate.

multiOpenGroups boolean

Is the user allowed to have multiple groups opened in the Inspector at the same time? The default is true.

For the classical accordion type of Inspector (only one open group at a time), set this option to false.

container string | Element | jQuery  A CSS selector, DOM element or jQuery object that is the container element which the select-box or 'color-palette' options is appended to.
validateInput(element, path, type, inspector) function

A callback function, called by Inspector fields to check whether user input was valid. The function should return true if the input had a valid value, and false if it did not. If false is returned, the input field does not save the input value to the cell model. See the Validation chapter for more information.

The callback function is passed four arguments:

  • element - a reference to the <input> HTMLElement the user interacted with
  • path - path to the input field within the Inspector's inputs object ('/' separated)
  • type - the type of the input field, as defined in its options object
  • inspector - a reference to the current Inspector instance (for context)
The default function checks the validity property of element:
function(element, path, type, inspector) {
    return (element.validity ? element.validity.valid : true);
}
renderFieldContent(options, path, value, inspector) function

A callback function that returns an HTML, DOM element or jQuery object that will be appended by the Inspector into the space reserved for the field. In other words, this function allows you to define custom fields.

The function is passed four arguments:

  • options - the object provided in the inputs option of the Inspector as a definition of the field
  • path - path to the input field within the Inspector's inputs object ('/' separated)
  • value - the value read from the cell property at the corresponding path at the time of the rendering of the field. It also takes into account defaultValue and valueRegExp from the options object defined for this field
  • inspector - a reference to the current Inspector instance (for context)
If the function is defined but returns undefined in some cases, the Inspector will try to understand the field as if it were one of the built-in field types.
getFieldValue(attribute, type, inspector) function

A callback function that returns an object of the form { value: [value read from a custom field] }. This function is especially useful in combination with the renderFieldContent() option; it allows the Inspector to understand custom fields.

The function is passed three arguments:

  • attribute - the DOM element container of the Inspector field (i.e. the value returned by renderFieldContent(), if used)
  • type - the type of the field, as defined in the field's options object
  • inspector - a reference to the current Inspector instance (for context)
renderLabel(options, path, inspector) function

A callback function that returns an HTML, DOM element or jQuery object that will be appended by the Inspector into the space reserved for field label. In other words, this function allows you to define custom labels.

The function is passed three arguments:

  • options - the object provided in the inputs option of the Inspector as a definition of the field
  • path - path to the input field within the Inspector's inputs object ('/' separated)
  • inspector - a reference to the current Inspector instance (for context)
focusField(options, path, element, inspector) function

A callback function called by Inspector when trying to focus an element. This function allows you to define the focus behaviour for custom fields.

The function is passed three arguments:

  • options - the object provided in the inputs option of the Inspector as a definition of the field
  • path - path to the input field within the Inspector's inputs object ('/' separated)
  • element - the rendered HTMLElement of this field
  • inspector - a reference to the current Inspector instance (for context)
stateKey(model) function

A callback function that should return a unique identifier for saving/restoring the state of the groups (i.e. which groups are opened and which ones are closed). The default function returns the id of the current Element, i.e. every Element instance has its own group state. (So, Inspectors opened on different instances of one Element type will not remember previously opened/closed groups.)

An alternative method, function(model) { return model.get('type'); } would store state per element type. Then, Inspectors of all basic.Rect Elements would share the same group state.

storeGroupsState boolean (Applicable only when used with the create method) Should group state be saved? (That is, should the Inspector remember which groups were opened and which groups were closed, when it is reopened?) The default is true. (Group state can be restored by the restoreGroupsState option when the Inspector is created. It defaults to true. See below.)
restoreGroupsState boolean (Applicable only when used with the create method) Should previous group state be restored (if any group state had been saved)? The default is true.
updateCellOnClose boolean (Applicable only when used with the create method) Should the current inspector values be saved to the cell when a new inspector is about to be created? The default is true.

Options properties storeGroupsState / restoreGroupsState are applicable only if they are passed into the static create() method. Otherwise, you can use the API methods storeGroupsState() and restoreGroupsState() to manually manipulate group states.

Inputs Configuration

The inputs object is extremely important. Its structure mimics the structure of properties of the cell model. Instead of the final values, it contains definitions of input fields. (The input field is in charge of setting the user input on the specified cell model.)

The options object of inputs can contain the following parameters:

type string

(Mandatory) The type of the input field. The supported types are:

'number'

creates an HTML 5 number input field. Special properties are:

property default description
min -Infinity The minimum value that is acceptable and valid for the input.
max Infinity The maximum value that is acceptable and valid for the input.
'text'

creates a text input field.

'textarea'

creates a textarea.

'content-editable'

creates a content-editable div (resize automatically as user types). Special properties are:

property default description
html true Is the content HTML or plain text?
readonly false Makes the field not mutable. The user can not edit the content.
'range'

creates an HTML 5 range input field. Special properties are:

property default description
min -Infinity The minimum value that is acceptable and valid for the input.
max Infinity The maximum value that is acceptable and valid for the input.
step 1 A number that specifies the granularity that the value must adhere to.
unit "" The name of the unit displayed next to the value.
'color'

creates an HTML 5 color input field.

'select'

creates a select box. Special properties are:

property default description
options [] An array that contains the options for the select box
multiple false One or more values can be selected.
size options.length When multiple is specified, it is the number of rows in the list that should be visible at one time.
workdays: {
  type: 'select',
  options: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
  multiple: true,
  overwrite: true,
  group: 'occupation'
}
'toggle'

creates a toggle (checkbox).

'object'

creates inputs for the properties of an object.

'list'

creates a widget for adding/removing items from an array.

property default description
addButtonLabel "+" The label of the add list item button.
removeButtonLabel "-" The label of the remove list item button.
label string Label for the form input.
group string Group the input belongs to.
index number Index of the input within its group.
defaultValue any The value that will be used in the input field in case the associated cell property is undefined.
valueRegExp string A regular expression used to extract (and set) a property value on the cell. Use in combination with the defaultValue option to make sure the Inspector does not try to extract something from an undefined value.
overwrite boolean Should the input value overwrite the current contents of the cell? The default is false, which means that the input attributes are merged with the model's current attributes. Example:
// overwrite: false (default)
Model: { existingProperty: 'value1' }
Input: { newProperty: 'value2' }
=>
Model: { newProperty: 'value2', existingProperty: 'value1' }

// overwrite: true
Model: { existingProperty: 'value1' }
Input: { newProperty: 'value2' }
=>
Model: { newProperty: 'value2' }
options

Specific to several input types ('select', 'select-box', 'select-button-group', 'color-palette'). Can be defined in several different ways:

array<string> A simple list of string values. For example:
['option1', 'option2']
array<object> A list of value/content objects. For example:
// select & select-box
[
    { value: 'value1', content: 'option1' },
    { value: 'value2', content: 'option2' }
]
// color-palette
[
    { content: 'red', icon: 'image1.svg' },
    { content: 'blue' }
]
// select-button-group
[
    {
        value: 'value1',
        content: 'option1',
        buttonWidth: 20,
        icon: 'image.png',
        iconSelected: 'image2.png',
        iconWidth: 20,
        iconHeight: 20
    }
]
string A string path pointing to a cell property (which contains an array of options). For example:
'path'
=>
cell.prop('path')
=>
['option1', 'option2']
min number
The minimum value of a range input. Specific to the 'range' input type.
The minimum number of items in an array. Specific to the 'list' input type.
max number
The maximum value of a range input. Specific to the 'range' input type.
The maximum number of items in an array. Specific to the 'list' input type.
properties object An object containing definitions of the properties of an object. Specific to the 'object' type.
item object A definition of a generic item of a list. Specific for the 'list' type.
attrs object An object of the form <selector>: { <attributeName>: <attributeValue>, ... }. This object allows you to set arbitrary HTML attributes on the generated HTML for this input field. Useful if you want to mark certain input fields or store some additional content in them (for example, to display a tooltip).
when object An object containing conditions that determine whether this input should be shown or hidden based on the values of other inputs. For more information see chapter expressions.
previewMode boolean Should preview mode be enabled on the widget? The default is false. Specific to several input types ('select-box', 'select-button-group', 'color-palette').

In preview mode, when the user hovers over one of the items in the widget (e.g. it being a dropdown item in the case of the 'select-box' type), the Inspector still changes the connected model but triggers the change with the dry flag set to true. You can then react to this by using:

myCell.on('change:myProperty', function(cell, change, opt) {
    if (opt.dry) {
        /* do something when in preview mode */
    } else {
        sendToDatabase(JSON.stringify(graph));
    }
});

This is useful, for example, if your application wants to reflect the values of the hovered items in the diagram but does not want to store the change to the database.

Note that the 'list' and 'object' types allow you to create input fields in the Inspector for arbitrary nested structures. If your cell contains an object as a property (cell.set('myobject', { first: 'John', last: 'Good' })), you can instruct the inspector to use the 'object' type for myobject property and then define types for each of the nested properties of that object:

var inspector = new joint.ui.Inspector({
    cellView: cellView,
    inputs: {
        myobject: {
            type: 'object',
            properties: {
                first: { type: 'text' },
                last: { type: 'text' }
            }
        }
    },
    groups: {}
});

Similarly, if your cell contains a list as a property (cell.set('mylist', [{ first: 'John', last: 'Good' }, { first: 'Jane', last: 'Good' }])), you can instruct the inspector to use the 'list' type for mylist property and then define the type of the list item. Importantly, the 'list' input type enables users to add and remove list items. In our example, the mylist item contains a nested object, so we need to define its properties as well:

var inspector = new joint.ui.Inspector({
    cellView: cellView,
    inputs: {
        mylist: {
            type: 'list',
            item: {
                type: 'object',
                properties: {
                    first: { type: 'text' },
                    last: { type: 'text' }
                }
            }
        }
    },
    groups: {}
});

Groups Configuration

Each group options object in the groups object can contain the following parameters:

label string A label for the group. This label will be displayed as a header of the group section in the accordion-like inspector.
index number An index of the group relative to other groups. Use this to put the groups in a certain order.
closed boolean If set to true, the group will be closed by default.
when object An object containing conditions that determine if this group should be shown or hidden based on the values of other inputs. For more information see chapter expressions.

Expressions

The inspector relies on expressions defined in the when parameter to switch the visibility of an input field based on the values of other inputs. Whenever an input field's expression is evaluated to false (meaning the condition is not met), the input field is hidden. Otherwise, the input field is shown.

Definition of Expressions

When evaluated in a model context, expressions return a boolean (true/false) based on the value of specified model attributes. Expressions are defined recursively as follows:

  • { <primitive>: { <path>: <value> }, <*options> } is an expression
  • { <unary-operator>: expression, <*options> } is an expression
  • { <multiary-operator>: [expression-1, expression-2, ..., expression-n], <*options> } for n > 0 is an expression

...where:

pathIs a string determining a property of the model (e.g 'attrs/text/text', 'property', 'myobject/nestedProperty', 'mylist/${index}', 'mylist/${index}/nestedProperty')
valueIs a number, string or an array (e.g 13, 'jointjs', [1, 3, 5])
*options

(Optional) Additional options that affect the evaluation of the expression:

otherwise

(Optional) What should happen if the expression evaluates to false?

unsetIf true, the input field is cleared when hidden. If the field has a defaultValue specified, the field is reverted to that value. (By default, when an input field is hidden, it remembers its user-submitted content and presents it again whenever it becomes visible.)
dependencies(Optional) An array of property paths on inspector cell which are necessary for the evaluation of custom operators in the expression. See the relevant chapter for more information.
primitive

Can be one of the following simple operators:

eqreturns true if the value at <path> equals <value> (using == internally)...
equalreturns true if the value at <path> equals <value> (using _.isEqual() internally)...
nereturns true if the value at <path> doesn't equal <value> (using != internally)...
regexreturns true if the value at <path> matches a regular expression described in <value>...
textreturns true if the value at <path> contains <value> as a substring...
ltreturns true if the value at <path> is less than <value>...
ltereturns true if the value at <path> is less than or equal to <value>...
gtreturns true if the value at <path> is greater than <value>...
gtereturns true if the value at <path> is greater than or equal to <value>...
inreturns true (only if <value> is an array) if the value at <path> is an element in <value> array...
ninreturns true (only if <value> is an array) if the value at <path> is not an element in <value> array...

...and returns false otherwise.

unary_operator

Accepts exactly one expression

notreturns the negation of the provided expression
multiary_operator

Accepts an array of at least one expression

and returns a conjunction of all the provided expressions (i.e. expr1 && expr2 && expr3)
or returns a disjunction of all the provided expressions (i.e. expr1 || expr2 || expr3)
nor returns the negation of a disjunction of all the provided expressions (i.e. !(expr1 || expr2 || expr3))

Examples on using Expressions

Here are a few valid expressions:

{ eq: { 'size/width': 300 }}
{ regex: { 'attrs/text/text' : 'JointJS|Rappid' }}
{ lt: { 'count': 10 }}
{ in: { 'index': [0,2,4] }}
{ not: { eq: { 'day': 'Monday' }}}
{ and: [{ gte: { 'position/x': 100 }}, { lte: { 'position/x': 400 }}]}

Imagine a scenario where you have a 'select' input field with options 'email' and 'tel'. Below this input field, you want to show either a text field or a number input field, based on the selected option. Assuming your cell properties structure is as follows: { contact_option: 'email', contact_email: '', contact_tel: '' }, your inspector could look like this:

var inspector = new joint.ui.Inspector({
    cell: mycell,
    inputs: {
        contact_option: { type: 'select', options: ['email', 'tel'] },
        contact_email: { type: 'text', when: { eq: { 'contact_option': 'email' }}},
        contact_tel: { type: 'number', when: { eq: { 'contact_option': 'tel' }}}
    }
});

It is also possible to refer to input fields inside nested objects, by using more complicated paths:

var inspector = new joint.ui.Inspector({
    cell: mycell,
    inputs: {
        user_info: {
            type: 'object',
            properties: {
                contact_option: { type: 'select', options: ['email', 'tel'] },
                name: { type: 'text'}
            }
        },
        contact_email: { type: 'text', when: { eq: { 'user_info/contact_option': 'email' }}},
        contact_tel: { type: 'number', when: { eq: { 'user_info/contact_option': 'tel' }}}
    }
});

It does not make sense to reference list items from the outside, but it does make sense to reference sibling input fields within a list item's when clause. To do that, a wildcard ('${index}') has to be placed within the path - it will be dynamically substituted for the actual index of the item inside which the when clause is being evaluated:

var inspector = new joint.ui.Inspector({
    cell: mycell,
    inputs: {
        user_list: {
            type: 'list',
            item: {
                type: 'object',
                properties: {
                    contact_option: { type: 'select', options: ['email', 'tel'] },
                    contact_email: { type: 'text', when: { eq: { 'user_list/${index}/contact_option': 'email' }}},
                    contact_tel: { type: 'number', when: { eq: { 'user_list/${index}/contact_option': 'tel' }}}
                }
            }
        }
    }
});

It is also possible to toggle groups with when expressions.

var inspector = new joint.ui.Inspector({
    groups: {
        first: { label: 'F' },
        second: { label: 'S', when: { eq: { 'attribute2': true }}}
    }
});

Custom Operators in Expressions

As you can see above, ui.Inspector provides a good list of useful built-in primitive operators (eq, lt, in, ...). However, sometimes this is not enough and applications have special requirements for when fields in the inspector should be hidden/displayed based on other information. To meet this requirement while still taking advantage of the inspector configurability through expressions, ui.Inspector provides a way to define your own custom operators.

First, the custom primitive operator has to be defined inside the operators array option on the Inspector. Each operator definition is an object of the form: { custom-primitive: function(cell, value, *arguments) }. The provided callback function should return true when the operator condition is successful, and false otherwise. The cell parameter is the cell associated with the Inspector, value is the value of the field at the path specified in the when clause (see below), and *arguments is anything that was passed to the operator (see below).

Second, in the when clause of a definition within the Inspector's inputs object, an expression with a custom operator has to be defined according to the following format:

  • { <custom-primitive>: { <path>: <*arguments> }, <*options> }

The custom primitive expression can be used as an operand in unary and multiary operators, the same as if it was a built-in primitive expression.

For example, let's say you want to show an inspector field only when a value of another input field is longer (has more characters) than the value of a numeric property set on the associated inspector cell:

var inspector = new joint.ui.Inspector({
    cell: mycell,
    inputs: {
        title: { type: 'text' },
        description: { type: 'text', when: { longerThan: { 'title': 'titleThreshold' } } },
    },
    operators: {
        longerThan: function(cell, value, prop, valuePath) {
            // value === contents of 'title' input field
            // prop === 'titleThreshold'
            // valuePath === 'title'
            return (value ? (value.length > cell.prop(prop)) : false);
        }
    }
});

The example above displays the description field only when the content of title is longer than a numeric threshold which we have stored in a property on the cell model named titleThreshold. Now whenever the user types within the 'title' input field in the inspector and the text becomes longer than cell.get('titleThreshold'), the description field appears (and vice versa, if the text becomes shorter than titleThreshold, the description field gets hidden).

However, the example above has a small problem. If the value of the titleThreshold property changes on the cell model (e.g. due to some other change somewhere else in the application), that change is not taken into account by the expression. In order to fix this, we have to tell the inspector that there are prop dependencies that could affect the resolution of the expression in the when clause - we do that by providing a dependencies list inside the when clause. Here's the fixed version of the code provided above:

var inspector = new joint.ui.Inspector({
    cell: mycell,
    inputs: {
        title: { type: 'text' },
        description: {
            type: 'text',
            when: {
                longerThan: { 'title': 'titleThreshold' },
                dependencies: ['titleThreshold']
            }
        },
    },
    operators: {
        longerThan: function(cell, value, prop, valuePath) {
            // value === contents of 'title' input field
            // prop === 'titleThreshold'
            // valuePath === 'title'
            return (value ? (value.length > cell.prop(prop)) : false);
        }
    }
});

Validation

The following example shows how to reflect a custom shape's validation functions inside your Inspector. The inspector definition provides a custom validateInput method. That method then refers to Shape's custom validateProperty method, which uses some common regex validators. Notice that this architecture makes the Shape (the model instance) responsible for accepting/rejecting user input data, not the Inspector. As such, this arrangement manages to fulfill one of the goals of the JointJS framework, namely the separation of Model-View-Controller components from each other.

(function(joint, Shape) {

    joint.setTheme('modern');

    var paper = new joint.dia.Paper({
        el: document.getElementById('paper'),
        width: 500,
        height: 500
    });

    var shape = new Shape();
    shape.position(150,50);
    shape.size(200,200);
    shape.addTo(paper.model);

    var inspector = joint.ui.Inspector.create('#inspector', {
        cell: shape,
        inputs: shape.getInspectorDefinition(),
        validateInput: function(el, path, type, inspector) {
            var $el = $(el);
            var value = inspector.parse(type, inspector.getFieldValue(el, type), el);
            $el.removeClass('error').parent().find('error').remove();
            var error = shape.validateProperty(path, value);
            if (error) {
                var $error = $('<error/>').text(error);
                $el.addClass('error').before($error);
            }
            return !error;
        }
    });

    // run the first validity check
    inspector.updateCell();

})(joint, joint.dia.Element.define('Shape', {

    attrs: {
        body: {
            refWidth: '100%',
            refHeight: '100%',
            fill: '#dddddd',
            stroke: 'lightblue',
            strokeWidth: 2
        }
    },

    phoneNumber: '',
    emailAddress: 'org@client.io'

}, {

    markup: [{
        tagName: 'rect',
        selector: 'body'
    }],

    REGEX_PHONE_NUMBER: /^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$/,
    REGEX_HEXCOLOR: /^#([a-f0-9]{3}){1,2}\b/i,
    REGEX_EMAIL_ADDRESS: /^\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}$/,

    validateProperty: function(path, value) {
        switch (path) {
            case 'attrs/body/stroke':
            case 'attrs/body/fill':
                if (this.REGEX_HEXCOLOR.test(value)) break;
                return 'Invalid Color (e.g. #ff0000)';
            case 'attrs/body/strokeWidth':
                if (_.isNumber(value) && value >= 0) break;
                return 'Invalid Stroke Width (A positive number)';
            case 'phoneNumber':
                if (this.REGEX_PHONE_NUMBER.test(value)) break;
                return 'Invalid Phone Number (e.g. 123-456-7890)';
            case 'emailAddress':
                if (this.REGEX_EMAIL_ADDRESS.test(value)) break;
                return 'Invalid Email Address.';
        }
        return null;
    },

    getInspectorDefinition: function() {
        return {
            'attrs/body/fill': {
                type: 'text',
                label: 'Fill Color'
            },
            'attrs/body/stroke': {
                type: 'text',
                label: 'Stroke Color'
            },
            'attrs/body/strokeWidth': {
                type: 'number',
                label: 'Stroke Width'
            },
            'phoneNumber': {
                type: 'text',
                label: 'Phone Number'
            },
            'emailAddress': {
                type: 'text',
                label: 'Email Address'
            }
        };
    }

}));

Custom Fields

The Inspector has a useful built-in set of ready-to-use field types. However, in some cases, you might want to render your own custom fields (or to integrate a third party widget) while still taking advantage of the two-way data binding and the configuration options provided by Inspector. This can be done with two Inspector options: renderFieldContent(options, path, value) and getFieldValue(attribute, type).

The following example shows how to render two buttons in a single custom field and how to integrate the Select2 widget for advanced select boxes:

function createInspector(cellView) {

    joint.ui.Inspector.create('.inspector-container', {
        cellView: cellView,
        inputs: {
            attrs: {
                label: {
                    style: {
                        textDecoration: {
                            label: 'Text Style',
                            type: 'select2',
                            group: 'text-decoration',
                            options: ['none', 'underline', 'overline', 'line-through']
                        }
                    },
                    text: {
                        label: 'Text Content',
                        type: 'my-button-set',
                        group: 'text'
                    }
                }
            }
        },
        groups: {
            'text-decoration': { label: 'Text Decoration (Select2)' },
            'text': { label: 'Text' }
        },

        renderFieldContent: function(options, path, value, inspector) {

            switch (options.type) {

                case 'my-button-set':

                    var $buttonSet = $('<div/>');
                    var $yes = $('<button/>').text('Say YES!');
                    var $no = $('<button/>').text('Say NO!');
                    var $buttons = $('<div/>').append([$yes, $no]).css({
                        margin: 10,
                        display: 'inline-block'
                    });
                    var $label = $('<label/>').text(options.label || path);
                    $buttonSet.append([$label, $buttons]);
                    $buttonSet.data('result', value);
                    // When the user clicks one of the buttons, set the result to our field attribute
                    // so that we can access it later in `getFieldValue()`.
                    $yes.on('click', function() {
                        $buttonSet.data('result', 'YES');
                        inspector.updateCell($buttonSet, path, options);
                    });
                    $no.on('click', function() {
                        $buttonSet.data('result', 'NO');
                        inspector.updateCell($buttonSet, path, options);
                    });

                    return $buttonSet;

                case 'select2':

                    var $select = $('<select/>').width(170).hide();

                    $select.select2({ data: options.options }).val(value || 'none').trigger('change');
                    $select.data('select2').$container.css('margin', 10);
                    $select.on('change', function() {
                        inspector.updateCell($select, path, options);
                    });

                    return $('<div/>').append([
                        $('<label/>').text(options.label || path),
                        $select.data('select2').$container
                    ]);
            }
        },

        getFieldValue: function(attribute, type) {

            if (type === 'my-button-set') {
                return { value: $(attribute).data('result') };
            }

            if (type === 'select2') {
                const $select = $(attribute).find('.select2').data('element');
                return { value: $select.val() };
            }
        }
    });
}

source code

Custom Labels

It is also possible to customize the appearance and behavior of field labels in your Inspector. This can be used to create labels with custom HTML - as demonstrated by the myList label (an <a> tag with a <label> tag inside), which links to an address specified in myList.url. Additionally, our templating functionality can be used to define dynamic labels. We use this for myList.item elements; when an element is added to the array with the + button, the '{{index}}' placeholder is replaced with the element's actual index within myList.

joint.ui.Inspector.create('#container', {
    cell: model,
    inputs: {
        myList: {
            type: 'list',
            url: 'https://jointjs.com',
            item: {
                type: 'text',
                label: 'Item {{index}}'
            }
        }
    },
    renderLabel: function(opt, path) {
        // returns an HTMLElement (`$el`) as a custom label
        // this method is called for every element inside inspector when being rendered
        // in this case, it is called when the example loads, to create a label for the whole `myList`
        // it is also called whenever the + button is pressed, to create a label for each element added to `myList`

        var $label = document.createElement('label');
        var $el = $label;

        // List numbering:
        var text = opt.label;
        var indexPlaceholder = '{{index}}';
        // is this an inspector element with a `label` text specified (i.e. one of `myList` items)?
        // does this inspector element contain a substring to replace?
        if (text && text.indexOf(indexPlaceholder) > -1) {

            // every input field in the inspector is addressed via a `path`
            // elements added to `myList` are addressed as 'myList/0', 'myList/1', etc.
            // we can use this in a regex to identify list elements
            // we do this by checking if there is a '/' followed by a digit at the end of the path
            var match = path.match(/\/(\d+)$/);
            if (match) {

                // if this is a list element, use its index as index
                // (+1 to make sure the rendered labels start from 1)
                var index = parseInt(match[1], 10) + 1;
                // actually add the human-readable index to the label
                text = text.replace(indexPlaceholder, index);
            }
        }
        // then, set the text of the label to the text we generated

        // else: set it to the raw `path` string
        // (this happens for `myList` itself - so it gets a label that says 'myList')
        $label.textContent = text || path;

        // item labels are hidden via CSS by default, we need to unhide them
        $label.style.cssText = 'display:block !important;';

        // Clickable label:
        // is this an inspector element with an `url` specified (i.e. `myList` itself)?
        if (opt.url) {
            // then create a new <a> element and add <label> to it
            var $a = document.createElement('a');
            $a.href = opt.url;
            $a.target = 'blank';
            $a.appendChild($el);
            $el = $a;
        }

        // we have created a custom label
        return $el;
    }
});

Inspector Events

The Inspector object triggers events when the user changes its inputs or when the Inspector needs to re-render itself partially. These events can be handled by using the Inspector on() method.

render Triggered when the Inspector re-renders itself partially. If you're adding event listeners or your own custom HTML into the inspector, you should always do it inside the `render` event handler.
change:[path to the changed property] Triggered when the user changes a property of a cell through the inspector. The handler is passed the value at the property path and the input DOM element as arguments.
close Triggered when the inspector gets closed.

Inspector API

static

create(container, options) A helper for creating the inspector, where container is an HTMLElement or a selector (container is a DOM placeholder into which the Inspector will be rendered). For more information about options, see the Configuration chapter. An instance of joint.ui.Inspector is returned.
close() A helper for closing Inspector instances which were created via the create() method above.

public

render() Render the Inspector based on the options passed to the constructor function. Note that this does not add the inspector to the live DOM tree. This must be done manually by appending the Inspector DOM element (accessible as its el property) to a live DOM element on the page.
updateCell() Manually update the associated cell based on the current values in the Inspector. This is especially useful if you use the inspector with live mode disabled. See the Configuration chapter for more information.
remove() Remove the Inspector from the DOM. This should be called once you're finished with using the Inspector.
focusField(path) Focuses the field identified by the '/' separated path.
openGroup(name) Open the group identified by name.
closeGroup(name) Close the group identified by name.
toggleGroup(name) Toggle the group identified by name.
openGroups() Open all groups.
closeGroups() Close all groups.
storeGroupsState() Save the current group state - which groups are opened and which are closed. The key for storing the state is determined by the stateKey Inspector option.
restoreGroupsState() Apply the stored group state - open and close groups according this state. The state information is looked up by the current stateKey Inspector option.
getGroupsState() Get the current group state - array of closed groups.