LoginSignup
11
8

More than 5 years have passed since last update.

AtomパッケージでGUIを作る(etchの紹介)

Last updated at Posted at 2017-12-16

Atomアドベントカレンダー2017の17日目の記事です。
ぼちぼち記事を書いてくださる方が増えてきてとても嬉しいです。ありがとうございます!!

Atomは主にHTML、CSS、javascriptで構成されているエディタなのでサードパーティのパッケージも独自のGUIをHTMLで作成が可能です。
パッケージを作るためのPackage Generatorによって作成されるテンプレートではtoggleというコマンドを実行するとモーダルが表示されるのですが、そのモーダルはローレベルな標準APIを使用してこのようにDOMを組み立てています。

my-atom-package-template-view.js
// コンストラクタ部分だけ抜粋
constructor(serializedState) {
  // Create root element
  this.element = document.createElement('div');
  this.element.classList.add('my-atom-package-template');

  // Create message element
  const message = document.createElement('div');
  message.textContent = 'The MyAtomPackageTemplate package is Alive! It\'s ALIVE!';
  message.classList.add('message');
  this.element.appendChild(message);
}

シンプルなUIであれば問題はありませんが、ある程度複雑なUIを構築しようとすると少々厳しいものがあります。

Atom界隈ではatom-space-pen-viewsというViewを作るためのヘルパーが使われていたのですが、READMEから分かるようにCoffeeScriptがメインだった時代で止まっており最近は更新もされていません。

代わりに現在ではetchというモジュールが使われています。これはJSXとVirtualDOMを使ってViewを作ることができるものであり、Reactを非常にシンプルにしたようなモジュールとなっています。

おそらくReactやVue.jsを使うこともできますが(試したことないけど大変そう)、たかだかエディタの拡張機能のGUIなのでWebアプリよりは圧倒的にシンプルなUIで問題ないでしょう。
etchはReactなどに比べて機能は少ないですが、その分覚えることも少なくすぐに使い始めることができます。
今回はPackage Generatorによって作成されるパッケージの最初のテンプレートを題材にetchの使い方を紹介していきます。

etchのセットアップ

事前準備として、etchはAtomには標準で組み込まれていないのでnpm i --save-dev etchを実行して自分で追加する必要があります。

etchのインストールが終わったら、まずは手始めにPackage Generatorによって生成された最初のテンプレートをetchで書き直してみましょう。このようなコードになります。

my-atom-package-template-view.js
'use babel';
/** @jsx etch.dom */

import etch from 'etch'

export default class MyAtomPackageTemplateView {

  constructor(props) {
    this.props = props
    etch.initialize(this)
  };

  // jsx
  render () {
    return (
      <div class="my-atom-package-template">
        <div class="message">
          The MyAtomPackageTemplate package is Alive! It's ALIVE!
        </div>
      </div>
    )
  };

  // DOMを更新する
  update (props) {
    return etch.update(this)
  }

  // このViewを破棄するときはetchの管理下から外す
  async destroy() {
    await etch.destroy(this)
  }

  // 必須ではないが今回はテストコードなど既存部分の修正を避けるために残しておく
  getElement() {
    return this.element;
  }

非常にシンプルですね!これだけでDOMを構築して更新することが可能です。
注意点としては最初の'use babel';/** @jsx etch.dom */の2行はAtomが必要とするようなので忘れないようにしましょう。

このViewクラスを使用する方のコードはこのようになります。

my-atom-package-template.js
// my-atom-package-template.jsから抜粋
  activate(state) {
    this.myAtomPackageTemplateView = new MyAtomPackageTemplateView(state.myAtomPackageTemplateViewState);
    this.modalPanel = atom.workspace.addModalPanel({
      item: this.myAtomPackageTemplateView.getElement(),
      visible: false
    });

MyAtomPackageTemplateViewのコンストラクタの中でetch.initialize(this)を実行するとに自動的にrender()が実行されてthis.elementに描画されたHTMLがセットされます。
後はgetElement()で取り出してDOMを追加したい場所に埋め込むだけです。上のコードではaddModalPanel()itemにDOMを渡すことでモーダルに表示されるHTMLとしてセットしています。

これでetchへの置き換えは完了です!Atomを再起動してモーダルを開いてみましょう。見た目は全く変わっていませんが、内部ではetchによってHTMLが構築されています。

etch_1.png

変数を使ってDOMを更新する

次は変数を使ってDOMを更新してみましょう。今回はモーダルを表示するたびにカウントアップしていくカウンターを作成していきます。

まずはViewを使う側のメインモジュールでcountを保持して、モーダルを表示するたびにインクリメントさせます。
そしてViewのコンストラクタとupdateするときにカウントの変数を渡します。この変数は後ほどView側でDOMを構築する際に使用します。

my-atom-package-template.js
// 変更した部分だけ抜粋。全体は以下のurl参照
// https://github.com/Kesin11/MyAtomPackageTemplate/blob/43c7a87fd0569638d87b3c034346aa8d90617801/lib/my-atom-package-template.js

constructor () {
  // ... 省略

  this.count = 0
}

async activate() {
  this.myAtomPackageTemplateView = new MyAtomPackageTemplateView({count: this.count})

  // ... 省略
}

toggle() {
  this.count += 1
  this.myAtomPackageTemplateView.update({count: this.count})

  // ... 省略
}

View側では引数として受け取ったオブジェクトをthis.propsとして保持しておき、JSXで変数を埋め込むだけです。
ReactやVue.jsを少し触ったことがあれば雰囲気で何となく分かるかと思います。

my-atom-package-template-view.js
// 説明に必要な部分だけ抜粋。全体は以下のurl参照
// https://github.com/Kesin11/MyAtomPackageTemplate/blob/43c7a87fd0569638d87b3c034346aa8d90617801/lib/my-atom-package-template-view.js

constructor(props) {
  this.props = props
  etch.initialize(this)
}

render () {
  return (
    <div class="my-atom-package-template">
      <div class="message">
        The MyAtomPackageTemplate package is Alive! It's ALIVE!
      </div>
      <div>count: {this.props.count}</div>
    </div>
  )
}

update (props) {
  this.props = props
  return etch.update(this)
}

これだけでcountの変数によって表示される内容を変えることができました。toggleコマンドでモーダルを呼び出すたびにカウントが増えていきます。

etch_2.png

Componentの親子関係を作る

ReactはUI上のパーツを細かくコンポーネントとして分割する思想ですが、同じことがetchでも可能です。
カウントを表示する部分を別のコンポーネントとして切り出してみましょう。

my-atom-package-template-view.js
// 説明に必要な部分だけ抜粋。全体は以下のurl参照
// https://github.com/Kesin11/MyAtomPackageTemplate/blob/e6883be04f111cd3a8cb1c894803788ec52eac35/lib/my-atom-package-template-view.js

export default class MyAtomPackageTemplateView {
  // .. 省略

  // カウンタ部分をCounterComponentとして子コンポーネントに変更
  render () {
    return (
      <div class="my-atom-package-template">
        <div class="message">
          The MyAtomPackageTemplate package is Alive! It's ALIVE!
        </div>
        <CounterComponent childcount={this.props.count}/>
      </div>
    )
  }

  // .. 省略
}

// カウンタ部分を別のコンポーネントとして切り出し
class CounterComponent { // eslint-disable-line no-unused-vars
  constructor (props) {
    this.props = props
    etch.initialize(this)
  }

  render () {
    return (
      <div>count: {this.props.childcount}</div>
    )
  }

  // 親コンポーネントが更新されたときに自動的に呼ばれる
  // propsは親に記述されたchildcount={this.props.count}がオブジェクトとして渡される
  update (props) {
    this.props = props
    return etch.update(this)
  }
}

親コンポーネントとは別のクラスとしてコンポーネントを定義するとJSXの中でHTMLのタグのように扱うことができます。
<CounterComponent childcount={this.props.count}/>のようにコンポーネントのタグの中でkey={value}の形式で記述することにより子コンポーネントに親から変数を渡すことが可能です。

イベントハンドリング

最後にボタンをクリックしたときなどのイベントをハンドリングする方法を紹介します。
今回はボタンをクリックしたときにメッセージを変化させてみましょう。

my-atom-package-template-view.js
// 説明に必要な部分だけ抜粋。全体は以下のurl参照
// https://github.com/Kesin11/MyAtomPackageTemplate/blob/f0dfe1fbce020862b622e4ef884939e73b0e6eb5/lib/my-atom-package-template-view.js

export default class MyAtomPackageTemplateView {
  // .. 省略
    render () {
    return (
      <div class="my-atom-package-template">
        <div class="message">
          The MyAtomPackageTemplate package is Alive! It's ALIVE!
        </div>
        <CounterComponent childcount={this.props.count}/>
        <div>{this.message}</div>
        <br/>
        <button on={{click: this.didClick}}>didClick</button>
        <button onClick={() => this.onClick()}>onClick</button>
      </div>
    )
  }

  didClick (event) {
    console.log(event) // eslint-disable-line no-console
    this.message = 'didClick'
    this.update(this.props)
  }

  onClick () {
    this.message = 'onClick'
    this.update(this.props)
  }

  // .. 省略
}

それぞれのボタンをクリックするとテキストがdidClickとonClickに変化します。

out.gif

イベントをハンドリングするには2通りの書き方が可能でクリックの場合はこのように書きます。

  • on={{click: func_name}}
  • onClick={() => this.func_name()}

2つの書き方による違いですが、onの場合は発生したイベントを引数として参照できる点が異なります。もしイベントを参照する必要がないのであればどちらの書き方でもかまわないでしょう。

終わりに

以上がetchの使い方のほぼ全てです。とてもシンプルですがVirtualDOMとJSXによるモダンなViewフレームワークの基本機能はしっかり実現しているモジュールです。
GUIを含むAtomのパッケージを作成する場合にはとても役に立つでしょう。

より詳しい使い方を調べたい方はetchのREADMEや、Atom本家で使われているパッケージでetchがどのように使われているかソースコードを検索してみるのが良いでしょう。

11
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
8