Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    primarykey
    data
    text
    <p>I figured out three different ways to do this bidirectional binding of computed values. I only wrote the solution for the <code>assets</code> sliders and not the <code>roa</code> sliders, since I <code>assets</code> is a basic sum, and <code>roa</code> is something else all together. (@flashpunk: Please explain, if you need more help with that aspect of your question.) The key issue for why this is difficult is that, in javascript, numbers are not represented in a precise base 10 format; when this loss of imprecision is encountered, angular goes into an infinite loop, recalculating the changes based on the changes caused by the imprecision.</p> <p>In other words, the problem is that numerical calculations are <em>lossy</em>, these problems would not occur if the calculations were <em>lossless</em>.</p> <h2><a href="http://plnkr.co/edit/Ciq6BZ?p=preview" rel="nofollow">Approach 1 (click for plunkr)</a></h2> <p>This was my first solution. It uses a (fragile) locking mechanism to switch the canonical data source depending on which value was changed. In order to make this solution more robust, I would create an angular module that extends <code>Scope</code> to allow for more advanced and complex watchers. If I run into the problem in the future, I may start a project on github; I'll revise this answer if I do so.</p> <pre><code>// Both of these functions can be replaced with library functions from lodash or underscore, etc. var sum = function(array) { return array.reduce(function(total, next) { return total + (+next); }, 0) }; var closeTo = function(a, b, threshold) { return a == b || Math.abs(a - b) &lt;= threshold; }; $scope.sliders = [ $scope.slide1 = { assets: 10, roa: 20 }, ... ]; $scope.mainSlide = { assets: 0, roa: 0 }; // you might want to make or look for a helper function to control this "locking" // in a better fashion, that integrates and resets itself with the digest cycle var modifiedMainAssets = false; var modifiedCollectionAssets = false; $scope.$watch(function() { return sum($scope.sliders.map(function(slider) { return slider.assets; })) }, function(totalAssets) { if (modifiedCollectionAssets) { modifiedCollectionAssets = false; return; } $scope.mainSlide.assets = totalAssets; modifiedMainAssets = true; }); $scope.$watch('mainSlide.assets', function(totalAssets, oldTotalAssets) { if (modifiedMainAssets) { modifiedMainAssets = false; return; } if (oldTotalAssets === null || closeTo(totalAssets, oldTotalAssets, 0.1)) { return; } // NOTE: has issues with floating point math. either round &amp; accept the failures, // or use a library that supports a proper IEEE 854 decimal type var incrementAmount = (totalAssets - oldTotalAssets) / $scope.sliders.length; angular.forEach($scope.sliders, function(slider) { slider.assets += incrementAmount; }); modifiedCollectionAssets = true; }); </code></pre> <h2><a href="http://plnkr.co/edit/p6LEo2?p=preview" rel="nofollow">Approach 2</a></h2> <p>In this version, I avoid the clunky locking mechanism, and unify the watchers with what's currently available in angular. I use the individual sliders' values as the canonical data source, and recalculate the total based on how much has changed. Again, the built-in facilities are inadequate for thoroughly expressing the issue, and I need to manually save the <code>oldValues</code>. (The code could be cleaner, but as I didn't anticipate having to perform the aforementioned operation).</p> <pre><code>$scope.calculateSum = function() { return sum($scope.sliders.map(function(slider) { return slider.assets; })); }; $scope.mainSlide.assets = $scope.calculateSum(); var modifiedMainAssets = false; var modifiedCollectionAssets = false; var oldValues = [$scope.mainSlide.assets, $scope.mainSlide.assets]; $scope.$watchCollection('[calculateSum(), mainSlide.assets]' , function(newValues) { var newSum = newValues[0]; var oldSum = oldValues[0]; var newAssets = newValues[1]; var oldAssets = oldValues[1]; if (newSum !== oldSum) { $scope.mainSlide.assets = newSum; } else if (newAssets !== oldAssets) { var incrementAmount = (newAssets - oldAssets) / $scope.sliders.length; angular.forEach($scope.sliders, function(slider) { slider.assets += incrementAmount; }); $scope.mainSlide.assets = $scope.calculateSum(); } oldValues = [$scope.mainSlide.assets, $scope.mainSlide.assets]; }); </code></pre> <h2><a href="http://jsfiddle.net/forivall/b4RBD/" rel="nofollow">Approach 3: Back to basics</a></h2> <p>This is what I probably should have advised in the first place. Have one canonical data source, which are the individual sliders. For the main slider, create a custom slider directive that, instead of having an ng-model, fires events that report the changes made. (You can wrap or fork an existing directive to do this.)</p> <pre><code>$scope.$watch('calculateSum()', function(newSum, oldSum) { $scope.mainSlide.assets = newSum; }); $scope.modifyAll = function(amount) { angular.forEach($scope.sliders, function(slider) { slider.assets += amount; }); }; </code></pre> <p>Revised html:</p> <pre><code>Assets: &lt;span ng-bind="mainSlide.assets"&gt;&lt;/span&gt; &lt;button ng-click="modifyAll(1)"&gt;Up&lt;/button&gt; &lt;button ng-click="modifyAll(-1)"&gt;Down&lt;/button&gt; </code></pre> <h2>Review</h2> <p>This seems to be a problem that the angular team has yet to address. Event-based data binding systems (what's used in knockout, ember and backbone) would address this by simply not firing the change event on the specific computed change that is lossy; in angular's case, some tweaking needs to be done to the digest cycle. I have no idea if my first two solutions are what would be advised by Misko or another angular guru, but they work.</p> <p>For now, I would prefer Approach 3. If that doesn't satisfy you, use approach 2, and clean it up; but use a lossless decimal or fraction representation, not the built-in javascript Number type (like this <a href="https://github.com/shinuza/decimal-js" rel="nofollow">for decimal</a> or <a href="https://github.com/ekg/fraction.js" rel="nofollow">this, for fractional</a>). -- <a href="https://www.google.com/search?q=javascript+number+libaries" rel="nofollow">https://www.google.com/search?q=javascript+number+libaries</a></p>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. VO
      singulars
      1. This table or related slice is empty.
    2. VO
      singulars
      1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload