如何在自定义指令中检测ng模型对象的更改?

时间:2015-06-03 10:09:16

标签: javascript angularjs angularjs-directive

考虑具有我追求的行为的Plunker

问题是1)我认为我做错了,2)它使用了一块很贵且不必要的深表。

这个例子有点人为。我在我的应用程序中实际拥有的是一个功能齐全的typeahead / autocomplete控件,它绑定到对象并根据表达式呈现它们。剥离的羽毛被展开以仅显示与此问题相关的部分; 如何正确实现此类事情?

主要困难是确保在输入中输入“something”并单击按钮应该传播到指令中并使span.outlet相应地更新。即,点击Alpha,并尝试更改描述,应该会在页面上显示“已选择:A - 某事”。

如果我删除深度监视,除非我用新的对象引用替换$ scope.selected而不是仅更改现有对象引用的属性(参见plunker的mainCtrl中的注释),否则不会发生这种情况。

因此,第一个要求是自定义指令处理对象,而不仅仅是字符串。

第二个要求是,只要span.outlet对象从自定义指令外部发生更改,指令就必须能够更新$scope.selected

第三,指令应尽可能高效。这就是我提出这个问题的原因。 ng-model已经内部有一个浅的$ watch,现在我在它上面加了一个深的$ watch,这对于perf来说很糟糕。如果没有这么深的$ watch,有没有办法做到这一点?

如果我不必拥有scope.ngModel绑定,那将是很好的。感觉很脏。

相关标记:

<input ng-model="newDescription">
<button ng-click="setNewDescription(newDescription)">Set description</button>
<hr/>
<my-list ng-model="selected" expression="{{::expression}}" options="options"></my-list>

相关主控制器代码:

  $scope.options = [
    {key:'A', desc: 'Alpha'},
    {key:'B', desc: 'Beta'},
    {key:'G', desc: 'Gamma'},
    {key:'D', desc: 'Delta'}
  ];
  $scope.selected = $scope.options[1];
  $scope.expression = '{{key}} - {{desc}}';

myList指令:

app.directive('myList', ['$interpolate', function($interpolate) {
  return {
    restrict: 'E',
    require: 'ngModel',
    replace: true,
    template: '<div>Selected: <span class="outlet"></span><ul><li ng-repeat="item in vm.items"><a href="" ng-click="vm.select(item)">{{vm.render(item)}}</a></li></ul></div>',
    scope: {
      ngModel: '=',
      items: '=options'
    },
    link: Link,
    controller: Ctrl,
    controllerAs: 'vm',
    bindToController: true 
  };

  function Link(scope, element, attrs, ngModelCtrl) {
     var outlet = element.children()[0];
     scope.vm.render = $interpolate(attrs.expression);

     ngModelCtrl.$formatters.push(function(modelValue) {
       if (ngModelCtrl.$isEmpty(modelValue))
            return '';
        else
            return scope.vm.render(modelValue);
     });

     ngModelCtrl.$render = function() {
       console.log('rendering', ngModelCtrl.$viewValue);
       outlet.textContent = scope.vm.render(ngModelCtrl.$modelValue);
     };

    ngModelCtrl.$parsers.push(function(value) {
        // Only gets called due to the $setViewValue call in the deep $watch.
        // We don't have a way of going from a string to an object, but the $modelValue contains the right thing.
        console.log('parsing', value);
        return ngModelCtrl.$modelValue;
    });

    // I would prefer it if I could solve this without a deep watch on ngModel!
    scope.$watch('vm.ngModel', function(n, o) {
      if (angular.equals(n, o)) return;
      console.log('ngModel $watch\r\n', o, '->\r\n', n);
      ngModelCtrl.$setViewValue(scope.vm.render(n));
      ngModelCtrl.$render();
    }, true); 
  }

  function Ctrl($scope) {
    this.select = function(item) {
      console.log('selecting', item);
      this.ngModel = item;
    }.bind(this);
  }
}]);

对此有任何帮助,我非常感激,因为我一直试图解决这个问题一段时间。谢谢!

2 个答案:

答案 0 :(得分:1)

我不确定我是否完全了解您的需求,但我准备了sample

app.directive('myList', ['$interpolate', function($interpolate) {
  return {
    restrict: 'E',
    replace: true,
    template: '<div>Selected: <span class="outlet"></span><ul><li ng-repeat="item in vm.items"><a href="" ng-click="vm.select(item)">{{vm.render(item)}}</a></li></ul></div>',
    scope: {
      selected: '=',
      items: '=options'
    },
    link: Link,
    controller: Ctrl,
    controllerAs: 'vm',
    bindToController: true 
  };

  function Link(scope, element, attrs, ctrl) {
     var outlet = element.children()[0];
     scope.vm.render = $interpolate(attrs.expression);

    scope.$watch(function () {
      return ctrl.selected;
    }, function (value) {
      console.log('rendering', value);
      outlet.textContent = scope.vm.render(value);
    });
  }

  function Ctrl($scope) {
    this.select = function(item) {
      console.log('selecting', item);
      this.selected = item;
    }.bind(this);
  }
}]);

HTML:

 <input ng-model="newDescription">
 <button ng-click="setNewDescription(newDescription)">Set description</button>
 <hr/>
 <my-list selected="selected" expression="{{::expression}}" options="options"></my-list>

更新2

<{> Sampleng-model。你不需要在$ watch中调用$ render函数。它会在模型​​自动更改后调用。

这也是如何使用ngModel here创建指令的好例子。

答案 1 :(得分:0)

在与@Okazari进行一些讨论后,我至少可以通过将其替换为根据表达式渲染模型的功能手表来消除深度观察。

scope.$watch(function() {
  return scope.vm.render(scope.vm.ngModel);
}, function(n, o) {
  if (n && n !== o) {
    console.log('ngModel $watch\r\n', o, '->\r\n', n);
    ngModelCtrl.$setViewValue(scope.vm.render(n));
    ngModelCtrl.$render();
  }
});

我仍然没有信心我这样做是正确的。但至少现在它是一种有效的方式。