How to Implement DOM Data Binding in JavaScript
Please treat this question as strictly educational. I'm still interested in hearing new answers and ideas to implement this
tl;dr
How would I implement bi-directional data-binding with JavaScript?
Data Binding to the DOM
By data binding to the DOM I mean for example, having a JavaScript object a
with a property b
. Then having an <input>
DOM element (for example), when the DOM element changes, a
changes and vice versa (that is, I mean bidirectional data binding).
Here is a diagram from AngularJS on what this looks like:
So basically I have JavaScript similar to:
var a = {b:3};
Then an input (or other form) element like:
<input type='text' value=''>
I'd like the input's value to be ab
's value (for example), and when the input text changes, I'd like ab
to change too. When ab
changes in JavaScript, the input changes.
The Question
What are some basic techniques to accomplish this in plain JavaScript?
In specific, I'd like a good answer to refer to:
What have I tried?
I'm a big fan of Mustache so I tried using it for templating. However, I ran into issues when trying to perform the data binding itself since Mustache processes HTML as a string so after I get its result I have no reference to where the objects in my viewmodel are. The only workaround I could think for this was modifying the HTML string (or created DOM tree) itself with attributes. I don't mind using a different templating engine.
Basically, I got a strong feeling that I was complicating the issue at hand and there is a simple solution.
Note: Please do not provide answers that use external libraries, especially ones that are thousands of lines of code. I've used (and like!) AngularJS and KnockoutJS. I really don't want answers in the form 'use framework x'. Optimally, I'd like a future reader who doesn't know how to use many frameworks to grasp how to implement bi-directional data-binding herself. I do not expect a complete answer, but one that gets the idea across.
An abstraction that updates both objects
I suppose there are other techniques, but ultimately I'd have an object that holds reference to a related DOM element, and provides an interface that coordinates updates to its own data and its related element.
The .addEventListener()
provides a very nice interface for this. You can give it an object that implements the eventListener
interface, and it'll invoke its handlers with that object as the this
value.
This gives you automatic access to both the element and its related data.
Defining your object
Prototypal inheritance is a nice way to implement this, though not required of course. First you'd create a constructor that receives your element and some initial data.
function MyCtor(element, data) {
this.data = data;
this.element = element;
element.value = data;
element.addEventListener("change", this, false);
}
So here the constructor stores the element and data on properties of the new object. It also binds a change
event to the given element
. The interesting thing is that it passes the new object instead of a function as the second argument. But this alone won't work.
Implementing the eventListener
interface
To make this work, your object needs to implement the eventListener
interface. All that's needed to accomplish this is to give the object a handleEvent()
method.
That's where the inheritance comes in.
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;
};
There are many different ways in which this could be structured, but for your example of coordinating updates, I decided to make the change()
method only accept a value, and have the handleEvent
pass that value instead of the event object. This way the change()
can be invoked without an event as well.
So now, when the change
event happens, it'll update both the element and the .data
property. And the same will happen when you call .change()
in your JavaScript program.
Using the code
Now you'd just create the new object, and let it perform updates. Updates in JS code will appear on the input, and change events on the input will be visible to the JS code.
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/
So, I decided to throw my own solution in the pot. Here is a working fiddle . Note this only runs on very modern browsers.
What it uses
This implementation is very modern - it requires a (very) modern browser and users two new technologies:
MutationObserver
s to detect changes in the dom (event listeners are used as well) Object.observe
to detect changes in the object and notifying the dom. Danger, since this answer has been written Oo has been discussed and decided against by the ECMAScript TC, consider a polyfill. How it works
domAttribute:objAttribute
mapping - for example bind='textContent:name'
The solution
Here is the dataBind
function, note it's just 20 lines of code and could be shorter:
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];
}
Here is some usage:
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);
Here is a working fiddle . Note that this solution is pretty generic. Object.observe and mutation observer shimming is available.
I'd like to add to my preposter. I suggest a slightly different approach that will allow you to simply assign a new value to your object without using a method. It must be noted though that this is not supported by especially older browsers and IE9 still requires use of a different interface.
Most notably is that my approach does not make use of events.
Getters and Setters
My proposal makes use of the relatively young feature of getters and setters, particularly setters only. Generally speaking, mutators allow us to "customize" the behavior of how certain properties are assigned a value and retrieved.
One implementation I'll be using here is the Object.defineProperty method. It works in FireFox, GoogleChrome and - I think - IE9. Haven't tested other browsers, but since this is theory only...
Anyways, it accepts three parameters. The first parameter being the object that you wish to define a new property for, the second a string resembling the the name of the new property and the last a "descriptor object" providing information on the behavior of the new property.
Two particularly interesting descriptors are get
and set
. An example would look something like the following. Note that using these two prohibits the use of the other 4 descriptors.
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;
}
});
}
Now making use of this becomes slightly different:
var obj = new MyCtor(document.getElementById('foo')),
i = 0;
setInterval(function() {
obj.value += ++i;
}, 3000);
I want to emphasize that this only works for modern browsers.
Working fiddle: http://jsfiddle.net/Derija93/RkTMD/1/
链接地址: http://www.djcxy.com/p/35358.html上一篇: 我如何从C#中的泛型方法返回NULL?