I want to show a loading animation (ideally that shows % of how much is loaded) whilst content loads from my $http get.
I have made an attempt, but it does not seem to hide the content I am trying to hide.
I set a time length- but I do not want it to show the loading overlay for a set time. I want it to show the loading overlay (possibly until a minimum of 3 images are loaded?) until the element is loaded.
Here is my attempt in a plunker:
http://plnkr.co/edit/7ScnGyy2eAmGwcJ7XZ2Z?p=preview
.factory('cardsApi', ['$http', '$ionicLoading', '$timeout', function ($http, $ionicLoading, $timeout) {
var apiUrl = 'http://mypage.com/1/';
$ionicLoading.show({
duration: 3000,
noBackdrop: true,
template: '<p class="item-icon-left">Loading stuff...<ion-spinner icon="lines"/></p>'
});
var getApiData = function () {
return $http.get(apiUrl).then($ionicLoading.hide, $ionicLoading.hide);
};
return {
getApiData: getApiData,
};
}])
.controller('CardsCtrl', ['$scope', 'TDCardDelegate', 'cardsApi', '$http',
function ($scope, TDCardDelegate, cardsApi, $http) {
$scope.cards = [];
cardsApi.getApiData()
.then(function (result) {
console.log(result.data) //Shows log of API incoming
$scope.cards = result.data;
$scope.product_id = result.data.product_id;
})
.catch(function (err) {
//$log.error(err);
})
Remove the duration line from your $ionicLoading.show declaration.
duration: 3000,
So that it looks like:
$ionicLoading.show({
noBackdrop: true,
template: '<p class="item-icon-left">Loading stuff...<ion-spinner icon="lines"/></p>'
});
And that should work (at least it does in the plunker). The duration property specifies when to close the ionicLoading instance and does not wait for ionicLoading.hide().
You want to wait until the image is actually loaded and rendered, but you are hiding the loading messages as soon as the API call returns. From your code it looks as though the API returns the image URL, not the image data itself?
In which case you could do it using the element.onload(), however the problem with this is that it's no longer a generic API which works for loading anything but I'll let you decide whether that's OK for your use case.
var imagesLoaded = 0;
var loadImage = function(result) {
var image = new Image();
image.onload = function () {
imagesLoaded++;
if (imagesLoaded >= 3)
$ionicLoading.hide();
};
// We still want to hide the loading message if the image fails to load - 404, 401 or network timeout etc
image.onerror = function () {
imagesLoaded++;
if (imagesLoaded >= 3)
$ionicLoading.hide();
};
image.src = result.image_url;
};
// We still want to hide the loading message if the API call fails - 404, 401 or network timeout etc
var handleError = function() {
imagesLoaded++;
if (imagesLoaded >= 3)
$ionicLoading.hide();
};
var getApiData = function () {
return $http.get(apiUrl).then(loadImage, handleError);
};
Related
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.
I've read the docs but it's 3am and I'm at the end of me teather. Here's my controller:
controller('makeDashCtrl', function ($scope, $rootScope, $cookies, $location, $http, $interval) {
var userId = $cookies.get('user_id');
var orgId = $cookies.get('org_id');
$http.get('/api/consolidateProfile/').then(function(data){
console.log(data);
}, function(res){
console.log(res + " - Eh, consolidateProfile probably timed out, no biggie")
})
var testStart = $interval(function(){
$http.get('/api/testProgress/' + userId).then(function(obj){
$scope.message = obj.data.message;
$scope.sub = obj.data.sub;
if(obj.data.nextPhase){
console.log("Should be cancelling...");
nextScrape();
$interval.cancel(testStart); // This one cancels fine
}
}, function(err){
$interval.cancel(testStart);
});
}, 1000);
function nextScrape(){
console.log("In checkScraperadsfsdfads!")
var checkScraper = $interval(function(){
console.log("In checkScraper REAL!")
$http.get('/api/checkScraper/' + userId).then(function(obj){
var msg = JSON.parse(obj.data);
console.log("Got some data!")
console.log(obj);
if(msg.doneFbs){
$scope.fbMsg = "We've finished gathering " + msg.doneFbs + " Facebook accounts";
}
if(msg.doneTwits){
$scope.twitMsg = "We've finished gathering " + msg.doneTwits + " Twitter accounts";
}
$scope.message = msg.message;
$scope.sub = msg.sub;
if(msg.finished){
$interval.cancel(checkScraper); // This does NOT cancel
console.log("Scraping Done!")
$location.path('/dashboard') // This successfully redirects to the next page
}
},function(err){
console.log("There was an error in Checkscraper ")
console.log(err)
$interval.cancel(checkScraper); // This does NOT cancel when there's an error
});
}, 3000)
}
})
See comments in the code above. Perhaps it's an issue within the nextScrape() function, but I cannot get that $interval to cancel. Even when the $location.path() changes to the next page, the interval is still running.
What am I doing wrong here?
You might have multiple ajax requests at a time if your call takes more than a second to get back. Instead of using $interval, why don't you make one call and use $tiemout to schedule a second call if you need to when the first call is done?
function checkProgress() {
$http.get('/api/testProgress/' + userId).then(function(obj){
$scope.message = obj.data.message;
$scope.sub = obj.data.sub;
if(obj.data.nextPhase){
// move on to next phase
nextScrape();
} else {
// not done, schedule another check
$timeout(checkProgress, 1000);
}
}, function(err){
// error, you cancel, but maybe schedule another progress check?
});
}
I suspect the code is making multiple call before receiving response from the Ajax. That means your ajax is taking more than one second to respond. This is happening because you have mentioned very less amount of time to pool the data from server again.
To deactivate the $interval after page redirection you could take a use of $destroy event on scope. Events needs to be clear manually, because they won't get vanished until we detach them. You could stop interval while leaving the page. Destroy the event on scope event
Code
$scope.$on('$destroy', function(){
$interval.cancel(testStart);
})
I am fetching a data with this function in my controller:
var fetchStoreItems = function () {
return $http.get('/enterprises/_store').then(function (response) {
$scope.items = response.data;
}, function (errResponse) {
console.error("Error while fetching data")
})
};
It is working fine and fast enough. But what if there are lots of items to fetch?! I want to put a spinner while data is being fetched.
How can I do this?
In your controller,
var fetchStoreItems = function () {
$scope.loading = true; //define a `$scope` variable; show loading image when ajax starts
return $http.get('/enterprises/_store').then(function (response) {
$scope.items = response.data;
$scope.loading = false; //hide loading image when ajax successfully completes
}, function (errResponse) {
console.error("Error while fetching data");
$scope.loading = false; //hide loading image when ajax error
})
};
<img src="pathToLoadingImage" ng-show="loading" /> // show the loading image according to `$scope.loading`
If you want to use it for some of the rest API calls, kalhano's answer works well.
However if you want spinner for all your angular http calls, you might consider using an interceptor.
Please check this link for that:
Http call docs with interceptors explained
I'm quite new to angular, now I was able to show an alert message when someone requests a new password, from our app:
Usermodel:
.service('userModel', ['$q', '$http', 'authorizedTracker', function($q, $http, authorizedTracker) {
this.passwordreset = function(token) {
var deferred = $q.defer();
$http({
method: 'GET',
url: '/reset/'+token
})
.then(function(response) {
if (!_(response.data).has('user')) {
deferred.reject('unkown user id');
}
model.resetted = true;
}, function(error) {
deferred.reject(error.data.message);
});
authorizedTracker.addPromise( deferred.promise)
return deferred.promise;
};
So if resetted is true, the message will show up, see below:
html:
<!-- Succes-->
<div class="alert alert-success animated fadeInDown" ng-cloak ng-show="userModel.resetted">
<strong><i class="icon-attention"></i>Success!</strong> New password is sent to your e-mail
</div>
But now I want to hide the alert after x amount of seconds, or if the user clicks to another page. How is that possible? Any solution?
you should use the $timeout service (symilar to the window.setTimeout in native javascript).
In your code you should add the following code
...
model.resetted = true;
$timeout(function() {
model.resetted = false;
}, xxx)//replace xx by the amount of milisecond you want
here is an example, hope it will help you
http://plnkr.co/edit/Qt39x5Xo5JhP61QHYLwO?p=preview
To close after X seconds use the $timeout service.
model.resetted = true;
$timeout(function() {
model.resetted = false;
}, X); // X in milliseconds
For angular2+ users:
You should add timeout to the method witch is showing alert:
setTimeout(() => this.hideAlert(), X);
where hideAlert makes a logic needed to hide an alert and X is number of miliseconds
the idea behind this is to start counting X miliseconds until this.hideAlert is called
arrow function is needed to keep original context (be able to use this as we are used to - pointing to parent class for example)
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.