I am using AngularJS and I have 2 controllers which are siblings. The first controller gets data from an $http request on click. The second controller has to retrieve this data, but I don't know how this controller will get the data because it has to wait for the function in the first controller before it can get its data.
This is the first controller:
$scope.getMessageData = function(username, full_url, main_item, item_id, sub_id){
$scope.ajax_spinner = true;
$http({
method: "GET",
url: "/getMessageData",
params:{
"adUsername" : username,
"fullUrl" : full_url,
"mainItem" : main_item,
"tagId" : item_id,
"subId" : sub_id
}
}).then(function successCallback(response) {
$scope.adUsername = response.data.adUsername;
$scope.fullUrl = response.data.fullUrl;
$scope.main_item = response.data.main_item;
$scope.message = response.data.docs[0];
$scope.sub_item = response.data.sub_docs[0];
$scope.ajax_spinner = false;
Data.setItem($scope.message)
});
};
The factory I use to share the data with the second controller:
App.factory('Data', function(){
return {
setItem: function(item){
this.item = item;
},
getItem: function(){
return this.item;
}
};
});
The second controller:
App.controller("dataController", function($scope, $http, $sce, Data){
$scope.message = Data.getItem();
console.log($scope.message)
});
Now obviously $scope.message is undefined because the controller already loads on page load, but the getMessageData function is not ready called, so how can I "wait" for the second controller to load before getMessageData is fired?
You could use $watch in your controller to keep up with the changes in your service, e.g.
var unwatch = $scope.$watch(function() {
/* set Data.item value in your service */
return Data.item;
}, function(item) {
$scope.item = item || 'nothing here';
});
$scope.$on('$destroy', unwatch);
Or you could implement simple mechanism to register and unregister callbacks in your service to get notified when ever data is changed, e.g.
Template
<html ng-app="App">
<head>
...
</head>
<body>
<div ng-controller="SomeController">
SomeController
<button ng-click="getData()">Fetch</button>
<div ng-bind="data | json"></div>
</div>
<br>
<div ng-controller="OtherController">
OtherController
<div ng-bind="data | json"></div>
</div>
</body>
</html>
JavaScript
angular.module('App', [])
.controller('SomeController', function($scope, Data) {
$scope.getData = function() {
Data.getData().then(function(data) {
$scope.data = data;
});
};
})
.controller('OtherController', function($scope, Data) {
var callback = function(data) {
$scope.data = data;
};
Data.register(callback);
$scope.$on('$destroy', function() {
Data.unregister(callback);
});
})
.factory('Data', function($http) {
var callbacks = [];
return {
register: function(callback) {
var index = callbacks.indexOf(callback);
if (index === -1) {
callbacks.push(callback);
}
},
unregister: function(callback) {
var index = callbacks.indexOf(callback);
if (index !== -1) {
callbacks.splice(index, 1);
}
},
getData: function() {
/* where data.json is { "message": "My message" } */
return $http.get('data.json').then(function(response) {
var data = response.data;
data.timestamp = (new Date()).getTime();
/* move inside $interval out of this fn maybe... */
angular.forEach(callbacks, function(callback) {
callback(data);
});
return data;
});
}
};
});
Related
I have a resolve object in UI-router working correctly; however my current setup makes two separate API calls to the same source. I don't want to cache the data because it is frequently changing, but I would like both functions to pull from one data api call. Here is my current setup.
Basically one function gets all results and one does some looping through the results to create an array.
resolve: {
recipeResource: 'RecipeResource',
getRecipeData: function($stateParams, recipeResource){
var recipeId = $stateParams.id;
var getData = function(){
var data = recipeResource.get({recipeId: recipeId}).$promise;
return data;
}
return {
recipeId: recipeId,
getData: getData
}
},
recipe: function($stateParams, recipeResource, getRecipeData){
return getRecipeData.getData();
},
subCatArray: function($stateParams, recipeResource, getRecipeData){
var data = getRecipeData.getData().then(function(value){
var ingredients = value.data.ingredients;
var log = [];
angular.forEach(ingredients, function(value, key) {
if(value){
if(value.subCategory){
log.push(value.subCategory);
};
};
}, log);
return $.unique(log.concat(['']));
});
return data;
}
}
You could use nested resolves. First get all entries and then use that resolved data in your subCatArray resolve to do additional filtering.
Then you'll only have one api call.
Please have a look at the demo below or in this jsfiddle.
angular.module('demoApp', ['ui.router'])
.factory('dummyData', function($q, $timeout) {
return {
getAll: function() {
var deferred = $q.defer(),
dummyData = ['test1', 'test2', 'test3', 'test4'];
$timeout(function() {
deferred.resolve(dummyData);
}, 1000);
return deferred.promise
}
}
})
.config(function($urlRouterProvider, $stateProvider) {
$urlRouterProvider.otherwise('/');
$stateProvider
.state('main', {
url: '/',
template: '{{all}} {{filtered}}',
resolve: {
'allEntries': function(dummyData) {
return dummyData.getAll().then(function(data) {
return data;
});
},
'filteredEntries': function(allEntries, $filter) {
return $filter('limitTo')(allEntries, 2);
}
},
controller: function($scope, allEntries, filteredEntries) {
$scope.all = allEntries;
$scope.filtered = filteredEntries;
}
})
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.js"></script>
<div ng-app="demoApp">
<div ui-view=""</div>
</div>
I have a service to share data between controllers and a controller to handle $http request in my application. I use list with ng-repeat to display
data and currently have hardcoded JSON data on service. I plan to modify the hardcoded method to get data from server using $http.post and I got the data but my list is not updated. I have tried adding $timeout but still no luck.
Here is my code:
app.service("Storage", function ($rootScope, $http, $q) {
var predicate = 'name';
var reverse = false;
var content = this;
content.items = [];
content.filteredItems = [];
return {
requestData: function() {
var deferred = $q.defer();
var data = $.param({
json: JSON.stringify({
req_type: 'type_1',
reg_name: 'name_2'
})
});
$http.post("http://yourdomain/json.php", data)
.success(function(data, status) {
deferred.resolve(data);
})
.error(function(data) {
deferred.reject(data);
});
return deferred.promise;
},
setContent : function(data){
content.items = data;
},
getContentItems : function () {
return content.items;
}
}
});
Controller:
app.controller('MainController', function($scope, $window, $filter, $timeout, $http, Storage) {
$scope.predicate = null;
$scope.reverse = null;
this.items = null;
Storage.requestData().then(function(data) {
Storage.setContent(data);
$timeout(function(){
$scope.predicate = Storage.getPredicate();
$scope.reverse = Storage.getReverse();
this.items = Storage.getContentItems();
});
//$scope.$apply();
console.log('After11 : ' + JSON.stringify(Storage.getContentItems()));
});
});
You made mistake at this line:
this.items = Storage.getContentItems();
this !=== controller's this
I always create mv variable at the top of controller and use mv instead of this for avoiding this mistake.
angular
.module('app', [])
.service('Storage', function($http, $q, $timeout) {
var content = this;
var items = [];
this.requestData = function() {
var deferred = $q.defer();
//TODO Some http request
$timeout(function() {
deferred.resolve(['A', 'B', 'C']);
}, 1000);
return deferred.promise;
};
this.setContent = function(data) {
items = data;
};
this.getContentItems = function() {
return items;
};
})
.controller('MainCtrl', function(Storage) {
var mv = this;
mv.items = [];
Storage.requestData().then(function(data) {
Storage.setContent(data);
mv.items = Storage.getContentItems();
});
});
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.min.js"></script>
<meta charset="utf-8">
</head>
<body ng-app="app">
<ul ng-controller="MainCtrl as mv">
<li ng-repeat="item in mv.items" ng-bind="item"></li>
</ul>
</body>
</html>
I have the following
Service:
angular.module('app')
.factory('UserFactory', function ($http) {
function profile () {
return $http.get('/gimme')
.then(function success (response) {
return response;
});
};
var user = {
profile: profile
};
return user;
It is used in a controller as follows:
Controller
angular.module('app')
.controller('HeaderCtrl', function ($scope, UserFactory) {
$scope.awesomeThings = [
'HTML5 Boilerplate',
'AngularJS',
'Karma'
];
$scope.user = UserFactory.profile().then(function (response) {
$scope.user = response.data;
});
$scope.change = function () {
$scope.user.name = 'New Name'
}
}
If I call the change() method in a directive which uses HeaderCtrl, what is the best way to make sure that that change, which temporarily changes the user.name, actually changes it on my server as well? In other words, how would I trigger the put request (I am assuming some function needs to be called on the Factory, but I am not sure the best way to make sure it is called or where to put the function call in the controller).
Thanks
Here's an example extending the code you provided, using free JSONPlaceholder API. I think example itself is enough of an answer?
HTML
<body ng-controller="Ctrl as vm">
<div>data: {{ vm.todo | json }}</div>
<div>response: {{ vm.response | json }} </div>
<hr>
<button type="button" ng-click="vm.change('my new title')">Change title</button>
</body>
JavaScript
app.controller('Ctrl', function(TodoService) {
var vm = this;
var id = 1;
TodoService.getPost(id).then(function(response) { // Get
vm.todo = response.data;
});
vm.change = function(val) {
vm.todo.title = val;
TodoService.putPost(vm.todo).then(function(response) { // Put
vm.response = response.status + ' ' + response.statusText;
}).catch(function(error) {
vm.response = angular.toJson(error);
});
};
});
app.factory('TodoService', function($http) {
var endpoint = 'http://jsonplaceholder.typicode.com/todos/';
var todoService = {};
todoService.getPost = function(id) {
return $http.get(endpoint + id).then(function(response) {
return response;
});
};
todoService.putPost = function(todo) {
return $http.put(endpoint + todo.id, todo).then(function(response) {
return response;
});
};
return todoService;
});
Related plunker here http://plnkr.co/edit/VBvVen
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
I'm storing data and language terms for the website with json, and depending on the language selected i load the adequate file. The problem is when i switch the language, only the adress in the adressbar change without reloading the right json file.
factory.js
app.factory('PostsFactory', function ($http, $q, $timeout) {
var factory = {
posts: false,
find: function (lang) {
var deferred = $q.defer();
if (factory.posts !== false) {
deferred.resolve(factory.posts);
} else {
$http.get(lang+'/json.js').success(function (data, status) {
factory.posts = data;
$timeout(function () {
deferred.resolve(factory.posts);
})
}).error(function (data, status) {
deferred.reject('error')
});
}
return deferred.promise;
},
get: function (id, lang) {
var deferred = $q.defer();
var post = {};
var posts = factory.find(lang).then(function (posts) {
angular.forEach(posts, function (value, key) {
if (value.id == id) {
post = value;
};
});
deferred.resolve(post);
}, function (msg) {
deferred.reject(msg);
})
return deferred.promise;
}
}
return factory;
})
controller
posts.js
app.controller('PostsCtrl', function ($scope, PostsFactory, $rootScope, $routeParams) {
$rootScope.loading = true;
lang = $routeParams.lang;
PostsFactory.find(lang).then(function (posts) {
$rootScope.loading = false;
$scope.posts = posts;
}, function (msg) {
alert(msg);
});
});
post.js
app.controller('PostCtrl', function ($scope, PostsFactory, $routeParams, $rootScope) {
$rootScope.loading = true;
SounanFactory.get($routeParams.id, $routeParams.lang).then(function (post) {
$rootScope.loading = false;
$scope.title = post.title;
$scope.text = post.text;
$scope.image = post.image;
$scope.id = post.id;
}, function (msg) {
alert(msg);
});
});
app.js
var app = angular.module('Posts', ['ngRoute']);
app.config(function ($routeProvider) {
$routeProvider.when('/:lang/', {
templateUrl: 'partials/posts.html', controller: 'PostsCtrl'})
.when('/:lang/post/:id', {
templateUrl: 'partials/post.html', controller: 'PostCtrl'})
.otherwise({
redirectTo: '/en'
});
})
posts.html
<div ng-hide="loading">
<a href="#/en/post/{{post.id}}" ng-repeat="post in posts " class="{{post.class}}">
<div class="title">{{post.title}}</div>
<div class="index">{{post.id}}</div>
</a>
</div>
index.html
<body ng-app="Posts">
<div ng-show="loading">Loading ...</div>
<div class="container" ng-view></div>
<a href="#/fr" >FR</a>
<a href="#/en" >EN</a>
</body>
In your posts.js have you tried adding $scope.$apply() underneath $scope.posts = posts; in posts.js and again under $scope.id = post.id; in post.js?
The problem that you're facing is probably due to Angular's digest cycle and the bizarre way you're trying to pass values around through promises/objects/wizardry.
I mean, it's kind of the least of your worries. I know nothing of the project but there are a few things that could be causing an issue here.
Why do you have a $timeout() without a time?
Why are you setting a lang var and then immediately using it instead of just passing it through?
Why are you assigning a posts member of your factory and then using it immediately instead of just passing it through?
You should be using ng-href for dynamic url's
Let us know how you get on.