JavaScriptでDOMデータバインディングを実装する方法


244

この質問は厳密に教育的なものとして扱ってください。私はまだこれを実装するための新しい答えとアイデアを聞くことに興味があります

tl; dr

JavaScriptで双方向データバインディングを実装するにはどうすればよいですか?

DOMへのデータバインディング

DOMへのデータバインディングとは、たとえば、aプロパティを持つJavaScriptオブジェクトを持つことbです。次に、<input>(たとえば)DOM要素があると、DOM要素がa変化すると変化し、逆もまた同様です(つまり、双方向のデータバインディングを意味します)。

これは、これがどのように見えるかについてのAngularJSの図です:

双方向データバインディング

だから基本的に私は次のようなJavaScriptを持っています:

var a = {b:3};

次に、次のような入力(または他のフォーム)要素:

<input type='text' value=''>

入力の値をa.b(たとえば)の値にしたいのですが、入力テキストが変更さa.bれたときにも変更したいと思います。場合はa.bJavaScriptでの変更は、入力が変化します。

質問

これをプレーンJavaScriptで実現するための基本的なテクニックは何ですか?

具体的には、次のことを参考にしてください。

  • オブジェクトに対してバインディングはどのように機能しますか?
  • フォームの変更を聞くことはどのように機能しますか?
  • テンプレートレベルでHTMLのみを変更することは簡単な方法で可能ですか?HTMLドキュメント自体のバインディングは追跡せず、JavaScript(DOMイベント、およびJavaScriptが使用されているDOM要素への参照を維持する)でのみ追跡したいと思います。

何を試しましたか?

私は口ひげの大ファンなので、テンプレートに使用してみました。ただし、MustacheはHTMLを文字列として処理するため、データバインディング自体を実行しようとすると問題が発生しました。これについて考えられる唯一の回避策は、HTML文字列(または作成されたDOMツリー)自体を属性で変更することでした。別のテンプレートエンジンを使用してもかまいません。

基本的に、手元の問題を複雑にしていて、簡単な解決策があると強く感じました。

注:外部ライブラリ、特に数千行のコードを使用する回答は提供しないでください。私はAngularJSとKnockoutJSを使用しました(そして好きです!)。私は「フレームワークxを使用する」という形式での回答を本当に望んでいません。理想的には、双方向のデータバインディングを自分で実装する方法を把握するために多くのフレームワークを使用する方法を知らない将来の読者が欲しいです。私は完全な答えを期待しているわけではありませんが、アイデアを理解してもらうためのものです。


2
CrazyGlueは、Benjamin Gruenbaumのデザインに基づいています。また、SELECT、チェックボックス、ラジオタグもサポートしています。jQueryは依存関係です。
JohnSz 2014年

12
この質問は完全に素晴らしいです。話題から外れたり、他の愚かなナンセンスのために閉鎖されたりした場合、私は真剣にチェックされます。
OCDev 2014

@JohnSz CrazyGlueプロジェクトについて言及していただきありがとうございます。私は長い間、シンプルな2ウェイデータバインダーを探していました。Object.observeを使用していないようですので、ブラウザのサポートは素晴らしいはずです。そして、口ひげテンプレートを使用していないので、完璧です。
Gavin

@ベンジャミン結局何をしたの?
ジョニー2016年

@johnnyは私の意見では、JSで(Reactのように)DOMを作成するのが正しいアプローチであり、その逆ではありません。それが最終的には私たちがやろうとしていることだと思います。
Benjamin Gruenbaum 2016年

回答:


106
  • オブジェクトに対してバインディングはどのように機能しますか?
  • フォームの変更を聞くことはどのように機能しますか?

両方のオブジェクトを更新する抽象化

他の手法もあると思いますが、最終的には、関連するDOM要素への参照を保持するオブジェクトがあり、独自のデータとその関連要素への更新を調整するインターフェイスを提供します。

.addEventListener()このために非常に良いインターフェイスを提供します。eventListenerインターフェースを実装するオブジェクトを指定すると、そのオブジェクトをthis値としてハンドラーが呼び出されます。

これにより、要素とその関連データの両方に自動的にアクセスできます。

オブジェクトを定義する

もちろん、必須ではありませんが、プロトタイプの継承はこれを実装する良い方法です。まず、要素といくつかの初期データを受け取るコンストラクターを作成します。

function MyCtor(element, data) {
    this.data = data;
    this.element = element;
    element.value = data;
    element.addEventListener("change", this, false);
}

したがって、ここではコンストラクターが新しいオブジェクトのプロパティに関する要素とデータを格納します。またchange、指定されたにイベントをバインドしelementます。興味深いのは、関数の代わりに新しいオブジェクトを2番目の引数として渡すことです。しかし、これだけでは機能しません。

eventListenerインターフェースの実装

これを機能させるには、オブジェクトにeventListenerインターフェイスを実装する必要があります。これを実現するために必要なのは、オブジェクトにhandleEvent()メソッドを与えることだけです。

それが継承の出番です。

MyCtor.prototype.handleEvent = function(event) {
    switch (event.type) {
        case "change": this.change(this.element.value);
    }
};

MyCtor.prototype.change = function(value) {
    this.data = value;
    this.element.value = value;
};

これを構造化するにはさまざまな方法がありますが、更新を調整する例として、change()メソッドが値のみを受け入れhandleEvent、イベントオブジェクトの代わりにその値を渡すようにすることにしました。これにより、change()イベントなしでを呼び出すこともできます。

したがって、changeイベントが発生すると、要素と.dataプロパティの両方が更新されます。そして、.change()JavaScriptプログラムで呼び出すときにも同じことが起こります。

コードを使用する

これで、新しいオブジェクトを作成し、更新を実行させることができます。JSコードの更新は入力に表示され、入力の変更イベントはJSコードに表示されます。

var obj = new MyCtor(document.getElementById("foo"), "20");

// simulate some JS based changes.
var i = 0;
setInterval(function() {
    obj.change(parseInt(obj.element.value) + ++i);
}, 3000);

デモ: http : //jsfiddle.net/RkTMD/


5
+1非常にクリーンなアプローチ。非常に単純で、人々が学ぶのに十分なほどシンプルで、私が持っていたものよりもはるかにクリーンです。一般的な使用例は、コードでテンプレートを使用してオブジェクトのビューを表すことです。ここでこれがどのように機能するのか疑問に思いましたか?MustacheのようなエンジンではMustache.render(template,object)、オブジェクトをテンプレート(Mustacheに固有ではない)と同期させておきたいと思って何かをしますが、どうすればよいですか?
Benjamin Gruenbaum 2013年

3
@BenjaminGruenbaum:クライアント側のテンプレートは使用していませんが、Mustacheには挿入ポイントを識別するための構文があり、その構文にはラベルが含まれていると思います。そのため、テンプレートの「静的」部分は、配列に格納されたHTMLのチャンクにレンダリングされ、動的部分はそれらのチャンクの間を移動すると思います。次に、挿入ポイントのラベルがオブジェクトプロパティとして使用されます。次にinput、これらのポイントの1つを更新する場合、入力からそのポイントへのマッピングがあります。簡単な例を考え出せるか見てみましょう。

1
@BenjaminGruenbaum:うーん... 2つの異なる要素をきれいに調整する方法については考えていません。これは、最初に思ったよりも少し複雑です。しかし、私は興味があるので、少し後でこれに取り組む必要があるかもしれません。:)

2
Template構文解析を行い、さまざまなMyCtorオブジェクトを保持し、それぞれをその識別子で更新するためのインターフェースを提供するプライマリコンストラクターがあることがわかります。ご不明な点がありましたらお知らせください。:) 編集: ... 代わりにこのリンクを使用してください ... JSの更新を示すために、10秒ごとに入力値が指数関数的に増加することを忘れていました。これはそれを制限します。

2
... 完全にコメントされたバージョンとマイナーな改善。

36

それで、私は自分の解決策をポットに入れることにしました。これが実用的なフィドルです。これは非常に最新のブラウザでのみ動作することに注意してください。

それが使用するもの

この実装は非常に最新です-(非常に)最新のブラウザーが必要で、2つの新しいテクノロジーを使用します。

  • MutationObservers domの変更を検出する(イベントリスナーも使用されます)
  • Object.observeオブジェクトの変化を検出し、domに通知します。危険です。この回答は、OoがECMAScript TCによって議論され、反対されていると書かれているため、ポリフィルを検討してください

使い方

  • 要素にdomAttribute:objAttributeマッピングを配置します-たとえばbind='textContent:name'
  • それをdataBind関数で読んでください。要素とオブジェクトの両方の変更を確認します。
  • 変更が発生した場合-関連する要素を更新します。

ソリューション

ここにdataBind関数があります、それはわずか20行のコードであり、短くなる可能性があることに注意してください:

function dataBind(domElement, obj) {    
    var bind = domElement.getAttribute("bind").split(":");
    var domAttr = bind[0].trim(); // the attribute on the DOM element
    var itemAttr = bind[1].trim(); // the attribute the object

    // when the object changes - update the DOM
    Object.observe(obj, function (change) {
        domElement[domAttr] = obj[itemAttr]; 
    });
    // when the dom changes - update the object
    new MutationObserver(updateObj).observe(domElement, { 
        attributes: true,
        childList: true,
        characterData: true
    });
    domElement.addEventListener("keyup", updateObj);
    domElement.addEventListener("click",updateObj);
    function updateObj(){
        obj[itemAttr] = domElement[domAttr];   
    }
    // start the cycle by taking the attribute from the object and updating it.
    domElement[domAttr] = obj[itemAttr]; 
}

ここにいくつかの使用法があります:

HTML:

<div id='projection' bind='textContent:name'></div>
<input type='text' id='textView' bind='value:name' />

JavaScript:

var obj = {
    name: "Benjamin"
};
var el = document.getElementById("textView");
dataBind(el, obj);
var field = document.getElementById("projection");
dataBind(field,obj);

これが実用的なフィドルです。このソリューションはかなり一般的であることに注意してください。Object.observeと変異オブザーバーのシミングが利用可能です。


1
たまたま便利だと思ったら、私はたまたまこの(es5)を楽しみに書いている-jsfiddle.net/P9rMmを
Benjamin

1
obj.nameセッターがある場合、外部から監視することはできませんが、セッター内から変更されたことをブロードキャストする必要があることに注意してください-html5rocks.com/en/tutorials/es7/observe/#toc-notifications-ちょっと作業中にレンチを投げますOo()の場合、セッターを使用してより複雑で相互依存の動作が必要な場合。さらに、obj.nameが構成可能でない場合、(通知を追加するためのさまざまなトリックを使用して)セッターを再定義することもできません。そのため、Oo()を使用したジェネリックは完全に破棄されます。
Nolo

8
Object.observeがすべてのブラウザーから削除されます:caniuse.com/#feat=object-observe
JvdBerg

1
Object.observe、github.com / anywhichway / proxy
observe

29

私の準備者に追加したいのですが。メソッドを使用せずにオブジェクトに新しい値を割り当てるだけの、少し異なるアプローチをお勧めします。ただし、これは特に古いブラウザーではサポートされておらず、IE9でも別のインターフェイスを使用する必要があることに注意してください。

最も注目すべきは、私のアプローチではイベントを利用しないことです。

ゲッターとセッター

私の提案は、ゲッターとセッターの比較的新しい機能を利用しています、特にセッターのみを利用しています。一般的に言えば、ミューテーターを使用すると、特定のプロパティに値を割り当てて取得する方法の動作を「カスタマイズ」できます。

ここで使用する実装の1つは、Object.definePropertyメソッドです。FireFox、GoogleChrome、および-IE9で動作します。他のブラウザはテストしていませんが、これは理論にすぎないため...

とにかく、それは3つのパラメータを受け入れます。最初のパラメータは新しいプロパティを定義するオブジェクトです。2番目のパラメータは新しいプロパティの名前に似た文字列で、最後のパラメータは新しいプロパティの動作に関する情報を提供する「記述子オブジェクト」です。

特に興味深い2つの記述子はget、およびsetです。例は次のようになります。これらの2つを使用すると、他の4つの記述子の使用が禁止されることに注意してください。

function MyCtor( bindTo ) {
    // I'll omit parameter validation here.

    Object.defineProperty(this, 'value', {
        enumerable: true,
        get : function ( ) {
            return bindTo.value;
        },
        set : function ( val ) {
            bindTo.value = val;
        }
    });
}

これを利用すると、少し異なります。

var obj = new MyCtor(document.getElementById('foo')),
    i = 0;
setInterval(function() {
    obj.value += ++i;
}, 3000);

これは最新のブラウザでのみ機能することを強調したいと思います。

作業フィドル:http : //jsfiddle.net/Derija93/RkTMD/1/


2
Harmony Proxyオブジェクトがあった場合のみ:)セッターはいい考えのように見えますが、実際のオブジェクトを変更する必要はありませんか?また、Object.create余談ですが、ここでも使用できます(ここでも、2番目のパラメーターを許可する最新のブラウザーを想定しています)。また、setter / getterを使用して、オブジェクトとDOM要素に別の値を「投影」することもできます:)。テンプレート作成についての洞察があるかどうか疑問に思っています。これは、特に適切に構造化するための、ここでの本当の課題のようです。:)
Benjamin Gruenbaum

私の準備者と同じように、私もクライアント側のテンプレートエンジンをあまり使用していません。:(しかし、実際のオブジェクトを変更するとはどういう意味ですか?そして、セッター/ゲッターを使用できることを理解する方法についての考えを理解したいと思います...ここのゲッター/セッターは何にも使用されていませんしかしProxy、あなたが言ったように、基本的にのように、オブジェクトへのすべての入力とオブジェクトからの取得をDOM要素にリダイレクトします。;)私は、2つの異なるプロパティを同期させることの難しさを理解しました。私の方法では両方の1つを排除します。
桐瀬

A Proxyはゲッター/セッターを使用する必要をなくし、要素のプロパティを知らなくても要素をバインドできます。つまり、ゲッターはbindTo.value以外にも変更できるため、ロジック(およびテンプレート)を含めることができます。問題は、この種の双方向バインディングをテンプレートを考慮してどのように維持するかです。オブジェクトをフォームにマッピングしているとしましょう。要素とフォームの両方を同期したままにしておきたいのですが、そのようなことをどうやってやろうと思っています。たとえば、knockout learn.knockoutjs.com/#/?tutorial=introでそれがどのように機能するかを確認できます
Benjamin

@BenjaminGruenbaum Gotcha。見てみます。
キルセ

@BenjaminGruenbaum私はあなたが理解しようとしているものを見ます。テンプレートを念頭に置いてこれらすべてを設定することは、もう少し難しいことがわかります。私はしばらくこのスクリプトに取り組んでます(そして継続的にリベースします)。しかし、今のところ、休憩しています。私には実際にはこれのための時間はありません。
桐瀬

7

私の答えはもっと技術的なものになると思いますが、他の人が異なる技術を使用して同じものを提示しているため、違いはありません。
したがって、まず最初に、この問題の解決策は、「オブザーバー」と呼ばれるデザインパターンを使用することです。これにより、データをプレゼンテーションから切り離し、1つの変更をリスナーにブロードキャストできますが、この場合はそれは双方向です。

DOMからJSへの方法

DOMからjsオブジェクトにデータをバインドするには、次の形式でマークアップを追加できます。 data属性(または互換性が必要な場合はクラス)のます。

<input type="text" data-object="a" data-property="b" id="b" class="bind" value=""/>
<input type="text" data-object="a" data-property="c" id="c" class="bind" value=""/>
<input type="text" data-object="d" data-property="e" id="e" class="bind" value=""/>

このようにして、jsを使用してアクセスできます querySelectorAll(またはgetElementsByClassName互換性のために旧友)。

これで、変更をリッスンするイベントを方法にバインドできます。オブジェクトごとに1つのリスナー、またはコンテナ/ドキュメントに1つの大きなリスナーです。ドキュメント/コンテナへのバインドは、ドキュメントまたはコンテナで行われたすべての変更に対してイベントをトリガーします。メモリフットプリントは小さくなりますが、イベント呼び出しが発生します。
コードは次のようになります。

//Bind to each element
var elements = document.querySelectorAll('input[data-property]');

function toJS(){
    //Assuming `a` is in scope of the document
    var obj = document[this.data.object];
    obj[this.data.property] = this.value;
}

elements.forEach(function(el){
    el.addEventListener('change', toJS, false);
}

//Bind to document
function toJS2(){
    if (this.data && this.data.object) {
        //Again, assuming `a` is in document's scope
        var obj = document[this.data.object];
        obj[this.data.property] = this.value;
    }
}

document.addEventListener('change', toJS2, false);

JSの場合、DOMを使用します

2つのものが必要です。魔女DOM要素の参照を保持する1つのメタオブジェクトは、各jsオブジェクト/属性にバインドされ、オブジェクトの変更をリッスンする方法です。これは基本的に同じ方法です。オブジェクトの変更をリッスンし、それをDOMノードにバインドする方法が必要です。オブジェクトにはメタデータを「含めることができない」ため、ある方法でメタデータを保持する別のオブジェクトが必要になりますプロパティ名がメタデータオブジェクトのプロパティにマッピングされること。コードは次のようになります。

var a = {
        b: 'foo',
        c: 'bar'
    },
    d = {
        e: 'baz'
    },
    metadata = {
        b: 'b',
        c: 'c',
        e: 'e'
    };
function toDOM(changes){
    //changes is an array of objects changed and what happened
    //for now i'd recommend a polyfill as this syntax is still a proposal
    changes.forEach(function(change){
        var element = document.getElementById(metadata[change.name]);
        element.value = change.object[change.name];
    });
}
//Side note: you can also use currying to fix the second argument of the function (the toDOM method)
Object.observe(a, toDOM);
Object.observe(d, toDOM);

お役に立てば幸いです。


.observerの使用に互換性の問題はありませんか?
Mohsen Shakiba 2015

現時点ではObject.observeサポートがクロムでのみ存在するため、シムまたはポリフィルが必要です。caniuse.com/#feat=object-observe
madcampos

9
Object.observeは停止しています。ここでそれを書き留めておきます。
Benjamin Gruenbaum、2015年

@BenjaminGruenbaumこれは死んでいるので、今使用する正しいものは何ですか?
ジョニー

1
@johnny私が間違っていなければ、オブジェクトで何ができるかをより詳細に制御できるため、プロキシトラップになりますが、調査する必要があります。
madcampos 2016年

7

昨日、自分でデータをバインドする方法を書き始めました。

それで遊ぶのはとても面白いです。

美しく、とても便利だと思います。少なくともFirefoxとChromeを使用したテストでは、Edgeも動作するはずです。他の人についてはわかりませんが、プロキシをサポートしていればうまくいくと思います。

https://jsfiddle.net/2ozoovne/1/

<H1>Bind Context 1</H1>
<input id='a' data-bind='data.test' placeholder='Button Text' />
<input id='b' data-bind='data.test' placeholder='Button Text' />
<input type=button id='c' data-bind='data.test' />
<H1>Bind Context 2</H1>
<input id='d' data-bind='data.otherTest' placeholder='input bind' />
<input id='e' data-bind='data.otherTest' placeholder='input bind' />
<input id='f' data-bind='data.test' placeholder='button 2 text - same var name, other context' />
<input type=button id='g' data-bind='data.test' value='click here!' />
<H1>No bind data</H1>
<input id='h' placeholder='not bound' />
<input id='i' placeholder='not bound'/>
<input type=button id='j' />

これがコードです:

(function(){
    if ( ! ( 'SmartBind' in window ) ) { // never run more than once
        // This hack sets a "proxy" property for HTMLInputElement.value set property
        var nativeHTMLInputElementValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
        var newDescriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
        newDescriptor.set=function( value ){
            if ( 'settingDomBind' in this )
                return;
            var hasDataBind=this.hasAttribute('data-bind');
            if ( hasDataBind ) {
                this.settingDomBind=true;
                var dataBind=this.getAttribute('data-bind');
                if ( ! this.hasAttribute('data-bind-context-id') ) {
                    console.error("Impossible to recover data-bind-context-id attribute", this, dataBind );
                } else {
                    var bindContextId=this.getAttribute('data-bind-context-id');
                    if ( bindContextId in SmartBind.contexts ) {
                        var bindContext=SmartBind.contexts[bindContextId];
                        var dataTarget=SmartBind.getDataTarget(bindContext, dataBind);
                        SmartBind.setDataValue( dataTarget, value);
                    } else {
                        console.error( "Invalid data-bind-context-id attribute", this, dataBind, bindContextId );
                    }
                }
                delete this.settingDomBind;
            }
            nativeHTMLInputElementValue.set.bind(this)( value );
        }
        Object.defineProperty(HTMLInputElement.prototype, 'value', newDescriptor);

    var uid= function(){
           return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
               var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
               return v.toString(16);
          });
   }

        // SmartBind Functions
        window.SmartBind={};
        SmartBind.BindContext=function(){
            var _data={};
            var ctx = {
                "id" : uid()    /* Data Bind Context Id */
                , "_data": _data        /* Real data object */
                , "mapDom": {}          /* DOM Mapped objects */
                , "mapDataTarget": {}       /* Data Mapped objects */
            }
            SmartBind.contexts[ctx.id]=ctx;
            ctx.data=new Proxy( _data, SmartBind.getProxyHandler(ctx, "data"))  /* Proxy object to _data */
            return ctx;
        }

        SmartBind.getDataTarget=function(bindContext, bindPath){
            var bindedObject=
                { bindContext: bindContext
                , bindPath: bindPath 
                };
            var dataObj=bindContext;
            var dataObjLevels=bindPath.split('.');
            for( var i=0; i<dataObjLevels.length; i++ ) {
                if ( i == dataObjLevels.length-1 ) { // last level, set value
                    bindedObject={ target: dataObj
                    , item: dataObjLevels[i]
                    }
                } else {    // digg in
                    if ( ! ( dataObjLevels[i] in dataObj ) ) {
                        console.warn("Impossible to get data target object to map bind.", bindPath, bindContext);
                        break;
                    }
                    dataObj=dataObj[dataObjLevels[i]];
                }
            }
            return bindedObject ;
        }

        SmartBind.contexts={};
        SmartBind.add=function(bindContext, domObj){
            if ( typeof domObj == "undefined" ){
                console.error("No DOM Object argument given ", bindContext);
                return;
            }
            if ( ! domObj.hasAttribute('data-bind') ) {
                console.warn("Object has no data-bind attribute", domObj);
                return;
            }
            domObj.setAttribute("data-bind-context-id", bindContext.id);
            var bindPath=domObj.getAttribute('data-bind');
            if ( bindPath in bindContext.mapDom ) {
                bindContext.mapDom[bindPath][bindContext.mapDom[bindPath].length]=domObj;
            } else {
                bindContext.mapDom[bindPath]=[domObj];
            }
            var bindTarget=SmartBind.getDataTarget(bindContext, bindPath);
            bindContext.mapDataTarget[bindPath]=bindTarget;
            domObj.addEventListener('input', function(){ SmartBind.setDataValue(bindTarget,this.value); } );
            domObj.addEventListener('change', function(){ SmartBind.setDataValue(bindTarget, this.value); } );
        }

        SmartBind.setDataValue=function(bindTarget,value){
            if ( ! ( 'target' in bindTarget ) ) {
                var lBindTarget=SmartBind.getDataTarget(bindTarget.bindContext, bindTarget.bindPath);
                if ( 'target' in lBindTarget ) {
                    bindTarget.target=lBindTarget.target;
                    bindTarget.item=lBindTarget.item;
                } else {
                    console.warn("Still can't recover the object to bind", bindTarget.bindPath );
                }
            }
            if ( ( 'target' in bindTarget ) ) {
                bindTarget.target[bindTarget.item]=value;
            }
        }
        SmartBind.getDataValue=function(bindTarget){
            if ( ! ( 'target' in bindTarget ) ) {
                var lBindTarget=SmartBind.getDataTarget(bindTarget.bindContext, bindTarget.bindPath);
                if ( 'target' in lBindTarget ) {
                    bindTarget.target=lBindTarget.target;
                    bindTarget.item=lBindTarget.item;
                } else {
                    console.warn("Still can't recover the object to bind", bindTarget.bindPath );
                }
            }
            if ( ( 'target' in bindTarget ) ) {
                return bindTarget.target[bindTarget.item];
            }
        }
        SmartBind.getProxyHandler=function(bindContext, bindPath){
            return  {
                get: function(target, name){
                    if ( name == '__isProxy' )
                        return true;
                    // just get the value
                    // console.debug("proxy get", bindPath, name, target[name]);
                    return target[name];
                }
                ,
                set: function(target, name, value){
                    target[name]=value;
                    bindContext.mapDataTarget[bindPath+"."+name]=value;
                    SmartBind.processBindToDom(bindContext, bindPath+"."+name);
                    // console.debug("proxy set", bindPath, name, target[name], value );
                    // and set all related objects with this target.name
                    if ( value instanceof Object) {
                        if ( !( name in target) || ! ( target[name].__isProxy ) ){
                            target[name]=new Proxy(value, SmartBind.getProxyHandler(bindContext, bindPath+'.'+name));
                        }
                        // run all tree to set proxies when necessary
                        var objKeys=Object.keys(value);
                        // console.debug("...objkeys",objKeys);
                        for ( var i=0; i<objKeys.length; i++ ) {
                            bindContext.mapDataTarget[bindPath+"."+name+"."+objKeys[i]]=target[name][objKeys[i]];
                            if ( typeof value[objKeys[i]] == 'undefined' || value[objKeys[i]] == null || ! ( value[objKeys[i]] instanceof Object ) || value[objKeys[i]].__isProxy )
                                continue;
                            target[name][objKeys[i]]=new Proxy( value[objKeys[i]], SmartBind.getProxyHandler(bindContext, bindPath+'.'+name+"."+objKeys[i]));
                        }
                        // TODO it can be faster than run all items
                        var bindKeys=Object.keys(bindContext.mapDom);
                        for ( var i=0; i<bindKeys.length; i++ ) {
                            // console.log("test...", bindKeys[i], " for ", bindPath+"."+name);
                            if ( bindKeys[i].startsWith(bindPath+"."+name) ) {
                                // console.log("its ok, lets update dom...", bindKeys[i]);
                                SmartBind.processBindToDom( bindContext, bindKeys[i] );
                            }
                        }
                    }
                    return true;
                }
            };
        }
        SmartBind.processBindToDom=function(bindContext, bindPath) {
            var domList=bindContext.mapDom[bindPath];
            if ( typeof domList != 'undefined' ) {
                try {
                    for ( var i=0; i < domList.length ; i++){
                        var dataTarget=SmartBind.getDataTarget(bindContext, bindPath);
                        if ( 'target' in dataTarget )
                            domList[i].value=dataTarget.target[dataTarget.item];
                        else
                            console.warn("Could not get data target", bindContext, bindPath);
                    }
                } catch (e){
                    console.warn("bind fail", bindPath, bindContext, e);
                }
            }
        }
    }
})();

次に、設定するには:

var bindContext=SmartBind.BindContext();
SmartBind.add(bindContext, document.getElementById('a'));
SmartBind.add(bindContext, document.getElementById('b'));
SmartBind.add(bindContext, document.getElementById('c'));

var bindContext2=SmartBind.BindContext();
SmartBind.add(bindContext2, document.getElementById('d'));
SmartBind.add(bindContext2, document.getElementById('e'));
SmartBind.add(bindContext2, document.getElementById('f'));
SmartBind.add(bindContext2, document.getElementById('g'));

setTimeout( function() {
    document.getElementById('b').value='Via Script works too!'
}, 2000);

document.getElementById('g').addEventListener('click',function(){
bindContext2.data.test='Set by js value'
})

ここでは、HTMLInputElement値のバインドを追加しました。

あなたがそれを改善する方法を知っているなら、私に知らせてください。


6

このリンク「JavaScriptでの簡単な双方向データバインディング」には、双方向データバインディングの非常に単純なベアボーン実装があります。

以前のリンクとknockoutjs、backbone.js、agility.jsのアイデアにより、この軽量で高速なMVVMフレームワークであるModelView.jsが生まれました jQueryに基づく これはjQueryでうまく動作し、私は謙虚な(または多分それほど謙虚ではない)作者です。

以下のサンプルコードを再現(ブログ投稿リンクから):

DataBinderのサンプルコード

function DataBinder( object_id ) {
  // Use a jQuery object as simple PubSub
  var pubSub = jQuery({});

  // We expect a `data` element specifying the binding
  // in the form: data-bind-<object_id>="<property_name>"
  var data_attr = "bind-" + object_id,
      message = object_id + ":change";

  // Listen to change events on elements with the data-binding attribute and proxy
  // them to the PubSub, so that the change is "broadcasted" to all connected objects
  jQuery( document ).on( "change", "[data-" + data_attr + "]", function( evt ) {
    var $input = jQuery( this );

    pubSub.trigger( message, [ $input.data( data_attr ), $input.val() ] );
  });

  // PubSub propagates changes to all bound elements, setting value of
  // input tags or HTML content of other tags
  pubSub.on( message, function( evt, prop_name, new_val ) {
    jQuery( "[data-" + data_attr + "=" + prop_name + "]" ).each( function() {
      var $bound = jQuery( this );

      if ( $bound.is("input, textarea, select") ) {
        $bound.val( new_val );
      } else {
        $bound.html( new_val );
      }
    });
  });

  return pubSub;
}

JavaScriptオブジェクトに関しては、この実験のためのユーザーモデルの最小実装は次のようになります。

function User( uid ) {
  var binder = new DataBinder( uid ),

      user = {
        attributes: {},

        // The attribute setter publish changes using the DataBinder PubSub
        set: function( attr_name, val ) {
          this.attributes[ attr_name ] = val;
          binder.trigger( uid + ":change", [ attr_name, val, this ] );
        },

        get: function( attr_name ) {
          return this.attributes[ attr_name ];
        },

        _binder: binder
      };

  // Subscribe to the PubSub
  binder.on( uid + ":change", function( evt, attr_name, new_val, initiator ) {
    if ( initiator !== user ) {
      user.set( attr_name, new_val );
    }
  });

  return user;
}

ここで、モデルのプロパティをUIの一部にバインドする場合は、対応するHTML要素に適切なデータ属性を設定するだけです。

// javascript
var user = new User( 123 );
user.set( "name", "Wolfgang" );

<!-- html -->
<input type="number" data-bind-123="name" />

このリンクで質問に答えることができますが、回答の重要な部分をここに含め、参照用のリンクを提供することをお勧めします。リンクされたページが変更されると、リンクのみの回答が無効になる可能性があります。
Sam Hanley

@sphanleyは、回答した投稿のコードがかなり長いため、おそらく時間があれば、更新するでしょう
Nikos M.

@sphanley、参照リンクからの回答のサンプルコードを再現(これにより、ほとんどの場合、重複したコンテンツが作成されますが)
Nikos M.

1
それは間違いなく複製コンテンツを作成しますが、それがポイントです-ブログのリンクはしばしば時間と共に壊れることがあり、ここで関連するコンテンツを複製することにより、それは将来の読者に利用可能で有用になるでしょう。答えは素晴らしいですね。
Sam Hanley

3

要素の値を変更すると、DOMイベントがトリガーされます。イベントに応答するリスナーを使用して、JavaScriptでデータバインディングを実装できます。

例えば:

function bindValues(id1, id2) {
  const e1 = document.getElementById(id1);
  const e2 = document.getElementById(id2);
  e1.addEventListener('input', function(event) {
    e2.value = event.target.value;
  });
  e2.addEventListener('input', function(event) {
    e1.value = event.target.value;
  });
}

以下は、DOM要素を相互に、またはJavaScriptオブジェクトとバインドする方法を示すコードとデモです。


3

HTML入力をバインドする

<input id="element-to-bind" type="text">

2つの関数を定義します。

function bindValue(objectToBind) {
var elemToBind = document.getElementById(objectToBind.id)    
elemToBind.addEventListener("change", function() {
    objectToBind.value = this.value;
})
}

function proxify(id) { 
var handler = {
    set: function(target, key, value, receiver) {
        target[key] = value;
        document.getElementById(target.id).value = value;
        return Reflect.set(target, key, value);
    },
}
return new Proxy({id: id}, handler);
}

関数を使用します。

var myObject = proxify('element-to-bind')
bindValue(myObject);

3

Object.definePropertyこれは、プロパティへのアクセス方法を直接変更するアイデアです。

コード:

function bind(base, el, varname) {
    Object.defineProperty(base, varname, {
        get: () => {
            return el.value;
        },
        set: (value) => {
            el.value = value;
        }
    })
}

使用法:

var p = new some_class();
bind(p,document.getElementById("someID"),'variable');

p.variable="yes"

フィドル:ここ


2

私は、onkeypressおよびonchangeイベントハンドラーを使用してjsおよびjsにバインドビューを作成するいくつかの基本的なjavascriptの例を確認しました。

ここに例のプランカーhttp://plnkr.co/edit/7hSOIFRTvqLAvdZT4Bcc?p=preview

<!DOCTYPE html>
<html>
<body>

    <p>Two way binding data.</p>

    <p>Binding data from  view to JS</p>

    <input type="text" onkeypress="myFunction()" id="myinput">
    <p id="myid"></p>
    <p>Binding data from  js to view</p>
    <input type="text" id="myid2" onkeypress="myFunction1()" oninput="myFunction1()">
    <p id="myid3" onkeypress="myFunction1()" id="myinput" oninput="myFunction1()"></p>

    <script>

        document.getElementById('myid2').value="myvalue from script";
        document.getElementById('myid3').innerHTML="myvalue from script";
        function myFunction() {
            document.getElementById('myid').innerHTML=document.getElementById('myinput').value;
        }
        document.getElementById("myinput").onchange=function(){

            myFunction();

        }
        document.getElementById("myinput").oninput=function(){

            myFunction();

        }

        function myFunction1() {

            document.getElementById('myid3').innerHTML=document.getElementById('myid2').value;
        }
    </script>

</body>
</html>

2
<!DOCTYPE html>
<html>
<head>
    <title>Test</title>
</head>
<body>

<input type="text" id="demo" name="">
<p id="view"></p>
<script type="text/javascript">
    var id = document.getElementById('demo');
    var view = document.getElementById('view');
    id.addEventListener('input', function(evt){
        view.innerHTML = this.value;
    });

</script>
</body>
</html>

2

変数を入力にバインドする簡単な方法(双方向バインド)は、ゲッターとセッターの入力要素に直接アクセスするだけです。

var variable = function(element){                    
                   return {
                       get : function () { return element.value;},
                       set : function (value) { element.value = value;} 
                   }
               };

HTML:

<input id="an-input" />
<input id="another-input" />

そして使用するには:

var myVar = new variable(document.getElementById("an-input"));
myVar.set(10);

// and another example:
var myVar2 = new variable(document.getElementById("another-input"));
myVar.set(myVar2.get());


ゲッター/セッターなしで上記を行うより洗練された方法:

var variable = function(element){

                return function () {
                    if(arguments.length > 0)                        
                        element.value = arguments[0];                                           

                    else return element.value;                                                  
                }

        }

使用するには:

var v1 = new variable(document.getElementById("an-input"));
v1(10); // sets value to 20.
console.log(v1()); // reads value.

1

これは、バニラJavaScriptでの非常にシンプルな双方向データバインディングです。

<input type="text" id="inp" onkeyup="document.getElementById('name').innerHTML=document.getElementById('inp').value;">

<div id="name">

</div>


2
確かにこれはonkeyupイベントでのみ機能しますか?つまり、ajaxリクエストを実行し、JavaScriptを介してinnerHTMLを変更した場合、これは機能しません
Zach Smith

1

パーティーの後半、特に2か月分のライブラリを2か月/年前に書いたので、後で触れますが、それでも私には関係があるようです。それを本当に短いスポイラーにするために、私の選択したテクノロジーは:

  • Proxy モデルの観察用
  • MutationObserver DOMの変更の追跡用(値の変更ではなく、バインドの理由による)
  • 値の変更(モデルフローのビュー)は通常のaddEventListenerハンドラーを介して処理されます

IMHO、OPに加えて、データバインディングの実装では次のことが重要です。

  • さまざまなアプリのライフサイクルケースを処理する(HTMLが最初に、次にJS、JSが最初に、次にHTML、動的属性の変更など)
  • モデルのディープバインディングを許可して、バインドできるようにする user.address.block
  • モデルとしての配列は正しくサポートされている(されるべきであるshiftspliceと同様)
  • ShadowDOMを処理する
  • 可能な限りテクノロジーの置き換えが容易になるように試みます。したがって、フレームワークとの結合が多すぎるため、テンプレートのサブ言語は将来の変更に対応しないアプローチです。

これらすべてを考慮に入れると、私の意見では、数十のJS行をスローするだけでは不可能です。私はlibではなくパターンとしてそれをやろうとしました-私にとってはうまくいきませんでした。

次に、持つことObject.observeは削除されますが、モデルの観察が重要な部分であることを考えると、この部分全体は別のlibに関係する必要があります。ここで、私がこの問題をどのように取り扱ったかについての原則を説明します。OPが尋ねたとおりです。

モデル(JS部分)

モデル観察の私の見解はProxyです。これは、それを機能させるための唯一の健全な方法であるIMHOです。完全な機能observerは独自のライブラリに値するのでobject-observer、その唯一の目的のためにライブラリを開発しました。

モデルはいくつかの専用APIを介して登録する必要があります。つまり、POJOがに変わるポイントでありObservable、ここにショートカットを表示できません。バインドされたビュー(以下を参照)と見なされるDOM要素は、最初にモデルの値で更新され、次に各データが変更されると更新されます。

ビュー(HTML部分)

バインディングを表現する最もクリーンな方法であるIMHOは、属性を使用します。多くの人が以前にこれを行い、多くの人が後に行うので、ここではニュースはありません。これはそれを行う正しい方法にすぎません。私の場合、次の構文を使用しました:<span data-tie="modelKey:path.to.data => targerProperty"></span>が、これはそれほど重要ではありません。何私にとって重要、HTMLには複雑なスクリプトの構文は-これは、再び、私見間違っていません。

バインドされたビューとして指定されたすべての要素が最初に収集されます。モデルとビューの間の一部の内部マッピングを管理することは、パフォーマンスの面からは避けられないように見えます。実行時のルックアップと更新を保存するためにメモリ+一部の管理を犠牲にする必要がある正しいケースのようです。

前述のように、ビューは、利用可能な場合は最初にモデルから更新され、その後のモデル変更時に更新されます。さらにMutationObserver、動的に追加/削除/変更された要素に反応(バインド/バインド解除)するために、DOM全体を監視する必要があります。さらに、バインドされていないブラックホールを残さないために、これらすべてをShadowDOM(もちろんオープンなもの)に複製する必要があります。

具体的なリストはさらに進んでいくかもしれませんが、私の意見では、これらは、一方からの機能の完全性ともう一方からのまともなシンプルさのバランスが取れた状態でデータバインディングを実装する主要な原則です。

したがって、object-observer上記に加えて、data-tier上記の概念に沿ってデータバインディングを実装するライブラリも作成しました。


0

過去7年間で状況は大きく変化しました。現在、ほとんどのブラウザーにネイティブWebコンポーネントがあります。問題の核となるIMOは、要素間で状態を共有することです。状態が変化したときにUIを更新するのは簡単です。

要素間でデータを共有するには、StateObserverクラスを作成し、そこからWebコンポーネントを拡張できます。最小限の実装は次のようになります。

// create a base class to handle state
class StateObserver extends HTMLElement {
	constructor () {
  	super()
    StateObserver.instances.push(this)
  }
	stateUpdate (update) {
  	StateObserver.lastState = StateObserver.state
    StateObserver.state = update
    StateObserver.instances.forEach((i) => {
    	if (!i.onStateUpdate) return
    	i.onStateUpdate(update, StateObserver.lastState)
    })
  }
}

StateObserver.instances = []
StateObserver.state = {}
StateObserver.lastState = {}

// create a web component which will react to state changes
class CustomReactive extends StateObserver {
	onStateUpdate (state, lastState) {
  	if (state.someProp === lastState.someProp) return
    this.innerHTML = `input is: ${state.someProp}`
  }
}
customElements.define('custom-reactive', CustomReactive)

class CustomObserved extends StateObserver {
	connectedCallback () {
  	this.querySelector('input').addEventListener('input', (e) => {
    	this.stateUpdate({ someProp: e.target.value })
    })
  }
}
customElements.define('custom-observed', CustomObserved)
<custom-observed>
  <input>
</custom-observed>
<br />
<custom-reactive></custom-reactive>

ここをいじる

私はこのアプローチが好きです:

  • data-プロパティを見つけるためのdomトラバーサルはありません
  • Object.observe(非推奨)
  • プロキシなし(フックを提供しますが、通信メカニズムはありません)
  • 依存関係なし(ターゲットブラウザーに応じたポリフィル以外)
  • それは合理的に一元化され、モジュール化されています... htmlで状態を記述し、どこにでもリスナーがいると非常に混乱します。
  • それは拡張可能です。この基本的な実装は20行のコードですが、簡単に操作できるように、便利さ、不変性、および状態の形の魔法を簡単に構築できます。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.