home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

JavaScript/ExtJS/Ext.Component を使ったカスタムコンポーネントの練習 (v1)

JavaScript/ExtJS/Ext.Component を使ったカスタムコンポーネントの練習 (v1)

JavaScript / ExtJS / Ext.Component を使ったカスタムコンポーネントの練習 (v1)
id: 1297 所有者: msakamoto-sf    作成日: 2014-07-05 22:56:34
カテゴリ: ExtJS JavaScript 

ExtJSは豊富なコンポーネントが用意されています。しかしながら、現実のアプリケーション開発ではどうしても、自分でコンポーネントを作成するようなケースが出てきます。カスタムコンポーネントの作成については、ExtJSのドキュメントにも説明はありますが、実際の開発ではHTML要素のレンダリングなどExtJSの内部を知る必要があります。
他のコンポーネントを包含しない、単体で動作するコンポーネントは一般的にExt.Componentを派生させて作成します。その時に、HTMLのレンダリング処理やイベントハンドラをどのように設定すればよいのか、イロハが分かるような資料を目指して本記事を作成しました。

環境:

  • ExtJS 5.0.0 GPLバージョン(ExtJSのCDN環境で配布されているものを使用)
  • 一部、ExtJSの内部ソースの調査では ExtJS 4.2 GPLバージョン(ext-4.2.1.883) を参照しています。

準備:Sencha公式サイトの"Hello World"を用意する。

まず、Sencha公式サイトで紹介されている"Hello World"を動かします。

index.html: 公式サイトのサンプルでは"ext-all.js"を読み込んでいますが、幸い"ext-all-debug.js"が置いてあるようでしたのでそちらに修正してます。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title></title>
  <link rel="stylesheet" type="text/css" href="http://cdn.sencha.com/ext/gpl/5.0.0/build/packages/ext-theme-neptune/build/resources/ext-theme-neptune-all.css">
  <script type="text/javascript" src="http://cdn.sencha.com/ext/gpl/5.0.0/build/ext-all-debug.js"></script>
  <script type="text/javascript" src="app.js"></script>
</head>
<body>
</body>
</html>

app.js: 公式サイトのままですので割愛します。

動作確認ができましたら、"app.js" をロードしている部分をサンプルのJSファイルに順次書き換えて、練習していきます。

HTML要素のレンダリングを試してみる。

Ext.Component で autoEl, html, tpl, renderTpl 設定を使い、どの設定がどのようなDOM要素にレンダリングされるのかステップバイステップで見ていきます。

元ネタ:

空っぽの Ext.Component

空のExt.Componentをレンダリングする、以下の様なJavaScriptファイルを作成します。

t1.js:

Ext.application({
  name   : 'MyApp',
  launch : function() {
    Ext.create('Ext.Component', {renderTo: Ext.getBody()});
  }
});

index.html: app.jsをロードしていた部分を、t1.jsをロードするよう修正します。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title></title>
  <link rel="stylesheet" type="text/css" href="http://cdn.sencha.com/ext/gpl/5.0.0/build/packages/ext-theme-neptune/build/resources/ext-theme-neptune-all.css">
  <script type="text/javascript" src="http://cdn.sencha.com/ext/gpl/5.0.0/build/ext-all-debug.js"></script>
  <script type="text/javascript" src="t1.js"></script>
</head>
<body>
</body>
</html>


ブラウザアドオンなどの開発者ツールからDOM構造をダンプすると、以下のようになりました。Ext.Componentが空っぽのdiv要素としてレンダリングされているのを確認できます。

<html lang="en">
...
<body id="ext-element-1" class="x-body x-gecko">

# ここから、quicktips系で自動的に追加されてるっぽい。
<div id="ext-quicktips-tip" class="x-tip  x-layer x-tip-default x-border-box" data-sticky="true" style="display: none;">
(長いので省略)
</div>

# これが、t1.jsで作成した Ext.Component のDOM要素
<div id="component-1010" class="x-component  x-component-default x-border-box"></div>

</body>
</html>

height, width, border, style を指定してみる

Ext.Componentのconfigurationで、height, width, border, style を指定してみると、style属性に反映されました。

t2.js:

Ext.application({
  name   : 'MyApp',
  launch : function() {
    Ext.create('Ext.Component', {
      height: 100,
      width: 200,
      border: 5,
      style: {
        borderColor: 'red',
        borderStyle: 'solid'
      },
      renderTo: Ext.getBody()
    });
  }
});


→DOM要素だけを抜き出し、属性で見やすく改行:

<div 
  id="component-1010" 
  style="border-color:red;border-style:solid;border-width:5px 5px 5px 5px;width:200px;height:100px;" 
  class="x-component  x-component-default x-border-box"></div>

html を指定してみる。

Ext.Component の html 設定を使うと、HTML文字列をそのままコンテンツとしてDOM要素に組み込まれます。

t3.js:

Ext.application({
  name   : 'MyApp',
  launch : function() {
    Ext.create('Ext.Component', {
      html: '<i>hello</i>',
      renderTo: Ext.getBody()
    });
  }
});


→DOM要素を確認:

<div id="component-1010" class="x-component  x-component-default x-border-box">
  <i>hello</i>
</div>

autoEl を指定してみる。

静的なHTML要素を構築したい場合は、autoEl 設定を使うこともできます。autoEl 設定ではExt.DomHelperで使える階層化されたオブジェクト要素を設定します。

t4.js:

Ext.application({
  name   : 'MyApp',
  launch : function() {
    Ext.create('Ext.Component', {
      autoEl: {
        tag: 'ul',
        cls: 'class-t4',
        children: [
          { tag: 'li', html: 'item1' },
          { tag: 'li', html: 'item2' },
          { tag: 'li', cn: [{ tag: 'a', href: 'http://www.example.com/', html: 'linktest'}] }
        ]
      },
      renderTo: Ext.getBody()
    });
  }
});


→DOM要素だけを抜き出し、見やすく改行:

<ul id="component-1010" class="class-t4">
<li>item1</li>
<li>item2</li>
<li><a href="http://www.example.com/">linktest</a></li>
</ul>

autoEl と html を併用してみる。

autoEl と html 設定は併用ができる。ただし、静的なHTML要素をレンダリングするだけであれば、この組み合わせを使うケースは無いかもしれません。
t5.js:

Ext.application({
  name   : 'MyApp',
  launch : function() {
    Ext.create('Ext.Component', {
      autoEl: {
        tag: 'p',
        cls: 'class-p',
        html: 'hello',
        children: [
          { tag: 'li', html: 'item1' },
          { tag: 'li', html: 'item2' },
          { tag: 'li', cn: [{ tag: 'a', href: 'http://www.example.com/', html: 'linktest'}] }
        ]
      },
      html: '<i>html configuration</i>',
      renderTo: Ext.getBody()
    });
  }
});


→DOM要素だけを抜き出し、見やすく改行:autoElのtagを<p>タグに指定してみたら、微妙に壊れてしまった・・・。

<p id="component-1010" class="class-p">
<i>html configuration</i>
hello
</p>
<li>item1</li>
<li>item2</li>
<li><a href="http://www.example.com/">linktest</a></li>
<p id="ext-element-3"></p></body>

autoEl と html と tpl と data を併用してみる。

tpl 設定と data 設定を使うと、Ext.XTemplate/Ext.Templateを使ったテンプレート処理を埋め込むことができます。これまでのautoEl, html と組み合わせてどの順番で出力されるのか確認してみます。
※なお、autoElのタグは<p>だと中にブロック要素を入れられないのか分断されてしまってたので、<div>にしました。

t6.js:

Ext.application({
  name   : 'MyApp',
  launch : function() {
    Ext.create('Ext.Component', {
      autoEl: {
        tag: 'div',
        cls: 'class-abc',
        html: '&nbsp;<s>hello</s>',
        children: [
          { tag: 'a', href: 'http://www.example.com/', html: 'item1' }
        ]
      },
      html: '<i>html configuration</i>',
      tpl: [
        '<ul>',
          '<tpl for=".">',
          '<li>{.:htmlEncode}</li>',
          '</tpl>',
        '</ul>'
      ],
      data: ['item1', 'item2', '<b>item3</b>'],
      renderTo: Ext.getBody()
    });
  }
});


→DOM要素だけを抜き出し、見やすく改行:

<div id="component-1010" class="class-abc">
<i>html configuration</i>
<ul>
<li>item1</li>
<li>item2</li>
<li>&lt;b&gt;item3&lt;/b&gt;</li>
</ul>
&nbsp;<s>hello</s>
<a href="http://www.example.com/">item1</a>
</div>

renderTpl と renderData を使ってみる。

renderTpl 設定は、後述しますが html や tpl 設定で構築した要素全体を制御する、全体的なテンプレートを設定しています。
Ext.AbstractComponentにおいて、renderTplのデフォルト値は以下のようになっています。テンプレートを使っており、この中の "this" はExt.XTemplateのインスタンスを指し、それのrenderContent()メソッドの内容を出力しています。後で見ていきますが、renderContent()は最終的に html や tpl 設定を出力します。ここでrenderTplを上書きすることで、htmlやtpl設定を無視するようなレンダリングも可能となります。

renderTpl: '{%this.renderContent(out,values)%}',

テンプレートの中では、Ext.AbstractComponent のドキュメントもあるように renderData で渡される値を使うことができます。
実際に、autoEl, renderTpl, renderData, html, tpl をすべて併用したサンプルを作ってみます。

t7.js:

Ext.application({
  name   : 'MyApp',
  launch : function() {
    Ext.create('Ext.Component', {
      autoEl: {
        tag: 'div',
        cls: 'class-abc',
        html: '&nbsp;<s>hello</s>',
        children: [
          { tag: 'a', href: 'http://www.example.com/', html: 'item1' }
        ]
      },
      html: '<i>html configuration</i>',
      tpl: [
        '<ul>',
          '<tpl for=".">',
          '<li>{.:htmlEncode}</li>',
          '</tpl>',
        '</ul>'
      ],
      data: ['item1', 'item2', '<b>item3</b>'],
      renderTpl: [
        // id, uiCls, baseCls, componentCls などは デフォルトでrenderDataとしてassign済み
        '<div id="{id}" class="{uiCls} {baseCls} {componentCls}">',
        '<h2>{title:htmlEncode}</h2>',
        '<tpl for="iter">',
        '<p>{.}</p>',
        '</tpl>',
        '{%this.renderContent(out,values)%}',
        '</div>'
      ],
      baseCls: 'myapp-custom',
      componentCls: 'myapp-component',
      renderData: {
        title: 'renderTpl() <s>test</s>',
        iter: [
          'sub-block1',
          'sub-block2',
          'sub-block3'
        ]
      },
      renderTo: Ext.getBody()
    });
  }
});


→DOM要素だけを抜き出し、見やすく改行:

<div id="component-1010" class="class-abc">
  <div class=" myapp-custom myapp-component" id="component-1010">
    <h2>renderTpl() &lt;s&gt;test&lt;/s&gt;</h2>
    <p>sub-block1</p>
    <p>sub-block2</p>
    <p>sub-block3</p>
    <i>html configuration</i>
    <ul>
      <li>item1</li>
      <li>item2</li>
      <li>&lt;b&gt;item3&lt;/b&gt;</li>
    </ul>
  </div>
  &nbsp;<s>hello</s>
  <a href="http://www.example.com/">item1</a>
</div>

autoEl, renderTpl, html, tpl の階層構造

ここまでのサンプルから、以下の様な階層構造でHTML要素が構築されていくことを確認しました。

autoEl.tag + autoEl.cls
  renderTpl + renderData
    # 以下、renderTpl中の '{%this.renderContent(out,values)%}' により出力される
    html
    tpl + data
    autoEl.html
    autoEl.children

Ext.Component#update() による動的更新

autoEl.tag の要素の中身は、Ext.Component#update()により動的に変更することが可能です。
ただし、以下のサンプルコードで試すと renderTpl + renderData も含めて、autoEl.tagの中身が丸ごと変更されてしまいました。
renderTplなど避けて、上手く特定の要素以下を update() で更新する方法については今回は未調査です。

update()の引数については、Ext.dom.Element#update()の引数と同じですので、そちらのAPIドキュメントが参考になります。第二引数の loadScripts は、HTMLソースを第一引数に指定した際、scriptタグが含まれていた場合にそれを実行するか否かを制御できるようです(デフォルトはfalseで実行しない)。
t8.js:

Ext.application({
  name   : 'MyApp',
  launch : function() {
    var cmp = Ext.create('Ext.Component', {
      autoEl: {
        tag: 'div',
        cls: 'class-abc',
        html: '&nbsp;<s>hello</s>',
        children: [
          { tag: 'a', href: 'http://www.example.com/', html: 'item1' }
        ]
      },
      html: '<i>html configuration</i>',
      tpl: [
        '<ul>',
          '<tpl for=".">',
          '<li>{.:htmlEncode}</li>',
          '</tpl>',
        '</ul>'
      ],
      data: ['item1', 'item2', '<b>item3</b>'],
      renderTpl: [
        '<div id="{id}" class="{uiCls} {baseCls} {componentCls}">',
        '<h2>{title:htmlEncode}</h2>',
        '<tpl for="iter">',
        '<p>{.}</p>',
        '</tpl>',
        '{%this.renderContent(out,values)%}',
        '</div>'
      ],
      baseCls: 'myapp-custom',
      componentCls: 'myapp-component',
      renderData: {
        title: 'renderTpl() <s>test</s>',
        iter: [
          'sub-block1',
          'sub-block2',
          'sub-block3'
        ]
      },
      renderTo: Ext.getBody()
    });
    Ext.create('Ext.Button', {
      text: 'update(data)',
      renderTo: Ext.getBody(),
      handler: function() {
        cmp.update(['item4', 'item5', 'item6']);
      }
    });
    Ext.create('Ext.Button', {
      text: 'update(html(, loadScripts : default = false))',
      renderTo: Ext.getBody(),
      handler: function() {
        cmp.update('<b>update(html)</b><script>alert("hello");</script>');
      }
    });
    Ext.create('Ext.Button', {
      text: 'update(html, loadScripts : true)',
      renderTo: Ext.getBody(),
      handler: function() {
        cmp.update('<b>update(html)</b><script>alert("hello");</script>', true);
      }
    });
  }
});


→最初に表示された段階でDOM要素だけを抜き出し、見やすく改行:

<div id="component-1010" class="class-abc">
  <div class=" myapp-custom myapp-component" id="component-1010">
    <h2>renderTpl() &lt;s&gt;test&lt;/s&gt;</h2>
    <p>sub-block1</p>
    <p>sub-block2</p>
    <p>sub-block3</p>
    <i>html configuration</i>
    <ul>
      <li>item1</li>
      <li>item2</li>
      <li>&lt;b&gt;item3&lt;/b&gt;</li>
    </ul>
  </div>
  &nbsp;<s>hello</s>
  <a href="http://www.example.com/">item1</a>
</div>

→"update(data)"ボタンをクリックすると、renderTplの要素も含めてごっそり、tpl + update()で渡した新しいデータのテンプレート処理内容に入れ替わりました。

<div id="component-1010" class="class-abc">
  <ul>
    <li>item4</li>
    <li>item5</li>
    <li>item6</li>
  </ul>
</div>


→"update(html(, loadScripts : default = false))"ボタンをクリックすると、renderTplの要素も含めてごっそり、指定したHTMLソースに入れ替わりました。scriptタグ中のJSは実行されませんでした。

<div id="component-1010" class="class-abc">
  <b>update(html)</b>
  <script>alert("hello");</script>
</div>


→"update(html, loadScripts : true)"ボタンをクリックすると、renderTplの要素も含めてごっそり、指定したHTMLソースに入れ替わりました。scriptタグ中のJSは実行されました。

<div id="component-1010" class="class-abc">
  <b>update(html)</b>
</div>


renderTo を指定して、autoEl, renderTpl + renderData, html, tpl + data がレンダリングされる仕組み

  • 実際にHTML要素をレンダリングする機能は、Ext.util.Renderable (private) クラスに集約されています。
  • Ext.Component は Ext.AbstractComponent (private) クラスから派生しており、Ext.AbstractComponentクラスには Ext.util.Renderable クラスがmixinされています。
  • Ext.Component およびその派生クラス (Ext.container.Containerなど含む) はmixinされたExt.util.Renderable クラスの render() メソッドを起点としてHTML要素のレンダリングを開始します。
renderTo が指定された場合の、constructorからの処理を追ってみる。

renderTo 設定が指定されていれば、Ext.AbstractComponent のコンストラクタ中でmixinされた Ext.util.Renderable クラスの render() メソッドが呼ばれます。
ext/src/AbstractComponent.js:

Ext.define('Ext.AbstractComponent', {
...
    constructor : function(config) {
        var me = this,
            i, len, xhooks;
...
       if (me.renderTo) {
            me.render(me.renderTo);
            // EXTJSIV-1935 - should be a way to do afterShow or something, but that
            // won't work. Likewise, rendering hidden and then showing (w/autoShow) has
            // implications to afterRender so we cannot do that.
        }
...

Ext.util.Renderable#update()では、初めてのレンダリング(= me.el が空)の場合は getRenderTree() でDOM要素の設定情報(= Ext.DomHelperが扱えるオブジェクト階層)を構築した後、Ext.DomHelperを使って親要素に追加してます。

render: function(container, position) {
        var me = this,
            el = me.el && (me.el = Ext.get(me.el)), // ensure me.el is wrapped
            vetoed,
            tree,
            nextSibling;
 
        Ext.suspendLayouts();
 
        container = me.initContainer(container);
 
        nextSibling = me.getInsertPosition(position);
 
        if (!el) {
            // 初回はelが空っぽなので、getRenderTree() でDOM要素の設定情報を構築する。
            tree = me.getRenderTree();
            if (me.ownerLayout && me.ownerLayout.transformItemRenderTree) {
                tree = me.ownerLayout.transformItemRenderTree(tree);
            }
 
            // tree will be null if a beforerender listener returns false
            if (tree) {
                // treeを構築したら、親要素に追加/挿入
                if (nextSibling) {
                    el = Ext.DomHelper.insertBefore(nextSibling, tree);
                } else {
                    el = Ext.DomHelper.append(container, tree);
                }
 
                me.wrapPrimaryEl(el);
            }
        } else {
            // 既にelが構築済みの場合の追加/挿入処理
            ...
        }
        if (el && !vetoed) {
            // レンダリングで要素の追加/購入処理が発生したら、finishRender()を呼ぶ。
            me.finishRender(position);
        }
 
        Ext.resumeLayouts(!me.hidden && !container.isDetachedBody);
    },

Ext.util.Renderable#getRenderTree() では、"beforerender"イベントが未設定 or 設定済みならイベントを発生させ、その戻り値がtrue の場合にExt.util.Renderable#getElConfig() の戻り値を返します。

getRenderTree: function() {
        var me = this;
 
        if (!me.hasListeners.beforerender || me.fireEvent('beforerender', me) !== false) {
            me.beforeRender();
 
            // Flag to let the layout's finishRenderItems and afterFinishRenderItems
            // know which items to process
            me.rendering = true;
 
            if (me.el) {
                // Since we are producing a render tree, we produce a "proxy el" that will
                // sit in the rendered DOM precisely where me.el belongs. We replace the
                // proxy el in the finishRender phase.
                return {
                    tag: 'div',
                    id: (me.$pid = Ext.id())
                };
            }
 
            return me.getElConfig();
        }
 
        return null;
    },

Ext.util.Renderable#getElConfig() では、autoElプロパティが設定されていればそれを使い、未設定であればdivタグ要素を使って、Ext.DomHelperが扱える形のDOM要素を表すオブジェクト階層を構築して返します。また、frameInfoは設定していないため、config.tplは initRenderTpl() の戻り値が返します。

getElConfig : function() {
        var me = this,
            autoEl = me.autoEl,
            frameInfo = me.getFrameInfo(),
            config = {
                tag: 'div',
                tpl: frameInfo ? me.initFramingTpl(frameInfo.table) : me.initRenderTpl()
            },
            protoEl = me.protoEl,
            i, frameElNames, len, suffix, frameGenId, frameData;
 
        me.initStyles(protoEl);
        protoEl.writeTo(config);
        protoEl.flush();
 
        if (Ext.isString(autoEl)) {
            config.tag = autoEl;
        } else {
            Ext.apply(config, autoEl); // harmless if !autoEl
        }
...
        return config;
    },

Ext.util.Renderable#initRenderTpl() では、"renderTpl"にテンプレートが設定されていれば Ext.util.Renderable#setupRenderTpl() を呼び、ばテンプレートインスタンスのセットアップを行います。
Ext.AbstractComponent においては、renderTplはデフォルトのテンプレートが設定済みなので、明示的にnullやundefinedで上書きしていない限りは、テンプレートインスタンスがセットアップされることになります。

initRenderTpl: function() {
        var tpl = this.getTpl('renderTpl');
 
        if (tpl && !tpl.renderContent) {
            this.setupRenderTpl(tpl);
        }
 
        return tpl;
    },

Ext.util.Renderable#setupRenderTpl() では、テンプレートインスタンスの "renderBocy()", "renderContent()" メソッドの実体をmixinされたExt.util.Renderableインスタンスの doRenderContent メソッドを参照するよう調整します。

setupRenderTpl: function (renderTpl) {
        renderTpl.renderBody = renderTpl.renderContent = this.doRenderContent;
    },

Ext.util.Renderable#doRenderContent() では、ここでようやく、mixinされたインスタンスに"html"か"tpl"プロパティがあれば、順繰りにDOM要素として追加していきます。

doRenderContent: function (out, renderData) {
        // Careful! This method is bolted on to the renderTpl so all we get for context is
        // the renderData! The "this" pointer is the renderTpl instance!
 
        var me = renderData.$comp;
 
        if (me.html) {
            Ext.DomHelper.generateMarkup(me.html, out);
            delete me.html;
        }
 
        if (me.tpl) {
            // Make sure this.tpl is an instantiated XTemplate
            if (!me.tpl.isTemplate) {
                me.tpl = new Ext.XTemplate(me.tpl);
            }
 
            if (me.data) {
                //me.tpl[me.tplWriteMode](target, me.data);
                me.tpl.applyOut(me.data, out);
                delete me.data;
            }
        }
    },

initRenderTpl()からのテンプレートセットアップ処理で寄り道してしまいましたが、getElConfig()に戻ると、autoElはデフォルトではdivタグ用に初期化されていますが、autoEl設定が指定されていればExt.apply()でマージしています。

getElConfig : function() {
        var me = this,
            autoEl = me.autoEl,
            frameInfo = me.getFrameInfo(),
            config = {
                tag: 'div',
                tpl: frameInfo ? me.initFramingTpl(frameInfo.table) : me.initRenderTpl()
            },
            protoEl = me.protoEl,
            i, frameElNames, len, suffix, frameGenId, frameData;
...
        if (Ext.isString(autoEl)) {
            config.tag = autoEl;
        } else {
            Ext.apply(config, autoEl); // harmless if !autoEl
        }
...
        return config;
    },

この結果、以下の様なオブジェクトがgetRenderTree()の戻り値として返されます。

{
  tag: 'div', //デフォルト。
  tpl: (renderTpl + renderDataを処理するExt.XTemplateのインスタンス),
  // 以下、指定されていれば。
  html: ...,
  css: ...,
  children: ...
}

このオブジェクトが、最終的にrender()メソッド中で Ext.DomHelper の insertBefore()/append() メソッドに渡され、そのなかで最終的にautoElやrenderTplのDOM構築が処理されます。

// tree will be null if a beforerender listener returns false
 if (tree) {
     if (nextSibling) {
         el = Ext.DomHelper.insertBefore(nextSibling, tree);
     } else {
         el = Ext.DomHelper.append(container, tree);
     }
 
     me.wrapPrimaryEl(el);
 }
autoEl, renderTpl, html, tpl の構築ポイント再考

render()の処理の流れを、autoEl, renderTpl, html, tpl の階層と重ねあわせてみると、以下の様な枠組みになるようです。(ちょっと分かりづらいかも・・・)

# Ext.AbstractComponent -> Ext.util.Renderable#render() -> getRenderTree() -> Ext.DomHelper.[insertBefore|append]()
autoEl.tag + autoEl.cls
  # Ext.AbstractComponent#renderTpl(default='{%this.renderContent(out,values)%}') -> Ext.util.Renderable#initRenderTpl()
  renderTpl + renderData
    # 以下、renderTpl中の '{%this.renderContent(out,values)%}' により出力される
    html
    tpl + data
    autoEl.html
    autoEl.children

カスタムコンポーネントのイベントハンドリング

HTML要素をカスタマイズした後は、イベント設定を考えることになります。
ExtJSでは Ext.util.Observable というmixin用のクラスが用意されており、Ext.AbstractComponentにmixinされています。
そのため、イベントの定義や追加、削除を Ext.Component の派生クラスから簡単にできるようになっています。

コンポーネントにおけるイベントの使い方については、以下の公式ドキュメントで解説されています。

今回は以下の様なサンプルコンポーネントを作って、実際にfireEvent()やボタンクリックでイベントの発生を確認してみました。

  • 'foo', 'bar', 'baz' という3種類のイベントを用意しておき、'bar'はイベントの定義だけで、処理は空っぽにしておく。
  • renderTplの中でボタンを2つ作成し、それぞれのclickイベントをサンプルコンポーネント側で設定する。
    • 1つ目のボタンは単にconsole.log()するだけ。
    • 2つ目のボタンはconsole.log()した後に、'baz' イベントをfireEvent()する。
  • 上記サンプルコンポーネントをExt.create()する際に、さらにconfigとして'foo'イベントを設定したlistenerを渡す。
    • 元々の'foo'イベントのハンドラは実行されるか?上書きされてしまい、実行されないか?
  • 上記サンプルコンポーネントをExt.create()した後、1秒後、2秒後、3秒後に 'foo', 'bar', 'baz' イベントを順に fireEvent() していき、特に'bar'イベントがどうなるか見てみる。

t9.js:

Ext.define('My.Cmp', {
  extend: 'Ext.Component',
  baseCls: 'my-cmp',
  componentCls: 'my-cmp-component',
  constructor: function(config) {
    var me = this;
    config = config || {};
    Ext.apply(me, config);
    me.renderData = me.renderData || {-
      title: 'My.Cmp Title'
    };
    me.callParent([config]);
    // deprecated in ExtJS 5.0
    //me.addEvents('foo', 'bar', 'baz');
  },
  listeners: {
    foo: function(el) {
      console.log('default foo event', el);
    },
    bar: Ext.emptyFn, // definition only
    baz: function(el) {
      console.log('default baz event', el);
    }
  },
  renderTpl: [
    '<h2 id="{id}-title" class="{baseCls}-title {componentCls}-title">{title}</h2>',
    '<div>',
    '<input class="my-cmp-btn1" type="button" name="btn1" value="btn1.click()"><br>',
    '<input class="my-cmp-btn2" type="button" name="btn2" value="btn2.click()"><br>',
    '</div>'
  ],
  renderSelectors: {
    button1: 'input.my-cmp-btn1',
    button2: 'input.my-cmp-btn2'
  },
  afterRender: function() {
    // renderTpl中の要素に対してrernderSelectorsでアクセスできる
    // タイミングとしてafterRender()を採用
    var me = this;
    me.button1.on('click', function() {
      console.log('button1-click', this);
    }, me);
    me.button2.on('click', function() {
      console.log('button2-click', this);
      this.fireEvent('baz', this);
    }, me);
  }
});
Ext.application({
  name   : 'MyApp',
  launch : function() {
    var cmp = Ext.create('My.Cmp', {
      renderTo: Ext.getBody()
    });
    cmp.on('foo', function(el) {
      console.log('foo-event', el);
    }, cmp);
    Ext.defer(function() {
      cmp.fireEvent('foo', cmp);
    }, 1000);
    Ext.defer(function() {
      cmp.fireEvent('bar', cmp);
    }, 2000);
    Ext.defer(function() {
      cmp.fireEvent('baz', cmp);
    }, 3000);
  }
});

画面表示後、Ext.defer() で 'foo', 'bar', 'baz' イベントをfireEvent()し終わった状態:

  • 'foo' イベントのイベントハンドラについては、元々のハンドラ + Ext.create()時に指定したハンドラが順に動作していました。(上書きされない)
  • 'bar' イベントのハンドラについては、空の処理を指定していたため、特にログ上に変化はありませんでした。
  • 'baz' イベントのハンドラについては、元々のハンドラが動作しました。

(consoleログをクリアしてから) "btn1.click()"ボタンをクリックした状態:

  • My.Cmp#afterRender() 中で設定した click イベントハンドラが正常に動作していました。

(consoleログをクリアしてから) "btn2.click()"ボタンをクリックした状態:

  • My.Cmp#afterRender() 中で設定した click イベントハンドラが動いてから、その中でfireEvent()した'baz'イベントハンドラが正常に動作していました。

Ext.util.ObservableとDOM要素のイベント設定について

Ext.Component がlistenできるのは、あくまでもmixinされたExt.util.Observableを経由した、ExtJS上でのイベント処理となります。
t9.js ではbutton要素のclickイベントを、afterRender()中で設定していましたが、これはrenderSelectors経由で取得したExt.dom.Elementインスタンスのon()メソッドを使っています。
Ext.dom.Elementクラスのon()メソッドは、ExtJS 4.2 における実装は以下のようにExt.EventManager#on()メソッドに委譲している状態です。

on: function(eventName, fn, scope, options) {
            Ext.EventManager.on(this, eventName, fn, scope || this, options);
            return this;
        },

ブラウザごとの細かい調整など泥臭い処理については、Ext.EventManagerの内部に封じ込められており、開発者は意識しなくて済むようになっています。

コンポーネントをカスタマイズして複雑なDOM要素を構築し、さらにDOM要素のイベントハンドラを調整するような場合に、最終的にそのDOM要素をExt.dom.Elementでラップしたインスタンスを取得できれば、Ext.EventManagerの恩恵を受けることができます。
ただし、DOM要素の取得で Ext#get()(=Ext.dom.Element#get()) や Ext#select()(=Ext.dom.Element#select()) などを使うと、root要素を指定できないため、document空間全体から要素を探す必要があります。そうなると、カスタムコンポーネントでHTML要素を構築するときにid指定を上手く使うか、class指定の衝突を常に意識する必要が出てきます。
renderSelectors によりDOM要素を保存したり、root要素を指定できる Ext#query()(=Ext.dom.Query#select()) を使って Ext.Component#getEl() をroot要素にして探索空間をインスタンスに限定するなど、工夫が必要になると感じました。



プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2014-07-06 01:47:06
md5:5d80e01c651ab8620f08ee345f9322a3
sha1:2b80c0e1bb545566d304fabdb82065bdf0b22a85
コメント
コメントを投稿するにはログインして下さい。