I am using Laravel as my back-end, serving as an API for user auth and any CRUD operations. I can't seem to get this implementation of user auth on the angular side working. What I want is just the user to be loaded from the API by the call to fetchUser(), then the user can be retrieved from getUser(). I am not using Angular for routing so I can't make it render only when resolved. I figured by using promises Angular would automatically update the view. I guess I don't understand the whole digest process because nothing I do makes the view change after the user is loaded. A simple ng-show="user.$resolved" or even ng-show="user" doesn't work. The page stays as it was before the promise was resolved. Is there something obvious I'm doing wrong? Or maybe a better way to implement this? I'm not too worried about security, it is not a very functional website. Just an API object I can fetch to display the page a certain way if they are logged in and what role they are.
controllers.js
var Controllers = new angular.module("Controllers", []);
Controllers.controller("MainController", ["$scope", "$location", "Auth", "$timeout", function($scope, $location, Auth, $timeout) {
$scope.user = Auth.fetchUser().then(function(data) {
$scope.user = data;
console.log(data);
});
}]);
services.js
Services.factory("Auth", function($http, $q) {
var user;
function getUser() {
return user;
}
function fetchUser() {
return $http.get("api/auth").then(function(response) {
if (response.data.user) {
user = response.data.user;
} else {
user = null;
}
return user;
});
}
return {
getUser: getUser,
fetchUser: fetchUser
};
});
master.blade.php
<div class="container">
#include("layouts/nav")
#yield("content")
</div>
nav.blade.php (partial)
<ul class="nav navbar-nav navbar-right">
<li>Inquire</li>
<li ng-show="user.$resolved && user.id">Profile</li>
<li ng-show="user.$resolved && user.role === 'User'">Admin</li>
<li ng-hide="user.$resolved && user.id">Login</li>
<li ng-hide="user.$resolved && user.id">Register</li>
</ul>
When the user is logged in, login and register only show as if the user has not been loaded/logged in yet.
Problem is, you're returning the entire response object in the $http promise resolver. You've also fallen prey to the deferred promise anti-pattern by wrapping a promise based API ($http) in a deferred object.
Try this...
function fetchUser() {
return $http.get('api/auth').then(function(response) {
if (response.data.user) {
return response.data.user;
}
return $q.reject('No user data');
});
}
and
Auth.fetchUser().then(function(data) {
$scope.user = data;
console.log(data);
});
Related
I have the following factory:
angularModule
.factory('ArticleCategoryService', function ($http, $q) {
// Service logic
// ...
var categories = [];
var _getCategories = $http.get('/api/articles/category').success(function (_categories) {
categories = _categories;
});
// .error( function (data, status, headers, config) {
// });
// Public API here
return {
getCategories: function () {
var deferred = $q.defer();
deferred.resolve(_getCategories);
return deferred.promise;
}
};
});
and this is the section that calls this service in the controller:
// Calls the getCategories function from the ArticleCategory Service,
// Will return a promise
ArticleCategoryService.getCategories()
.then(function (categoriesResult) {
$scope.categories = categoriesResult.data;
}, function (err) {
console.log(err);
});
This works but there will be a GET call to the server every time user comes back to this view/state and the categories object that belongs to the factory is never used.
I'm trying to make it so that it will return the categories variable in the factory singleton, and have it initialize on site load (or from first GET call).
But if I just return categories when user calls getCategories, it will return nothing since we need time for the $http call.
Check if categories is defined, and resolve the promise with the variable rather than the GET request if it is:
return {
getCategories: function () {
var deferred = $q.defer();
if (categories.length > 0) {
deferred.resolve(categories);
} else {
deferred.resolve(_getCategories);
}
return deferred.promise;
}
};
I'm doing exactly same thing on my app. I have a main module and a main controller within it that wraps any other controllers, so its scope is persistent over views.
In this main controller you could assign the factory's getCategory() data to a scope variable and then you could use that in your entire app, because the scope will be inherited to child scopes.
angularMainModule
.factory('ArticleCategoryService', function ($http) {
var ArticleCategoryServiceMethods = {};
//fn: getCategories
ArticleCategoryServiceMethods.getCategories = function(fromWhere){
return $http.get(fromWhere)
.then(function(res){
return res.data;
}, function(err){
return err.status;
});
}
return ArticleCategoryServiceMethods;
}
angularMainModule
.controller('MAINCTRL', function($scope, ArticleCategoryService) {
//get all categories
$scope.categories = ArticleCategoryService.getCategories('/api/articles/category');
//... the rest of the main ctrl code ... //
}
... when you define the main module, make sure you inject the rest of your modules in it
var angularMainModule = angular.module('angularMainModule', [
'ngRoute',
'ngTouch',
'ngAnimate',
//< ng - etc >,
'Module1',
'Module2',
//< module - n >
]);
...and the markup (i'm bootstrapping my angular app manually, but you could add the ng-app="angularMainModule" attribute on the html tag if you're doing it that way):
<html ng-controller="MAINCTRL">
<!-- head data -->
<body>
<div id="page" ng-view></div>
If you want to make sure data is loaded before your app opens the main page, you could add that service call in the routeProvider block of your app (on the default route), so when the MAINCTRL will be loaded the data will be already there, ready to be assigned.
angularModule
.factory('ArticleCategoryService', function ($http) {
// Service logic
// ...
var categories = [];
$http.get('/api/articles/category').success(function (_categories) {
categories = _categories;
});
// Public API here
return {
categories: categories
};
});
angularModule
.controller('ControllerMain', function($scope, ArticleCategoryService) {
$scope.categories = ArticleCategoryService.categories;
});
In the code below, I'd like handling errors :
401 : redirect to a login page
other : display error message (received in the message of the error)
I don't find the right way to do this.
Any idea ?
Thanks,
Module.js
var app;
(function () {
app = angular.module("studentModule", []);
})()
Service.js
app.service('StudentService', function ($http) {
this.getAllStudent = function () {
return $http.get("http://myserver/api/Student/");
}
});
Controller.js
app.controller('studentController', function ($scope, StudentService) {
function GetAllRecords() {
var promiseGet = StudentService.getAllStudent();
promiseGet.then(function (pl) { $scope.Students = pl.data },
function (errorPl) {
$log.error('Some Error in Getting Records.', errorPl);
});
}
});
As with most problems, there are many different ways to handle errors from AJAX requests in AngularJS. The easiest is to use an HTTP interceptor as already pointed out. This can handle both authentication and errors.
app.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push(['$rootScope', '$q', function($rootScope, $q) {
return {
responseError: function(rejection) {
var deferred;
// If rejection is due to user not being authenticated
if ( rejection.status === 401 ) {
$rootScope.$broadcast('unauthenticated', rejection);
// Return a new promise since this error can be recovered
// from, like redirecting to login page. The rejection and
// and promise could potentially be stored to be re-run
// after user is authenticated.
deferred = $q.defer();
return deferred.promise;
}
$rootScope.$broadcast('serverError', rejection);
// Just reject since this probably isn't recoverable from
return $q.reject(rejection);
}
}
};
}]);
The above interceptor is created using an anonymous function but factories can be used to handle one or many different interceptors. The AngularJS docs have decent information about how to write different ones: https://docs.angularjs.org/api/ng/service/$http#interceptors
With the interceptors in place, you now just need to listen from the broadcasted events in your run method or any controller.
app.run(['$rootScope', '$location', function($rootScope, $location) {
$rootScope.$on('unauthenticated', function(response) {
// Redirect to login page
$location.path('/login');
});
$rootScope.$on('serverError', function(response) {
// Show alert or something to give feedback to user that
// server error was encountered and they need to retry
// or just display this somewhere on the page
$rootScope.serverError = response.data.errorMessage;
});
}]);
In your view:
<body ng-app="studentModule">
<div id="server_error" ng-if="!!serverError">{{serverError}}</div>
...rest of your page
</body>
Like almost all AngularJS code, most of this can be abstracted into different factories and services but this should be a good place to start.
In one of my UI-router states, I have the following link '/users', which points to a page that shows a list of users. Below is the code I wrote to resolve the list of users by using a $resource call, however, the data doesn't seem resolved when the page is loaded:
.state('users', {
url: '/users',
templateUrl: '/assets/angularApp/partials/users.html',
controller: 'UserListCtrl as vm',
resolve: {
users: function ($resource) {
return $resource('http://localhost:8080/api/users').query().$promise.then(function(data) {
return data;
});
}
}
})
In the UserListCtrl, I have the following to assign the resolved users to vm.users so that the users can be displayed on the partial page:
function UserListCtrl($log, users) {
var vm = this;
vm.users = users;
}
The page, however, only displays a few empty rows without any data filled in. So I think the resolve is not working properly on my url /users, could anyone spot where might be problematic? Thanks
Change it to:
users: function ($resource) {
return $resource('http://localhost:8080/api/users').query().$promise;
}
Because that way you're returning the promise, and by using then(...), you're resolving the promise within and just returning data thus it won't pass it to the controller because it's returning them after the controller has loaded.
Using $q service will help you
var userVal = $resource('http://localhost:8080/api/users').query()
retrun $q.resolve(userVal.$promise).then(function(r){ return r;});
Inside you controller, you are good to go
vm.users = users
I've built a table with a filter, sorting, limit and pagination system.
With normal array of objects from the javascript works as expected.
My problem comes when I try to retrieve data from the server and to use it with my client-side pagination and filter. When the page is load for a few seconds the $scope.data is undefined until $http is resolved. I don't know how to solve it and I hope you can help me.
This is the pagination filter which I think is initialized before $http is resolve.
.filter('pagination', function () {
return function (inputArray, currentPage, pageSize) {
var start = (currentPage - 1) * pageSize;
return inputArray.slice(start, start + pageSize);
};
})
I will leave a fiddle here of my problem so you can see it there better.
Fiddle
I don't know if I should edit my post or I do it correctly posting an answer but I came up with a solution and I want to share it.
My problem was that the $scope.data (request from the server) was undefined when controller was initialized so what I did is to set up a route and wait for the promise to resolve before initializing the controller.
One problem I came up with is the fact that it's not possible to inject a .factory directly into the .config so I had to inject $provide to my .config and provide it with my service.
This is the a part of the code I'm talking about :
.config(['$routeProvider','$provide', function($routeProvider, $provide) {
$provide.factory('select', ['$http','$q', function($http, $q) {
return {
getItems : function () {
var deferred = $q.defer();
$http.get('php/select_diarios.php', {cache : true}).success(function (data) {
//Passing data to deferred's resolve function on successful completion
deferred.resolve(data);
}).error(function () {
//Sending an error message in case of failure
deferred.reject("An error occured while fetching items");
});
//Returning the promise object
return deferred.promise;
}
}
}]);
$routeProvider
.when('/', {
templateUrl: 'modulos/home/home.html',
controller: 'AppController',
resolve: {
**data**: function (select) {
return select.getItems();
}
}
}).
when('/genericos', {
controller: 'genericosController'
})
.otherwise({ redirectTo: '/' });
}])
Finally I only had to inject the data parameter of the resolve into my controller like so:
.controller('AppController', ['$scope', '$location','filterFilter','**data**', function ($scope, $location, searchFilter, **data**)
... more code
}]);
Also because of the otherwise redirect you must set a <a href="#/></a> in the pagination cause if not, each time you click to switch a page it doesn't know where to go and redirects to the first one and never changes the page.
this is how the pagination look like.
<ul class="pagination pull-right">
<li>«</li>
<li ng-repeat="n in range" ng-class="{active: n === (currentPage -1)}" ng-click="setPage(n+1)">{{n+1}}</li>
<li>»</li>
</ul>
I developed an angularjs app and I've a REST API for fetching app resources from my AngularJS app. When the user logs himself, I save his access token in a cookie. If he refreshes the page, I want to get back user information from the token like:
mymodule.run(function ($rootScope, $cookies, AuthService, Restangular) {
if ($cookies.usertoken) {
// call GET api/account/
Restangular.one('account', '').get().then( function(user) {
AuthService.setCurrentUser(user);
});
}
});
AuthService:
mymodule.factory('AuthService', function($http, $rootScope) {
var currentUser = null;
return {
setCurrentUser: function(user) {
currentUser = user;
},
getCurrentUser: function() {
return currentUser;
}
};
});
But if the user accesses a controller which needs a user variable:
mymodule.controller('DashboardCtrl', function (AuthService) {
var user = AuthService.getCurrentUser();
});
the controller code was executed before the end of the API call so I've a null var. Is there a good solution to wait on user data loading before starting controllers?
I've found this link but I'm looking for a more global method to initialize app context.
Here's one way I like to handle this, assuming that Restangular returns the new promise from then (I haven't used Restangular). The promise can be stored somewhere like on AuthService and then used later in the controller. First, add a property to AuthService to hold the new promise:
return {
authPromise: {}, // this will hold the promise from Restangular
// setCurrentUser, getCurrentUser
// ...
When calling Restangular, save the promise and be sure to return the user data so that the controller can access it later, like this:
AuthService.authPromise = Restangular.one('account', '').get()
.then( function(user) {
AuthService.setCurrentUser(user);
return user; // <--important
});
Lastly, create a new promise in the controller that will set the user variable.:
mymodule.controller('DashboardCtrl', function (AuthService) {
var user;
AuthService.authPromise.then(function(resultUser){
user = resultUser;
alert(user);
// do something with user
});
});
Demo: Here is a fiddle where I've simulated an AJAX call with $timeout. When the timeout concludes, the promise resolves.
You could try have the authentication method on a parent controller. Then in the resolve method in the routing you could go resolve:DashboardCtrl.$parent.Authenticate().