Skip to main content

Markup

All shapes in JointJS have markup associated with it. Normally, you will specify the markup of the shape at the time of its declaration. You can find information on creating custom shapes in the corresponding section.

Markup is either an XML string, or JSON markup specifying an array of JSON elements. Also, JSON markup can be described using an svg tagged template. The markup property is used as a template to build DOM Elements on the fly when the associated cellView is rendered.

warning

When using shapes as classes in JavaScript and TypeScript, to get data for rendering from the markup property, the shape instance needs to have the markup property set before the constructor is invoked. In that case, to set markup for a particular class, you should use the preinitialize() method.

JSON Markup

JSON markup is defined recursively as an array of JSONElement or strings representing text nodes, where JSONElement is a plain object with the following properties:

  • tagName (string) (required) The type of element to be created.
  • selector (string) A unique selector for targeting the element within the attr cell attribute.
  • groupSelector (string | string[]) A selector for targeting multiple elements within the attr cell attribute. The group selector name must not be the same as an existing selector name.
  • namespaceURI (string) The namespace URI of the element. It defaults to SVG namespace "http://www.w3.org/2000/svg".
  • attributes (object with attributes name-value pairs) Presentation attributes of the element.
  • style (object with CSS property-value pairs) The style attribute of the element.
  • className (string) The class attribute of the element.
  • children (JSONMarkup) The children of the element.
  • textContent (string) The text content of the element.
import { dia } from '@joint/core';

// single DOM element
const markup: dia.MarkupJSON = [{ tagName: 'rect' }];

// multiple DOM elements
const markup: dia.MarkupJSON = [{
tagName: 'rect',
selector: 'body'
}, {
tagName: 'text',
selector: 'label',
attributes: {
'stroke': 'none'
}
}]

// nested DOM elements
const markup: dia.MarkupJSON = [{
tagName: 'g',
children: [{
tagName: 'circle',
selector: 'circle1',
groupSelector: 'circles'
},
'text content',
{
tagName: 'circle',
selector: 'circle2',
groupSelector: 'circles'
}]
}]

JSON Markup using tagged template

JSON markup can also be defined using an svg tagged template. This tagged template converts an SVG representation of the markup into a JSON markup object. Let's write the previous example using an svg tagged template:

import { dia, util } from '@joint/core';

// single DOM element
const markup: dia.MarkupJSON = util.svg`<rect \>`

// multiple DOM elements
const markup: dia.MarkupJSON = util.svg`
<rect @selector="body" \>
<text
@selector="label"
stroke="none"
\>`

// nested DOM elements
const markup: dia.MarkupJSON = util.svg`
<g>
<circle
@selector="circle1"
@group-selector="circles"
/>
text content
<circle
@selector="circle2"
@group-selector="circles"
/>
</g>`

JSON Markup presentation attributes

Anything you define in markup is evaluated once at CellView creation (the DOM elements and their attributes). That means it's important to think about the runtime of your application. If you have SVG attributes that don't change throughout the runtime, you can add them to the markup.

As markup is something all instances of the shape type are expected to have in common, inheriting from the subtype prototype is more efficient. Nevertheless, it is still possible to provide custom markup to individual instances of your class by providing markup later.

Anything in the attrs attribute is evaluated on every change of the model (e.g. a resize or an attrs change). As JointJS special presentation attributes mostly depend on the current state of the model (size, attrs, angle), they should always be defined inside attrs.

SVG attributes that are modified at some point in the application should also be added to attrs (e.g. user changes the color of the element).

import { dia } from '@joint/core';

const standardRectangle = dia.Element.define('standard.Rectangle', {
attrs: {
body: {
width: 'calc(w)', // special calc attribute expression
height: 'calc(h)',
stroke: 'red',
fill: '#333333' // We wan't to modify the fill color of our instances
},
label: {
// attributes
}
}
}, {
markup: [{
tagName: 'rect',
selector: 'body',
attributes: {
strokeWidth: 2
}
}, {
tagName: 'text',
selector: 'label',
attributes: {
stroke: 'none' // We don't need any instances to have a stroke value for text
}
}]
});

Setting presentation attributes with selectors

Here are simple rules how SVG attributes are set on the nodes when one uses a combination of selector and groupSelector.

  • selector is unique i.e. can target a single node only.
  • selector takes precedence over groupSelector.
  • groupSelector targeting n nodes takes precedence over groupSelector targeting m nodes for n < m. When n === m the order how the presentation attributes are applied is unspecified.

In the example below, the circle with the selector circle1 is filled with 'red' color. All the other circles are filled with 'blue' color. You can find more information on the selector interactions in the presentation attributes section.

cell.attr({
circle1: { fill: 'red' },
circles: { fill: 'blue', stroke: 'black' }
});

Special selectors

There are a few special selectors that can be used in the markup property:

scalable

The scalable <g/> of the shape. It scales its content to fit the size of the model.

markup: util.svg`
<g @selector="scalable">
<rect x="0" width="10" height="10" fill="red"/>
<rect x="10" width="10" height="10" fill="blue"/>
</g>
`
important

It should be avoided unless using calc() expressions is not an option.

When using the scalable selector, the shape needs to be rendered, measured and then transformed. This can have an impact on the performance for large diagrams.

The example above can be rewritten using calc() expressions:

markup: util.svg`
<rect @selector="r1" fill="red" />
<rect @selector="r2" fill="blue" />
`,
attrs: {
r1: {
x: 0,
width: 'calc(w/2)',
height: 'calc(h)'
},
r2: {
x: 'calc(w/2)',
width: 'calc(w/2)',
height: 'calc(h)'
}
}
note

The width and height of the shape are set in the size attribute of the model.

el.set('size', { width: 100, height: 100 });

rotatable

The rotatable <g/> of the shape. By default, the whole markup is rotated around the center of the shape. If you want to rotate only a part of the markup, you can use the rotatable selector.

markup: util.svg`
<g @selector="rotatable">
<rect /><!-- this rect will be rotated -->
</g>
<text /><!-- this text will not be rotated -->
`
important

The rotatable selector is useful when you want to rotate only a part of the shape. If you want to rotate the whole shape, the rotatable group is implicitly the root group of the shape.

note

The angle of rotation in degrees is set in the angle attribute of the model.

el.set('angle', 45);

Implicit selectors

There are also selectors that are assigned implicitly to important nodes of the shape:

root

The wrapping <g/> of the shape in the context of a shape.

<g class="joint-element"> <!-- root -->
<rect />
<text />
</g>

The wrapping <g/> of the port in the context of a port.

<g class="joint-port"> <!-- root -->
<circle class="joint-port-body" />
<text class="joint-port-label" />
</g>

The wrapping <g/> of the label in the context of a link label.

<g class="label"> <!-- root -->
<rect />
<text />
</g>

portRoot

The root node of the port's body (excluding the label) in the context of a port.

It is either the implicit wrapping group <g/> in case the port consist of multiple SVGElements

<g class="joint-port">
<g class="joint-port-body"> <!-- portRoot -->
<rect />
<rect />
</g>
<text class="joint-port" />
</g>

or a single SVGElement of the port.

<g class="joint-port">
<circle class="joint-port-body" /> <!-- portRoot -->
<text class="joint-port-label" />
</g>

labelRoot

The root node of the label in the context of a port.

Either the implicit wrapping group <g/> in case the label consist of multiple SVGElements

<g class="joint-port">
<circle class="joint-port-body"/>
<g class="joint-port-label" /> <!-- labelRoot -->
<rect />
<text />
</g>
</>

or a single SVGElement of the label.

<g class="joint-port">
<circle class="joint-port-body"/>
<text class="joint-port-label" /> <!-- labelRoot -->
</g>

labelText

The <text /> of the label in the context of a port.

<g class="joint-port">
<circle class="joint-port-body" />
<g class="joint-port-label" />
<rect />
<text /> <!-- labelText -->
</g>
</>

Markup without selectors

Markup can also be specified as a valid XML string that contains either a single tagName or XML that can be parsed with DOMParser.

markup: 'rect'
markup: '<rect class="rectangle"/>'
markup: '<rect><g><circle/><circle/></g>'
important

Defining Cell markup with XML strings is slower than defining it with JSON arrays, precisely because of the need for parsing. We strongly recommend you to use the JSON markup for your Cell definitions.

Note that you also need to set the useCSSSelectors=true on the model's prototype in order to use CSS selectors in the attrs attribute.

import { dia } from '@joint/core';

const rectangle = dia.Element.define('Rectangle', {
attrs: {
rect: { // CSS Selector for the <rect/> element
fill: 'red'
}
}
}, {
markup: '<rect class="rectangle"/>',
useCSSSelectors: true
});