AngularJS - ngClick,自定义指令和隔离范围问题

时间:2022-07-29 19:41:52

Consider the following directive: (Live Demo)

请考虑以下指令:(现场演示)

app.directive('spinner', function() {
  return {
    restrict: 'A',
    scope: {
      spinner: '=',
      doIt: "&doIt"
    },
    link: function(scope, element, attrs) {
      var spinnerButton = angular.element("<button class='btn disabled'><i class='icon-refresh icon-spin'></i> Doing...</button>");
      element.after(spinnerButton);

      scope.$watch('spinner', function(showSpinner) {
        spinnerButton.toggle(showSpinner);
        element.toggle(!showSpinner);
      });
    }
  };
}); 

which is used like this:

使用如下:

<button ng-click="doIt()" spinner="spinIt">Spin It</button>

When spinner's value (i.e. the value of $scope.spinIt in this example) is true, the element should be hidden and spinnerButton should appear instead. When spinner's value is false, the element should be visible and spinnerButton should be hidden.

当spinner的值(即本例中$ scope.spinIt的值)为true时,应隐藏该元素,而应显示spinnerButton。当spinner的值为false时,该元素应该是可见的,并且应该隐藏spinnerButton。

The problem here is that doIt() is not in the isolated scope, thus not called on click.

这里的问题是doIt()不在隔离范围内,因此不会在单击时调用。

What would be the "Angular way" to implement this directive?

实施该指令的“Angular方式”是什么?

7 个解决方案

#1


8  

My suggestion is to look at what's going on with these spinners. Be a little more API focused.

我的建议是看看这些纺纱厂发生了什么。更专注于API。

Relevant part follows. We use a regular callback to indicate when we're done, so the spinner knows to reset the state of the button.

相关部分如下。我们使用常规回调来指示我们何时完成,因此微调器知道重置按钮的状态。

function SpinDemoCtrl($scope, $timeout, $q) {
  $scope.spinIt = false;

  $scope.longCycle = function(complete) {
    $timeout(function() {
      complete();
    }, 3000);
  };

  $scope.shortCycle = function(complete) {
    $timeout(function() {
      complete();
    }, 1000);
  };
}

app.directive('spinnerClick', function() {
  return {
    restrict: 'A',
    scope: {
      spinnerClick: "=",
    },
    link: function(scope, element, attrs) {
      var spinnerButton = angular.element("<button class='btn disabled'><i class='icon-refresh icon-spin'></i> Doing...</button>").hide();
      element.after(spinnerButton);

      element.click(function() {
        spinnerButton.show();
        element.hide();

        scope.spinnerClick(function() {
          spinnerButton.hide();
          element.show();
        });
      });
    }
  };
});

Here's one that expects use of $q. It'll work better with Angular-style asynchronous operations, and eliminates the callback functions by instead having the spinner reset on fulfilment of the promise.

这是一个期望使用$ q的人。使用Angular风格的异步操作可以更好地工作,并通过在履行承诺时重置微调器来消除回调函数。

#2


2  

Here is the polished version of the directive I ended up with (based on Yuki's suggestion), in case it helps someone: (CoffeeScript)

这是我最终得到的指令的抛光版本(基于Yuki的建议),以防它帮助某人:(CoffeeScript)

app.directive 'spinnerClick', ->
  restrict: 'A'
  link: (scope, element, attrs) ->
    originalHTML = element.html()
    spinnerHTML = "<i class='icon-refresh icon-spin'></i> #{attrs.spinnerText}"

    element.click ->
      return if element.is('.disabled')

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

      scope.$apply(attrs.spinnerClick).then ->
        element.html(originalHTML).removeClass('disabled')

Use it like so:

像这样使用它:

<button class="btn btn-primary" spinner-click="createNewTask()" 
                                spinner-text="Creating...">
  Create
</button>

Controller's code:

TasksNewCtrl = ($scope, $location, $q, Task) ->
  $scope.createNewTask = ->
    deferred = $q.defer()

    Task.save $scope.task, ->
      $location.path "/tasks"
    , (error) ->
      // Handle errors here and then:
      deferred.resolve()

    deferred.promise

#3


1  

Yes, it will call doIt in your isolated scope.

是的,它将在您的隔离范围内调用doIt。

You can use $parent.doIt in that case

在这种情况下,您可以使用$ parent.doIt

<button ng-click="$parent.doIt()" spinner="spinIt">Spin It</button>

#4


1  

From the AngularJS Documentation (http://docs.angularjs.org/guide/directive):

来自AngularJS文档(http://docs.angularjs.org/guide/directive):

& or &attr - provides a way to execute an expression in the context of the parent scope. If no attr name is specified then the attribute name is assumed to be the same as the local name. Given and widget definition of scope: { localFn:'&myAttr' }, then isolate scope property localFn will point to a function wrapper for the count = count + value expression. Often it's desirable to pass data from the isolated scope via an expression and to the parent scope, this can be done by passing a map of local variable names and values into the expression wrapper fn. For example, if the expression is increment(amount) then we can specify the amount value by calling the localFn as localFn({amount: 22}).

&or&attr - 提供在父作用域的上下文中执行表达式的方法。如果未指定attr名称,则假定属性名称与本地名称相同。范围的给定和窗口小部件定义:{localFn:'&myAttr'},然后隔离范围属性localFn将指向count = count + value表达式的函数包装器。通常需要通过表达式将数据从隔离范围传递到父范围,这可以通过将局部变量名称和值的映射传递到表达式包装器fn来完成。例如,如果表达式是increment(amount),那么我们可以通过将localFn称为localFn({amount:22})来指定金额值。

so inlclude doIt: "&doIt" in your scope declaration, then you can use doIt as a function in your isolated scope.

所以在你的范围声明中包含doIt:“&doIt”,然后你可以在你的孤立范围内使用doIt作为函数。

#5


0  

I'm confused as to why you are not packaging up everything in the directive as if it's a self-contained module. That's at least what I would do. In other words, you have the click-handler in the HTML, some behavior in the directive and some behavior in the external controller. This makes your code much less portable and more decentralized.

我很困惑为什么你没有在指令中打包所有内容,就像它是一个独立的模块一样。这至少是我要做的。换句话说,您在HTML中有click-handler,在指令中有一些行为,在外部控制器中有一些行为。这使您的代码更不便携,更分散。

Anyway, you may have reasons for this that are not shared, but my suggestion would be to put all the "Spin It" related stuff in the spinner directive. This means the click-handler, the doIt() function and template stuff all within the link function.

无论如何,你可能有理由不共享,但我的建议是将所有“Spin It”相关的东西放在spinner指令中。这意味着链接函数中的click-handler,doIt()函数和模板内容。

That way there's no need to worry about sharing scope and code entanglement. Or, am I just missing something?

这样就无需担心共享范围和代码纠缠。或者,我只是错过了什么?

#6


0  

I don't know about the 'angular' way of doing things, but i suggest not using an isolated scope but instead just creating a child scope. You then do attrs.$observe to get any properties you need for your directive.

我不知道“有角度”的做事方式,但我建议不要使用孤立的范围,而只是创建一个子范围。然后你做了attrs。$ observe获得你指令所需的任何属性。

I.E. :

app.directive('spinner', function() {
    return {
        restrict: 'A',
        scope: true, //Create child scope not isolated scope
        link: function(scope, element, attrs) {
            var spinnerButton = angular.element("<button class='btn disabled'><i class='icon-refresh icon-spin'></i> Doing...</button>");
            element.after(spinnerButton);

            //Using attrs.$observe
            attrs.$observe('spinner', function(showSpinner) {
            spinnerButton.toggle(showSpinner);
            element.toggle(!showSpinner);
            });
        }
    };
});

I find this way is better than using '$parent' to escape the isolated scope in other directives (eg ngClick or ngModel) as the end user of your directive does not need to know whether or not using your directive requires them to use '$parent' or not on core angularjs directives.

我发现这种方法比使用'$ parent'来逃避其他指令中的隔离范围(例如ngClick或ngModel)更好,因为指令的最终用户不需要知道使用你的指令是否要求他们使用'$在核心angularjs指令中是否为父级。

#7


0  

Using CoffeeScript and a FontAwesome icon.

使用CoffeeScript和FontAwesome图标。

  • No need to manually specify the spinner-text
  • 无需手动指定微调文本

  • It will just add the spinner content left of the text while loading
  • 它只会在加载时添加文本左侧的微调内容

  • We must use finally instead of then for the promise otherwise the spinner will stay there on failure?
  • 我们必须最终使用而不是当时的承诺否则旋转器会在失败时留在那里吗?

  • I must use $compile because the contents of the button is dynamically compiled as I am using https://github.com/angular-translate/angular-translate
  • 我必须使用$ compile,因为按钮的内容是动态编译的,因为我正在使用https://github.com/angular-translate/angular-translate


app.directive 'spinnerClick', ["$compile", ($compile) ->
    restrict: 'A'
    link: (scope, element, attrs) ->
        originalHTML = element.html()
        spinnerHTML = "<i class='fa fa-refresh fa-spin'></i> "

        element.click ->
            return if element.is('.disabled')
            element.html(spinnerHTML + originalHTML).addClass('disabled')
            $compile(element.contents())(scope)
            scope.$apply(attrs.spinnerClick).finally ->
                element.html(originalHTML).removeClass('disabled')
                $compile(element.contents())(scope)
]

#1


8  

My suggestion is to look at what's going on with these spinners. Be a little more API focused.

我的建议是看看这些纺纱厂发生了什么。更专注于API。

Relevant part follows. We use a regular callback to indicate when we're done, so the spinner knows to reset the state of the button.

相关部分如下。我们使用常规回调来指示我们何时完成,因此微调器知道重置按钮的状态。

function SpinDemoCtrl($scope, $timeout, $q) {
  $scope.spinIt = false;

  $scope.longCycle = function(complete) {
    $timeout(function() {
      complete();
    }, 3000);
  };

  $scope.shortCycle = function(complete) {
    $timeout(function() {
      complete();
    }, 1000);
  };
}

app.directive('spinnerClick', function() {
  return {
    restrict: 'A',
    scope: {
      spinnerClick: "=",
    },
    link: function(scope, element, attrs) {
      var spinnerButton = angular.element("<button class='btn disabled'><i class='icon-refresh icon-spin'></i> Doing...</button>").hide();
      element.after(spinnerButton);

      element.click(function() {
        spinnerButton.show();
        element.hide();

        scope.spinnerClick(function() {
          spinnerButton.hide();
          element.show();
        });
      });
    }
  };
});

Here's one that expects use of $q. It'll work better with Angular-style asynchronous operations, and eliminates the callback functions by instead having the spinner reset on fulfilment of the promise.

这是一个期望使用$ q的人。使用Angular风格的异步操作可以更好地工作,并通过在履行承诺时重置微调器来消除回调函数。

#2


2  

Here is the polished version of the directive I ended up with (based on Yuki's suggestion), in case it helps someone: (CoffeeScript)

这是我最终得到的指令的抛光版本(基于Yuki的建议),以防它帮助某人:(CoffeeScript)

app.directive 'spinnerClick', ->
  restrict: 'A'
  link: (scope, element, attrs) ->
    originalHTML = element.html()
    spinnerHTML = "<i class='icon-refresh icon-spin'></i> #{attrs.spinnerText}"

    element.click ->
      return if element.is('.disabled')

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

      scope.$apply(attrs.spinnerClick).then ->
        element.html(originalHTML).removeClass('disabled')

Use it like so:

像这样使用它:

<button class="btn btn-primary" spinner-click="createNewTask()" 
                                spinner-text="Creating...">
  Create
</button>

Controller's code:

TasksNewCtrl = ($scope, $location, $q, Task) ->
  $scope.createNewTask = ->
    deferred = $q.defer()

    Task.save $scope.task, ->
      $location.path "/tasks"
    , (error) ->
      // Handle errors here and then:
      deferred.resolve()

    deferred.promise

#3


1  

Yes, it will call doIt in your isolated scope.

是的,它将在您的隔离范围内调用doIt。

You can use $parent.doIt in that case

在这种情况下,您可以使用$ parent.doIt

<button ng-click="$parent.doIt()" spinner="spinIt">Spin It</button>

#4


1  

From the AngularJS Documentation (http://docs.angularjs.org/guide/directive):

来自AngularJS文档(http://docs.angularjs.org/guide/directive):

& or &attr - provides a way to execute an expression in the context of the parent scope. If no attr name is specified then the attribute name is assumed to be the same as the local name. Given and widget definition of scope: { localFn:'&myAttr' }, then isolate scope property localFn will point to a function wrapper for the count = count + value expression. Often it's desirable to pass data from the isolated scope via an expression and to the parent scope, this can be done by passing a map of local variable names and values into the expression wrapper fn. For example, if the expression is increment(amount) then we can specify the amount value by calling the localFn as localFn({amount: 22}).

&or&attr - 提供在父作用域的上下文中执行表达式的方法。如果未指定attr名称,则假定属性名称与本地名称相同。范围的给定和窗口小部件定义:{localFn:'&myAttr'},然后隔离范围属性localFn将指向count = count + value表达式的函数包装器。通常需要通过表达式将数据从隔离范围传递到父范围,这可以通过将局部变量名称和值的映射传递到表达式包装器fn来完成。例如,如果表达式是increment(amount),那么我们可以通过将localFn称为localFn({amount:22})来指定金额值。

so inlclude doIt: "&doIt" in your scope declaration, then you can use doIt as a function in your isolated scope.

所以在你的范围声明中包含doIt:“&doIt”,然后你可以在你的孤立范围内使用doIt作为函数。

#5


0  

I'm confused as to why you are not packaging up everything in the directive as if it's a self-contained module. That's at least what I would do. In other words, you have the click-handler in the HTML, some behavior in the directive and some behavior in the external controller. This makes your code much less portable and more decentralized.

我很困惑为什么你没有在指令中打包所有内容,就像它是一个独立的模块一样。这至少是我要做的。换句话说,您在HTML中有click-handler,在指令中有一些行为,在外部控制器中有一些行为。这使您的代码更不便携,更分散。

Anyway, you may have reasons for this that are not shared, but my suggestion would be to put all the "Spin It" related stuff in the spinner directive. This means the click-handler, the doIt() function and template stuff all within the link function.

无论如何,你可能有理由不共享,但我的建议是将所有“Spin It”相关的东西放在spinner指令中。这意味着链接函数中的click-handler,doIt()函数和模板内容。

That way there's no need to worry about sharing scope and code entanglement. Or, am I just missing something?

这样就无需担心共享范围和代码纠缠。或者,我只是错过了什么?

#6


0  

I don't know about the 'angular' way of doing things, but i suggest not using an isolated scope but instead just creating a child scope. You then do attrs.$observe to get any properties you need for your directive.

我不知道“有角度”的做事方式,但我建议不要使用孤立的范围,而只是创建一个子范围。然后你做了attrs。$ observe获得你指令所需的任何属性。

I.E. :

app.directive('spinner', function() {
    return {
        restrict: 'A',
        scope: true, //Create child scope not isolated scope
        link: function(scope, element, attrs) {
            var spinnerButton = angular.element("<button class='btn disabled'><i class='icon-refresh icon-spin'></i> Doing...</button>");
            element.after(spinnerButton);

            //Using attrs.$observe
            attrs.$observe('spinner', function(showSpinner) {
            spinnerButton.toggle(showSpinner);
            element.toggle(!showSpinner);
            });
        }
    };
});

I find this way is better than using '$parent' to escape the isolated scope in other directives (eg ngClick or ngModel) as the end user of your directive does not need to know whether or not using your directive requires them to use '$parent' or not on core angularjs directives.

我发现这种方法比使用'$ parent'来逃避其他指令中的隔离范围(例如ngClick或ngModel)更好,因为指令的最终用户不需要知道使用你的指令是否要求他们使用'$在核心angularjs指令中是否为父级。

#7


0  

Using CoffeeScript and a FontAwesome icon.

使用CoffeeScript和FontAwesome图标。

  • No need to manually specify the spinner-text
  • 无需手动指定微调文本

  • It will just add the spinner content left of the text while loading
  • 它只会在加载时添加文本左侧的微调内容

  • We must use finally instead of then for the promise otherwise the spinner will stay there on failure?
  • 我们必须最终使用而不是当时的承诺否则旋转器会在失败时留在那里吗?

  • I must use $compile because the contents of the button is dynamically compiled as I am using https://github.com/angular-translate/angular-translate
  • 我必须使用$ compile,因为按钮的内容是动态编译的,因为我正在使用https://github.com/angular-translate/angular-translate


app.directive 'spinnerClick', ["$compile", ($compile) ->
    restrict: 'A'
    link: (scope, element, attrs) ->
        originalHTML = element.html()
        spinnerHTML = "<i class='fa fa-refresh fa-spin'></i> "

        element.click ->
            return if element.is('.disabled')
            element.html(spinnerHTML + originalHTML).addClass('disabled')
            $compile(element.contents())(scope)
            scope.$apply(attrs.spinnerClick).finally ->
                element.html(originalHTML).removeClass('disabled')
                $compile(element.contents())(scope)
]