Architecture of single method for ajax in Angularjs - javascript

I'd like to understand how can i design single ajax-method for several controllers, which also can influence on user interface ('loading' animation, for example).
Idea is (without promises):
var myApp = angular.module('myApp', []);
myApp.controller('myCtrl',
function myCtrl($scope, myFactory){
$scope.loading = false;
$scope.someStuff = myFactory.getStuff(params);
});
myApp.factory('myFactory', function(myService){
return{
getStuff: function(params){
return myService.ajax(params);
}
}
});
myApp.service('myService', function($http) {
this.ajax = function(params){
// switch $scope.loading = true;
// make request
// return $http result
// switch $scope.loading = false;
};
});
As i know, i need use $scope for UI changes and ajax-method should be taken out to custom service. Services in Angularjs does not work with $scope and i have no idea how can i solve this problem.
I think, there must be a service with chain of promises.
How can it be designed?
Upd: I hope, with the time the documentation will be more complete and clear. But community of angular users is already great. Thanks.

In my project I have defined a service called appState which has (among other) methods: showGlobalSpinner and hideGlobalSpinner which modify a variable on the $rootScope.
Basically:
(…)
.factory('appState', ['$rootScope', function ($rootScope) {
return {
showGlobalSpinner: function () {
++$rootScope.loadingInProgress;
},
hideGlobalSpinner: function () {
if ($rootScope.loadingInProgress > 0) {
--$rootScope.loadingInProgress
}
}
};
}]);
What I do next is I show spinners wherever I need them using ng-show directive:
<div class="spinner" ng-show="loadingInProgress"></div>
Before each AJAX I just call AppState.showGlobalSpinner() and after success/error I call AppState.hideGlobalSpinner()

You could add this method to any of your controllers:
.controller('HeaderCtrl',['$scope','httpRequestTracker', function($scope,httpRequestTracker) {
$scope.hasPendingRequests = function () {
return httpRequestTracker.hasPendingRequests();
};
}]);
angular.module('services.httpRequestTracker', []);
angular.module('services.httpRequestTracker').factory('httpRequestTracker', ['$http', function($http){
var httpRequestTracker = {};
httpRequestTracker.hasPendingRequests = function() {
return $http.pendingRequests.length > 0;
};
return httpRequestTracker;
}]);

You can try a more generic approach by using an HTTP interceptor, some events and a directive:
Javascript
app.factory('myHttpInterceptor', function($q, $rootScope) {
return function(promise) {
$rootScope.$broadcast('RequestStarted');
var success = function(response) {
$rootScope.$broadcast('RequestFinished', true);
};
var error = function(response) {
$rootScope.$broadcast('RequestFinished', false);
return $q.reject(response);
};
promise.then(success, error);
return promise;
}
})
.config(function($httpProvider) {
$httpProvider.responseInterceptors.push('myHttpInterceptor');
})
.directive('loading', function() {
return {
restrict: 'E',
transclude: true,
template: '<div ng-show="visible" ng-transclude></div>',
controller: function($scope) {
$scope.visible = false;
$scope.$on('RequestStarted', function() {
$scope.visible = true;
});
$scope.$on('RequestFinished', function() {
$scope.visible = false;
});
}
};
});
HTML
...
<loading><h1>Loading...</h1></loading>
...
You can see a working demo here.
By using an HTTP interceptor, you'll be able to track every HTTP request made by the $http service ($resource included) across you Angular application and show the load animation accordingly.

Related

Trouble signing out with Google Sign in (gapi) with angularjs

So I have tried the following templates trying to integrate this:
HTML:
<google-sign-in-button button-id="login-button" options="options"></google-sign-in-button>
CSS:
.directive('googleSignInButton', function() {
return {
scope: { buttonId: '#', options: '&' },
template: '<div></div>',
link: function(scope, element, attrs) {
var div = element.find('div')[0];
div.id = attrs.buttonId;
gapi.signin2.render(div.id, scope.options());
}
};
})
--
I've also just tried doing this in the header and using the regular sign in button:
HTML:
<div class="g-signin2" data-onsuccess="onSignIn"></div>
IN THE HEADER:
<script>
window.onLoadCallback = function(){
gapi.auth2.init({
client_id: '123.apps.googleusercontent.com'
});
}
</script>
No matter what I do, i can't figure out how to log a user out. In my controller, when i try and do gapi.auth.signOut(); it says gapi is undefined
EDIT:
I've also tried dabbling in this to log a person out on run but ideally i'd want to make a log out work anywhere and not just when the page loads. I just don't really know where to put it or the correct way to make it happen:
.run(function ($rootScope, $state) {
gapi.load('auth2', function() {
auth2 = gapi.auth2.init();
auth2.then(function(){
auth2.signOut();
});
});
})
EDIT #2:
I also tried to create this factory with a resolve on my ui-router but it's not getting the data in time or at all
.factory('Auth', function($http, $state, $q, $rootScope) {
var factory = { loggedIn: loggedIn };
return factory;
function loggedIn() {
gapi.load('auth2', function() {
auth2 = gapi.auth2.init();
auth2.then(function(){
return auth2.isSignedIn.get();
});
});
}
})
EDIT #3:
I tried creating a service but I keep getting the following error for some reason to even test it:
Error: undefined is not an object (evaluating 'gapi.auth2.init')
.service('googleService', ['$http', '$rootScope', '$q', function ($http, $rootScope, $q) {
var self = this;
this.signedIn = function() {
auth2 = gapi.auth2.init();
auth2.then(function(){
return auth2.isSignedIn.get();
});
}
this.signOut = function(){
var auth2 = gapi.auth2.getAuthInstance();
auth2.signOut().then(function () {
console.log('User signed out.');
});
}
}])
.controller('login', ['$rootScope', '$scope', '$q', 'googleService', function ($rootScope, $scope, $q, googleService) {
console.log(googleService.signedIn());
}])
I build upon my fiddle from my previous answer on a related question.
Basically what I added was a function to the controller scope that would be called when a user clicks on the signout button.
angular.module('app', [])
.controller('MainController', ['$scope',
function($scope) {
$scope.isSignedIn = false;
...
$scope.signOut = function(){
var auth2 = gapi.auth2.getAuthInstance();
auth2.signOut().then(function () {
$scope.$apply(function(){
$scope.isSignedIn = false;
console.log('User signed out.');
});
});
}
}
])
I used the code snippet provided by Google documentation and that seemed to work immediately.
Do pay attention when changing variables in scope, you have to wrap your scope changes in $scope.$apply for angular to force to check changes in scope.
You can find the full code in this fiddle.
(I will be removing this Google api project at some point, so replace the API Key with your own if it doesn't work)
This is demo code, so if you would actually put this in project, I'd recommend hiding some of the complexity behind services and directives.
Update: if you want to use a service, you'll have to use angulars promises heavily, see $q docs.
Here's a sample on how you could create a service using promises and callbacks.
There's no simple way to get around the callback hell. But I hope wrapping these things into a service will solve that partially.
Here's an updated fiddle taking advantage of the service.
This is the js code:
angular.module('app', [])
.controller('MainController', ['$scope','googleService',
function($scope, googleService) {
$scope.isSignedIn = false;
googleService.load().then(function(){
$scope.signIn = function(){
googleService.signIn().then(function(){
$scope.isSignedIn = googleService.isSignedIn();
});
};
$scope.signOut = function(){
googleService.signOut().then(function(){
$scope.isSignedIn = googleService.isSignedIn();
});
};
});
}
])
.service('googleService', ['$q', function ($q) {
var self = this;
this.load = function(){
var deferred = $q.defer();
gapi.load('auth2', function(){
var auth2 = gapi.auth2.init();
//normally I'd just pass resolve and reject, but page keeps crashing (probably gapi bug)
auth2.then(function(){
deferred.resolve();
});
addAuth2Functions(auth2);
});
return deferred.promise;
};
function addAuth2Functions(auth2){
self.signIn = function() {
var deferred = $q.defer();
auth2.signIn().then(deferred.resolve, deferred.reject);
return deferred.promise;
};
self.isSignedIn = function(){
return auth2.isSignedIn.get();
}
self.signOut = function(){
var deferred = $q.defer();
auth2.signOut().then(deferred.resolve, deferred.reject);
return deferred.promise;
};
}
}]);
Basically, inside the load function you wrap the complexity of loading gapi, and auth2. After the load promise resolved in your controller, you are certain that the signIn, signOut, etc will work because it is loaded.
I took a slightly different approach. Though, I am not good enough to explain why your code does not work and this works for me. Hopefully, someone else can help on this.
Here is my approach. Let me know if this helps.
login.html
<script>var gapiOnLoadCallback = function() { window.gapiOnLoadCallback(); }</script>
<script src="https://apis.google.com/js/platform.js?onload=gapiOnLoadCallback" async defer></script>
<div id="googleLoginButton"></div>
<button ng-show="signedIn" ng-click="signOut()">Sign Out</button>
login.js
angular.module('app', [])
.controller('LoginController', ['$window','$scope', function($window, $scope) {
$window.gapiOnLoadCallback = function() {
gapi.signin2.render('googleLoginButton', {
'onsuccess': onSuccess,
'onfailure': onFailure
});
}
var onSuccess = function(googleUser) {
// Success handling here
};
var onFailure = function() {
// Failure handling here
};
$scope.signOut = function() {
var auth2 = gapi.auth2.getAuthInstance();
auth2.signOut().then(function () {
// Some UI update
});
};
};
login-directive.js
angular.module('app', []).directive("loginButton", function() {
return {
restrict: 'E',
scope: {},
templateUrl: 'login/login.html',
controller: 'LoginController'
}
});

Custom directive with $http injection angularjs

I want to make a widget by using angularjs's custom directive, but stuck somewhere injecting $http service in it.
var app = angular.module('app',[]);
app.directive('users',function($scope,$http){
return {
$http.get('https://jsonplaceholder.typicode.com/users')
.then(function(response) {
$scope.user = response.data;
})
}
})
Any thought how can it be done? I know above code doesn't work but I have no clue how to continue.
This should probably look like:
var app = angular.module('app',[]);
app.directive('users', users);
users.$inject = ['$http'];
function users($http){
return {
restrict: 'E', // if it's element
template: '<div><ul><li ng-repeat="user in users">{{user.name}}</li></ul>{{user}}</div>', // example template
link: function($scope){
$http.get('https://jsonplaceholder.typicode.com/users')
.then(function(response) {
$scope.users = response.data;
});
}
};
}
Here's a working jsbin: http://jsbin.com/qepibukovu/1/edit?html,js,console,output

Reference $scope variables from within $http request angular

I am very new to angularjs and am having a hard time trying to figure out this issue.
Basically, we are using a factory to request data for our application. When the factory returns a promise, we were hoping that the data inside the returned promise that was defined in our scope, would be able to be used, but it is only returning as text on the page.
For example: We have defined $scope.name in our controller:
app.controller('AccountController',function($scope,Account) {
$scope.name = 'Abby';
$scope.news = [];
Account.getSnapshot().success(function(data) {
$scope.news.push(data);
});
});
so the factory (getSnapshot) will return something like "Hello {{name}}" from an $http request as follows:
app.factory('Account',function($http) {
return {
getSnapshot : function() {
return $http.get('data.php');
}
}
});
Is it possible to allow the factory to access /use {{name}} from the $scope?
You will need to use internal Angular $interpolate service:
app.controller('AccountController', function($scope, $interpolate, Account) {
$scope.name = 'Abby';
$scope.news = [];
Account.getSnapshot().success(function(data) {
var text = $interpolate(data)($scope);
$scope.news.push(text);
});
});
Use $q and promises thanks to #dfsq's answer on my post similar to this. Works perfectly.
Here's a plunker.
// Factory method.
app.factory('Account', function($http, $q) {
var data;
return {
getSnapshot: function() {
return data ? $q.when(data) : $http.get('data.json').then(function(response) {
data = response.data;
return data;
})
}
}
});
// Controller method.
app.controller('AccountController', function($scope, Account) {
$scope.name = 'Abby';
$scope.news = [];
Account.getSnapshot().then(function(data) {
$scope.news = data;
});
});

Execute directive code before controller

Hi I am trying to create a display loading box implementation but I seem to have some problems.Here is what I have so far:
I have created an httpInterceptor:
mainModule.factory('httpLoadingInterceptorSvc', ['$q', '$injector', 'EVENTS', function ($q, $injector, EVENTS) {
var httpInterceptorSvc = {};
httpInterceptorSvc.request = function (request) {
var rootScope = $injector.get('$rootScope');
rootScope.$broadcast(EVENTS.LOADING_SHOW);
return $q.when(request);
};
httpInterceptorSvc.requestError = function (rejection) {
hideLoadingBox();
return $q.reject(rejection);
};
httpInterceptorSvc.response = function (response) {
hideLoadingBox();
return $q.when(response);
};
httpInterceptorSvc.responseError = function (rejection) {
hideLoadingBox();
return $q.reject(rejection);
};
function hideLoadingBox() {
var $http = $injector.get('$http');
if ($http.pendingRequests.length < 1) {
var rootScope = $injector.get('$rootScope');
rootScope.$broadcast(EVENTS.LOADING_HIDE);
}
}
return httpInterceptorSvc;
}]);
I have then added the directive to the interceptors of the httpProvideR:
$httpProvider.interceptors.push('httpLoadingInterceptorSvc');
I then created a directive:
mainModule.directive('loadingDir', ['EVENTS', function (EVENTS) {
var loadingDir = {
restrict: 'E',
templateUrl: 'App/scripts/main/directives/loading/LoadingDir.html'
};
loadingDir.link = function (scope, element) {
element.hide();
scope.$on(EVENTS.LOADING_SHOW, function () {
element.show();
});
scope.$on(EVENTS.LOADING_HIDE, function () {
element.hide();
});
};
return loadingDir;
}]);
And then added a simple ajaxCall that alerts a message on the controller:
dataSvc.getCurrentDate().then(function(currentDate) {
alert(currentDate);
});
I put th edirective on the html page:
<loading-dir></loading-dir>
Now my problem is that the directive code gets executed after the controller code this makes the dierective relatively useles until the page is loaded.Is there any way to make the directive code execute before the controller?
You can prepend a div to your page:
<body>
<div controller="beforeController">
<loading-dir></loading-dir>
</div>
[Rest of the page]
</body>
And beforeController should be loaded instantly.

Broadcasting across AngularJS controllers?

How do I broadcast a message between controllers?
Here is what I have tried:
function Ctrl1($scope) {
$scope.$broadcast('Update');
}
Ctrl1.$inject = ['$scope'];
function Ctrl2($scope) {
$scope.updated = false;
$scope.$on('Update', function () {
$scope.updated = true;
});
}
Ctrl2.$inject = ['$scope'];
To see it running: view the Plnkr.
Instead of using $broadcast() a shared service and $watch() might be a better alternative.
var myApp = angular.module('myApp', []);
myApp.factory("MyService", function () {
return {
updated: false
};
});
function Ctrl1($scope, MyService, $timeout) {
$timeout(function () { //Some work occurs and sets updated to true
MyService.updated = true;
}, 1000)
}
Ctrl1.$inject = ['$scope', "MyService", "$timeout"];
function Ctrl2($scope, MyService) {
$scope.$watch(function () {
return MyService.updated;
}, function (oldValue, newValue) {
$scope.updated = MyService.updated;
});
}
Ctrl2.$inject = ['$scope', "MyService"];
Updated Plnkr
It depends on the scopes hierarchy and therefore on where you bootstrap your Ctrl1 and Ctrl2 in your dom.
Say Ctrl1 is the parent of Ctrl2. $broadcast will transmit the event to child scopes: Ctrl2 in this case will notice it (use $on).
If you need to transmit an event from Ctrl2 to Ctrl1, use $emit that transmit the event to parent scopes.

Categories

Resources