JointJS Decorators

ECMAScript / TypeScript decorator for defining JointJS shapes.

This library fully depends on jointjs (>=3.7), so please read its README before using this library.

License

Mozilla Public License 2.0

Install

Enable the experimentalDecorators compiler option in your tsconfig.json.

npm i -S @joint/decorators

Usage

There are a few class decorators:

And several class member decorators:


@Model(options: ModelOptions)

The decorator allows you to:

import { dia } from 'jointjs';
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);
    }
}

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">
// Change the stroke of all rectangles
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 'jointjs';
import { Model } from '@joint/decorators';

@Model({
    attributes: {
        color: 'red'
    }
})
class MyElement extends dia.Element {

}

is equivalent to

import { dia, shapes } from 'jointjs';

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 'jointjs';
import { Model } from '@joint/decorators';

@Model({
    namespace: shapes
})
class MyElement extends dia.Element {

}

is equivalent to

import { dia, shapes } from 'jointjs';

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 'jointjs';
import { View } from '@joint/decorators';

@View({
    namespace: shapes
    models: [MyElement, MyOtherElement]
})
class MyElementView extends dia.ElementView {

}

is equivalent to

import { dia, shapes } from 'jointjs';

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 'jointjs';
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 {

    /* `stroke-dasharray` that adjusts based on the current node's `stroke-width` */
    @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);
    }
}