 <p>You could bind to an object wrapper on an elements properties, or bind the elements en masse to your own <code>do-something</code> object. I found it easiest to have a pair of maps, registrants as keys object or user etc, and property name, then the maps keep the current values and (when i get to it) a generator or iterator will update the properties on the appropriate registrants. Below is an example without extending the nodeList prototype with array functions, I did the extension because looping can be well, you will see the code.</p> <p>Here is some code I have in a codepen, on a project I started September 15. I am trying to get an interface for building and recording reading test passages and questions to maintain state, be able to recreate state, and offer some helpful clues about learning....</p> <p>Code is redundant throughout, as I had never done the work before without the Observers Addi mentions above. In Polymer components you dont have to do your own binding. You could try that new spec v1 polymer two approach too. It has essentially a clean Object observe, and a lot of other cool features I just had a spec issue with some changes for contractual requirements. </p> <p>`<br> var archive=[]; this.archive=[];//TODO ME: NOTE worked, recorded reported, returned re-impl state, but needs some 'slick' to stay updatable and fast... so double check then do again at least a hand full of times with generator/proxy/map... .Try on and off thread</p> <pre><code> var inputNodes=document.querySelectorAll("input");//NOTE TODO ME: replace with symbol iterator of symbols for dom nodes HTML REPLACED IN DEMO 6; var counterOne=inputNodes.length; this.inputArray=[];//check this.propertyArray=[];//check add? for(var i=0; i&lt;counterOne;i++) { var newProp=inputNodes[i].attributes.id.value; var elem=document.getElementById(newProp); this.propertyArray.push(newProp); this.inputArray.push(elem); } var selectNodes=document.querySelectorAll("select"); var counterT=selectNodes.length; for(var zz=0; zz&lt;counterT;zz++){ var newProp3=selectNodes[zz].attributes.id.value; var za=document.getElementById(newProp3); this.propertyArray.push(newProp3); this.inputArray.push(za); } var taNodes=document.querySelectorAll("textarea"); var counterTwo=taNodes.length; for(var i=0; i&lt;counterTwo;i++){ var newProp2=taNodes[i].attributes.id.value; var ta=document.getElementById(newProp2); this.propertyArray.push(newProp2); this.inputArray.push(ta); } NOTE: TODO-AGAIN: ME--reinstitute the extensions to nodeListProto or at least make a helper func, this looks gross this.propertyArray.forEach((prop, index)=&gt;{ var elem=this.inputArray[index]; if(elem.nodeName==="TEXTAREA"){ elem.value=elem.innerHTML; } if(elem.nodeName==="SELECT"){ console.log(elem.selectedIndex, "LOOK HERE DUMMY"); elem.value=elem.children[elem.selectedIndex].value; } var val=elem.value; Object.defineProperty(this, prop,{ enumerable: true, configurable: true, get: function() { if(elem.type=="checkbox" || elem.type=="radio"){ this.archive.push({name:prop, value: elem.checked}); return elem.checked; } else if(elem.nodeName==="SELECT"){ var retVal=(elem.selectedIndex==undefined)?elem.children[1].value:elem.children[elem.selectedIndex].value; return retVal; } else{ if(elem.nodeName==="TEXTAREA"){ var val=elem.value; } this.archive.push({name:prop, value: val}); return val; } }, set: function() { if(elem.type=="checkbox" || elem.type=="radio"){prop=elem.checked; } else{ prop = val; } } }); if(elem.nodeName=="SELECT"){ var nom=elem.attributes.id.value; elem.addEventListener("blur",()=&gt;{ this[`${nom}Func`]; alert("the thing changed");},false);} if(elem.type!="button"){ elem.addEventListener("change", ()=&gt;{ var keys=Object.keys(this); console.log(keys); keys.forEach((key)=&gt;{ console.log(key, this[key]); }); console.log(this[prop], elem.value,elem.checked, prop, "newprop values here", this.archive) }, false); } }, this); this.codeMirror={};//check ` </code></pre>