Marty Pitt
Marty Pitt

Reputation: 29280

AngularJS : What's the Angular way to interact with a form?

I understand one of the key principals of Angular is:

Thou shalt not reference thy DOM from withinst thou's controllers.

I'm trying to process a credit card payment, which requires the following steps:

In this scenario, how do I:

The form binds to a model on my controller, so I've tried something like the following:

<form action="{{paymentModel.urlFromTheResponse}}">
    <input type="hidden" name="accessCode" value="{{paymentModelaccessCodeFromResponse}}" />
    <button ng-click="startTransaction(paymentModel)"></button>
</form>

// in my success handler
.success(function(data) {
     paymentModel.urlFromTheResponse = data.url;
     paymentModel.accessCode = data.accessCode;
     $scope.apply();
}

the theory being here that if I can immediately get the form into the correct state via databinding, I can then do something to submit the form. However, this throws an error:

Digest already in progress

What's the Angular way to support this type of flow? It seems I'm required to interact directly with the DOM, which goes against the nature of controllers.

Upvotes: 1

Views: 1672

Answers (3)

Will Vincent
Will Vincent

Reputation: 331

As others have stated, you shouldn't need to call $scope.$apply() because the form should already be tied to angular by setting ng-model attributes on each of the fields.

However, occasionally it is necessary to call $scope.$apply() to update display when data is pulled in from some other source outside of angular...

In those cases, I've had great luck with this:

  // This method will be inherited by all other controllers
  // It should be used any time that $scope.$apply() would
  // otherwise be used.
  $scope.safeApply = function(fn) {
    var phase = this.$root.$$phase;
    if(phase == '$apply' || phase == '$digest') {
      if(fn && (typeof(fn) === 'function')) {
        fn();
      }
    } else {
      this.$apply(fn);
    }
  };

I place that in my outermost controller, so all other controllers on the page inherit the function from it.. Any time I find I need a call to apply, I instead call $scope.safeApply() which will call apply if there is not already an apply or digest in progress, otherwise, those changes will already be picked up by the currently running apply/digest.

In your code I would change this:

<input type="hidden" name="accessCode" value="{{paymentModelaccessCodeFromResponse}}" />

To this:

<input type="hidden" name="accessCode" ng-model="paymentModel.accessCode" />

I would probably also remove the form action, and instead add something like this in the controller:

$scope.$watch('paymentModel.accessCode', function() {
  // Fire off additional form submission here.
})

Upvotes: 1

e82
e82

Reputation: 146

Assuming you are using something like $http, you are already inside of the angular scope and should not need to manually call $scope.apply(); as you are inside of the angular execution already.

You should be able to ditch the $scope.apply() and simply have an

.success(function(data) {
     paymentModel.urlFromTheResponse = data.url;
     paymentModel.accessCode = data.accessCode;
 $http.post("/finalstep",paymentModel).success(function(data)
{
// other stuff
});
}

Upvotes: 0

Mark Rajcok
Mark Rajcok

Reputation: 364687

The error is generated because your success callback is already "inside Angular", so $scope.apply() will be called automatically for you.

If you use ng-model (instead of value) on your form elements, then you can modify the model/$scope properties in your success callback and the form will automatically update (due to two-way databinding via ng-model). However, instead of trying to submit the form, why not just use the $http or $resource service inside your controller to call the web service? (That's why I asked if the user needed to be involved in my comment.)

Upvotes: 0

Related Questions