I have two controllers and a service. In the first controller I have subscribed to an event to do some stuff. The second controller preforms some actions and when it is done, broadcasts the event. Please see the example below, the timeout is just for emulation of long running actions. I would like to test that hasLoaded is set to true using Jasmine 2.0 Please advise.
var myApp = angular.module('MyApp', []);
myApp.controller('MyCtrl1', ['$scope', 'myService', function($scope, myService) {
$scope.hasLoaded = false;
$scope.fileName = '';
myService.onLoaded($scope, function(e, data){
// I want to test the following two lines, in the really the code here is much more complex
$scope.fileName = data.fileName;
$scope.hasLoaded = true;
});
}]);
myApp.controller('MyCtrl2', ['$rootScope', '$scope', '$timeout', 'myService', function($rootScope, $scope, $timeout, myService) {
$scope.isLoading = false;
$scope.title = 'Click me to load';
$scope.load = function(){
$scope.isLoading = true;
$scope.title = 'Loading, please wait...';
$timeout(function() {
$rootScope.$emit('loaded', { fileName: 'test.txt'});
}, 1000);
};
myService.onLoaded($scope, function(){
$scope.hasLoaded = true;
});
}]);
myApp.service('myService', ['$rootScope', function ($rootScope) {
this.onLoaded = function(scope, callback) {
var handler = $rootScope.$on('loaded', callback);
scope.$on('$destroy', handler);
};
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
<div ng-app="MyApp">
<div ng-controller="MyCtrl1">
<div ng-show="hasLoaded">{{fileName}} loaded !!!</div>
</div>
<div ng-controller="MyCtrl2">
<button ng-click="load()" ng-hide="hasLoaded" ng-disabled="isLoading" ng-bind="title"></button>
</div>
</div>
UPDATE: I have added parameter into the broadcast call to make it more closer to my case.
You really should be testing each of your pieces (controllers and services) separately. In your case, the tests for the controller that sets the hasLoaded properly really just needs to test that your register with the service correctly and that the callback does what you expect:
it("should register with the service and do the right thing when the callback is executed", inject(function ($controller, $rootScope, myService) {
var $scope = $rootScope.$new();
spyOn(myService, 'onLoaded').and.callThrough();
var ctrl = $controller('MyCtrl1', {$scope: $scope, myService: myService});
$scope.$apply();
//verify that the controller registers its scope with the service
expect(myService.onLoaded).toHaveBeenCalledWith($scope, jasmine.any(Function));
//now call the callback that was registered to see if it sets the property correctly
var mockData = {
fileName: 'some file name'
};
myService.onLoaded.calls.argsFor(0)[1]('loaded', mockData);
expect($scope.hasLoaded).toBeTruthy();
expect($scope.fileName).toBe("some file name");
}));
Then write tests for your service and other controller separately.
Related
I have two controllers, when user press the button to load SeconvView, the FirctCtrl sends event to the SecondCtrl via $rootScope.$broadcast.
// FirstCtrl.js
app.controller('FirstCtrl', ['$scope', function($rootScope, $scope) {
// ...
$scope.emitEvent = function(){
console.log("Emitting event");
$rootScope.$broadcast("EventName", "event data");
}
}]);
// SecondCtrl.js
app.controller('SecondCtrl', ['$scope', function($rootScope, $scope) {
console.log("Hola from SecondCtrl");
$rootScope.$on("EventName", function(event, data){
console.log(data);
});
}]);
Log output:
Emitting event
Hola from SecondCtrl
So, the problem is that SecndCtrl can't catch the event. I think that this is because the events sends before SecondCtrl is loaded.
With little timeout i can solve this problem, but ... common, i must use timeout for this :/
// FirstCtrl.js
app.controller('FirstCtrl', ['$scope', function($rootScope, $scope) {
// ...
$scope.emitEvent = function(){
$timeout(function() {
console.log("Emitting event");
$rootScope.$broadcast("EventName", "event data");
},100);
}
}]);
Log output:
Hola from SecondCtrl
Emitting event
event data
Some better way to solve this ?
Passing data between controllers is one of the main uses of a service / factory instead
// FirstCtrl.js
app.controller('FirstCtrl', ['$scope', function(MyFactory, $scope) {
// ...
$scope.emitEvent = function(){
console.log("Emitting event");
MyFactory.storedvalue = "event data";
}
}]);
// SecondCtrl.js
app.controller('SecondCtrl', ['$scope', function(MyFactory, $scope) {
console.log("Hola from SecondCtrl");
console.log(MyFactory.storedvalue);
});
}]);
I have a controller that starts like this (simplified for this question):
angular.module('myApp.controllers')
.controller('MyController', ['$scope', '$routeParams', 'MyService',
function ($scope, $routeParams, MyService) {
MyService.fetchWithId($routeParams.id).then(function(model) {
$scope.model = model;
});
Which is fine, but then in many places throughout the controller, I have functions that are referred to in the view that refer to the model ...
$scope.someFunctionMyViewNeeds = function() {
return $scope.model.someModelAttribute;
};
Since these often run before the fetch completes, I end up with errors like "cannot read property of undefined" when the view tries to see someModelAttribute.
So far, I've tried three things:
// before the fetch
$scope.model = new Model();
...but I really don't want a new model, and in some cases, cannot complete initialization out of the blue without other dependences.
Another idea is to litter the code with defense against the unready model, like:
return ($scope.model)? $scope.model.someModelAttribute : undefined;
... but that's a lot of defense all over the code for a condition that only exists while the fetch completes.
My third idea has been to "resolve" the model in the route provider, but I don't know how to do that and get at the $routeParams where parameter to fetch the model is kept.
Have I missed a better idea?
Try this if you want to use resolve.
var app = angular.module('app', ['ngRoute']);
app.config(function ($routeProvider) {
$routeProvider.when('/things/:id', {
controller: 'ThingsShowController',
resolve: {
model: function ($routeParams, MyService) {
return MyService.fetchWithId(+$routeParams.id);
}
},
template: '<a ng-href="#/things/{{model.id}}/edit">Edit</a>'
});
$routeProvider.when('/things/:id/edit', {
controller: 'ThingsEditController',
resolve: {
model: function ($routeParams, MyService) {
return MyService.fetchWithId(+$routeParams.id);
}
},
template: '<a ng-href="#/things/{{model.id}}">Cancel</a>'
});
});
// Just inject the resolved model into your controllers
app.controller('ThingsShowController', function ($scope, model) {
$scope.model = model;
});
app.controller('ThingsEditController', function ($scope, model) {
$scope.model = model;
});
// The rest is probably irrelevant
app.factory('Model', function () {
function Model(attributes) {
angular.extend(this, attributes);
}
return Model;
});
app.service('MyService', function ($q, Model) {
this.fetchWithId = function (id) {
var deferred = $q.defer();
deferred.resolve(new Model({ id: id }));
return deferred.promise;
};
});
// Just to default where we are
app.run(function ($location) {
$location.path('/things/123');
});
app.run(function ($rootScope, $location) {
$rootScope.$location = $location;
});
// Because $routeParams does not work inside the SO iframe
app.service('$routeParams', function () {this.id = 123;});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.9/angular.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.9/angular-route.min.js"></script>
<div ng-app="app">
<div>Route: {{$location.path()}}</div>
<div ng-view=""></div>
</div>
I'm trying to make a basic data pulling using a service and print it on screen when I have data, but something is not working.
My service:
mymodule.factory('MyService', function($http, $q) {
var service = {
getData: function() {
var dfd = $q.defer();
$http.get(apiServerPath).success(function (data) {
dfd.resolve(data);
});
return dfd.promise;
}
}
return service
}
My Controller:
mymodule.controller('myCtrl', ['$scope', 'MyService', function($scope, MyService) {
$scope.myvar = MyService.getData();
}
HTML
<div> {{myvar}} </div>
What I can see from the browser console -
The myvar object turn into a promise object
The success function is being called and 'data' has valid data in it
And for my question and issue - the controller's variable does not change when the defer object is resolving - why?
Promises are no longer auto-unwrapped as of Angular 1.2. In your controller do the following:
mymodule.controller('myCtrl', ['$scope', 'MyService', function($scope, MyService) {
MyService.getData().then(function success(data) {
$scope.myvar = data;
});
}
I have a partial view that is using angular. How do I change a variable in the MainController from PartialController? I am not sure how to create the interdependence...
angularApp.controller('MainController', ['$scope', '$http', '$compile', function MainController($scope, $http, $compile) {
$scope.myVariable = "0";
//Had the following before refactoring due to repetitive code.
//Code now in PartialController
//$scope.searchData = function ($event) {
// //code
// $scope.myVariable = "1";
//}
}]);
angularApp.controller('PartialController', ['$scope', '$http', '$compile', function PartialController($scope, $http, $compile) {
$scope.searchData = function ($event) {
//code
$scope.myVariable = "1";
}
}]);
For sake of completeness, there are at least 3 ways:
With a service as #tymeJV suggested (BEST answer)
app.factory('dataStore', function () {
var dataStore = {};
return dataStore;
});
app.controller('ParentCtrl', function($scope, dataStore) {
$scope.dataStore = dataStore;
$scope.dataStore.foo = 'bar';
});
app.controller('ChildCtrl', function($scope, dataStore) {
dataStore.foo = 'not bar anymore';
});
With an object reference on the parent scope (A bit hackish)
app.controller('ParentCtrl', function($scope) {
$scope.data = {
foo: 'bar'
};
});
app.controller('ChildCtrl', function($scope) {
$scope.data.foo = 'not bar anymore';
});
With $parent (equally hackish)
app.controller('ParentCtrl', function($scope) {
$scope.foo = 'bar';
});
app.controller('ChildCtrl', function($scope) {
$scope.$parent.foo = 'not bar anymore';
});
Why are #2 and #3 hackish?
Because they create a dependency in your ChildCtrl of having it always be a child of the ParentCtrl... otherwise it will break.
So why include #2 and #3 at all?
For a few reasons:
Directives can have controllers, and required parent directives. Because of this, there are cases where you can "safely" use $parent or scope inheritance because you'll always know that ChildCtrl has ParentCtrl as a parent.
Sometimes you just need to hack something together.
As I said, for the sake of completeness.
This is a prime use for a service that can be injected to controllers when you need it and pull data from it:
app.factory("myService", function() {
var myVariable = null;
return {
get: function() {
return myVariable;
},
set: function(value) {
myVariable = value;
}
}
});
//Inject
angularApp.controller('MainController', ['$scope', '$http', 'myService', '$compile', function MainController($scope, $http, $compile, myService) {
myService.set(3);
});
tymeJV's answer is correct and is probably best practice in this case. I believe the reason it wasn't working in your example is because in Javascript, primitives (strings, numbers, booleans) are passed by value, whereas objects are passed by reference.
i.e. if you had $scope.obj.myVariable=1 in your main controller and edit $scope.obj.myVariable in your child controller, you should see the new value in both. (this is kinda #2 in blesh's answer). This is a common source of "bugs" in Angular so it's good to be aware of it.
I have 2 controllers defined:
var myApp = angular.module('nestedControllersModule',[]);
myApp.controller('ParentController', ['$scope', function($scope) {
}]);
myApp.controller('ChildController', ['$scope', '$injector', function($scope, $injector) {
$injector.invoke(ParentController, this, {$scope: $scope});
}]);
This gives: ReferenceError: ParentController is not defined.
This code works only if ParentController is defined as:
function ParentController($scope) {}
I am trying to inject the parent in the child as then I can inherit the common functions defined in the parent.
var myApp = angular.module('nestedControllersModule',[]);
myApp.controller('ParentController', ['$scope', function($scope) {
$scope.name = 'ParentName';
$scope.Type = 'ParentType';
$scope.clickme = function() {
alert('This is parent controller "ParentController" calling');
}
}]);
myApp.controller('ChildController', ['$scope', '$injector', '$ParentController', function($scope, $injector, $ParentController) {
$injector.invoke(ParentController, this, {$scope: $scope});
$scope.name = 'Child';
}]);
myApp.controller('ParentController', ['$scope', function($scope) {
}]);
myApp.controller('ChildController', ['$scope', 'ParentController', function($scope, ParentController) {
// ok now you have ParentController
}]);
But I think you need to use Services to share data/functions between Controllers or using PubSub model:
What's the correct way to communicate between controllers in AngularJS?
This reduces coupling between parts of your app.
This is a basic workaround to achieve what you're after:
var myApp = angular.module('nestedControllersModule',[]);
myApp.factory('ParentControllerFactory', function () {
function ParentControllerFactory($scope) {
$scope.name = 'ParentName';
$scope.Type = 'ParentType';
$scope.clickme = function() {
alert('This is parent controller "ParentController" calling');
}
}
return (ParentControllerFactory);
})
.controller('ParentController', ['$scope', '$injector', 'ParentControllerFactory', function ($scope, $injector, ParentControllerFactory) {
$injector.invoke(ParentControllerFactory, this, {
$scope: $scope
});
}])
.controller('ChildController', ['$scope', '$injector', 'ParentControllerFactory', function ($scope, $injector, ParentControllerFactory) {
$injector.invoke(ParentControllerFactory, this, {
$scope: $scope
});
}]);
I say workaround because it's probably worthwhile looking into properly implementing a service to manage any commonality as previously mentioned (or better yet, splitting commonality into directives, clickme for example is a good candidate)
...also note that $injector.invoke(ParentControllerFactory as it is above will most likely chuck a hissy fit if/when you minify your scripts later on, so be careful where and how it used.
Consider using the Mixin pattern possible by using the $controller service.
In your example, you would replace the $injector service with the $controller service:
var myApp = angular.module('nestedControllersModule',[]);
myApp.controller('ParentController', ['$scope', function($scope) {
$scope.name = 'ParentName';
$scope.Type = 'ParentType';
$scope.clickme = function() {
alert('This is parent controller "ParentController" calling');
}
}]);
myApp.controller('ChildController', ['$scope', '$controller', '$ParentController', function($scope, $controller, $ParentController) {
$controller('ParentController',{$scope: $scope})
$scope.name = 'Child';
}]);
This is a good overview of using the $controller service:
http://vadimpopa.com/split-large-angularjs-controllers-using-the-mixin-pattern/