AngularとjsPlumbでドラッグ可能なページを作成

# はじめに
Node-redやAzureのLogic Appsなど、ブラウザ上でノードとエッジを表現したノンコーディングの開発サービスが多く存在しています。
このようなアプリケーションを自分でも作成しようと思い、どうせモダンなWebアプリケーションを目指すならAngularと組み合わせて作ってみることにしました。

## jsPlumbとは？

ブラウザ上でユーザーが自由にドラッグ可能な要素を作成したり、ノード同士を矢印で繋いだりすることが可能なJavascriptライブラリです。
無償のCommunityバージョンと有償のToolkitがありますが、今回は無償のCommunityバージョンを使います。
https://github.com/jsplumb/jsplumb

# 使用環境

node 8.11.2
npm 5.6.0
AngularCLI 8.0.3

エディタはVisual Studio Code 1.35を使っています

# 目標

HTML中のp要素同士をエッジで繋ぎ、画面内をドラッグできるアプリケーションを作ります

# 前提

AngularCLIを導入済。
ng new コマンドとng serveコマンドで初期画面表示まで出来ていること

#手順

はじめにドラッグ有効化の部分を作成し、その後、エッジで接続していく流れで作っていきます。

## ドラッグ有効化

### jsPlumbのインストール

AngularプロジェクトにjsPlumbをインストールします。

`npm install jsplumb`

### jsPlumb用のサービスクラスを作成

今回はjsPlumbを直接呼び出すサービスクラスを作成し、
コンポーネントからはサービスを利用します。
以下のコマンドでjsPlumbのサービスクラスを作成します。

`ng generate service jsPlumb`

次にサービスクラスを次のように変更します

```typescript:jsPlumb.service.ts
import { Injectable } from '@angular/core';
import { jsPlumb, jsPlumbInstance } from 'jsPlumb';

@Injectable({
  providedIn: 'root'
})
export class JsPlumbService {
  instance: jsPlumbInstance;

  constructor() { 
    this.instance = jsPlumb.getInstance();
  }

}
```

具体的には以下の変更を行っています。

- jsPlumb, jsPlumbInstanceのインポート
- instanceプロパティの追加
- instanceプロパティの初期化

jsPlumbのNPMパッケージにindex.d.tsが既に入っていますので、
Typescriptでそのまま利用することができます。

jsPlumbではインスタンスを通してDOMを処理します。
後でもう少し必要な処理を追加しますがここでは一旦HTMLの編集へ進みます。

### ドラッグさせたいHTML要素の追加

ドラッグさせたい要素をHTMLへ追加します。
今回はデフォルトプロジェクトに入っているapp.component.htmlに以下のように要素を追加しました。

```html:app.component.html
<div id="canvas">
  <p id="node1">
    ノード１
  </p>
  <p id="node2">
    ノード２
  </p>
</div>
```

### ドラッグさせたい要素をabsoluteにする

先ほど追加したp要素をドラッグできるようにしますが、ドラッグする要素はposition: absolute属性をつけなくてはなりません

https://github.com/jsplumb/jsplumb/wiki/dragging

> You must set position:absolute on elements that you intend to be draggable

そこで、今回は以下のようにCSSを編集します

```css
.canvas{
        position: relative;
        padding: 0.5em 1em;
        margin: 1em 0;

}
.canvas p {
        position: absolute;
        margin: 0; 
        padding: 0;
        background: #d1f2ff;
        border-left: solid 6px #2acfc7;
        box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.5);
}

#node1 {
        left: 20px;
        top: 0px;
}

#node2 {
        left: 20px;
        top: 50px; 
}
```

position: absoluteにしてしまうとノード１とノード２が被ってしまうので位置を少し変えています

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136146/709a5aae-6305-4e92-3d0b-45c0d6cd411a.png)

### サービスクラスにドラッグを有効にする機能を追加する

HTML要素に対してドラッグを有効化するメソッドを追加します。

jsPlumbServiceクラスにsetDraggableメソッドを追加します。
ここでは、jsPlumbInstanceのdraggableを呼び出しているだけです。

```typescript:jsPlumb.service.ts
  setDraggable(id: string): void {
    this.instance.draggable(id);
  }
```

このメソッドに対してドラッグを有効化したいIDを渡してあげることでドラッグが有効化されます。

### componentクラスから呼び出す

コンポーネントにjsPlumbServiceをDIし、ドラッグを有効化するsetDraggableを呼び出します。
setDraggableを呼び出すタイミングは、HTML要素の読み込みが完了してから出なくてはならないので、ngAfterViewInitを実装するように、AfterViewInitをimplementsしています。

```typescript:app.component.ts
import { Component, AfterViewInit } from '@angular/core';
import { JsPlumbService } from './jsPlumb.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit {
  title = 'jsplumbApp';

  constructor(private jsPlumbService: JsPlumbService){}

  ngAfterViewInit() {
    this.jsPlumbService.setDraggable("node1");
    this.jsPlumbService.setDraggable("node2");
  }
}
```

### 実行してみる

以上の状態で実行してみると、2つのノードが自由に動かすことができるようになっているはずです。

![draggableNode.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136146/d164079f-237d-1c41-da41-8f58ce1f0705.gif)

## エッジの接続

### サービスクラスにエッジ接続機能を追加

2つのノードをエッジで接続する機能をサービスクラスに追加します。

```typescript:jsPlumb.service.ts
  connect(
    srcId: string, 
    targetId: string, 
    srcAnchorPosition: AnchorSpec, 
    targetAnchorPosition: AnchorSpec): void {
      this.instance.connect({
        source: srcId,
        target: targetId,
        anchors: [srcAnchorPosition, targetAnchorPosition]
      });
  }
```

AnchorSpecクラスは、ノードのどの部分をエッジで接続するかを規定するものです。これもimportが必要です

```typescript:jsPlumb.service.ts
import { jsPlumb, jsPlumbInstance, AnchorSpec } from 'jsPlumb';
```

### componentクラスから呼び出す

作成したメソッドをコンポーネントクラスから呼び出します。
呼び出す場所はsetDraggableと同じく、ngAfterViewメソッド内です

```typescript:app.component.ts
  ngAfterViewInit() {
    this.jsPlumbService.setDraggable("node1");
    this.jsPlumbService.setDraggable("node2");
    this.jsPlumbService.connect("node1", "node2", "RightMiddle", "RightMiddle");
  }
```

先ほど説明したAnchorSpecについては右側の中央を示すRightMiddleを設定しています。使用可能なAnchorの一覧はindex.d.tsを見ると以下のように記載されています

```typescript:index.d.ts
    type AnchorId =
        "Assign" |
        "AutoDefault" |
        "Bottom" |
        "BottomCenter" |
        "BottomLeft" |
        "BottomRight" |
        "Center" |
        "Continuous" |
        "ContinuousBottom" |
        "ContinuousLeft" |
        "ContinuousRight" |
        "ContinuousTop" |
        "Left" |
        "LeftMiddle" |
        "Perimeter" |
        "Right" |
        "RightMiddle" |
        "Top" |
        "TopCenter" |
        "TopLeft" |
        "TopRight";
```

### 実行してみる

ここまで完了した状態で実行すると、以下のようにノード同士がエッジで接続された状態になるはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136146/0684e156-fc20-88a0-a7c2-bbb7dfb705e6.png)

# 参考

https://qiita.com/dog-ears/items/54f776da6c170663b3a2