JavaScript'te DOM Veri Bağlama Nasıl Yapılır


244

Lütfen bu soruyu kesinlikle eğitici olarak ele alın. Hala bunu uygulamak için yeni cevaplar ve fikirler duymakla ilgileniyorum

tl; Dr.

JavaScript ile çift yönlü veri bağlamayı nasıl uygulayabilirim?

DOM'ye Veri Bağlama

DOM'ye veri bağlayarak, örneğin, bir özelliğe sahip bir JavaScript nesnesine asahip olmak demek istiyorum b. Daha sonra bir <input>DOM öğesi (örneğin), DOM öğesi değiştiğinde, adeğiştiğinde ve tersi olduğunda (yani, çift yönlü veri bağlama anlamına gelir).

İşte AngularJS'den bunun nasıl göründüğüne dair bir diyagram:

iki yönlü veri bağlama

Yani temelde benzer JavaScript var:

var a = {b:3};

Sonra aşağıdaki gibi bir girdi (veya başka bir form) öğesi:

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

Girişin değerinin değer olmasını istiyorum a.b(örneğin) ve giriş metni değiştiğinde ben a.bde değiştirmek istiyorum . Ne zaman a.bJavaScript değişiklikler, giriş değiştirir.

Soru

Bunu basit JavaScript'te gerçekleştirmek için bazı temel teknikler nelerdir?

Özellikle, iyi bir cevap istiyorum:

  • Bağlama nesneler için nasıl çalışır?
  • Formdaki değişikliği dinlemek nasıl işe yarayabilir?
  • Basit bir şekilde HTML'nin sadece şablon düzeyinde değiştirilmesi mümkün müdür? HTML belgesinin kendisinde bağlayıcılığı izlemek istemiyorum, sadece JavaScript'te (DOM olayları ve kullanılan DOM öğelerine referans tutarak JavaScript ile).

Ne denedim?

Ben büyük bir Bıyık hayranıyım, bu yüzden şablon olarak kullanmaya çalıştım. Ancak, bıyık HTML bir dize olarak işlediği için veri bağlama kendini gerçekleştirmeye çalışırken sorunları karşılaştım, bu yüzden sonuç aldıktan sonra benim görünüm modelindeki nesnelerin nerede referans yok. Bunun için düşünebildiğim tek çözüm, HTML dizesini (veya oluşturulan DOM ağacını) özniteliklerle değiştirmekti. Farklı bir şablon motor kullanmayı umursamıyorum.

Temel olarak, eldeki sorunu karmaşıklaştırdığımı güçlü bir şekilde hissettim ve basit bir çözüm var.

Not: Lütfen, özellikle binlerce satırlık kod satırları olan harici kütüphaneleri kullanan cevaplar vermeyin. AngularJS ve KnockoutJS kullandım (ve beğendim!). Gerçekten 'x çerçevesini kullan' biçiminde cevaplar istemiyorum. Optimal olarak, çift yönlü veri bağlama yönteminin nasıl uygulanacağını kavramak için birçok çerçevenin nasıl kullanılacağını bilmeyen gelecekteki bir okuyucu istiyorum. Tam bir cevap beklemiyorum , ama fikri karşılayan bir cevap bekliyorum .


2
CrazyGlue'yu Benjamin Gruenbaum'un tasarımına dayandırdım . Ayrıca SELECT, onay kutusu ve radyo etiketlerini de destekler. jQuery bir bağımlılıktır.
JohnSz

12
Bu soru tamamen harika. Konu dışı veya aptalca saçmalık olduğu için kapanırsa, ciddiye alınacağım.
OCDev

@JohnSz CrazyGlue projenizden bahsettiğiniz için teşekkür ederiz. Uzun zamandır 2 yönlü basit bir veri bağlayıcısı arıyordum. Görünüşe göre Object.observe kullanmıyorsunuz, bu nedenle tarayıcı desteğiniz harika olmalı. Ve bıyık templating kullanmıyorsunuz, bu yüzden mükemmel.
Gavin

@Benjamin Sonunda ne yaptın?
johnny

Benim görüşüme göre @johnny doğru yaklaşım JS (React gibi) oluşturmak ve tersi oluşturmaktır. Bence eninde sonunda bunu yapacağız.
Benjamin Gruenbaum

Yanıtlar:


106
  • Bağlama nesneler için nasıl çalışır?
  • Formdaki değişikliği dinlemek nasıl işe yarayabilir?

Her iki nesneyi de güncelleyen bir soyutlama

Sanırım başka teknikler var, ama sonuçta ilgili bir DOM öğesine referans tutan ve kendi verilerine ve ilgili öğesine yönelik güncellemeleri koordine eden bir arayüz sağlayan bir nesnem olurdu.

Bunun .addEventListener()için çok güzel bir arayüz sağlar. Ona eventListenerarabirimi uygulayan bir nesne verebilirsiniz ve işleyiciyi bu nesne ile thisdeğer olarak çağırır .

Bu, hem öğeye hem de ilgili verilere otomatik erişim sağlar.

Nesnenizi tanımlama

Prototip paleti, elbette gerekli olmasa da, bunu uygulamanın güzel bir yoludur. İlk olarak, öğenizi ve bazı başlangıç ​​verilerini alan bir oluşturucu oluşturursunuz.

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

Böylece burada yapıcı eleman ve verileri yeni nesnenin özellikleri üzerine depolar. Ayrıca changeverilen bir olayı bağlar element. İlginç olan şey, ikinci argüman olarak işlev yerine yeni nesneyi geçirmesidir. Ama bu tek başına işe yaramaz.

Uygulama eventListenerarayüzü

Bunu yapmak için nesnenizin eventListenerarabirimi uygulaması gerekir . Bunu gerçekleştirmek için gereken tek şey nesneye bir handleEvent()yöntem vermektir .

Miras burada devreye giriyor.

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;
};

Bunun yapılandırılabileceği birçok farklı yol var, ancak güncellemeleri koordine etme örneğiniz için, change() yöntemi yalnızca bir değer kabul etmeye handleEventve olay nesnesi yerine bu değere geçirmeye . Bu şekilde change()olay olmadan da çağrılabilir.

Şimdi, change olay gerçekleştiğinde hem öğeyi hem de .dataözelliği güncelleyecektir . Aynı şey .change()JavaScript programınızı aradığınızda da olacaktır .

Kodu kullanma

Şimdi sadece yeni nesneyi oluşturacak ve güncellemeler yapmasına izin vereceksiniz. Girişte JS kodunda güncellemeler görünecek ve girişteki değişiklik olayları JS kodunda görünecektir.

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);

DEMO: http://jsfiddle.net/RkTMD/


5
+1 Çok temiz bir yaklaşım, çok basit ve insanların öğrenebileceği kadar basit, sahip olduğumdan çok daha temiz. Yaygın bir kullanım örneği, nesnelerin görünümlerini temsil etmek için koddaki şablonları kullanmaktır. Bunun nasıl çalıştığını merak ediyordum? Bıyık gibi motorlarda Mustache.render(template,object), bir nesneyi şablonla senkronize tutmak istediğimi varsayarak (Bıyık'a özgü olmayan) bir şey yapıyorum, bu konuda nasıl devam edeceğim?
Benjamin Gruenbaum

3
@BenjaminGruenbaum: İstemci tarafı şablonları kullanmadım, ancak Bıyık'ın ekleme noktalarını tanımlamak için bazı sözdizimi olduğunu ve sözdiziminin bir etiket içerdiğini hayal ediyorum. Bu yüzden şablonun "statik" kısımlarının bir Array'da depolanan HTML parçalarına dönüştürüleceğini ve dinamik parçaların bu parçalar arasında gideceğini düşünürdüm. Daha sonra ekleme noktalarındaki etiketler nesne özellikleri olarak kullanılır. Daha sonra bazıları inputbu noktalardan birini güncellemek istiyorsa, girdiden o noktaya bir eşleme olur. Kısa bir örnek bulup bulamayacağımı göreceğim.

1
@BenjaminGruenbaum: Hmmm ... İki farklı öğeyi nasıl temiz bir şekilde koordine edeceğimi düşünmedim. Bu, ilk başta düşündüğümden biraz daha dahil. Yine de merak ediyorum, bu yüzden biraz sonra bunun üzerinde çalışmam gerekebilir. :)

2
TemplateAyrıştırma yapan, farklı MyCtornesneleri tutan ve her birini tanımlayıcı tarafından güncellemek için bir arayüz sağlayan birincil bir kurucu olduğunu göreceksiniz . Sorunuz olursa lütfen bana söyleyin. :) DÜZENLEME: ... bunun yerine bu bağlantıyı kullanın ... JS güncellemelerini göstermek için her 10 saniyede bir girdi değerinde üstel bir artış olduğunu unutmuştum. Bu onu sınırlar.

2
... tamamen yorumlanmış sürüm artı küçük iyileştirmeler.

36

Ben de kendi çözümümü çöpe atmaya karar verdim. İşte çalışan bir keman . Bunun yalnızca çok modern tarayıcılarda çalıştığını unutmayın.

Ne kullanır

Bu uygulama çok modern - (çok) modern bir tarayıcı ve kullanıcılar iki yeni teknoloji gerektiriyor:

  • MutationObserverdom'daki değişiklikleri algılamak için s (olay dinleyicileri de kullanılır)
  • Object.observenesnedeki değişiklikleri tespit etmek ve dom'a bildirmek için. Tehlike, bu cevap yazıldığı için Oo, ECMAScript TC tarafından tartışıldı ve kararlaştırıldı, bir çoklu dolguyu düşünün .

Nasıl çalışır

  • Öğeye bir domAttribute:objAttributeeşleme koyun - örneğinbind='textContent:name'
  • Bunu dataBind işlevinde okuyun. Hem öğede hem de nesnede yapılan değişiklikleri gözlemleyin.
  • Bir değişiklik meydana geldiğinde - ilgili öğeyi güncelleyin.

Çözüm

İşte dataBindfonksiyon, sadece 20 satır kod olduğunu ve daha kısa olabileceğini unutmayın:

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]; 
}

İşte bazı kullanım:

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);

İşte çalışan bir keman . Bu çözümün oldukça genel olduğunu unutmayın. Object.observe ve mutasyon gözlemci parlaması mevcuttur.


1
Ben kimse bunu yararlı bulursa, eğlence için bu (es5) yazmak oldu - kendinizi dışarı vurmak jsfiddle.net/P9rMm
Benjamin

1
obj.nameBir ayarlayıcı olduğunda harici olarak gözlemlenemediğini, ancak ayarlayıcıdan değiştiğini yayınlaması gerektiğini unutmayın - html5rocks.com/en/tutorials/es7/observe/#toc-notifications - eserlere biraz anahtar atar ayarlayıcıları kullanarak daha karmaşık, birbirine bağlı davranışlar istiyorsanız Oo () için. Ayrıca, obj.nameyapılandırılamadığında, ayarlayıcısının yeniden tanımlanmasına (bildirim eklemek için çeşitli numaralarla) da izin verilmez - bu nedenle Oo () ile jenerikler bu durumda tamamen hurdaya çıkarılır.
Nolo

8
Object.observe tüm tarayıcılardan kaldırıldı: caniuse.com/#feat=object-observe
JvdBerg

1
Object.observe veya github.com/anywhichway/proxy-observe veya gist.github.com/ebidel/1b553d571f924da2da06 veya daha eski çoklu dolgular yerine github @JvdBerg
jimmont

29

Preposter'ime eklemek istiyorum. Bir yöntem kullanmadan nesnenize yeni bir değer atamanıza izin verecek biraz farklı bir yaklaşım öneririm. Bununla birlikte, özellikle eski tarayıcılar tarafından desteklenmediği ve IE9'un hala farklı bir arayüz kullanılmasını gerektirdiği unutulmamalıdır.

En önemlisi, yaklaşımım olaylardan faydalanmıyor.

Getters ve Setters

Teklifim , alıcıların ve ayarlayıcıların nispeten genç özelliğini kullanıyor , özellikle sadece olmak üzere . Genel olarak konuşursak, mutasyoncular, belirli özelliklerin bir değere nasıl atandığı ve alındığı davranışını "özelleştirmemize" izin verir.

Burada kullanacağım bir uygulama Object.defineProperty yöntemidir. FireFox, GoogleChrome ve - sanırım - IE9'da çalışır. Diğer tarayıcıları test etmedim, ama bu sadece teori olduğu için ...

Her neyse, üç parametre kabul eder. İlk parametre yeni bir özellik tanımlamak istediğiniz nesnedir, ikincisi yeni özelliğin adına benzeyen bir dize ve son olarak yeni özelliğin davranışı hakkında bilgi sağlayan bir "tanımlayıcı nesne" dir.

Özellikle ilginç iki tanımlayıcı getve set. Bir örnek aşağıdaki gibi görünecektir. Bu ikisini kullanmanın diğer 4 tanımlayıcının kullanılmasını yasakladığını unutmayın.

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;
        }
    });
}

Şimdi bundan faydalanmak biraz farklı oluyor:

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

Bunun sadece modern tarayıcılarda işe yaradığını vurgulamak istiyorum.

Çalışan keman: http://jsfiddle.net/Derija93/RkTMD/1/


2
Sadece Harmony Proxynesnelerimiz olsaydı :) Setters güzel bir fikir gibi görünüyor, ama bu gerçek nesneleri değiştirmemizi gerektirmez mi? Ayrıca, bir yan notta - Object.createburada kullanılabilir (yine, ikinci parametreye izin veren modern tarayıcı varsayalım). Ayrıca, ayarlayıcı / alıcı nesneye ve DOM öğesine :) farklı bir değeri 'yansıtmak' için kullanılabilir. Ben de templating hakkında herhangi bir fikir var mı merak ediyorum, özellikle burada güzel bir yapı, burada gerçek bir meydan okuma gibi görünüyor :)
Benjamin Gruenbaum

Tıpkı preposter'ım gibi, müşteri tarafı şablonlama motorlarında da çok fazla çalışmıyorum, üzgünüm. :( Ama gerçek nesneleri değiştirmekle ne demek istiyorsun ? Ve ayarlayıcının / alıcının ne için kullanılabileceğini nasıl anlayacağına dair düşüncelerini anlamak istiyorum ... Buradaki alıcılar / ayarlayıcılar hiçbir şey için kullanılıyor ancak nesneye DOM'dan tüm girdileri ve geri alma işlemlerini yönlendirirsiniz, temelde sizin gibi a Proxy.;) İki farklı özelliği senkronize tutmak için zorluğu anladım. Benim yöntem her ikisini de ortadan kaldırır.
Kiruse

A Proxy, alıcıları / ayarlayıcıları kullanma ihtiyacını ortadan kaldıracak, sahip oldukları özellikleri bilmeden öğeleri bağlayabilirsiniz. Demek istediğim, getters mantık (ve belki de bir şablon) içerebilir bindTo.value daha fazla değişebilir olmasıdır. Soru, bu tür bir çift yönlü bağlamayı akılda bir şablonla nasıl koruyacağınızdır? Diyelim ki nesnemi bir forma eşleştiriyorum, hem öğeyi hem de formu senkronize etmek istiyorum ve bu tür şeyler hakkında nasıl devam edeceğimi merak ediyorum. Dışarı kontrol edebilirsiniz nasıl nakavt o eserler learn.knockoutjs.com/#/?tutorial=intro örneğin
Benjamin Gruenbaum

@BenjaminGruenbaum Gotcha. Bir bakayım.
Kiruse

@BenjaminGruenbaum Anlamaya çalıştığın şeyi görüyorum. Tüm bunları şablonlar göz önünde bulundurarak ayarlamak biraz daha zor olur. Bu senaryo üzerinde bir süre çalışacağım (ve sürekli olarak yeniden temellendireceğim). Ama şimdilik bir ara veriyorum. Aslında bunun için yeterli zamanım yok.
Kiruse

7

Sanırım cevabım daha teknik olacak ama diğerleri farklı teknikleri kullanarak aynı şeyi sunduğundan farklı olmayacak.
İlk önce, bu sorunun çözümü "gözlemci" olarak bilinen bir tasarım modelinin kullanılmasıdır, verilerinizi sunumunuzdan ayırmanıza izin verir, bir şeydeki değişikliği dinleyicilerine yayınlar, ancak bu durumda iki yönlüdür.

DOM'dan JS'ye yolu için

DOM'dan js nesnesine veri bağlamak için, datanitelikler (veya uyumluluk gerekiyorsa sınıflar) biçiminde işaretleme ekleyebilirsiniz , örneğin:

<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=""/>

Bu şekilde js querySelectorAll(veya eski arkadaşınızla getElementsByClassNameuyumluluk) kullanarak erişilebilir .

Artık olayları dinleyerek olayı şu yolla bağlayabilirsiniz: nesne başına bir dinleyici veya kapsayıcıya / belgeye büyük bir dinleyici. Belgeye / konteynere bağlanma, olayda veya alt öğesinde yapılan her değişiklik için olayı tetikler, daha küçük bir bellek alanı olur, ancak olay çağrıları ortaya çıkar.
Kod şöyle görünecektir:

//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 için DOM yolu yapın

İki şeye ihtiyacınız olacak: cadı DOM öğesinin referanslarını tutacak bir meta nesne, her js nesnesine / özniteliğine ve nesnelerdeki değişiklikleri dinlemenin bir yoluna bağlanır. Temel olarak aynıdır: Nesnedeki değişiklikleri dinlemek ve daha sonra DOM düğümüne bağlamak için bir yolunuz olmalıdır, çünkü nesneniz "olamaz" meta verileri, meta verileri bir şekilde tutan başka bir nesneye ihtiyacınız olacak özellik adının meta veri nesnesinin özellikleriyle eşlenmesidir. Kod şöyle bir şey olacaktır:

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);

Umarım yardımcı olmuşumdur.


.observer kullanımıyla ilgili karşılaştırılabilirlik sorunu yok mu?
Mohsen Shakiba

Object.observeşimdilik sadece kromda destek sunulduğu için şim veya çoklu dolguya ihtiyacı var . caniuse.com/#feat=object-observe
madcampos

9
Object.observe öldü. Bunu burada not edeceğimi düşündüm.
Benjamin Gruenbaum

@BenjaminGruenbaum Bu öldüğü için şimdi kullanılacak doğru şey nedir?
johnny

1
@johnny Eğer yanlış değilim, onlar bir nesne ile ne yapabilirim daha ayrıntılı bir kontrol izin vekil proxy tuzakları olurdu, ama bunu araştırmak zorunda.
madcampos

7

Dün, verileri bağlamak için kendi yolumu yazmaya başladım.

Onunla oynamak çok komik.

Bence çok güzel ve çok kullanışlı. En azından firefox ve krom kullanan testlerimde, Edge de çalışmalı. Diğerleri hakkında emin değilim, ancak Proxy'yi desteklerse işe yarayacağını düşünüyorum.

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' />

İşte kod:

(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);
                }
            }
        }
    }
})();

Sonra, ayarlamak için, sadece:

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'
})

Şimdilik, HTMLInputElement değer bağlamasını ekledim.

Bunu nasıl geliştireceğinizi biliyorsanız bana bildirin.


6

"JavaScript'te Kolay İki Yönlü Veri Bağlama" bağlantısında 2 yönlü veri bağlamanın çok basit bir barebone uygulaması var

Önceki bağlantı knockoutjs, backbone.js ve agility.js'den gelen fikirlerle birlikte bu hafif ve hızlı MVVM çerçevesine, ModelView.js yol açtı. jQuery tabanlı hangi jQuery ile güzel oynar ve ben mütevazı (ya da belki de mütevazı değil) yazarım.

Aşağıdaki örnek kodu yeniden oluşturma ( blog yazısı bağlantısından ):

DataBinder için örnek kod

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 nesnesini ilgilendiren şey için, bu deneme uğruna bir Kullanıcı modelinin minimal uygulaması aşağıdaki olabilir:

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;
}

Şimdi, bir modelin özelliğini bir kullanıcı arayüzüne bağlamak istediğimizde, karşılık gelen HTML öğesinde uygun bir veri özelliği ayarlamamız gerekir:

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

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

Bu bağlantı soruyu cevaplayabilse de, cevabın temel kısımlarını buraya eklemek ve bağlantıyı referans olarak sağlamak daha iyidir. Bağlantı verilen sayfa değişirse, yalnızca bağlantı yanıtları geçersiz olabilir.
Sam Hanley

@sphanley, kaydetti, bir cevap gönderisi için oldukça uzun bir kod olduğu için muhtemelen daha fazla zamanım olduğunda güncelleyeceğim
Nikos M.

(ben bu çoğu zaman duplicarte içerik oluşturur thinbk rağmen, yine de) @sphanley, başvurulan bağlantısından yanıta örnek kod çoğaltılamaz
Nikos M.

1
Kesinlikle yinelenen içerik oluşturuyor, ancak nokta bu - blog bağlantıları genellikle zamanla kopabilir ve ilgili içeriği burada çoğaltarak gelecekteki okuyucular için kullanılabilir ve kullanışlı olmasını sağlar. Cevap şimdi harika görünüyor!
Sam Hanley

3

Bir öğenin değerini değiştirmek bir DOM etkinliğini tetikleyebilir . Olaylara yanıt veren dinleyiciler JavaScript'te veri bağlama uygulamak için kullanılabilir.

Örneğin:

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;
  });
}

İşte DOM öğelerinin birbirine veya bir JavaScript nesnesine nasıl bağlanabileceğini gösteren kod ve bir demo.


3

Tüm html girişlerini bağlama

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

iki işlevi tanımlayın:

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);
}

fonksiyonları kullanın:

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

3

Aşağıda Object.defineProperty, bir özelliğe erişilme şeklini doğrudan değiştiren bir fikir verilmiştir .

Kod:

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

Kullanımı:

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

p.variable="yes"

keman: İşte


2

Ben görüntülemek için bizim js ve js bağlama görünümü yapmak için onkeypress ve onchange olay işleyicileri kullanarak bazı temel javascript örneği geçti

Burada örnek plunker 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

Bir değişkeni bir girişe (iki yönlü ciltleme) bağlamanın basit bir yolu, alıcı ve ayarlayıcıdaki giriş öğesine doğrudan erişmektir:

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

HTML'de:

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

Ve kullanmak için:

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());


Yukarıdakiler / ayarlayıcı olmadan yukarıdakileri yapmanın daha zarif bir yolu:

var variable = function(element){

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

                    else return element.value;                                                  
                }

        }

Kullanmak:

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

1

Vanilya javascript çok basit iki yönlü veri bağlama ....

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

<div id="name">

</div>


2
şüphesiz bu sadece onkeyup olayıyla çalışır mı? yani bir ajax isteği yaptıysanız ve daha sonra innerHTML'yi JavaScript aracılığıyla değiştirdiyseniz, bu işe yaramaz
Zach Smith

1

Partiye geç, özellikle aylar / yıllar önce 2 libs yazdığım için, daha sonra bahsedeceğim, ama yine de benimle alakalı görünüyor. Gerçekten kısa bir spoyler yapmak için, benim tercih ettiğim teknolojiler:

  • Proxy modelin gözlemlenmesi için
  • MutationObserver DOM'un izleme değişikliklerinde (bağlayıcı nedenlerle değil, değer değişiklikleri için)
  • değer değişiklikleri (model akışına görünüm) normal addEventListenerişleyiciler aracılığıyla gerçekleştirilir

IMHO, OP'ye ek olarak, veri bağlama uygulamasının şunları yapması önemlidir:

  • farklı uygulama yaşam döngüsü vakalarını işlemek (önce HTML, sonra JS, önce JS, ardından HTML, dinamik özellikler değişir vb.)
  • modelin derin bağlanmasına izin verin, böylece bağlanabilir user.address.block
  • model olarak diziler doğru şekilde desteklenmelidir ( shift,splice ve benzerleri)
  • ShadowDOM ile başa çıkmak
  • teknolojinin değiştirilmesi için olabildiğince kolay olmaya çalışın, bu nedenle herhangi bir geçici alt dil, çerçeve ile çok fazla birleştiğinden geleceği değiştirmeyen bir yaklaşımdır

Tüm bunları dikkate alarak, bence birkaç düzinelerce JS hattı atmayı imkansız hale getiriyor. Bunu lib yerine desen olarak yapmaya çalıştım - benim için çalışmadı.

Daha sonra, sahip Object.observeolmak kaldırılır ve yine de modelin gözlemlenmesinin çok önemli bir parçası olduğu göz önüne alındığında - bu bölümün tamamı başka bir lib'e endişe ile ayrılmalıdır ZORUNLU. Şimdi bu sorunu nasıl aldığımın ilkelerine gelince - tam olarak OP'nin istediği gibi:

Model (JS bölümü)

Model gözlem için benim vekilim Proxy , onu çalıştırmanın tek aklı yolu IMHO. Tamamen özellikli observerkendi kütüphanesini hak ediyor, bu yüzden geliştirdimobject-observer sadece bu amaç için kütüphane .

Modeller, özel bir API aracılığıyla kaydedilmelidir, POJO'ların s'ye dönüştüğü nokta, burada Observableherhangi bir kısayol göremez. Bağlı görünümler olarak kabul edilen DOM öğeleri (aşağıya bakın), önce modelin / değerlerin ve ardından her veri değişikliğinin ardından güncellenir.

Görünümler (HTML bölümü)

Bağlamayı ifade etmenin en temiz yolu IMHO, niteliklerdir. Birçoğu bunu daha önce yaptı ve birçoğu sonra yapacak, bu yüzden burada haber yok, bu sadece doğru bir yol. Benim durumumda şu sözdizimine geçtim: <span data-tie="modelKey:path.to.data => targerProperty"></span>ama bu daha az önemli. Benim için önemli olan, HTML'de karmaşık komut dosyası sözdizimi yok - bu yine yanlış, IMHO.

Sınırlı görünüm olarak belirlenen tüm unsurlar ilk önce toplanacaktır. Modeller ve görünümler arasında bazı dahili eşlemeleri yönetmek benim için performans tarafından kaçınılmaz görünüyor, çalışma zamanı aramalarını ve güncellemelerini kaydetmek için bellek + bazı yönetimin feda edilmesi gereken doğru bir durum gibi görünüyor.

Görüşler, ilk önce modelden, varsa daha sonra ve daha sonra söylediğimiz gibi, model değişikliklerinde güncellenir. Dahası MutationObserver, dinamik olarak eklenen / kaldırılan / değiştirilen elemanlar üzerinde reaksiyona girme (bağlama / bağlama) için tüm DOM gözlenmelidir . Ayrıca, tüm bunlar, bağlı olmayan kara delikler bırakmamak için ShadowDOM'a (elbette açık olana) kopyalanmalıdır.

Spesifikasyonlar listesi daha da ileri gidebilir, ancak bunlar bence veri bağlayıcılığını birinden iyi bir özellik bütünlüğü dengesi ve diğer taraftan akıllıca basitlik ile uygulayacak ana ilkelerdir.

Ve böylece, object-observeryukarıda belirtilenlere ek olarak data-tier, yukarıda belirtilen kavramlar boyunca veri bağlamayı uygulayan gerçekten de kütüphane yazdım .


0

Son 7 yılda işler çok değişti, şu anda çoğu tarayıcıda yerel web bileşenlerimiz var. IMO sorunun özü, durum değiştiğinde UI'yi güncellemenin önemsiz olduğunu düşündüğünüzde, öğeler arasında durumu paylaşmaktır.

Öğeler arasında veri paylaşmak için bir StateObserver sınıfı oluşturabilir ve web bileşenlerinizi bundan genişletebilirsiniz. Minimal bir uygulama şuna benzer:

// 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>

buraya keman

Bu yaklaşımı seviyorum çünkü:

  • data-mülk bulmak için dom traversal yok
  • no Object.observe (kullanımdan kaldırıldı)
  • Proxy yok (bir kanca sağlar, ancak yine de iletişim mekanizması yoktur)
  • bağımlılık yok (hedef tarayıcılarınıza bağlı olarak çoklu dolgu dışında)
  • makul bir şekilde merkezi ve modüler ... html'de durumu tanımlamak ve her yerde dinleyicilere sahip olmak çok hızlı bir şekilde dağınık olur.
  • genişletilebilir. Bu temel uygulama 20 kod satırıdır, ancak çalışmayı kolaylaştırmak için kolayca bazı kolaylık, değişmezlik ve durum şekli büyüsü oluşturabilirsiniz.
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.