如何在JavaScript中实现DOM数据绑定
请把这个问题视为严格的教育。 我仍然有兴趣听到新的答案和想法来实现这一点
TL;博士
我将如何实现与JavaScript的双向数据绑定?
数据绑定到DOM
通过数据绑定到DOM我的意思是,例如,一个JavaScript对象与属性b
。 然后有一个<input>
DOM元素(例如),当DOM元素改变时,改变反之亦然(即我的意思是双向数据绑定)。
下面是AngularJS的一个图表:
所以基本上我有JavaScript类似于:
var a = {b:3};
然后input(或其他forms)的元素,如:
<input type='text' value=''>
我希望input的值是ab
的值(例如),当input文本改变时,我也想改变ab
。 当ab
在JavaScript中改变时,input改变。
问题
什么是一些基本的技术来完成这个普通的JavaScript?
具体来说,我想要一个很好的答案来提到:
- 绑定如何为对象工作?
- 如何听取forms的变化可能会奏效?
- 是否可以简单的方式只在模板级别修改HTML? 我不想跟踪HTML文档本身的绑定,只能在JavaScript中进行绑定(使用DOM事件,JavaScript保留对所使用的DOM元素的引用)。
我试过了什么?
我是胡子的粉丝,所以我试着用它做模板。 然而,当我尝试执行数据绑定本身时遇到了问题,因为Mustache将HTML处理为string,所以在得到结果之后,我没有参考viewmodel中对象的位置。 我唯一能想到的解决方法是使用属性修改HTMLstring(或创build的DOM树)本身。 我不介意使用不同的模板引擎。
基本上,我有一个强烈的感觉,我正在把问题复杂化,并有一个简单的解决scheme。
注意:请不要提供使用外部库的答案,尤其是那些有数千行代码的答案。 我已经使用(和喜欢!)AngularJS和KnockoutJS。 我真的不希望在'使用框架x'的forms的答案。 最好是,我想要一个未知的读者不知道如何使用许多框架来掌握如何实现双向数据绑定。 我不期待一个完整的答案,而是一个能够把这个想法传递出去的答案。
- 绑定如何为对象工作?
- 如何听取forms的变化可能会奏效?
更新这两个对象的抽象
我想还有其他技术,但最终我会有一个对象来保存对相关的DOM元素的引用,并提供一个接口来协调更新自己的数据及其相关的元素。
.addEventListener()
为此提供了一个非常好的界面。 你可以给它一个实现eventListener
接口的对象,并且它将调用它的处理器作为this
值。
这使您可以自动访问元素及其相关数据。
定义你的对象
原型inheritance是实现这一点的好方法,当然不是必须的。 首先,您将创build一个构造函数来接收元素和一些初始数据。
function MyCtor(element, data) { this.data = data; this.element = element; element.value = data; element.addEventListener("change", this, false); }
所以这里的构造函数将元素和数据存储在新对象的属性中。 它还将一个change
事件绑定到给定的element
。 有趣的是,它传递的是新对象而不是函数作为第二个参数。 但是,这一点是行不通的。
实现eventListener
接口
为了使这个工作,你的对象需要实现eventListener
接口。 所有这一切都需要给对象一个handleEvent()
方法。
那就是inheritance进来的地方。
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
属性。 在JavaScript程序中调用.change()
时也会发生同样的情况。
使用代码
现在,您只需创build新的对象,并让它执行更新。 JS代码中的更新将出现在input中,并且input中的更改事件对于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/
所以,我决定把我自己的解决scheme放在锅里。 这是一个工作小提琴 。 请注意,这只能在非常新的浏览器上运行。
它使用什么
这个实现是非常现代的 – 它需要一个(非常)现代的浏览器和用户两种新技术:
-
MutationObserver
检测dom中的变化(事件监听器也被使用) -
Object.observe
检测对象的变化并通知dom。 危险,因为这个答案已经被写出了Oo已经被ECMAScript TC讨论和决定了,所以考虑一个polyfill 。
怎么运行的
- 在元素上,放一个
domAttribute:objAttribute
映射 – 例如bind='textContent:name'
- 在dataBind函数中读取它。 观察对元素和对象的更改。
- 发生更改时 – 更新相关元素。
解决scheme
这里是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);
这是一个工作小提琴 。 请注意,这个解决scheme非常通用。 Object.observe和突变观察者匀场是可用的。
我想添加到我的preposter。 我build议一个稍微不同的方法,可以让你简单地分配一个新的值给你的对象而不使用方法。 但必须指出的是,这并不是特别老的浏览器所支持的,IE9仍然需要使用不同的接口。
最值得注意的是我的方法不利用事件。
吸气剂和固化剂
我的build议是利用getters和setter的相对年轻的function,特别是setter只。 一般来说,mutators允许我们“定制”某些属性如何赋值并检索的行为。
我将在这里使用的一个实现是Object.defineProperty方法。 它适用于FireFox,GoogleChrome和 – 我认为 – IE9。 没有testing过其他浏览器,但由于这只是理论…
无论如何,它接受三个参数。 第一个参数是要为其定义新属性的对象,第二个参数是类似于新属性名称的string,最后一个是提供有关新属性行为信息的“描述符对象”。
两个特别有趣的描述符是get
和set
。 一个例子如下所示。 请注意,使用这两个禁止使用其他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/
我认为我的答案会更技术化,但是与其他人使用不同的技术呈现相同的东西并没有什么不同。
所以,首先,这个问题的解决scheme是使用一种称为“观察者”的devise模式,它可以让你将你的数据从你的演示中分离出来,把一件事情的变化传播给他们的听众,但是在这种情况下它是双向的。
对于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
兼容性)来访问它。
现在,您可以将侦听到更改的事件绑定到以下方法:每个对象一个侦听器或者一个容器/文档的大侦听器。 绑定到文档/容器将触发事件对其进行的每一个变化或它的孩子,它将有一个较小的内存足迹,但会派生事件调用。
代码将如下所示:
//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方式
您将需要两件事:一个元对象将保存女巫DOM元素的引用绑定到每个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);
我希望我有帮助。
在这个链接“简单的双向数据绑定在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" />
昨天我开始写我自己的方式来绑定数据。
玩它非常有趣。
我觉得这很漂亮,非常有用。 至less在我使用Firefox和Chrome的testing中,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值绑定。
让我知道如果你知道如何改善它。
绑定任何htmlinput
<input id="element-to-bind" type="text">
定义两个function:
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); }
使用function:
var myObject = proxify('element-to-bind') bindValue(myObject);
更改元素的值可以触发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对象绑定。
我已经通过一些基本的JavaScript示例使用onkeypress和onchange事件处理程序进行绑定视图我们的js和js查看
这里的示例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>
<!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>