简体   繁体   English

带有2种方式绑定的AngularJs $ watch

[英]AngularJs $watch with 2 way binding

I'm creating a page where there are range sliders that effect eachother depending on what they're set at. 我正在创建一个页面,其中有范围滑块会根据它们的设置而相互影响。 I have a watch function that looks like below, that works one way, however I'd like to have the watch effect both ways, but I can't figure out if there is a way I can do this without creating one for each value (there are about 12 sliders). 我有一个如下所示的监视功能,它只能以一种方式工作,但是我想同时具有两种监视效果,但是我无法弄清楚是否有一种方法可以针对每个值创建一个(大约有12个滑块)。

Here is a Plunker outlining what I am trying to do: http://plnkr.co/edit/CXMeOT?p=preview 这是一个Plunker概述了我想做的事情: http ://plnkr.co/edit/CXMeOT?p=preview

I figured out three different ways to do this bidirectional binding of computed values. 我想出了三种不同的方法来进行计算值的双向绑定。 I only wrote the solution for the assets sliders and not the roa sliders, since I assets is a basic sum, and roa is something else all together. 我只为assets滑块而不是roa滑块编写了解决方案,因为我的assets是基本金额,而roa则是其他所有东西。 (@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; (@flashpunk:如果您需要更多有关问题的帮助,请解释。)为何如此困难的关键问题是,在javascript中,数字不能以精确的10进制格式表示; when this loss of imprecision is encountered, angular goes into an infinite loop, recalculating the changes based on the changes caused by the imprecision. 当遇到这种不精确性的损失时,角度将进入无限循环,并根据不精确性引起的变化重新计算变化。

In other words, the problem is that numerical calculations are lossy , these problems would not occur if the calculations were lossless . 换句话说,问题在于数值计算是有损的 ,如果计算是无损的,则不会发生这些问题。

Approach 1 (click for plunkr) 方法1(点击查看)

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 Scope to allow for more advanced and complex watchers. 为了使该解决方案更可靠,我将创建一个角度模块,该模块扩展了Scope以允许更高级和更复杂的观察者。 If I run into the problem in the future, I may start a project on github; 如果将来遇到问题,可以在github上启动一个项目。 I'll revise this answer if I do so. 如果这样做,我会修改此答案。

// 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) <= 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 & 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;
});

Approach 2 方法2

In this version, I avoid the clunky locking mechanism, and unify the watchers with what's currently available in angular. 在此版本中,我避免了笨拙的锁定机制,而将观察者与当前可用的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 oldValues . 同样,内置功能不足以彻底表达问题,因此我需要手动保存oldValues (The code could be cleaner, but as I didn't anticipate having to perform the aforementioned operation). (代码可能更简洁,但是由于我没有想到必须执行上述操作)。

$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];
});

Approach 3: Back to basics 方法3:回到基础

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. 对于主滑块,创建一个自定义滑块指令,该指令不具有ng-model,而是触发报告所做更改的事件。 (You can wrap or fork an existing directive to do this.) (您可以包装或分叉现有的指令来执行此操作。)

$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: 修改后的html:

Assets: <span ng-bind="mainSlide.assets"></span>
<button ng-click="modifyAll(1)">Up</button>
<button ng-click="modifyAll(-1)">Down</button>

Review 评论

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. 我不知道我的前两个解决方案是否是Misko或其他有经验的专家建议的方案,但它们确实有效。

For now, I would prefer Approach 3. If that doesn't satisfy you, use approach 2, and clean it up; 现在,我希望使用方法3。如果您不满意,请使用方法2,然后进行清理; but use a lossless decimal or fraction representation, not the built-in javascript Number type (like this for decimal or this, for fractional ). 但请使用无损的小数或小数表示形式,而不要使用内置的javascript Number类型( 对于小数小数来说 ,例如 )。 -- https://www.google.com/search?q=javascript+number+libaries -https://www.google.com/search?q=javascript+number+libaries

I believe this is what you want. 我相信这就是您想要的。

$scope.watchList = [$scope.item1, $scope.item2];
$scope.$watchCollection('watchList', function(newValues){...});

That way you can watch all of your slider scope variables in the same watch. 这样,您可以在同一观察表中观察所有滑块范围变量。

What's wrong with creating a $watch in both directions? 双向创建$watch有什么问题? If you have 12 different cases here, I'd suggest building a little factory function to set them up. 如果您在这里有12种不同的情况,建议您建立一些工厂功能来进行设置。 You just have to make sure that your calculated values stabilize or otherwise you'll have an infinite recursion with each $watch statement triggering the other. 您只需要确保计算值稳定即可,否则每个$watch语句都会触发另一个无限递归。

I'm guessing at your dependencies between sliders as a bit, but I'd suggest something like the following: 我在猜测您在滑块之间的依赖性,但是我建议如下所示:

function bindSliders() {

    var mainSlider = arguments[0];
    var childSliders = arguments.slice(1);

    angular.forEach(childSliders, function(slider) {

        // Forward Direction
        $scope.$watch(function() {
            return slider.roa + slider.assets;
        }, function (newValue, oldValue) {
            // A child slider changed
            // Sum up all the child sliders and update mainSlider
        });

        // Reverse Direction
        $scope.$watch(function() {
            return mainSlider.assett + mainSlider.roa
        }, function (newValue, oldValue) {
            // Update this `slider` based on the updated main value

        });

    });
}

bindSliders($scope.mainSlide, $scope.slide1, $scope.slide2, $scope.slide3, $scope.slide4);

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM