Suppose you want to have binding in your web application, but just a very rudimentary one and not a complete framework like KnockoutJS, Ember.js or AngularJS.
So I was thinking: How can one just wrote the minimum amount of code to get a working bindable base system, which can be easily extended and used without requiring to insert additional dependencies? The answer is the following piece of code.
I started with a Bindable
object. This one stores information about a specified variable. It stores the callback functions when changing its value, an (optional) transformation and an (optional) validation function and a little bit of decoration.
Instances of the Bindable
object will be placed in a Bindcore
object. This is just a nice container, that should be somewhat global.
For the binding to work one should only use the setValue()
method of the Bindcore
instance or directly of the Bindable
object.
Let's see some sample code first:
//Just create a new bindcore
var core = new Bindcore();
//We need the values to be stored in an object - if we just use var text = ''; we would need to specify the current context, like window and 'text' or similar
var obj = { text: '' };
//Just for demonstration - get some div
var pg = $('#playground');
//Append some input and set the change event
var input1 = $('<input />').appendTo(pg).change(function() {
core.setValue(obj, 'text', this.value);
});
//Some label on the page
var span = $('<span />').appendTo(pg);
//Another label
var strong = $('<strong />').appendTo(pg.parent());
//Append another input and set the change event
var input2 = $('<input />').appendTo(pg).change(function() {
core.setValue(obj, 'text', this.value);
});
//Register the callbacks
core.register(obj, 'text', function(value) {
strong.text(value); //change the text
}).register(function(value) {
span.text(value); //change the text
}).register(function(value) {
input2.val(value); //change the input
}).register(function(value) {
input1.val(value); //change the input
}).setTransformer(function(value) {
return value * 1; //set a transformer - string to number
}).setValidator(function(value) {
return !isNaN(value); //validate the input - is valid number?
});
What can we see here?
- The system uses a kind of fluent syntax. Once we registered an object, we get back the
Bindable
instance. This instance has again someregister()
method, just the (already given) information (which object and which property) does not have to be specified. - The transformer (if given) is applied before the validator. This, however, does not require the transformer to be set before the validator.
- Transformers have one input (value) and one output (changed value).
- Validators have one input (value) and one output (boolean).
- By default the transformer is the identity and the validator always returns true.
Even though we could just create a new binding, we never specified it here. This means that the core will automatically add new bindings for given objects, if notifications are requested. This does not ensure that notifications will ever happen.
Now to the source of this code. First let's inspect the Bindable
definition:
var Bindable = function(obj, propName) {
var notifiers = [];
var _transformer;
var _validator;
this.setValidator = function(validator) {
_validator = validator || function(value) {
return true;
};
return this;
};
this.getValidator = function() {
return _validator;
};
this.setTransformer = function(transformer) {
_transformer = transformer || function(value) {
return value;
};
return this;
};
this.getTransformer = function() {
return _transformer;
};
this.binds = function(_obj, _propName) {
return obj === _obj && propName === _propName;
};
this.getValue = function() {
return obj[propName];
};
this.setValue = function(value) {
value = _transformer(value);
value = _validator(value) ? value : this.getValue();
obj[propName] = value;
for (var i = notifiers.length; i--; )
notifiers[i](value);
return this;
};
this.register = function(callback) {
notifiers.push(callback);
return this;
};
this.unregister = function(callback) {
for (var i = notifiers.length; i--; ) {
if(notifiers[i] === callback)
notifiers.splice(i, 1);
}
return this;
};
this.setTransformer();
this.setValidator();
};
All in all just a constructor and nothing more. The notifications, validator and transformer variables are private, the rest can be changed from the outside. Most of the properties are actually functions.
var Bindcore = function() {
var bindings = [];
this.addBinding = function(obj, propName, validator, transformer) {
var bindable = this.getBinding(obj, propName) || new Bindable(obj, propName);
bindable.setTransformer(transformer).setValidator(validator);
bindings.push(bindable);
return bindable;
};
this.removeBinding = function(obj, propName) {
for (var i = bindings.length; i--; ) {
if(bindings[i].binds(obj, propName)) {
var binding = bindings[i];
bindings.splice(i, 1);
}
}
return this;
};
this.getBinding = function(obj, propName) {
for (var i = bindings.length; i--; ) {
if(bindings[i].binds(obj, propName))
return bindings[i];
}
};
this.register = function(obj, propName, callback) {
var binding = this.getBinding(obj, propName) || this.addBinding(obj, propName);
binding.register(callback);
return binding;
};
this.unregister = function(obj, propName, callback) {
var binding = this.getBinding(obj, propName) || this.addBinding(obj, propName);
binding.unregister(callback);
return binding;
};
this.setValue = function(obj, propName, value) {
var binding = this.getBinding(obj, propName) || this.addBinding(obj, propName);
binding.setValue(value);
return binding;
};
this.getValue = function(obj, propName) {
var binding = this.getBinding(obj, propName) || this.addBinding(obj, propName);
return binding.getValue();
};
};
In the Bindcore
everything comes together. We see that most of the time an existing binding is used, otherwise a new binding is created. This is actually quite beautiful, because we only need to know the name of the property and the object or context it is stored in and we can actually set its value.