2016/10/22

JS製テキストエディタAceをVue.js2.0のコンポーネントとして使う方法

Vue.jsでWebサービスを作っているとき、JavaScrip製のテキストエディタ「Ace」を使いたくて検索していたら「vue-ace-editor」というパッケージを見つけた。
しかし、いざ実装しようとしたら動かない。コードを読んでみるとVue.js1.x基準で書かれており、2.0ではDeplicatedになっていたりで古いままだった。

ということで、自分でAceのVueコンポーネントをつくった。


使用するバージョンは以下のとおり

requireやimportで読み込むことができる「brace」というパッケージもあったが、あまり更新されていないようなので本家からコードをダウンロードしてきて使っている。
そのため、index.htmlなどで<script src="./<任意のディレクトリ>/ace.js"></script>と指定する必要があるので注意。

Ace用のVueコンポーネントをつくる


<div id="app">
  <h1>Ace editor for Vue.js2.0 Components</h1>
  <p>Editor A</p>
  <div style="height: 100px">
    <editor editor-id="editorA" :content="contentA" v-on:change-content="changeContentA"></editor>
  </div>
  <p class="result">{{contentA}}</p>
  <p>Editor B</p>
  <div style="height: 100px">
    <editor editor-id="editorB" :content="contentB" v-on:change-content="changeContentB"></editor>
  </div>
  <p class="result">{{contentB}}</p>
  <button @click="reset">Reset</button>
</div>

Vue.component('Editor', {
  template: '<div :id="editorId" style="width: 100%; height: 100%;"></div>',
  props: ['editorId', 'content', 'lang', 'theme'],
  data () {
    return {
      editor: Object,
      beforeContent: ''
    }
  },
  watch: {
    'content' (value) {
      if (this.beforeContent !== value) {
        this.editor.setValue(value, 1)
      }
    }
  },
  mounted () {
    const lang = this.lang || 'text'
    const theme = this.theme || 'github'
  
    this.editor = window.ace.edit(this.editorId)
    this.editor.setValue(this.content, 1)
    
    // mode-xxx.js or theme-xxx.jsがある場合のみ有効
    this.editor.getSession().setMode(`ace/mode/${lang}`)
    this.editor.setTheme(`ace/theme/${theme}`)

    this.editor.on('change', () => {
      this.beforeContent = this.editor.getValue()
      this.$emit('change-content', this.editor.getValue())
    })
  }
})

const app = new Vue({
  el: "#app",
  data () {
    return {
      contentA: 'default content for Editor A',
      contentB: 'default content for Editor B'
    }
  },
  methods: {
    reset () {
      this.contentA = 'reset content for Editor A'
      this.contentB = 'reset content for Editor B'
    },
    changeContentA (val) {
      if (this.contentA !== val) {
        this.contentA = val
      }
    },
    changeContentB (val) {
      if (this.contentB !== val) {
        this.contentB = val
      }
    }
  }
})


実際に実装すると以下のようになる。

Vue Componentの解説



Aceについて


Aceの初期化は、mountedで行っている。
基本的に ace.edit('Element ID') するだけで、エディタとして利用することができる。

editor.setValue(セットする値、カーソルポジション)で、エディタに値をセットできる。
カーソルポジションは、0 or undefinedなら全選択、-1なら先頭、1なら末尾といった具合だ。

今回は、ace.jsしかインポートしていないのでモードやテーマはデフォルトになっている。
editor.getSession().setMode('ace/mode/{MODE}')をすると、モードにあわせたシンタックスハイライトが有効になる。
editor.setTheme('ace/theme/{THEME}')をすると、エディタのテーマを変更できる。有名なのはgithubやmonokaiとか。

editor.on('change', () => { ... })でエディタの内容が変更されたときのイベントをキャッチすることができる。


親コンポーネントのデータが変更されたときにエディタに反映する


これについては、次の記事を参照してほしい。
親コンポーネントのデータに変更があった場合にエディタ内容を変更するために、watchを定義している。
このwatchは親コンポーネントから渡ってくるcontentを監視して、変更があったら実行されるイベントだ。また無限ループを避けるためにひとつ前の値を保持しておき、エディタ内容を書き換えるかどうかを制御している。


エディタの内容が変更されたときに親コンポーネントにデータを渡す


これについては、後日詳細な記事を書こうと思う。
軽く説明しておくと、子コンポーネントから親コンポーネントにデータを渡す場合は、Emit Eventsを利用する。データを渡したいときにthis.$emit(イベント名, 値)のようにしてイベントを発火させ、親コンポーネントのイベントリスナーで受け取ってデータを書き換える。

今回の例でいくと、this.editor.on('change', () => { this.$emit('change-content', this.editor.getValue() }のところだ。
Propsで親コンポーネントからデータを受け取っている。


追記: 2016/10/26 10:00
Vue.js 2.x系で親子コンポーネント間のデータのやりとりについて、サンプルコードと図解を交えて解説している。



おまけ:vue-loaderを使う場合のコンポーネント



<!-- Editor.vue -->
<template>
  <div :id="editorId" style="width: 100%; height: 100%;"></div>
</template>

<script>
export default {
  props: ['editorId', 'content', 'lang', 'theme'],
  data () {
    return {
      editor: Object,
      beforeContent: ''
    }
  },
  watch: {
    'content' (value) {
      if (this.beforeContent !== value) {
        this.editor.setValue(value, 1)
      }
    }
  },
  mounted () {
    const lang = this.lang || 'text'
    const theme = this.theme || 'github'
  
    this.editor = window.ace.edit(this.editorId)
    this.editor.setValue(this.content, 1)
    
    // mode-xxx.js or theme-xxx.jsがある場合のみ有効
    this.editor.getSession().setMode(`ace/mode/${lang}`)
    this.editor.setTheme(`ace/theme/${theme}`)

    this.editor.on('change', () => {
      this.beforeContent = this.editor.getValue()
      this.$emit('change-content', this.editor.getValue())
    })
  }
}
</script>



参考サイト






以上

written by @bc_rikko

0 件のコメント :

コメントを投稿