【问题标题】:angularjs disable button on $http/$q calls$http/$q 调用上的 angularjs 禁用按钮
【发布时间】:2017-05-31 21:57:55
【问题描述】:

按照 DRY 原则,我想编写一个按钮指令,在 $http 类期间保持按钮禁用。

我想这样做是为了禁止用户多次单击按钮,但我无法考虑如何在指令中获取函数承诺状态,因为该函数位于 $scope 上

该场景非常通用,按钮 ng-click 确实调用了一个函数,该函数又进行了 $http 调用。在用户单击时:按钮应该被禁用,并且只有在 $http 调用成功或失败后才应该启用。

【问题讨论】:

标签: angularjs angularjs-directive


【解决方案1】:

为什么不让它变得更容易。

<button ng-click="save()" ng-disabled="isProcessing">Save</button>


$scope.save = function(){
  $scope.isProcessing = true;
  $http.post('Api/Controller/Save', data).success(
    $scope.isProcessing = false;
  );
}

如果您在应用程序中的极少数地方需要此逻辑,那么情况就是如此。

如果你有这样的逻辑重复很多次(如果你不懒惰:)),那么为了遵循 SOLID 原则,最好将此功能包装到指令中(查看此问题的其他答案以查看示例这样的指令)。

【讨论】:

  • 我目前正在按照您的建议进行操作,但如果那时我必须为每个按钮管理一个布尔值.. 创建指令,将在整个项目中自动处理
  • 我推荐这种方式。如果您希望它灵活,这是唯一的方法。使用拦截器侦听请求不会包括您在请求之前和之后可能想要做的客户端事情。我看到了一些点击指令,但它们都需要返回的承诺,这会影响控制器代码
  • 这不是 DRY,因为您必须注意每个单独按钮的异步状态。接受的答案是一个更好的解决方案。这对于简单的项目来说已经足够简单了,但是当事情变得更加复杂时,这就会成为一个沉重的负担。
  • 在这种情况下,该按钮可能会在摘要循环禁用之前被多次单击。
【解决方案2】:

虽然我会小心过度设计,但一种方法是使用自定义指令。该指令

  • 接受范围内必须返回承诺的函数的选项(按属性传递)
  • 单击按钮时,调用此函数并禁用按钮
  • 在承诺的finally 上,它会重新启用按钮

-

app.directive('clickAndDisable', function() {
  return {
    scope: {
      clickAndDisable: '&'
    },
    link: function(scope, iElement, iAttrs) {
      iElement.bind('click', function() {
        iElement.prop('disabled',true);
        scope.clickAndDisable().finally(function() {
          iElement.prop('disabled',false);
        })
      });
    }
  };
});

这可以用在按钮上,如下所示:

<button click-and-disable="functionThatReturnsPromise()">Click me</button>

您可以在 http://plnkr.co/edit/YsDVTlQ8TUxbKAEYNw7L?p=preview 看到这一点,其中返回承诺的函数是:

$scope.functionThatReturnsPromise = function() {
  return $timeout(angular.noop, 1000);
} 

但您可以将 $timeout 替换为对 $http 的调用,或者任何服务中返回承诺的函数。

【讨论】:

  • 这个问题是,我删除了超时功能,但我不能让它在点击后执行默认操作(例如,一旦点击按钮就提交表单),它只是禁用按钮.是否有可能使表单也被提交?
  • 我在使用这个解决方案时遇到了问题。当 HTTP 调用返回错误代码时,该按钮未重新启用。在这种情况下,下面的@codef0rmer 解决方案效果更好。
  • 我做了一点修改,不必使用隔离范围。这让我可以在同一个元素上使用 translate。 scope.$eval(attrs.clickAndDisable).finally(...) 并移除 scope 属性。
  • 想象一下,点击按钮时,我们想从服务器获取一些数据——这将从指令中调用我们的函数,而不是从视图中调用,这是 IMO 不好的做法。无论如何,好主意;)
  • 这很优雅。它还可以利用附加属性在按钮处于非活动状态时设置临时按钮标签,例如 working... 或类似的。
【解决方案3】:

我喜欢 @codef0rmer 的解决方案,因为它是集中式的——也就是说,每个 HTTP 请求都不需要额外的代码,您只需检查 HTML 中的全局 progress 标志。但是,使用transformResponse 来确定请求何时完成是不可靠的,因为服务器可能不会返回任何内容;在这种情况下,不会调用处理程序,并且永远不会将progress 设置回false。此外,正如所写,该答案不考虑多个同时请求(progress 可能会在所有请求完成之前返回false)。

我提出了一个类似的解决方案,它使用拦截器来解决这些问题。你可以把它放在你的 Angular 应用的配置函数中:

.config(function ($httpProvider) {
    $httpProvider.interceptors.push(function($q, $rootScope) {
        var numberOfHttpRequests = 0;
        return {
            request: function (config) {
                numberOfHttpRequests += 1;
                $rootScope.waitingForHttp = true;
                return config;
            },
            requestError: function (error) {
                numberOfHttpRequests -= 1;
                $rootScope.waitingForHttp = (numberOfHttpRequests !== 0);
                return $q.reject(error);
            },
            response: function (response) {
                numberOfHttpRequests -= 1;
                $rootScope.waitingForHttp = (numberOfHttpRequests !== 0);
                return response;
            },
            responseError: function (error) {
                numberOfHttpRequests -= 1;
                $rootScope.waitingForHttp = (numberOfHttpRequests !== 0);
                return $q.reject(error);
            }
        };
    });
})

现在您可以使用waitingForHttp 禁用按钮(或显示“忙碌”页面)。使用拦截器带来了额外的好处,现在您可以根据需要使用错误函数将所有 HTTP 错误记录在一个地方。

【讨论】:

    【解决方案4】:

    您可以在运行块中使用它。这将确保只要有活动的 XHR 调用,所有按钮都将被禁用。

    myApp.run(function($rootScope, $http) {
        $http.defaults.transformRequest.push(function (data) {
            $rootScope.progress = true;
            return data;
        });
        $http.defaults.transformResponse.push(function(data){ 
            $rootScope.progress = false;
            return data;
        }) 
    });
    

    然后在任何你想要的地方使用相同的模型。

     <button ng-click="save()" ng-disabled="progress">Save</button>
    

    【讨论】:

    • 我喜欢这种方法,但我建议修改您的答案以考虑多个同时请求的可能性。如果没有这个,当第一个请求返回时(但在任何其他请求返回之前)progress 将被错误地设置为false。我通过添加一个计数器numberOfHttpRequests 来解决这个问题,该计数器在函数中递增/递减;仅当 numberOfHttpRequests 为 0 时将 progress 设置为 false
    • 在进一步测试中,似乎有一些响应没有通过“transformResponse”处理程序(可能是错误,以及那些不返回任何内容的服务?),所以progress 标志可能永远不会被重置。我想出了一个类似的解决方案,而是使用拦截器。请看那个答案。
    【解决方案5】:

    您可以创建一个可用于您想要禁用的元素的指令。

     var loadingDisabled = function ($http) {
        return {
          restrict: "a",
          link: function (scope, elem, attr) {
                    scope.isLoading = function () {
                        return $http.pendingRequests.length > 0;
                    }
                    scope.$watch(scope.isLoading, function(v) {
                        if (v) {
                            elem.disabled = true;
                        } else {
                            elem.disabled = false;
                        }
                    });
    
                }
    
        };
      };
    

    【讨论】:

      【解决方案6】:

      您还可以考虑设置一个标志,并使用 html 标签字段集和 ng-disabled。然后您可以根据 $http 调用、$timeout 等来控制 yourDisableFlag 为 true 的时间长度。

      <form name="myForm">
        <fieldset ng-disabled="yourDisableFlag">
          <button id="b1" ng-click="b1function">B1</button>
          <button id="b2" ng-click="b2function">B2</button>
          <button id="b3" ng-click="b3function">B3</button>
        </fieldset>
      </form>
      

      【讨论】:

      • 当然我可以这样做,但正如问题所述,我正在寻找通用方式。
      • 没错!我认为它可能对按钮组仍然有用,但您不希望每个按钮都有单独的字段集。
      【解决方案7】:

      我尝试了不同的方法,包括指令,并想出了一个简单的过滤器,可以将 promise 转换为状态对象。然后,您可以使用状态对象来禁用相关按钮(或其他任何东西)以及显示一些进度消息。所以过滤代码是:

      function promiseStatus() {
        return function(promise) {
          var status = {
            inProgress: true,
            resolved: false,
            rejected: false
          };
          promise
            .then(function() {
              status.resolved = true;
            })
            .catch(function() {
              status.rejected = true;
            })
            .finally(function() {
              status.inProgress = false;
            });
          return status;
        }
      }
      

      假设你有这样的控制器

      function SubmitController($http) {
        var vm = this;
      
        vm.submitData = function() {
          return $http.post('....')
            .then(function() {
              //Doing something usefull
            });
        }
      }
      

      那么模板会是这样的

      <button 
        ng-disabled='status.inProgress' 
        ng-click='status = (vm.submitData() | promiseStatus)'>Submit</button>
      <span class='ng-hide' ng-show='status.inProgress'>Submitting...</span>
      <span class='ng-hide' ng-show='status.resolved'>Submitted succsssfully!</span>
      <span class='ng-hide' ng-show='status.rejected'>Failed to submit!</span>
      

      完整的源代码在Github

      【讨论】:

        【解决方案8】:

        此指令将禁用按钮,直到未完成保存/承诺。 单击必须返回承诺,否则不会禁用按钮。

        如果没有promise但仍然想禁用按钮,那么建议编写自己的禁用按钮逻辑

        app.directive('singleClick', function ($parse) {
            return {
                compile: function ($element, attr) {
                    var handler = $parse(attr.singleClick);
                    return function (scope, element, attr) {
                        element.on('click', function (event) {
                            scope.$apply(function () {
                                var promise = handler(scope, { $event: event }); /// calls and execute the function specified in attrs.
                                if (promise && angular.isFunction(promise.finally)) { /// will execute only if it returns any kind of promises.
                                    element.attr('disabled', true);
                                    promise.finally(function () {
                                        element.attr('disabled', false);
                                    });
                                }
                            });
                        });
                    };
                }
            };
        });
        

        【讨论】:

          【解决方案9】:

          我已经准备好了关于 plnkr 的指令,它会覆盖默认的 ngClick 指令

          <button ng-click="loadData($notify)">submit</button>
          

          和控制器:

          $scope.loadData = function($notify) {
              $timeout(function() {
                $notify && $notify();
              }, 1000);
            }
          

          【讨论】:

          • 并非所有 ng-click 都会返回承诺?在这种情况下如何处理那部分......??您认为所有 ng-click 都必须返回承诺的假设不成立。在这种情况下,按钮将永远保持禁用状态
          • 嗨@entre,我不确定你在想什么承诺?我认为将自定义参数传递给 ng-click 方法(在本例中为 $notify)的概念非常灵活,可以让您更好地控制管理按钮状态。
          猜你喜欢
          • 1970-01-01
          • 2021-02-09
          • 1970-01-01
          • 2018-03-10
          • 1970-01-01
          • 2017-08-30
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多