Is it possible to listen for changes to an object’s attributes in JavaScript?
I’m working on a fiddly web interface which is mostly built with JavaScript. Its basically one (very) large form with many sections. Each section is built based on options from other parts of the form. Whenever those options change the new values are noted in a “registry” type object and the other sections re-populate accordingly.
Having event listeners on the many form fields is starting to slow things down, and refreshing the whole form for each change would be too heavy/slow for the user.
I’m wondering whether its possible to add listeners to the registry object’s attributes rather than the form elements to speed things up a bit? And, if so, could you provide/point me to some sample code?
Further information:
- This is a plug-in for jQuery, so any functionality I can build-on from that library would be helpful but not essential.
- Our users are using IE6/7, Safari and FF2/3, so if it is possible but only for “modern” browsers I’ll have to find a different solution.
As far as I know, there are no events fired on Object attribute changes (edit: except, apparently, for Object.watch
).
Why not use event delegation wherever possible? That is, events on the form rather than on individual form elements, capturing events as they bubble up?
For instance (my jQuery is rusty, forgive me for using Prototype instead, but I’m sure you’ll be able to adapt it easily):
$(form).observe('change', function(e) {
// To identify changed field, in Proto use e.element()
// but I think in jQuery it's e.target (as it should be)
});
You can also capture input
and keyup
and paste
events if you want it to fire on text fields before they lose focus. My solution for this is usually:
- Gecko/Webkit-based browsers: observe
input
on theform
. - Also in Webkit-based browsers: observe
keyup
andpaste
events ontextarea
s (they do not fireinput
ontextarea
s for some reason). - IE: observe
keyup
andpaste
on theform
- Observe
change
on theform
(this fires onselect
s). - For
keyup
andpaste
events, compare a field’s current value against its default (what its value was when the page was loaded) by comparing a text field’svalue
to itsdefaultValue
Edit: Here’s example code I developed for preventing unmodified form submission and the like:
What is the best way to track changes in a form via javascript?
Thanks for the comments guys. I’ve gone with the following:
var EntriesRegistry = (function(){
var instance = null;
function __constructor() {
var
self = this,
observations = {};
this.set = function(n,v)
{
self[n] = v;
if( observations[n] )
for( var i=0; i < observations[n].length; i++ )
observations[n][i].apply(null, [v, n]);
}
this.get = function(n)
{
return self[n];
}
this.observe = function(n,f)
{
if(observations[n] == undefined)
observations[n] = [];
observations[n].push(f);
}
}
return new function(){
this.getInstance = function(){
if (instance == null)
{
instance = new __constructor();
instance.constructor = null;
}
return instance;
}
}
})();
var entries = EntriesRegistry.getInstance();
var test = function(v){ alert(v); };
entries.set('bob', 'meh');
entries.get('bob');
entries.observe('seth', test);
entries.set('seth', 'dave');
Taking on-board your comments, I’ll be using event delegation on the form objects to update the registry and trigger the registered observing methods.
This is working well for me so far… can you guys see any problems with this?
You could attach a listener to a container (the body or the form) and then use the event parameter to react to the change. You get all the listener goodness but only have to attach one for the container instead of one for every element.
$('body').change(function(event){
/* do whatever you want with event.target here */
console.debug(event.target); /* assuming firebug */
});
The event.target holds the element that was clicked on.
SitePoint has a nice explanation here of event delegation:
JavaScript event delegation is a simple technique by which you add a single event handler to a parent element in order to avoid having to add event handlers to multiple child elements.
Mozilla-engined browsers support Object.watch, but I’m not aware of a cross-browser compatible equivalent.
Have you profiled the page with Firebug to get an idea of exactly what’s causing the slowness, or is “lots of event handlers” a guess?
Small modification to the previous answer : by moving the observable code to an object, one can make an abstraction out of it and use it to extend other objects with jQuery’s extend method.
ObservableProperties = {
events : {},
on : function(type, f)
{
if(!this.events[type]) this.events[type] = [];
this.events[type].push({
action: f,
type: type,
target: this
});
},
trigger : function(type)
{
if (this.events[type]!==undefined)
{
for(var e = 0, imax = this.events[type].length ; e < imax ; ++e)
{
this.events[type][e].action(this.events[type][e]);
}
}
},
removeEventListener : function(type, f)
{
if(this.events[type])
{
for(var e = 0, imax = this.events[type].length ; e < imax ; ++e)
{
if(this.events[type][e].action == f)
this.events[type].splice(e, 1);
}
}
}
};
Object.freeze(ObservableProperties);
var SomeBusinessObject = function (){
self = $.extend(true,{},ObservableProperties);
self.someAttr = 1000
self.someMethod = function(){
// some code
}
return self;
}
See the fiddle : https://jsfiddle.net/v2mcwpw7/3/
jQuery is just amazing. Although you could take a look to ASP.NET AJAX Preview.
Some features are just .js files, no dependency with .NET. May be you could find usefull the observer pattern implementation.
var o = { foo: "Change this string" };
Sys.Observer.observe(o);
o.add_propertyChanged(function(sender, args) {
var name = args.get_propertyName();
alert("Property '" + name + "' was changed to '" + sender[name] + "'.");
});
o.setValue("foo", "New string value.");
Also, Client Side templates are ready to use for some interesting scenarios.
A final note, this is fully compatible with jQuery (not problem with $)
Links: Home page, Version I currently use
I was searching for the same thing and hitted your question… none of the answers satisfied my needs so I came up with this solution that I would like to share:
var ObservedObject = function(){
this.customAttribute = 0
this.events = {}
// your code...
}
ObservedObject.prototype.changeAttribute = function(v){
this.customAttribute = v
// your code...
this.dispatchEvent('myEvent')
}
ObservedObject.prototype.addEventListener = function(type, f){
if(!this.events[type]) this.events[type] = []
this.events[type].push({
action: f,
type: type,
target: this
})
}
ObservedObject.prototype.dispatchEvent = function(type){
for(var e = 0; e < this.events[type].length; ++e){
this.events[type][e].action(this.events[type][e])
}
}
ObservedObject.prototype.removeEventListener = function(type, f){
if(this.events[type]) {
for(var e = 0; e < this.events[type].length; ++e){
if(this.events[type][e].action == f)
this.events[type].splice(e, 1)
}
}
}
var myObj = new ObservedObject()
myObj.addEventListener('myEvent', function(e){// your code...})
It’s a simplification of the DOM Events API and works just fine!
Here is a more complete example