diff --git a/examples/bind-input.js b/examples/bind-input.js index 1f0fd6bb1a..6668f1bed7 100644 --- a/examples/bind-input.js +++ b/examples/bind-input.js @@ -36,23 +36,30 @@ var visible = new ol.dom.Input(document.getElementById('visible')); visible.bindTo('checked', layer, 'visible'); var opacity = new ol.dom.Input(document.getElementById('opacity')); -opacity.bindTo('valueAsNumber', layer, 'opacity'); +opacity.bindTo('value', layer, 'opacity') + .transform(parseFloat, String); var hue = new ol.dom.Input(document.getElementById('hue')); -hue.bindTo('valueAsNumber', layer, 'hue'); +hue.bindTo('value', layer, 'hue') + .transform(parseFloat, String); var saturation = new ol.dom.Input(document.getElementById('saturation')); -saturation.bindTo('valueAsNumber', layer, 'saturation'); +saturation.bindTo('value', layer, 'saturation') + .transform(parseFloat, String); var contrast = new ol.dom.Input(document.getElementById('contrast')); -contrast.bindTo('valueAsNumber', layer, 'contrast'); +contrast.bindTo('value', layer, 'contrast') + .transform(parseFloat, String); var brightness = new ol.dom.Input(document.getElementById('brightness')); -brightness.bindTo('valueAsNumber', layer, 'brightness'); +brightness.bindTo('value', layer, 'brightness') + .transform(parseFloat, String); var rotation = new ol.dom.Input(document.getElementById('rotation')); -rotation.bindTo('valueAsNumber', map.getView(), 'rotation'); +rotation.bindTo('value', map.getView(), 'rotation') + .transform(parseFloat, String); var resolution = new ol.dom.Input(document.getElementById('resolution')); -resolution.bindTo('valueAsNumber', map.getView(), 'resolution'); +resolution.bindTo('value', map.getView(), 'resolution') + .transform(parseFloat, String); diff --git a/examples/layer-group.js b/examples/layer-group.js index 61861b4f01..b7f7d8ec6c 100644 --- a/examples/layer-group.js +++ b/examples/layer-group.js @@ -45,7 +45,8 @@ function bindInputs(layerid, layer) { $.each(['opacity', 'hue', 'saturation', 'contrast', 'brightness'], function(i, v) { new ol.dom.Input($(layerid + ' .' + v)[0]) - .bindTo('valueAsNumber', layer, v); + .bindTo('value', layer, v) + .transform(parseFloat, String); } ); } diff --git a/examples/mouse-position.js b/examples/mouse-position.js index 7a43a9abf4..f399735b70 100644 --- a/examples/mouse-position.js +++ b/examples/mouse-position.js @@ -35,9 +35,16 @@ var map = new ol.Map({ }); var projectionSelect = new ol.dom.Input(document.getElementById('projection')); -projectionSelect.on('change:value', function() { - mousePositionControl.setProjection(ol.proj.get(projectionSelect.getValue())); -}); +projectionSelect.bindTo('value', mousePositionControl, 'projection') + .transform( + function(code) { + // projectionSelect.value -> mousePositionControl.projection + return ol.proj.get(/** @type {string} */ (code)); + }, + function(projection) { + // mousePositionControl.projection -> projectionSelect.value + return projection.getCode(); + }); var precisionInput = document.getElementById('precision'); precisionInput.addEventListener('change', function() { diff --git a/src/ol/dom/input.js b/src/ol/dom/input.js index dfaedb61e3..fb59399b17 100644 --- a/src/ol/dom/input.js +++ b/src/ol/dom/input.js @@ -11,7 +11,6 @@ goog.require('ol.Object'); */ ol.dom.InputProperty = { VALUE: 'value', - VALUE_AS_NUMBER: 'valueAsNumber', CHECKED: 'checked' }; @@ -50,9 +49,6 @@ ol.dom.Input = function(target) { goog.events.listen(this, ol.Object.getChangeEventType(ol.dom.InputProperty.VALUE), this.handleValueChanged_, false, this); - goog.events.listen(this, - ol.Object.getChangeEventType(ol.dom.InputProperty.VALUE_AS_NUMBER), - this.handleValueAsNumberChanged_, false, this); goog.events.listen(this, ol.Object.getChangeEventType(ol.dom.InputProperty.CHECKED), this.handleCheckedChanged_, false, this); @@ -88,20 +84,6 @@ goog.exportProperty( ol.dom.Input.prototype.getValue); -/** - * Get the value of the input as a number. - * @return {number|null|undefined} input value as number. - * @todo stability experimental - */ -ol.dom.Input.prototype.getValueAsNumber = function() { - return /** @type {number} */ (this.get(ol.dom.InputProperty.VALUE_AS_NUMBER)); -}; -goog.exportProperty( - ol.dom.Input.prototype, - 'getValueAsNumber', - ol.dom.Input.prototype.getValueAsNumber); - - /** * Sets the value of the input. * @param {string} value Value. @@ -116,20 +98,6 @@ goog.exportProperty( ol.dom.Input.prototype.setValue); -/** - * Sets the number value of the input. - * @param {number} value Number value. - * @todo stability experimental - */ -ol.dom.Input.prototype.setValueAsNumber = function(value) { - this.set(ol.dom.InputProperty.VALUE_AS_NUMBER, value); -}; -goog.exportProperty( - ol.dom.Input.prototype, - 'setValueAsNumber', - ol.dom.Input.prototype.setValueAsNumber); - - /** * Set whether or not a checkbox is checked. * @param {boolean} checked Checked. @@ -152,10 +120,6 @@ ol.dom.Input.prototype.handleInputChanged_ = function() { this.setChecked(this.target_.checked); } else { this.setValue(this.target_.value); - var number = this.target_.valueAsNumber; - if (goog.isDef(number) && !isNaN(number)) { - this.setValueAsNumber(number); - } } }; @@ -174,12 +138,3 @@ ol.dom.Input.prototype.handleCheckedChanged_ = function() { ol.dom.Input.prototype.handleValueChanged_ = function() { this.target_.value = this.getValue(); }; - - -/** - * @private - */ -ol.dom.Input.prototype.handleValueAsNumberChanged_ = function() { - // firefox raises an exception if this.target_.valueAsNumber is set instead - this.target_.value = this.getValueAsNumber(); -}; diff --git a/src/ol/object.js b/src/ol/object.js index 70864f815a..4d82943de0 100644 --- a/src/ol/object.js +++ b/src/ol/object.js @@ -11,6 +11,7 @@ goog.provide('ol.ObjectEventType'); goog.require('goog.array'); goog.require('goog.events'); goog.require('goog.events.EventTarget'); +goog.require('goog.functions'); goog.require('goog.object'); @@ -22,6 +23,51 @@ ol.ObjectEventType = { }; + +/** + * @constructor + * @param {ol.Object} target + * @param {string} key + * @todo stability experimental + */ +ol.ObjectAccessor = function(target, key) { + + /** + * @type {ol.Object} + */ + this.target = target; + + /** + * @type {string} + */ + this.key = key; + + /** + * @type {function(?): ?} + */ + this.from = goog.functions.identity; + + /** + * @type {function(?): ?} + */ + this.to = goog.functions.identity; +}; + + +/** + * @param {function(?): ?} from A function that transforms the source value + * before it is set to the target. + * @param {function(?): ?} to A function that transforms the target value + * before it is set to the source. + */ +ol.ObjectAccessor.prototype.transform = function(from, to) { + this.from = from; + this.to = to; + + this.target.notify(this.key); +}; + + /** * @enum {string} */ @@ -87,7 +133,7 @@ ol.Object.capitalize = function(str) { /** * @param {ol.Object} obj Object. - * @return {Object.} Accessors. + * @return {Object.} Accessors. */ ol.Object.getAccessors = function(obj) { return obj[ol.ObjectProperty.ACCESSORS] || @@ -139,14 +185,35 @@ ol.Object.getSetterName = function(key) { /** - * Binds a View to a Model. + * The bindTo method allows you to set up a two-way binding between a + * `source` and `target` object. The method returns an + * ol.ObjectAccessor with a transform method that lets you transform + * values on the way from the source to the target and on the way back. + * + * For example, if you had two map views (sourceView and targetView) + * and you wanted the target view to have double the resolution of the + * source view, you could transform the resolution on the way to and + * from the target with the following: + * + * sourceView.bindTo('resolution', targetView) + * .transform( + * function(sourceResolution) { + * // from sourceView.resolution to targetView.resolution + * return 2 * sourceResolution; + * }, + * function(targetResolution) { + * // from targetView.resolution to sourceView.resolution + * return targetResolution / 2; + * } + * ); + * * @param {string} key Key. * @param {ol.Object} target Target. * @param {string=} opt_targetKey Target key. + * @return {ol.ObjectAccessor} * @todo stability experimental */ -ol.Object.prototype.bindTo = - function(key, target, opt_targetKey) { +ol.Object.prototype.bindTo = function(key, target, opt_targetKey) { var targetKey = opt_targetKey || key; this.unbind(key); var eventType = ol.Object.getChangeEventType(targetKey); @@ -154,9 +221,11 @@ ol.Object.prototype.bindTo = listeners[key] = goog.events.listen(target, eventType, function() { this.notifyInternal_(key); }, undefined, this); + var accessor = new ol.ObjectAccessor(target, targetKey); var accessors = ol.Object.getAccessors(this); - accessors[key] = {target: target, key: targetKey}; + accessors[key] = accessor; this.notifyInternal_(key); + return accessor; }; @@ -179,6 +248,7 @@ ol.Object.prototype.get = function(key) { } else { value = target.get(targetKey); } + value = accessor.to(value); } else if (this.values_.hasOwnProperty(key)) { value = this.values_[key]; } @@ -289,6 +359,7 @@ ol.Object.prototype.set = function(key, value) { var target = accessor.target; var targetKey = accessor.key; var setterName = ol.Object.getSetterName(targetKey); + value = accessor.from(value); if (target[setterName]) { target[setterName](value); } else { diff --git a/test/spec/ol/object.test.js b/test/spec/ol/object.test.js index 938b364e12..1c59d6f273 100644 --- a/test/spec/ol/object.test.js +++ b/test/spec/ol/object.test.js @@ -473,6 +473,95 @@ describe('ol.Object', function() { expect(o.getKeys()).to.eql(['K']); }); }); + + describe('transforms', function() { + + describe('with multiple binds to a single property', function() { + + var original, plusOne, asString; + + beforeEach(function() { + original = new ol.Object(); + original.set('x', 1); + plusOne = new ol.Object(); + plusOne.bindTo('x', original).transform( + function(x) { return x - 1; }, + function(x) { return x + 1; } + ); + asString = new ol.Object(); + asString.bindTo('x', original).transform( + function(x) { return +x; }, + function(x) { return x + ''; } + ); + }); + + it('returns the expected value', function() { + expect(original.get('x')).to.be(1); + expect(plusOne.get('x')).to.be(2); + expect(asString.get('x')).to.be('1'); + }); + + it('allows the original value to be set correctly', function() { + original.set('x', 2); + expect(plusOne.get('x')).to.be(3); + expect(asString.get('x')).to.be('2'); + }); + + it('allows the transformed values to be set correctly', function() { + plusOne.set('x', 3); + expect(original.get('x')).to.be(2); + expect(asString.get('x')).to.be('2'); + asString.set('x', '3'); + expect(original.get('x')).to.be(3); + expect(plusOne.get('x')).to.be(4); + }); + + }); + + describe('with transitive binds', function() { + + var original, plusOne, plusOneAsString; + + beforeEach(function() { + original = new ol.Object(); + original.set('x', 1); + plusOne = new ol.Object(); + plusOne.bindTo('x', original).transform( + function(x) { return x - 1; }, + function(x) { return x + 1; } + ); + plusOneAsString = new ol.Object(); + plusOneAsString.bindTo('x', plusOne).transform( + parseFloat, + function(x) { return x + ''; } + ); + }); + + it('returns the expected value', function() { + expect(original.get('x')).to.be(1); + expect(plusOne.get('x')).to.be(2); + expect(plusOneAsString.get('x')).to.be('2'); + }); + + it('allows the original value to be set correctly', function() { + original.set('x', 2); + expect(plusOne.get('x')).to.be(3); + expect(plusOneAsString.get('x')).to.be('3'); + }); + + it('allows the transformed values to be set correctly', function() { + plusOne.set('x', 3); + expect(original.get('x')).to.be(2); + expect(plusOneAsString.get('x')).to.be('3'); + plusOneAsString.set('x', '4'); + expect(original.get('x')).to.be(3); + expect(plusOne.get('x')).to.be(4); + }); + + }); + + }); + });