Knockout.js 日本語ドキュメント

カスタムエレメント

カスタムエレメントは、あなたのビューに コンポーネント を注入する、便利な方法を提供します。

導入

カスタムエレメントは、component バインディング に対する構文上の代替手段です (そして実際には、カスタムエレメントは舞台裏で component バインディングを利用します)。

例えば、以下のように記述する代わりに:

<div data-bind='component: { name: "flight-deals", params: { from: "lhr", to: "sfo" } }'></div>

... このように記述することができます:

<flight-deals params='from: "lhr", to: "sfo"'></flight-deals>

これによって、非常に古いブラウザのサポートを維持しながら、とても現代的なWebComponents 的な方法で、コードを構成することができます( カスタムエレメントとInternet Explorer 6 ~ 8 を参照してください)。

実行例

こちらの例ではコンポーネントを宣言してから、ビューにその2つのインスタンスを注入します。下記のソースコードを参照してください。

First instance, without parameters

Second instance, passing parameters

ソースコード: ビュー
<h4>First instance, without parameters</h4>
<message-editor></message-editor>

<h4>Second instance, passing parameters</h4>
<message-editor params='initialText: "Hello, world!"'></message-editor>
ソースコード: ビューモデル
ko.components.register('message-editor', {
    viewModel: function(params) {
        this.text = ko.observable(params.initialText || '');
    },
    template: 'Message: <input data-bind="value: text" /> '
            + '(length: <span data-bind="text: text().length"></span>)'
});

ko.applyBindings();

注: より現実的なケースでは、通常、登録時にコンポーネントのビューモデルとテンプレートをハードコーディングする代わりに、外部ファイルからそれらをロードするでしょう。 実行例 と、登録についてのドキュメント を参照してください。

パラメータの受け渡し

上記の例で見てきたように、コンポーネントのビューモデルにパラメータを供給するため、 params 属性を使用することができます。 params 属性の内容はJavaScriptのオブジェクトリテラルとして解釈され( data-bind 属性と同様です)、任意の型でどのような値でも渡すことができます。例えば:

<unrealistic-component
    params='stringValue: "hello",
            numericValue: 123,
            boolValue: true,
            objectValue: { a: 1, b: 2 },
            dateValue: new Date(),
            someModelProperty: myModelValue,
            observableSubproperty: someObservable().subprop'>
</unrealistic-component>

コンポーネントの親子間の通信

もし、あなたが params 属性内でモデルのプロパティを参照する場合、コンポーネント自体がまだインスタンス化されていないので、もちろんコンポーネント外のビューモデル('親' または 'ホスト' のビューモデル)上のプロパティを参照しています。上記の例では、 myModelValue は親のビューモデルのプロパティになり、そして子コンポーネントのビューモデルのコンストラクタで params.someModelProperty のよう受け取ることになります。

これが、子コンポーネントに親のビューモデルからプロパティを渡すことができる方法です。プロパティがそれ自体observableである場合には、親のビューモデルはそのプロパティを観察し、子コンポーネントによって挿入された新しい値に反応することができるようになります。

observable な式を渡す

以下の例では、

<some-component
    params='simpleExpression: 1 + 1,
            simpleObservable: myObservable,
            observableExpression: myObservable() + 1'>
</some-component>

...コンポーネントのビューモデルにある params パラメータは3つの値を含みます。

  • simpleExpression

    • これは数値の 2 のような類です。observable または computed な値ではなく、observable と関連しません。

      一般的に、パラメータの評価がobservable の評価を伴わない場合(この場合、値は全くobservableを伴いません)、値はリテラルとして渡されます。もし値がobject であった場合、子のコンポーネントはそれを変化させることができますが、それがobservableではないため、親のコンポーネントは子がそれを行ったことを知ることができません。

  • simpleObservable

    • これは、親のビューモデルで定義された myObservable のような、 ko.observable インスタンスです。これはラッパーではありません - 実際に親で参照されているものと同じインスタンスです。したがって、もし子のビューモデルがこのobservable に書き込むと、親のビューモデルはその変更を受け取ります。

    一般的に、もしパラメータの評価がobservable の評価を伴わない場合(この場合、observable は単に評価されずに渡されます)、値はリテラルとして渡されます。

  • observableExpression

    • これはトリッキーです。式自体は、評価されたときに、observableを読み込みます。そのobservableな値は、時間の経過とともに変化する可能性があるため、式の結果も、時間の経過とともに変化できます。

    子コンポーネントが式の値の変化に反応できることを保証するため、Knockoutは、 自動的にこのパラメータをcomputed プロパティにアップグレードします 。従って、子コンポーネントは params.observableExpression() を読むことで現在の値を取得することができ、または params.observableExpression.subscribe(...) 等が使用できます。

    一般的に、カスタムエレメントにおいて、パラメータの評価が observable の評価を伴う場合、knockout は自動的に、与えられた式の結果として ko.computed の値を作成し、コンポーネントに供給します。

要約すると、一般的なルールは次のようになります:

  1. もしパラメータの評価がobservable/computed の評価を 伴わない 場合、それはリテラルとして渡されます。
  2. もし、パラメータの評価が 1つまたは複数の observable/computed の評価を 伴う 場合、それは computed プロパティとして渡され、パラメータ値の変化に反応させることができます。

コンポーネントにマークアップを渡す

時には、マークアップを受信し、出力の一部としてそれを使用するコンポーネントを作成したいことがあるでしょう。例えば、グリッド、リスト、ダイアログ、またはタブセットのような、それ自身の内部で任意のマークアップをバインドする "コンテナ" UIエレメントを構築したい場合などです。

以下のように呼び出すことのできる、特殊なリストコンポーネントについて考えてみましょう:

<my-special-list params="items: someArrayOfPeople">
    <!-- Look, I'm putting markup inside a custom element -->
    The person <em data-bind="text: name"></em>
    is <em data-bind="text: age"></em> years old.
</my-special-list>

デフォルトでは、 <my-special-list> 内部のDOMノードは(いかなるビューモデルにもバインドされずに)取り除かれ、コンポーネントの出力に置き換えられます。 しかし、それらのDOMノードは失われません: それは記憶され、コンポーネントに二つの方法で供給されます。

  • $componentTemplateNodes の配列として、コンポーネントのテンプレート内においてなんらかのバインド式に対して(つまり、 バインディングコンテキスト のプロパティとして)使用できます。通常これは供給されたマークアップを使用するもっとも便利な方法です。以下の例を参照してください。

  • componentInfo.templateNodes の配列として、その createViewModel関数 に渡されます。

コンポーネントは供給されたDOMノードを、コンポーネントのテンプレート内のなんらかの要素に対して、 template: { nodes: $componentTemplateNodes } のように使用することで、希望する出力の一部として使用するように選択することができます。

例えば、 my-special-list コンポーネントのテンプレートは $componentTemplateNodes を参照することができ、その出力は供給されたマークアップを含むことがdけいます。以下が、完全な動作例です。

The person is years old.
ソースコード: ビュー
<!-- This could be in a separate file -->
<template id="my-special-list-template">
    <h3>Here is a special list</h3>

    <ul data-bind="foreach: { data: myItems, as: 'myItem' }">
        <li>
            <h4>Here is another one of my special items</h4>
            <!-- ko template: { nodes: $componentTemplateNodes, data: myItem } --><!-- /ko -->
        </li>
    </ul>
</template>

<my-special-list params="items: someArrayOfPeople">
    <!-- Look, I'm putting markup inside a custom element -->
    The person <em data-bind="text: name"></em>
    is <em data-bind="text: age"></em> years old.
</my-special-list>
ソースコード: ビューモデル
ko.components.register('my-special-list', {
    template: { element: 'my-special-list-template' },
    viewModel: function(params) {
        this.myItems = params.items;
    }
});

ko.applyBindings({
    someArrayOfPeople: ko.observableArray([
        { name: 'Lewis', age: 56 },
        { name: 'Hathaway', age: 34 }
    ])
});

この "特殊なリスト"の例は 、それぞれのリスト項目上にヘッダを挿入する以上のことはしません。しかし、同様のテクニックを、洗練されたグリッド、ダイアログ、タブセットやその類に使用することができ、このようなUIエレメント全てに必要とされるのは、供給されたマークアップによって囲われた共通のUIマークアップ(例えば、グリッドの定義やダイアログのヘッダと枠線など)です。

このテクニックはまた、例えば、 []component バインディングを直接使用する際にマークアップを渡す場合](./component-binding#note-passing-markup-to-components) など、カスタムエレメントなしでコンポーネントを使用するためにも使用できます。

カスタムエレメントのタグ名を制御する

デフォルトでは、Knockout はあなたのカスタムエレメントのタグ名が、ko.components.register によって登録されたコンポーネント名と正確に対応していることを前提としています。この"設定より規約" 戦略は、ほとんどのアプリケーションに最適です。

もし、あなたが異なるカスタムエレメントのタグ名を使用したい場合、それを制御するために getComponentNameForNode をオーバーライドすることができます。例えば、

ko.components.getComponentNameForNode = function(node) {
    var tagNameLower = node.tagName && node.tagName.toLowerCase();

    if (ko.components.isRegistered(tagNameLower)) {
        // If the element's name exactly matches a preregistered
        // component, use that component
        return tagNameLower;
    } else if (tagNameLower === "special-element") {
        // For the element <special-element>, use the component
        // "MySpecialComponent" (whether or not it was preregistered)
        return "MySpecialComponent";
    } else {
        // Treat anything else as not representing a component
        return null;
    }
}

あなたはこのテクニックを例えば、登録されたコンポーネントのサブセットを制御して、カスタムエレメントとして使用したい場合などに使用できます。

カスタムエレメントの登録

もし、あなたがデフォルトのコンポーネントローダを使用しており、それ故に ko.components.register を使用してあなたのコンポーネントを登録している場合、あなたはそれ以上何かを行う必要はありません。この方法で登録されたコンポーネントは、直ちにカスタムエレメントとして使用することができます。

もし、あなたが カスタムコンポーネントローダ を実装しており、それが ko.components.register を使用していない場合、カスタムエレメントとして使用したいエレメント名をKnockout に伝える必要があります。これを行うためには、単に ko.components.register を呼び出します - あなたのカスタムコンポーネントローダがそれを全く使用しないため、あなたは呼び出しで特になんらかの設定を行う必要はありません。例えば、

ko.components.register('my-custom-element', { /* No config needed */ });

他の方法としては、getComponentNameForNode をオーバーライド して、事前の登録とは独立した形で、動的にコンポーネント名とエレメントのマッピングを制御することができます。

注: 通常のバインディングとカスタムエレメントを組み合わせる

カスタムエレメントは、もし必要であれば通常の data-bind 属性 (加えてなんらかの params 属性)を持つことができます。例えば、

<products-list params='category: chosenCategory'
               data-bind='visible: shouldShowProducts'>
</products-list>

しかしながら、例えば texttemplate バインディングのような要素の内容を修正するバインディングを使用しても、それはコンポーネントによって注入されたテンプレートを上書きするので、あまり意味がありません。

Knockout は、ビューモデルと注入されたテンプレートをバインドする際にコンポーネントと衝突してしまうため、controlsDescendantBindings を使用している全てのバインディングの使用を妨害します。従って、もしあなたが ifforeach のような制御フローバインディングを使用したい場合、カスタムエレメント上で直接使用する代わりに、カスタムエレメントの周りを囲う形で使用する必要があります。例えば:

<!-- ko if: someCondition -->
    <products-list></products-list>
<!-- /ko -->

または:

<ul data-bind='foreach: allProducts'>
    <product-details params='product: $data'></product-details>
</ul>

注: カスタムエレメントは自己完結できない

あなたは <my-custom-element /> ではなく<my-custom-element></my-custom-element> のように記述する必要があります。さもないと、カスタムエレメントは閉じられたとみなされず、それ以下の要素が子要素として処理されるでしょう。

これはHTML 仕様上の制限であり、Knockout が制御できるスコープの範囲外です。HTML仕様に準拠しているHTMLパーサは、 自己完結したスラッシュを無視します (パーサにハードコーディングされている、少数の特別な"仕様外の要素" を除きます)。HTMLはXMLと同じではありません。

注: カスタムエレメントとInternet Explorer 6 ~ 8

Knockout は 特に古いブラウザに関連するクロスブラウザの互換性について、開発者がこの問題を扱う痛みを和らげることに挑戦しています。カスタムエレメントはWeb開発のとても現代的なスタイルを提供するだけでなく、一般的に遭遇する全てのブラウザ上で動作します。

  • Internet Explorer9 およびそれ以降を含む、HTML5時代のブラウザでは何ら難しいことなく自動的にカスタムエレメントが許可されます。

  • Internet Explorer 6 ~ 8 もまたカスタムエレメントをサポートしますが、しかしそれはHTMLパーサがそれらのエレメントのいずれかに遭遇する以前に登録されていることが条件です。

IE 6-8 のHTMLパーサは、認識できない要素は全て破棄します。あなたのカスタムエレメントが放り出されないようにするためには、以下のいずれかを行う必要があります。

  • ko.components.register('your-component') の呼び出しを、HTMLパーサが <your-component> エレメントのいずれかに出会う前に、確実に行うようにします。

  • または、少なくとも document.createElement('your-component') の呼び出しを、HTMLパーサが <your-component> エレメントのいずれかに出会う前に行うようにします。あなたは createElement 呼び出しの結果について無視することができます - 必要なのは、あなたがそれを呼び出すことです。

例えば、あなたがページを以下のように構成している場合、全てが上手くいきます:

<!DOCTYPE html>
<html>
    <body>
        <script src='some-script-that-registers-components.js'></script>

        <my-custom-element></my-custom-element>
    </body>
</html>

もし、あなたが AMDを使用している場合、 以下のような構成がよいでしょう:

<!DOCTYPE html>
<html>
    <body>
        <script>
            // Since the components aren't registered until the AMD module
            // loads, which is asynchronous, the following prevents IE6-8's
            // parser from discarding the custom element
            document.createElement('my-custom-element');
        </script>

        <script src='require.js' data-main='app/startup'></script>

        <my-custom-element></my-custom-element>
    </body>
</html>

または、もしあなたが document.createElement 呼び出しのハックを好まない場合、カスタムエレメントの代わりに トップレベルのコンポーネントに対する component バインディング を使用することができます。 他のコンポーネントが ko.applyBindings 呼び出しの前に登録されている限り、それらは IE6-8 上で問題を起こすことなく、カスタムエレメントとして使用できます。

<!DOCTYPE html>
<html>
    <body>
        <!-- The startup module registers all other KO components before calling
             ko.applyBindings(), so they are OK as custom elements on IE6-8 -->
        <script src='require.js' data-main='app/startup'></script>

        <div data-bind='component: "my-custom-element"'></div>
    </body>
</html>

上級: $raw パラメータへのアクセス

以下のように、 useObservable1observable1 、そして observable2 の全てが observable であるような、特殊な場合について考えてみましょう:

<some-component
    params='myExpr: useObservable1() ? observable1 : observable2'>
</some-component>

myExpr の評価がobservable の読出しを伴うため( useObservable1 )、KO はパラメータを computed プロパティとしてコンポーネントに供給します。

しかしながら、 computed プロパティの値はそれ自体が observable です。これは二重のアンラッピングを伴うことになり、厄介なシナリオにつながるように思われます(例えば、 params.myExpr()() 、ここでは最初の括弧で式の値を取得し、二番目のかっこではobservable インスタンスの結果の値を取得します)。

二重のアンラッピングは醜く不便であり、予想外であるため、Knockout は自動的にcomputed プロパティがアンラップされた値 ( params.myExpr ) として生成されるようセットアップして渡します。つまり、コンポーネントは二重のアンラッピングを必要とせず、どちらのobservable が選択されていても( observable1 または observable2 )、 params.myExpr() の値を読み取ることができます。

万が一、あなたが自動アンラッピングを望んでおらず、 observable1 / observable2 インスタンスに直接アクセスしたい場合、 params.$raw から値を読み取ることができます。例えば、

function MyComponentViewModel(params) {
    var currentObservableInstance = params.$raw.myExpr();

    // Now currentObservableInstance is either observable1 or observable2
    // and you would read its value with "currentObservableInstance()"
}

これはかなり特殊なシナリオであるべきですので、通常はあなたが $raw を扱う必要はないでしょう。

原文はこちら

side menu