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:

  • User fills out a form, and clicks a submit button
  • A portion of that form is sent to our servers, which starts a transaction with the payment gateway
  • The response from our servers updates values in the form, which must then be submitted directly to the payment gateway, via a form POST.
  • Other stuff happens.

In this scenario, how do I:

  • Update the data in the form (without referencing the form from the controller)
  • Get the form to submit?

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.

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.
})

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.)

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
});
}