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.
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 theattr
cell attribute.groupSelector
(string | string[]) A selector for targeting multiple elements within theattr
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) Thestyle
attribute of the element.className
(string) Theclass
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 overgroupSelector
.groupSelector
targetingn
nodes takes precedence overgroupSelector
targetingm
nodes forn < m
. Whenn === 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>
`
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)'
}
}
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 -->
`
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.
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>'
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
});