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>
Related
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;
});
}
};
});
How do we access the data from the resolve function without relading the controller?
We are currently working on a project which uses angular-ui-router.
We have two seperated views: on the left a list of parent elements, on the right that elements child data.
If selecting a parent on the left, we resolve it's child data to the child-view on the right.
With the goal not to reaload the childs controller (and view), when selecting a different parent element, we set notify:false.
We managed to 're-resolve' the child controllers data while not reloading the controller and view, but the data (scope) won't refresh.
We did a small plunker to demonstrate our problem here
First click on a number to instantiate the controllers childCtrl. Every following click should change the child scopes data - which does not work.
You might notice the alert output already has the refreshed data we want to display.
Based on sielakos answer using an special service i came up with this solution.
First, i need a additional service which keeps a reference of the data from the resovle.
Service
.service('dataLink', function () {
var storage = null;
function setData(data) {
storage = data;
}
function getData() {
return storage;
}
return {
setData: setData,
getData: getData
};
})
Well, i have to use the service in my resolve function like so
Resolve function
resolve: {
detailResolver: function($http, $stateParams, dataLink) {
return $http.get('file' + $stateParams.id + '.json')
.then(function(response) {
alert('response ' + response.data.id);
dataLink.setData(response.data);
return response.data;
});
}
}
Notice the line dataLink.setData(response.data);. It keeps the data from the resolve in the service so I can access it from within the controller.
Controller
I modified the controller a little. I wrapped all the initialisation suff in an function i can execute when the data changes.
The second thing is to watch the return value of the dataLink.getData();
As of https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$watch $scope.$watch provides functionality to watch return values of functions.
Here is some Q&D example:
.controller('childCtrl', function($scope, $log, detailResolver, $interval, dataLink) {
initialise();
/*
* some stuff happens here
*/
$interval(function() {
console.log(detailResolver.id)
}, 1000);
$scope.$watch(dataLink.getData, function(newData) {
detailResolver = newData;
initialise();
});
function initialise() {
$log.info('childCtrl detailResolver.id == ' + detailResolver);
$scope.id = detailResolver;
}
})
The line $scope.$watch(dataLink.getData, function(newData) { ... }); does the trick. Every time the data in the dataLink service changes the callback kicks in and replaces the old data with the new one.
Ive created a plunker so you can give it a try https://plnkr.co/edit/xyZKQgENrwd4uEwS9QIM
You don't have to be afraid of memory leaks using this solution cause angular is removing watchers automatically. See https://stackoverflow.com/a/25114028/6460149 for more information.
Not so pretty, but working solution would be to use events. Well, maybe it is not that bad, at least it is not complicated.
https://plnkr.co/edit/SNRFhaudhsWLKUNMFos6?p=preview
angular.module('app',[
'ui.router'
])
.config(function($stateProvider) {
$stateProvider.state('parent', {
views:{
'parent':{
controller: 'parentCtrl',
template: '<div id="parent">'+
'<button ng-click="go(1)">1</button><br>'+
'<button ng-click="go(2)">2</button><br>'+
'<button ng-click="go(3)">3</button><br>'+
'</div>'
},
},
url: ''
});
$stateProvider.state('parent.child', {
views:{
'child#':{
controller: 'childCtrl',
template:'<b>{{ id }}</b>'
}
},
url: '/:id/child',
resolve: {
detailResolver: function($http, $stateParams, $rootScope) {
return $http.get('file'+$stateParams.id+'.json')
.then(function(response) {
alert('response ' + response.data.id);
$rootScope.$broadcast('newData', response.data);
return response.data;
});
}
}
});
})
.controller('parentCtrl', function ($log, $scope, $state) {
$log.info('parentCtrl');
var notify = true;
$scope.go = function (id) {
$state.go('parent.child', {id: id}, {notify:notify});
notify = false;
};
})
.controller('childCtrl', function ($scope, $log, detailResolver, $interval) {
/*
* some stuff happens here
*/
$log.info('childCtrl detailResolver.id == ' + detailResolver);
$scope.$on('newData', function (event, detailResolver) {
$scope.id = detailResolver;
});
$scope.id = detailResolver;
$interval(function(){
console.log(detailResolver.id)
},1000)
})
;
EDIT:
A little bit more complicated solution, that requires changing promise creator function into observables, but works:
https://plnkr.co/edit/1j1BCGvUXjtv3WhYN84T?p=preview
angular.module('app', [
'ui.router'
])
.config(function($stateProvider) {
$stateProvider.state('parent', {
views: {
'parent': {
controller: 'parentCtrl',
template: '<div id="parent">' +
'<button ng-click="go(1)">1</button><br>' +
'<button ng-click="go(2)">2</button><br>' +
'<button ng-click="go(3)">3</button><br>' +
'</div>'
},
},
url: ''
});
$stateProvider.state('parent.child', {
views: {
'child#': {
controller: 'childCtrl',
template: '<b>{{ id }}</b>'
}
},
url: '/:id/child',
resolve: {
detailResolver: turnToObservable(['$http', '$stateParams', function($http, $stateParams) { //Have to be decorated either be this or $inject
return $http.get('file' + $stateParams.id + '.json')
.then(function(response) {
alert('response ' + response.data.id);
return response.data;
});
}])
}
});
})
.controller('parentCtrl', function($log, $scope, $state) {
$log.info('parentCtrl');
var notify = true;
$scope.go = function(id) {
$state.go('parent.child', {id: id}, {notify: notify});
notify = false;
};
})
.controller('childCtrl', function($scope, $log, detailResolver, $interval) {
/*
* some stuff happens here
*/
$log.info('childCtrl detailResolver.id == ' + detailResolver);
detailResolver.addListener(function (id) {
$scope.id = id;
});
});
function turnToObservable(promiseMaker) {
var promiseFn = extractPromiseFn(promiseMaker);
var listeners = [];
function addListener(listener) {
listeners.push(listener);
return function() {
listeners = listeners.filter(function(other) {
other !== listener;
});
}
}
function fireListeners(result) {
listeners.forEach(function(listener) {
listener(result);
});
}
function createObservable() {
promiseFn.apply(null, arguments).then(fireListeners);
return {
addListener: addListener
};
}
createObservable.$inject = promiseFn.$inject;
return createObservable;
}
function extractPromiseFn(promiseMaker) {
if (angular.isFunction(promiseMaker)) {
return promiseMaker;
}
if (angular.isArray(promiseMaker)) {
var promiseFn = promiseMaker[promiseMaker.length - 1];
promiseFn.$inject = promiseMaker.slice(0, promiseMaker.length - 1);
return promiseFn;
}
}
1) For current task ng-view is not needed (IMHO). If you need two different scopes then redesign ng-views to become directives with their own controllers. This will prevent angular to reload them
2) if you need to share data between scopes then service could be used to store data (see helperService in the following code)
3) if we talk about current code simplification then it could be done so: use service from 2) and just use one controller:
(function() {
angular.module('app',[
'ui.router'
]);
})();
(function() {
angular
.module('app')
.service('helperService', helperService);
helperService.$inject = ['$http', '$log'];
function helperService($http, $log) {
var vm = this;
$log.info('helperService');
vm.data = {
id: 0
};
vm.id = 0;
vm.loadData = loadData;
function loadData(id) {
vm.id = id;
$http
.get('file'+id+'.json')
.then(function(response) {
alert('response ' + response.data.id);
vm.data = response.data;
});
}
}
})();
(function() {
angular
.module('app')
.controller('AppController', ParentController);
ParentController.$inject = ['helperService', '$log'];
function ParentController(helperService, $log) {
var vm = this;
$log.info('AppController');
vm.helper = helperService;
}
})();
4) interval, watch, broadcast, etc are not needed as well
Full code is here: plunker
P.S. don't forget about angularjs-best-practices/style-guide
How would i change the following code form $http.get to a $resource
//The created resource (not using it for now)
hq.factory('LogsOfUser', function ($resource) {
return $resource('/HQ/Graph/GetLoggedinTimes?userName=:userName', {
userName: '#userName'
})
});
//The Controller
var ModalViewLogActionsCtrl = function ($scope, $http, $log, LogsOfUser, $modal) {
$scope.openLogs = function (userName) {
$http.get("/HQ/Graph/GetLoggedinTimes?userName=" + userName).success(function (data) {
var modalInstance = $modal.open({
templateUrl: 'LogView.html',
controller: 'ModalLogViewInstance',
resolve: {
items: function () {
//$scope.items = data;
$log.log(data);
$scope.items = data;
return $scope.items; //return data;
},
userName: function () {
return userName;
}
}
});
}).error(function () {
alert("eror :(");
});;
};
};
You've already done most of the work. All you need now is to call the service inside the controller :
LogsOfUser.query({
userName: userName
}, function success(data) {
//your code
}, function err() {
alert("Error")
});
Use query to get an array of data, and get to get a single document.
Here is a example how to call a resource from a controller:
app.controller('MainCtrl', function($scope, $resource) {
var userName = 'Bob';
var LoggedinTimes = $resource('/HQ/Graph/GetLoggedinTimes');
var data = LoggedinTimes.get({userName : userName}, function () {
console.log(data);
});
});
First, you would want to move data-related logic behind a Service, so your controller doesn't know about server-specifics. More importantly, your Service becomes reusable as all services in AngularJS are global singletons. your controller stays small, as it should be.
Next, your controller would call getLoggedIntimes() and work with the outcome as if the data is there. The result of a $resource.get() or similar functions return an empty object or array which fills itself when the REST call returns with data.
In your service you would do the actual $resource.get().
something along the lines of the following pseudo code:
//The Controller
var ModalViewLogActionsCtrl = function ($scope, MyService, $log, LogsOfUser, $modal) {
$scope.openLogs = function (userName) {
var items = MyService.getLoggedInTimes(userName);
var modalInstance = $modal.open({
templateUrl: 'LogView.html',
controller: 'ModalLogViewInstance',
resolve: {
items: function () {
$scope.items = items;
return $scope.items;
},
userName: function () {
return userName;
}
}
});
};
};
app.service('MyService', function ($resource) {
var loggedInResource = $resource('/HQ/Graph/GetLoggedinTimes/:userName');
return {
getLoggedInTimes: functio(username) {
return loggedInResource.get({
username: username
});
}
};
});
just starting out really with Angular and need some advice regarding preventing repeated ajax requests for the same data when re-using a controller with multiple view.
So I have say 6 views all referencing the same controller but different views
app.js
(function() {
var app = angular.module('myApp', ['ngRoute','ui.unique']);
app.config(function ($routeProvider) {
// Routes
$routeProvider
.when('/',
{
controller: 'SamplesController',
templateUrl: 'app/views/home.html'
})
.when('/view2/',
{
controller: 'SamplesController',
templateUrl: 'app/views/main.html'
})
.when('/view3/:rangeName',
{
controller: 'SamplesController',
templateUrl: 'app/views/samples.html'
})
.when('/view4/:rangeName',
{
controller: 'SamplesController',
templateUrl: 'app/views/samples.html'
})
.when('/view5/',
{
controller: 'SamplesController',
templateUrl: 'app/views/basket.html'
})
.when('/view6/',
{
controller: 'SamplesController',
templateUrl: 'app/views/lightbox.html'
})
.otherwise({ redirectTo: '/' });
});
}());
samplesController.js
(function() {
var SamplesController = function ($scope, SamplesFactory, appSettings, $routeParams) {
function init() {
// back function
$scope.$back = function() {
window.history.back();
};
// app settings
$scope.settings = appSettings;
// samples list
SamplesFactory.getSamples()
.success(function(data){
var returnSamples = [];
for (var i=0,len=data.length;i<len;i++) {
if (data[i].range === $routeParams.rangeName) {
returnSamples.push(data[i]);
}
}
$scope.samples = returnSamples;
})
.error(function(data, status, headers, config){
// return empty object
return {};
});
// variables for both ranges
$scope.rangeName = $routeParams.rangeName;
// click to change type
$scope.populate = function(type) {
$scope.attributeValue = type;
};
};
init();
};
SamplesController.$inject = ['$scope','SamplesFactory', 'appSettings', '$routeParams'];
angular.module('myApp').controller('SamplesController', SamplesController);
}());
samplesFactory.js
(function () {
var SamplesFactory = function ($http) {
var factory = {};
factory.getSamples = function() {
return $http.jsonp('http://www.website.com/app/index.php?callback=JSON_CALLBACK');
};
return factory;
};
SamplesFactory.$inject = ['$http'];
angular.module('myApp').factory('SamplesFactory', SamplesFactory);
}());
So with this - every time a new view is loaded the ajax request is made again - how would I re-purpose to have only a single request happen?
As always thanks in advance
Carl
UPDATE: Answer marked below but I also had success by changing the "cache" config item/property (whatever its called) to true in the jsonp request
return $http.jsonp('http://www.website.com/app/index.php?callback=JSON_CALLBACK',{cache: true});
You could change your factory in this way:
(function () {
var SamplesFactory = function ($http) {
var factory = {},
samples = $http.jsonp('http://www.website.com/app/index.php?callback=JSON_CALLBACK');
factory.getSamples = function() {
return samples;
};
return factory;
};
SamplesFactory.$inject = ['$http'];
angular.module('myApp').factory('SamplesFactory', SamplesFactory);
}());
Now getSamples() returns a promise that you should manage in your controllers.
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.