JavaScript 用 Windows ライブラリ (WinJS) を使ってカスタム コントロールを構築する

JavaScript を使って Windows ストア アプリを開発されたことがあれば、JavaScript 用 Windows ライブラリ (WinJS) をお使いになったことがあるのではないでしょうか。このライブラリには、Windows ストアの UX ガイドラインに従ったアプリをすばやく構築するうえで便利な CSS スタイル、JavaScript コントロール、およびユーティリティのセットが用意されています。WinJS が提供するユーティリティには関数セットが含まれており、これを使って開発するアプリのカスタム コントロールを作成できます。

JavaScript コントロールの作成には、任意のパターンやライブラリを使用できます。WinJS のライブラリ関数は、1 つの選択肢にすぎません。WinJS を使ってコントロールを構築する最大のメリットは、このライブラリの他のコントロールと整合性を保って連携できるカスタム コントロールを作成できる点です。カスタム コントロールの開発と操作のパターンは、WinJS.UI 名前空間の他のコントロールと同じです。

この記事では、構成可能なオプション、イベント、およびパブリック メソッドをサポートする、独自のコントロールを構築する方法を説明します。同様の XAML コントロールの開発にご興味がある場合は、近々公開予定の記事を参照してください。

HTML ページへの JavaScript ベースのコントロールの組み込み

まず、WinJS コントロールをページに組み込む方法を再確認しましょう。それには 2 種類の方法があります。1 つは強制的な方法 (暗黙的に JavaScript のみを使用して処理する方法) で、もう 1 つは宣言的な方法 (HTML 要素に属性を追加して、HTML ページにコントロールを組み込む方法) です。後者の場合、ツールボックスからコントロールをドラッグするなど、ツールを使って設計時のエクスペリエンスを提供できます。詳細については、「クイック スタート: WinJS コントロールとスタイルの追加」を参照してください。

この記事では、WinJS で宣言型の処理モデルによりメリットが得られる JavaScript コントロールを生成する方法について説明します。宣言型でコントロールをページに組み込むには、次の手順を実行します。

  1. コントロールは以下のファイルの API を使うため、WinJS 参照を HTML ページに含めます。

     <script src="//Microsoft.WinJS.1.0/js/base.js"></script>
    <script src="//Microsoft.WinJS.1.0/js/ui.js"></script>
    
  2. 上記のスクリプト タグ参照の後に、コントロールが含まれるスクリプト ファイルへの参照を含めます。

     <script src="js/hello-world-control.js"></script>
    
  3. アプリの JavaScript コードから、WinJS.UI.processAll() (英語) を呼び出します。この関数は HTML を解析し、検出された宣言型のコントロールをインスタンス化します。Visual Studio のアプリ テンプレートが使われている場合、WinJS.UI.processAll() が自動的に default.js から呼び出されます。

  4. 宣言的に、コントロールをページに組み込みます。

     <div data-win-control="Contoso.UI.HelloWorld" data-win-options="{blink: true}"></div>
    

シンプルな JavaScript コントロール

今度は、非常にシンプルなコントロールを構築しましょう。Hello World のコントロール版です。以下は、このコントロールの定義に使う JavaScript です。プロジェクトに新しいファイルを作成して、hello-world-control.js という名前を付け、次のコードを入力します。

 function HelloWorld(element) {
    if (element) {
        element.textContent = "Hello, World!";
    }
};

WinJS.Utilities.markSupportedForProcessing(HelloWorld);

次に、ページの本体に、次のマークアップを使ってこのコントロールを含めます。

 <div data-win-control="HelloWorld"></div>

アプリを実行すると、コントロールが読み込まれて、"Hello, World!" というテキストがページの本体に表示されます。

このコードで WinJS に関係のある唯一の部分は、WinJS.Utilities.markSupportedForProcessing (英語) への呼び出しで、コードが宣言型の処理で使用できることを示しています。このようにして、このコードを信頼してページへのコンテンツ挿入を許可することを WinJS に伝えます。詳細については、WinJS.Utilities.markSupportedForProcessing (英語) 関数についての MSDN ドキュメントを参照してください。

WinJS ユーティリティなどのライブラリを使ってコントロールを作成する理由

ここまで、WinJS を完全には使わずに、宣言型コントロールを作成する方法を説明してきました。今度は、以下のコード スニペットを見てください。このコードでも、実装の大部分は WinJS を使っていません。これは、イベント、構成可能なオプション、およびパブリック メソッドを使った、より複雑なコントロールです。

 (function (Contoso) {
    Contoso.UI = Contoso.UI || {};

    Contoso.UI.HelloWorld = function (element, options) {
        this.element = element;
        this.element.winControl = this;

        this.blink = (options && options.blink) ? true : false;
        this._onblink = null;
        this._blinking = 0;

        element.textContent = "Hello, World!";
    };

    var proto = Contoso.UI.HelloWorld.prototype;

    proto.doBlink = function () {
        var customEvent = document.createEvent("Event");
        customEvent.initEvent("blink", false, false);

        if (this.element.style.display === "none") {
            this.element.style.display = "block";
        } else {
            this.element.style.display = "none";
        }

        this.element.dispatchEvent(customEvent);
    };

    proto.addEventListener = function (type, listener, useCapture) {
        this.element.addEventListener(type, listener, useCapture);
    };

    proto.removeEventListener = function (type, listener, useCapture) {
        this.element.removeEventListener(type, listener, useCapture);
    };

    Object.defineProperties(proto, {
        blink: {
            get: function () {
                return this._blink;
            },

            set: function (value) {
                if (this._blinking) {
                    clearInterval(this._blinking);
                    this._blinking = 0;
                }
                this._blink = value;
                if (this._blink) {
                    this._blinking = setInterval(this.doBlink.bind(this), 500);
                }
            },
            enumerable: true,
            configurable: true
        },

        onblink: {
            get: function () {
                return this._onblink;
            },
            set: function (eventHandler) {
                if (this._onblink) {
                    this.removeEventListener("blink", this._onblink);
                    this._onblink = null;
                }
                this._onblink = eventHandler;
                this.addEventListener("blink", this._onblink);
            }
        }
    });

    WinJS.Utilities.markSupportedForProcessing(Contoso.UI.HelloWorld);
})(window.Contoso = window.Contoso || {}); 

多くの開発者は、コントロールをこの方法 (匿名関数、コンストラクター関数、プロパティ、カスタム イベントを使用) で作成しています。この方法に抵抗がなければ、ぜひ採用してください。ただし、多くの開発者にとって、このコードは少しわかりにくいかもしれません。多くの Web 開発者にとって、ここで使用した手法になじみがありません。そこで、ライブラリが役立ちます。ライブラリを使うと、このコード作成に関して紛らわしい部分がいくぶん解消されます。

WinJS をはじめとするライブラリを使うことで、読みやすくなるだけでなく、さまざまな細かい問題が処理されるため、開発者が気に掛ける必要がなくなります (プロトタイプ、プロパティ、カスタム イベントを効率的に使用)。ライブラリは、メモリの使用率を最適化し、開発者がよく犯しがちなミスの防止に役立ちます。WinJS はそのようなライブラリの 1 つですが、どのライブラリを選ぶかは読者しだいです。ライブラリがどのように役立つかを示す具体的な例として、この記事を読み終えたら、このセクションにあるコードをもう一度見直し、WinJS ユーティリティを使って実装した、この記事の最後にある同じコントロールのコードと比較することをお勧めします。

WinJS の JavaScript コントロールの基本パターン

以下は、WinJS を使って JavaScript コントロールを作成する場合の最小限のベスト プラクティス パターンです。

 (function () {
    "use strict";

    var controlClass = WinJS.Class.define(
            function Control_ctor(element) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                this.element.textContent = "Hello, World!"
            });
    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });
})();

そして次のように、宣言的にコントロールをページに組み込みます。

 <div data-win-control="Contoso.UI.HelloWorld"></div>

特に WinJS を初めて使用する場合には、なじみのないコードが上記に含まれているかもしれませんので、細かく説明しましょう。

  1. この例では、コードを即時実行関数と呼ばれる JavaScript の共通パターンを使ってラップしています。

     (function () {
    …
    })();
    

    これは、このコードが自己完結型であり、意図しない変数やグローバル割り当てが後に残らないようにするためです。これは一般的なベスト プラクティスであり、元のソースに加えたい変更の 1 つであることに注意してください。

  2. 関数の最初に "use strict" ステートメントを使用することで、ECMAScript 5 の strict モードを有効にしています。これは、エラー チェックと、JavaScript の将来のバージョンとの互換性を高めるために、Windows ストア アプリ テンプレート全体のベスト プラクティスとして行っています。これも一般的なベスト プラクティスであり、元のソースに加えたい変更の 1 つです。

  3. ここからは、WinJS に関係のあるコードに入ります。WinJS.Class.define() を呼び出して、コントロールのクラスを作成しています。このクラスの代表的な機能は、markSupportedForProcessing() への呼び出しを処理し、コントロールのプロパティの今後の作成を支援することです。これは、標準の Object.defineProperties 関数を基にした、本当にシンプルなヘルパーです。

  4. named Control_ctor という名前のコンストラクターを定義しています。WinJS.UI.processAll() が default.js から呼び出されると、data-win-control 属性を使って参照されているコントロールがないかページのマークアップをスキャンし、このコントロールを見つけて、このコンストラクターを呼び出します。

  5. このコンストラクター内に、コントロール オブジェクトを使ってページ上の要素への参照が保存され、コントロール オブジェクトへの参照が要素を使って保存されます。

    • element || document.createElement("div") の部分は、強制型モデルのサポートに使われます。これにより、ユーザーが後で要素をページに追加できます。
    • この方法でページ上に要素への参照を保持し、element.winControl を設定することでコントロール オブジェクトへの参照を要素内に保持することをお勧めします。このようにしておくと、イベントなどの機能を追加するときに、一部のライブラリ関数は特に何もしなくても機能します。オブジェクトや DOM 要素の循環参照によるメモリ リークについては心配要りません。Internet Explorer 10 が自動的に対応します。
    • コントロールのテキスト コンテンツは、コンストラクターによって変更され、画面に表示されるテキスト「Hello, World!」が設定されます。
  6. 最後に、WinJS.Namespace.define() を使ってコントロール クラスを公開し、アプリのどのコードからでもアクセスできるようにコントロールをパブリックにしています。この方法を使用しない場合、グローバル名前空間を使って、作業中のインライン関数外でコードを作成するために、コントロールを公開する別のソリューションを考え出さなければならなかったでしょう。

コントロール オプションの定義

この記事の例をもう少し興味深いものにするため、コントロールに構成可能なオプションのサポートを追加しましょう。ここでは、ユーザーがコンテキストを点滅させることができるオプションを追加します。

 var controlClass = WinJS.Class.define(
            function Control_ctor(element, options) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                // Set option defaults
                this._blink = false;

                // Set user-defined options
                WinJS.UI.setOptions(this, options);

                element.textContent = "Hello, World!"
            },
            {
                _blinking: 0,

                blink: {
                    get: function () {
                        return this._blink;
                    },

                    set: function (value) {
                        if (this._blinking) {
                            clearInterval(this._blinking);
                            this._blinking = 0;
                        }
                        this._blink = value;
                        if (this._blink) {
                            this._blinking = setInterval(this._doBlink.bind(this), 500);
                        }
                    }
                },

                _doBlink: function () {
                    if (this.element.style.display === "none") {
                        this.element.style.display = "block";
                    } else {
                        this.element.style.display = "none";
                    }
                },
            });

    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });

この場合は、ページにコントロールを含めるときに、data-win-options 属性を使って点滅オプションを構成できます。

 <div data-win-control="Contoso.UI.HelloWorld" data-win-options="{blink: true}">
</div>

オプションのサポートを追加するために、次のようにコードを変更しました。

  1. オプションをコントロールに渡しました。このために、コンストラクター関数で (options という名前の) パラメーターを使っています。
  2. 既定の設定を構成しました。このために、クラスでプライベート プロパティを使用しています。
  3. WinJS.UI.setOptions() を呼び出しました。このために、コントロール オブジェクトに渡しています。この呼び出しによって、コントロールの構成可能なオプションの既定値が上書きされます。
  4. (blink という) パブリック プロパティを追加しました。これは、新しいオプションに使用します。
  5. 機能を追加しました。画面に表示されるテキストを点滅する機能を追加しました (実際には、スタイルをここでハードコーディングするのではなく、CSS クラスを切り替えることをお勧めします)。

この例で面倒な処理をしている部分は、WinJS.UI.setOptions() への呼び出しです。ユーティリティ関数の setOptions は、options オブジェクトの各フィールドを 1 つずつ処理し、ターゲット オブジェクト側の同じ名前のフィールドに値を割り当てます。これは、setOptions の最初のパラメーターです。

この例では、win-control の data-win-options 引数を使って options オブジェクトを構成し、フィールド "blink" に true という値を渡しています。この値が渡されると、コンストラクター関数の setOptions() の呼び出しで "blink" というフィールドが認識され、このフィールドの値がコントロール オブジェクト側の同名のフィールドにコピーされます。blink という名前のプロパティを定義しています。このプロパティはセッター関数を提供します。この例では setOptions() がセッター関数であり、この関数はコントロールの _blink メンバーを設定します。

イベントのサポートの追加

非常に便利な点滅オプションが実装できたので、イベント サポートを追加して点滅が生じたときに応答できるようにしましょう。

 var controlClass = WinJS.Class.define(
            function Control_ctor(element, options) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                // Set option defaults
                this._blink = false;

                // Set user-defined options
                WinJS.UI.setOptions(this, options);

                element.textContent = "Hello, World!"
            },
            {
                _blinking: 0,
                _blinkCount: 0,

                blink: {
                    get: function () {
                        return this._blink;
                    },

                    set: function (value) {
                        if (this._blinking) {
                            clearInterval(this._blinking);
                            this._blinking = 0;
                        }
                        this._blink = value;
                        if (this._blink) {
                            this._blinking = setInterval(this._doBlink.bind(this), 500);
                        }
                    }
                },

                _doBlink: function () {
                    if (this.element.style.display === "none") {
                        this.element.style.display = "block";
                    } else {
                        this.element.style.display = "none";
                    }
                    this._blinkCount++;
                    this.dispatchEvent("blink", {
                        count: this._blinkCount
                    });
                },
            });

    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });

    // Set up event handlers for the control
    WinJS.Class.mix(Contoso.UI.HelloWorld,
        WinJS.Utilities.createEventProperties("blink"),
        WinJS.UI.DOMEventMixin);

前と同様に、コントロールをページに組み込みます。要素を後で取得できるように、要素に ID を追加していることに注意してください。

 <div id="hello-world-with-events"
    data-win-control="Contoso.UI.HelloWorld"
    data-win-options="{blink: true}"></div>

このように変更することで、"blink" イベントをリッスンするイベント リスナーをアタッチできるようになりました (注: この例では document.getElementById のエイリアスとして $ を使っています)。

 $("hello-world-with-events").addEventListener("blink",
        function (event) {
            console.log("blinked element this many times: " + event.count);
        });

このコードを実行すると、500 ミリ秒ごとにメッセージが Visual Studio の JS コンソールに書き出されます。

この動作をサポートするために、次の 3 種類の変更をコントロールに施しています。

  1. WinJS.Class.mix(Contoso.UI.HelloWorld, WinJS.Utilities.createEventProperties("blink")) を呼び出しました。これにより、ユーザーがプログラムを使って設定するか、宣言により HTML ページにバインドできる "onblink" プロパティが作成されます。
  2. WinJS.Class.mix(Contoso.UI.HelloWorld, WinJS.UI.DOMEventMixin) を呼び出しました。addEventListener、removeEventListener、および dispatchEvent 関数をコントロールに追加します。
  3. blink イベントを実行しました。that.dispatchEvent("blink", {element: that.element}); を呼び出すことで blink イベントを実行し、カスタムのイベント オブジェクトが要素フィールドを使って作成されます。
  4. 点滅イベントをリッスンするためのイベント ハンドラーをアタッチしました。イベントが発生したら、カスタム イベント オブジェクトの要素フィールドにアクセスします。

ここで、dispatchEvent() への呼び出しは、this.element をコントロールのコンストラクターに設定している場合にしか機能しないことに注意してください。その理由は、イベント組み込みの内部的なしくみにより、DOM の要素にアクセスする必要があるためです。これは、以前に説明した、コントロール オブジェクトに要素メンバーが必要になるケースの 1 つです。これにより、DOM レベル 3 イベント (英語) パターンに従って、ページの親要素にイベントを渡すことができます。

パブリック メソッドの公開

コントロールに対する最後の変更として、doBlink() パブリック関数を追加しましょう。この関数を呼び出すと、点滅をいつでも実行できます。

 var controlClass = WinJS.Class.define(
            function Control_ctor(element, options) {
                this.element = element || document.createElement("div");
                this.element.winControl = this;

                // Set option defaults
                this._blink = false;

                // Set user-defined options
                WinJS.UI.setOptions(this, options);

                element.textContent = "Hello, World!"
            },
            {
                _blinking: 0,
                _blinkCount: 0,

                blink: {
                    get: function () {
                        return this._blink;
                    },

                    set: function (value) {
                        if (this._blinking) {
                            clearInterval(this._blinking);
                            this._blinking = 0;
                        }
                        this._blink = value;
                        if (this._blink) {
                            this._blinking = setInterval(this.doBlink.bind(this), 500);
                        }
                    }
                },

                doBlink: function () {
                    if (this.element.style.display === "none") {
                        this.element.style.display = "block";
                    } else {
                        this.element.style.display = "none";
                    }
                    this._blinkCount++;
                    this.dispatchEvent("blink", {
                        count: this._blinkCount
                    });
                },
            });
    WinJS.Namespace.define("Contoso.UI", {
        HelloWorld: controlClass
    });

    // Set up event handlers for the control
    WinJS.Class.mix(Contoso.UI.HelloWorld,
        WinJS.Utilities.createEventProperties("blink"),
        WinJS.UI.DOMEventMixin);

これは規約の変更にすぎません。_doBlink 関数の名前は doBlink に変更できます。

JavaScript から doBlink() を呼び出すには、コントロールにこのオブジェクトへの参照が必要です。強制的にコントロールを作成した場合は、既に参照が作成されている可能性があります。宣言型処理を採用している場合は、コントロールの HTML 要素で winControl プロパティを使うことで、このコントロール オブジェクトにアクセスできます。たとえば、前と同じマークアップの場合、次のコードによってコントロール オブジェクトにアクセスできます。

$("hello-world-with-events").winControl.doBlink();

まとめ

ここでは、次のような、コントロールを実装する場合に最も一般的に行われる処理について方法を説明しました。

  1. ページへのコントロールの組み込み。
  2. 構成オプションの提供。
  3. イベントのディスパッチと応答。
  4. パブリック メソッドによる機能の公開。

このチュートリアルが、シンプルな JavaScript ベースのカスタム コントロールの構築方法を理解するうえで、お役に立てていればさいわいです。カスタム コントロールを作成中に不明点が生じたときは、Windows デベロッパー センターにアクセスし、フォーラム (英語) で質問してください。また、XAML 開発者の方は、近々公開する予定の、XAML コントロール開発についての同様の記事を参照してください。

Jordan Matthiesen 
Microsoft Visual Studio、プログラム マネージャー