Google Closure Library と Closure Compiler を思い出しながら ES6 を書く


# Google Closure Tools
https://developers.google.com/closure/
https://developers.google.com/closure/compiler/
https://developers.google.com/closure/library/
8年ぐらい前に話題になったやつ。
Closure Library は YUIとかExtJSとか、JavaScriptでGUIアプリを作るためのライブラリ。
Closure Compiler はES6をトランスパイルするのにも使えるようになった。未使用関数を削除したりインライン展開したりもするのでトランスパイルより名前の通りコンパイルが近いイメージ。

今でも淡々と更新されていて、特に宣伝はされていないので流行っていない。8年ぐらい前も流行るところまでいけてなかった気がする。
フレームワークを作るためのライブラリ集という感じで、UIですぐ使える便利機能というものは無い。特にUI系ライブラリのデザインは当初のものと同じなので、そのまま使うと8年前のGoogle系サービスの管理画面のようなデザインになる。

5年ぐらい触ってなかったのでリハビリする。
ES6で書くのはChrome拡張以外では初めてなのでそれも込みで。


## アコーディオンコンポーネントを作る

jQueryの`slideToggle`のようなもの。
`goog.ui.Component` を継承して書く。

```js
goog.module('app.anim.Accordion');

const Component = goog.require('goog.ui.Component');
const EventHandler = goog.require('goog.events.EventHandler');
const EventType = goog.require('goog.events.EventType');
const AnimEvent = goog.require('goog.fx.Animation.EventType');
const PredefinedEffect = goog.require('goog.fx.dom.PredefinedEffect');
const style = goog.require('goog.style');
const log = goog.require('app.log');

const CSS_CLASS_BODY = goog.getCssName('js-accordion-body');

class Accordion extends Component
{
  /**
   * @param {Element} inputEl hidden switch input element
   * @param {number} time mili sec
   * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
   */
  constructor(inputEl, time, opt_domHelper){
    log('Accordion#constructor');

    super(opt_domHelper);

    /**
     * radio or checkbox
     * @type {Element}
     * @private
     */
    this.inputEl_ = inputEl;

    /**
     * duration
     * @type {number}
     * @private
     */
    this.time_ = time;

    /**
     * animation logic
     * @type {AccordionAnim}
     * @private
     */
    this.anim_ = null;

  }

  /**
   * TODO: 動作未確認
   * @override
   */
  createDom(){
    this.setElementInternal(
      this.getDomHelper().createDom(
        goog.dom.TagName.DIV, CSS_CLASS_BODY));
  }

  update(e){
    if (this.anim_)
      this.anim_.dispose();

    const el = this.getElement();
    this.anim_ = new AccordionAnim(el, this.time_, this.inputEl_.checked);
    this.anim_.play();
  }

  /**
   * @override
   */
  enterDocument(){
    log('Accordion#enterDocument');
    super.enterDocument();

    this.getHandler().listen(this.inputEl_, EventType.CHANGE, this.update);
  }

  /**
   * @override
   */
  exitDocument(){
    log('Accordion#exitDocument');

    this.getHandler().unlisten(this.inputEl_, EventType.CHANGE, this.update);
    if (this.anim_)
      this.anim_.dispose();

    super.exitDocument();
  }


  /**
   * @override
   */
  disposeInternal(){
    log('Accordion#disposeInternal');
    this.inputEl_ = null;
    this.anim_ = null;

    super.disposeInternal();
  }
}
```
初期からあまり変わっていない`goog.ui.Component`を`extends`で継承しても特に問題なさそう。prototypeベースの親クラスにも`goog.base`の代わりに`super`で動く。

重いと嫌なので終了処理は細かく書いた。この辺の解説記事はほぼ無いと言っても過言ではない。
[goog.ui.Componentの継承で気をつけるべきこと一覧](https://blog.kengo-toda.jp/entry/2014/10/24/100557)
[goog.ui.Componentのはぐれかた](https://www.slideshare.net/stakamur/closure-library)
昔の記事で、この二つぐらい。
まあ構造はずっと変わっていないようなので昔の本を読むのが良いのかもしれない。

このコードは上記リンク先と違って出力したHTMLに後から`decorate`で対応させる用。HTMLが無い状態でDOM生成にも対応させるなら他のコントロール系コンポーネントのようにコンストラクタで`inputEl`じゃなくて何らかのコントローラークラスを受け取るのが良さそう。今回は使わないので非対応。


アニメーションクラス。

```js
/**
 * @extends {goog.fx.dom.PredefinedEffect};
 */
class AccordionAnim extends PredefinedEffect {

  /**
   * @param {Element} el content element
   * @param {number} time mili sec
   * @param {boolean} toOpen if true then open animation
   */
  constructor(el, time, toOpen){
    log('AccordionAnim#constructor');

    const start = toOpen ?
          0 : goog.style.getSize(el).height;
    const end = toOpen ?
          goog.style.getSize(el).height : 0;

    goog.style.setStyle(el, 'display', 'block');
    goog.style.setStyle(el, 'height', start + 'px');

    super(el, [start], [end], time);



    this.el_ = el;
    this.eh_ = new EventHandler(this);
    this.eh_.listenOnce(this, AnimEvent.END, function(e){
      if (!toOpen)
        goog.style.setStyle(el, 'display', '');
      goog.style.setStyle(el, 'height', '');
    });
  }

  /**
   * @override
   */
  updateStyle(){
    const height = this.coords[0];

    goog.style.setStyle(this.el_, 'height', height + 'px');
  }

  /**
   * @override
   */
  disposeInternal(){
    log('AccordionAnim#disposeInternal');

    this.eh_.dispose();
    super.disposeInternal();
  }
}

exports = Accordion;
/** @const {string} */
exports.CSS_CLASS_BODY = CSS_CLASS_BODY;
```

ここでしか使わないヘルパー的な処理なので同じファイルの下側に追記した。
DOMいじってる感がすごくある。でもES6でJavaっぽくなって、Closure Libraryの設計自体が元々Javaっぽいから、これはこれで書きやすくなったのではないかな？


このクラスを使うコードはこんな感じ。

```js
// ... snip ...

const dom = goog.require('goog.dom');

const CSS_CLASS_AREA = goog.getCssName('js-accordion-area');
const CSS_CLASS_INPUT = goog.getCssName('js-accordion-input');

class App {

  // ... snip ...

  decorateAccordion(el){
    this.accs_ = [];
    goog.array.forEach(dom.getElementsByClass(CSS_CLASS_AREA, el), function(a){
      const inputEl = dom.getElementByClass(CSS_CLASS_INPUT, a);
      const bodyEl = dom.getElementByClass(Accordion.CSS_CLASS_BODY, a);

      if (!inputEl || !bodyEl)
        return;

      const acc = new Accordion(inputEl, 500);
      acc.decorate(bodyEl);

      this.accs_.push(acc);
    }, this);
  }
}
```

ES6が使えると言っても`Array.from`とか`map`とかを直接使うのは危険。ユーティリティ系関数を使うとブラウザサポートに合わせて切り替えるコードが出力されるのでそっちを使う。

元コードはかなり冗長だけれど、コンパイル後は

```js
        var c = Ua("w-x-y", b || a.a);
        b = Ua("w-x-z", b || a.a);
        c && b && (c = new Z(c,this.l),
        Wb(c, b),
        this.c.push(c))
```

のように展開される。これは最後の呼び出し部分。
`goog.getCssName` が展開され、その定数もインライン展開されている。

ここのCSSは

```css
.js-accordion-area
{
    margin: 0;
}
.js-accordion-input
{
    display: none;
}
.js-accordion-input:not(:checked) ~ .js-accordion-body
{
    display: none;
}

.js-accordion-body
{
    overflow: hidden;
}
```

このような、よくあるJavaScript無しで表示/非表示を切り替えるCSS。


jQueryなら3行くらいで書けるものがこんなに長大になった。大抵のUI系ライブラリは用意されている機能を使って楽して早く作りたい人向け、Closure Libraryは自前実装したい人向けというか。
一応似たようなコンポーネントで`goog.ui.Zippy`というクラスはあるんだけれど、これはなんか動きが違う。


HTMLは

```html
<div class="@css('css-accordion-area')">
  <input type="checkbox" id="ac1" value="1" class="@css('js-accordion-input')">
  <label for="ac1" class="@css('title')">タイトル１</label>
  <div class="@css('js-accordion-body')">
    コンテンツ１
  </div>
</div>
<div class="@css('css-accordion-area')">
  <input type="checkbox" id="ac2" value="1" class="@css('js-accordion-input')">
  <label for="ac2" class="@css('title')">タイトル２</label>
  <div class="@css('js-accordion-body')">
    コンテンツ２
  </div>
</div>
```

こんな感じ。
`@css`はサーバー側の関数呼び出しでClosure Stylesheetsの短縮形を出力している。使わずに普通のテキストでも良い。その場合は`goog.getCssName`も使わない。
JavaScript無しだと単なる表示/非表示切り替えのところを、スライドアニメーションを後から追加する形になる。

## メモ

基本的に `goog.provide` を `goog.module` と `exports` に、`goog.require` はそのまま使える。

モジュールからスクリプトを呼び出す場合は普通に`goog.require`で良い。
スクリプトからモジュールを呼び出すには`goog.scope`の中で`goog.module.get`が必要。
https://github.com/google/closure-library/wiki/goog.module:-an-ES6-module-like-alternative-to-goog.provide

closure-compiler.jar のオプションはかなりややこしい。これは試すしかない。

モジュールは`this`が`window`ではないが、Closure Tools はグローバル変数を結構使う。
モジュールじゃない設定系のファイルを作って読み込んだり、Closure Stylesheetsで出力した`renaming_map.js`を読み込むために`goog.provide('renamingcss')`を追記したりした。グローバルの処理は全部実行してくれると思っていたんだけど、ES6と組み合わせると`require`しないと使えない？この辺の情報は全然分からなくて、推理力が試される。

あと素っ気ないAPIドキュメントがあるだけでマニュアルのようなものは無いので、困った時はソースコードを読むのが結局は早い。コード量は膨大だが昔ながらのJavaScriptで1ファイルが短いのですぐ読める。