JavaScript コンポーネントのイベント

Greg Murray

課題

JavaScript コンポーネントにおいて、イベントは不可欠なものです。イベントによってユーザーインタフェースが適用され、AJAX 要求が生成され、JavaScript コンポーネント間の対話が可能になります。複数の異なるブラウザに使用できるイベント処理のコードを、新規に作成するのは困難です。JavaScript にはさまざまなイベント処理の方法があると同時に、ブラウザごとに独自の動作や問題があるからです。

ページ内の JavaScript コンポーネントの数が増えると、コンポーネントのコードは結合が密になる傾向があります。この場合、完成したコードが再利用しにくくなり、管理や保守も困難になるので、ユーザーインタフェースの開発方法として効果的ではありません。JavaScript コンポーネントを密に結合させずに、最小限のコードでほかの多くのコンポーネントと組み合わせて使用できるようにする効果的な方法が必要です。

対処法

Dojo ライブラリを使用します。Dojo ライブラリは、JavaScript イベントシステムを抽象化し、イベント処理のための一連の JavaScript API と、コンポーネント間イベント通信の手段を提供します。また、単純なイベントハンドラ、イベントリスナー、パブリッシュ/サブスクライブイベントなど、イベント処理のための複数のオプションを備えています。Dojo のイベント処理 API は相互排他的ではありません。多くの場合、事例に合わせて API を組み合わせて使用します。これから、詳細な事例と API について見ていきます。

このあとの各節に、各 API の詳しい説明と、各 API が適している事例を示します。

単純なイベントハンドラ

JavaScript コンポーネントにイベントハンドラを登録するには、DOM またはプロパティーベースのイベントハンドラではなく、dojo.event.connect() を使用します。dojo.event.connect() は、一貫性のある API を提供し、ブラウザ間の差異を抽象化し、一部のブラウザで発生するメモリーリークを回避します。この API は、1 つのイベントタイプに複数のイベントハンドラを接続する際の詳細も処理します。

dojo.event.connect() を使用すると、1 つのオブジェクトに 1 つまたは複数のイベントを接続できます。イベントは、追加した順序で呼び出されます。

イベントハンドラを追加するために必要な引数は次のとおりです。

dojo.event.connect(srcObj, "srcFunc", "targetFunc")

イベントハンドラを接続する例を次に示します。

<script type="text/javascript" src="dojo.js"></script>
<script type="text/javascript">
window.onload = function () {
  var link = document.getElementById("mylink");
  dojo.event.connect(link, "onclick", myHandler);
}

function myHandler(evt) {
    alert("dojo.connect handler");
}
</script>
<a href="#" id="mylink">Click Me</a>

前出のコードでは、link 要素の onclick プロパティーを、イベントハンドラ関数 myHandler に割り当てています。既存のハンドラがある場合は、既存のハンドラが呼び出されたあとに Dojo ハンドラが呼び出されます。匿名のイベントハンドラを接続する詳しいコード例を次に示します。

<script type="text/javascript" src="dojo.js"></script>
<script type="text/javascript">
window.onload = function () {
  var link = document.getElementById("mylink");
  // link 要素の 'onclick' プロパティーを匿名の関数に接続する
  dojo.event.connect(link, "onclick", function(evt) {
     var srcElement;
     // この関数にはブラウザ固有のマウスイベントが渡される
     if (evt.target) {
         srcElement = evt.target;
     } else if (evt.srcElement) {
         srcElement = evt.srcElement;
     }
     if (srcElement) {
        alert("dojo.event.connect event: " + srcElement.getAttribute("id"));
    }
  });
}"
</script>
<a href="#" id="mylink" onclick="alert('inline event');">Click Me</a>

前出の例では、ドキュメント本体で定義されている ID mylink の link 要素に、匿名のイベントハンドラ (関数) を作成しています。既存のイベントハンドラがない場合は、dojo.event.connect を使用して定義したハンドラがデフォルトのハンドラになります。前出のようなインラインのハンドラが存在する場合は、デフォルトのインラインイベントのあとに Dojo イベントが呼び出されます。この例からわかるように、dojo.event.connect API では、引数としてイベントオブジェクト (evt) にアクセスできます。これは、Dojo でブラウザ固有のマウスイベントが抽象化されないということではありません。前出の例では、オブジェクト検出を使用してソース要素を取得しています。

イベントリスナー

1 つのオブジェクトまたはコンポーネントで別のオブジェクトに対するイベントを監視し、何らかの処理を行う場合は、イベントリスナーを使用します。リスナーは監視するだけですが、監視対象のイベントハンドラに渡される引数にアクセスすることはできます。

リスナーは、ソースイベントハンドラの before (前) または after (あと) に 1 つ以上呼び出すことができます。イベントリスナーには、ソースイベントハンドラと同じ引数が渡されます。リスナーは、ソースイベントハンドラに接続された順序で呼び出されます。before と after のどちらを使用するかは、アプリケーションの使用事例によって異なります。重要なことは、Dojo にはどちらも使用できる柔軟性があるということです。

リスナーを接続するために必要な引数は次のとおりです。

dojo.event.connect("before/after", srcObj, "srcFunc", targetObj, "targetFunc")

1 つ目の引数は、リスナーを呼び出すタイミングに応じて、before または after を指定します。2 つ目の引数は、イベントの接続先のソースオブジェクト (通常は HTML 要素) です。3 つ目の引数は、監視対象の関数の名前です (文字列を引用符で囲みます)。4 つ目の引数は、ターゲットオブジェクトです。5 つ目の引数は、ターゲットオブジェクトの関数です (文字列を引用符で囲みます)。この関数は、1 つ目の引数に従ってソース関数の前またはあとに呼び出されます。すべての引数名を追跡するのが面倒な場合は、dojo.event.kwConnect() を使用することもできます。このメソッドは、パラメータの名前と値をプロパティーとして持つオブジェクトリテラルを引数として取ります。詳細は、「The Dojo Event System」(Dojo イベントシステム) を参照してください。

次の例では、dojo.event.connect を使用して loadMenuListener 関数を接続しています。loadMenu イベントハンドラが呼び出される前に関数が呼び出されるように指定しています。

  function loadMenu(args) {
      alert("args=" + args);
  }
  
  function loadMenuListener(args) {
      alert("loadMenuListener: args=" + args);
  }
  
  dojo.event.connect("before", this, "loadMenu", this, "loadMenuListener");
  loadMenu({name: "MyMenu", items: ['File', 'Save']});
  // alerts loadMenuListener: args=[object Object]
  // alerts args=args=[object Object]

イベントハンドラが呼び出されたあとでリスナーに通知する場合は、dojo.event.connect の 1 つ目の引数に after を指定します。

  function loadMenu(args) {
      alert("args=" + args);
  }
  
  function loadMenuListener(args) {
      alert("loadMenuListener: args=" + args);
  }
  
  dojo.event.connect("after", this, "loadMenu", this, "loadMenuListener");
  loadMenu({name: "MyMenu", items: ['File', 'Save']});
  // alerts args=[object Object]
  // alerts loadMenuListener: args=[object Object]

この例では、ソースイベントハンドラが呼び出されたあとでリスナーが呼び出されます。

リスナーは Java プラットフォーム全体で使用されているので、多くの Java 開発者にはこの方法がもっとも馴染みがあるはずです。この方法には、特定のハンドラが呼び出される前またはあとにリスナーを呼び出すことができるというメリットもあります。

イベントハンドララッパー

使用中のコンポーネントの JavaScript ソースを変更せずに、イベントハンドラの動作を遮断および変更する場合は、around を使用してイベントをラップします。

イベントハンドラリスナーに before または after を指定するだけでは十分でない場合があります。たとえば、使用中の JavaScript コンポーネントのソースコードを変更せずに、イベントハンドラの動作や引数を変更したい場合があります。Dojo では、dojo.io.connect の 1 つ目の引数に around を指定することで、このような処理を実現できます。

イベントラッパーを追加するために必要な引数は次のとおりです。

dojo.event.connect("around", srcObj, "srcFunc", targetObj, "targetFunc")

パラメータは、1 つ目の引数が around である点を除き、リスナーの場合と同じです。

次に、オブジェクトのイベントハンドラをカスタムイベントハンドラでラップする例を示します。カスタムイベントハンドラは、独自のロジックを適用してから、ソースイベントハンドラを呼び出しています。

    // カスタムイベントハンドララッパー
    function customLoadHandler(invocation) {
       alert("custom menu name =" + invocation.args[0].name);
       // 引数の name プロパティーを更新する
       invocation.args[0].name = "Custom Menu";
       // デフォルトのイベントハンドラを呼び出す
       invocation.proceed();
    }

    function ImageScroller() {   
        this.load = function (args) {
            alert("default menu name=" + args.name);
        }
    }

    var is = new ImageScroller();
    dojo.event.connect("around", is, "load", this, "customLoadHandler");
    is.load({name: "My Menu", items: ['File', 'Save']});
    // alerts "custom menu name=My Menu"
    // alerts "default menu name=Custom Menu"    

前出の例は、ImageScroller に対する public 関数 load の呼び出しを遮断し、それを customLoadHandler 関数でラップするカスタムコードの作成方法を示しています。この例の customLoadHandler 関数では、load 関数に渡す引数を操作しています。customLoadHandler 関数には、Dojo ランタイムによって、2 つのプロパティーが指定された単一の引数が渡されます (前出の invocation)。args プロパティーはターゲットイベントハンドラに渡される引数の配列で、proceed プロパティーはターゲットイベントハンドラを呼び出す関数です。

ソース関数によって値が返された場合、ラッパーは proceed の呼び出し時に値を取得し、その値を変数に割り当てることができます。ラッパーは、そのロジックに応じて、戻り変数の値を変更することもできます。

この方法は、要求を遮断し、動作を変更できるという点で、サーブレットフィルタに似ています。サーブレットフィルタの getParameters() が前出のコードの invocation.args に相当し、doFilter()invocation.proceed() に相当します。

パブリッシュ/サブスクライブによるイベント処理

コンポーネント間でイベントを匿名で通信するには、パブリッシュとサブスクライブを使用します。トピック名を初期化パラメータとして渡すことができるようにコンポーネントをカスタマイズすることで、コンポーネントの柔軟性を高めることもできます。すべてのイベント処理をパブリッシュおよびサブスクライブで公開する必要はありません。将来、ほかのコンポーネントと統合できるように柔軟性を考慮してください。

たとえば、ImageScroller という名前のコンポーネントがあり、そこに表示される製品は、AccordionMenu という名前の別のコンポーネントで設定できるとします。このような処理は、次の例に示すように、dojo.event.publish API と dojo.event.subscribe API を使用して実現できます。

<script type="text/javascript" src="dojo.js"></script>

<script type="text/javascript">
window.onload=init;
var ac;
var is;

function init() {
    ac = new AccordionMenu();
    ac.load();
    is = new ImageScroller();
    is.load();
}

function ImageScroller() {   
    this.setProducts = function(pid) {
        // pid の製品を表示する
    }

    this.handleEvent = function(args) {
        if (args.event == 'showProducts') {
            this.setProducts(args.value);
        }
    }
    
    this.load = function () {
        dojo.event.topic.subscribe("/scroller", this, handleEvent);
    }
}

// アコーディオンメニューはこのあと、またはページに含まれる別の .js ファイルで定義する

<script>

前出の例では、トピック /scroller のイベントを監視するように JavaScript コンポーネント ImageScroller が登録されます。handleEvent 関数をイベントハンドラとして設定しています。handleEvent 関数は 'this' を使用して定義しています。これにより、外部の JavaScript コードまたはコンポーネントから、コンポーネントのイベント処理関数を手動で呼び出せるようになります。この例では、イベントハンドラのイベントタイプに event プロパティー、イベント値に value プロパティーが指定されたオブジェクトリテラル args を使用しています。オブジェクトリテラルを引数として使用することをお勧めします。オブジェクトリテラルは、柔軟性があり、関数シグニチャーを変更せずに、関数に渡すパラメータをカスタマイズできるからです。

function AccordionMenu() {
    function expandRow(target) {
       ...
       var link = document.createElement("a");
       dojo.event.connect(link, "onclick", function(evt) {
           this.target = target;
           dojo.event.topic.publish("/scroller", {event: "showProducts", value : target});
       });
    }
}

linkonclick イベントを受け取ると、イベントハンドラはイベントをトピック /scroller にパブリッシュします。このトピックには、値が showProducts である event プロパティーと、値が target である value プロパティーが指定されたオブジェクトリテラルが含まれます。この例では、target プロパティーの値を適切に維持するためにクロージャーを使用しています。この値は、匿名ハンドラが呼び出されるとスコープ外になります。この種類のクロージャーを使用するときは、クロージャーを形成するために使用されているプロパティーへの参照がある限り、匿名ハンドラが存在することに留意してください。また、DOM 要素の参照は、メモリーリークの原因になる可能性があるので、注意してください。

イベントハンドラの切断

ここまで、イベントハンドラへの接続の事例に重点を置いて説明してきましたが、イベントが不要になったときにはオブジェクトから切り離す必要があります。切り離すには、イベントの場合は dojo.event.disconnect を、トピックの場合は dojo.event.unsubscribe を呼び出します。パラメータはイベントハンドラへの接続時またはサブスクライブ時と同じです。

コンポーネントを開発するときには、作業内容によって、ここで紹介したイベント処理方法を 1 つまたは複数使用できます。Dojo には、イベント処理のための強力なメソッドが豊富に用意されています。詳細および例については、次の「リソース」の節に示すドキュメントを参照してください。

リソース

The Dojo Event System (Dojo イベントシステム)

Dojo Event Examples (Dojo イベント例)


© Sun Microsystems 2006. Java BluePrints Solutions Catalog の内容はすべて著作権保護されており、サン・マイクロシステムズ社の書面による許可なしに他の著作物に発表することを禁止します。