AngularJS:如何从使用子范围的指令设置控制器属性?

时间:2013-07-07 12:07:25

标签: angularjs angularjs-directive angularjs-scope

LIVE DEMO

考虑以下spinner-click指令:

指令使用:

<button class="btn btn-mini"
        ng-class="{'btn-warning': person.active, disabled: !person.active}"
        spinner-click="deleteItem($index)"
        spinner-text="Please wait..."
        spinner-errors="alerts">
  Delete
</button>

指令:

app.directive('spinnerClick', function() {
  return {
    restrict: 'A',
    scope: true,
    link: function(scope, element, attrs) {
      var originalHTML = element.html();
      var spinnerHTML = "<i class='icon-refresh icon-spin'></i> " + attrs.spinnerText;

      element.click(function() {
        if (element.is('.disabled')) {
          return;
        }

        element.html(spinnerHTML).addClass('disabled');

        scope.$apply(attrs.spinnerClick).then(function() {
          element.html(originalHTML).removeClass('disabled');
        }, function(errors) {
          element.html(originalHTML).removeClass('disabled');

          // This is ugly! Is there a better way?
          var e = scope[attrs.spinnerErrors];
          e.length = 0;
          e.push.apply(e, errors);
        });
      });
    }
  };
});

控制器:

app.controller('MainCtrl', function($scope, $q, $timeout) {
  $scope.alerts = ['First alert'];
  $scope.people = [
    { name: 'David', active: true },
    { name: 'Layla', active: false }
  ];

  $scope.deleteItem = function(index) {
    var defer = $q.defer();

    $timeout(function() {
      defer.reject(["Something 'bad' happened.", "Check your logs."]);
    }, 2000);

    return defer.promise;
  };
});

注意: spinner-click可与其他指令一起使用(例如,此示例中为ng-class)。

如您所见,我使用非常讨厌的方式在指令中设置$scope.alerts。你能找到更好的方法吗?


更新: (DEMO)

我试图像这样使用$parse

var errorsModel = $parse(attrs.spinnerErrors);
errorsModel.assign(scope, errors);

这不起作用。

但是,如果我有spinner-errors="wrapper.alerts"而不是spinner-errors="alerts"it does work!

有没有办法避免使用包装器?

5 个答案:

答案 0 :(得分:3)

我认为你可以更简单地使用隔离范围。

而不是scope: true,,你应该把:

scope:{
    spinnerClick:"&",
    spinnerText : "@",
    spinnerErrors: "="
 }

然后,在您的指令中直接使用scope.spinnerClickscope.spinnerTextscope.spinnerErrors

&amp; 用于绑定属性中定义的函数表达式并将其传递给指令的作用域, @ 将绑定属性的文本值和 = 将设置与属性中传递的表达式的双重绑定。

你可以在这里做一个更精确的解释http://docs.angularjs.org/guide/directive(查看长版本),以及更清晰的解释http://www.egghead.io/(查看隔离范围视频,只需几分钟时间让它看起来如此简单)。

答案 1 :(得分:2)

回答你关于丑陋的原始问题

// This is ugly! Is there a better way?
var e = scope[attrs.spinnerErrors];
e.length = 0;
e.push.apply(e, errors);

您可以使用angular.copy来获得相同的结果

angular.copy(errors, scope[attrs.spinnerErrors]);

这在你的指令中如此丑陋的原因是由于你使用了一个子范围。如果您没有创建子范围,或者愿意创建隔离范围,那么这不是问题。您无法使用$scope.alerts,因为

  

子范围获取自己的属性,隐藏/隐藏父级   同名财产。你的解决方法是

     
      
  1. 在模型的父级中定义对象,然后在子级中引用该对象的属性:parentObj.someProp
  2.   
  3. 使用$ parent.parentScopeProperty(并非总是可行,但在可能的情况下比1.更容易)
  4.   
  5. 在父作用域上定义一个函数,并从子作业中调用它(并非总是可行)
  6.   

可以找到详细的解释here

答案 2 :(得分:0)

一个选项是在控制器中创建一个可以在指令中调用的setter函数。然后可以使用对子作用域中的函数的引用来设置父作用域中的值。另一个选择是创建隔离范围,然后使用&绑定传递setter函数。

答案 3 :(得分:0)

你用$ parse有了正确的想法。问题是您将新的错误数组分配给子作用域,该作用域隐藏(但不替换)父/控制器作用域上的数组。

您需要做的是获取对父数组的引用,然后替换内容(就像您在原始版本中所做的那样)。 See here

答案 4 :(得分:0)

我怀疑是否需要将错误逻辑放在指令中。您可以简单地将错误作为控制器的一部分来处理。除非您在操作警报数组之前绝对需要更换html并删除类,否则您的代码可能会被重写为:

app.directive('spinnerClick', function() {
  return {
    restrict: 'A',
    scope: true,
    link: function(scope, element, attrs) {
      var originalHTML = element.html();
      var spinnerHTML = "<i class='icon-refresh icon-spin'></i> " + attrs.spinnerText;

      function onClickDone(){
        element.html(originalHTML).removeClass('disabled');
      }

      element.click(function() {
        if (element.is('.disabled')) {
          return;
        }

        element.html(spinnerHTML).addClass('disabled');

        scope.$apply(attrs.spinnerClick).then(onClickDone, onClickDone);
      });
    }
  };
});

app.controller('MainCtrl', function($scope, $q, $timeout) {
  $scope.alerts = ['First alert'];
  $scope.people = [
    { name: 'David', active: true },
    { name: 'Layla', active: false }
  ];

  $scope.deleteItem = function(index) {
    var defer = $q.defer();

    $timeout(function() {
      defer.reject(["Something 'bad' happened.", "Check your logs."]);
    }, 2000);

    return defer.promise.then(function(){
        //Success handler
    }, function(error){
        $scope.alerts.length = 0;
        $scope.alerts.push(error);
    });
  };
});