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:
<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:
const inspector = new joint.ui.Inspector(options);
document.getElementById('inspector-holder').appendChild(inspector.render().el);
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.
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 theattrs/body/fill
model attribute, - a
color-palette
allowing the user to view and edit theattrs/body/stroke
model attribute, - and a
range
allowing the user to view and edit theattrs/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 thetype
(passed togetFieldValue()
). - The
getFieldValue()
callback determines how values will be read from input fields of thetype
(as reported byrenderFieldContent()
) on user input (passed tovalidateInput()
). - If specified, the
validateInput()
callback determines how user input into input fields of thetype
(as reported bygetFieldValue()
) 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 thetype
.
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 asource()
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 ofdependencies
(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
) andin
(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
andnor
. 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:
- It may be used within an expression inside the
when
property of any input field definition. - It may be used as an operand of unary and multiary operators.
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 standardlabelEl
HTMLElement (<label>
) in an<a>
HTMLElement (whosehref
attribute is determined by the custominputConfig.url
property specified within the Inspector'sinputs
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 withinmyList
. (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:
-
The
color-palette
input field type of Inspector instead, which allows using a'transparent'
color. -
The
text
input field type of Inspector may be used to allow users to enter a raw CSS color value, which can have transparency.