Bagaimana Menerapkan Pengikatan DOM Data dalam JavaScript

244

Silakan perlakukan pertanyaan ini sebagai sangat mendidik. Saya masih tertarik mendengar jawaban dan ide baru untuk mengimplementasikannya

tl; dr

Bagaimana saya menerapkan pengikatan data dua arah dengan JavaScript?

Mengikat data ke DOM

Dengan mengikat data ke DOM yang saya maksud misalnya, memiliki objek JavaScript adengan properti b. Kemudian memiliki <input>elemen DOM (misalnya), ketika elemen DOM berubah, aberubah dan sebaliknya (yaitu, maksud saya pengikatan data dua arah).

Ini adalah diagram dari AngularJS tentang seperti apa ini:

pengikatan data dua arah

Jadi pada dasarnya saya punya JavaScript mirip dengan:

var a = {b:3};

Kemudian elemen input (atau bentuk lain) seperti:

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

Saya ingin nilai input menjadi a.b nilai (misalnya), dan ketika teks input berubah, saya juga ingin a.bmengubahnya. Ketika a.bperubahan dalam JavaScript, input berubah.

Pertanyaan

Apa saja teknik dasar untuk mencapai ini dalam JavaScript biasa?

Secara khusus, saya ingin jawaban yang bagus untuk merujuk pada:

  • Bagaimana cara mengikat bekerja untuk objek?
  • Bagaimana mendengarkan perubahan dalam bentuk mungkin bekerja?
  • Apakah mungkin dengan cara sederhana untuk hanya memodifikasi HTML pada tingkat templat? Saya ingin tidak melacak pengikatan dalam dokumen HTML itu sendiri tetapi hanya dalam JavaScript (dengan peristiwa DOM, dan JavaScript menjaga referensi ke elemen DOM yang digunakan).

Apa yang sudah saya coba?

Saya penggemar berat Kumis jadi saya mencoba menggunakannya untuk templating. Namun, saya mengalami masalah ketika mencoba melakukan pengikatan data itu sendiri karena Moustache memproses HTML sebagai string sehingga setelah saya mendapatkan hasilnya saya tidak memiliki referensi ke tempat objek di viewmodel saya. Satu-satunya solusi yang dapat saya pikirkan adalah memodifikasi string HTML (atau membuat pohon DOM) dengan atribut. Saya tidak keberatan menggunakan mesin templating yang berbeda.

Pada dasarnya, saya punya perasaan kuat bahwa saya memperumit masalah yang ada dan ada solusi sederhana.

Catatan: Tolong jangan memberikan jawaban yang menggunakan perpustakaan eksternal, terutama yang ribuan baris kode. Saya telah menggunakan (dan suka!) AngularJS dan KnockoutJS. Saya benar-benar tidak ingin jawaban dalam bentuk 'use framework x'. Secara optimal, saya ingin pembaca masa depan yang tidak tahu bagaimana menggunakan banyak kerangka kerja untuk memahami bagaimana menerapkan pengikatan data dua arah sendiri. Saya tidak mengharapkan jawaban yang lengkap , tetapi jawaban yang menyampaikan gagasan.

Benjamin Gruenbaum
sumber
2
Saya mendasarkan CrazyGlue pada desain Benjamin Gruenbaum. Ini juga mendukung SELECT, kotak centang dan tag radio. jQuery adalah ketergantungan.
JohnSz
12
Pertanyaan ini sangat luar biasa. Jika itu ditutup karena di luar topik atau omong kosong konyol lainnya, saya akan secara serius diperingatkan.
OCDev
@ JohnSz terima kasih telah menyebutkan proyek CrazyGlue Anda. Saya telah mencari pengikat data 2 arah yang sederhana untuk waktu yang lama. Sepertinya Anda tidak menggunakan Object.observe sehingga dukungan browser Anda harus hebat. Dan Anda tidak menggunakan kumis templating sehingga sempurna.
Gavin
@Benjamin Apa yang akhirnya Anda lakukan?
johnny
@ Johnny menurut pendapat saya pendekatan yang benar adalah membuat DOM di JS (seperti Bereaksi) dan bukan sebaliknya. Saya pikir pada akhirnya itulah yang akan kami lakukan.
Benjamin Gruenbaum

Jawaban:

106
  • Bagaimana cara mengikat bekerja untuk objek?
  • Bagaimana mendengarkan perubahan dalam bentuk mungkin bekerja?

Abstraksi yang memperbarui kedua objek

Saya kira ada teknik lain, tetapi pada akhirnya saya akan memiliki objek yang memegang referensi ke elemen DOM terkait, dan menyediakan antarmuka yang mengoordinasikan pembaruan untuk data sendiri dan elemen terkait.

Ini .addEventListener()menyediakan antarmuka yang sangat bagus untuk ini. Anda bisa memberinya objek yang mengimplementasikan eventListenerantarmuka, dan itu akan memanggil penangannya dengan objek tersebut sebagai thisnilainya.

Ini memberi Anda akses otomatis ke elemen dan data terkait.

Menentukan objek Anda

Warisan prototipe adalah cara yang bagus untuk menerapkan ini, meskipun tentu saja tidak diperlukan. Pertama, Anda akan membuat konstruktor yang menerima elemen Anda dan beberapa data awal.

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

Jadi di sini konstruktor menyimpan elemen dan data pada properti objek baru. Itu juga mengikat suatu changeperistiwa dengan yang diberikan element. Yang menarik adalah ia melewatkan objek baru alih-alih fungsi sebagai argumen kedua. Tetapi ini saja tidak akan berhasil.

Menerapkan eventListenerantarmuka

Untuk membuat ini berfungsi, objek Anda perlu menerapkan eventListener antarmuka. Semua yang diperlukan untuk mencapai ini adalah memberi objek handleEvent()metode.

Di situlah warisan datang.

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

Ada banyak cara di mana hal ini dapat disusun, tetapi untuk contoh Anda mengoordinasikan pembaruan, saya memutuskan untuk membuat change()metode hanya menerima nilai, dan memilikihandleEvent lulus itu bukan objek acara. Dengan cara ini change()dapat dipanggil tanpa acara juga.

Jadi sekarang, ketika changeperistiwa itu terjadi, itu akan memperbarui elemen dan.data properti. Dan hal yang sama akan terjadi ketika Anda memanggil .change()program JavaScript Anda.

Menggunakan kodenya

Sekarang Anda baru saja membuat objek baru, dan biarkan ia melakukan pembaruan. Pembaruan dalam kode JS akan muncul pada input, dan perubahan acara pada input akan terlihat oleh kode 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);

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

pengguna1106925
sumber
5
+1 Pendekatan yang sangat bersih, sangat sederhana dan cukup sederhana untuk dipelajari orang, jauh lebih bersih daripada yang saya miliki. Kasus penggunaan umum adalah menggunakan template dalam kode untuk mewakili pandangan objek. Saya bertanya-tanya bagaimana ini bisa bekerja di sini? Di mesin seperti Kumis saya melakukan sesuatu Mustache.render(template,object), dengan asumsi saya ingin menyimpan objek yang disinkronkan dengan template (tidak khusus untuk Kumis), bagaimana saya akan melanjutkan tentang itu?
Benjamin Gruenbaum
3
@BenjaminGruenbaum: Saya belum pernah menggunakan templat sisi klien, tapi saya akan membayangkan bahwa Moustache memiliki beberapa sintaks untuk mengidentifikasi titik penyisipan, dan bahwa sintaksis itu termasuk label. Jadi saya akan berpikir bahwa bagian "statis" dari template akan dirender menjadi potongan HTML yang disimpan dalam Array, dan bagian dinamis akan pergi di antara potongan-potongan itu. Kemudian label pada titik penyisipan akan digunakan sebagai properti objek. Kemudian jika ada inputyang memperbarui salah satu dari titik-titik itu, akan ada pemetaan dari input ke titik itu. Saya akan melihat apakah saya bisa memberikan contoh cepat.
1
@BenjaminGruenbaum: Hmmm ... Saya belum memikirkan bagaimana cara mengoordinasikan dua elemen yang berbeda. Ini sedikit lebih terlibat daripada yang saya pikirkan pada awalnya. Saya penasaran, jadi saya mungkin perlu mengerjakan ini sedikit nanti. :)
2
Anda akan melihat bahwa ada Templatekonstruktor utama yang melakukan parsing, memegang MyCtorobjek yang berbeda , dan menyediakan antarmuka untuk memperbarui masing-masing oleh pengenalnya. Beri tahu saya jika Anda memiliki pertanyaan. :) EDIT: ... gunakan tautan ini sebagai gantinya ... Saya lupa bahwa saya memiliki peningkatan nilai input yang eksponensial setiap 10 detik untuk menunjukkan pembaruan JS. Ini membatasi itu.
2
... versi yang sepenuhnya dikomentari plus perbaikan kecil.
36

Jadi, saya memutuskan untuk melemparkan solusi saya sendiri ke dalam panci. Ini biola yang berfungsi . Perhatikan ini hanya berjalan di browser yang sangat modern.

Apa yang digunakannya

Implementasi ini sangat modern - ia membutuhkan (sangat) browser modern dan pengguna dua teknologi baru:

  • MutationObservers untuk mendeteksi perubahan dalam dom (pendengar acara juga digunakan)
  • Object.observeuntuk mendeteksi perubahan pada objek dan memberitahukan dom. Bahaya, karena jawaban ini telah dituliskan Oo telah dibahas dan diputuskan oleh ECMAScript TC, pertimbangkan polyfill .

Bagaimana itu bekerja

  • Pada elemen, letakkan domAttribute:objAttributepemetaan - misalnyabind='textContent:name'
  • Baca itu di fungsi dataBind. Amati perubahan pada elemen dan objek.
  • Ketika perubahan terjadi - perbarui elemen yang relevan.

Solusinya

Ini dataBindfungsinya, perhatikan hanya 20 baris kode dan bisa lebih pendek:

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

Berikut ini beberapa penggunaannya:

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

Ini biola yang berfungsi . Perhatikan bahwa solusi ini cukup umum. Object.observe dan pengamat mutasi tersedia.

Benjamin Gruenbaum
sumber
1
Saya kebetulan menulis ini (es5) untuk bersenang-senang, kalau ada yang merasa berguna - jatuhkan
Benjamin Gruenbaum
1
Perlu diingat bahwa ketika obj.namememiliki setter, ia tidak dapat diamati secara eksternal, tetapi harus disiarkan bahwa itu telah berubah dari dalam setter - html5rocks.com/en/tutorials/es7/observe/#toc-notifications - agak melempar kunci pas dalam karya untuk Oo () jika Anda ingin perilaku yang lebih kompleks dan saling bergantung menggunakan setter. Selain itu, ketika obj.nametidak dapat dikonfigurasi, mendefinisikan ulang penyetel itu (dengan berbagai trik untuk menambahkan pemberitahuan) juga tidak diperbolehkan - jadi obat generik dengan Oo () sepenuhnya dihapus dalam kasus tertentu.
Nolo
8
Object.observe dihapus dari semua browser: caniuse.com/#feat=object-observe
JvdBerg
1
Proxy dapat digunakan sebagai ganti Object.observe, atau github.com/anywhichway/proxy-observe atau gist.github.com/ebidel/1b553d571f924da2da06 atau polyfill yang lebih lama, juga di github @JvdBerg
jimmont
29

Saya ingin menambahkan ke preposter saya. Saya menyarankan pendekatan yang sedikit berbeda yang akan memungkinkan Anda untuk hanya menetapkan nilai baru ke objek Anda tanpa menggunakan metode. Namun harus dicatat bahwa ini tidak didukung oleh browser yang lebih tua dan IE9 masih memerlukan penggunaan antarmuka yang berbeda.

Yang paling penting adalah bahwa pendekatan saya tidak memanfaatkan peristiwa.

Setter dan Setter

Proposal saya memanfaatkan fitur getter dan setter yang relatif muda , terutama setter saja. Secara umum, mutator memungkinkan kita untuk "menyesuaikan" perilaku tentang bagaimana properti tertentu diberi nilai dan diambil.

Salah satu implementasi yang akan saya gunakan di sini adalah metode Object.defineProperty . Ia bekerja di FireFox, GoogleChrome dan - saya pikir - IE9. Belum menguji peramban lain, tetapi karena ini hanya teori ...

Bagaimanapun, ia menerima tiga parameter. Parameter pertama adalah objek yang Anda inginkan untuk mendefinisikan properti baru, yang kedua string yang menyerupai nama properti baru dan yang terakhir "objek deskriptor" yang menyediakan informasi tentang perilaku properti baru.

Dua deskriptor yang sangat menarik adalah getdan set. Contoh akan terlihat seperti berikut ini. Perhatikan bahwa menggunakan kedua ini melarang penggunaan 4 deskriptor lainnya.

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

Sekarang memanfaatkan ini menjadi sedikit berbeda:

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

Saya ingin menekankan bahwa ini hanya berfungsi untuk browser modern.

Biola yang bekerja: http://jsfiddle.net/Derija93/RkTMD/1/

Kiruse
sumber
2
Kalau saja kita punya Proxyobjek Harmony :) Setters memang tampak seperti ide yang bagus, tetapi bukankah itu mengharuskan kita untuk memodifikasi objek yang sebenarnya? Juga, di samping catatan - Object.createdapat digunakan di sini (sekali lagi, dengan asumsi browser modern yang diperbolehkan untuk parameter kedua). Juga, penyetel / pengambil dapat digunakan untuk 'memproyeksikan' nilai yang berbeda ke objek dan elemen DOM :). Saya ingin tahu apakah Anda memiliki wawasan tentang templating juga, yang tampaknya seperti tantangan nyata di sini, terutama untuk struktur dengan baik :)
Benjamin Gruenbaum
Sama seperti preposter saya, saya juga tidak banyak bekerja dengan mesin templating sisi klien, maaf. :( Tapi apa yang Anda maksud dengan memodifikasi benda yang sebenarnya ? Dan saya ingin memahami pikiran Anda tentang bagaimana Anda harus mengerti bahwa setter / getter bisa digunakan untuk ... . The getter / setter di sini digunakan untuk apa-apa tetapi mengarahkan semua input ke dan pengambilan dari objek ke elemen DOM, pada dasarnya seperti Proxy, seperti yang Anda katakan.;) Saya mengerti tantangannya adalah untuk menjaga dua sifat yang berbeda disinkronkan. Metode saya menghilangkan salah satu dari keduanya.
Kiruse
A Proxyakan menghilangkan kebutuhan untuk menggunakan getter / setter, Anda dapat mengikat elemen tanpa mengetahui properti apa yang mereka miliki. Yang saya maksudkan, adalah bahwa getter dapat mengubah lebih dari bindTo.value mereka dapat berisi logika (dan bahkan mungkin sebuah templat). Pertanyaannya adalah bagaimana cara mempertahankan pengikatan dua arah semacam ini dengan pola pikir? Katakanlah saya memetakan objek saya ke formulir, saya ingin mempertahankan elemen dan formulir yang disinkronkan dan saya bertanya-tanya bagaimana saya akan melanjutkan tentang hal semacam itu. Anda dapat memeriksa cara kerjanya pada knockout learn.knockoutjs.com/#/?tutorial=intro misalnya
Benjamin Gruenbaum
@BenjaminGruenbaum Gotcha. Saya akan melihatnya.
Kiruse
@BenjaminGruenbaum Saya melihat apa yang Anda coba pahami. Menyiapkan semua ini dengan template dalam pikiran ternyata sedikit lebih sulit. Saya akan mengerjakan skrip ini untuk sementara waktu (dan terus rebase). Tetapi untuk sekarang, saya istirahat. Sebenarnya saya tidak punya waktu untuk ini.
Kiruse
7

Saya pikir jawaban saya akan lebih teknis, tetapi tidak berbeda karena yang lain menyajikan hal yang sama menggunakan teknik yang berbeda.
Jadi, hal pertama yang pertama, solusi untuk masalah ini adalah penggunaan pola desain yang dikenal sebagai "pengamat", itu memungkinkan Anda memisahkan data Anda dari presentasi Anda, membuat perubahan dalam satu hal disiarkan ke pendengar mereka, tetapi dalam hal ini itu dibuat dua arah.

Untuk cara DOM ke JS

Untuk mengikat data dari DOM ke objek js Anda dapat menambahkan markup dalam bentuk dataatribut (atau kelas jika Anda memerlukan kompatibilitas), seperti ini:

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

Dengan cara ini dapat diakses melalui js menggunakan querySelectorAll(atau teman lamagetElementsByClassName untuk kompatibilitas).

Sekarang Anda dapat mengikat acara mendengarkan perubahan dalam cara: satu pendengar per objek atau satu pendengar besar ke wadah / dokumen. Mengikat dokumen / wadah akan memicu acara untuk setiap perubahan yang dibuat di dalamnya atau itu anak, itu akan memiliki jejak memori yang lebih kecil tetapi akan menelurkan acara.
Kode akan terlihat seperti ini:

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

Untuk JS lakukan cara DOM

Anda akan membutuhkan dua hal: satu meta-objek yang akan menampung referensi elemen DOM penyihir diikat ke setiap objek / atribut js dan cara untuk mendengarkan perubahan pada objek. Pada dasarnya cara yang sama: Anda harus memiliki cara untuk mendengarkan perubahan pada objek dan kemudian mengikatnya ke simpul DOM, karena metadata objek Anda "tidak bisa", Anda akan memerlukan objek lain yang menyimpan metadata dengan cara bahwa nama properti memetakan ke properti objek metadata. Kode akan seperti ini:

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

Saya harap saya bisa membantu.

madcampos
sumber
tidak ada masalah komparatif dengan menggunakan .observer?
Mohsen Shakiba
untuk saat ini diperlukan shim atau polyfill Object.observesebagai dukungan hanya tersedia di chrome untuk saat ini. caniuse.com/#feat=object-observe
madcampos
9
Object.observe sudah mati. Saya pikir saya akan mencatatnya di sini.
Benjamin Gruenbaum
@BenjaminGruenbaum Apa hal yang benar untuk digunakan sekarang, karena ini sudah mati?
johnny
1
@ Johnny jika saya tidak salah itu akan menjadi perangkap proxy karena mereka memungkinkan untuk kontrol yang lebih rinci dari apa yang bisa saya lakukan dengan objek, tetapi saya harus menyelidiki itu
madcampos
7

Kemarin, saya mulai menulis cara saya sendiri untuk mengikat data.

Sangat lucu bermain dengannya.

Saya pikir itu indah dan sangat berguna. Setidaknya pada pengujian saya menggunakan firefox dan chrome, Edge juga harus bekerja. Tidak yakin tentang orang lain, tetapi jika mereka mendukung Proxy, saya pikir itu akan berhasil.

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

Ini kodenya:

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

Kemudian, untuk mengatur, cukup:

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

Untuk saat ini, saya baru saja menambahkan ikatan nilai HTMLInputElement.

Beri tahu saya jika Anda tahu cara memperbaikinya.

ton
sumber
6

Ada implementasi barebone yang sangat sederhana dari pengikatan data 2 arah dalam tautan ini "Pengikatan Data Dua Arah Mudah dalam JavaScript"

Tautan sebelumnya bersama dengan ide-ide dari knockoutjs, backbone.js dan agility.js, menyebabkan kerangka kerja MVVM yang ringan dan cepat ini, ModelView.js berdasarkan jQuery yang bermain baik dengan jQuery dan saya penulis yang rendah hati (atau mungkin tidak begitu rendah hati).

Mereproduksi kode sampel di bawah ini (dari tautan posting blog ):

Kode sampel untuk 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;
}

Untuk apa yang menyangkut objek JavaScript, implementasi minimal model Pengguna untuk eksperimen ini adalah sebagai berikut:

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

Sekarang, setiap kali kita ingin mengikat properti model ke sepotong UI, kita hanya perlu menetapkan atribut data yang sesuai pada elemen HTML yang sesuai:

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

<!-- html -->
<input type="number" data-bind-123="name" />
Nikos M.
sumber
Meskipun tautan ini dapat menjawab pertanyaan, lebih baik untuk memasukkan bagian-bagian penting dari jawaban di sini dan memberikan tautan untuk referensi. Jawaban hanya tautan dapat menjadi tidak valid jika halaman tertaut berubah.
Sam Hanley
@ sphanley, perhatikan, saya mungkin akan memperbarui ketika saya memiliki lebih banyak waktu, karena itu kode yang agak panjang untuk jawaban jawaban
Nikos M.
@ sphanley, kode sampel yang direproduksi pada jawaban dari tautan yang direferensikan (walaupun saya pikir ini membuat sebagian besar konten duparte)
Nikos M.
1
Itu memang membuat duplikat konten, tapi itu intinya - tautan blog sering kali dapat memutuskan waktu, dan dengan menduplikasi konten yang relevan di sini memastikan bahwa itu akan tersedia dan bermanfaat bagi pembaca masa depan. Jawabannya tampak hebat sekarang!
Sam Hanley
3

Mengubah nilai elemen dapat memicu peristiwa DOM . Pendengar yang merespons peristiwa dapat digunakan untuk menerapkan pengikatan data dalam JavaScript.

Sebagai contoh:

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

Berikut ini adalah kode dan demo yang menunjukkan bagaimana elemen DOM dapat diikat satu sama lain atau dengan objek JavaScript.

pembuat lucu
sumber
3

Bind input html apa pun

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

tentukan dua fungsi:

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

gunakan fungsinya:

var myObject = proxify('element-to-bind')
bindValue(myObject);
Ollie Williams
sumber
3

Berikut adalah ide penggunaan Object.definePropertyyang secara langsung mengubah cara properti diakses.

Kode:

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

Pemakaian:

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

p.variable="yes"

biola: Di sini

Thornkey
sumber
2

Saya telah melalui beberapa contoh javascript dasar menggunakan onkeypress dan penangan event onchange untuk membuat tampilan mengikat ke js dan js kami untuk melihat

Berikut contoh 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>
macha devendher
sumber
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>
Anthony Newlineinfo
sumber
2

Cara sederhana untuk mengikat variabel ke input (pengikatan dua arah) adalah dengan langsung mengakses elemen input di pengambil dan penyetel:

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

Dalam HTML:

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

Dan untuk menggunakan:

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


Cara yang lebih bagus untuk melakukan hal di atas tanpa pengambil / penyetel:

var variable = function(element){

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

                    else return element.value;                                                  
                }

        }

Menggunakan:

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

Ini sangat sederhana mengikat data dua arah dalam javascript vanilla ....

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

<div id="name">

</div>

Subodh Gawade
sumber
2
tentunya ini hanya akan berfungsi dengan acara onkeyup? yaitu jika Anda melakukan permintaan ajax, dan kemudian mengubah innerHTML melalui JavaScript maka ini tidak akan berhasil
Zach Smith
1

Terlambat ke pesta, terutama karena saya sudah menulis 2 lib terkait bulan / tahun yang lalu, saya akan menyebutkannya nanti, tetapi masih terlihat relevan bagi saya. Untuk membuatnya spoiler sangat pendek, teknologi pilihan saya adalah:

  • Proxy untuk observasi model
  • MutationObserver untuk pelacakan perubahan DOM (untuk alasan yang mengikat, bukan perubahan nilai)
  • perubahan nilai (view to model flow) ditangani melalui addEventListenerpenangan biasa

IMHO, selain OP, penting bahwa implementasi pengikatan data akan:

  • menangani berbagai kasus siklus aplikasi (HTML pertama, lalu JS, JS terlebih dahulu kemudian HTML, atribut dinamis berubah dll)
  • memungkinkan pengikatan dalam model, sehingga seseorang dapat mengikat user.address.block
  • array sebagai model harus didukung dengan benar ( shift, splicedan serupa)
  • menangani ShadowDOM
  • berusaha semudah mungkin untuk penggantian teknologi, sehingga setiap sub-bahasa templating adalah pendekatan yang ramah-perubahan-masa depan karena terlalu banyak digabungkan dengan kerangka kerja

Mempertimbangkan semua itu, menurut pendapat saya membuatnya tidak mungkin untuk hanya membuang beberapa baris JS. Saya sudah mencoba melakukannya sebagai pola daripada lib - tidak berhasil untuk saya.

Selanjutnya, setelah Object.observedihapus, namun mengingat bahwa pengamatan model adalah bagian penting - seluruh bagian ini HARUS dipisah-pisahkan dengan lib lain. Sekarang ke pokok kepala sekolah tentang bagaimana saya mengambil masalah ini - persis seperti yang ditanyakan OP:

Model (bagian JS)

Pandangan saya untuk observasi model adalah Proxy , ini adalah satu-satunya cara yang waras untuk membuatnya bekerja, IMHO. Fitur lengkap observerlayaknya perpustakaan itu sendiri, jadi saya telah mengembangkan object-observerperpustakaan untuk tujuan tunggal itu.

Model / s harus didaftarkan melalui beberapa API khusus, itulah titik di mana POJO berubah menjadi Observables, tidak dapat melihat pintasan apa pun di sini. Elemen DOM yang dianggap sebagai pandangan terikat (lihat di bawah), diperbarui dengan nilai-nilai model pada awalnya dan kemudian pada setiap perubahan data.

Tampilan (bagian HTML)

IMHO, cara terbersih untuk menyatakan ikatan, adalah melalui atribut. Banyak yang melakukan ini sebelumnya dan banyak yang akan melakukannya setelah itu, jadi tidak ada berita di sini, ini hanya cara yang tepat untuk melakukannya. Dalam kasus saya, saya menggunakan sintaks berikut:, <span data-tie="modelKey:path.to.data => targerProperty"></span>tetapi ini kurang penting. Apa yang penting bagi saya, tidak ada sintaks scripting yang kompleks dalam HTML - ini adalah salah, sekali lagi, IMHO.

Semua elemen yang ditunjuk sebagai pandangan terikat harus dikumpulkan terlebih dahulu. Tampaknya bagi saya dari sisi kinerja tidak terelakkan untuk mengelola beberapa pemetaan internal antara model dan tampilan, tampaknya kasus yang tepat di mana memori + beberapa manajemen harus dikorbankan untuk menyimpan pencarian dan pembaruan runtime.

Pandangan diperbarui pada awalnya dari model, jika tersedia dan perubahan model kemudian, seperti yang kita katakan. Terlebih lagi, seluruh DOM harus diamati dengan cara MutationObserveruntuk bereaksi (mengikat / mengikat) pada elemen yang ditambahkan / dihapus / diubah secara dinamis. Selanjutnya, semua ini, harus direplikasi ke dalam ShadowDOM (satu terbuka, tentu saja) agar tidak meninggalkan lubang hitam yang tidak terikat.

Daftar spesifik memang bisa melangkah lebih jauh, tetapi menurut pendapat saya prinsip-prinsip utama yang akan membuat pengikatan data diimplementasikan dengan keseimbangan yang baik dari kelengkapan fitur dari satu dan kesederhanaan waras dari sisi lain.

Dan dengan demikian, selain yang object-observerdisebutkan di atas, saya telah menulis juga data-tierperpustakaan, yang mengimplementasikan pengikatan data di sepanjang konsep yang disebutkan di atas.

GullerYA
sumber
0

Banyak hal telah berubah banyak dalam 7 tahun terakhir, kami memiliki komponen web asli di sebagian besar browser sekarang. IMO inti masalahnya adalah berbagi status di antara elemen, begitu Anda memiliki hal yang sepele untuk memperbarui ui ketika keadaan berubah dan sebaliknya.

Untuk berbagi data antar elemen Anda bisa membuat kelas StateObserver, dan memperluas komponen web Anda dari itu. Implementasi minimal terlihat seperti ini:

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

biola di sini

Saya suka pendekatan ini karena:

  • tidak ada dom traversal untuk menemukan data-properti
  • tidak ada Object.observe (usang)
  • tidak ada Proxy (yang menyediakan pengait tetapi tidak ada mekanisme komunikasi)
  • tidak ada dependensi, (selain polyfill tergantung pada browser target Anda)
  • itu cukup terpusat & modular ... menggambarkan keadaan dalam html, dan memiliki pendengar di mana-mana akan menjadi sangat cepat berantakan.
  • itu bisa diperluas. Implementasi dasar ini adalah 20 baris kode, tetapi Anda dapat dengan mudah membangun beberapa kenyamanan, kekekalan, dan bentuk sihir untuk membuatnya lebih mudah untuk dikerjakan.
Mr5o1
sumber