ビューモデルとデータバインディング

データのバインドとそれを駆動する ViewModel は Ext JS 5 の強力な追加機能です。これらを合わせて、コードの効率を高め、さらに宣言スタイルで記述できるだけでなく、問題点を明確に分離できます。

ViewModel はデータオブジェクトを管理するクラスです。このクラスを使用すると、このデータの対象をバインドさせ、変更された場合に通知できます。ViewController などの ViewModel はそれを参照するビューによって所有されます。ViewModel はビューに関連付けされているため、コンポーネント階層の親コンポーネントが所有する親 ViewModel にもリンクできます。これによって、子ビューは親 ViewModel のデータを単に「継承」することができます。

コンポーネントには新しい “bind” コンフィグがあり、コンフィグを ViewModel からのデータに関連付けることができます。バインドを使用すると、適切なコンポーネントコンフィグのセッターメソッドが、バインドされた値が変更されるたびに呼び出されるようにできます。カスタムイベントハンドラは必要ありません。

本ガイドでは、ViewModel とデータバインディングの優れた機能を示す例を紹介していきます。

コンポーネントのバインド

バインディングと ViewModel を理解する最良の方法は、コンポーネントのバインディングに使用できる様々な方法を見ていくことです。これはコンポーネントが主にデータバインディングを消費し、コンポーネントが Ext JS 開発者にとって馴染みのあるものだからです。ただし、バインディングが機能するには、ViewModel が必要です。ここでは1つを参照し、後で定義します。

バインディングとコンフィグ

コンポーネントのバインディングはデータを Ext.app.ViewModel からコンポーネントの config プロパティに接続するプロセスです。コンポーネントの設定はセッターメソッドがある限りバインドすることができます。例えば、Ext.panel.Panel に setTitle() メソッドがあるので、タイトル設定にバインドできます。

この例では、 ViewModel のデータの結果に従ってパネルの幅を設定します。setWidth() は Ext.panel.Panel が使用できるメソッドなので、データをwidthにバインドできます。

Ext.create('Ext.panel.Panel', {
    title: 'Simple Form',

    viewModel: {
        type: 'test'  // we will define the "test" ViewModel soon
    },

    bind: {
        html: '<p>Hello {name}</p>',
        width: '{someWidth}'
    }
});

バインドの値に使用できる構文は Ext.Template に非常に似ています。括弧内のトークンの周囲にテキストを配置できます。Ext.Template の場合と同様にフォーマッタを使用できます。Ext.Template とは異なり、テンプレートが単一のトークン(‘{someWidth}’ など)の場合、その値が変更されずに渡されます。つまり、文字列には変換されません。

“name” と “someWidth” のデータがどのように定義されるかは後述します。上記の例は単にデータがコンポーネントにどのように消費されるかを示します。

Boolean コンフィグのバインディング

バインドしたいコンフィグの多くはvisible(または hidden)、disabled、checked、pressed などの Boolean 値です。Bind テンプレートはテンプレートで Boolean 否定 “inline” をサポートします。他の代数は数式を参照するとして(以下参照)、Boolean 転置は一般的なので特別に説明する必要があります。例:

Ext.create('Ext.panel.Panel', {
    title: 'Simple Form',

    viewModel: {
        type: 'test'
    },

    items: [{
        xtype: 'button',
        bind: {
            hidden: '{!name}'  // negated
        }
    }]
});

これは、単一のトークンテンプレートの値が文字列に変換されない仕組みを示します。上記では、“name” は文字列の値ですが、“!” を使用して否定され、Boolean 値となり、ボタンの setHidden メソッドに渡されます。

バインディングと優先順位

バインドされたコンフィグプロパティは、バインドされた結果が利用できるようになるとすぐに、コンポーネントに静的に設定された内容を常に上書きします。言い換えると、バインドされたデータは常に静的設定値に優先されますが、そのデータを取得するために遅れることがあります。

Ext.create('Ext.panel.Panel', {
    title: 'Simple Form',

    viewModel: {
        type: 'test'
    },

    bind: {
        title: 'Hello {name}'
    }
});

“name” のバインディングが配布されると、“Simple Form” タイトルが置換されます。

バインディングと子コンポーネント

バインディングの最も便利な機能は、viewModel のある子コンポーネントのすべてがコンテナのデータにアクセスできることです。

この例では、フォームの子アイテムがそのコンテナの viewModel にバインドされることが分かります。

Ext.create('Ext.panel.Panel', {
    title: 'Simple Form',

    viewModel: {
        type: 'test'
    },

    layout: 'form',
    defaultType: 'textfield',

    items: [{
        fieldLabel: 'First Name',
        bind: '{firstName}' // uses "test" ViewModel from parent
    },{
        fieldLabel: 'Last Name',
        bind: '{lastName}'
    }]
});

双方向バインディング

バインドコンフィグで双方向データバインディングが可能になります。これは、ビューとモデル間でデータをライブで同期させます。ビューでデータが変更されると自動的にモデルにも記述されます。これによって、同じデータにバインドされる可能性のある他のコンポーネントが自動的に更新されます。

上記の例では、“firstName” プロパティと “lastName” プロパティはテキストフィールドにバインドされ、入力の変更は ViewModel に記述されます。このすべてがどのように結び付くかについては、ここで例を終えて ViewModel を定義する必要がありそうです。

Ext.define('TestViewModel', {
    extend: 'Ext.app.ViewModel',

    alias: 'viewmodel.test', // connects to viewModel/type below

    data: {
        firstName: 'John',
        lastName: 'Doe'
    },

    formulas: {
        // We'll explain formulas in more detail soon.
        name: function (get) {
            var fn = get('firstName'), ln = get('lastName');
            return (fn && ln) ? (fn + ' ' + ln) : (fn || ln || '');
        }
    }
});

Ext.define('TestView', {
    extend: 'Ext.panel.Panel',
    layout: 'form',

    // Always use this form when defining a view class. This
    // allows the creator of the component to pass data without
    // erasing the ViewModel type that we want.
    viewModel: {
        type: 'test'  // references alias "viewmodel.test"
    },

    bind: {
        title: 'Hello {name}'
    },

    defaultType: 'textfield',
    items: [{
        fieldLabel: 'First Name',
        bind: '{firstName}'
    },{
        fieldLabel: 'Last Name',
        bind: '{lastName}'
    },{
        xtype: 'button',
        text: 'Submit',
        bind: {
            hidden: '{!name}'
        }
    }]
});

Ext.onReady(function () {
    Ext.create('TestView', {
        renderTo: Ext.getBody(),
        width: 400
    });
});

上記のパネルが表示されると、テキストフィールドの変更がパネルタイトルと [Submit(送信)] ボタンの非表示状態に反映されます。

バインディングとコンポーネントの状態

場合によっては、チェックボックスの「チェック」状態やグリッドの選択レコードなどのコンポーネントの状態は他のコンポーネントにとって意味があります。コンポーネントが識別するために「リファレンス」を割り当てると、そのコンポーネントが ViewModel に主なプロパティの一部を公開します。

この例では、“Admin Key” テキストフィールドの無効になっているコンフィグがチェックボックスのチェック状態にバインドされます。この結果、チェックボックスがチェックされるまでテキストフィールドが無効になります。このような動作は次のようなダイナミックフォームに適しています。

Ext.create('Ext.panel.Panel', {
    title: 'Sign Up Form',

    viewModel: {
        type: 'test'
    },

    items: [{
        xtype: 'checkbox',
        boxLabel: 'Is Admin',
        reference: 'isAdmin'
    },{
        xtype: 'textfield',
        fieldLabel: 'Admin Key',
        bind: {
            disabled: '{!isAdmin.checked}'
        }
    }]
});

バインドディスクリプタ

基本的な3つのバインドディスクリプタを見てきました。

  • {firstName} - ViewModel の一部の値への「直接的バインド」。この値は変更されずに渡されるので、あらゆるタイプのデータで届きます。

  • Hello {name} - 「バインドテンプレート」は様々なバインド式のテキストの値を挿入することで文字列を常に生成します。また、バインドテンプレートは次のような通常の Ext.Template でフォーマッタを使用できます。‘Hello {name:capitalize}’.

  • {!isAdmin.checked} - Boolean コンフィグプロパティへのバインドに便利な直接バインドの否定フォームです。

これらの基本フォーム以外にも、使用できる特殊フォームのバインドディスクリプタがあります。

マルチバインド

オブジェクトまたは配列がバインドディスクリプタとして与えられている場合、ViewModel は同じ形状のオブジェクトまたは配列を作成しますが、その様々なプロパティはバインド結果によって置換されます。例:

Ext.create('Ext.Component', {
    bind: {
        data: {
            fname: '{firstName}',
            lname: '{lastName}'
        }
    }
});

これはコンポーネントの “data” コンフィグを ViewModel から設定された値を持つ2つのプロパティのあるオブジェクトに設定します。

レコードバインド

id が42の「ユーザー」など特定のレコードが必要な場合、バインドディスクリプタは “reference” プロパティを持つオブジェクトです。例:

Ext.create('Ext.Component', {
    bind: {
        data: {
            reference: 'User',
            id: 42
        }
    }
});

この場合、コンポーネントの tpl がロードされるとユーザーレコードを受け取ります。現在のところ、これには Ext.data.Session を使用する必要があります。

アソシエーションバインド

レコードバインドと同様に、ユーザーのアドレスのレコードなど、アソシエーションにもバインドできます。

Ext.create('Ext.Component', {
    bind: {
        data: {
            reference: 'User',
            id: 42,
            association: 'address'
        }
    }
});

この場合、コンポーネントの tpl がロードされるとユーザーの “address” レコードを受け取ります。現在のところ、これにも Ext.data.Session を使用する必要があります。

バインドオプション

バインディングオプションを記述する必要がある場合、バインドディスクリプタの最終フォームが使用されます。以下の例では、バインディングで値を1つだけ受け取り、自動的に切断する方法を示します。

Ext.create('Ext.Component', {
    bind: {
        data: {
            bindTo: '{name}',
            single: true
        }
    }
});

“bindTo” プロパティはバインドディスクリプタオブジェクトの2番目の予約名です(最初は“reference”)。ある場合には、“bindTo” の値が実際のバインドディスクリプタであり、他のプロパティがバインディングの設定オプションであることを示します。

現時点でサポートされる他のバインドオプションは “deep” です。このオプションは、オブジェクトにバインドする場合に使用されます。これによって、リファレンス自体だけでなくそのオブジェクトのプロパティが変更されるとバインディングが通知されます。これは、コンポーネントの “data” コンフィグがオブジェクトを受け取ることが多いのでバインドする場合に便利です。

Ext.create('Ext.Component', {
    bind: {
        data: {
            bindTo: '{someObject}',
            deep: true
        }
    }
});

ViewModel の作成

これで、コンポーネントの ViewModel の使用方法と ViewModel の概要を見たので、次に ViewModel とその機能について見てみましょう。

すでに説明したように、ViewModel は基底の data オブジェクトのマネージャーです。バインド文で消費されるのはそのオブジェクトのコンテンツです。親 ViewModel からその子 ViewModel へのデータの継承には JavaScript プロトタイプチェーンを利用します。これについては View Model Internalsガイドで詳しく説明していますが、簡単に説明するならば、子 ViewModel の data オブジェクトにはその親 ViewModel の data オブジェクトがプロトタイプとして含まれます。

数式

データを保持してバインディングを提供する以外に、ViewModel は「数式」と呼ばれる他のデータからデータを計算するのに便利な方法です。数式を利用することによって、ViewModel でデータの依存関係をカプセル化し、ビューツリーがその構造の宣言に専念できるようにします。

言い換えるならば、データは ViewModel のデータでは変更されませんが、数式を使用して変換されることによって異なった表示にできます。これは、従来のデータモデルのフィールドに対する変換設定の仕組みと同様です。

前の例では、シンプルな “name” 式を見ました。そこでは、“name” 式は ViewModel の2つの値(“firstName” と “lastName”)を組み合わせた関数に過ぎませんでした。

また、数式は結果が別のデータプロパティであるかのように他の数式の結果も使用できます。例:

Ext.define('TestViewModel2', {
    extend: 'Ext.app.ViewModel',

    alias: 'viewmodel.test2',

    formulas: {
        x2y: function (get) {
            return get('x2') * get('y');
        },

        x2: function (get) {
            return get('x') * 2;
        }
    }
});

“x2” 式は “x” プロパティを使用して “x2” を “x * 2” と定義します。“x2y” 式は “x2” と “y” の両方を使用します。この定義は、“x” が変更されると “x2”、“x2y” の順に再計算されることを意味します。ただし、“y” が変更されると再計算する必要があるのは “x2y” だけです。

明示的なバインディングのある数式

上記の例では、数式の依存関係は関数を確認することによって分かりますが、これは最善のソリューションとは限りません。バインドのすべての値が表示される場合にシンプルなオブジェクトを返す、明示的なバインド文を使用できます。

Ext.define('TestViewModel2', {
    extend: 'Ext.app.ViewModel',

    alias: 'viewmodel.test2',

    formulas: {
        something: {
            bind: {
                x: '{foo.bar.x}',
                y: '{bar.foo.thing.zip.y}'
            },

            get: function (data) {
                return data.x + data.y;
            }
        }
    }
});

双方向数式

数式が転置可能な場合、値が設定されたら “set” メソッドを呼び出すように定義できます(双方向バインディングなどで)。「 この」ポインタは ViewModel なので、“set” メソッドは “this.set()” を呼び出して適切なプロパティを ViewModel に設定できます。

以下の TestViewModel の修正バージョンは “name” を双方向数式として定義する方法を示します。

Ext.define('TestViewModel', {
    extend: 'Ext.app.ViewModel',

    alias: 'viewmodel.test',

    formulas: {
        name: {
            get: function (get) {
                var fn = get('firstName'), ln = get('lastName');
                return (fn && ln) ? (fn + ' ' + ln) : (fn || ln || '');
            },

            set: function (value) {
                var space = value.indexOf(' '),
                    split = (space < 0) ? value.length : space;

                this.set({
                    firstName: value.substring(0, split),
                    lastName: value.substring(split + 1)
                });
            }
        }
    }
});

推奨事項

ViewModel、数式、データバインディングのあらゆる機能によって、これらのメカニズムを使いすぎたり乱用し、理解やデバッグが難しいアプリケーションやメモリの更新が遅く、リークしたりするアプリケーションを作成しがちになります。これらの問題を回避し、ViewModel を最大限に活用するために、いくつかのテクニックをお勧めします。

  • viewModel の設定時に常に以下のフォームを使用する。これはコンフィグシステムがコンフィグ値をマージする方法を考えると重要です。このフォームでは、“type” プロパティがマージ中に予約されます。

    Ext.define('TestView', {
        //...
        viewModel: {
            type: 'test'
        },
    });

特にハイレベル ViewModel で+ 名前を分かりやすくする。JavaScript では、テキスト検索を利用しているので、検索が可能な名前を選んでください。プロパティを使用するコードが増えると、意味のある名前や一意の名前を選ぶことがさらに重要になります。

  • データを必要以上に深くオブジェクトにネストしないでください。複数の上位オブジェクトが ViewModel にストアされていると、ネストされたサブオブジェクトがたくさんあるオブジェクトよりも管理作業が少なくて済みます。さらに、これによって、多くのコンポーネントが大規模オブジェクトに依存する場合よりもこの情報の依存関係が明らかになります。オブジェクトを共有するには理由がありますが、ViewModel は管理されたオブジェクトに過ぎないので、そのプロパティも使用できます。

必要なコンポーネントで+ 子 ViewModel を使用してデータのクリーンアップを可能にします。ハイレベル ViewModel にすべてのデータを配置している場合、必要としている子ビューが破棄されてもそのデータは削除されない可能性が高いです。その代わり、子ビューに ViewModel を作成し、データをその ViewModel に渡します。

  • 子 ViewModel は実際に必要でない限り作成しないでください。それぞれの ViewModel インスタンスは作成に時間がかかり、管理するためには、メモリが必要です。子ビューは一意のデータを必要とする場合、コンテナから継承する ViewModel を使用できます。親 ViewModel を汚染し、メモリのリークを実質的に発生させるよりも、子 ViewModel が必要な時に作成するほうが好ましいため、前述の推奨を参照してください。

  • バインドを繰り返すより数式を使用してください。数式でどのようにバインドされた値の結果を組み合わせるかを見ていると、同じ値を多くの場所で直接使用することに比べて、数式を使用して依存関係の数をどのように削減できるかお分かりになります。例えば、3つの依存関係のある1つの数式で4ユーザーいる場合、ViewModel で追跡する依存関係は 3 + 4 = 7 になります。4ユーザーでこのような3つの依存関係がある場合と比べ、3 * 4 = 12 の依存関係となります。追跡する依存関係が少ないと、メモリの使用量も処理時間も少なくて済みます。

  • 数式を深く連鎖しないでください。これは実行時のコストの問題というよりもコードの見やすさの問題です。連結数式はデータとコンポーネントの接続をマスクでき、何が起こっているかを分かりづらくします。

  • 双方向数式は安定している必要があります。数式 “foo” は値 “bar” を計算したものとします。“foo” が設定されると、get メソッドから数式を転置して “bar” を設定します。その結果、get メソッドが設定された “foo” に対して正確に同じ値を生成すると安定します。そうでない場合、安定ポイントに達するまでプロセスが繰り返されるか、永遠に続行します。いずれの結果も好ましくはありません。

参考文献

viewModel に関する詳細は、ViewModel の内部ガイドを参照してください。

Last updated