I know this has been covered many times and most articles refer to this bit of code: Modal window with custom URL in AngularJS
But I just don't get it. I don't find that to be very clear at all. I also found this jsfiddle which was actually great, very helpful except this doesn't add the url and allow for me to use the back button to close the modal.
Edit: This is what I need help with.
So let me try explain what I am trying to achieve. I have a form to add a new item, and I have a link 'add new item'. I would like when I click 'add new item' a modal pops up with the form I have created 'add-item.html'. This is a new state so the url changes to /add-item.
I can fill out the form and then choose to save or close. Close, closes the modal :p (how odd) . But I can also click back to close the modal as well and return to the previous page(state).
I don't need help with Close at this point as I am still struggling with actually getting the modal working.
This is my code as it stands:
Navigation Controller: (is this even the correct place to put the modal functions?)
angular.module('cbuiRouterApp')
.controller('NavbarCtrl', function ($scope, $location, Auth, $modal) {
$scope.menu = [{
'title': 'Home',
'link': '/'
}];
$scope.open = function(){
// open modal whithout changing url
$modal.open({
templateUrl: 'components/new-item/new-item.html'
});
// I need to open popup via $state.go or something like this
$scope.close = function(result){
$modal.close(result);
};
};
$scope.isCollapsed = true;
$scope.isLoggedIn = Auth.isLoggedIn;
$scope.isAdmin = Auth.isAdmin;
$scope.getCurrentUser = Auth.getCurrentUser;
$scope.logout = function() {
Auth.logout();
$location.path('/login');
};
$scope.isActive = function(route) {
return route === $location.path();
};
});
This is how I am activating the modal:
<li ng-show='isLoggedIn()' ng-class='{active: isActive("/new-item")}'>
<a href='javascript: void 0;' ng-click='open()'>New Item</a>
</li>
new-item.html:
<div class="modal-header">
<h3 class="modal-title">I'm a modal!</h3>
</div>
<div class="modal-body">
<ul>
<li ng-repeat="item in items"><a ng-click="selected.item = item">{{ item }}</a></li>
</ul>Selected:<b>{{ selected.item }}</b>
</div>
<div class="modal-footer">
<button ng-click="ok()" class="btn btn-primary">OK</button>
<button ng-click="close()" class="btn btn-primary">OK</button>
</div>
Also whilst this does open a modal it doesn't close it as I couldn't work that out.
It's intuitive to think of a modal as the view component of a state. Take a state definition with a view template, a controller and maybe some resolves. Each of those features also applies to the definition of a modal. Go a step further and link state entry to opening the modal and state exit to closing the modal, and if you can encapsulate all of the plumbing then you have a mechanism that can be used just like a state with ui-sref or $state.go for entry and the back button or more modal-specific triggers for exit.
I've studied this fairly extensively, and my approach was to create a modal state provider that could be used analogously to $stateProvider when configuring a module to define states that were bound to modals. At the time, I was specifically interested in unifying control over modal dismissal through state and modal events which gets more complicated than what you're asking for, so here is a simplified example.
The key is making the modal the responsibility of the state and using hooks that modal provides to keep the state in sync with independent interactions that modal supports through the scope or its UI.
.provider('modalState', function($stateProvider) {
var provider = this;
this.$get = function() {
return provider;
}
this.state = function(stateName, options) {
var modalInstance;
$stateProvider.state(stateName, {
url: options.url,
onEnter: function($modal, $state) {
modalInstance = $modal.open(options);
modalInstance.result['finally'](function() {
modalInstance = null;
if ($state.$current.name === stateName) {
$state.go('^');
}
});
},
onExit: function() {
if (modalInstance) {
modalInstance.close();
}
}
});
};
})
State entry launches the modal. State exit closes it. The modal might close on its own (ex: via backdrop click), so you have to observe that and update the state.
The benefit of this approach is that your app continues to interact mainly with states and state-related concepts. If you later decide to turn the modal into a conventional view or vice-versa, then very little code needs to change.
Here is a provider that improves #nathan-williams solution by passing resolve section down to the controller:
.provider('modalState', ['$stateProvider', function($stateProvider) {
var provider = this;
this.$get = function() {
return provider;
}
this.state = function(stateName, options) {
var modalInstance;
options.onEnter = onEnter;
options.onExit = onExit;
if (!options.resolve) options.resolve = [];
var resolveKeys = angular.isArray(options.resolve) ? options.resolve : Object.keys(options.resolve);
$stateProvider.state(stateName, omit(options, ['template', 'templateUrl', 'controller', 'controllerAs']));
onEnter.$inject = ['$uibModal', '$state', '$timeout'].concat(resolveKeys);
function onEnter($modal, $state, $timeout) {
options.resolve = {};
for (var i = onEnter.$inject.length - resolveKeys.length; i < onEnter.$inject.length; i++) {
(function(key, val) {
options.resolve[key] = function() { return val }
})(onEnter.$inject[i], arguments[i]);
}
$timeout(function() { // to let populate $stateParams
modalInstance = $modal.open(options);
modalInstance.result.finally(function() {
$timeout(function() { // to let populate $state.$current
if ($state.$current.name === stateName)
$state.go(options.parent || '^');
});
});
});
}
function onExit() {
if (modalInstance)
modalInstance.close();
}
return provider;
}
}]);
function omit(object, forbidenKeys) {
var prunedObject = {};
for (var key in object)
if (forbidenKeys.indexOf(key) === -1)
prunedObject[key] = object[key];
return prunedObject;
}
then use it like that:
.config(['modalStateProvider', function(modalStateProvider) {
modalStateProvider
.state('...', {
url: '...',
templateUrl: '...',
controller: '...',
resolve: {
...
}
})
}]);
I answered a similar question, and provided an example here:
Modal window with custom URL in AngularJS
Has a complete working HTML and a link to plunker.
The $modal itself doesn't have a close() funcftion , I mean If you console.log($modal) , You can see that there is just an open() function.
Closing the modal relies on $modalInstance object , that you can use in your modalController.
So This : $modal.close(result) is not actually a function!
Notice :
console.log($modal);
==>> result :
Object { open: a.$get</k.open() }
// see ? just open ! , no close !
There is some way to solve this , one way is :
First you must define a controller in your modal like this :
$modal.open({
templateUrl: 'components/new-item/new-item.html',
controller:"MyModalController"
});
And then , Later on , :
app.controller('MyModalController',function($scope,$modalInstance){
$scope.closeMyModal = function(){
$modalInstance.close(result);
}
// Notice that, This $scope is a seperate scope from your NavbarCtrl,
// If you want to have that scope here you must resolve it
});
Related
I am using angular modal service to show incoming call popup.
Everything seems to work but in particular case the popup closes leaving behind grey overlay blocking the whole UI.
Popup closes perfectly when i manually click reject and close button provided in popup but gives unusual behaviour when i use timeout to close the popup whithout doing any operation on it.
For reference i am giving my whole code.
----------------------------modal popup UI code---------------------------
<div class="modal fade">
<div class="modal-dialog modal-lg modal-dialog-custom">
<div class="modal-content modal-content-dialog">
<div class="modal-header">
<audio class="incoming-videoconference-audio" autoplay loop>
<source src="../images/dataCallIncoming.mp3" type="audio/mpeg">
</audio>
<button type="button" class="close" ng-click="vm.hangUp()" data-dismiss="modal" aria-hidden="true">×</button>
<h4 class="modal-title">Incoming Call</h4>
</div>
<img class="incoming-nowConf-logo" src="../images/new_nowconfer_e.png" />
<div id="state" class="grid_4 alpha">
<div class="gps_ring"></div>
</div>
<div class="modal-body modal-body-custom">
<div style="text-overflow:ellipsis;overflow:hidden;" class="call-from">
{{vm.confName}}
</div>
<div class="call-control">
<button type="button"class="btn-sm btn-sm-gray cancel-btn" ng-click="vm.hangUp()" data-dismiss="modal">Reject</button>
<span style="width:50px;"> </span>
<button type="button"class="btn-sm btn-sm-green" ng-click="vm.accept()" data-dismiss="modal">Answer</button>
</div>
</div>
</div>
</div>
</div>
-------------------------modal popup controller------------------------------
(function () {
'use strict';
angular
.module('incomingModule')
.controller('IncomingCallController', IncomingCallController);
IncomingCallController.$inject = ['$scope','$rootScope','plivoclient','$routeParams','$location','close','from', 'instId','confName','$timeout'];
function IncomingCallController($scope,$rootScope , plivoclient,$routeParams ,$location,close, from, instId,confName,$timeout) {
var vm = this;
vm.connecting = false;
vm.from = from;
vm.confName = confName;
vm.dismissModal = function(result) {
plivoclient.conn.reject();
console.log('vm.dismissModal::'+result);
close(result, 200); // close, but give 200ms for bootstrap to animate
};
activate();
function activate(){
$timeout(function(){
vm.dismissModal('cancel');
},25000);
}
vm.accept = function() {
plivoclient.conn.answer();
vm.connecting = true;
console.log("incoming call accept............");
vm.dismissModal('accept');
$timeout(function(){
$location.path( "/call/"+$rootScope.id2);
},300);
};
vm.hangUp = function() {
plivoclient.conn.reject();
vm.dismissModal('reject');
console.log("incoming call hangedup............");
};
}
}());
-------------------------opening modal code----------------------------------------
ModalService.showModal({
templateUrl: '../../partials/calls.incoming.popup.html',
controller: 'IncomingCallController',
controllerAs: 'vm',
inputs: {
from: dataNew.callerName || '',
instId: dataNew.extraHeaders['X-Ph-Instid'] || dataNew.extraHeaders['X-Ph-instid'],
confName:$rootScope.conferenceData.conf_name
}
}).then(function(modal) {
modal.element.modal();
modal.close.then(function(result) {
//$scope.message = result ? "You said Yes" : "You said No";
});
});
----------------------------------angular modal service code----------------------------------
'use strict';
let module = angular.module('angularModalService', []);
module.factory('ModalService', ['$animate', '$document', '$compile', '$controller', '$http', '$rootScope', '$q', '$templateRequest', '$timeout',
function($animate, $document, $compile, $controller, $http, $rootScope, $q, $templateRequest, $timeout) {
function ModalService() {
var self = this;
// Returns a promise which gets the template, either
// from the template parameter or via a request to the
// template url parameter.
var getTemplate = function(template, templateUrl) {
var deferred = $q.defer();
if (template) {
deferred.resolve(template);
} else if (templateUrl) {
$templateRequest(templateUrl, true)
.then(function(template) {
deferred.resolve(template);
}, function(error) {
deferred.reject(error);
});
} else {
deferred.reject("No template or templateUrl has been specified.");
}
return deferred.promise;
};
// Adds an element to the DOM as the last child of its container
// like append, but uses $animate to handle animations. Returns a
// promise that is resolved once all animation is complete.
var appendChild = function(parent, child) {
var children = parent.children();
if (children.length > 0) {
return $animate.enter(child, parent, children[children.length - 1]);
}
return $animate.enter(child, parent);
};
self.showModal = function(options) {
// Get the body of the document, we'll add the modal to this.
var body = angular.element($document[0].body);
// Create a deferred we'll resolve when the modal is ready.
var deferred = $q.defer();
// Validate the input parameters.
var controllerName = options.controller;
if (!controllerName) {
deferred.reject("No controller has been specified.");
return deferred.promise;
}
// Get the actual html of the template.
getTemplate(options.template, options.templateUrl)
.then(function(template) {
// Create a new scope for the modal.
var modalScope = (options.scope || $rootScope).$new();
var rootScopeOnClose = $rootScope.$on('$locationChangeSuccess', cleanUpClose);
// Create the inputs object to the controller - this will include
// the scope, as well as all inputs provided.
// We will also create a deferred that is resolved with a provided
// close function. The controller can then call 'close(result)'.
// The controller can also provide a delay for closing - this is
// helpful if there are closing animations which must finish first.
var closeDeferred = $q.defer();
var closedDeferred = $q.defer();
var inputs = {
$scope: modalScope,
close: function(result, delay) {
if (delay === undefined || delay === null) delay = 0;
$timeout(function() {
cleanUpClose(result);
}, delay);
}
};
// If we have provided any inputs, pass them to the controller.
if (options.inputs) angular.extend(inputs, options.inputs);
// Compile then link the template element, building the actual element.
// Set the $element on the inputs so that it can be injected if required.
var linkFn = $compile(template);
var modalElement = linkFn(modalScope);
inputs.$element = modalElement;
// Create the controller, explicitly specifying the scope to use.
var controllerObjBefore = modalScope[options.controllerAs];
var modalController = $controller(options.controller, inputs, false, options.controllerAs);
if (options.controllerAs && controllerObjBefore) {
angular.extend(modalController, controllerObjBefore);
}
// Finally, append the modal to the dom.
if (options.appendElement) {
// append to custom append element
appendChild(options.appendElement, modalElement);
} else {
// append to body when no custom append element is specified
appendChild(body, modalElement);
}
// We now have a modal object...
var modal = {
controller: modalController,
scope: modalScope,
element: modalElement,
close: closeDeferred.promise,
closed: closedDeferred.promise
};
// ...which is passed to the caller via the promise.
deferred.resolve(modal);
function cleanUpClose(result) {
// Resolve the 'close' promise.
closeDeferred.resolve(result);
// Let angular remove the element and wait for animations to finish.
$animate.leave(modalElement)
.then(function () {
// Resolve the 'closed' promise.
closedDeferred.resolve(result);
// We can now clean up the scope
modalScope.$destroy();
// Unless we null out all of these objects we seem to suffer
// from memory leaks, if anyone can explain why then I'd
// be very interested to know.
inputs.close = null;
deferred = null;
closeDeferred = null;
modal = null;
inputs = null;
modalElement = null;
modalScope = null;
});
// remove event watcher
rootScopeOnClose && rootScopeOnClose();
}
})
.then(null, function(error) { // 'catch' doesn't work in IE8.
deferred.reject(error);
});
return deferred.promise;
};
}
return new ModalService();
}]);
I have spent hours on internet to figure out why this is happening but failed to solve it,i feel when any click event happens then it works fine but fails to close properly when on operation is performed.Please help!!
thanks in advance
I had the same issue and it was due to a comment at the top of my HTML file. When I removed the comment, it worked fine.
I didn't get the reason of this bug though.
hope you have the same case.
I am using ng-show and ng-hide to display/hide content. I would like to change the showme status from true to false within the controller. But when I use the code below, it doesn't work. I'm using the Controller As syntax. Any suggestions on how to get this working right?
HTML:
<h1 ng-show="showme">Confirm Order</h1>
<h4 ng-hide="showme">Contact Information</h4>
Javascript:
.controller('ContactFormCtrl',
function ($http, serviceF, $scope) {
var contactForm = this;
$scope.$watch(serviceF.get, function(valid)
{
if (valid === 'yes') {
contactForm.showme=true;
}
else
{
contactForm.showme=false;
}
});
});
Service:
.service('serviceF', function() {
var valid = 'true';
return {
get: function () {
return valid;
},
set: function (value) {
valid = value;
}
};
UI Router:
.state('payment', {
url: '/payment',
views: {
// . . .
'top': {
templateUrl: 'views/clientinfo.html',
controller: 'ContactFormCtrl as contactForm'
// . . .
}
})
I'm not sure what you're trying to do, but the Controller As syntax goes this way in HTML:
<div ng-controller="ContactFormCtrl as contactForm">
<h1 ng-show="contactForm.showme">Confirm Order</h1>
<h1 ng-show="contactForm.showme">Confirm Order</h1>
</div>
Note the 'as contactForm' thingy passed in the ng-controller directive
Now you know that showme is actually a property of contactForm which is essentially an "alias" of the ContactFormCtrl controller
From there, whenever the showme property changes in the controller, the view will behave accordingly.
// In your controller
var contactForm = this; // aliasing this
contactForm.showme = true; //or false
UPDATE:
Since you're using ui-router, you should be good without ng-controller in your view. I'm noticing you are not passing $scope to your controller, that could be a reason why $scope.$watch isn't working, thus not updating the view.
.controller('ContactFormCtrl', function ($scope, $http, serviceF) {
var contactForm = this;
$scope.$watch(serviceF.get, function(valid) {
if (valid === 'yes') {
contactForm.showme = true;
}else{
contactForm.showme = false;
}
});
});
I've made a really simple repeat of some books, when the user clicks the book, it opens a new modal. Where they can edit the book. As i'm using the two way binding, the 'display page' automatically changes as i type on the modal - which is brilliant.
However what i want to do is allow the user to press a cancel button, and the state of the book goes back to what it was before it was altered. Is this possible in Angular without going back and resetting the entire $scope.books object?
In the real application this would be an API call, and i'd rather not make another server call unless entirely necessary of course. Is there a pattern that takes care of this already?
(function(){
var app = angular.module('ngModalDemo', ['ui.bootstrap'])
.controller('formController', function($scope, $modal, $log){
$scope.books = [
{ Id: 1, Name:'A Feast For Crows'},
{ Id: 2, Name:'Before they are Hanged'}
];
$scope.openModal = function (currentBook) {
var modalInstance = $modal.open({
templateUrl: 'SomeModal.html',
controller: [
'$scope', '$modalInstance', function($scope, $modalInstance){
$scope.editBook = currentBook;
$scope.saveModal = function (book) {
$modalInstance.close();
};
$scope.cancelModal = function () {
// Restore the previous state here!
$modalInstance.close();
};
}]
});
};
})
})();
<div ng-controller="formController">
<p ng-repeat="displayBook in books">
{{displayBook.Name}}
</p>
<script type="text/ng-template" id="SomeModal.html">
<form name="editForm" ng-submit="saveModal(editBook)" noValidate>
<div class="modal-header">
Name: <input ng-model="editBook.Name" required /><br />
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" ng-click="cancelModal()">Cancel</button>
<button class="btn btn-info" ng-disabled="editForm.$dirty && editForm.$invalid">Save</button>
</div>
</form>
</script>
</div>
You can create a deep copy of your object to a temp, and then set it back if neccessary:
var temp = angular.copy(currentBook);
$scope.editBook = currentBook;
$scope.saveModal = function (book) {
$modalInstance.close();
};
$scope.cancelModal = function () {
// Restore the previous state here!
angular.copy(temp, $scope.editBook);
$modalInstance.close();
};
Angular has method copy that makes job for you by cloning object or array.
The idea is to pass copy of your data to modal and not instance itself. So when user press Cancel the main instance doesn't change.
In your case instead:
$scope.editBook = currentBook;
write:
$scope.editBook = angular.copy(currentBook);
Consider caching a copy of the original model before displaying the modal, and then resetting it if the user cancels. This can easily be done directly in JavaScript, or you can opt to use Angular's $cacheFactory for more complex scenarios.
For instance, you can add an index to your ng-repeat:
<p ng-repeat="displayBook in books track by $index">
<a href="#" ng-click="openModal(displayBook, $index)">
{{displayBook.Name}}
</a>
</p>
And then alter your controller method to reset the $scope.books collection if the user cancels the modal:
$scope.openModal = function (currentBook, idx) {
// Cache the book, so that we can reset it later.
var cachedBook = angular.copy(currentBook);
var $outerScope = $scope;
var modalInstance = $modal.open({
// ...
controller: [
'$scope', '$modalInstance', function($scope, $modalInstance){
$scope.editBook = currentBook;
$scope.saveModal = function (book) {
// ...
};
$scope.cancelModal = function () {
// Restore the previous state
$outerScope.books[idx] = cachedBook;
// ...
};
}]
});
};
If you expect your users to be hitting Cancel more often than they actually save the edits, then perhaps consider reversing the operations and passing in a copy of the book instead of the original, only modify the original after saveModal is called.
I'm probably getting confused with mvc and angularjs and trying to set a boolean to control a scope variable to hide a div.
I have a list html page that includes this:
<tbody>{{isAuthorised}}
<tr ng-repeat="calendarEvent in items" id="event_{{calendarEvent.Id}}">
<td><strong>{{calendarEvent.EventTitle}}</strong><br/>{{calendarEvent.EventDescription}}</td>
<td>{{calendarEvent.EventDate | date:mediumDate}}</td>
<td><img src="{{calendarEvent.ThumbnailUrl}}" alt="" width="100" /></td>
<td>
<div ng-show="isAuthorised">
<i class="glyphicon glyphicon-edit"></i>
<a ng-click="delete()"><i class="glyphicon glyphicon-remove"></i></a>
</div>
</td>
</tr>
</tbody>
I'm outputting the value currently to try to figure out what is going on. So if I hit this page with setting the value the div shows my edit and delete buttons which I don't want. The value of the scope variable displays as {}.
I have this app.js code:
var ListCtrl = function ($scope, $location, CalendarEvent, SharedService) {
** lots of stuff removed as irrelevant **
$scope.isAuthorised = SharedService.get();
};
My login controller via a separate html content section that is setting the value (in the shared service)
var LoginCtrl = function ($scope, $location, $http, SharedService) {
$scope.login = function () {
$http.get("/AuthorisedUser/IsValidUser/" + $scope.item.ValidEmailAddress + "/")
.success(function (result) {
var isAuthorised = result.toLowerCase();
if (isAuthorised) {
SharedService.set(isAuthorised);
$location.path('/');
} else {
alert('you do not have the power!');
}
})
.error(function() {
alert('Email could not be Validated at this time');
});
}
};
the result is an MVC method returning a bool type. I thought maybe I needed to convert the bool to lower case because javascript would like it better, but maybe that's doing some implicit conversion to a string or something?! I'm not sure what I need to change in my list html to properly show that div only when the value is true. I'm coming from a .NET background with limited AngularJS understanding.
The value seems to being set, because if I put in a valid email address I'm seeing
true
in the html page where the scope variable is.
It seemed to work once in Chrome - but now that's not working, and just showing the stuff that should be hidden.
Sorry forgot to include the shared service:
EventsCalendarApp.factory('SharedService', function() {
var savedData = {}
function set(data) {
savedData = data;
}
function get() {
return savedData;
}
return {
set: set,
get: get
}
});
I think everything would be simplified in your controller, service, and UI if your service dealt with object references rather than a Boolean value (which is a primitive).
Your service:
EventsCalendarApp.factory('SharedService', function() {
var savedData = { isAuthorised: false }
function set(data) {
// overwrites savedData properties with data's properties,
// but preserves the reference
angular.copy(data, savedData);
}
function setAuthorised(authorised) {
savedData.isAuthorised = authorised;
}
function get() {
return savedData;
}
return {
set: set,
get: get,
setAuthorised: setAuthorised
}
});
Your Login controller:
var LoginCtrl = function ($scope, $location, $http, SharedService) {
// helper function to determine if str contains 'true'
function parseBoolean(str) {
return /^true$/i.test(str);
}
$scope.login = function () {
$http.get("/AuthorisedUser/IsValidUser/" + $scope.item.ValidEmailAddress + "/")
.success(function (result) {
var isAuthorised = parseBoolean(result);
if (isAuthorised) {
SharedService.set({ isAuthorised: isAuthorised });
// OR
SharedService.setAuthorised(isAuthorised);
$location.path('/');
} else {
alert('you do not have the power!');
}
})
.error(function() {
alert('Email could not be Validated at this time');
});
}
};
Your List Controller:
var ListCtrl = function ($scope, $location, CalendarEvent, SharedService) {
** lots of stuff removed as irrelevant **
$scope.savedData = SharedService.get();
};
HTML:
<tbody>{{savedData.isAuthorised}}
<tr ng-repeat="calendarEvent in items" id="event_{{calendarEvent.Id}}">
<td><strong>{{calendarEvent.EventTitle}}</strong><br/>{{calendarEvent.EventDescription}}</td>
<td>{{calendarEvent.EventDate | date:mediumDate}}</td>
<td><img ng-src="{{calendarEvent.ThumbnailUrl}}" alt="" width="100" /></td>
<td>
<div ng-show="savedData.isAuthorised">
<i class="glyphicon glyphicon-edit"></i>
<a ng-click="delete()"><i class="glyphicon glyphicon-remove"></i></a>
</div>
</td>
</tr>
</tbody>
When you use object references, then any changes to the reference from within your service is automatically propagated to the views; as do any changes to the reference that happen inside a controller. There is no real magic behind this - they are automatically updated because they are the same reference. In contrast, when you use primitives, then a copy of the value is passed around, and it becomes more challenging to keep them all in synch.
NOTE: on an unrelated note, you should use ng-src for image URLs that are binding expressions. This ensures that the image URL is only downloaded by the browser after the expression is evaluated and rendered.
var LoginCtrl = function ($scope, $location, $http, SharedService) {
$scope.login = function () {
$http.get("/AuthorisedUser/IsValidUser/" + $scope.item.ValidEmailAddress + "/")
.success(function (result) {
$scope.isAuthorised = result.toLowerCase();
})
.error(function() {
alert('Email could not be Validated at this time');
});
}
};
Keep one thing in mind you $scope works as a bridge between controller and view. if your controller update $scope, your view gets changed.
Don't use sharedservice here. its useless for what you want to do. try my above snippet.
So the answer was to update the ListCtrl to have this logic:
var ListCtrl = function ($scope, $location, CalendarEvent, SharedService) {
var authorised = SharedService.get();
if (authorised != "true")
$scope.isAuthorised = false;
else
$scope.isAuthorised = SharedService.get();
};
It now seems to be working! I'm still confused about the handling of booleans in javascript as I seem to have a mix of boolean and string going on in the various methods.
I am using ng-include in order to include a persistent menu, that exists in all of the views of my SPA.
The problem is that I want to display different options and content in this menu per each user type(admin, guest, user etc.), and this requires the service function authService.loadCurrentUser to be resolved first.
For the purpose of managing this content easily and comfortably, I have created a simple directive, that takes an attribute with the required access level, and at the compile phase
of the element, if the permissions of the given user are not sufficient, removes the element and it's children.
So after failing miserably at trying to make the ng-include go through the routeProvider function, I've tried to use ng-init, but nothing seems to work, the user role remain undefined at the time that I am logging it out.
I am thinking about trying a new approach, and making the entire menu a directive that includes the template that is suitable for each user type, but first I would like to try and solve this matter.
Directive:
'use strict';
/* Directives */
angular.module('myApp.directives', []).
directive('restrict', function(authService){
return{
restrict: 'A',
prioriry: 100000,
scope: {
// : '#'
},
link: function(){
// alert('ergo sum!');
},
compile: function(element, attr, linker){
var user = authService.getUser();
if(user.role != attr.access){
console.log(attr.access);
console.log(user.role);//Always returns undefined!
element.children().remove();
element.remove();
}
}
}
});
Service:
'use strict';
/* Services */
angular.module('myApp.services', []).
factory('authService', function ($http, $q) {
var authServ = {};
var that = this;
that.currentUser = {};
authServ.authUser = function () {
return $http.head('/users/me', {
withCredentials: true
});
},
authServ.getUser = function () {
return that.currentUser;
},
authServ.setCompany = function (companyId) {
that.currentUser.company = companyId;
},
authServ.loadCurrentUser = function () {
var defer = $q.defer();
$http.get('/users/me', {
withCredentials: true
}).
success(function (data, status, headers, config) {
console.log(data);
that.currentUser.company = {};
that.currentUser.company.id = that.currentUser.company.id ? that.currentUser.company.id : data.main_company;
that.currentUser.companies = [];
for (var i in data.roles) {
that.currentUser.companies[data.roles[i]['company']] = data.roles[i]['company_name'];
if (data.roles[i]['company'] == that.currentUser.company.id){
that.currentUser.role = data.roles[i]['role_type'];
that.currentUser.company.name = data.roles[i]['company_name'];
// console.log(that.currentUser.role);
}
}
// defer.resolve(data);
defer.resolve();
}).
error(function (data, status, headers, config) {
that.currentUser.role = 'guest';
that.currentUser.company = 1;
defer.reject("reject");
});
return defer.promise;
}
return authServ;
});
Menu controller:
angular.module('myApp.controllers', []).
controller('menuCtrl', function($scope, $route, $location, authService){
//TODO: Check if this assignment should be local to each $scope func in order to be compliant with 2-way data binding
$scope.user = authService.getUser();
console.log($scope.user);
// $scope.companies = $scope.user.companies;
$scope.companyOpts = function(){
// var user = authService.getUser();
if(typeof $scope.user.company == 'undefined')
return;
var companies = [];
companies[$scope.user.company.id] = $scope.user.company.name;
for(var i in $scope.user.companies){
if(i != $scope.user.company.id){
companies[i] = $scope.user.companies[i];
}
}
// console.log(companies);
// if(nonCurrentComapnies.length > 0){
console.log(companies);
return companies;
// }
}
$scope.$watch('user.company.name', function(company){
for(var i in $scope.user.companies)
if(company == $scope.user.companies[i].id)
authService.setCompany(i);
});
$scope.$watch(function(){return authService.getUser().company; }, function(company){
//Refresh the page on company change here, first time, and each time the user changes the select
// $scope.companyOpts();
// $scope.currentComapany = company;
})
;})
Main SPA HTML page:
<div ng-init="authservice.loadCurrentUser" ng-include src="'partials/menu.html'"></div>
menu element that should be visible only to the admin:
<ul class="left" restrict access="admin">
<li>You are the admin!</li>
</ul>
Thanks in advance for any assistance!
I personally would do the "reverse" way. Which mean: I will add the menu in when the user role is "admin", or "user", etc...
This way, you can do something like this in the "restrict" directive:
...
var roleListener = $scope.$watch('user.role', function (newVal, oldVal) {
if (newVal == $scope.access) {
// add the menu items
// supposed that loadcurrentuser be called only once
// we should clear the watch
roleListener();
} else {
// personally, I would remove the item here too
// so the menu would be added or removed when user.role update
}
});
...
One more thing, for just display menu base on the user role, you can use ngSwitch, something like this:
<ul class="left" ng-switch="user.role">
<li ng-switch-when="admin">You are the admin!</li>
<li ng-switch-when="user">You are the user!</li>
<li ng-switch-default><img src="some-thing-running.gif"/>Your menu is loading, please wait...</li>
</ul>
And let the magical AngularJS binding render up the menus for you!
The call to authServ.getUser should also return a promise by calling internally
authServ.loadCurrentUser
which should be modified a bit to check if the user context exists to avoid making another API call and always returning resolve with the user context:
defer.resolve(that.currentUser);
Loading the user context should also be done early on as this enables the authorization of the app. The app.run function can be used for this purpose.
hope it helps others.