I have this problem using resolve with component & ui-router, the data "after" resolving promise are "undefined" in a controller
Service:
class userService {
constructor ($http, ConfigService, authService) {
this.$http = $http;
this.API_URL = `${ConfigService.apiBase}`;
this.authService = authService;
}
testAuth () {
return this.$http.get(this.API_URL + '/test-auth')
}
getCollaboratores () {
return this.$http.get(this.API_URL + '/collaboratores').then(
(resolve) => { // promise resolve
console.log('Success',resolve.data);
}
)
}
getAccount () {
var config = {
headers: { "X-Shark-CollaboratoreId" : "1"}
};
return this.$http.get(this.API_URL + '/accounts' + '/' + 1, config).then(
(resolve) => { // promise resolve
console.log('Success',resolve.data);
}
)
}
Module/Component/Routing:
.config(($stateProvider, $urlRouterProvider) => {
"ngInject";
$urlRouterProvider.otherwise('/');
$stateProvider
.state('core', {
redirectTo: 'dashboard',
url: '/core',
component: 'core',
resolve: {
userdata: (userService) => {
return userService.getCollaboratores();
},
accdata: (userService) => {
return userService.getAccount();
}
}
});
})
Controller:
let self;
class CoreController {
constructor($state,userService,authService,userdata,accdata) {
this.name = 'core';
this.$state = $state;
this.userService = userService;
this.authService = authService;
this.userdata = userdata;
this.accdata = accdata;
console.log('name',this.name);
self = this;
console.log('userdata',self);
}
}
CoreController.$inject = ['$state','userService', 'authService','userdata','accdata'];
export default CoreController;
After injecting in the controller the object "resolved" by promise after "http" call
this.userdata = userdata;
this.accdata = accdata;
are undefined!!!
where is the bug.??
thanks a lot...
Change the getCollaboratores function to below :
getCollaboratores () {
return this.$http.get(this.API_URL + '/collaboratores').then(
(resolve) => { // promise resolve
console.log('Success',resolve.data);
return resolve;
});
}
Do the same with other one getAccount (i.e inside the success callback return resolve).
This will solve your problem.
Reason is once you chain success callbacks, 1st callback as to return the something which can be the arguments for the 2nd callback. Since the success callback in service was not returning anything(default return value of a function in js is undefined), hence resolved value was not available in the controller.
What version of angularjs are you using?
Remember that in 1.6, a then callback now receives 4 parameters: data, status, headers, config and statusText.
In this scenario, a resolve.data could result in an undefined value.
More info on docs: https://docs.angularjs.org/api/ng/service/$http
I see that you relate to a component in your state not to a controller, so you should create a component and bind the resolved values to it.
angular
.module('app')
.component('core', {
templateUrl: 'app/app.html',
controller: CoreController,
controllerAs: 'vm',
bindings: {
userdata: '<',
accdata: '<'
}
});
And to get the resolve data in your component controller do this:
const vm = this;
vm.$onInit = () => {
console.log('userdata: ', vm.userdata);
console.log('accdata: ', vm.accdata);
}
Related
I'm working on a very modularized project and currently I'm building an Element Directive which changes templateUrl based on user login/logout.
To do that, I'm trying to execute a Factory's Function inside templateUrl. That particular functions calls another method from a JWT Factory and returns true if the user is logged or false if not.
Then, If in my templateUrl I receive true, I pick a certain url, if false another one.
But, sadly, I receive the following error:
[$http:badreq] Http request configuration url must be a string. Received: {}
All $log.log() print the correct result.
Of course, it won't render nor page1 nor page2
Directive
(function () {
'use strict';
angular
.module('myApp')
.directive('myDirective', ['SessionCheckerFactory', function (SessionCheckerFactory) {
return {
restrict: 'E',
templateUrl : function(){
return SessionCheckerService.checkSession().then( function (res) {
console.log(res);//true
return res ? 'app/page1.html' : 'app/page2.html';
});
},
controller : 'MyController',
controllerAs : 'myCtrl',
bindToController : true
};
}]);
})();
SessionCheckerFactory
(function () {
'use strict';
angular
.module('myApp')
.factory('SessionCheckerFactory', function (AuthTokenFactory) {
function checkSession() {
return AuthTokenFactory.isAuth();
}
return {
checkSession: checkSession
}
});
})();
AuthTokenFactory
(function() {
'use strict';
angular.module('myApp')
.factory('AuthTokenFactory', function AuthTokenFactory(store, $cookies) {
//Takes user's info from LocalStorage, if not empty returns a String with encoded string informations
function getToken() {
if (store.get(key)) {
return store.get(key);
}
//Takes user's info from cookie
var token = $cookies.get('token', {path: '/'});
store.set(key, token);
return token;
}
//If getToken is empty returns false, else true
function isAuth() {
return Promise.resolve(Boolean(getToken()));
}
return {
isAuth : isAuth,
getToken : getToken
}
});
})();
I read around that this problem is usually generated by $http requests, but that's not my case. I didn't find any solution to that so far.
How can I fix this?
Thanks in advance.
Then, If in my templateUrl I receive true, I pick a certain url, if false another one.
Actually you don't. If you receive true, you pick one url, if some truthy value, another url, and if something falsy then you don't pick any url:
if (res) {
if (res === true) {
return resolve('app/page1.html');
} // else
return resolve('app/page2.html');
}
// else return undefined;
You probably want
templateUrl : function(){
return SessionCheckerFactory.checkSession().then(function (res) {
if (res) {
return 'app/page1.html';
} else {
return 'app/page2.html';
}
})
},
I managed to fix the issue using a link function and $templateRequest
Directive
link: function (scope, element) {
SessionCheckerService.renderTemplate().then(function (temp){
$templateRequest(temp).then(function (requestedTemplate) {
element.html(requestedTemplate);
$compile(element.contents())(scope);
});
});
}
Factory
var templateConfig = './app/config/templates.config.json';
function getTemplate(){
return $http.get(templateConfig)
.then(function(templates) {
return templates.data;
});
}
function checkSession() {
return Promise.resolve(AuthTokenFactory.isAuth());
}
function whichTemplate(template, result) {
var myTemplate = '';
if(result){
myTemplate = template.logIn;
} else {
myTemplate = template.logOut;
}
if(myTemplate){
return Promise.resolve(myTemplate);
}
}
//Chaining the methods and returning the correct template
function renderTemplate() {
return new Promise(function (resolve) {
checkSession().then(function(isAuth){
getTemplate().then( function(templates){
whichTemplate(templates, isAuth).then( function (temp) {
return resolve(temp);
});
});
});
});
}
return {
renderTemplate : renderTemplate
}
Templates Config
{
"logOut" : "app/page1.html",
"logIn" : "app/page2.html"
}
I hope It'll be helpful.
I'm trying to unit test a function within my controller but am unable to get a $scope variable to be testable. I'm setting the variable in my controller's .then() and want to unit test to make sure this is set appropriately when it hits the .then block.
My test controller code:
function submit() {
myService.submit().then(function(responseData){
if(!responseData.errors) {
$scope.complete = true;
$scope.details = [
{
value: $scope.formattedCurrentDate
},
{
value: "$" + $scope.premium.toFixed(2)
},
];
} else {
$scope.submitError = true;
}
});
}
Where this service call goes is irrelevant. It will return JSON with action: 'submitted', 'response' : 'some response'. The .then() checks if errors are present on responseData, and if not it should set some details. These $scope.details are what I'm trying to test in my unit test below:
it('should handle submit details', function () {
var result;
var premium = 123.45;
var formattedCurrentDate = "2016-01-04";
var promise = myService.submit();
mockResponse = {
action: 'submitted',
response: 'some response'
};
var mockDetails = [
{
value: formattedCurrentDate
},
{
value: "$"+ premium.toFixed(2)
}
];
//Resolve the promise and store results
promise.then(function(res) {
result = res;
});
//Apply scope changes
$scope.$apply();
expect(mockDetails).toEqual(submitController.details);
});
I'm receiving an error that $scope.details is undefined. I'm not sure how to make the test recognize this $scope data changing within the controller.
Before each and other functions in my unit test:
function mockPromise() {
return {
then: function(callback) {
if (callback) {
callback(mockResponse);
}
}
}
}
beforeEach(function() {
mockResponse = {};
module('myApp');
module(function($provide) {
$provide.service('myService', function() {
this.submit = jasmine.createSpy('submit').and.callFake(mockPromise);
});
});
inject(function($injector) {
$q = $injector.get('$q');
$controller = $injector.get('$controller');
$scope = $injector.get('$rootScope');
myService = $injector.get('myService');
submitController = $controller('myController', { $scope: $scope, $q : $q, myService: myService});
});
});
How do I resolve the promise within my unit test so that I can $scope.$digest() and see the $scope variable change?
You should look how to test promises with jasmine
http://ng-learn.org/2014/08/Testing_Promises_with_Jasmine_Provide_Spy/
using a callFake would do what you try to mock
spyOn(myService, 'submit').and.callFake(function() {
return {
then: function(callback) { return callback(yourMock); }
};
});
i used a tutorial to create a angularfire chat app. it is a standalone app that uses ui-router. I integrated it succssfully as a view in my app but that is not practical. I need to be able to use the chat on any view I am at. I am stuck at moving a resolve function to a controller. I have read some docs and I believe it is returning a promise that I need to resolve in the controller. the link to the tutorial is here.
tutorial
here is the ui-router I am trying to get away from
.state('channels.direct', {
url: '/{uid}/messages/direct',
templateUrl: 'views/chat/_message.html',
controller: 'MessageController',
controllerAs: 'messageCtrl',
resolve: {
messages: function ($stateParams, MessageService, profile) {
return MessageService.forUsers($stateParams.uid, profile.$id).$loaded();
},
channelName: function ($stateParams, UserService) {
return UserService.all.$loaded().then(function () {
return '#' + UserService.getDisplayName($stateParams.uid);
});
}
}
})
The message service
var channelMessagesRef = new Firebase(AppConstant.FirebaseUrl + 'channelMessages');
var userMessagesRef = new Firebase(AppConstant.FirebaseUrl + 'userMessages')
return {
forChannel: function (channelId) {
return $firebaseArray(channelMessagesRef.child(channelId));
},
forUsers: function (uid1, uid2) {
var path = uid1 < uid2 ? uid1 + '/' + uid2 : uid2 + '/' + uid1;
return $firebaseArray(userMessagesRef.child(path));
}
};
the user service
var usersRef = new Firebase(AppConstant.FirebaseUrl + 'users');
var connectedRef = new Firebase(AppConstant.FirebaseUrl + '.info/connected');
var users = $firebaseArray(usersRef);
return {
setOnline: function (uid) {
var connected = $firebaseObject(connectedRef);
var online = $firebaseArray(usersRef.child(uid + '/online'));
connected.$watch(function () {
if (connected.$value === true) {
online.$add(true).then(function (connectedRef) {
connectedRef.onDisconnect().remove();
});
}
});
},
getGravatar: function (uid) {
return '//www.gravatar.com/avatar/' + users.$getRecord(uid).emailHash;
},
getProfile: function (uid) {
return $firebaseObject(usersRef.child(uid));
},
getDisplayName: function (uid) {
return users.$getRecord(uid).displayName;
},
all: users
};
here is what I have so far in the controller
$scope.directMessage = function (uid) {
UserService.all.$loaded().then(function () {
$scope.selectedChatUser = '#' + UserService.getDisplayName(uid);
});
$scope.selectedChatUserMessages = MessageService.forUsers(uid, profile.$id).$loaded();
};
I am returning the
$scope.selectedChatUser
fine. the issue is with the Message Service
this is the what i am currently returning from the message service
$$state: Object
__proto__: Promise
how do i resolve this?
You're trying to return from inside a promise in your channelName function.
The object you're getting back is an unresolved promise. You want the resolved data from the promise injected into your controller.
You need to create a to return from this function.
.state('channels.direct', {
url: '/{uid}/messages/direct',
templateUrl: 'views/chat/_message.html',
controller: 'MessageController',
controllerAs: 'messageCtrl',
resolve: {
messages: function ($stateParams, MessageService, profile) {
return MessageService.forUsers($stateParams.uid, profile.$id).$loaded();
},
channelName: function ($stateParams, UserService, $q) {
// create a deferred for this function
var deferred = $q.defer();
// load async data with UserService.all's promise
UserService.all.$loaded()
.then(function () {
var name = UserService.getDisplayName($stateParams.uid);
deferred.resolve(name);
});
// return promise
return deferred.promise;
}
}
})
However in getDisplayName I would just recommend returning back the object rather than just the name, as the entire set is synchronized by Firebase.
I have myService that uses myOtherService, which makes a remote call, returning promise:
angular.module('app.myService', ['app.myOtherService'])
.factory('myService', [
myOtherService,
function(myOtherService) {
function makeRemoteCall() {
return myOtherService.makeRemoteCallReturningPromise();
}
return {
makeRemoteCall: makeRemoteCall
};
}
])
To make a unit test for myService I need to mock myOtherService, such that its makeRemoteCallReturningPromise method returns a promise. This is how I do it:
describe('Testing remote call returning promise', function() {
var myService;
var myOtherServiceMock = {};
beforeEach(module('app.myService'));
// I have to inject mock when calling module(),
// and module() should come before any inject()
beforeEach(module(function ($provide) {
$provide.value('myOtherService', myOtherServiceMock);
}));
// However, in order to properly construct my mock
// I need $q, which can give me a promise
beforeEach(inject(function(_myService_, $q){
myService = _myService_;
myOtherServiceMock = {
makeRemoteCallReturningPromise: function() {
var deferred = $q.defer();
deferred.resolve('Remote call result');
return deferred.promise;
}
};
}
// Here the value of myOtherServiceMock is not
// updated, and it is still {}
it('can do remote call', inject(function() {
myService.makeRemoteCall() // Error: makeRemoteCall() is not defined on {}
.then(function() {
console.log('Success');
});
}));
As you can see from the above, the definition of my mock depends on $q, which I have to load using inject(). Furthermore, injecting the mock should be happening in module(), which should be coming before inject(). However, the value for the mock is not updated once I change it.
What is the proper way to do this?
I'm not sure why the way you did it doesn't work, but I usually do it with the spyOn function. Something like this:
describe('Testing remote call returning promise', function() {
var myService;
beforeEach(module('app.myService'));
beforeEach(inject( function(_myService_, myOtherService, $q){
myService = _myService_;
spyOn(myOtherService, "makeRemoteCallReturningPromise").and.callFake(function() {
var deferred = $q.defer();
deferred.resolve('Remote call result');
return deferred.promise;
});
}
it('can do remote call', inject(function() {
myService.makeRemoteCall()
.then(function() {
console.log('Success');
});
}));
Also remember that you will need to make a $digest call for the then function to be called. See the Testing section of the $q documentation.
------EDIT------
After looking closer at what you're doing, I think I see the problem in your code. In the beforeEach, you're setting myOtherServiceMock to a whole new object. The $provide will never see this reference. You just need to update the existing reference:
beforeEach(inject( function(_myService_, $q){
myService = _myService_;
myOtherServiceMock.makeRemoteCallReturningPromise = function() {
var deferred = $q.defer();
deferred.resolve('Remote call result');
return deferred.promise;
};
}
We can also write jasmine's implementation of returning promise directly by spy.
spyOn(myOtherService, "makeRemoteCallReturningPromise").andReturn($q.when({}));
For Jasmine 2:
spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when({}));
(copied from comments, thanks to ccnokes)
describe('testing a method() on a service', function () {
var mock, service
function init(){
return angular.mock.inject(function ($injector,, _serviceUnderTest_) {
mock = $injector.get('service_that_is_being_mocked');;
service = __serviceUnderTest_;
});
}
beforeEach(module('yourApp'));
beforeEach(init());
it('that has a then', function () {
//arrange
var spy= spyOn(mock, 'actionBeingCalled').and.callFake(function () {
return {
then: function (callback) {
return callback({'foo' : "bar"});
}
};
});
//act
var result = service.actionUnderTest(); // does cleverness
//assert
expect(spy).toHaveBeenCalled();
});
});
You can use a stubbing library like sinon to mock your service. You can then return $q.when() as your promise. If your scope object's value comes from the promise result, you will need to call scope.$root.$digest().
var scope, controller, datacontextMock, customer;
beforeEach(function () {
module('app');
inject(function ($rootScope, $controller,common, datacontext) {
scope = $rootScope.$new();
var $q = common.$q;
datacontextMock = sinon.stub(datacontext);
customer = {id:1};
datacontextMock.customer.returns($q.when(customer));
controller = $controller('Index', { $scope: scope });
})
});
it('customer id to be 1.', function () {
scope.$root.$digest();
expect(controller.customer.id).toBe(1);
});
using sinon :
const mockAction = sinon.stub(MyService.prototype,'actionBeingCalled')
.returns(httpPromise(200));
Known that, httpPromise can be :
const httpPromise = (code) => new Promise((resolve, reject) =>
(code >= 200 && code <= 299) ? resolve({ code }) : reject({ code, error:true })
);
Honestly.. you are going about this the wrong way by relying on inject to mock a service instead of module. Also, calling inject in a beforeEach is an anti-pattern as it makes mocking difficult on a per test basis.
Here is how I would do this...
module(function ($provide) {
// By using a decorator we can access $q and stub our method with a promise.
$provide.decorator('myOtherService', function ($delegate, $q) {
$delegate.makeRemoteCallReturningPromise = function () {
var dfd = $q.defer();
dfd.resolve('some value');
return dfd.promise;
};
});
});
Now when you inject your service it will have a properly mocked method for usage.
I found that useful, stabbing service function as sinon.stub().returns($q.when({})):
this.myService = {
myFunction: sinon.stub().returns( $q.when( {} ) )
};
this.scope = $rootScope.$new();
this.angularStubs = {
myService: this.myService,
$scope: this.scope
};
this.ctrl = $controller( require( 'app/bla/bla.controller' ), this.angularStubs );
controller:
this.someMethod = function(someObj) {
myService.myFunction( someObj ).then( function() {
someObj.loaded = 'bla-bla';
}, function() {
// failure
} );
};
and test
const obj = {
field: 'value'
};
this.ctrl.someMethod( obj );
this.scope.$digest();
expect( this.myService.myFunction ).toHaveBeenCalled();
expect( obj.loaded ).toEqual( 'bla-bla' );
The code snippet:
spyOn(myOtherService, "makeRemoteCallReturningPromise").and.callFake(function() {
var deferred = $q.defer();
deferred.resolve('Remote call result');
return deferred.promise;
});
Can be written in a more concise form:
spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue(function() {
return $q.resolve('Remote call result');
});
I am learning how to use resolve from an example, and applying it on to my Todo script.
Then I realised an issue, that the example is only showing me how to resolve GET call to get me the Todo List when I first visit this route.
However, in the same route same page I have an Add button to POST new todo item, also a Clear button to DELETE completed items.
Looking at my $scope.addTodo = function() { and $scope.clearCompleted = function () { I want to Resolve my TodoList again after the action. How can I do that?
Here is my code. In my code, the initial resolve: { todos: TodosListResl } is working, it hits TodosListResl function and produces the promise. However, I don't know what to do with addTodo and clearComplete when I want to resolve the todo list again.
main.js
var todoApp = angular.module('TodoApp', ['ngResource', 'ui']);
todoApp.value('restTodo', 'api/1/todo/:id');
todoApp.config(function ($locationProvider, $routeProvider) {
$routeProvider.when("/", { templateUrl: "Templates/_TodosList.html",
controller: TodosListCtrl, resolve: { todos: TodosListResl } });
$routeProvider.otherwise({ redirectTo: '/' });
});
//copied from example, works great
function TodoCtrl($scope, $rootScope, $location) {
$scope.alertMessage = "Welcome";
$scope.alertClass = "alert-info hide";
$rootScope.$on("$routeChangeStart", function (event, next, current) {
$scope.alertMessage = "Loading...";
$scope.alertClass = "progress-striped active progress-warning alert-info";
});
$rootScope.$on("$routeChangeSuccess", function (event, current, previous) {
$scope.alertMessage = "OK";
$scope.alertClass = "progress-success alert-success hide";
$scope.newLocation = $location.path();
});
$rootScope.$on("$routeChangeError",
function (event, current, previous, rejection) {
alert("ROUTE CHANGE ERROR: " + rejection);
$scope.alertMessage = "Failed";
$scope.alertClass = "progress-danger alert-error";
});
}
//also copied from example, works great.
function TodosListResl($q, $route, $timeout, $resource, restTodo) {
var deferred = $q.defer();
var successCb = function(resp) {
if(resp.responseStatus.errorCode) {
deferred.reject(resp.responseStatus.message);
} else {
deferred.resolve(resp);
}
};
$resource(restTodo).get({}, successCb);
return deferred.promise;
}
//now, problem is here in addTodo and clearCompleted functions,
//how do I call resolve to refresh my Todo List again?
function TodosListCtrl($scope, $resource, restTodo, todos) {
$scope.src = $resource(restTodo);
$scope.todos = todos;
$scope.totalTodos = ($scope.todos.result) ? $scope.todos.result.length : 0;
$scope.addTodo = function() {
$scope.src.save({ order: $scope.neworder,
content: $scope.newcontent,
done: false });
//successful callback, but how do I 'resolve' it?
};
$scope.clearCompleted = function () {
var arr = [];
_.each($scope.todos.result, function(todo) {
if(todo.done) arr.push(todo.id);
});
if (arr.length > 0) $scope.src.delete({ ids: arr });
//successful callback, but how do I 'resolve' it?
};
}
I think you're missing the point of resolve. The point of resolve is to " delay route change until data is loaded. In your case, you are already on a route, and you want to stay on that route. But, you want to update the todos variable on the successful callback. In this case, you don't want to use resolve. Instead, just do what needs to be done. For example
$scope.addTodo = function() {
$scope.src.save({ order: $scope.neworder,
content: $scope.newcontent,
done: false }, function () {
todos.push({ order: $scope.neworder,
content: $scope.newcontent,
done: false });
});
//successful callback, but how do I 'resolve' it?
};
As a side point, I noticed you're using _ most likely from the Underscore library. You don't need to use another library for that because Angular already has $angular.forEach().