When establishing a controller, and setting the $scope to use a factory method (for GETs and POSTs), during the page load process, my POSTs are fired. Below is an example of the code. To fix this, I wrapped the "POST" in a jQuery click event function and everything works smoothly. Below is the code.
In the controller (app.js):
demoApp.controller('SimpleController', function ($scope, simpleFactory) {
$scope.customers = [];
init();
function init() {
$scope.departments = simpleFactory.getDepartments();
}
// Works fine
$('#myButton').click(function () {
simpleFactory.postDepartments();
});
// When setting the "scope" of a controller, during page load the scope factory method is fired off!
// seems like a defect.
//$scope.addDepartment = simpleFactory.postDepartments();
});
So, what is going on here is that if I uncomment the $scope.addDepartment = ... on page load, the postDepartments() factory method is called. This is not the desired behavior. Here is how I have the Html Dom element wired:
<button id="myButton" data-ng-click="addDepartment()">Add Department</button>
So, if I uncomment, like I said above, it adds the department before the user even clicks the button. However, approaching it the jQuery way, there is no issue.
Is this a known bug? Is this the intended functionality? Also, see the factory below, maybe the problem is there?
demoApp.factory('simpleFactory', function ($http) {
var departments = [];
var factory = {};
factory.getDepartments = function () {
$http.get('/Home/GetDepartments').success(function (data) {
for (var i = 0; i < data.length; i++) {
departments.push({ desc: data[i].desc, id: data[i].id });
}
})
.error(function () {
$scope.error = "An Error has occured while loading posts!";
$scope.loading = false;
});
return departments;
};
factory.postDepartments = function () {
$http.post('/Home/PostDepartment', {
cName: 'TST',
cDescription: 'Test Department'
}).success(function (data) {
departments.push({ desc: 'Test Department', id: departments.length + 1 });
})
.error(function () {
$scope.error = "An Error has occured while loading posts!";
$scope.loading = false;
});
return departments;
};
return factory;
});
Try this:
$scope.addDepartment = function() {
return simpleFactory.postDepartments();
}
This will also allow you to pass in arguments in the future, should you decide to. The way you originally had it, you were both assigning the function and calling it at the same time.
Then, you can use it in ngClick:
<button ng-click="addDepartment()">Add Department</button>
Don't use the jQuery click method in your controller, it defeats the purpose of separating the concerns into models, views, controllers, etc. That's what directives are for.
Related
Hi Im having an issue where im showing a mdDialog from Angular Material and using my directives controller as the controller of the dialog so i can call a specific function without having to pass stuff back and add in extra steps to the code. The function gets called successfully but the UI is not updated when the function successfully ends. Wondering if anyone can see where im going wrong with this.
Assume for now that the first if statement is true.
Dialog call
this.showImageUploadModal = function() {
$mdDialog.show({
clickOutsideToClose: true,
scope: $scope, // use parent scope in template
preserveScope: true, // do not forget this if use parent scope
templateUrl: 'app/directives/modals/upload-files-modal.html',
controller: MessagingController,
controllerAs: 'controller'
});
};
Function being called but not updating UI
this.addAttachment = function() {
console.log("sending attachment");
var ref = this;
var note = this.user.first_name + " has attached a file.";
if($state.current.name === 'inbox') {
MessagingService.createMessage(this.convo.id, note, this.userUploadedNoteFiles).then(
function success(response) {
console.log("Inbox attachment sent", response);
ref.convo.messages.push(response.data);
console.log(ref.convo.messages);
// ref.viewableNoteFiles = [];
},
function failure(response) {
$mdToast.show(
$mdToast.simple().
textContent("Failed to send the message please try again.").
theme('error-toast'));
}
);
} else if (this.notes === 'true') {
TicketingService.addNote($stateParams.id, note, this.userUploadedNoteFiles).then(
function success(response) {
console.log("Notes attachment sent", response);
ref.convo.messages.push(response.data);
// ref.viewableNoteFiles = [];
},
function failure(response) {
$mdToast.show(
$mdToast.simple().
textContent("Failed to send the message please try again.").
theme('error-toast'));
}
);
} else if(this.contractor === 'true') {
TicketingService.createMessage($stateParams.id, this.convo.id, note, this.userUploadedNoteFiles).then(
function success (response) {
console.log("Contractor attachment sent", response);
ref.convo.messages.push(response.data);
},
function failure () {
$mdToast.show(
$mdToast.simple().
textContent("Failed to upload the file attachments").
theme('error-toast'));
}
);
}
};
In the end i found i could achieve what i was looking for using Angulars $rootScope.$broadcast. to broadcast the return data back to the controller that needed it.
Im not sure this is the right way to do it but it works.
I have been struggling with creating a system to allow articles to be retrieved and opened in a modal pop-up window. It has been implemented successfully using the Bootstrap modal, but due to some new requirements I need to convert to using the Angular UI Modal.
I think the issue is stemming from my handling of URL changes by Angular's $location.search(), but I can't pinpoint it.
Since adding the $uibModal.open() call, this infinite digest loop occurs whenever I click on an article, this launching the openModal function in my controller.
I will include my controller code and the error message I receive below. The two points of entry to the controller are near the bottom at the $rootScope.$on and $scope.$watch calls. They allow the modal to respond to changes in the URL.
The end goal is the ability to open an Angular UI modal when the URL changes, so that I can remove the URL params when the modal is dismissed.
Thanks for any help!
My controller:
(function () {
'use strict';
//Create the LinkModalController and bind it to the core app. This makes it always available.
angular
.module('app.core')
.controller('LinkModalController', LinkModalController);
LinkModalController.$inject = ['$location', '$q', '$rootScope', '$scope', '$uibModal', 'contentpartservice', 'logger'];
/* #ngInject */
function LinkModalController($location, $q, $rootScope, $scope, $uibModal, contentpartservice, logger) {
var vm = this;
/*--------------Variable Definitions--------------*/
vm.modalData = {};
vm.isModalLoading = true;
vm.selectedTab;
vm.urlHistory;
/*--------------Function Definitions--------------*/
vm.selectTab = selectTab;
vm.openModal = openModal;
/*Activate Controller*/
activate();
/*--------------Functions--------------*/
/*Announcement clicks are handled separately because the announcement data contains the full article*/
function handleAnnouncementClick(data) {
vm.modalData = data;
$("#announcementModal").modal();
return;
}
/*Set the active tab for the open modal*/
function selectTab(tab) {
vm.selectedTab = tab;
return;
}
/*Clicking an article of any content type should be funneled through this function. Eventually to be merged with handleSearchResultClick*/
function handleContentTypeClick(data) {
setUrl(data.id, data.contentType.value);
return;
}
function handleUrlParamsModalLaunch(data) {
console.log('launching modal');
/*Ensure modal is not displaying any data*/
vm.modalData = {};
vm.selectedTab = null;
/*Show modal loading screen*/
vm.isModalLoading = true;
var modalInstance = $uibModal.open({
templateUrl: 'app/modals/contentTypeModalTemplate.html',
controller: 'LinkModalController as vm',
});
/*Call the content service to return the clicked content article*/
contentpartservice.getContentItem(data.id, data.type).then(function (contentItem) {
if (contentItem) {
vm.isModalLoading = false;
vm.modalData = contentItem;
return;
} else {
closeModal("#contentPartModal").then(function () {
vm.isModalLoading = false;
logger.error('An error occurred while fetching content');
});
return;
}
}, function (error) {
closeModal("#contentPartModal").then(function () {
vm.isModalLoading = false;
logger.error('An error occurred while fetching content');
});
return;
});
}
/*Close a modal and return a promise object - This allows other code to be executed only after the modal closes*/
function closeModal(modalId) {
$(modalId).modal('hide');
var defer = $q.defer();
defer.resolve();
return defer.promise;
}
//Function to append information to the URL required to retrieve the displayed article
function setUrl(contentId, contentType) {
var urlParams = $location.search();
if (urlParams.q) {
$location.search({ q: urlParams.q, type: contentType, id: contentId });
} else {
$location.search({ type: contentType, id: contentId });
}
console.log($location.search());
return;
}
/*Route link click calls to handle different data structures*/
function openModal(data, context) {
switch (context) {
case 'urlParams':
handleUrlParamsModalLaunch(data);
break;
case 'announcement':
handleAnnouncementClick(data);
break;
case 'contentType':
handleContentTypeClick(data);
break;
default:
logger.error('An error occurred while fetching content');
}
return;
}
/*--------------Listeners--------------*/
/*Catch links click events broadcast from the $rootScope (shell.controller.js)*/
$rootScope.$on('openModal', function (event, data, context) {
vm.openModal(data, context);
return;
});
/*--------------Activate Controller--------------*/
function activate() {
/*Create a watcher to detect changes to the URL*/
$scope.$watch(function () { return $location.search() }, function () {
alert('url changed');
/*Wait for modals to render*/
var urlParams = $location.search();
if (urlParams.type && urlParams.id) {
vm.openModal(urlParams, 'urlParams');
}
/*Handle the inital page load. (Must wait until content is loaded to open modal). This code only runs once.*/
$rootScope.$on('$includeContentLoaded', function () {
alert('url changed first laod');
/*Wait for modals to render*/
var urlParams = $location.search();
if (urlParams.type && urlParams.id) {
vm.openModal(urlParams, 'urlParams');
}
});
}, true);
}
}
})();
The error message that was logged is a massive block of text, so I've pasted it into a Google Doc: https://docs.google.com/document/d/1esqZSMK4_Tiqckm-IjObqTvMGre2Ls-DWrIycvW5CKY/edit?usp=sharing
don't know if you have tried $locationProvider.html5Mode(true); in your app's config module. If you use jquery model to open the popup, it might have conflict between angular and jquery because jquery also watches the change on the url. I used to have similar issue like this.
Requirement : To Show and Hide a div.
HTML
<div ng-show="IsSuccess">
My Div Content
</div>
HTML after page load
<div class="ng-hide" ng-show="false">
HTML after updated from controller (http post call)
<div class="ng-hide" ng-show="true">
ng-show is true but still class contains ng-hide
How to resolve this issue ?
For reference, below is my controller
myController.controller('AuthenticationController',
function AuthenticationController($scope, $location, authDataService, loginDuration) {
$scope.Login = {};
$scope.IsSuccess= false;
$scope.login = function () {
authDataService.authenticateUser($scope.Login, $scope).then(
function (status) {
if (status === 200) {
if ($scope.message == 'Login failed') {
$scope.IsSuccess= true;
}
else {
$scope.IsSuccess= false;
}
}
},
function (data) {
$scope.ErrorMessage = data.Message;
}
);
}
});
Because authDataService.authenticateUser is returning a promise that looks like it's outside of the angular context, angular doesn't know when the scope changes. In that situation, you need to add $scope.$apply()
if ($scope.message == 'Login failed') {
scope.IsSuccess= true;
}
else {
$scope.IsSuccess= false;
}
$scope.$apply();
** Edit: Extended Explanation **
Because you asked for more details about this, I'll try to explain a little further.
$scope.$apply() needs to be called when outside of the angular context. Here's what I mean by outside of the angular context:
$scope.login = function() {
// inside angular context
console.log('a');
setTimeout(function() {
// outside angular context
console.log('b');
$scope.hello = 'b';
// $scope.$apply() needs to be called
$scope.$apply();
}, 1000);
// inside angular context
console.log('c');
$scope.hello = 'c';
};
In this example, here's the output to the log:
a
c
// $scope.$apply() is assumed at this point
b
Angular knows it needs to adjust its bindings after the last line of $scope.login() is processed, and so $scope.$apply() is assumed then, but Angular doesn't know if you have any other callback functions that might be called later on through another context, another context being setTimeout or jQuery's $.ajax or $.Deferred, etc. If that different context modifies the $scope, then you need to call $scope.$apply() to manually update the Angular bindings.
If I am understanding your question correctly, I would change your HTML to show
<div ng-hide="IsSuccess">
My Div Content
</div>
and then in your angular file
$scope.login = function () {
if(<!-- logic to hide or show-->){
$scope.IsSuccess = false;
}else{
$scope.IsSuccess = true;
}
}
Hopefully this helps.
HTML code:
<button class="show-more-btn ng-hide" ng-show="hasMoreItemsToShow()" ng-click="showMoreItems()"">Show More</button>
Javascript code:
setTimeout(function(){ $('.show-more-btn').removeClass('ng-hide');
}, 3000);
I am (sometimes) getting a weird $apply already in progress error when opening a confirm dialog box in the following and innocent looking situation :
var mod = angular.module('app', []);
mod.controller('ctrl', function($scope, $interval, $http) {
$scope.started = false;
$scope.counter = 0;
// some business function that is called repeatedly
// (here: a simple counter increase)
$interval(function() {
$scope.counter++;
}, 1000);
// this function starts some service on the backend
$scope.start = function() {
if(confirm('Are you sure ?')) {
return $http.post('start.do').then(function (res) {
$scope.started = true;
return res.data;
});
};
};
// this function stops some service on the backend
$scope.stop = function() {
if(confirm('Are you sure ?')) {
return $http.post('stop.do').then(function (res) {
$scope.started = false;
return res.data;
});
};
};
});
// mock of the $http to cope with snipset sandbox (irrelevant, please ignore)
mod.factory('$http', function ($q) {
return {
post: function() {
return $q.when({data:null});
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app">
<div ng-controller="ctrl">
<button ng-disabled="started" ng-click="start()">Start</button>
<button ng-disabled="!started" ng-click="stop()">Stop</button>
<br/><br/>seconds elapsed : {{counter}}
</div>
</div>
The error message is :
$rootScope:inprog] $apply already in progress http://errors.angularjs.org/1.2.23/$rootScope/inprog?p0=%24apply
And the callstack is :
minErr/<#angular.js:78:12
beginPhase#angular.js:12981:1
$RootScopeProvider/this.$get</Scope.prototype.$apply#angular.js:12770:11
tick#angular.js:9040:25
$scope.start#controller.js:153:8
Parser.prototype.functionCall/<#angular.js:10836:15
ngEventHandler/</<#angular.js:19094:17
$RootScopeProvider/this.$get</Scope.prototype.$eval#angular.js:12673:16
$RootScopeProvider/this.$get</Scope.prototype.$apply#angular.js:12771:18
ngEventHandler/<#angular.js:19093:15
jQuery.event.dispatch#lib/jquery/jquery-1.11.2.js:4664:15
jQuery.event.add/elemData.handle#lib/jquery/jquery-1.11.2.js:4333:6
To reproduce :
use Firefox (I could not reproduce it with Chrome or IE)
open the javascript console
click alternatively the start and stop buttons (and confirm the dialogs)
try multiple times (10-20x), it does not occur easily
The problem goes away if I remove the confirm dialog box.
I have read AngularJS documentation about this error (as well as other stackoverflow questions), but I do not see how this situation applies as I do not call $apply nor do I interact directly with the DOM.
After some analysis, it seems to be a surprising interaction between the $interval and modal dialog in Firefox.
What is the problem ?
The callstack shows something strange : an AngularJS function tick is called within the controller's start function. How is that possible ?
Well, it seems that Firefox does not suspend the timeout/interval functions when displaying a modal dialog box : this allows configured timeout and intervals callback to be called on the top of the currently executing javascript code.
In the above situation, the start function is called with an $apply sequence (initiated by AngularJS when the button was clicked) and when the $interval callback is executed on the top the start function, a second $apply sequence is initiated (by AngularJS) => boom, an $apply already in progress error is raised.
A possible solution
Define a new confirm service (adapted from this and that blog posts) :
// This factory defines an asynchronous wrapper to the native confirm() method. It returns a
// promise that will be "resolved" if the user agrees to the confirmation; or
// will be "rejected" if the user cancels the confirmation.
mod.factory("confirm", function ($window, $q, $timeout) {
// Define promise-based confirm() method.
function confirm(message) {
var defer = $q.defer();
$timeout(function () {
if ($window.confirm(message)) {
defer.resolve(true);
}
else {
defer.reject(false);
}
}, 0, false);
return defer.promise;
}
return confirm;
});
... and use it the following way :
// this function starts some service on the backend
$scope.start = function() {
confirm('Are you sure ?').then(function () {
$http.post('start.do').then(function (res) {
$scope.started = true;
});
});
};
// this function stops some service on the backend
$scope.stop = function() {
confirm('Are you sure ?').then(function () {
$http.post('stop.do').then(function (res) {
$scope.started = false;
});
});
};
This solution works because the modal dialog box is opened within an interval's callback execution, and (I believe) interval/timeout execution are serialized by the javascript VM.
I was having the same problem in Firefox. Using window.confirm rather than just confirm fixed it for me.
I have the following functionin my controller
$scope.progress = function () {
var form = $scope.coverDetails;
for (i in form) {
if ($scope.coverDetails[i].hasOwnProperty('$valid') && !$scope.coverDetails[i].$valid) {
$location.hash(i + '-label');
break;
}
};
$scope.submitted = true;
$scope.validateForm();
if ($scope.coverDetails.$valid) {
$location.path('/zones');
}
$anchorScroll();
};
This is kind of working. But not really.
When I click on the submit button (which calls the above function) it successfully updates the hash, however, none of the ng-class or ng-show directives update, until the 2nd press of the button, assuming $location.hash hasn't changed between the first and second.
an example of one of the bits not working is
<label id="reg-label" for="reg" ng-class="{'error': coverDetails.reg.$invalid && submitted}">
Number plate
</label>
in this example, the class 'error' isn't applied, but it will scroll to the label if the field is invalid.
anyone able to help?
So I figured out what was going on,
changing the hash causes a tempalte reload, so one has to prevent that using the following code
angular.module("ScrollToErrorPrevention", []).factory('$preventErrorReload', ['$route',
function ($route) {
return function ($scope) {
var lastRoute = $route.current;
$scope.$on('$locationChangeSuccess', function () {
if (lastRoute.$$route.templateUrl === $route.current.$$route.templateUrl) {
$rootScope.$broadcast('locationChangedWithoutReload', $route.current);
$route.current = lastRoute;
}
});
};
}
])
and including it within the app thus:
angular.module('app', ['ScrollToErrorPrevention'])
and call the factory method from the controller
angular.module('app').controller('controller', ['$scope', $preventErrorReload,
function($scope, $preventErrorReload) {
$preventErrorReload($scope);
...
}
]);
after this, using $location.hash(...) along with $anchorScroll() works fine
The work is not entirely my own, but modified from one or 2 (more?) other sources