Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fahrenheit and Celsius Bidirectional Conversion in AngularJS

In AngularJS, it is straightforward how to $watch a $scope variable and use that to update another. But what is the best practice if two scope variables need to watch each other?

I have as an example a bidirectional converter that will convert Fahrenheit to Celsius, and vice versa. It works okay if you type "1" into the Fahrenheit, but try "1.1" and Angular will bounce around a little before overwriting Fahrenheit that you just entered to be a slightly different value (1.1000000000000014):

function TemperatureConverterCtrl($scope) {
  $scope.$watch('fahrenheit', function(value) {
    $scope.celsius = (value - 32) * 5.0/9.0;
  });
  $scope.$watch('celsius', function(value) {
    $scope.fahrenheit = value * 9.0 / 5.0 + 32;
  });
}

Here's a plunker: http://plnkr.co/edit/1fULXjx7MyAHjvjHfV1j?p=preview

What are the different possible ways to stop Angular from "bouncing" around and force it to use the value you typed as-is, e.g. by using formatters or parsers (or any other trick)?

like image 715
David Baird Avatar asked Jul 13 '13 01:07

David Baird


2 Answers

I think the simplest, fastest, and most correct solution is to have a flag to track which field is being edited, and only allow updates from that field.

All you need is to use the ng-change directive to set the flag on the field being edited.

Working Plunk

Code changes necessary:

Modify the controller to look like this:

function TemperatureConverterCtrl($scope) {
  // Keep track of who was last edited
  $scope.edited = null;
  $scope.markEdited = function(which) {
    $scope.edited = which;
  };

  // Only edit if the correct field is being modified
  $scope.$watch('fahrenheit', function(value) {
    if($scope.edited == 'F') {
      $scope.celsius = (value - 32) * 5.0/9.0;
    }
  });
  $scope.$watch('celsius', function(value) {
    if($scope.edited == 'C') {
      $scope.fahrenheit = value * 9.0 / 5.0 + 32;
    }
  });
}

And then add this simple directive to the input fields (using F or C as appropriate):

<input ... ng-change="markEdited('F')/>

Now only the field being typed in can change the other one.

If you need the ability to modify these fields outside an input, you could add a scope or controller function that looks something like this:

$scope.setFahrenheit = function(val) {
  $scope.edited = 'F';
  $scope.fahrenheit = val;
}

Then the Celsius field will be updated on the next $digest cycle.

This solution has a bare minimum of extra code, eliminates any chance of multiple updates per cycle, and doesn't cause any performance issues.

like image 100
OverZealous Avatar answered Nov 07 '22 12:11

OverZealous


This is a pretty good question and I'm answering it even though it's already accepted :)

In Angular, the $scope is the model. The model is a place to store data you might want to persist or use in other parts of the app, and as such, it should be designed with a good schema just as you would in a database for example.

Your model has two redundant fields for temperature, which isn't a good idea. Which one is the "real" temperature? There are times when you want to denormalize a model, just for access efficiency, but that's only really an option when the values are idempotent, which as you found, these aren't due to floating point precision.

If you wanted to continue using the model it would look something like this. You'd pick one or the other as the "source of truth" temperature, then have the other input as a convenience entry box with a formatter and parser. Let's say we want Fahrenheit in the model:

<input type="number" ng-model="temperatureF">
<input type="number" ng-model="temperatureF" fahrenheit-to-celcius>

And the conversion directive:

someModule.directive('fahrenheitToCelcius', function() {
  return {
    require: 'ngModel',
    link: function(scope, element, attrs, ngModel) {
      ngModel.$formatters.push(function(f) {
        return (value - 32) * 5.0 / 9.0;
      });
      ngModel.$parsers.push(function(c) {
        return c * 9.0 / 5.0 + 32;
      });
    }
  };
});

With this, you'd avoid the "bouncing around" because $parsers only run on user action, not on model changes. You'd still have long decimals but that could be remedied with rounding.

However, it sounds to me like you shouldn't be using a model for this. If all you want is "each box updates the other box", you can do exactly that and not even have a model. This assumes the input boxes start out blank and it's just a simple tool for users to convert temperatures. If that's the case, you have no model, no controller, and aren't even hardly using Angular at all. It's a purely view-based widget at that point, and it's basically just jQuery or jQLite. It's of limited usefulness though, since with no model it can't effect anything else in Angular.

To do that, you could just make a temperatureConverter directive that has a template with a couple of input boxes, and watches both boxes and sets their values. Something like:

fahrenheitBox.bind('change', function() {
  celciusBox.val((Number(fahrenheitBox.val()) - 32) * 5.0 / 9.0);
});
like image 25
jpsimons Avatar answered Nov 07 '22 12:11

jpsimons