JointJS Decorators
ECMAScript / TypeScript decorator for defining JointJS shapes.
This library fully depends on JointJS (>=3.5), so please read its README before using this library.
Setup
Enable the experimentalDecorators compiler option in your tsconfig.json
.
Then install JointJS Decorators from NPM:
npm i -S @joint/decorators
License
Mozilla Public License 2.0
Usage
There are a few class decorators:
And several class member decorators:
@Model(options: ModelOptions)
The decorator allows you to:
- paste an existing SVG and use it as the model's markup
- keep your SVG attributes in sync with your model's attributes
- transform data using functions
- introduce a new SVG attribute or change the behavior of an existing one
import { dia } from '@joint/core';
import { Model } from '@joint/decorators';
@Model({
template: `
<g>
<rect
width="calc(w)"
height="calc(h)"
:fill="{{color}}"
stroke="black"
/>
<text
x="calc(0.5*w)"
y="calc(0.5*h)"
text-anchor="middle"
text-vertical-anchor="middle"
font-size="14"
font-family="sans-serif"
fill="black"
>{{firstName}} {{lastName}}</text>
</g>
`,
attributes: {
color: 'red',
firstName: 'John',
lastName: 'Doe'
}
})
class MyElement extends dia.Element {
}
ModelOptions
Option | Description | Optional |
---|
template | the SVG string markup of the model | No |
attributes | the default attributes of the model | Yes |
namespace | the namespace for the model class to be added to | Yes |
template
The decorator uses an SVG-based template syntax that allows you to declaratively bind the rendered DOM to the underlying model's data.
All templates are syntactically valid SVG that can be parsed by spec-compliant browsers and SVG parsers.
While using the SVG XML string in the markup attribute is not recommended (every cell view needs to parse the string and it might affect the performance), the parsing of the decorator's template runs only once per class (translating it into JSON markup and defining event listeners needed for reactivity).
Text Interpolation
The most basic form of data binding is text interpolation using the "Mustache" syntax (double curly braces):
<text font-size="14">{{ label }}</text>
The mustache tag will be replaced with the value of the label
property from the corresponding model's instance. It will also be updated whenever the label
property changes.
Attributes Binding
To bind to an SVG attribute to a model's attribute, add a colon symbol (:
) before the attribute's name.
<rect :fill="color" />
The colon symbol, :
, instructs the decorator to keep the SVG attribute in sync with the model's attribute.
The model's color
value can be set this way.
model.set('color', 'red');
If the bound value is null
, then the SVG attribute will be removed from the rendered element.
model.set('color', null);
It's possible to use mustaches inside binding expressions
to combine multiple model's attributes into a single result.
<rect :fill="rgb({{red}},{{green}},{{blue}})" />
Calling Functions
The value of an attribute can be modified with functions before set/display.
<rect :stroke="color" :fill="lighten(color)"/>
<text>{{ capitalize(label) }}</text>
Functions called inside binding expressions will be called every time the cell view updates, so they should not have any side effects, such as changing data or triggering asynchronous operations.
It is possible to call a component-exposed method inside a binding expression only if the method is decorated with the Function decorator.
The function can accept any number of additional arguments. Every such argument shall be parsable with the JSON.parse function.
@Model({
template: `
<text y="10">{{ maxLength(label1, 20) }}</text>
<text y="30">{{ maxLength(label2, 10) }}</text>
`
})
class MyElement extends dia.Element {
@Function()
maxLength(value: string, max: number) {
return value.substr(0, max);
}
}
Multiple dependencies can be defined using an array.
<path :d="data([param1, param2])"/>
The function is run every time one or more attributes in the dependency array are changed.
@Model({
attributes: {
param1: 20,
param2: 30
},
template: `
<path :d="data([param1, param2])" stroke="white" fill="transparent" />
`
})
class Arrow extends dia.Element {
@Function()
data(param1: number, param2: number): string {
return `
M ${param1} 0
L calc(w) calc(0.5*h)
L ${param1} calc(h)
V calc(h/2 + ${param2 / 2})
H 0
v -${param2}
H ${param1}
Z
`;
}
}
Selectors
If you want to modify any of the template attributes programmatically, you must add the @selector
attribute to the SVG element.
<rect @selector="body" stroke="black" fill="red">
model.attr(['body', 'fill'], 'blue');
The <g>
wrapper in the template is added automatically and always has a @selector
equal to root
. In case you want to add SVG attributes to the root group, wrap the template with one of them.
<g @selector="root" data-tooltip="My Tooltip">
<rect @selector="body" stroke="black" fill="red">
</g>
To create selectors pointing to multiple SVG elements at once, use @group-selector
.
<rect @group-selector="rectangles" fill="red">
<rect @group-selector="rectangles" fill="blue">
<rect @group-selector="rectangles" fill="green">
model.attr(['rectangles', 'stroke'], 'black');
Caveats
Some JointJS attributes expect their value to be an object (fill & stroke gradient, filters, markers and textWrap).
The solution is to define the property inside the attributes (mixing the template attributes with explicit model attributes).
const selector = 'label';
@Model({
attributes: {
title: 'My Title',
attrs: {
[selector]: {
textWrap: {
maxLineCount: 1,
ellipsis: true
}
}
}
},
template: `
<text @selector="${selector}">{{title}}</text>
`
})
class MyElement extends dia.Element {
}
attributes
The default attributes of the model. When creating an instance of the model, any unspecified attributes will be set to their default value.
import { dia, shapes } from '@joint/core';
import { Model } from '@joint/decorators';
@Model({
attributes: {
color: 'red'
}
})
class MyElement extends dia.Element {
}
is equivalent to
import { dia, shapes } from '@joint/core';
class MyElement extends dia.Element {
defaults() {
const attributes = {
color: 'red'
};
return {
...super.defaults,
...attributes,
type: 'MyElement'
}
}
}
namespace
Syntactic sugar for adding a model to the namespace.
import { dia, shapes } from '@joint/core';
import { Model } from '@joint/decorators';
@Model({
namespace: shapes
})
class MyElement extends dia.Element {
}
is equivalent to
import { dia, shapes } from '@joint/core';
class MyElement extends dia.Element {
}
Object.assign(shapes, {
'MyElement': MyElement
});
@View(options: ViewOptions)
ViewOptions
Option | Description | Optional |
---|
namespace | the namespace for the view class to be added to | Yes |
models | an array of model classes this view is to be used with | Yes |
Define a new cell view, which is automatically used by 2 different models.
import { dia, shapes } from '@joint/core';
import { View } from '@joint/decorators';
@View({
namespace: shapes
models: [MyElement, MyOtherElement]
})
class MyElementView extends dia.ElementView {
}
is equivalent to
import { dia, shapes } from '@joint/core';
class MyElementView extends dia.ElementView {
}
Object.assign(shapes, {
'MyElementView': MyElementView,
'MyOtherElementView': MyElementView
});
@Function(name?: string)
Define functions to transform data (e.g strings, amounts, dates) to be used within the template.
@Model({
template: `
<text>{{ capitalize(name) }}</text>
`,
attributes: {
name: 'john',
}
})
class MyElement extends dia.Element {
@Function()
capitalize(value: string) {
return value.charAt(0).toUpperCase() + value.slice(1);
}
}
@SVGAttribute(attributeName: string)
Introduce new SVG attributes or redefine existing ones.
import { dia, g, attributes } from '@joint/core';
import { Model, SVGAttribute } from '@joint/decorators';
@Model({
attributes: {
width: 140,
height: 100
},
template: `
<rect
line-style="dashed"
stroke-width="2"
/>
`,
})
class MyElement extends dia.Element {
@SVGAttribute('line-style')
setStrokeDasharray(
this: dia.CellView,
value: string,
rect: g.Rect,
node: SVGElement,
nodeAttrs: attributes.SVGAttributes
) {
const { strokeWidth = 1 } = nodeAttrs;
let pattern;
switch (value) {
case 'dashed': {
pattern = `${4 * strokeWidth},${2 * strokeWidth}`;
break;
}
case 'dotted': {
pattern = `${strokeWidth},${strokeWidth}`;
break;
}
case 'solid': {
pattern = 'none';
break;
}
default: {
throw new Error('Invalid line-style value.');
}
}
node.setAttribute('stroke-dasharray', pattern);
}
}
SVGAttribute function signature
Argument | Description | Example |
---|
value | the right-hand side of the template's attribute | "dashed" |
rect | a rectangle describing the coordinate system the node is rendered in (if no ref attribute is in use, the value is the model's bounding box relative to the model's position, otherwise it is the relative bounding box of the node referenced by the ref attribute) | new g.Rect(0, 0, 140, 100) |
node | a rendered DOM SVGElement | <rect/> as SVGElement |
nodeAttrs | an object with all defined attributes of the node | { lineStyle: "dashed", strokeWidth: "2" } |
@On(eventName: string)
Decorate an event handler in the context of the method it refers to.
class MyElementView extends dia.ElementView {
@On('click')
onClick() {
console.log('click!', this.model.id);
}
}
Copyright © 2013-2024 client IO