This question already has answers here:
How do I return the response from an asynchronous call?
(41 answers)
Closed 7 years ago.
I have a problem with angularjs.
I want to return values within the Facebook SDK functions but will not let me get them in variables.
The "perfil_nombre" and "perfil_foto" the variables returned as "undefined" and wish to send to the scope.
Any chance? I'm totally lost.
Excuse my bad English.
login.controller('inicio_ctrl', function($scope, $http, $timeout, $window)
{
var laspaginas;
var response;
var perfil_foto;
var perfil_nombre;
var resp;
$scope.FBLogin = function()
{
FB.login(function(response)
{
if(response.authResponse)
{
FB.api('/me', function(response)
{
perfil_nombre = response.name; //<<<<<-------- good
FB.api('/me/picture?redirect=false', function(resp)
{
perfil_foto = resp.data.url; //<<<<<-------- good
});
});
}
else
{
console.log('User cancelled login or did not fully authorize.');
}
},
{ scope: 'email, public_profile, manage_pages,publish_pages,read_insights,user_friends, publish_actions'});
$scope.perfil_foto = perfil_foto; //<<<<<-------- undefined
$scope.perfil_nombre = perfil_nombre; //<<<<<-------- undefined
});
Angular is designed as an MVC or MV* framework. As such it is generally better to separate your logic into a service and then inject the service into the controller. This way you can prevent negative interactions between Angular and other frameworks. It can be difficult to anticipate how Angular will interact with outside libraries.
The simplest way to do this is make your function work using just javascript and then wrap it in a .factory or .service.
For example
(function(){
'use strict';
login.factory('FBService', FBService);
FBService.$inject = [(/*What ever dependencies are needed*/)];
function FBService(/*What ever dependencies are needed*/){
//Build the profile object (I assume perfil translates to profile in english)
var profileObject;
//Add Javascript Logic Here
//Create Getters to obtain the data
function getFoto(){
return profileObject.foto;
}
//expose Getter
return {getFoto: getFoto}
}
})();
(function(){
'use strict';
login.controller('inicio_ctrl', inicioCtrl);
inicioCtrl.$inject = ["$scope", "$http", "$timeout", "$window", "FBService"];
function inicioCtrl($scope, $http, $timeout, $window, FBService){
var ctrl = this;
ctrl.login = function(){
ctrl.perfil_folo = FBService.getFoto();
}
}
})();
If you can get the Javascript to work outside of Angular then this will allow you to preserve the work and integrate it into Angular
Related
I'm using ruby on rails with angularjs one, and testing it with teaspoon-jasmine for the first time and am running into issues. Basically, I have a controller that creates an empty array and upon load calls a factory method to populate that array. The Factory makes an http request and returns the data. Right now, i'm trying to test the controller, and i'm trying to test that 1) the factory method is called upon loading the controller, and 2) that the controller correctly assigns the returned data through it's callback. For a while I was having trouble getting a mocked factory to pass a test, but once I did, I realized I wasn't actually testing my controller anymore, but the code below passes. Any tips on how I can still get it to pass with mock, promises/callbacks, but accurately test my controller functionality. Or should I even test the this at all in my controller since it calls a factory method and just gives it a callback? My 3 files are below. Can anyone help here? It would be greatly appreciated
mainController.js
'use strict';
myApp.controller('mainController', [ 'mainFactory', '$scope', '$resource', function(factory, scope, resource){
//hits the /games server route upon page load via the factory to grab the list of video games
scope.games = [];
factory.populateTable(function(data){
scope.games = data;
});
}]);
mainFactory.js
'use strict';
myApp.factory('mainFactory', ['$http', '$routeParams', '$location', function(http, routeParams, location) {
var factory = {};
factory.populateTable = function(callback) {
http.get('/games')
.then(function(response){
callback(response.data);
})
};
return factory;
}]);
And finally my mainController_spec.js file
'use strict';
describe("mainController", function() {
var scope,
ctrl,
deferred,
mainFactoryMock;
var gamesArray = [
{name: 'Mario World', manufacturer: 'Nintendo'},
{name: 'Sonic', manufacturer: 'Sega'}
];
var ngInject = angular.mock.inject;
var ngModule = angular.mock.module;
var setupController = function() {
ngInject( function($rootScope, $controller, $q) {
deferred = $q.defer();
deferred.resolve(gamesArray);
mainFactoryMock = {
populateTable: function() {}
};
spyOn(mainFactoryMock, 'populateTable').and.returnValue(deferred.promise);
scope = $rootScope.$new();
ctrl = $controller('mainController', {
mainFactory: mainFactoryMock,
$scope: scope
});
})
}
beforeEach(ngModule("angularApp"));
beforeEach(function(){
setupController();
});
it('should start with an empty games array and populate the array upon load via a factory method', function(){
expect(scope.games).toEqual([])
mainFactoryMock.populateTable();
expect(mainFactoryMock.populateTable).toHaveBeenCalled();
mainFactoryMock.populateTable().then(function(d) {
scope.games = d;
});
scope.$apply(); // resolve promise
expect(scope.games).toEqual(gamesArray)
})
});
Your code looks "non-standard" e.g still using scope.
If you are just starting with angular I hardly recommend you to read and follow this:
https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md
Angular controllers cannot be tested, extract the logic into factories/services and test from there.
This question already has answers here:
Passing data between controllers in Angular JS?
(18 answers)
Closed 6 years ago.
Below this code console.log is working properly. How to set this data another page using another controller.
$scope.selectedJsonObject=function(category)
{
console.log(category);
}
You can use Service to share the variable across two controllers,
angular.module('Shared', []);
angular.module("Shared").factory("myService", function(){
return {sharedObject: {data: "eran" } }
});
angular.module('Shared').controller('MainCtrl', function ($scope, myService) {
$scope.myVar = myService.sharedObject;
});
angular.module('Shared').controller('Secondtrl', function($scope, $http, myService) {
$scope.myVar = myService.sharedObject;
});
Working App
For some reason, even following the example provided by Josh in the post reply How to test John papa vm.model unit testing with jasmine?, I can't get my controller's values to show up in the testing area. I think it's because of the data service, but it is a necessary component for our SPA, as is using John Papa's styling.
Below is a code snippet to hold the code and display the errors I'm receiving.
(function() {
'use strict';
angular.module('tickets')
.service("DataService", DataService)
/* #ngInject */
DataService.$inject = ["$rootScope", "$q"];
function DataService($rootScope, $q) {
var vm = this;
vm.nameDefault = "Name -- Please Authenticate";
vm.name = vm.nameDefault;
};
})();
(function() {
'use strict';
angular.module('tickets')
.controller('HomeController', HomeController);
/* #ngInject */
HomeController.$inject = ['$scope', '$location', 'DataService'];
function HomeController($scope, $location, DataService) {
var vm = this;
vm.name = DataService.name;
vm.nameDefault = DataService.nameDefault;
};
})();
<link href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.1.0/jasmine.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.1.0/jasmine.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.1.0/jasmine-html.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.1.0/boot.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.4/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.4/angular-mocks.js"></script>
<script>
'use strict';
describe('Controller: HomeController', function() {
beforeEach(module('tickets'));
var controller, $location, DataService;
var tests = 0;
beforeEach(inject(function($controller, _$location_, _DataService_) {
$location = _$location_;
DataService = _DataService_;
scope = {};
controller = $controller('HomeController', {});
}));
var controller, scope, $location, DataService;
var tests = 0;
/* // This version works up until I try to verify the name and nameDefault in controller \\
*
* beforeEach(inject(function ($rootScope, $controller, _$location_, _DataService_) {
* $location = _$location_;
* DataService = _DataService_;
* scope = $rootScope.$new();
*
* controller = function () {
* return $controller('HomeController', {});
* };
* }));
*/
afterEach(function() {
tests += 1;
});
describe('local variables', function() {
describe('load the data model and values for all pertinent variables', function() {
it('should be instantiated', function() {
expect(DataService).toBeDefined();
});
it('should contain a name with an initial value before authentication', function() {
expect(DataService.nameDefault).toBe('Name -- Please Authenticate');
expect(DataService.name).toEqual(DataService.nameDefault);
});
});
describe('should load the controller with data model values, and update as data model values update', function() {
it('should be instantiated', function() {
expect(controller).toBeDefined();
})
it('should not have a vm attribute that can be reached from here', function() {
expect(controller.vm).toBeUndefined();
})
it('should contain a name with an initial value before authentication, both from the data model', function() {
expect(controller.name).toBe(DataService.name);
expect(controller.nameDefault).toBe(DataService.nameDefault);
});
});
});
it('should have tests', function() {
expect(tests).toBeGreaterThan(0);
});
});
</script>
My code, when I use it in our native environment, works to verify that everything in the data service has been instantiated properly (and using the commented out beforeEach block), but the styling using the example in the question referenced above throws even more errors about the scope not being instantiated, even though it is the same (with added dependencies) as that question.
I would expect the answer to be similar to the (currently unanswered) question: How to test John papa vm.model controllers and factories unit testing with jasmine?
I appreciate any help you guys offer.
-C§
Edit Even though I've answered and have success, I would love to get some feedback on why the implementation below works and the other two attempts do not. This is using Karma version 0.13.8 (latest), jasmine 2.1.0, and Angular 1.4.0.
I know it seems like I came up with the solution pretty quickly, but I've been wrestling with this since Friday afternoon (8/7) and have tried a dozen different formats without success.
Again, I welcome your comments and votes so that I can better understand why the version below works and the others have not, especially since I am still very green to AngularJS (1 month in, now).
Thanks again,
-C§
I get it now. Just had to look at Globally mock object in angularjs for jasmine/karma testing and my brain clicked.
The declaration and beforeEach block in the beginning of the test needs to look like this:
var controller, scope, $location, DataService;
var tests = 0;
beforeEach(inject(function ($rootScope, $controller, _$location_, _DataService_) {
$location = _$location_;
DataService = _DataService_;
scope = $rootScope.$new();
controller = $controller('HomeController', {
$scope: scope
});
}));
I think since I've messed with our initial setup a little too much (started the SPA from a template), I needed a strange implementation to make it all work. I'm now getting successful tests throughout:
I hope this helps someone else with similar issues.
I'm dealing with an app that manages users login. Like in many apps, i want to change the header when the user logs in.
I've a main file (index.html) which uses ng-include to include the header.html
I found two solutions (i'm new to angular, so both may be wrong):
1) use a $rootScope.broadcast()
So when the user logs in I broadcast (the auth.js, it's inside a factory) a message that is intercepted by the controller in the header.
the auth.js
$rootScope.$broadcast('logged',user);
the controller.js
$scope.$on('logged', function(evnt, message){
$scope.user = message;
});
the header.html
<div class="header" ng-controller="GcUserCtrl as gcUserCtrl">
...
<li><a ng-show="user" href="#">User: {{user.name}}</a></li>
2) set a $rootScope variable
As far as I understood $rootScope is the root of all the scope (the naming is quite smart) and all the $scope have access to it.
the auth.js
$rootScope.user=user;
the heaeder.html (no controller is needed here)
<div class="header">
...
<li><a ng-show="user" href="#">User: {{user.name}}</a></li>
Now, what's the correct way to handle it?
the first seems a bit more expensive since the broadcast may have to do many checks.
the second .. well, I'm not a fan of global variables..
EDIT
3) use service
after the comment of alex I add this options, even if I'm not able to make it working. (here the plunkr)
it does not work without events
index.html
...
<ng-include src="'header.html'"></ng-include>
...
header.html
as for the number 1)
controller.js
.controller('GcUserCtrl', ['$scope','my.auth','$log', function ($scope, auth, $log) {
$scope.user = auth.currentUser();
}]);
my.auth.js
.factory('my.auth', ['$rootScope', '$log', function ($rootScope, $log, localStorageService) {
var currentUser = undefined;
return {
login: function (user) {
currentUser = user;
...
},
...
currentUser: function () {
return currentUser;
}
};
}]);
The problem here is that the controller is called only the first time and nothing happens after the login.
As I stated earlier you will want to use a Service which will store the user's information. Attach user information to this service where ever you are authenticating the user. If you have questions about the best way to authenticate that would be a seperate question but you may want to look into using a Login Factory that does the actual authentication (and any authorization). You can then inject the login Service into that factory. I have created a Plunker here as a reference.
var app = angular.module('myApp', []);
app.service('SessionService', function () {
this.attachUser = function(userId, fName){
this.userId = userId;
this.fName = fName;
}
});
app.controller('MainCtrl', function($scope, SessionService){
// You will want to invoke attachUser some other way (perhaps on authentication), this is for test purposes only
SessionService.attachUser(1234, 'John');
$scope.userName = SessionService.fName;
});
The code above is an example of your Service. This will act as a Session handler and store important information about the user. The controller MainCtrl can then invoke properties in the SessionService using dependency injection. The part I mentioned at the beginning of this post, SessionService.attachUser(userId, fName) would most likely live in a login factory.
The reason this is the best choice is because it decouples your application. It puts the session (which is really what you are storing in global variables) in a place that is designated to store that data. It makes it maintainable. You do not need to find every occurrence of $rootScope, for instance.
EDIT:
New plunker uses rootScope broadcast/on to capture changes
Events are the preferred way to communicate that action needs to be taken by something else. That an action occurred that something else might be interested in action against. It also reduces scope pollution as you mentioned.
The comment about using a service in this case is only partially accurate. All of the login logic could, and should, be put into a single service specific to logging and logging out. That service would then broadcast the event when a login occurs.
module.service('LoginHelper', function($rootScope) {
this.loginUser = function(username, password) {
// on success
$rootScope.broadcast('loggedIn', logginUserData)
}
this.logout = function() {
// on success
$rootScope.broadcast('loggedOut')
}
})
The logged in data should be stored and accessible by the service.
Alternatively, $emit could be used on $rootScope. You would then only be able to watch for the 'loggedIn' event on the $rootScope by there would be marginally less overhead.
Avoid watches
An event would be the appropriate way to go for this kind of requirement, like how alex has pointed out. A plunk demonstrating an example: http://plnkr.co/edit/v6OXjOXZzF9McMvtn6hG?p=preview
But for this particular scenario, I don't think the "angular way" is the "way". Given the nature of how $broadcast and/or $emit works (i.e. the default way events work in angular) I would avoid them...(Read the docs to understand why). In short, these mechanisms are meant to trigger listeners (attached to some scope) up/down the scope heirarchy. You don't really need all that. (Ref code for $emit)
I'd normally rely on other event propagation mechanisms (considering this pattern of requirement).
app.controller('MainCtrl', function($scope, SessionService, $document){
// You will want to invoke attachUser some other way (perhaps on authentication), this is for test purposes only
$scope.isLoggedIn = false;
$document.bind('$loggedin', function(){
$scope.isLoggedIn = true;
$scope.user = SessionService.fName;
});
$scope.logout = function() {
SessionService.attachUser(null, null);
$scope.isLoggedIn = false;
};
});
app.controller('LoginCtrl', function($scope, SessionService, $document){
$scope.doLogin = function() {
console.log('doLogin');
SessionService.attachUser(1234, $scope.username);
var doc = $document[0];
var evt = new Event('$loggedin');
doc.dispatchEvent(evt);
};
});
Plunk
Of course, when you are done with that view, always cleanup. Handle the $destroy event on that controller's scope and unbind the event handler...
$scope.$on('$destroy', function() {
$document.unbind('$loggedin');
};
Refer MDN for more on how to trigger events using DOM.
Update: [24 Sep]
Here is a small directive setup which demonstrates the point:
app.directive('ngNest', function($parse, $compile, $timeout){
var end = false;
var level = 0;
var fnPostLink = function(scope, element, attrs) {
//console.log('start:', ++fnPostLink.count);
var lvl = attrs.level;
if(!lvl) {
throw 'Level not specified';
}
var create = document.createElement.bind(document);
var level = parseInt(lvl);
var count = 0;
var div = create('div');
div.setAttribute('ng-controller', 'DummyCtrl');
var cls = function() {
return 'margin ' + (count % 2 ? 'even' : 'odd');
//return 'margin even';
};
div.setAttribute('class', cls());
var node = div;
while(count++ < level - 1) {
var child = create('div');
child.setAttribute('ng-controller', 'DummyCtrl');
child.setAttribute('class', cls());
node.appendChild(child);
node = child;
}
node.setAttribute('ng-controller', 'FinalCtrl');
node.innerHTML = 'foo';
var $new = $compile(div)(scope);
var el = element;
el.append($new);
};
fnPostLink.count = 0;
var fnPreLink = function(scope, element, attrs) {
//console.log('prelink');
};
var api = {
link: {
post: fnPostLink,
pre: fnPreLink
},
template: '<div></div>',
scope: {},
restrict: 'E',
replace: true
};
return api;
});
It simply nests divs attaching a controllers to it. I am attaching these two controllers:
app.controller('DummyCtrl', function($scope){
});
app.controller('FinalCtrl', function($scope, $document){
$scope.$on('$myEvt', function(){
console.log('$myEvt', $scope.$id, new Date().getTime());
});
$document.bind('$myEvt', function(){
console.log('$myEvt', $scope.$id, new Date().getTime());
});
});
FinalCtrl is added to the tail; DummyCtrl is added to the rest.
In the html template I do something like:
<ng-nest level="10"></ng-nest>
There is also in the html file a nested markup which is manually put there...
Entire code may be found here: https://gist.github.com/deostroll/a9a2de04d3913f021f13
Here are the results I've obtained running from my browser:
Live reload enabled.
$broadcast 1443074421928
$myEvt 14 1443074421929
$myEvt 19 1443074421930
DOM 1443074426405
$myEvt 14 1443074426405
$myEvt 19 1443074426405
You can notice the difference in the ticks when I've done $broadcast. I have done a $broadcast on $rootScope; hence angular walks down the scope tree depth-first and triggers those listeners attached the respective scopes, and, in that order...The stuff in $emit & $broadcast source code also validates this fact.
I'm using a framework called Radiant UI, which is a way to get HTML5 UI into Unreal Engine 4. I'm trying to pick up some modern Javascript while I do that, so I'm building the UI in AngularJS.
My understanding of Angular is still pretty weak though, and I'm a bit confused about what the best practice is here. The extension injects the following Javascript when it sets up.
var RadiantUI;
if (!RadiantUI)
RadiantUI = {};
(function() {
RadiantUI.TriggerEvent = function() {
native function TriggerEvent();
return TriggerEvent(Array.prototype.slice.call(arguments));
};
RadiantUI.SetCallback = function(name, callback) {
native function SetHook();
return SetHook(name, callback);
};
RadiantUI.RemoveCallback = function(name) {
native function RemoveHook();
return RemoveHook(name);
};
})();;
So this is simply pushing RadiantUI into the global namespace. That would be fine if the extension was always there, but it isn't. In the test environment (Chrome), it's not there. It's only there when running in the game engine. That, combined with the fact that globals suck, means I want to encapsulate it.
In the previous iteration of this, I had it wrapped in an AMD module, and it worked well. Like this:
define([], function()
{
if ("RadiantUI" in window)
{
console.log("RadiantUI in global scope already!");
return window.RadiantUI;
}
var RadiantUI;
if (!RadiantUI) {
RadiantUI = {};
RadiantUI.TriggerEvent = function() {}
RadiantUI.SetCallback = function() {}
RadiantUI.RemoveCallback = function() {}
}
console.log("Using fake RadiantUI bindings");
return RadiantUI;
});
So here's what I want to do:
I want to include radiant as a dependency to my app/stateProvider and have it injected, much the same way it would be in AMD. With the stub methods in place if the extension isn't present. What's the proper approach to this? A module? A service provider?
UPDATE: This is the working code using the answer given.
var myapp = angular.module('bsgcProtoApp', ['ui.router' ]);
myapp.value('radiant', window.RadiantUI || {
TriggerEvent: function()
{
console.log("TriggerEvent called");
},
SetCallback: function(name, callback)
{
console.log("Setcallback called");
},
RemoveCallback: function(name)
{
console.log("RemoveCallback called");
}
});
myapp.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider )
{
$urlRouterProvider.otherwise("/mainmenu");
$stateProvider.state('mainmenu',
{
name: "mainmenu",
url: "/mainmenu",
templateUrl: 'templates/mainmenu.html',
controller: ['$scope', 'radiant', function($scope, radiant)
{
$scope.tester = function()
{
radiant.TriggerEvent("DuderDude");
console.log("Duder!");
}
}],
});
}]);
You presumably have an Angular module or app. For the sake of this answer, let's call it MyApp.
Now you can do
MyApp.value("RadiantUI", window.RadiantUI || {
TriggerEvent = function(){},
//... more properties
});
Now to access this value as a dependency in a controller for example, you'd do this
MyApp.controller(["$scope", "RadiantUI", function($scope, RadiantUI){
// ... controller code ...
}]);