【JointJS】使いやすいカスタム要素を作るコツ

## はじめに

JointJSでは再利用可能なカスタム要素を定義することができます。
今回は、カスタム要素を作る上で個人的に工夫している / 気をつけているポイントを紹介します。

※カスタム要素については過去の記事で紹介しておりますので、「カスタム要素とはなんぞや？」という方はまず過去記事を先にご確認ください。

https://qiita.com/acnaman/items/d9eb80ce25ebd50749d9

##

## 1. コンストラクタを作成する

第4引数の`staticProperties`に、可変なデータを引数に受け取るコンストラクタ[^1]を作成します。
これには以下のメリットが生まれます。

- 完全な状態のオブジェクトを最初から作れるため、不完全な状態でオブジェクトが使用される危険をなくせる
- オブジェクトの使用側でのコード量を減らすことができるため、再利用性が高まる

### コード例

```悪い例.js
const rectangle1 = new joint.shapes.standard.Rectangle();
rectangle1.position(10, 30);
rectangle1.size(200, 100);
rectangle1.attr('label/text', '1個めの四角形');
rectangle1.attr('body/fill', 'yellow');

const rectangle2 = new joint.shapes.standard.Rectangle();
rectangle2.position(230, 50);
rectangle2.size(200, 100);
rectangle2.attr('label/text', '2個めの四角形');
rectangle2.attr('body/fill', 'yellow');

const rectangle3 = new joint.shapes.standard.Rectangle();
rectangle3.position(450, 10);
rectangle3.size(200, 100);
rectangle3.attr('label/text', '3個めの四角形');
rectangle3.attr('body/fill', 'yellow');

graph.addCells(rectangle1,rectangle2,rectangle3);
```


```良い例.js
// カスタム要素を定義
joint.shapes.standard.Rectangle.define('example.MyRectangle', 
  {
    size: {
      width: 200,
      height: 100,
    },
    attrs: {
      body: {
        fill: 'yellow'
      }
    },
  },
  {},
  {
    create: function(x, y, text) {
      const rect = new joint.shapes.example.MyRectangle();
      rect.position(x, y);
      rect.attr('label/text', text);
      
      return rect;
    }
  });

// カスタム要素をコンストラクタで生成
const rectangle1 = joint.shapes.example.MyRectangle.create(10, 30, '1個めの四角形');
const rectangle2 = joint.shapes.example.MyRectangle.create(230, 50, '2個めの四角形');
const rectangle3 = joint.shapes.example.MyRectangle.create(450, 10, '3個めの四角形');

graph.addCells(rectangle1,rectangle2,rectangle3);
```

上の例ではコンストラクタを定義することで要素の生成部分を1行ずつに収めることができました。
その分カスタム要素の定義部分の行数は増えてしまっていますが、生成する回数が増えるほど恩恵を受けやすくなります。また、クリックイベントの実装なども増えた場合は、各生成ごとにイベントを追加していくのは流石に効率が悪いです。
共通の設定がある要素を複数回生成する場合については、カスタム要素でまとめた上で可変の部分のみをコンストラクタで出し分ける設計を検討しましょう。

## 2. `prop`にデータを持たせる

Vueなどのフレームワークを使用する場合、フレームワークで使用する変数を、内部値の情報としてJointJSの要素側に持たせたくなることがあります。
1で紹介したコンストラクタでデータを引数に渡し、`prop()`メソッドで任意のパラメーターに値を保持させることができます。

### コード例

```js
joint.shapes.standard.Rectangle.define('example.MyRectangle', 
  {
    // 中略
  },
  {},
  {
    // 内部値としてinternalIdを保持させるコンストラクタ
    create: function(x, y, text, internalId) {
      const rect = new joint.shapes.example.MyRectangle();
      rect.position(x, y);
      rect.attr('label/text', text);
      
      rect.prop('internalId', internalId);
      
      return rect;
    }
  });

const rectangle1 = joint.shapes.example.MyRectangle.create(10, 30, '1個目', '00001');
const rectangle2 = joint.shapes.example.MyRectangle.create(230, 50, '2個目','00002');
const rectangle3 = joint.shapes.example.MyRectangle.create(450, 10, '3個目','00003');

paper.on('myrect:click', function(cellView) {
  const id = cellView.model.prop('internalId');
  alert('id:' + id);         
});
```

### 実行結果

![dadlink.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129526/a7f83ab1-3f66-3e5a-8177-065b132701d1.gif)

## 3. 適切な単位で`selector`を設定する

`attr()`メソッドやHeighlightersでは`selector`単位で属性を設定することができます。
後から表示を変更するような要件がある場合、変更が生じる単位でselectorに分けておくと、後から変更するのが楽になります。

## まとめ

今回はカスタム要素を作る際に気をつけるべきことを紹介しました。使いやすいカスタム要素を作ることで、JointJSの良さをより引き出すことができるようになります。
他にもコツがあるかもしれませんので、気付き次第更新します。

※この記事は [JointJS Advent Calendar 2023](https://qiita.com/advent-calendar/2023/jointjs) の記事です。他の記事を読む場合はカレンダーのページを参照してください。

https://qiita.com/advent-calendar/2023/jointjs

[^1]: 正確にはファクトリメソッドな気がしますが、公式ドキュメントには`constructor`の表現で記載されていたのでそちらを踏襲します。