Skip to main content

Property editor and viewer

The plugin to implement an editor and viewer of Cell model properties is called Inspector.

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 component for use in applications.

Installation

Import the Inspector from the ui namespace and create a new instance of it.

import { ui } from '@joint/plus';

const inspector = new ui.Inspector(options);
document.getElementById('inspector-holder').appendChild(inspector.render().el);
There is also a UMD version available

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

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

And access the Inspector through the joint.ui namespace:

index.js
const inspector = new joint.ui.Inspector(options);
document.getElementById('inspector-holder').appendChild(inspector.render().el);
info

Applications usually only display one Inspector at a time. For this common use case, we have introduced a static Inspector.create() helper method, which is simpler to use:

import { ui } from '@joint/plus';

ui.Inspector.create('.inspector-holder', options);

How does Inspector work?

The ui.Inspector constructor takes an options object, which contains a mandatory cell or cellView property (to specify the object whose model properties the editor will be editing) alongside additional options.

Among the additional options, two are the most important:

  • inputs is object that contains model property names as keys (which may be nested) and definitions of input fields that the Inspector should display (inputConfig objects) as values. A single input field is in charge of setting user input for a specific property on the provided Cell model. See the Inputs section for more information.
  • groups is an object that contains group identifiers as keys and group options (groupConfig objects) as values. Each group may contain any number of input fields in the Inspector panel, and the user may show/hide the whole group by clicking a toggle button. See the Groups section for more information.

The next thing to do is rendering the Inspector widget (inspector.render()) and appending the Inspector HTML Element (el) to a holder, which can be any element in your HTML.

info

Since applications usually only display one Inspector at a time which is reused for various application models and differs only in the configuration of input fields, we have introduced a static Inspector.create() helper method.

You provide the holder element (or a CSS selector identifying one) in the first argument of the method, and in the second argument you may provide an object specifying the same options as the constructor (see above).

The method automatically makes sure that the previous Inspector instance (if any) is properly removed, creates a new instance, and renders it into the DOM. It also keeps track of open/closed groups and restores them based on the last used state. See the example below to see it in action.

Example

Here is an example which shows an Inspector (the component with grey background) with inputs of several different types separated into two groups. Any changes you make in the Inspector immediately take effect in the Elements themselves. Meanwhile, clicking the other Element refreshes the Inspector to show that Element's property values:

Inputs

The inputs object is extremely important. Its structure mimics the structure of properties of the Cell model. However, instead of any static values, it eventually contains definitions of input fields.

These input field definitions are used by the Inspector to construct input fields - HTML form controls in charge of setting a user provided value on the Cell model. Note that only those Cell properties that have an input field specified via the inputs option will have an input field shown in the Inspector; all other properties remain invisible to the user.

Each input field definition is specified via an inputConfig object, which may have different properties specified (as detailed in the API reference of the inputs option). The most important property is the type of the input field, but you may provide additional properties in the inputConfig object in order to customize the way the input field is presented in the Inspector, such as:

  • label,
  • defaultValue,
  • options (for input field types which allow you to select among several options, see below),
  • attrs is an object that specifies HTML attributes that should be applied on the input field,
  • when is an object that specifies conditional expressions that dynamically determine whether the input field should be shown in the Inspector (see below),
  • ...and others.

An example of an inputs object is presented below. The objects inside fill, stroke, and strokeWidth properties are inputConfig objects specifying one input field definition each. This creates an Inspector with three form controls:

  • a color-palette allowing the user to view and edit the attrs/body/fill model attribute,
  • a color-palette allowing the user to view and edit the attrs/body/stroke model attribute,
  • and a range allowing the user to view and edit the attrs/body/strokeWidth model attribute:
inputs: {
attrs: {
body: {
fill: {
type: 'color-palette',
options: [
{ content: '#FFFFFF' },
{ content: '#00FF00' },
{ content: '#000000' }
],
label: 'Fill color',
group: 'presentation',
index: 1
},
stroke: {
type: 'color-palette',
options: [
{ content: '#FFFFFF' },
{ content: '#00FF00' },
{ content: '#000000' }
],
label: 'Outline color',
group: 'presentation',
index: 2
},
strokeWidth: {
type: 'range',
min: 0,
max: 50,
unit: 'px',
label: 'Outline thickness',
group: 'presentation',
index: 3
}
}
}
}

Built-in input field types

By default, the Inspector recognizes several input field types - the Inspector constructor matches the type of each input field definition to one of those values and creates an input field accordingly.

The built-in input field types are listed below in order of their type identifier:

color

Insert an HTML 5 color-type input field to the Inspector. See the API reference for color input field type of Inspector for more information.

const inspector = new ui.Inspector({
cellView: cellView,
inputs: {
myProperty: {
type: 'color'
}
}
});

color-palette

Insert an instance of ColorPalette to the Inspector. See the API reference for color-palette input field type of Inspector for more information.

const inspector = new ui.Inspector({
cellView: cellView,
inputs: {
myProperty: {
type: 'color-palette'
}
}
});

content-editable

Insert a content-editable div (which resizes automatically as the user types) to the Inspector. See the API reference for content-editable input field type of Inspector for more information.

const inspector = new ui.Inspector({
cellView: cellView,
inputs: {
myProperty: {
type: 'content-editable'
}
}
});

list

Insert a widget for adding/removing items to/from an arbitrary array. The inputs of each list item can be specified via the item property. See the API reference for list input field type of Inspector for more information.

const inspector = new ui.Inspector({
cellView: cellView,
inputs: {
myList: {
type: 'list',
item: {
type: 'text'
}
}
}
});

number

Insert an HTML 5 number-type input field to the Inspector. See the API reference for number input field type of Inspector for more information.

const inspector = new ui.Inspector({
cellView: cellView,
inputs: {
myProperty: {
type: 'number',
properties: {
min: 0,
max: 100
}
}
}
});

object

Insert a wrapper for the inputs of specified properties of an arbitrary object (specified via the properties property). See the API reference for object input field type of Inspector for more information.

const inspector = new ui.Inspector({
cellView: cellView,
inputs: {
myObject: {
type: 'object',
properties: {
myObjectFirstProperty: {
type: 'text'
},
myObjectSecondProperty: {
type: 'number'
}
}
}
}
});

radio-group

Insert an instance of RadioGroup to the Inspector. See the API reference for radio-group input field type of Inspector for more information.

const inspector = new ui.Inspector({
cellView: cellView,
inputs: {
myProperty: {
type: 'radio-group'
}
}
});

range

Insert an HTML 5 range-type input field to the Inspector. See the API reference for range input field type of Inspector for more information.

const inspector = new ui.Inspector({
cellView: cellView,
inputs: {
myProperty: {
type: 'range',
properties: {
min: 0,
max: 100
}
}
}
});

select

Insert a select box to the Inspector. See the API reference for select input field type of Inspector for more information.

const inspector = new ui.Inspector({
cellView: cellView,
inputs: {
myProperty: {
type: 'select',
properties: {
options: ['One', 'Two', 'Three']
}
}
}
});

select-box

Insert an instance of SelectBox to the Inspector. See the API reference for select-box input field type of Inspector for more information.

const inspector = new ui.Inspector({
cellView: cellView,
inputs: {
myProperty: {
type: 'select-box'
}
}
});

select-button-group

Insert an instance of SelectButtonGroup to the Inspector. See the API reference for select-button-group input field type of Inspector for more information.

const inspector = new ui.Inspector({
cellView: cellView,
inputs: {
myProperty: {
type: 'select-button-group'
}
}
});

text

Insert a text input field to the Inspector. See the API reference for text input field type of Inspector for more information.

const inspector = new ui.Inspector({
cellView: cellView,
inputs: {
myProperty: {
type: 'text'
}
}
});

textarea

Insert a textarea input field to the Inspector. See the API reference for textarea input field type of Inspector for more information.

const inspector = new ui.Inspector({
cellView: cellView,
inputs: {
myProperty: {
type: 'textarea'
}
}
});

toggle

Insert a toggle (a checkbox-type input field) to the Inspector. See the API reference for toggle input field type of Inspector for more information.

const inspector = new ui.Inspector({
cellView: cellView,
inputs: {
myProperty: {
type: 'toggle'
}
}
});

Custom input field types

Although Inspector provides a good selection of built-in input field types, sometimes applications need to render custom input fields (e.g. to integrate a third-party widget) while still taking advantage of the two-way data binding and configuration options provided by the Inspector plugin.

Custom input field types allow you to define a new input field type string with custom rendering and a custom set of behaviors - these tell the Inspector how to work with the type in all possible situations.

The custom input field type's behaviors are defined within the following Inspector callback options (most often as case blocks of logic within switch(type) statements). At a minimum, you must define the behavior of the custom input field type within the renderFieldContent() and the getFieldValue() Inspector callback options:

  • The renderFieldContent() callback determines what HTMLElement or DOM Element should be rendered by the Inspector for input fields of the type (passed to getFieldValue()).
  • The getFieldValue() callback determines how values will be read from input fields of the type (as reported by renderFieldContent()) on user input (passed to validateInput()).
  • If specified, the validateInput() callback determines how user input into input fields of the type (as reported by getFieldValue()) should be evaluated as valid. (If the user input is evaluated as invalid, the input field value is not saved to the Cell model.)
  • If specified, the focusField() callback determines custom focus behavior of input fields of the type.

The following example shows how to render two different kinds of custom fields:

  • The my-button-set input field type contains two buttons. In the example, it is used to modify text content (illustrates creating an input field from scratch).
  • The select2 input field type implements the Select2 widget for advanced select boxes. In the example, it is used to modify text style (illustrates integrating a third-party widget as an input field).
How do we define the two custom input field types via switch-case blocks?

In our example, we are defining two custom input field types. Therefore, we need to specify two case blocks inside the renderFieldContent() Inspector callback option...

renderFieldContent: function(options, path, value, inspector) {
switch (options.type) {
case 'my-button-set': {
// return an HTML element to be rendered for a `my-button-set` input field
}

case 'select2': {
// return an HTML element to be rendered for a `select2` input field
}
}
},

...and we need to specify two case blocks inside the getFieldValue() Inspector callback option:

getFieldValue: function(attribute, type) {
switch(type) {
case 'my-button-set': {
// return the value of a `my-button-set` input field
}

case 'select2': {
// return the value of a `select2` input field
}
}
}

Nested structures

The list and object input field types allow you to create input fields for arbitrary nested structures.

Object properties

If your Cell contains an object as a property (e.g. cell.set('myObject', { first: 'John', last: 'Good' })), you can instruct the inspector to use the object input field type for your myObject property and then define nested input field definitions for properties of that object:

const inspector = new ui.Inspector({
cellView: cellView,
inputs: {
myObject: {
type: 'object',
properties: {
first: {
type: 'text'
},
last: {
type: 'text'
}
}
}
}
});
How is this different from specifying input field definitions directly?

You might be wondering why the object input field type is necessary - couldn't we do without it and specify input field definitions for myObject/first and myObject/last directly, as in the example below?

const inspector = new ui.Inspector({
cellView: cellView,
inputs: {
myObject: {
first: {
type: 'text'
},
last: {
type: 'text'
}
}
}
});

The difference is that in the example above, the Inspector treats the two input fields as two completely separate form controls, whereas the object input field instructs the Inspector to wrap the two input fields into a common wrapper (with its own label).

Both approaches are possible, and choosing one over the other depends on what you want to show in the Inspector. You may wish to pretend that there is no nesting and that all input fields in your Inspector are completely separate, you may choose to use the object input field everywhere in order to match the nesting exactly, or you may choose anything in between these two extremes.

Array properties

Similarly, if your Cell contains an array as a property (e.g. cell.set('myList', [{ first: 'John', last: 'Good' }, { first: 'Jane', last: 'Good' }])), you can instruct the inspector to use the list input field type for your myList property and then define the input field type of each array item (via the item property). Importantly, the list input field type enables users to add and remove array items.

In our example, the items of myList array contain a nested object, and we define its properties as well:

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

Options

Some input field types (color-palette, radio-group, select, select-box, select-button-group) allow the user to choose from among multiple options. You may provide a static array of items, or you can provide the options dynamically, based on the value of one or more other properties on the Cell.

  • Defining options as a string instructs the Inspector to interpret the string as a path to another property within the Cell. If the property at that path contains an array, the array's items are used as options of the input field. The Inspector keeps track of the array in order to refresh the list of options of this input field whenever there is a change.

  • Defining options as an object allows you to provide a source() callback function which is expected to return an array of options based on the current state of the Inspector and the current values of an arbitrary array of dependencies (other properties within the Cell). The Inspector keeps track of all dependencies and calls the callback function in order to refresh the list of options of this input field whenever there is a change (but updates can also be triggered manually).

See the API reference for select input field type of Inspector for more information.

Groups

The groups object allows you to define the IDs of groups of input fields within your Inspector, and lets you provide a groupConfig object for each, to specify the groups' behavior within the Inspector.

You can assign an input field to a group by writing the group's ID into the group property of the input field's inputConfig object. You can also modify the ordering of input fields within the group by providing index properties in inputConfig for the input fields.

You may specify label and index for a group via its groupConfig object, which works analogously to the inputConfig properties, and you may also specify a when expression to show/hide the group dynamically (see below)

Conditional expressions

The Inspector relies on conditional expressions defined in the inputConfig.when parameter of the inputs option (and the groupConfig.when parameter of the groups option) to switch the visibility of an input field (or a group) based on the values of arbitrary Cell model properties.

The Inspector keeps track of the value of all properties mentioned in an expression, and re-evaluates the condition whenever there is a change. Whenever the conditional expression returns false, it means that the condition is not met, and the Inspector hides the input field (or group) in question. Conversely, whenever the conditional expression returns true, the input field (or group) in question is shown by the Inspector.

Types of conditional expressions

Conditional expressions are objects with a property whose key identifies the operator of the expression, and whose value depends on the type of the operator. There are three types of operators which may be used in an expression:

  • Primitive operators, such as eq (equals), lt (less than) and in (is an item in array). They take an object whose key is a string path to a Cell model property and whose value is a value to compare it with. Examples:

    { eq: { 'size/width': 300 }}
    { lt: { 'count': 10 }}
    { in: { 'index': [0,2,4] }}
    { regex: { 'attrs/label/text' : 'JointJS|Rappid' }}
  • Unary operators (actually just a single operator), not. They take a single expression and return a result based on its return value. For example:

    // `true` when `!util.isEqual({ 'width': 300 }, cell.prop('size'))`
    { not: { equal: { 'size': { 'width': 300 }}}}
  • Multiary operators, and, or and nor. They take an array of expressions and return a result based on their return values. For example:

    // `true` when `((cell.prop('position/x') >= 100) &&
    // (cell.prop('position/x') <= 400) &&
    // (cell.prop('position/y') >= 200) &&
    // (cell.prop('position/y') <= 300))`
    { and: [
    { gte: { 'position/x': 100 }},
    { lte: { 'position/x': 400 }},
    { gte: { 'position/y': 200 }},
    { lte: { 'position/y': 300 }}
    ]}

Conditional expressions can be recursively nested within unary and multiary operators to create complex conditionals that combine multiple primitive operations via NOT, AND, OR and NOR logical operations.

Examples of using conditional expressions

Imagine a scenario where you have a select input field with two options, 'email' and 'tel'. Below this input field, you want to show either a text input field or a number input field, based on the selected option. Assuming your Cell model properties structure were as follows: { contactOption: 'email', contactEmail: '', contactTel: '' }, your Inspector could be defined like the following:

const inspector = new ui.Inspector({
cell: cell,
inputs: {
contactOption: {
type: 'select',
options: ['email', 'tel']
},
contactEmail: {
type: 'text',
when: { eq: { 'contactOption': 'email' }}
},
contactTel: {
type: 'number',
when: { eq: { 'contactOption': 'tel' }}
}
}
});

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

const inspector = new ui.Inspector({
cell: cell,
inputs: {
userInfo: {
type: 'object',
properties: {
contactOption: {
type: 'select',
options: ['email', 'tel']
},
name: {
type: 'text'
}
}
},
contactEmail: {
type: 'text',
when: { eq: { 'userInfo/contactOption': 'email' }}
},
contactTel: {
type: 'number',
when: { eq: { 'userInfo/contactOption': '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:

const inspector = new ui.Inspector({
cell: cell,
inputs: {
userList: {
type: 'list',
item: {
type: 'object',
properties: {
contactOption: {
type: 'select',
options: ['email', 'tel']
},
contactEmail: {
type: 'text',
when: { eq: { 'userList/${index}/contactOption': 'email' }}
},
contactTel: {
type: 'number',
when: { eq: { 'userList/${index}/contactOption': 'tel' }}
}
}
}
}
}
});

It is also possible to toggle Inspector groups with when expressions:

const inspector = new ui.Inspector({
cell: cell,
inputs: {
// ...
},
groups: {
first: {
label: 'F'
},
second: {
label: 'S',
when: { eq: { 'myCellProperty': true }}
}
}
});

Custom conditional expressions

Although Inspector provides a good selection of built-in types of conditional expressions, sometimes this is not enough and applications have special requirements for when input fields in the Inspector should be hidden/visible based on the values in other Cell model properties. To meet this requirement while still taking advantage of the Inspector configurability through expressions, Inspector provides a way to define your own custom primitive operators.

Custom primitive operators compare the value of a Cell model property with some other value (fixed or dynamic) based on custom logic.

In order to be able to use a custom primitive operator in a custom conditional expression, the operator first needs to be defined as a callback function inside the operators option on the Inspector under a unique identifier. When that is done, the custom primitive operator may be used in the same way as any built-in primitive operator:

The value of an expression where the key is a custom primitive operator is an object with a single property:

  • The key is a string path pointing to a Cell model property and the value is the value with which should be passed as ...arguments to the custom primitive operator's callback function.

For example, if we wanted to make myDescription editable only when the value of myTitle (always editable) has more characters than the number specified by myTitleThreshold (a hidden property on the Cell which is not editable by the user), we could specify our Inspector as the following:

const inspector = new ui.Inspector({
cell: cell,
inputs: {
myTitle: {
type: 'text'
},
myDescription: {
type: 'text',
when: { longerThan: { 'myTitle': 'myTitleThreshold' }}
}
},
operators: {
longerThan: function(cell, value, prop, _valuePath) {
// value === current contents of 'myTitle' property of cell
// prop === 'myTitleThreshold'
// _valuePath === 'myTitle'
return (value ? (value.length > cell.prop(prop)) : false);
}
}
});

In the above example, whenever the user types within the myTitle input field in the Inspector and the text becomes longer than the value of the myTitleThreshold property, the myDescription gets displayed (and vice versa - if the text becomes shorter than the value of the myTitleThreshold property, the myDescription field gets hidden).

However, the above example would be insufficient if myTitleThreshold property were editable as well - since the Inspector does not listen for changes in myTitleThreshold by default, the operator's callback function would never be called if only myTitleThreshold were changed, even if a change in myTitleThreshold meant that the operator's condition were no longer met and myDescription should get hidden. In order to fix this, we would need to register myTitleThreshold within the dependencies list inside the when clause of the myDescription input field:

const inspector = new ui.Inspector({
cell: cell,
inputs: {
myTitle: {
type: 'text'
},
myTitleThreshold: {
type: 'number',
min: 0
},
myDescription: {
type: 'text',
when: {
longerThan: { 'myTitle': 'myTitleThreshold' },
dependencies: ['myTitleThreshold']
}
}
},
operators: {
longerThan: function(cell, value, prop, _valuePath) {
// value === current contents of 'myTitle' property of cell
// prop === 'myTitleThreshold'
// _valuePath === 'myTitle'
return (value ? (value.length > cell.prop(prop)) : false);
}
}
});

Compared to the first example, in the second example myDescription gets displayed also if the value of myTitleThreshold is reduced such that the value of myTitle becomes longer than the threshold (and vice versa - myDescription gets hidden also if the value of myTitleThreshold is increased such that the value of myTitle becomes shorter than the threshold).

Validation

The following example shows how to reflect a custom shapes's validation functions inside your Inspector. The Inspector definition specifies a custom validateInput() method that refers to the validateProperty() method on the Inspector's associated Cell, working with the assumption that the diagram will only ever contain custom shapes (like validation.ContactCard) that define this function inside its prototypeProperties object.

Notice that this architecture makes the shape responsible for accepting/rejecting user input data, not the Inspector. We consider this arrangement a best practice because it manages to fulfill one of the goals of the JointJS framework, namely the separation of Model-View-Controller components from each other.

Custom input field labels

It is possible to customize the appearance and behavior of input field labels in your Inspector by specifying a custom callback function in the renderLabel() option. You can use this functionality in several ways, including the following:

  • You can create labels with custom HTML. This is illustrated in the example below by the label of myList (list-type input field), which wraps the standard labelEl HTMLElement (<label>) in an <a> HTMLElement (whose href attribute is determined by the custom inputConfig.url property specified within the Inspector's inputs object as part of the definition of the current input field).
  • You can make use of the templating functionality of JointJS to define labels with dynamic content. This is illustrated in the example below by the labels of myList.item (text-type input fields), in which the {{index}} placeholder is replaced with the item's current index within myList. (You can add new items with the + button and remove existing items with the - button.)

Events

The Inspector object triggers events when the user changes its input values or when the Inspector needs to re-render itself partially, which you can react to in your applications. These events can be handled by using the inspector.on(eventName, handler) method. The list of events can be found in the API reference.

Frequently asked questions

Is it possible to add a transparent color to Inspector color picker?

The specification of the HTML element used by the color input field type of Inspector (i.e. <input type="color">) allows only simple colors - that is, only RGB colors without an alpha channel. Therefore, a transparent color cannot be added to it. However, there are alternatives.

Learn more...

Use a third party library:

  • The advice we give to our customers with similar questions is to investigate third party libraries (e.g. Spectrum) to achieve this functionality. The interactive content can be included inside Inspector as a custom input field type.

Use a different built-in Inspector input field type: