e0ecf8db by Michael Richards

Merge pull request #90 from mdekmetzian/master

Support for two-way formatters [#74]
2 parents 11c6d643 24689897
......@@ -130,7 +130,7 @@ describe('Rivets.Binding', function() {
});
it('applies any formatters to the value before performing the routine', function() {
rivets.formatters.awesome = function(value) { return 'awesome ' + value };
rivets.formatters.awesome = function(value) { return 'awesome ' + value; };
binding.formatters.push('awesome');
spyOn(binding.binder, 'routine');
binding.set('sweater');
......@@ -163,15 +163,149 @@ describe('Rivets.Binding', function() {
});
});
describe('publishTwoWay()', function() {
it('applies a two-way read formatter to function same as a single-way', function() {
rivets.formatters.awesome = {
read: function(value) { return 'awesome ' + value; },
};
binding.formatters.push('awesome');
spyOn(binding.binder, 'routine');
binding.set('sweater');
expect(binding.binder.routine).toHaveBeenCalledWith(el, 'awesome sweater');
});
it("should publish the value of a number input", function() {
rivets.formatters.awesome = {
publish: function(value) { return 'awesome ' + value; },
};
numberInput = document.createElement('input');
numberInput.setAttribute('type', 'number');
numberInput.setAttribute('data-value', 'obj.num | awesome');
view = rivets.bind(numberInput, {obj: {num: 42}});
binding = view.bindings[0];
model = binding.model;
numberInput.value = 42;
spyOn(rivets.config.adapter, 'publish');
binding.publish({target: numberInput});
expect(rivets.config.adapter.publish).toHaveBeenCalledWith(model, 'num', 'awesome 42');
});
it("should format a value in both directions", function() {
rivets.formatters.awesome = {
publish: function(value) { return 'awesome ' + value; },
read: function(value) { return value + ' is awesome'; }
};
valueInput = document.createElement('input');
valueInput.setAttribute('type','text');
valueInput.setAttribute('data-value', 'obj.name | awesome');
view = rivets.bind(valueInput, {obj: { name: 'nothing' }});
binding = view.bindings[0];
model = binding.model;
spyOn(rivets.config.adapter, 'publish');
valueInput.value = 'charles';
binding.publish({target: valueInput});
expect(rivets.config.adapter.publish).toHaveBeenCalledWith(model, 'name', 'awesome charles');
spyOn(binding.binder, 'routine');
binding.set('fred');
expect(binding.binder.routine).toHaveBeenCalledWith(valueInput, 'fred is awesome');
});
it("should not fail or format if the specified binding function doesn't exist", function() {
rivets.formatters.awesome = { };
valueInput = document.createElement('input');
valueInput.setAttribute('type','text');
valueInput.setAttribute('data-value', 'obj.name | awesome');
view = rivets.bind(valueInput, {obj: { name: 'nothing' }});
binding = view.bindings[0];
model = binding.model;
spyOn(rivets.config.adapter, 'publish');
valueInput.value = 'charles';
binding.publish({target: valueInput});
expect(rivets.config.adapter.publish).toHaveBeenCalledWith(model, 'name', 'charles');
spyOn(binding.binder, 'routine');
binding.set('fred');
expect(binding.binder.routine).toHaveBeenCalledWith(valueInput, 'fred');
});
it("should apply read binders left to right, and write binders right to left", function() {
rivets.formatters.totally = {
publish: function(value) { return value + ' totally'; },
read: function(value) { return value + ' totally'; }
};
rivets.formatters.awesome = {
publish: function(value) { return value + ' is awesome'; },
read: function(value) { return value + ' is awesome'; }
};
valueInput = document.createElement('input');
valueInput.setAttribute('type','text');
valueInput.setAttribute('data-value', 'obj.name | awesome | totally');
view = rivets.bind(valueInput, {obj: { name: 'nothing' }});
binding = view.bindings[0];
model = binding.model;
spyOn(binding.binder, 'routine');
binding.set('fred');
expect(binding.binder.routine).toHaveBeenCalledWith(valueInput, 'fred is awesome totally');
spyOn(rivets.config.adapter, 'publish');
valueInput.value = 'fred';
binding.publish({target: valueInput});
expect(rivets.config.adapter.publish).toHaveBeenCalledWith(model, 'name', 'fred totally is awesome');
});
it("binders in a chain should be skipped if they're not there", function() {
rivets.formatters.totally = {
publish: function(value) { return value + ' totally'; },
read: function(value) { return value + ' totally'; }
};
rivets.formatters.radical = {
publish: function(value) { return value + ' is radical'; },
};
rivets.formatters.awesome = function(value) { return value + ' is awesome'; };
valueInput = document.createElement('input');
valueInput.setAttribute('type','text');
valueInput.setAttribute('data-value', 'obj.name | awesome | radical | totally');
view = rivets.bind(valueInput, {obj: { name: 'nothing' }});
binding = view.bindings[0];
model = binding.model;
spyOn(binding.binder, 'routine');
binding.set('fred');
expect(binding.binder.routine).toHaveBeenCalledWith(valueInput, 'fred is awesome totally');
spyOn(rivets.config.adapter, 'publish');
valueInput.value = 'fred';
binding.publish({target: valueInput});
expect(rivets.config.adapter.publish).toHaveBeenCalledWith(model, 'name', 'fred totally is radical');
});
});
describe('formattedValue()', function() {
it('applies the current formatters on the supplied value', function() {
rivets.formatters.awesome = function(value) { return 'awesome ' + value };
rivets.formatters.awesome = function(value) { return 'awesome ' + value; };
binding.formatters.push('awesome');
expect(binding.formattedValue('hat')).toBe('awesome hat');
});
it('uses formatters on the model', function() {
model.modelAwesome = function(value) { return 'model awesome ' + value };
model.modelAwesome = function(value) { return 'model awesome ' + value; };
binding.formatters.push('modelAwesome');
expect(binding.formattedValue('hat')).toBe('model awesome hat');
});
......
......@@ -40,8 +40,14 @@ class Rivets.Binding
value = if @model[id] instanceof Function
@model[id] value, args...
else if Rivets.formatters[id]
Rivets.formatters[id] value, args...
if Rivets.formatters[id].read instanceof Function
Rivets.formatters[id].read value, args...
else if Rivets.formatters[id] instanceof Function # could occur if fmt = { publish: function() {}}
Rivets.formatters[id] value, args...
else # skip if no read function exists
value
else # skip if no formatter exists
value
value
# Sets the value for the binding. This Basically just runs the binding routine
......@@ -62,8 +68,20 @@ class Rivets.Binding
Rivets.config.adapter.read @model, @keypath
# Publishes the value currently set on the input element back to the model.
publish: =>
Rivets.config.adapter.publish @model, @keypath, getInputValue @el
publish: =>
value = getInputValue @el
if @formatters
i = @formatters.length-1
while(i > -1)
formatter = @formatters[i]
args = formatter.split /\s+/
id = args.shift()
# only re-assign if there is a two-way formatter.
if Rivets.formatters[id] and Rivets.formatters[id].publish
value = Rivets.formatters[id].publish value, args...
i--
if(value)
Rivets.config.adapter.publish @model, @keypath, value
# Subscribes to the model for changes at the specified keypath. Bi-directional
# routines will also listen for changes on the element to propagate them back
......