Vue と JointJS でトイ・ダイアグラムエディタ

Vue 人気ありますね。あまり真剣に触ったことがなく ToDo サンプルとかだとどうも分かった気になれない。ということで JointJS 使って CodePen で書いたトイ・ダイアグラムエディタを題材に構造を検討してみます。

blog.kondoumh.com

JointJS は Backbone ベースなので Vue にうまく混ぜられるのかなあと思いましたが、あっさり表示できました。data に graph オブジェクトを保持して mounted で joint.dia.Graph オブジェクトを設定すれば、methods 内のメソッドから graph オブジェクト経由でダイアグラムにノードを追加することができるようになりました。

JointDia.vue 抜粋

<template>
  <div>
    <div ref="myholder"></div>
    <input type="text" placeholder="new node name" v-model="nodeName" />
    <input type="button" v-on:click="addNode" value="add node" />
  </div>
</template>

<script>
  export default {
    mounted() {
      this.graph = new joint.dia.Graph
      let paper = new joint.dia.Paper({
        el: this.$refs.myholder,
        model: this.graph,
      }
    },
    data() {
      return {
        graph: {}
      }
    },
    methods : {
      addNode() {
        const rect = new joint.shapes.standard.Rectangle()
        rect.addTo(this.graph)
      }
    }
</script>

一通り動きましたが、ダイアグラム部分とノード登録の Form 部分が分かれていないので両者のデータ、メソッドが混然一体となっています。Vue らしく別コンポーネントにしたいところです。

Form 部分を InputForm.Vue に切り出しました。追加するノード名を nodeName というデータで保持し、ボタンのクリック時 $emit で親コンポーネント (App.vue) に通知するようにしました。

<template>
  <div>
    <div>
      <input type="text" placeholder="new node name" v-model.trim="nodeName" />
      <input type="button" v-on:click="add" value="add node" />
    </div>
  </div>
</template>

<script>
  export default {
    name: 'InputForm',
    data() {
      return {
          nodeName: '',
      }
    },
    methods: {
      add() {
        this.$emit('addNode', this.nodeName)
        this.nodeName = ''
      }
    }
  }
</script>

App.vue では InputForm と JointDia をコンポーネントとして利用しています。InputForm の v-on 属性で InputForm で emit したイベントハンドラーの関数 addNode を登録しています JointDia の v-bind: 属性で JointDia の nodeName プロパティと App.vue の data である nodeName をバインドしています。addNode 関数では、nodeName 受け取ったデータで書き換えます。これにより、JointDia コンポーネントの nodeName プロパティが書き換わります。

<template>
  <div id="app">
    <InputForm v-on:addNode="addNode" />
    <JointDia v-bind:nodeName="nodeName" />
  </div>
</template>

<script>
import JointDia from '@/components/JointDia'
import InputForm from '@/components/InputForm'

export default {
  name: 'App',
  components: {
    JointDia,
    InputForm
  },
  data() {
    return {
      nodeName: ''
    }
  },
  methods: {
    addNode(name) {
      this.nodeName = name
    }
  }
}
</script>

JointDia.vue の抜粋。プロパティ nodeName の変化を watch し変化時に Graph にノードを追加しています。

<template>
  <div>
    <div ref="myholder"></div>
  </div>
</template>

<script>
  import joint from 'jointjs'

  export default {
    name: 'JointDia',
    mounted(){
    },
    data() {
    },
    props: {
      nodeName: String,
    },
    watch: {
      nodeName: {
        handler (newVal, oldVal) {
          this.addNode(newVal)
        }
      }
    },
    methods: {
      addNode(name) {
      }
    }
  }
</script>

コンポーネントを分割すると、データ伝搬のコードが増えますが、コンポーネント内部は自身の責務に集中できてシンプルになる感じです。Vue らしいもっとすっきりした書き方があるかもしれませんが、ひとまずコードは CodeSandbox に置きました。

codesandbox.io

新規追加のノード名だけではなくダイアグラム内のノードのコレクションも管理する場合、JointJS のデータ構造とマッピングするためのコードを追加していく必要があります。