Roaders
Roaders

Reputation: 4525

How to refresh view in angularJs app when onError called on Observable Subject

I have an AngularJs app and use RXJS. I have a service that waits for a user to allow or deny access to an OAuth provider and has this signature:

authorise( requestDetails : IRequestDetails ) : Rx.Observable<string>;

When the access is allowed and I get my access token back I pass this back to the controller like this:

request.observable.onNext( request.access_token );
request.observable.onCompleted();

and the controller passed this back to the view by updating the backer variable for the message:

    private _message : string = "Awaiting authorisation";

    get message() : string {
        return this._message;
    }

    authenticate() : void
    {
        this._oAuthService.authorise( Example.googleAuthDetails ).subscribe(
            ( result : string ) => this.handleAuthorisation( result ),
            ( fault : string ) => this.handleError( fault )
        );
    }

    private handleAuthorisation( result : string ) : void {
        this._message = result;
    }

This all works fine. I have discovered that the reason it works in this case is because the onNext call is made as part of a result from a $http call. This call is wrapped in a call to $apply that triggers a digest cycle. However when the user denies access I call onError on the observable:

    request.observable.onError( "access token denied" );
    request.observable.onCompleted();

and in the controller I update the string with the error:

    private handleError( fault : string ) : void {
        this._message = fault;
    }

but in this case the view does not update and the displayed string remains as "Awaiting Authorisation". In this case as the authorisation was denied no $http call is made and the controller is triggered in a function that is not wrapped in an $apply call.

I have included the rx.angular.js library in my html page hoping that would automatically fix it but it hasn't. Reading the docs it seems that I might have to watch something specifically. I thought that I could just get it work with RXJS and always update the scope when something updates. I don't want to inject scopes into my controller to force a refresh if I don't have to. I don't like having a dependency on that.

The whole project is available to view in this branch:

https://github.com/Roaders/Typescript-OAuth-SPA/blob/rxjs_issues/index.html

Upvotes: 1

Views: 816

Answers (2)

Cyril Gandon
Cyril Gandon

Reputation: 17048

Just wrap everything into a $timeout call, it will ensure that a digest is called.

RX.angular seems nice, but I think it is a little outdated with the new norm to not use $scope everywhere.

var myAppModule = angular.module('myApp', []).controller('ctrl', SettingsController1);

function SettingsController1($timeout) {

  this.message = "waiting";
  this.observable = new Rx.ReplaySubject(2 /* buffer size */ );
  var _this = this;
  this.observable.subscribe(
    function(x) {
      $timeout(function() {
        _this.message = x;
      }, 0);
    },
    function(err) {
      $timeout(function() {
        _this.message = err;
      }, 0);
    },
    function() {
      this.message = 'completed';
    });

  this.success = function() {
    this.observable.onNext('success');
  }

  this.error = function() {
    this.observable.onError('failed');
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/4.0.7/rx.all.min.js"></script>

<div ng-app="myApp" ng-controller="ctrl as ctrl">
  <div>
    {{ ctrl.message }}
  </div>
  <button ng-click="ctrl.success()">
    Success
  </button>
  <button ng-click="ctrl.error()">
    Error
  </button>
</div>

This solution is implemented in this branch: https://github.com/Roaders/Typescript-OAuth-SPA/tree/rx_solved_with_timeout

Upvotes: 1

Roaders
Roaders

Reputation: 4525

The reason that it didn't work for the error case is because there was no http call to trigger a digest cycle so we need to trigger a digest cycle in this case.

To do that we need to inject the $rootScope into the service and when we call onError we need to wrap it in a $rootScope.$apply call:

        this._scope.$apply( () => {
            request.observable.onError( "access token denied" );
            request.observable.onCompleted();
        } );

This solution is here: https://github.com/Roaders/Typescript-OAuth-SPA/tree/rx_solved_with_scope

I know that I said I didn't want a scope injected but now I have a better understanding of why I need to I think I am OK with this. I am also happier about injecting the root scope rather than a controller scope that is not used for anything else.

Upvotes: 0

Related Questions