Related
I'm writing a small AngularJS app that has a login view and a main view, configured like so:
$routeProvider
.when('/main' , {templateUrl: 'partials/main.html', controller: MainController})
.when('/login', {templateUrl: 'partials/login.html', controller: LoginController})
.otherwise({redirectTo: '/login'});
My LoginController checks the user/pass combination and sets a property on the $rootScope reflecting this:
function LoginController($scope, $location, $rootScope) {
$scope.attemptLogin = function() {
if ( $scope.username == $scope.password ) { // test
$rootScope.loggedUser = $scope.username;
$location.path( "/main" );
} else {
$scope.loginError = "Invalid user/pass.";
}
}
Everything works, but if I access http://localhost/#/main I end up bypassing the login screen. I wanted to write something like "whenever the route changes, if $rootScope.loggedUser is null then redirect to /login"
...
... wait. Can I listen to route changes somehow? I'll post this question anyway and keep looking.
After some diving through some documentation and source code, I think I got it working. Perhaps this will be useful for someone else?
I added the following to my module configuration:
angular.module(...)
.config( ['$routeProvider', function($routeProvider) {...}] )
.run( function($rootScope, $location) {
// register listener to watch route changes
$rootScope.$on( "$routeChangeStart", function(event, next, current) {
if ( $rootScope.loggedUser == null ) {
// no logged user, we should be going to #login
if ( next.templateUrl != "partials/login.html" ) {
// not going to #login, we should redirect now
$location.path( "/login" );
}
}
});
})
The one thing that seems odd is that I had to test the partial name (login.html) because the "next" Route object did not have a url or something else. Maybe there's a better way?
Here is maybe a more elegant and flexible solution with 'resolve' configuration property and 'promises' enabling eventual data loading on routing and routing rules depending on data.
You specify a function in 'resolve' in routing config and in the function load and check data, do all redirects. If you need to load data, you return a promise, if you need to do redirect - reject promise before that.
All details can be found on $routerProvider and $q documentation pages.
'use strict';
var app = angular.module('app', [])
.config(['$routeProvider', function($routeProvider) {
$routeProvider
.when('/', {
templateUrl: "login.html",
controller: LoginController
})
.when('/private', {
templateUrl: "private.html",
controller: PrivateController,
resolve: {
factory: checkRouting
}
})
.when('/private/anotherpage', {
templateUrl:"another-private.html",
controller: AnotherPriveController,
resolve: {
factory: checkRouting
}
})
.otherwise({ redirectTo: '/' });
}]);
var checkRouting= function ($q, $rootScope, $location) {
if ($rootScope.userProfile) {
return true;
} else {
var deferred = $q.defer();
$http.post("/loadUserProfile", { userToken: "blah" })
.success(function (response) {
$rootScope.userProfile = response.userProfile;
deferred.resolve(true);
})
.error(function () {
deferred.reject();
$location.path("/");
});
return deferred.promise;
}
};
For russian-speaking folks there is a post on habr "Вариант условного раутинга в AngularJS."
I have been trying to do the same. Came up with another simpler solution after working with a colleague. I have a watch set up on $location.path(). That does the trick. I am just starting to learn AngularJS and find this to be more cleaner and readable.
$scope.$watch(function() { return $location.path(); }, function(newValue, oldValue){
if ($scope.loggedIn == false && newValue != '/login'){
$location.path('/login');
}
});
A different way of implementing login redirection is to use events and interceptors as described here. The article describes some additional advantages such as detecting when a login is required, queuing the requests, and replaying them once the login is successful.
You can try out a working demo here and view the demo source here.
1. Set global current user.
In your authentication service, set the currently authenticated user on the root scope.
// AuthService.js
// auth successful
$rootScope.user = user
2. Set auth function on each protected route.
// AdminController.js
.config(function ($routeProvider) {
$routeProvider.when('/admin', {
controller: 'AdminController',
auth: function (user) {
return user && user.isAdmin
}
})
})
3. Check auth on each route change.
// index.js
.run(function ($rootScope, $location) {
$rootScope.$on('$routeChangeStart', function (ev, next, curr) {
if (next.$$route) {
var user = $rootScope.user
var auth = next.$$route.auth
if (auth && !auth(user)) { $location.path('/') }
}
})
})
Alternatively you can set permissions on the user object and assign each route a permission, then check the permission in the event callback.
Here's how I did it, in case it helps anyone:
In the config, I set a publicAccess attribute on the few routes that I want open to the public (like login or register):
$routeProvider
.when('/', {
templateUrl: 'views/home.html',
controller: 'HomeCtrl'
})
.when('/login', {
templateUrl: 'views/login.html',
controller: 'LoginCtrl',
publicAccess: true
})
then in a run block, I set a listener on the $routeChangeStart event that redirects to '/login' unless the user has access or the route is publicly accessible:
angular.module('myModule').run(function($rootScope, $location, user, $route) {
var routesOpenToPublic = [];
angular.forEach($route.routes, function(route, path) {
// push route onto routesOpenToPublic if it has a truthy publicAccess value
route.publicAccess && (routesOpenToPublic.push(path));
});
$rootScope.$on('$routeChangeStart', function(event, nextLoc, currentLoc) {
var closedToPublic = (-1 === routesOpenToPublic.indexOf($location.path()));
if(closedToPublic && !user.isLoggedIn()) {
$location.path('/login');
}
});
})
You could obviously change the condition from isLoggedIn to anything else... just showing another way to do it.
I'm doing it using interceptors. I have created a library file which can be added to the index.html file. This way you'll have global error handling for your rest service calls and don't have to care about all errors individually. Further down I also pasted my basic-auth login library. There you can see that I also check for the 401 error and redirect to a different location. See lib/ea-basic-auth-login.js
lib/http-error-handling.js
/**
* #ngdoc overview
* #name http-error-handling
* #description
*
* Module that provides http error handling for apps.
*
* Usage:
* Hook the file in to your index.html: <script src="lib/http-error-handling.js"></script>
* Add <div class="messagesList" app-messages></div> to the index.html at the position you want to
* display the error messages.
*/
(function() {
'use strict';
angular.module('http-error-handling', [])
.config(function($provide, $httpProvider, $compileProvider) {
var elementsList = $();
var showMessage = function(content, cl, time) {
$('<div/>')
.addClass(cl)
.hide()
.fadeIn('fast')
.delay(time)
.fadeOut('fast', function() { $(this).remove(); })
.appendTo(elementsList)
.text(content);
};
$httpProvider.responseInterceptors.push(function($timeout, $q) {
return function(promise) {
return promise.then(function(successResponse) {
if (successResponse.config.method.toUpperCase() != 'GET')
showMessage('Success', 'http-success-message', 5000);
return successResponse;
}, function(errorResponse) {
switch (errorResponse.status) {
case 400:
showMessage(errorResponse.data.message, 'http-error-message', 6000);
}
}
break;
case 401:
showMessage('Wrong email or password', 'http-error-message', 6000);
break;
case 403:
showMessage('You don\'t have the right to do this', 'http-error-message', 6000);
break;
case 500:
showMessage('Server internal error: ' + errorResponse.data.message, 'http-error-message', 6000);
break;
default:
showMessage('Error ' + errorResponse.status + ': ' + errorResponse.data.message, 'http-error-message', 6000);
}
return $q.reject(errorResponse);
});
};
});
$compileProvider.directive('httpErrorMessages', function() {
return {
link: function(scope, element, attrs) {
elementsList.push($(element));
}
};
});
});
})();
css/http-error-handling.css
.http-error-message {
background-color: #fbbcb1;
border: 1px #e92d0c solid;
font-size: 12px;
font-family: arial;
padding: 10px;
width: 702px;
margin-bottom: 1px;
}
.http-error-validation-message {
background-color: #fbbcb1;
border: 1px #e92d0c solid;
font-size: 12px;
font-family: arial;
padding: 10px;
width: 702px;
margin-bottom: 1px;
}
http-success-message {
background-color: #adfa9e;
border: 1px #25ae09 solid;
font-size: 12px;
font-family: arial;
padding: 10px;
width: 702px;
margin-bottom: 1px;
}
index.html
<!doctype html>
<html lang="en" ng-app="cc">
<head>
<meta charset="utf-8">
<title>yourapp</title>
<link rel="stylesheet" href="css/http-error-handling.css"/>
</head>
<body>
<!-- Display top tab menu -->
<ul class="menu">
<li>Users</li>
<li>Vendors</li>
<li><logout-link/></li>
</ul>
<!-- Display errors -->
<div class="http-error-messages" http-error-messages></div>
<!-- Display partial pages -->
<div ng-view></div>
<!-- Include all the js files. In production use min.js should be used -->
<script src="lib/angular114/angular.js"></script>
<script src="lib/angular114/angular-resource.js"></script>
<script src="lib/http-error-handling.js"></script>
<script src="js/app.js"></script>
<script src="js/services.js"></script>
<script src="js/controllers.js"></script>
<script src="js/filters.js"></script>
lib/ea-basic-auth-login.js
Nearly same can be done for the login. Here you have the answer to the redirect ($location.path("/login")).
/**
* #ngdoc overview
* #name ea-basic-auth-login
* #description
*
* Module that provides http basic authentication for apps.
*
* Usage:
* Hook the file in to your index.html: <script src="lib/ea-basic-auth-login.js"> </script>
* Place <ea-login-form/> tag in to your html login page
* Place <ea-logout-link/> tag in to your html page where the user has to click to logout
*/
(function() {
'use strict';
angular.module('ea-basic-auth-login', ['ea-base64-login'])
.config(['$httpProvider', function ($httpProvider) {
var ea_basic_auth_login_interceptor = ['$location', '$q', function($location, $q) {
function success(response) {
return response;
}
function error(response) {
if(response.status === 401) {
$location.path('/login');
return $q.reject(response);
}
else {
return $q.reject(response);
}
}
return function(promise) {
return promise.then(success, error);
}
}];
$httpProvider.responseInterceptors.push(ea_basic_auth_login_interceptor);
}])
.controller('EALoginCtrl', ['$scope','$http','$location','EABase64Login', function($scope, $http, $location, EABase64Login) {
$scope.login = function() {
$http.defaults.headers.common['Authorization'] = 'Basic ' + EABase64Login.encode($scope.email + ':' + $scope.password);
$location.path("/user");
};
$scope.logout = function() {
$http.defaults.headers.common['Authorization'] = undefined;
$location.path("/login");
};
}])
.directive('eaLoginForm', [function() {
return {
restrict: 'E',
template: '<div id="ea_login_container" ng-controller="EALoginCtrl">' +
'<form id="ea_login_form" name="ea_login_form" novalidate>' +
'<input id="ea_login_email_field" class="ea_login_field" type="text" name="email" ng-model="email" placeholder="E-Mail"/>' +
'<br/>' +
'<input id="ea_login_password_field" class="ea_login_field" type="password" name="password" ng-model="password" placeholder="Password"/>' +
'<br/>' +
'<button class="ea_login_button" ng-click="login()">Login</button>' +
'</form>' +
'</div>',
replace: true
};
}])
.directive('eaLogoutLink', [function() {
return {
restrict: 'E',
template: '<a id="ea-logout-link" ng-controller="EALoginCtrl" ng-click="logout()">Logout</a>',
replace: true
}
}]);
angular.module('ea-base64-login', []).
factory('EABase64Login', function() {
var keyStr = 'ABCDEFGHIJKLMNOP' +
'QRSTUVWXYZabcdef' +
'ghijklmnopqrstuv' +
'wxyz0123456789+/' +
'=';
return {
encode: function (input) {
var output = "";
var chr1, chr2, chr3 = "";
var enc1, enc2, enc3, enc4 = "";
var i = 0;
do {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output +
keyStr.charAt(enc1) +
keyStr.charAt(enc2) +
keyStr.charAt(enc3) +
keyStr.charAt(enc4);
chr1 = chr2 = chr3 = "";
enc1 = enc2 = enc3 = enc4 = "";
} while (i < input.length);
return output;
},
decode: function (input) {
var output = "";
var chr1, chr2, chr3 = "";
var enc1, enc2, enc3, enc4 = "";
var i = 0;
// remove all characters that are not A-Z, a-z, 0-9, +, /, or =
var base64test = /[^A-Za-z0-9\+\/\=]/g;
if (base64test.exec(input)) {
alert("There were invalid base64 characters in the input text.\n" +
"Valid base64 characters are A-Z, a-z, 0-9, '+', '/',and '='\n" +
"Expect errors in decoding.");
}
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
do {
enc1 = keyStr.indexOf(input.charAt(i++));
enc2 = keyStr.indexOf(input.charAt(i++));
enc3 = keyStr.indexOf(input.charAt(i++));
enc4 = keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
if (enc3 != 64) {
output = output + String.fromCharCode(chr2);
}
if (enc4 != 64) {
output = output + String.fromCharCode(chr3);
}
chr1 = chr2 = chr3 = "";
enc1 = enc2 = enc3 = enc4 = "";
} while (i < input.length);
return output;
}
};
});
})();
In your app.js file:
.run(["$rootScope", "$state", function($rootScope, $state) {
$rootScope.$on('$locationChangeStart', function(event, next, current) {
if (!$rootScope.loggedUser == null) {
$state.go('home');
}
});
}])
It's possible to redirect to another view with angular-ui-router. For this purpose, we have the method $state.go("target_view"). For example:
---- app.js -----
var app = angular.module('myApp', ['ui.router']);
app.config(function ($stateProvider, $urlRouterProvider) {
// Otherwise
$urlRouterProvider.otherwise("/");
$stateProvider
// Index will decide if redirects to Login or Dashboard view
.state("index", {
url: ""
controller: 'index_controller'
})
.state('dashboard', {
url: "/dashboard",
controller: 'dashboard_controller',
templateUrl: "views/dashboard.html"
})
.state('login', {
url: "/login",
controller: 'login_controller',
templateUrl: "views/login.html"
});
});
// Associate the $state variable with $rootScope in order to use it with any controller
app.run(function ($rootScope, $state, $stateParams) {
$rootScope.$state = $state;
$rootScope.$stateParams = $stateParams;
});
app.controller('index_controller', function ($scope, $log) {
/* Check if the user is logged prior to use the next code */
if (!isLoggedUser) {
$log.log("user not logged, redirecting to Login view");
// Redirect to Login view
$scope.$state.go("login");
} else {
// Redirect to dashboard view
$scope.$state.go("dashboard");
}
});
----- HTML -----
<!DOCTYPE html>
<html>
<head>
<title>My WebSite</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="description" content="MyContent">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="js/libs/angular.min.js" type="text/javascript"></script>
<script src="js/libs/angular-ui-router.min.js" type="text/javascript"></script>
<script src="js/app.js" type="text/javascript"></script>
</head>
<body ng-app="myApp">
<div ui-view></div>
</body>
</html>
If you do not want to use angular-ui-router, but would like to have your controllers lazy loaded via RequireJS, there are couple of problems with event $routeChangeStart when using your controllers as RequireJS modules (lazy loaded).
You cannot be sure the controller will be loaded before $routeChangeStart gets triggered -- in fact it wont be loaded. That means you cannot access properties of next route like locals or $$route because they are not yet setup.
Example:
app.config(["$routeProvider", function($routeProvider) {
$routeProvider.when("/foo", {
controller: "Foo",
resolve: {
controller: ["$q", function($q) {
var deferred = $q.defer();
require(["path/to/controller/Foo"], function(Foo) {
// now controller is loaded
deferred.resolve();
});
return deferred.promise;
}]
}
});
}]);
app.run(["$rootScope", function($rootScope) {
$rootScope.$on("$routeChangeStart", function(event, next, current) {
console.log(next.$$route, next.locals); // undefined, undefined
});
}]);
This means you cannot check access rights in there.
Solution:
As loading of controller is done via resolve, you can do the same with your access control check:
app.config(["$routeProvider", function($routeProvider) {
$routeProvider.when("/foo", {
controller: "Foo",
resolve: {
controller: ["$q", function($q) {
var deferred = $q.defer();
require(["path/to/controller/Foo"], function(Foo) {
// now controller is loaded
deferred.resolve();
});
return deferred.promise;
}],
access: ["$q", function($q) {
var deferred = $q.defer();
if (/* some logic to determine access is granted */) {
deferred.resolve();
} else {
deferred.reject("You have no access rights to go there");
}
return deferred.promise;
}],
}
});
}]);
app.run(["$rootScope", function($rootScope) {
$rootScope.$on("$routeChangeError", function(event, next, current, error) {
console.log("Error: " + error); // "Error: You have no access rights to go there"
});
}]);
Note here that instead of using event $routeChangeStart I'm using $routeChangeError
$routeProvider
.when('/main' , {templateUrl: 'partials/main.html', controller: MainController})
.when('/login', {templateUrl: 'partials/login.html', controller: LoginController}).
.when('/login', {templateUrl: 'partials/index.html', controller: IndexController})
.otherwise({redirectTo: '/index'});
I'm trying to use $location.path() inside of an $interval to automatically navigate from tab to tab on a timer.
However, whenever I call $location.path(x) it only reloads my current view.
My code is as follows:
tabsController.js
(function () {
var injectParams = ['$location', '$routeParams', '$scope', '$rootScope', '$interval', 'teleAiDiagnosticsConfig'];
var TabsController = function ($location, $routeParams, $scope, $rootScope, $interval, teleAiDiagnosticsConfig) {
var vm = this;
$rootScope.autoNavInterval = {};
$rootScope.AutoNavActive = false;
$scope.tabs = [
{ link: '#!/vehicles', label: 'Vehicles', active: (($location.$$url.indexOf('/vehicle') !== -1) ? true : false) },
{ link: '#!/workzones', label: 'Workzones', active: (($location.$$url.indexOf('/workzone') !== -1) ? true : false) },
{ link: '#!/operatorStations', label: 'Operator Stations', active: (($location.$$url.indexOf('/operatorStation') !== -1) ? true : false) },
{ link: '#!/sessionStatus/123', label: 'Session Status (123)', active: (($location.$$url.indexOf('/sessionStatus') !== -1) ? true : false) }
];
// find the tab that should be marked as active
for (var i = 0; i < $scope.tabs.length; i++) {
if ($scope.tabs[i].active == true) {
$scope.selectedTab = $scope.tabs[i];
break;
}
}
if ($scope.selectedTab == undefined)
$scope.selectedTab = $scope.tabs[0];
$scope.setSelectedTab = function (tab) {
$scope.selectedTab = tab;
}
$scope.tabClass = function (tab) {
if ($scope.selectedTab == tab) {
return "active";
} else {
return "";
}
}
$scope.nextTab = function () {
for (var i = 0; i < $scope.tabs.length; i++) {
if ($scope.tabs[i] == $scope.selectedTab) {
if (i == $scope.tabs.length - 1) {
$location.path($scope.tabs[0].link);
if (!$scope.$$phase) $scope.$apply()
$scope.tabs[0].active = true;
$scope.selectedTab = $scope.tabs[0];
break;
}
else {
$location.path($scope.tabs[i + 1].link);
if (!$scope.$$phase) $scope.$apply()
$scope.tabs[i + 1].active = true;
$scope.selectedTab = $scope.tabs[i + 1];
break;
}
}
}
}
$scope.startAutoNav = function () {
$rootScope.AutoNavActive = true;
$rootScope.autoNavInterval = $interval(function () {
$scope.nextTab();
}, teleAiDiagnosticsConfig.autoNavTimer);
}
$scope.stopAutoNav = function () {
$rootScope.AutoNavActive = false;
$interval.cancel($rootScope.autoNavInterval);
}
$scope.startAutoNav();
};
TabsController.$inject = injectParams;
angular.module('teleAiDiagnostics').controller('TabsController', TabsController);
}());
Yes, yes, I know using $rootScope is not advised. I'm going to change it once I get $location.path() working.
I've tried it with and without the if (!$scope.$$phase) $scope.$apply() lines. I've also tried adding a call to $location.replace() but this doesn't do anything for me.
Whenever $location.path() is called the view section flashes but it just reloads the Vehicles view.
app.js
(function () {
var app = angular.module('teleAiDiagnostics', ['ngRoute', 'ngAnimate', 'ui.bootstrap']);
app.config(['$routeProvider', function ($routeProvider) {
var viewBase = 'app/views/';
$routeProvider
.when('/vehicles', {
controller: 'VehiclesController',
templateUrl: viewBase + 'vehicles.html',
controllerAs: 'vm'
})
.when('/vehicle/:id', {
controller: 'VehicleController',
templateUrl: viewBase + "vehicle.html",
controllerAs: 'vm'
})
.when('/workzones', {
controller: 'WorkzonesController',
templateUrl: viewBase + 'workzones.html',
controllerAs: 'vm'
})
.when('/workzone/:id', {
controller: 'WorkzoneController',
templateUrl: viewBase + "workzone.html",
controllerAs: 'vm'
})
.when('/operatorStations', {
controller: 'OperatorStationsController',
templateUrl: viewBase + 'operatorStations.html',
controllerAs: 'vm'
})
.when('/operatorStation/:id', {
controller: 'OperatorStationController',
templateUrl: viewBase + 'operatorStation.html',
controllerAs: 'vm'
})
.when('/sessionStatus/:id', {
controller: 'SessionStatusController',
templateUrl: viewBase + 'sessionStatus.html',
controllerAs: 'vm'
})
.otherwise({ redirectTo: '/vehicles' });
}]);
// Intialize slider
$('.flexslider').flexslider({
animation: 'slide',
slideshow: true,
pauseOnAction: true,
controlNav: true,
pauseOnHover: true,
touch: true,
directionalNav: false,
direction: 'horizontal',
slideshowSpeed: 6000,
smoothHeight: true
});
}());
Not sure why this isn't working. I've followed all the tips I could find and I'm still getting the same result. I'm tempted to just add UI-Router to my application and replace my routes with states.
Found it. The issue was related to the hash-bang (#!) in the URL.
I added a link to my tab definitions which does not contain the hash-bang and it works perfectly now.
tabsController.js
(function () {
var injectParams = ['$location', '$routeParams', '$scope', '$rootScope', '$interval', 'teleAiDiagnosticsConfig'];
var TabsController = function ($location, $routeParams, $scope, $rootScope, $interval, teleAiDiagnosticsConfig) {
var vm = this;
$rootScope.autoNavInterval = {};
$rootScope.AutoNavActive = false;
$scope.tabs = [
{ link: '#!/vehicles', refLink: '/vehicles', label: 'Vehicles', active: (($location.$$url.indexOf('/vehicle') !== -1) ? true : false) },
{ link: '#!/workzones', refLink: '/workzones', label: 'Workzones', active: (($location.$$url.indexOf('/workzone') !== -1) ? true : false) },
{ link: '#!/operatorStations', refLink: '/operatorStations', label: 'Operator Stations', active: (($location.$$url.indexOf('/operatorStation') !== -1) ? true : false) },
{ link: '#!/sessionStatus/123', refLink: '/sessionStatus/123', label: 'Session Status (123)', active: (($location.$$url.indexOf('/sessionStatus') !== -1) ? true : false) }
];
// find the tab that should be marked as active
for (var i = 0; i < $scope.tabs.length; i++) {
if ($scope.tabs[i].active == true) {
$scope.selectedTab = $scope.tabs[i];
break;
}
}
if ($scope.selectedTab == undefined)
$scope.selectedTab = $scope.tabs[0];
$scope.setSelectedTab = function (tab) {
$scope.selectedTab = tab;
}
$scope.tabClass = function (tab) {
if ($scope.selectedTab == tab) {
return "active";
} else {
return "";
}
}
$scope.nextTab = function () {
for (var i = 0; i < $scope.tabs.length; i++) {
if ($scope.tabs[i] == $scope.selectedTab) {
if (i == $scope.tabs.length - 1) {
$location.path($scope.tabs[0].refLink);
$location.replace();
if (!$scope.$$phase) $scope.$apply()
$scope.tabs[0].active = true;
$scope.selectedTab = $scope.tabs[0];
break;
}
else {
$location.path($scope.tabs[i + 1].refLink);
$location.replace();
if (!$scope.$$phase) $scope.$apply()
$scope.tabs[i + 1].active = true;
$scope.selectedTab = $scope.tabs[i + 1];
break;
}
}
}
}
$scope.startAutoNav = function () {
$rootScope.AutoNavActive = true;
$rootScope.autoNavInterval = $interval(function () {
$scope.nextTab();
}, teleAiDiagnosticsConfig.autoNavTimer);
}
$scope.stopAutoNav = function () {
$rootScope.AutoNavActive = false;
$interval.cancel($rootScope.autoNavInterval);
}
$scope.startAutoNav();
};
TabsController.$inject = injectParams;
angular.module('teleAiDiagnostics').controller('TabsController', TabsController);
}());
Hope this helps someone.
Complete Angular newbie here. I have the app hosted here https://bitbucket.org/builtbyvern/ultimate-angularjs-course/src and have been following along on a tutorial. It had me refactor the app using controllerAs.
The app does seem to work until I hit the ng-repeat. No errors are returned... just a commented out ng-repeat:
<!-- ngRepeat: classified in vm.classifieds -->
Code as follows but feel free to look at the bitbucket link.
<md-card flex="30" ng-repeat="classified in vm.classifieds | filter:classifiedsFilter | filter:category" class="classified">
...
<md-card>
Controller:
(function(){
"use strict";
angular
.module('ngClassifieds')
.controller('classifiedsCtrl', function($scope, $http, classifiedsFactory, $mdSidenav, $mdToast, $mdDialog, $stateParams) {
// vm for view model
var vm = this;
vm.openSidebar = openSidebar;
vm.closeSidebar = closeSidebar;
vm.saveClassified = saveClassified;
vm.editClassified = editClassified;
vm.saveEdit = saveEdit;
vm.deleteClassified = deleteClassified;
vm.classifieds;
vm.categories;
vm.editing;
vm.classified;
classifiedsFactory.getClassifieds().then(function(classifieds) {
vm.classifieds = classifieds.data;
vm.categories = getCategories(vm.classifieds);
});
var contact = {
name: "Vern",
phone: '208.283.6343',
email: 'vernworldwide#gmail.com'
}
function openSidebar() {
$mdSidenav('left').open();
}
function closeSidebar() {
$mdSidenav('left').close();
}
function saveClassified(classified) {
if (classified) {
classified.contact = contact;
vm.classifieds.push(classified);
vm.classified = {};
vm.closeSidebar();
showToast('Classified Saved!');
}
}
function editClassified(classified) {
vm.editing = true;
openSidebar();
vm.classified = classified;
}
function saveEdit(classified) {
vm.editing = false;
vm.classified = {};
closeSidebar();
showToast("Edit Saved!");
}
function deleteClassified(event, classified) {
var confirm = $mdDialog.confirm()
.title('Are your sure you want to delete ' + classified.title + '?')
.ok("Yep")
.cancel('Nope').
targetEvent(event);
$mdDialog.show(confirm).then(function(){
var index = vm.classifieds.indexOf(classified);
vm.classifieds.splice(index, 1);
}, function(){
});
}
function showToast(message){
$mdToast.show(
$mdToast.simple()
.content(message)
.position('top, right')
.hideDelay(3000)
);
}
function getCategories(classifieds) {
var categories = [];
angular.forEach(classifieds, function(item) {
angular.forEach(item.categories, function(category){
categories.push(category);
});
});
return _.uniq(categories);
}
});
})();
and of course the config
angular
.module("ngClassifieds",['ngMaterial', 'ui.router'])
.config(function($mdThemingProvider, $stateProvider) {
$mdThemingProvider.theme('default')
.primaryPalette('teal')
.accentPalette('orange');
$stateProvider
.state('classifieds', {
url: '/classifieds',
templateUrl: 'components/classifieds/classifieds.tpl.html',
controllerAs: 'classifiedsCtrl as vm'
});
});
I want to re-organize my code so that I can share a dialog page with different controllers (ie. each controller opens the same dialog)
Should my dialog be a service or a directive... I'm not sure how to make it available to the controllers and have access to scopes
Something like this:
app.controller('PropertiesCtrl', function($scope,$rootScope,$http, DTOptionsBuilder, DTColumnBuilder, apiserv, $filter, $mdDialog, $mdMedia){
$scope.getProperties = function(){
$scope.data=[];
//var url = 'http://www.filltext.com/?rows=10&fname={firstName}&lname={lastName}&delay=3&callback=JSON_CALLBACK';
var data = "?db="+ $rootScope.globals.currentUser.agents[$rootScope.globals.currentDB].db_name;
var url = apiserv+"api.properties.test.php"+data;
$http.jsonp(url).success(function(data){
$scope.data=data;
});
};
var vm = this;
function stateChange(iColumn, bVisible) {
console.log('The column', iColumn, ' has changed its status to', bVisible);
}
// TO-DO: Rather load from a Factory promise, like here: https://github.com/l-lin/angular-datatables/issues/14
// Example here: http://embed.plnkr.co/B9ltNzIRCwswgHqKD5Pp/preview
var data = "?db="+ $rootScope.globals.currentUser.agents[$rootScope.globals.currentDB].db_name;
var url = apiserv+"api.properties.test.php"+data;
vm.dtOptions = DTOptionsBuilder.fromSource(url)
.withBootstrap()
// Active Buttons extension
.withButtons([
//'columnsToggle',
'colvis',
'copy',
'print',
'excel'
])
.withOption('fnRowCallback',myCallback)
.withOption('order', [[ 3, "desc" ]])
.withOption('stateSave',true);
vm.dtColumns = [
DTColumnBuilder.newColumn('File_Num').withTitle('File'),
DTColumnBuilder.newColumn('Description').withTitle('Description'),
DTColumnBuilder.newColumn('street').withTitle('Street'),
DTColumnBuilder.newColumn('bedrooms').withTitle('Bed'),
DTColumnBuilder.newColumn('bathrooms').withTitle('Bath'),
DTColumnBuilder.newColumn('garages').withTitle('Garages'),
DTColumnBuilder.newColumn('car_port').withTitle('Car Ports').notVisible(),
DTColumnBuilder.newColumn('Current_Monthly_Rental').withTitle('Rental').renderWith(function(data, type, full) {
return $filter('currency')(data, 'R ', 2); //could use currency/date or any angular filter
})
];
})
.controller('PagesListCtrl', function($scope,$rootScope,$http, DTOptionsBuilder, DTColumnBuilder, apiserv, $filter, $mdDialog, $mdMedia){
$scope.page = {
title: 'Dashboard',
subtitle: 'Place subtitle here...'
};
});
function myCallback(nRow, aData, iDisplayIndex, iDisplayIndexFull) {
$('td', nRow).bind('click', function() {
//$scope.$apply(function() {
showTabDialog(aData);
//});
});
return nRow;
};
function showTabDialog(ev) {
console.log(ev.file_id);
var data = "?db="+ $rootScope.globals.currentUser.agents[$rootScope.globals.currentDB].db_name+"&file="+ev.file_id;
var url = apiserv+"api.properties.view.php"+data;
console.log(url);
$http({url:url}).then(function(rs){
console.log(rs.data[0]);
$scope.propdata=rs.data[0];
$mdDialog.show({
controller: DialogController,
templateUrl: 'pages/properties/tabDialog.tmpl.html',
//parent: angular.element(document.body),
targetEvent: ev,
//onComplete: afterShowAnimation,
//scope:$scope,
//preserveScope: true
locals: { propdata: $scope.propdata }
})
.then(function(propdata) {
console.log(propdata);
//$scope.$parent.propdata = propdata; // Still old data
//vm.sname = propdata.street;
});
}, function(rs){
console.log("error : "+rs.data+" status : "+rs.status);
});
};
function DialogController($scope, $mdDialog, propdata) {
$scope.propdata = propdata;
$scope.form_size = "form-group-sm";
$scope.font_size = "font-size-sm";
$scope.hide = function(ret) {
//$scope.$apply(); Throws an error
$mdDialog.hide(ret);
}
$scope.changesize = function() {
var fsize = $scope.form_size.split("-").pop(); // "form-group-xs";
switch(fsize) {
case "sm" : fsize = "md";
break;
case "md" : fsize = "lg";
break;
case "lg" : fsize = "sm";
break;
}
$scope.form_size = "form-group-" + fsize;
$scope.font_size = "font-size-" + fsize;
}
}
This is how I had it and it worked, but as you can see the dialog is nested inside the controller and only usefull to that controller:
app
.controller('PropertiesCtrl', function($scope,$rootScope,$http, DTOptionsBuilder, DTColumnBuilder, apiserv, $filter, $mdDialog, $mdMedia){
$scope.page = {
title: 'Dashboard',
subtitle: 'Place subtitle here...'
};
$scope.getProperties = function(){
$scope.data=[];
//var url = 'http://www.filltext.com/?rows=10&fname={firstName}&lname={lastName}&delay=3&callback=JSON_CALLBACK';
var data = "?db="+ $rootScope.globals.currentUser.agents[$rootScope.globals.currentDB].db_name;
var url = apiserv+"api.properties.test.php"+data;
$http.jsonp(url).success(function(data){
$scope.data=data;
});
};
var vm = this;
function stateChange(iColumn, bVisible) {
console.log('The column', iColumn, ' has changed its status to', bVisible);
}
// TO-DO: Rather load from a Factory promise, like here: https://github.com/l-lin/angular-datatables/issues/14
// Example here: http://embed.plnkr.co/B9ltNzIRCwswgHqKD5Pp/preview
var data = "?db="+ $rootScope.globals.currentUser.agents[$rootScope.globals.currentDB].db_name;
var url = apiserv+"api.properties.test.php"+data;
vm.dtOptions = DTOptionsBuilder.fromSource(url)
.withBootstrap()
// Active Buttons extension
.withButtons([
//'columnsToggle',
'colvis',
'copy',
'print',
'excel'
])
.withOption('fnRowCallback',$scope.myCallback)
.withOption('order', [[ 3, "desc" ]])
.withOption('stateSave',true);
vm.dtColumns = [
DTColumnBuilder.newColumn('File_Num').withTitle('File'),
DTColumnBuilder.newColumn('Description').withTitle('Description'),
DTColumnBuilder.newColumn('street').withTitle('Street'),
DTColumnBuilder.newColumn('bedrooms').withTitle('Bed'),
DTColumnBuilder.newColumn('bathrooms').withTitle('Bath'),
DTColumnBuilder.newColumn('garages').withTitle('Garages'),
DTColumnBuilder.newColumn('car_port').withTitle('Car Ports').notVisible(),
DTColumnBuilder.newColumn('Current_Monthly_Rental').withTitle('Rental').renderWith(function(data, type, full) {
return $filter('currency')(data, 'R ', 2); //could use currency/date or any angular filter
})
/*DTColumnBuilder.newColumn('file_id').withTitle('').withClass('dt-right').renderWith(function(data, type, full) {
//return "<a href='#/app/statement/"+full.id+"' class='btn btn-default'>View</a>";
return "<a class=\"btn btn-default\" onclick='$(\"#myview\").click()'>View</a>";
}).notSortable()*/
];
$scope.showTabDialog = function(ev) {
console.log(ev.file_id);
var data = "?db="+ $rootScope.globals.currentUser.agents[$rootScope.globals.currentDB].db_name+"&file="+ev.file_id;
var url = apiserv+"api.properties.view.php"+data;
console.log(url);
$http({url:url}).then(function(rs){
console.log(rs.data[0]);
$scope.propdata=rs.data[0];
$mdDialog.show({
controller: DialogController,
templateUrl: 'pages/properties/tabDialog.tmpl.html',
//parent: angular.element(document.body),
targetEvent: ev,
//onComplete: afterShowAnimation,
//scope:$scope,
//preserveScope: true
locals: { propdata: $scope.propdata }
})
.then(function(propdata) {
console.log(propdata);
//$scope.$parent.propdata = propdata; // Still old data
//vm.sname = propdata.street;
});
}, function(rs){
console.log("error : "+rs.data+" status : "+rs.status);
});
};
function DialogController($scope, $mdDialog, propdata) {
$scope.propdata = propdata;
$scope.form_size = "form-group-sm";
$scope.font_size = "font-size-sm";
$scope.hide = function(ret) {
//$scope.$apply(); Throws an error
$mdDialog.hide(ret);
}
$scope.changesize = function() {
var fsize = $scope.form_size.split("-").pop(); // "form-group-xs";
switch(fsize) {
case "sm" : fsize = "md";
break;
case "md" : fsize = "lg";
break;
case "lg" : fsize = "sm";
break;
}
$scope.form_size = "form-group-" + fsize;
$scope.font_size = "font-size-" + fsize;
}
}
$scope.myCallback = function(nRow, aData, iDisplayIndex, iDisplayIndexFull) {
$('td', nRow).bind('click', function() {
$scope.$apply(function() {
$scope.showTabDialog(aData);
});
});
return nRow;
};
});
Here is factory to store and retrieve user from any controller:
app.factory('Auth', [, function () {
function getUser() {
return user;
}
var currentUser = getUser();
return {
currentUser: currentUser
}]);
And use it:
app.controller('controller', ['Auth', function(Auth) {
var currentUser = Auth.currentUser;
}]);
And don't forget to include new factory on page before controller.
This is the scenario :
<export-team>
<ul>
<li>
<button buy-ticket="{{data}}" buy-callback="onBuyTicket()">buy</button>
</li>
<li>
<button buy-ticket="{{data}}" buy-callback="onBuyTicket()">buy</button>
</li>
</ul>
</export-team>
The buyTicket directive
(function() {
'use strict';
angular
.module('myApp')
.directive('buyTicket', buyTicket);
/** #ngInject */
function buyTicket($parse, ngDialog, authService, APPCONFIG, $rootScope, shareToken, contestsFactory, shareCurrentTicket, shareIdSession, shareSessionAAMS, $location) {
var vm = this;
var directive = {
restrict: 'A',
link : function(scope, element, attributes) {
var buyCompatible = attributes['buyCompatible'];
function addZero(i) {
if (i < 10) {
i = "0" + i;
}
return i;
}
var buyTicket = function(contest) {
var d = new Date();
var y = d.getFullYear();
var m = addZero(d.getMonth()+1);
var day = addZero(d.getDate());
var h = addZero(d.getHours());
var min = addZero(d.getMinutes());
var s = addZero(d.getSeconds());
var date = ''+y+m+day+h+min+s+'';
var transactionId = $rootScope.TRANSACTIONID;
var currentTOKEN = shareToken.get();
var data = {
idSessione:currentTOKEN, // ->TOKEN
userAgent:navigator.userAgent,
sessioneAAMS:contest.aams_session_id,
gameId:APPCONFIG.GAME_ID,
transactionId:transactionId,
dateTime:date,
buyIn:contest.buy_in
}
var dialogLoading = ngDialog.open({
closeByDocument : false,
closeByEscape : false,
showClose : false,
id : 'ft-modal-loading',
controller: ['$scope', function($scope){
$scope.bodyUrl = 'app/components/modals/body/loading.html';
$scope.title = 'Acquisto Ticket';
$scope.error = 'Il sistema sta procedendo all\'acquisto del ticket';
}]
});
contestsFactory.buyTicket(data).success(function(response){
dialogLoading.close();
if (response.esito == "0") {
if (!buyCompatible) {
shareCurrentTicket.set(response.ticketSogei);
shareSessionAAMS.set(contest.aams_session_id);
shareIdSession.set(contest.id_session);
$location.path('my-contests/'+contest.id_contest+'/'+contest.contest_status);
}
} else {
var message = response.descrizione;
var ids = ngDialog.getOpenDialogs();
var dialogError = ngDialog.open({
id : "ft-modal-error-2",
controller: ['$scope', function($scope){
$scope.bodyUrl = 'app/components/modals/body/error.html';
$scope.title = 'Errore';
$scope.error = message;
}]
});
}
})
.error(function(){
var dialogErrorNotEndled = ngDialog.close('ft-modal-loading');
ngDialog.open({
id : 'ft-modal-error',
controller: ['$scope', function($scope){
$scope.bodyUrl = 'app/components/modals/body/error.html';
$scope.title = 'Errore';
$scope.error = 'Il servizio non è attualmente disponibile';
}]
});
})
}
var openConfirmBuyTicket = function(contest) {
contest = JSON.parse(contest);
if (ngDialog.isOpen('ft-modal-contest-detail')) {
ngDialog.close('ft-modal-contest-detail');
};
if (!authService.isLogged()) {
ngDialog.open({
controller: ['$scope', function($scope){
$scope.bodyUrl = 'app/components/modals/body/not_logged.html';
$scope.title = 'Spiacenti';
$scope.error = 'Devi essere loggato per poter partecipare ad un contest';
}]
});
} else {
ngDialog.openConfirm({
controller: ['$scope', function($scope){
$scope.title = 'CONFERMA';
$scope.bodyUrl = 'app/components/modals/body/confirm_buy.html';
$scope.contest_name = contest.name_contest;
$scope.buy_in = contest.buy_in;
$scope.currency = APPCONFIG.CURRENCY_SYMBOL;
}],
}).then(function (confirm) {
buyTicket(contest);
}, function(reject) {
});
}
}
element.on('click', function(e){
var contest = attributes['buyTicket'];
openConfirmBuyTicket(contest);
})
}
};
return directive;
}
})();
The export directive
(function() {
'use strict';
angular
.module('myApp')
.directive('exportTeam', exportTeam);
/** #ngInject */
function exportTeam(contestsFactory, ngDialog, APPCONFIG, formatDateFactory) {
var vm = this;
var directive = {
restrict: 'AE',
transclude: true,
controller : function($scope) {
$scope.test = function() {
alert('hey');
}
},
link : function(scope, element, attributes) {
element.on('click', function(e){
var ticket = attributes['exportTeam'];
var id_session = attributes['idsession'];
scope.openExportTeamDialog(ticket, id_session, false);
})
scope.openExportTeamDialog = function(ticket, aams_session_id, afterSave) {
ngDialog.open({
id : 'ft-modal-exportTeam-detail',
className : 'ngdialog ngdialog-theme-default ft-dialog-exportTeam',
controller: ['$scope', 'contestsFactory', 'APPCONFIG', function($scope, contestsFactory, APPCONFIG){
$scope.title = "Aggiungi contest compatibili";
$scope.bodyUrl = 'app/components/modals/body/exportTeam.html';
$scope.contentLoading = true;
$scope.currency = APPCONFIG.CURRENCY_SYMBOL;
$scope.afterSave = afterSave;
$scope.CompatibleContests = [];
contestsFactory.getCompatibleContests(ticket).then(function(response){
angular.forEach(response.data[0], function(item, i){
var multientryOptions = [];
if(item.multientry > 1) {
item.isMultientry = false;
var n = parseInt(item.multientry);
for (i = 1; i <= n; i++) {
multientryOptions.push({
text : i+" team",
value : i
})
}
item.multientryOptions = multientryOptions;
item.multientryOptionSelected = multientryOptions[0];
}else{
item.isMultientry = true;
};
})
$scope.CompatibleContests = response.data[0];
$scope.contentLoading = false;
})
}]
});
}
scope.openExportTeamDialog('N3E94100A725F9QG', 'M3E921013C6DCFCT', false);
}
};
return directive;
}
})();
The buy-ticket directive makes an http call, on the response i want to be able to call the onBuyTicket method of the <export> directive.
I'm trying to understand the best way to do that.
Thanks everyone
This sample show to you how can call an function from your directive
In this sample you can see we just insert data in our directive, and then we handle the data and other action in the directive.
var app = angular.module("app", []);
app.controller("ctrl", function ($scope) {
$scope.dataFromYourController = [
{ name: "Concert Jennifer", value: 200 },
{ name: "007", value: 100 }
];
})
.directive("export", function () {
var template = "<div>" +
"<ul>" +
"<li ng-repeat=\"array in arrays\">" +
"<button ng-click=\"onBuyTicket()\">buy Ticket {{array.name}}</button><hr>" +
"</li>" +
"</ul>" +
"</div>";
return {
restrict: "E",
template: template,
scope: {
data: "="
},
link: function (scope, elem, attrs, ngModel) {
scope.arrays = scope.data;
scope.onBuyTicket = function () {
alert("calling function from directive");
}
}
};
})
<!doctype html>
<html ng-app="app" ng-controller="ctrl">
<head>
</head>
<body>
<h1>call action from your directive</h1>
<export data="dataFromYourController"></export>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
</body>
</html>
#eesdil
var directive = {
restrict: 'AE',
transclude: true,
controller : function($scope) {
$scope.onBuyTicket = function() {
alert('hey');
}
}
}
Ho can I call that from the buy-ticket directive ?
Use the $parent
<button buy-ticket="{{data}}" buy-callback="$parent.onBuyTicket()">buy</button>
So the expor directive something like:
var directive = {
restrict: 'AE',
template: '<ng-tansclude></ng-transclude>',
transclude: true,
controller : function($scope) {
$scope.onBuyTicket = function() {
alert('hey');
}
}
}
UPDATED:
see the plunker:
https://plnkr.co/edit/fmyJ4oPLvTiI0TzO7h1b?p=preview
It really depends what you can call and what you cannot based on the scopes... here if you would remove the scope from the export directive would work without the $parent also as export would share the same scope as the parent (main view)
The best way to communicate events from a child directive to a parent directive (or controller) is to use the $emit method of the scope.
What you want to do is take an ng-click event, get additional information with an $http call, and $emit an event with the additional information to be used by your parent directive (or controller).
HTML
<button buy-ticket="data" ng-click="onBuyTicket()">buy</button>
The directive:
angular.module("myApp").directive("buyTicket", function($http) {
function linkFn(scope,elem,attrs) {
scope.onBuyTicket = function() {
var buyData = scope.$eval(attrs.buyTicket);
var url = someFunction(buyData);
$http.get(url).then (function (response) {
var httpData = response.data;
scope.$emit("buyTicket.click", buyData, httpData);
});
};
};
return {
restrict: "AE",
link: linkFn
};
});
In the parent controller:
$scope.$on("buyTicket.click", function (buyData, httpData) {
console.log(buyData);
console.log(httpData);
});
Notice that I used the $eval method to get the data from the variable named by the buy-ticket attribute.
When choosing a name for the event, I recommend including the name of the directive in the event's name. It makes it clear the source of the event and is unlikely to be duplicated elsewhere.