0

Using AngularJS 1.6 and braintree-web 3.6.2 Hosted Fields. I've wrapped the Braintree callbacks in promises using $q (though new Promise() works fine, too). Right now, I'm simulating $scope.$apply() using $timeout with no time parameter.

Here's a snippet of ES6 code from the class for my service:

'use strict';
import braintree from 'braintree-web';

export class Cart {
  constructor($q, $log) {
    this.$q = $q;
    this.$log = $log;
  }

  // Handle the callback as a promise
  braintreeHostedFieldsCreate(clientInstance) {
    return this.$q((resolve, reject) => {
      braintree.hostedFields.create({
        client: clientInstance,
        fields: {
          number: {
            selector: '#card-number',
            placeholder: '4111 1111 1111 1111'
          },
          cvv: {
            selector: '#cvv',
            placeholder: '123'
          },
          expirationDate: {
            selector: '#expiration-date',
            placeholder: '10/2019'
          }
        },
        styles: {
          input: {
            'font-size': '14px',
            'font-family': 'Helvetica Neue, Helvetica, Arial, sans-serif',
            color: '#555'
          },
          ':focus': {
            'border-color': '#66afe9'
          },
          'input.invalid': {
            color: 'red'
          },
          'input.valid': {
            color: 'green'
          }
        }
      }, (hostedFieldsErr, hostedFieldsInstance) => {
        // Make event handlers run digest cycle using $timeout (simulate $scope.apply())
        hostedFieldsInstance.on('blur', event => this.$timeout(() => event));
        hostedFieldsInstance.on('focus', event => this.$timeout(() => event));
        hostedFieldsInstance.on('validityChange', event => this.$timeout(() => event));
        hostedFieldsInstance.on('empty', event => this.$timeout(() => event));
        hostedFieldsInstance.on('notEmpty', event => this.$timeout(() => event));

        // Reject or resolve the promise
        if(hostedFieldsErr) {
          this.$log.error('Not able to create the hosted fields with Braintree.', hostedFieldsErr);
          return reject(hostedFieldsErr);
        } else {
          this.hostedFieldsInstance = hostedFieldsInstance;
          return resolve(hostedFieldsInstance);
        }
      });
    });
  }

}

Is using $timeout in this situation a good substitute for $scope.$apply() as the latter's use after AngularJS 1.5 appears to be discouraged?

nstuyvesant
  • 1,392
  • 2
  • 18
  • 43
  • Found that I can run the digest cycle using $timeout (with no time parameter) within the event handlers but not sure if this is the best way to do it. – nstuyvesant Dec 18 '16 at 18:16

1 Answers1

1

If an Angular service wraps code that uses for example DOM events, it should be the service´s responsibility to make sure the digest loop is started (if needed).

Using $apply/$digest is still the correct way to do this.

The service can simply inject the $rootScope and wrap the event handler functionality in a call to $apply, keeping it blackboxed from the users.

An alternative is to demand the service user to pass a scope and instead call $digest, which would limit the amount of watchers processed. In my mind however the (most likely negligible) performance increase from this wouldn't be worth the added complexity.

While $timeout is also a possibility, it would just postpone the execution unnecessarily and in the end call $apply on the $rootScope anyway.

tasseKATT
  • 38,470
  • 8
  • 84
  • 65
  • Thanks tasseKATT! As you posted your response, I had updated my code to use $timeout without a time parameter to force immediate execution but it sounds like $apply would still be the preferred approach. – nstuyvesant Dec 18 '16 at 18:28
  • You're welcome :) Even with 0 as time, `$timeout` still uses `setTimeout` internally, so the execution would be postponed in the event loop at least, although in time it would probably not be noticed. I would go with `$rootScope.$apply`, since it makes the purpose clearer. – tasseKATT Dec 18 '16 at 18:31