I have successfully found three different methods to achieve bidirectional binding of computed values. My focus was on solving the problem for the assets
sliders, rather than the roa
sliders. This decision was made based on the fact that assets
involves a simple sum calculation, while roa
is more complex and unrelated. (To @flashpunk: Feel free to reach out if you require further clarification on this topic.) The main challenge behind this task lies in the inherent imprecision of numerical values in JavaScript. When encountered with such imprecisions, Angular falls into an endless loop of recalculating changes triggered by these inaccuracies.
In essence, the issue at hand is the lossiness of numerical computations; these difficulties would not arise if the calculations were lossless.
This marks my initial solution, which implements a delicate locking mechanism to switch the primary data source based on the altered value. To strengthen this solution, I suggest developing an Angular module that extends Scope
to support more sophisticated and intricate watchers. Should I encounter similar challenges in the future, I may consider initiating a project on GitHub; any updates will be reflected in this response.
// These functions can be substituted with utility functions from libraries like lodash or underscore.
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) <= threshold; };
$scope.sliders = [
$scope.slide1 = {
assets: 10,
roa: 20
}, ... ];
$scope.mainSlide = {
assets: 0,
roa: 0
};
// A better method is recommended for managing this "locking" process,
// integrating it seamlessly within 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: Precision issues in floating-point math. Round & accept failures,
// or utilize a library supporting proper IEEE 854 decimal types
var incrementAmount = (totalAssets - oldTotalAssets) / $scope.sliders.length;
angular.forEach($scope.sliders, function(slider) { slider.assets += incrementAmount; });
modifiedCollectionAssets = true;
});
In this revised version, the clunky locking mechanism is circumvented, and the watchers are consolidated using existing Angular capabilities. I employ individual slider values as the standard data source and recalculate the total depending on the magnitude of changes. Nevertheless, the built-in features fall short in adequately addressing the scenario, necessitating manual preservation of oldValues
. (While the code could be refined, the need to perform the mentioned operation wasn't anticipated).
$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];
});
This approach reflects what should have been initially suggested. Employing a single canonical data source constituted by individual sliders. For the primary slider, a custom slider directive is fashioned, where events indicating alterations are reported instead of an ng-model usage. (Existing directives can be enhanced or branched to fulfill this purpose.)
$scope.$watch('calculateSum()', function(newSum, oldSum) {
$scope.mainSlide.assets = newSum;
});
$scope.modifyAll = function(amount) {
angular.forEach($scope.sliders, function(slider) { slider.assets += amount; });
};
Revised HTML:
Assets: <span ng-bind="mainSlide.assets"></span>
<button ng-click="modifyAll(1)">Up</button>
<button ng-click="modifyAll(-1)">Down</button>
Evaluation
This appears to be a persistent issue that the Angular team has yet to tackle. Event-driven data binding frameworks (as seen in knockout, ember, and backbone) would navigate this obstacle simply by refraining from triggering change events pertaining to lossy computed changes; in Angular's instance, some modifications within the digest cycle are essential. Although the efficiency of my first two solutions remains uncertain without expert confirmation from Misko or another Angular authority, they prove effective.
For now, my preference leans towards Method 3. If this doesn't meet your requirements, opt for Method 2 and refine it using a lossless decimal or fraction representation, steering clear of the native JavaScript Number type (consider incorporating options like decimal or fractional). -- https://www.google.com/search?q=javascript+number+libaries