Now my task is to rewrite $exceptionHandler provider so that it will output modal dialog with message and stop default event.
What I do:
in project init I use method .provider:
.provider('$exceptionHandler', function(){
//and here I would like to have rootScope to make event broadcast
})
standart inject method does not work.
UPD: sandbox - http://jsfiddle.net/STEVER/PYpdM/
You can inject the injector and lookup the $rootScope.
Demo plunkr: http://plnkr.co/edit/0hpTkXx5WkvKN3Wn5EmY?p=preview
myApp.factory('$exceptionHandler',function($injector){
return function(exception, cause){
var rScope = $injector.get('$rootScope');
if(rScope){
rScope.$broadcast('exception',exception, cause);
}
};
})
Update: add .provider technique too:
app.provider('$exceptionHandler', function() {
// In the provider function, you cannot inject any
// service or factory. This can only be done at the
// "$get" method.
this.$get = function($injector) {
return function(exception,cause){
var rScope = $injector.get('$rootScope');
rScope.$broadcast('exception',exception, cause);
}
};
});
My way of doing this - using a decorator and reverting to the previous exception handler on unknown errors:
app.config(function ($provide) {
$provide.decorator('$exceptionHandler', function($delegate, $injector) {
return function (exception, cause) {
if (ICanHandleThisError) {
var rootScope= $injector.get('$rootScope');
// do something (can use rootScope)
} else
$delegate(exception, cause);
};
});
});
You need to inject the $rootScope:
.provider('$exceptionHandler', '$rootScope', function(){
//and here I would like to have rootScope to make event broadcast
})
Is this what you tried? And if so do you have an error message or a jsfillde/plnkr to see why it failed?
Related
In my angular application (using ES6) I need to make an ajax call after some custom event is fired from other directive with the code below:
class GridController {
constructor($scope, $rootScope, MyService) {
this.$scope = $scope;
this.$rootScope = $rootScope;
this.MyService = MyService
this.MyService.getItems(arg1, arg2).then((resp) => {
this.viewModel= resp;
});
this.$rootScope.$on('myEvent', (e, args) => {
this.MyService.getItems(arg1, args[1]).then((resp) => {
this.viewModel= resp;
});
}
});
}
The problem is that the view is not updated after the successful ajax call.
Is there any ideas why this is happening?
It may be model update problem try this.
$scope.$apply();
Maybe the ajax-call happens outside of angulars digest cycle. You have tried calling $scope.$digest()? This fires the watchers of the current scope; they should pick up the model change.
THE SITUATION:
I am unit testing my Angular / Ionic app.
I am having troubles with the modal.
At the moment i can test that the modal has been called. That's all so far. I cannot test the proper show() and hide() method of the modal.
I am getting the following errors:
TypeError: $scope.modal_login.show is not a function
Error: show() method does not exist
TypeError: $scope.modal_login.hide is not a function
Error: hide() method does not exist
I think it depends entirely on the spy. I don't know how to properly spy on the modal, and i think that once that is done, everything will work fine.
THE CODE:
The controller:
$scope.open_login_modal = function()
{
var temp = $ionicModal.fromTemplateUrl('templates/login.html',{scope: $scope});
temp.then(function(modal) {
$scope.modal_login = modal;
$scope.modal_login.show();
$scope.for_test_only = true;
});
};
$scope.close_login_modal = function()
{
$scope.modal_login.hide();
};
Note: the code of open_login_modal function has been refactored to facilitate the test. The original code was:
$scope.open_login_modal = function()
{
$ionicModal.fromTemplateUrl('templates/login.html', {
scope: $scope
}).then(function(modal) {
$scope.modal_login = modal;
$scope.modal_login.show();
});
};
The test:
describe('App tests', function()
{
beforeEach(module('my_app.controllers'));
function fakeTemplate()
{
return {
then: function(modal){
$scope.modal_login = modal;
}
}
}
beforeEach(inject(function(_$controller_, _$rootScope_)
{
$controller = _$controller_;
$rootScope = _$rootScope_;
$scope = _$rootScope_.$new();
$ionicModal =
{
fromTemplateUrl: jasmine.createSpy('$ionicModal.fromTemplateUrl').and.callFake(fakeTemplate)
};
var controller = $controller('MainCtrl', { $scope: $scope, $rootScope: $rootScope, $ionicModal: $ionicModal });
}));
describe('Modal tests', function()
{
beforeEach(function()
{
$scope.open_login_modal();
spyOn($scope.modal_login, 'show'); // NOT WORKING
spyOn($scope.modal_login, 'hide'); // NOT WORKING
});
it('should open login modal', function()
{
expect($ionicModal.fromTemplateUrl).toHaveBeenCalled(); // OK
expect($ionicModal.fromTemplateUrl.calls.count()).toBe(1); // OK
expect($scope.modal_login.show()).toHaveBeenCalled(); // NOT PASS
expect($scope.for_test_only).toEqual(true); // NOT PASS
});
it('should close login modal', function()
{
$scope.close_login_modal();
expect($scope.modal_login.hide()).toHaveBeenCalled(); // NOT PASS
});
});
});
As you can see from the code $scope.for_test_only it should be equal to true but is not recognized. I get this error message instead:
Expected undefined to equal true.
The same happens to the show() and hide() method. They are not seen by the test.
And i think because they are not declared in the spy.
THE QUESTION:
How can i properly spy on a modal?
Thank you very much!
The question here could be extrapolated to how to properly spy on a promise. You are very much on the right track here.
However, if you want to test that whatever your callback to the success of the promise is called, you have to execute two steps:
Mock the service (in your case $ionicModal) and return some fake function
In that fake function, execute the callback that is passed to you by the production code.
Here is an illustration:
//create a mock of the service (step 1)
var $ionicModal = jasmine.createSpyObj('$ionicModal', ['fromTemplateUrl']);
//create an example response which just calls your callback (step2)
var successCallback = {
then: function(callback){
callback.apply(arguments);
}
};
$ionicModal.fromTemplateUrl.and.returnValue(successCallback);
Of course, you can always use $q if you don't want to be maintaining the promise on your own:
//in your beforeeach
var $ionicModal = jasmine.createSpyObj('$ionicModal', ['fromTemplateUrl']);
//create a mock of the modal you gonna pass and resolve at your fake resolve
var modalMock = jasmine.createSpyObj('modal', ['show', 'hide']);
$ionicModal.fromTemplateUrl.and.callFake(function(){
return $q.when(modalMock);
});
//in your test
//call scope $digest to trigger the angular digest/apply lifecycle
$scope.$digest();
//expect stuff to happen
expect(modalMock.show).toHaveBeenCalled();
I've multiple controllers in my application, where I have some duplicate code like:
$scope.alert = null;
$scope.addAlert = function (message) {
$scope.alert = { type: 'danger', msg: message };
};
$scope.clearAlerts = function () {
$scope.alert = null;
};
What is the recommended way sharing these scope functions and variables in AngularJS? Using controller inheritance?
Create a one controller and then place common methods inside that controller scope. So that you can use that scope anywhere else and get access to method inside controller.
Controller
app.controller('commonCtrl', function($scope) {
$scope.alert = null;
$scope.addAlert = function(message) {
$scope.alert = {
type: 'danger',
msg: message
};
};
$scope.clearAlerts = function() {
$scope.alert = null;
};
});
Thereafter use scope of this controller by inject it using $controller, and then inside curly brace you could assign common controller scope to the current scope of controller.
Utilization of Common Controller
app.controller('testCtrl', function($scope, $controller) {
//inject comomon controller scope to current scope ,
//below line will add 'commonCtrl' scope to current scope
$controller('commonCtrl', { $scope: $scope });
//common controller scope will be available from here
});
Or more precise way would be using common sharable service, that exposed two method and alert data, you can use this service method by injecting service name inside your controller.
Service
app.service('commonService', function($scope) {
this.alert = null;
this.addAlert = function(message) {
this.alert = {
type: 'danger',
msg: message
};
};
this.clearAlerts = function() {
this.alert = null;
};
});
Utilization of service inside Controller
app.controller('testCtrl', function($scope, commonService) {
console.log(commonService.alert);
commonService.addAlert("Something");
console.log("Updated Alert" + commonService.alert);
});
Hope this has cleared your concept, Thanks.
My own solution for this use case was to define a type of Observer Pattern.
The code was structured in the following way:
var app = angular.module('testModule', []);
app.factory('alertService', ['$timeout', function($timeout){
var alertListeners = [];
this.register = function (listener) {
alertListeners.push(listener);
};
this.notifyAll = function (data) {
for (// each listener in array) {
var listenerObject = alertListeners[i];
try { // do not allow exceptions in individual listeners to corrupt other listener processing
listenerObject.notify(data);
} catch(e) {
console.log(e);
}
}
};
}]).
directive('myAlerts', ['alertService', function(alertService){
var alertDirectiveObserver = function($scope, alertService) {
this.notify = function(data) {
/*
* TO DO - use data to show alert
*/
};
/*
* Register this object as an event Listener. Possibly supply an event key, and listener id to enable more resuse
*/
alertService.register(this);
$scope.on('$destroy', function() {
alertService.unregister(// some listener id);
});
};
return {
restrict: 'A',
template: '<div ng-class="alertClass" ng-show="alertNeeded">{{alertMessage}}</div>',
controller: ['$scope', 'alertService', alertDirectiveObserver],
link: function(scope){
}
}
}]).
controller('alertShowingController', ['$scope', 'alertService', function($scope, alertService){
alertService.notifyAll({'warning', 'Warning alert!!!'})
]);
The alertShowingController is a simple example of how all controllers can simply inject the alertService and generate an event.
My own implementation is more elaborate in that it uses separate event keys to allow the controllers to generate other event notifications.
I could then define a single div that was in a fixed position at the top of the page that would dispay bootstrap alerts.
<div my-alerts ng-repeat="alert in alertList" type="{{alert.type}}" close="closeAlert(alertList, $index)">{{alert.msg}}</div>
I'm trying to understand how to unit test my directive in my situation below.
Basically I'm trying to unit test a directive which has a controller. On the loading of this directive the controller makes a http request by a service which brings some data to the controller again then provides this data to the directive view.
On the scenario below in my understanding I should do:
A $httpBackend to avoid an exception when the http request is done;
Populate the fake data to be able to unit test the directive with diff behaviors
Compile the directive
What I've been trying so far, as you can see, is override the Service with the fake data. What I could not make work so far.
Some doubts come up now.
As you can see in my Controller. I'm providing the whole Service to the view:
$scope.ItemsDataservice = ItemsDataservice;
What makes me believe that my approach to override the Service should work.
My question:
On scenario below I understand that I could override the Service to manipulate the data or even override the controller to manipulate the data by scope.
What's the right thing to do here?
Am I understand wrong?
Am I mixing the unit tests?
In my current unit test code, when I'm applying the fake data(or not), is not make any difference:
ItemsDataservice.items = DATARESULT;
ItemsDataservice.items = null;
Controller:
angular.module('app')
.controller('ItemsCtrl', function ($scope, $log, ItemsDataservice) {
$scope.ItemsDataservice = ItemsDataservice;
$scope.ItemsDataservice.items = null;
$scope.loadItems = function() {
var items = [];
ItemsDataservice.getItems().then(function(resp) {
if (resp.success != 'false') {
for (resp.something ... ) {
items.push({ ... });
};
ItemsDataservice.items = items;
};
}, function(e) {
$log.error('Error', e);
});
};
$scope.loadItems();
});
Service:
angular.module('app')
.service('ItemsDataservice', function ItemsDataservice($q, $http) {
ItemsDataservice.getItems = function() {
var d = $q.defer();
var deffered = $q.defer();
var url = 'http://some-url?someparameters=xxx'
$http.get(url)
.success(function (d) {
deffered.resolve(d);
});
return deffered.promise;
};
return ItemsDataservice;
});
Directive:
angular.module('app')
.directive('items', function () {
return {
templateUrl: '/items.html',
restrict: 'A',
replace: true,
controller: 'ItemsCtrl'
};
});
Unit testing directive:
ddescribe('Directive: Items', function () {
var element, scope, _ItemsDataservice_, requestHandler, httpBackend;
var URL = 'http://some-url?someparameters=xxx';
var DATARESULT = [{ ... }];
// load the directive's module
beforeEach(module('app'));
beforeEach(module('Templates')); // setup in karma to get template from .html
beforeEach(inject(function ($rootScope, ItemsDataservice) {
httpBackend = $httpBackend;
scope = $rootScope.$new();
_ItemsDataservice_ = ItemsDataservice;
requestHandler = httpBackend.when('GET', URL).respond(200, 'ok');
}));
afterEach(function() {
//httpBackend.verifyNoOutstandingExpectation();
//httpBackend.verifyNoOutstandingRequest();
});
it('Show "No Items available" when empty result', inject(function ($compile) {
_ItemsDataservice_.items = null;
element = angular.element('<div data-items></div>');
element = $compile(element)(scope);
scope.$digest();
element = $(element);
expect(element.find('.msg_noresult').length).toBe(1);
}));
it('Should not show "No Items available" when data available ', inject(function ($compile) {
_ItemsDataservice_.items = DATARESULT;
element = angular.element('<div data-items></div>');
element = $compile(element)(scope);
scope.$digest();
element = $(element);
expect(element.find('.msg_noresult').length).toBe(0);
}));
});
I sorted out the problem.
Changed this line:
element = $compile(element)(scope);
To this line:
element = $compile(element.contents())(scope);
The only diff is the jquery method .contents()
I did not get yet why. But it solved.
Update:
Another thing I've just discovered and that was really useful for me.
You can use regular expression on you httpBackend:
httpBackend.whenGET(/.*NameOfThePageXXX\.aspx.*/).respond(200, 'ok');
So, you don't need to worry to use exactly the same parameters etc if you just want to avoid an exception.
For an app I'm using a skeleton that is very similar to https://github.com/angular/angular-seed.
I've tried something like this, in services.js:
'use strict';
/* Services */
angular.module('mApp.services', []).
factory('$exceptionHandler', function () {
return function (exception, cause) {
alert(exception.message);
}
});
This doesn't do anything, it doesn't seem to overwrite the exceptionHandler.
If I try:
var mod = angular.module('mApp', []);
mod.factory('$exceptionHandler', function() {
return function(exception, cause) {
alert(exception);
};
});
It overwrite the whole app.
How can I properly overwrite the exceptionHandler if I am using a skeleton similar to the default angular app?
It's hard to know for certain without seeing the rest of your app, but I'm guessing angular.module('myApp').factory( ... will work. If you leave out the second parameter (,[]) angular will retrieve an existing module for further configuring. If you keep it angular will create a new module.
try this example
http://jsfiddle.net/STEVER/PYpdM/
var myApp = angular.module('myApp', ['ng']).provider({
$exceptionHandler: function(){
var handler = function(exception, cause) {
alert(exception);
//I need rootScope here
};
this.$get = function() {
return handler;
};
}
});
myApp.controller('MyCtrl', function($scope, $exceptionHandler) {
console.log($exceptionHandler);
throw "Fatal error";
});