Reputation: 4525
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
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
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