Properties which depend on each other without an infinite loop


#1

On the app I am working on I display a two text fields - one with an amount in Euros and one with an amount in Pounds. I need the customer to be able to update either figure and for the other figure to automatically update.

The solution I have settled on looks like this:

Blah.HomeController = Ember.ObjectController.extend({
  isInternalEuroUpdate: false,
  exchangeRate: 1.2,
  euros: '',
  pounds: function(key, value) {
    if (arguments.length === 2) {
      this.set('isInternalEuroUpdate', true);
      this.set('euros', value * this.get('exchangeRate'));
      this.set('isInternalEuroUpdate', false);
    }
    return value;
  }.property('euros'),
  euroChange: function(value) {
    if (!this.get('isInternalEuroUpdate')) {
      this.set('pounds', this.get('euros') / this.get('exchangeRate'));
    }
  }.observes('euros')
});

(see it in action or edit it here: http://jsbin.com/AViZATE/103/ )

It seems pretty ugly to me (the isInternalEuroUpdate variable smells bad) - is there a better way to deal with a situation like this?

Thanks!


#2

Spoke to soon. This solution doesn’t even work and suffers from the problem that I was trying to solve in my application…

Since the change event is fired on keyup then you can’t use the arrow keys to navigate within the Euro field because every time you do it gets re-set to the value and the selection jumps to the end of the field…

There must be a simpler way?


I can make it work if the pound value isn’t updated until the Euro field is blurred:

http://jsbin.com/ECAYIqA/2/

Ember.ObjectController.extend({
  exchangeRate: 1.2,
  euros: '',
  pounds: function(key, value) {
    if (arguments.length === 2) {
      this.set('euros', (value * this.get('exchangeRate')).toFixed(2));
    }
    return value;
  }.property(),
  actions: {
    eurosBlur: function() {
      this.set('pounds', (this.get('euros') / this.get('exchangeRate')).toFixed(2));
    }
  }
});

Still not ideal though…


Update - here’s a working version:

var isEurosFocused = false;
var euroValue;
var poundValue;

Blah.HomeController = Ember.ObjectController.extend({
  exchangeRate: 1.2,
  euros: function(key, value) {
    if (arguments.length === 2) {
      if (isEurosFocused) {
        this.set('pounds', (value / this.get('exchangeRate')).toFixed(2));
      }
      euroValue = value;
    }
    return euroValue;
  }.property('pounds'),
  pounds: function(key, value) {
    if (arguments.length === 2) {
      if (!isEurosFocused) {
        this.set('euros', (value * this.get('exchangeRate')).toFixed(2));
      }
      poundValue = value;
    }
    return poundValue;
  }.property('euros'),
  actions: {
    eurosFocus: function() {
      isEurosFocused = true;
    },
    eurosBlur: function() {
      isEurosFocused = false;
    }
  }
});

I’d like to know if there is a better way though, this still feels pretty messy…


#3

I think the first step is to decide which value is the “source of truth”. For sake of example, let’s say that the euro is the source of truth. That value will always be a primitive and the other value (pounds) will always be calculated.

Then, this could look like:

Blah.HomeController = Ember.ObjectController.extend({
  exchangeRate : 1.2,
  euros : 1,
  pounds : function(key, value) {
    if(arguments.length === 2) {
      this.set('euros', (value / this.get('exchangeRate')).toFixed(2) );
      return value;
    } else {
      return (this.get('euros') * this.get('exchangeRate')).toFixed(2);
    }
  }.property('exchangeRate', 'euros')
});

#4

It works (just had to switch the multiplication and division around to make the numbers make sense)!

And is much much cleaner and simpler - I think this could make a good example for the cookbook.

Thanks :slight_smile:


#5

Would it be possible to do the same thing but with arrays and have them update whenever I push/remove something in one or the other? The above solution doesn’t work in this case, because a push is different from a set and you won’t be able to play nicely with the arguments length. Here’s my use case:

export default Ember.Controller.extend({
    queryParams: ["a1"],
    a1: Ember.computed.mapBy("a1", "value"),
    a2: Ember.computed.map("a1", function(d) { return { value: d }; }),
    actions: {
        add: function(item) { this.get("a2").pushObject(item); } // item is an object with a "value" and a "label" property
    },
    a3: [{value:"red",label:"RED"},{value:"blue",label:"BLUE"}]
});

Array a3 serves in my template as the options of a <select> element. Whatever then gets selected (a click on an option fires the “add” action) is pushed into a2, which then updates a1, which is also a param in the queryParams array (a1 is there purely because I don’t want to serialize an array of objects in the URL but an array of strings, it’s just cleaner).

You might wonder why then have a2 depend on a1? Well that’s because I want my select element to be populated on a page refresh, because its selection is bound to a2.