13

I want to prevent multiple form submissions using angular.js. The question is related to this question.

When the user clicks on a form submit button the value / label of the submit button should change to "loading..", status of the button will be set to disabled AND the submit event should be triggered in the normal way, leading to a submit call to the server. This way the user would see the following effect:

  1. Immediately: The submit button value changes to "loading.." and gets disabled

  2. As soon as the server responds: user gets presented the result of the server request (whereas server responds are handled without angular intervention)

I created this plunk to show what I mean. My issue relates to this line: elm.attr('disabled',true); . This does not only disable the button, but also prevent to propagate the submit event. Thus I get a disabled button (desired result), but the form does not get submitted (undesired result).

You can see the changing behavior if you comment / uncomment this line : elm.attr('disabled',true);

Any idea how to change this?

1

9 Answers 9

21

I have a standard form and just use angular in the front-end, so if you just need to prevent a button being clicked twice while the server is responding then you can use this simple directive which is re-usable and requires no controller or ngModel.

http://plnkr.co/edit/2aZWQSLS8s6EhO5rKnRh?p=preview

app.directive('clickOnce', function($timeout) {
    return {
        restrict: 'A',
        link: function(scope, element, attrs) {
            var replacementText = attrs.clickOnce;

            element.bind('click', function() {
                $timeout(function() {
                    if (replacementText) {
                        element.html(replacementText);
                    }
                    element.attr('disabled', true);
                }, 0);
            });
        }
    };
});

It will disable the button and optionally change the text of the button. Use like so:

<button click-once>Button just disables</button>
<button click-once="Loading...">Text changes and button disables</button>

In its current form this will only work if you are doing standard form submissions and not ajax submission.

Sign up to request clarification or add additional context in comments.

4 Comments

almost perfect, but bumber it doesn't work with ajax. any recomendation for ajax submits?
Yea it's deliberately simplified for my case which was to integrate with a standard form submission. There are a number of solutions for AJAX request from re-enabling the button on response, to generic hooks. For example there is a blog post here: blog.codebrag.com/post/57412530001/…. Like I said, I deliberately chose a dumbed-down version for standard form submissions, easy to test, etc.
@MattByrne this is great and i've implemented it into my form. but it doesn't allow people to correct invalid fields and resubmit. the submit button becomes disabled for good. any thoughts on how to fix this? thanks!
I haven't looked at this for some time. I generally use ajax requests these days - this was a legacy app that I did this for. If you want to conditionally disable the button you can pass a function that returns a boolean as a parameter to the directive. You can do if (!conditionFunction || conditionFunction()) { just before the $timeout. You would need to define a scope parameter called conditionFunction that is optional. When you call it you will pass the parameter condition-function="myControllerFunction".
15

Just add a new property in your controller

$scope.processing = false;

In your method

$scope.processData = function(){
    $scope.processing = true;
    $http.post('').then(function(){
        $scope.processing = false;
    });
});

In your html bind ng-disabled attribute to the $scope.processing property to disable the button and show text while the method is processing.

1 Comment

Basically the easiest and most flexible way to do it, in my opinion
11

Try a $timeout (the angularjs function)

$timeout(function(){
    elm.attr('disabled',true);
}, 0)

3 Comments

Thanks, this seems to do the trick :) plnkr.co/edit/pl7d7uCuA4tRbproT5PP?p=preview .. can you explain why!? ..and why a timeout of zero?
The timeout is there to allow the browser to first submit, then disable the button. The function in the setTimeout call is placed in the browser's event loop and executed immediately after the form has been submitted. Without it, the button is disabled too early.
Probably should use $timeout instead? i.e. add the $timeout param to your directive function then use $timeout(function(){ elm.attr('disabled',true); }, 0);
6

Alternative (flexible & simple) solution (inspiration) : a wrapper function around the submit code that sets a scope variable. See live example.

Usage in controller:

$scope.submit = mutexAction($scope, 'sending', submit);

in view:

<form ng-submit="submit()">
  ...
  <button ng-disabled="sending">
    {{sending ? "Sending..." : "Send"}}
  </button>
</form>

The function (put it in a service):

function mutexAction(scope, semaphoreName, action) {
  return function() {
    if (scope[semaphoreName]) {
      // not queuing action, forget it!
      return;
    }
    scope[semaphoreName] = true;
    action()['finally'](function () {
      scope[semaphoreName] = false;
    });
  };
}

Comments

2

An addition to spenthil answer, a variant in coffee script + you can enable a button back if you need (e.g. when a form validation has failed and you want to try again)

class ClickOnceDirective

    constructor: (@$timeout) ->
        link = (scope, element, attrs) =>

            originalText = element.html()
            replacementText = attrs.clickOnce

            element.bind('click', =>
                @$timeout ->

                    if (replacementText)
                        element.html(replacementText)

                    element.attr('disabled', true)

                    # enable back
                    @$timeout ->
                         element.attr('disabled', false)
                        if (replacementText)
                             element.html(originalText)
                    , 500

                , 0)

         return {
         link
         restrict: 'A'
        }

directivesModule.directive 'clickOnce', ['$timeout', ClickOnceDirective]

Comments

2

The easiest and most common way of doing it is to rely on $submitted property of form object like so:

<form name="formName" ng-submit="submit()">
  ...
  <button type="submit" ng-disabled="formName.$submitted">Submit</button>
</form>

Comments

1

I ended up going the directive route as well. The following is used instead of ng-click and expects the passed in function to return a promise (which restangular does). When the promise is resolved (response is returned) - it will allow subsequent submittals. Could also tweak this to add/remove ng-disabled.

// copied from ngClick and modified to provide single click only behavior
// expects function to return promise
app.directive('srMutexClick', function ($parse) {
  return {
    compile: function ($element, attr) {
      var fn = $parse(attr['srMutexClick']);
      return function srEventHandler(scope, element) {
        var submitting = false;
        element.on('click', function (event) {
          scope.$apply(function () {
            if (submitting) {
              return
            }
            submitting = true;
            // `submitting` is reset when promise is resolved
            fn(scope, {$event: event}).finally(function() { submitting = false });
          });
        });
      };
    }
  };
});

Comments

1

here is the simple way to do similar stuff with simple logical checks, text change can also be done in similar fashion.

<button type="submit" class="btn btn-success btn-lg btn-block"  ng-disabled="!userForm.$valid || isValid">Submit!</button>


 $scope.isValid = false;
    $scope.submitForm = function () {
        $scope.isValid = true;
        console.log('submit');
    }

Comments

0

Here is a general way to do it for all AJAX requests using $http interceptors. If you have all of your REST routes starting from /api/ then:

     angular.module('yourapp').factory('loadingInterceptor',['$q','$rootScope',function($q,$rootScope) {
         var apiRe = /^\/api\//;
    return {
        request: function(config) {
            if (config.url.match(apiRe)) {
                $rootScope.loading = true;
            }
            config.headers = config.headers || {};

            return config;
        },
        response: function(res) {
            $rootScope.loading = false;
            return $q.resolve(res);
        },

        'responseError': function(rejection) {
            $rootScope.loading = false;
            return $q.reject(rejection);
        }
    };
}]);


angular.module('yourapp').config(['$httpProvider', function($httpProvider) {
    $httpProvider.interceptors.push('loadingInterceptor');
}]);

Using interceptor you won't have to put $scope.isLoading in each controller. The downside is that any button with ng-disabled="loading" will be blocked during request.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.