DOM manipulation within an Angular controller seems to be wrong. But this is not coming without a few headaches :)
I have a button, and on ng-click, it will perform an asynchronous request in the background. During the time of that asynchronous request I would like all the buttons (and maybe a few more elements on the page) to be disabled and the clicked button to have a loading icons playing.
What is the cleanest way of doing this?
I usually do this with a variable on the $scope called loading. Whenever an asynch operation is happening, just set it to true. Then anything that's need to be disabled or otherwise affected can base it's state off of that.
Here's a dummy control:
function TestCtrl($scope, $http) {
$scope.loading = false;
$scope.doASynch = function () {
$scope.loading = true;
$http.get("/url").success(function () {
$scope.loading = false;
});
}
}
And here's a sample template.
<div ng-controller="TestCtrl">
<a class="button" ng-disabled="loading" ng-click="doASynch()">
<span ng-hide="loading">Click me!</span>
<span ng-show="loading">Loading....</span>
</a>
</div>
Here is exactly what you are looking for
Loading animations with Asynchronous HTTP Requests in Angular JS
var app = angular.module('myapp', ["ui.utils", "ui.router"]);
app.factory('iTunesData', function($http) {
return {
doSearch: function(sQuery) {
//return the promise directly.
return $http.jsonp('http://itunes.apple.com/search', {
params: {
"callback": "JSON_CALLBACK",
"term": sQuery
}
});
}
}
});
app.controller('iTunesSearch', function($scope, $location, $routeParams, iTunesData) {
$scope.search = function() {
iTunesData2.doSearch($scope.searchTerm)
.then(function(result) {
$scope.data = result.data;
$location.path($scope.searchTerm);
});
}
$scope.searchTerm = $location.$$path.split("/")[1];
if($scope.searchTerm!="") {
$scope.search();
}
});
Related
I have a login function which fetches user data from my back end and store it in device's local storage.
on my profile view, I retrieve the user data from the local storage and parse it as a json, it successfully fetches the data on first login attempt, but when i log in to another account the view is not updated even though the values of user data from the local storage changes.
below is my code for the login and profile controller
var app = angular.module('starter.controllers', []);
app.controller('LoginCtrl', function(apiURL, $scope, $state, $http, $ionicLoading,$ionicPopup) {
var userData =localStorage.getItem('user');
if (userData != null || userData != undefined) {
$state.go('app.browse');
}
$scope.LogIn = function() {
var url = apiURL+'/login.php';
var credentials = {
username: document.getElementById('username').value,
pword: document.getElementById('pword').value
};
$http.post(url,credentials).then(function(result){
localStorage.setItem('user',JSON.stringify(result.data));
$state.go('app.browse',{},{reload: true});
}).catch(function(error){
//error
})
};
});
app.controller('ProfileCtrl', function(webURL, apiURL, $scope, $stateParams, $state) {
$scope.data = JSON.parse(localStorage.getItem('user'));
$scope.goTo = function(id){
$state.go('app.pet',{petId:id},{reload: true});
}
$scope.doRefresh = function() {
setTimeout( function() {
$scope.data = JSON.parse(localStorage.getItem('user'));
}, 1000);
$scope.$broadcast('scroll.refreshComplete');
};
})
and this is my view
<div class="list">
<a class="item item-thumbnail-left item-icon-right">
<img ng-src="{{url}}/{{data.userData.avatar}}" id="myImage" >
<h2 class="cli_name">{{data.userData.cli_name}}</h2>
<p>{{data.userData.cli_address}}<br>
{{data.userData.cli_cont}}<br>
{{data.userData.cli_email}}</p>
<button class="button button-icon icon ion-image" ng-click="loadImage()"></button>
</a>
</div>
Use the $timeout service:
$scope.doRefresh = function() {
̶s̶e̶t̶T̶i̶m̶e̶o̶u̶t̶(̶ ̶f̶u̶n̶c̶t̶i̶o̶n̶(̶)̶ ̶{̶
$timeout( function() {
$scope.data = JSON.parse(localStorage.getItem('user'));
}, 1000);
AngularJS modifies the normal JavaScript flow by providing its own event processing loop. This splits the JavaScript into classical and AngularJS execution context. Only operations which are applied in the AngularJS execution context will benefit from AngularJS data-binding, exception handling, property watching, etc.
The $timeout service is AngularJS's wrapper for window.setTimeout.
Or use $applyAsync
$scope.doRefresh = function() {
$applyAsync(() => {
$scope.data = JSON.parse(localStorage.getItem('user'));
};
i've seen many posts about this subject, but not specificly about this question.
I'm wondering if there could be a generic directive/controller in AngularJs to disable a button (that calls an ajax request) and re-enable it after the request ends.
I've used the word "generic" because i've seen some solutions using a callback after a specific ajax request, but it's not what i need.
I need that:
when clicking on a button, and the button calls an ajax request, it becomes disabled until the request ends.
Thank you for your help
Here is a possibility.
You can think of http service calls just as a promise. Once a http service call is fulfilled then it will call the next promise in the chain, if there is one.
You can receive a funcion in a directive, then do a wrapping call to it and chain another function to it. So this way you know when the promise is being executed and when it's fulfilled.
You need to be sure to retun the promise in the controller. Then pass that funcion to the directive.
Check the following example
https://codepen.io/bbologna/pen/zdpxoB
JS
var app = angular.module('testApp', []);
app.factory('fakeHttpService', ['$timeout', function($timeout){
var doSomething = function(value) {
return new Promise(function(resolve, reject) {
$timeout(() => { resolve(value + 1); $timeout() }, 1000);
})
}
return { doSomething: doSomething }
}])
app.controller('sampleController', ['$scope', 'fakeHttpService',
function($scope, fakeHttpService) {
$scope.doSomething = function(value){
return fakeHttpService.doSomething(value);
}
}
])
app.directive('buttonAsync', function(){
return {
restrict: 'E',
template: `<button ng-click="excecute()" ng-disabled="disabled">DO</button>`,
scope: {
on : '&'
},
controller : ['$scope', function($scope) {
$scope.disabled = false;
$scope.excecute = function() {
$scope.disabled = true;
$scope.on()
.then(function() {
$scope.disabled = false;
})
}
}]
}
})
Html
<div ng-app="testApp" ng-controller="sampleController">
<button-async on="doSomething(3)"></button-async>
</div>
I have a MainController aliased vm, that has a property vm.data that is loaded from some DataService.
The View of the Controller has a custom component rs-dropdown, that is supposed to load dropdown values once the vm.data is loaded. vm.data is a property of Main Controller.
Is there any way how the component itself can watch for changes in parent MainController and, once the data is loaded, perform its load action?
Thanks
UPDATE: Thanks Clavies, adding ng-if="condition" to the component actually makes this component to be created and initialized once the condition is true. Looks like angular constantly monitors ng-if and turns the component ON when the condition is met.
Here I added ng-if="vm.woReAssign" attribute to my component, and now the component with be initialized and created only when this condition is true.
<rs-dropdown ng-if="vm.woReAssign"
rs-server-controller="WorkOrder"
rs-server-action="GetServiceProviderDropDownList"
rs-filter="{woNum:'{{vm.woReAssign.WorkOrderNumber}}'}"
rs-class="form-control">
</rs-dropdown>
P.S. Even though the condition becomes true via async calls with possible delay, it seems that Angular checks this condition all the time, and then revives the component.
I tried to create the scenario you mentioned. You can listen the controller update via lifecycle lifecycle hook $onchange in component desing.
See the example where dropdown component gets filled when data is populated into parent controller on button click.Hope this helps!
var app = angular.module("myApp", []);
app.controller("mainController", function(bookService, $scope) {
var vm = this;
vm.showDropdown = false;
vm.loadBooks = function() {
bookService.getAllBooks().then(function(books) {
vm.books = books;
vm.showDropdown = true;
}, function(err) {
vm.showDropdown = false;
//error
});
}
});
function dropdownControllerFn(bookService) {
var model = this;
model.$onInit = function() {
//do intialization
}
model.$onChanges = function(changes) {
model.books = changes;
};
}
app.component("rsDropdown", {
templateUrl: 'dropdownComponent.html',
bindings: {
books: '<'
},
controllerAs: 'model',
controller: dropdownControllerFn
});
app.factory('bookService', ['$http', '$timeout', '$q', function($http, $timeout, $q) {
var urlBase = 'favouriteBooks.json';
var service = {};
service.getAllBooks = function() {
return $http.get(urlBase).then(function(response) {
return response.data;
});
};
return service;
}]);;
}]);
dropdownComponent.html
<div>
<select ng-options="s.bookName for s in model.books" ng-model="selitem">
<option value="">--Select--</option>
</select>
</div>
index.html
<body data-ng-app="myApp" data-ng-controller="mainController as vm">
<rs-dropdown books=vm.books ng-if="vm.showDropdown"></rs-dropdown>
<button ng-click="vm.loadBooks()">Get Books</button>
</body>
I need in my app to auto refresh when the back-end changes. I added a button to reload the GET to my back-end but I don't wish do that. This is my code
<body data-ng-app="myPr">
<div ng-controller="TodosController">
<div ng-repeat="todo in todos">
<p>{{todo.title}} ...... {{todo.is_completed}}</p>
</div>
<button ng-click="reload()">Reload</button>
</div>
</body>
my app.js
var myPr = angular.module('myPr',[]);
myPr.controller("TodosController", function ($scope,$http){
$scope.reload = function () {
$http.get('http://localhost:3000/api/todos').
success(function (data) {
$scope.todos = data.todos;
});
};
$scope.reload();
});
Thanks
You could just reload your data at regular intervals. Otherwise you'd need to setup something like socket.io or Pusher and push notifications to the browser when the server updates.
var myPr = angular.module('myPr',[]);
myPr.controller("TodosController", function ($scope,$http,$timeout){
$scope.reload = function () {
$http.get('http://localhost:3000/api/todos').
success(function (data) {
$scope.todos = data.todos;
});
$timeout(function(){
$scope.reload();
},30000)
};
$scope.reload();
});
You can use $interval(fuctionToRepeat, intervalInMillisecond) as documented here.
var myPr = angular.module('myPr',[]);
myPr.controller("TodosController", function ($scope,$http){
$scope.reload = function () {
$http.get('http://localhost:3000/api/todos').
success(function (data) {
$scope.todos = data.todos;
});
};
$scope.reload();
$interval($scope.reload, 5000);
});
Note: Intervals created by this service must be explicitly destroyed when you are finished with them. In particular they are not automatically destroyed when a controller's scope or a directive's element are destroyed. You should take this into consideration and make sure to always cancel the interval at the appropriate moment.
I already write a code to display a loader div, when any resources is in pending, no matter it's getting via $http.get or routing \ ng-view.
I wan't only information if i'm going bad...
flowHandler service:
app.service('flowHandler', function(){
var count = 0;
this.init = function() { count++ };
this.end = function() { count-- };
this.take = function() { return count };
});
The MainCTRL append into <body ng-controller="MainCTRL">
app.controller("MainCTRL", function($scope, flowHandler){
var _this = this;
$scope.pageTitle = "MainCTRL";
$scope.menu = [];
$scope.loader = flowHandler.take();
$scope.$on("$routeChangeStart", function (event, next, current) {
flowHandler.init();
});
$scope.$on("$routeChangeSuccess", function (event, next, current) {
flowHandler.end();
});
updateLoader = function () {
$scope.$apply(function(){
$scope.loader = flowHandler.take();
});
};
setInterval(updateLoader, 100);
});
And some test controller when getting a data via $http.get:
app.controller("BodyCTRL", function($scope, $routeParams, $http, flowHandler){
var _this = this;
$scope.test = "git";
flowHandler.init();
$http.get('api/menu.php').then(function(data) {
flowHandler.end();
$scope.$parent.menu = data.data;
},function(error){flowHandler.end();});
});
now, I already inject flowHandler service to any controller, and init or end a flow.
It's good idea or its so freak bad ?
Any advice ? How you do it ?
You could easily implement something neat using e.g. any of Bootstrap's progressbars.
Let's say all your services returns promises.
// userService ($q)
app.factory('userService', function ($q) {
var user = {};
user.getUser = function () {
return $q.when("meh");
};
return user;
});
// roleService ($resource)
// not really a promise but you can access it using $promise, close-enough :)
app.factory('roleService', function ($resource) {
return $resource('role.json', {}, {
query: { method: 'GET' }
});
});
// ipService ($http)
app.factory('ipService', function ($http) {
return {
get: function () {
return $http.get('http://www.telize.com/jsonip');
}
};
});
Then you could apply $scope variable (let's say "loading") in your controller, that is changed when all your chained promises are resolved.
app.controller('MainCtrl', function ($scope, userService, roleService, ipService) {
_.extend($scope, {
loading: false,
data: { user: null, role: null, ip: null}
});
// Initiliaze scope data
function initialize() {
// signal we are retrieving data
$scope.loading = true;
// get user
userService.getUser().then(function (data) {
$scope.data.user = data;
// then apply role
}).then(roleService.query().$promise.then(function (data) {
$scope.data.role = data.role;
// and get user's ip
}).then(ipService.get).then(function (response) {
$scope.data.ip = response.data.ip;
// signal load complete
}).finally(function () {
$scope.loading = false;
}));
}
initialize();
$scope.refresh = function () {
initialize();
};
});
Then your template could look like.
<body ng-controller="MainCtrl">
<h3>Loading indicator example, using promises</h3>
<div ng-show="loading" class="progress">
<div class="progress-bar progress-bar-striped active" style="width: 100%">
Loading, please wait...
</div>
</div>
<div ng-show="!loading">
<div>User: {{ data.user }}, {{ data.role }}</div>
<div>IP: {{ data.ip }}</div>
<br>
<button class="button" ng-click="refresh();">Refresh</button>
</div>
This gives you two "states", one for loading...
...and other for all-complete.
Of course this is not a "real world example" but maybe something to consider. You could also refactor this "loading bar" into it's own directive, which you could then use easily in templates, e.g.
//Usage: <loading-indicator is-loading="{{ loading }}"></loading-indicator>
/* loading indicator */
app.directive('loadingIndicator', function () {
return {
restrict: 'E',
scope: {
isLoading: '#'
},
link: function (scope) {
scope.$watch('isLoading', function (val) {
scope.isLoading = val;
});
},
template: '<div ng-show="isLoading" class="progress">' +
' <div class="progress-bar progress-bar-striped active" style="width: 100%">' +
' Loading, please wait...' +
' </div>' +
'</div>'
};
});
Related plunker here http://plnkr.co/edit/yMswXU
I suggest you to take a look at $http's pendingRequest propertie
https://docs.angularjs.org/api/ng/service/$http
As the name says, its an array of requests still pending. So you can iterate this array watching for an specific URL and return true if it is still pending.
Then you could have a div showing a loading bar with a ng-show attribute that watches this function
I would also encapsulate this requests in a Factory or Service so my code would look like this:
//Service that handles requests
angular.module('myApp')
.factory('MyService', ['$http', function($http){
var Service = {};
Service.requestingSomeURL = function(){
for (var i = http.pendingRequests.length - 1; i >= 0; i--) {
if($http.pendingRequests[i].url === ('/someURL')) return true;
}
return false;
}
return Service;
}]);
//Controller
angular.module('myApp')
.controller('MainCtrl', ['$scope', 'MyService', function($scope, MyService){
$scope.pendingRequests = function(){
return MyService.requestingSomeURL();
}
}]);
And the HTML would be like
<div ng-show="pendingRequests()">
<div ng-include="'views/includes/loading.html'"></div>
</div>
I'd check out this project:
http://chieffancypants.github.io/angular-loading-bar/
It auto injects itself to watch $http calls and will display whenever they are happening. If you don't want to use it, you can at least look at its code to see how it works.
Its very simple and very useful :)
I used a base controller approach and it seems most simple from what i saw so far. Create a base controller:
angular.module('app')
.controller('BaseGenericCtrl', function ($http, $scope) {
$scope.$watch(function () {
return $http.pendingRequests.length;
}, function () {
var requestLength = $http.pendingRequests.length;
if (requestLength > 0)
$scope.loading = true;
else
$scope.loading = false;
});
});
Inject it into a controller
angular.extend(vm, $controller('BaseGenericCtrl', { $scope: $scope }));
I am actually also using error handling and adding authorization header using intercepting $httpProvider similar to this, and in this case you can use loading on rootScope
I used a simpler approach:
var controllers = angular.module('Controllers', []);
controllers.controller('ProjectListCtrl', [ '$scope', 'Project',
function($scope, Project) {
$scope.projects_loading = true;
$scope.projects = Project.query(function() {
$scope.projects_loading = false;
});
}]);
Where Project is a resource:
var Services = angular.module('Services', [ 'ngResource' ]);
Services.factory('Project', [ '$resource', function($resource) {
return $resource('../service/projects/:projectId.json', {}, {
query : {
method : 'GET',
params : {
projectId : '#id'
},
isArray : true
}
});
} ]);
And on the page I just included:
<a ng-show="projects_loading">Loading...</a>
<a ng-show="!projects_loading" ng-repeat="project in projects">
{{project.name}}
</a>
I guess, this way, there is no need to override the $promise of the resource