AngularJS 1.5+ 组件不支持观察者,有什么解决办法?

AngularJS 1.5+ Components do not support Watchers, what is the work around?

我一直在将我的自定义指令升级到新的 component architecture. I've read that components do not support watchers. Is this correct? If so how do you detect changes on an object? For a basic example I have custom component myBox which has a child component game with a binding on the game . If there is a change game within the game component how do I show an alert message within the myBox? I understand there is rxJS method is it possible to do this purely in angular? My JSFiddle

JavaScript

var app = angular.module('myApp', []);
app.controller('mainCtrl', function($scope) {

   $scope.name = "Tony Danza";

});

app.component("myBox",  {
      bindings: {},
      controller: function($element) {
        var myBox = this;
        myBox.game = 'World Of warcraft';
        //IF myBox.game changes, show alert message 'NAME CHANGE'
      },
      controllerAs: 'myBox',
      templateUrl: "/template",
      transclude: true
})
app.component("game",  {
      bindings: {game:'='},
      controller: function($element) {
        var game = this;


      },
      controllerAs: 'game',
      templateUrl: "/template2"
})

HTML

<div ng-app="myApp" ng-controller="mainCtrl">
  <script type="text/ng-template" id="/template">
    <div style='width:40%;border:2px solid black;background-color:yellow'>
      Your Favourite game is: {{myBox.game}}
      <game game='myBox.game'></game>
    </div>
  </script>

 <script type="text/ng-template" id="/template2">
    <div>
    </br>
        Change Game
      <textarea ng-model='game.game'></textarea>
    </div>
  </script>

  Hi {{name}}
  <my-box>

  </my-box>

</div><!--end app-->

$watch 对象在 $scope 对象中可用,因此您需要在控制器工厂函数中添加 $scope 然后将观察者放在变量上。

$scope.$watch(function(){
    return myBox.game;
}, function(newVal){
   alert('Value changed to '+ newVal)
});

Demo Here

Note: I know you had converted directive to component, to remove dependency of $scope so that you will get one step closer to Angular2. But it seems like it didn't get removed for this case.

更新

基本上 angular 1.5 确实添加了 .component 方法来区分两种不同的功能。像 component. 表示通过添加 selector 来执行特定行为,而 directive 表示向 DOM 添加特定行为。指令只是 .directive DDO(指令定义对象)上的包装方法。只有你能看到的是,他们在使用 .component 方法时删除了 link/compile 函数,你可以在其中获得 angular 编译 DOM.

请使用 Angular 组件生命周期钩子的 $onChanges/$doCheck 生命周期钩子,这些将在 Angular 1.5.3+ 版本后可用。

$onChanges(changesObj) - Called whenever bindings are updated. The changesObj is a hash whose keys are the names of the bound properties.

$doCheck() - Called on each turn of the digest cycle when binding changes. Provides an opportunity to detect and act on changes.

通过在组件内部使用相同的功能将确保您的代码兼容移动到 Angular 2.

在没有 Watchers

的情况下编写组件

此答案概述了五种技术,用于在不使用 watchers.

的情况下编写 AngularJS 1.5 组件

使用ng-change指令

what alt methods available to observe obj state changes without using watch in preparation for AngularJs2?

您可以使用 ng-change 指令对输入变化做出反应。

<textarea ng-model='game.game' 
          ng-change="game.textChange(game.game)">
</textarea>

要将事件传播到父组件,需要将事件处理程序添加为子组件的属性。

<game game='myBox.game' game-change='myBox.gameChange($value)'></game>

JS

app.component("game",  {
      bindings: {game:'=',
                 gameChange: '&'},
      controller: function() {
        var game = this;
        game.textChange = function (value) {
            game.gameChange({$value: value});
        });

      },
      controllerAs: 'game',
      templateUrl: "/template2"
});

并且在父组件中:

myBox.gameChange = function(newValue) {
    console.log(newValue);
});

这是今后的首选方法。使用$watch的AngularJS策略是不可扩展的,因为它是一个轮询策略。当 $watch 听众数量达到 2000 左右时,UI 变得迟钝。 Angular 2 中的策略是使框架更具反应性,避免将 $watch 放在 $scope.


使用$onChanges生命周期挂钩

使用 1.5.3 版本,AngularJS 将 $onChanges 生命周期挂钩添加到 $compile 服务。

来自文档:

The controller can provide the following methods that act as life-cycle hooks:

  • $onChanges(changesObj) - Called whenever one-way (<) or interpolation (@) bindings are updated. The changesObj is a hash whose keys are the names of the bound properties that have changed, and the values are an object of the form { currentValue: ..., previousValue: ... }. Use this hook to trigger updates within a component such as cloning the bound value to prevent accidental mutation of the outer value.

— AngularJS Comprehensive Directive API Reference -- Life-cycle hooks

$onChanges 挂钩用于通过 < 单向绑定对组件的外部更改作出反应。 ng-change 指令用于从具有 & 绑定的组件外部的 ng-model 控制器传播更改。


使用 $doCheck 生命周期挂钩

使用 1.5.8 版本,AngularJS 将 $doCheck 生命周期挂钩添加到 $compile 服务。

来自文档:

The controller can provide the following methods that act as life-cycle hooks:

  • $doCheck() - Called on each turn of the digest cycle. Provides an opportunity to detect and act on changes. Any actions that you wish to take in response to the changes that you detect must be invoked from this hook; implementing this has no effect on when $onChanges is called. For example, this hook could be useful if you wish to perform a deep equality check, or to check a Date object, changes to which would not be detected by Angular's change detector and thus not trigger $onChanges. This hook is invoked with no arguments; if detecting changes, you must store the previous value(s) for comparison to the current values.

— AngularJS Comprehensive Directive API Reference -- Life-cycle hooks


require

的组件间通信

指令可以require其他指令的控制器来启用彼此之间的通信。这可以在组件中通过为 require 属性 提供对象映射来实现。对象键指定 属性 名称,所需的控制器(对象值)将根据这些名称绑定到所需组件的控制器。

app.component('myPane', {
  transclude: true,
  require: {
    tabsCtrl: '^myTabs'
  },
  bindings: {
    title: '@'
  },
  controller: function() {
    this.$onInit = function() {
      this.tabsCtrl.addPane(this);
      console.log(this);
    };
  },
  templateUrl: 'my-pane.html'
});

有关详细信息,请参阅 AngularJS Developer Guide - Intercomponent Communicatation


使用 RxJS

从服务中推送值

What about in a situation where you have a Service that's holding state for example. How could I push changes to that Service, and other random components on the page be aware of such a change? Been struggling with tackling this problem lately

使用 RxJS Extensions for Angular 构建服务。

<script src="//unpkg.com/angular/angular.js"></script>
<script src="//unpkg.com/rx/dist/rx.all.js"></script>
<script src="//unpkg.com/rx-angular/dist/rx.angular.js"></script>
var app = angular.module('myApp', ['rx']);

app.factory("DataService", function(rx) {
  var subject = new rx.Subject(); 
  var data = "Initial";

  return {
      set: function set(d){
        data = d;
        subject.onNext(d);
      },
      get: function get() {
        return data;
      },
      subscribe: function (o) {
         return subject.subscribe(o);
      }
  };
});

然后只需订阅更改即可。

app.controller('displayCtrl', function(DataService) {
  var $ctrl = this;

  $ctrl.data = DataService.get();
  var subscription = DataService.subscribe(function onNext(d) {
      $ctrl.data = d;
  });

  this.$onDestroy = function() {
      subscription.dispose();
  };
});

客户端可以使用 DataService.subscribe 订阅更改,生产者可以使用 DataService.set 推送更改。

DEMO on PLNKR.

对于任何对我的解决方案感兴趣的人,我最终求助于 RXJS Observables,当你到达 Angular 2 时你将不得不使用它。这是一个有效的 fiddle 用于之间的通信组件,它让我可以更好地控制观看内容。

JS FIDDLE RXJS Observables

class BoxCtrl {
    constructor(msgService) {
    this.msgService = msgService
    this.msg = ''

    this.subscription = msgService.subscribe((obj) => {
      console.log('Subscribed')
      this.msg = obj
    })
    }

  unsubscribe() {
    console.log('Unsubscribed')
    msgService.usubscribe(this.subscription)
  }
}

var app = angular
  .module('app', ['ngMaterial'])
  .controller('MainCtrl', ($scope, msgService) => {
    $scope.name = "Observer App Example";
    $scope.msg = 'Message';
    $scope.broadcast = function() {
      msgService.broadcast($scope.msg);
    }
  })
  .component("box", {
    bindings: {},
    controller: 'BoxCtrl',
    template: `Listener: </br>
    <strong>{{$ctrl.msg}}</strong></br>
    <md-button ng-click='$ctrl.unsubscribe()' class='md-warn'>Unsubscribe A</md-button>`
  })
  .factory('msgService', ['$http', function($http) {
    var subject$ = new Rx.ReplaySubject();
    return {
      subscribe: function(subscription) {
        return subject$.subscribe(subscription);
      },
      usubscribe: function(subscription) {
        subscription.dispose();
      },
      broadcast: function(msg) {
        console.log('success');
        subject$.onNext(msg);
      }
    }
  }])

关于使用 ng-change 的小提示,建议使用已接受的答案,以及 angular 1.5 组件。

如果您需要观看 ng-modelng-change 不工作的组件,您可以将参数传递为:

使用组件的标记:

<my-component on-change="$ctrl.doSth()"
              field-value="$ctrl.valueToWatch">
</my-component>

组件 js:

angular
  .module('myComponent')
  .component('myComponent', {
    bindings: {
      onChange: '&',
      fieldValue: '='
    }
  });

组件标记:

<select ng-model="$ctrl.fieldValue"
        ng-change="$ctrl.onChange()">
</select>

在 IE11 中可用,MutationObserver https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver。您需要将 $element 服务注入到半中断 DOM/controller 分离的控制器中,但我认为这是 angularjs 中的一个基本异常(即缺陷)。由于 hide/show 是异步的,我们需要 on-show 回调,angularjs 和 angular-bootstrap-tab 不提供。它还要求您知道要观察哪个特定 DOM 元素。我为 angularjs 控制器使用了以下代码来触发 Highcharts 图表回流显示。

const myObserver = new MutationObserver(function (mutations) {
    const isVisible = $element.is(':visible') // Requires jquery
    if (!_.isEqual(isVisible, $element._prevIsVisible)) { // Lodash
        if (isVisible) {
            $scope.$broadcast('onReflowChart')
        }
        $element._prevIsVisible = isVisible
    }
})
myObserver.observe($element[0], {
    attributes: true,
    attributeFilter: ['class']
})

接受的答案非常好,但我可能会补充一点,您也可以使用事件的力量(如果您愿意的话,有点像 Qt 信号/插槽)。

广播了一个事件:$rootScope.$broadcast("clickRow", rowId) 通过任何 parent(甚至 children 控制器)。 然后在你的控制器中你可以像这样处理事件:

$scope.$on("clickRow", function(event, data){
    // do a refresh of the view with data == rowId
});

您还可以像这样添加一些日志记录(取自此处:

var withLogEvent = true; // set to false to avoid events logs
app.config(function($provide) {
    if (withLogEvent)
    {
      $provide.decorator("$rootScope", function($delegate) {
        var Scope = $delegate.constructor;
        var origBroadcast = Scope.prototype.$broadcast;
        var origEmit = Scope.prototype.$emit;

        Scope.prototype.$broadcast = function() {
          console.log("$broadcast was called on $scope " + this.$id + " with arguments:",
                     arguments);
          return origBroadcast.apply(this, arguments);
        };
        Scope.prototype.$emit = function() {
          console.log("$emit was called on $scope " + this.$id + " with arguments:",
                     arguments);
          return origEmit.apply(this, arguments);
        };
        return $delegate;
      });
    }
});

我来晚了。但它可以帮助其他人。

app.component("headerComponent", {
    templateUrl: "templates/header/view.html",
    controller: ["$rootScope", function ($rootScope) {
        let $ctrl = this;
        $rootScope.$watch(() => {
            return $ctrl.val;
        }, function (newVal, oldVal) {
            // do something
        });
    }]
});