In my AngularJS controller I'm trying to do something relatively simple: I'm trying to populate a <select> element dynamically in the controller. To do so I need to wait for my localized UI text data to be loaded and data from my server to be loaded and this is causing a problem for me.
What my HTML Looks like:
<select
data-ng-model="group"
data-ng-options="options.value as options.label for options in userGroups">
<option>--</option>
</select>
Then my controller is actually implementing a base controller "class" which allows me to share logic between controllers:
// exampleController.js
myModule.controller('exampleController',
['$scope', '$timeout', '$routeParams', '$controller',
function ($scope, $timeout, $routeParams, $controller) {
// Instantiate the base controller class and set it's scope
// to this controller's scope. This is how the base and child
// controllers will share data and communicate.
var base = $controller('baseController', { $scope: $scope });
}]);
And here is a relevant snippet of the baseController:
// baseController.js
$scope.getDataFromUrl = function (url, cache, successFunction) {
$http.get(url, { cache: cache })
.success(function (data) {
if (!handleErrorInData(data))
{
successFunction(data);
}
});
};
$scope.getDataFromUrl(uiTextResourceUrl, true, function (data) {
$scope.uiText = data;
});
So baseController fetches the text resources when it loads and sets it to the scope when it's finished retrieving the data. exampleController on the other hand will fetch other data from the server via the getDataFromUrl() function defined in baseController like so:
$scope.getDataFromUrl(dataUrl, false, function (data) {
// Do stuff with the returned data...
};
My issue is coming from this part of the exampleController code where I populate the data of the <select> element from earlier:
// exampleController.js (Continued...)
$scope.getDataFromUrl(userGroupsUrl, false, function (data) {
initSelectDropdown(data);
});
var initSelectDropdown = function (data) {
var userGroups = [];
// Parse data retrieved from the server and populate the <select> bound data
// array with it
var index;
for (index = 0; index < data.length; ++index)
{
var newGroup = {
value: data[index],
label: data[index]
};
// One of the data entries will have a value of "", this group needs its
// label to be set to the localized string "No Group"
if (newGroup.value === "")
{
newGroup.label = '<' + $scope.uiText['NoGroup.Text'] + '>';
}
userGroups.push(newGroup);
}
// Set local userGroups to scope
$scope.userGroups = userGroups;
};
The problem I'm having is here in the initSelectDropdown() function. I need to have both the data from the server and the uiText resource data from the server, particularly the line newGroup.label = '<' + $scope.uiText['NoGroup.Text'] + '>'; where the data is being transformed in a way that is dependant on localized resources being loaded. I researched the issue and saw that using $q.all() might be a solution but unfortunately in my case there is no way for me to call $q.all() because the two calls to fetch data are being made from different functions in different controllers (data being requested from child controller and resources being requested from base controller).
In the view it's easy to fix this because if I bind an element to $scope.uiText['SomeText.Text'] then it doesn't care if SomeText.Text is undefined at first and when it is eventually populated the UI will automatically pick up on the change.
How can I make this work? Is it possible to achieve something like how binding works in the view?
For sharing code angular provides services/factory, you don't need to use base controller.
Define a factory class and add two methods, one to fetch your server data and other to fetch uiText data. these methods will return promises.
Now in your controller you can use $q.all() passing the two promises that will be resolved when ajax call is complete.
Hope it makes sense ?
Related
I'm looking for some information on the best way to retrieve data from a local JSON file and handle the response. After browsing through Stack Overflow, I have some mixed thoughts as I've seen multiple ways of doing the same thing (although no explanation on why one may or may not be preferred).
Essentially, I have an Angular app that is utilising a factory to retrieve data from a JSON file; I'm then waiting for the response to resolve in my controller before using it in my html file, similar to the below:
Option 1
Factory:
comparison.factory('Info', ['$http', function($http) {
var retrievalFile = 'retrievalFile.json';
return {
retrieveInfo: function() {
return $http.get(retrievalFile);
}
}
}]);
Controller:
comparison.controller('comparisonController', ['$scope', 'Info', function($scope, Info) {
Info.retrieveInfo().then(function(response) {
$scope.info = response.data;
});
}]);
My main point of contention is figuring out when it's best to wait for the response to resolve, or if it even matters. I'm toying with the idea of having the factory return the fulfilled promise, and wait for the controller to retrieve the data also. In my view, it's best to abstract all data retrieval out of the controller and into the factory, but I'm not sure if this extends to waiting for the actual data to be returned within the factory itself. With this in mind, I'm confused about whether to opt for option 1 or option 2 and would really appreciate some feedback from more experienced/qualified developers!
Option 2
Factory:
comparison.factory('Info', ['$http', function($http) {
var retrievalFile = 'retrievalFile.json';
return {
retrieveInfo: function() {
return $http.get(retrievalFile).then(function(response) {
return response.data;
});
}
}
}]);
Controller:
comparison.controller('comparisonController', ['$scope', 'Info', function($scope, Info) {
Info.retrieveInfo().then(function(response) {
$scope.info = response;
});
}]);
Thank you for any input/suggestions in advance!
It depends on what your controller is expecting and how you set up your application. Generally, I always go with the second option. Its because I usually have global error or success handlers in all api requests and I have a shared api service. Something like below.
var app = angular.module('app', []);
app.service('ApiService', ['$http', function($http) {
var get = function(url, params) {
$http.get(url, { params: params })
.then(handleSuccess, handleError);
};
// handle your global errors here
// implementation will vary based upon how you handle error
var handleError = function(response) {
return $q.reject(response);
};
// handle your success here
// you can return response.data or response based upon what you want
var handleSuccess = function(response) {
return response.data;
};
}]);
app.service('InfoService', ['ApiService', function(ApiService) {
var retrieveInfo = function() {
return ApiService.get(retrievalFile);
/**
// or return custom object that your controller is expecting
return ApiService.get.then(function(data) {
return new Person(data);
});
**//
};
// I prefer returning public functions this way
// as I can just scroll down to the bottom of service
// to see all public functions at one place rather than
// to scroll through the large file
return { retrieveInfo: retrieveInfo };
}]);
app.controller('InfoController', ['InfoService', function(InfoService) {
InfoService.retrieveInfo().then(function(info) {
$scope.info = info;
});
}])
Or if you are using router you can resolve the data into the controller. Both ngRouter and uiRouter support resolves:
$stateProvider.state({
name: 'info',
url: '/info',
controller: 'InfoController',
template: 'some template',
resolve: {
// this injects a variable called info in your controller
// with a resolved promise that you return here
info: ['InfoService', function(InfoService) {
return InfoService.retrieveInfo();
}]
}
});
// and your controller will be like
// much cleaner right
app.controller('InfoController', ['info', function(info) {
$scope.info = info;
}]);
It's really just preference. I like to think of it in terms of API. What is the API you want to expose? Do you want your controller to receive the entire response or do you want your controller to just have the data the response wraps? If you're only ever going to use response.data then option 2 works great as you never have to deal with anything but the data you're interested in.
A good example is the app we just wrote where I work. We have two apps: a back-end API and our front-end Angular application. We created an API wrapper service in the front-end application. In the service itself we place a .catch for any of the API endpoints that have documented error codes (we used Swagger to document and define our API). In that .catch we handle those error codes and return a proper error. When our controllers/directives consume the service they get back a much stricter set of data. If an error occurs then the UI is usually safe to just display the error message sent from the wrapper service and won't have to worry about looking at error codes.
Likewise for successful responses we do much of what you're doing in option 2. In many cases we refine the data down to what is minimally useful in the actual app. In this way we keep a lot of the data churning and formatting in the service and the rest of the app has a lot less to do. For instance, if we need to create an object based on that data we'll just do that in return the object to the promise chain so that controllers aren't doing that all over the place.
I would choose option two, as it your options are really mostly the same. But let see when we add a model structure like a Person suppose.
comparison.factory('Info', ['$http', function($http) {
var retrievalFile = 'retrievalFile.json';
return {
retrieveInfo: function() {
return $http.get(retrievalFile).then(function(response) {
//we will return a Person...
var data = response.data;
return new Person(data.name, data.age, data.gender);
});
}
}
}]);
This is really simple, but if you have to map more complex data into object models (you retrieve a list of people with their own items... etc), that's when things get more complicated, you will probably want to add a service to handle the mapping between data and models. Well you have another service DataMapper(example), if you choose your first option you will have to inject DataMapper into your controller and you will have to make your request through your factory, and map the response with the injected service. And then you probably say, Should I have all this code here? ... Well probably no.
That is an hypothetical case, something that count a lot is how you feel structuring your code, won't architecture it in a way you won't understand. And at the end take a look at this: https://en.wikipedia.org/wiki/SOLID_(object-oriented_design) and research more information about this principles but focused to javascript.
Good question. A couple of points:
Controllers should be view centric versus data centric therefore you
want remove data logic from the controller and rather have it focus
on business logic.
Models (M in MVC) are a data representation of your application and
will house the data logic. In Angular case this would be a service
or factory class as you rightfully pointed out. Why is that well for
example:
2.1 AccountsController (might have multiple data models injected)
2.1.1 UserModel
2.1.2 AuthModel
2.1.3 SubscriptionModel
2.1.4 SettingsModel
There are numerous ways to approach the data model approach, but I would say your service class should be the data REST model i.e. getting, storing, caching, validating, etc. I've included a basic example, but suggest you investigate JavaScript OOP as that will help point you in the right direction as to how to build data models, collections, etc.
Below is an example of service class to manage your data.Note I have not tested this code but it should give you a start.
EXAMPLE:
(function () {
'use strict';
ArticleController.$inject = ['$scope', 'Article'];
function ArticleController($scope, Article) {
var vm = this,
getArticles = function () {
return Article.getArticles()
.then(function (result) {
if (result) {
return vm.articles = result;
}
});
};
vm.getArticles = getArticles;
vm.articles = {};
// OR replace vm.articles with $scope if you prefer e.g.
$scope.articles = {};
$scope.userNgClickToInit = function () {
vm.getArticles();
};
// OR an init on document ready
// BUT to honest I would put all init logic in service class so all in calling is init in ctrl and model does the rest
function initArticles() {
vm.getArticles();
// OR chain
vm.getArticles()
.then(getCategories); // doesn't here, just an example
}
initArticles();
}
ArticleModel.$inject = ['$scope', '$http', '$q'];
function ArticleModel($scope, $http, $q) {
var model = this,
URLS = {
FETCH: 'data/articles.json'
},
articles;
function extract(result) {
return result.data;
}
function cacheArticles(result) {
articles = extract(result);
return articles;
}
function findArticle(id) {
return _.find(articles, function (article) {
return article.id === parseInt(id, 10);
})
}
model.getArticles = function () {
return (articles) ? $q.when(articles) : $http.get(URLS.FETCH).then(cacheArticles);
};
model.getArticleById = function (id) {
var deferred = $q.defer();
if (articles) {
deferred.resolve(findArticle(id))
} else {
model.getBookmarks().then(function () {
deferred.resolve(findArticle(id))
})
}
return deferred.promise;
};
model.createArticle = function (article) {
article.id = articles.length;
articles.push(article);
};
model.updateArticle = function (bookmark) {
var index = _.findIndex(articles, function (a) {
return a.id == article.id
});
articles[index] = article;
};
model.deleteArticle = function (article) {
_.remove(articles, function (a) {
return a.id == article.id;
});
};
}
angular.module('app.article.model', [])
.controller('ArticleController', ArticleController)
.service('Article', ArticleModel);
})()
I want to clean up my controller and move all the data fetching logic to the service, so that there wouldn't be the following code in my controller:
myService.fetchData().then(funtion(response) {
self.data = response.data;
});
This is my achievement so far: LINK
Is this a good practice? What problems could rise from this?
Also how can I remove the app.users() call in my template and leave only app.users like a variable?
It is a good practice to move the logic to the service, but you should assign the users to a property (model) instead of relinking to the service from the controller:
var self = this;
this.fetchUsers = function() {
// assign result to 'users' property, which can be used
// in the view
UserService.fetchUsers().then(function(data){
// success
self.users = data;
// or, because you remember the users in your service:
// self.users = UserService.getUsers();
});
}
and your view:
<button ng-click="app.fetchUsers()">Get users</button>
<github-table columns="app.columns" rows="app.users"></github-table>
EDIT
the service:
function fetchUsers () {
// return the $http call (which is a promise) so the
// .then can be used
return $http
.get('https://api.github.com/users')
.success(function (response) {
users = response;
});
}
I think the following code should be in the controller logic because it increases extensibility.
For an example you might call this service method in two different controllers and wants to handle the response differently. And also if you have a function that needs to execute after all the data has been retrieved, then this logic is the best.
myService.fetchData().then(funtion(response) {
self.data = response.data;
});
I'm fairly new to angular but, i'm getting it...
Using some ui-route states, I am able to have urls (views) that progressively build with the user selected data. Its similar to a wizard interface. step 1 => step 2 => step 3 => final result
From start to finish there isn't much of an issue as the data is saved in a service as the user clicks along. The problem comes when the page is refreshed or entered directly into the browser (i.e. someone sent me a link)
/step-1/step-2 should display the same data as if the user selected step 1 and then step 2
In such a case, I need to get all the user selected data up to that point.
Get the data for step 1, when that data is loaded, get the data for step 2, then get the data for step 3, etc. However, I only need to get the data if it has not already been retrieved.
To accomplish such an idea i need the service to return data stored in its data property or if that data is empty go get it, save it to its property and then return it.
any ideas on how to implement this?
Here is some pseudo code that illustrates what I am attempting to do in the service. I am not certain the syntax is 100% correct but it should illustrate the point.
app.factory('dataService', ['$http', '$q', '$filter', 'ngAuthSettings', 'localStorageService', function ($http, $q, $filter, ngAuthSettings, localStorageService) {
var ServiceFactory = {};
var _getData = function () {
var deferred = $q.defer();
_Step1Data= localStorageService.get('Step1Data');
if (_Step1Data) {
$http.get('api/Step1Data').then(function (response) {
_data = response.data.results;
localStorageService.set('Step1Data', _Step1Data);
deferred.resolve(response);
});
}
else {
deferred.resolve(_Step1Data);
}
return deferred.promise;
};
ServiceFactory.getStep1Data = _getStep1Data;
return ServiceFactory;
}]);
Assuming that the process to get step 2, step 3 data is the same as step 1, the call to load the data would be as follows
dataService.getStep1Data ()
.then(function (data) {
vm.Step1Data = data;
dataService.getStep2Data ()
.then(function (data) {
vm.Step2Data = data;
}, function (error) {
//some error stuff
})
}, function (error) {
//some error stuff
});
The reason for the stepped approach is that each step depends on the previous steps selected data to get the current steps data.
It's the resolve property you are looking for. You can use it to resolve the step data before any step controller is instantiated. The resolved step data can then be injected in your controller.
You might combine this with your dataservice, like so:
step1.state({
resolve: {
step1Data: function(dataService) {
return dataService.getStep1Data ()
}
}
....
}
We are creating a Telecom App dashboard. We are trying to fetch the logs using Logstash and Elastic search, and displaying it on UI using ng-Table directive of Angularjs .
We are able to obtain logs but the issue is to send the response between two controllers of different module.
Here is the code,
For retrieving logs from elastic search:
// We define an EsConnector module that depends on the elasticsearch module.
var EsConnector = angular.module('EsConnector', ['elasticsearch']);
// Create the es service from the esFactory
EsConnector.service('es', function (esFactory) {
return esFactory({ host: 'localhost:9200' });
});
// We define an Angular controller that returns the server health
// Inputs: $scope and the 'es' service
EsConnector.controller('ServerHealthController', function($scope, es) {
es.cluster.health(function (err, resp) {
if (err) {
$scope.data = err.message;
} else {
$scope.data = resp;
}
});
});
// We define an Angular controller that returns query results,
// Inputs: $scope and the 'es' service
EsConnector.controller('QueryController', function($scope, es) {
// search for documents
es.search({
index: 'logstash-2014.08.29',
size: 500,
body: {
"query":
{
"match": {
"CallId":-1 }
},
}
}).then(function (response) {
$scope.hits = response.hits.hits;
});
});
We need to pass the data ie hits obtained from QueryController(of EsConnector module) to MainController( of app module)
Here is the app module:-
var app = angular.module('SnapshotApp',['ngTable']);
app.controller('MainController', function($scope, ngTableParams){
$scope.query = {}
$scope.queryBy = '$'
var data = ; \\ we want to populate 'data' with 'hits' of QueryController
$scope.tableParams = new ngTableParams({
page: 1, // show first page
count: 10 // count per page
}, {
total: data.length, // length of data
getData: function($defer, params) {
$defer.resolve(data.slice((params.page() - 1) * params.count(), params.page() * params.count()));
}
});
});
Another approach could be to merge both modules.
Thanks.
You can do this in many ways, but the cleanest (loosely coupled way) would be events.
If the modules are loaded on the same page, it means they are at least the child of $rootScope and you can listen and raise events on $rootScope to communicate these info ($rootScope.$on('myEvent', function (data){}) - $rootScope.$emit('myEvent', {...}).
A less cleaner solution would be to have some sort of data managers (AngularJS services) in which you could store this information and fetch in multiple places. The receiver controller would then $watch the response of this service.getLogs() and on change you would have the latest data (this is a bit HARDCORE in terms of performance, so not really recommended).
EDIT: And in case you need to display a specific block of information when that info arrives, you can just mark your receiver controller data model with ("$scope.myModel.logs") with null and add an ng-if condition on the block that should show these logs with myModel.logs (when the event will be received, the data model will be updated and the block will be displayed)
you can checkout one of my earlier post -
http://stackoverflow.com/questions/25301581/angular-js-newbie-link-in-a-controller-view-that-triggers-another-controller-a/25305951#25305951
I have added a plunk as a demo - http://plnkr.co/edit/1dhdPfmB1WwAkYhsz4Hv
I have a resource factory that builds objects for accessing our API. I use an environment variable to determine the base part of the URL - whether or not to include 'account/id' path segments when the admin user is 'engaging' a client account.
The sessionStorage item that holds the 'engagedAsId' doesn't get read, though for instances created after engaging an account. It requires a full reload of the app to pick up that change. Here is the factory code:
myapp.factory('ResourceSvcFactory',
['$rootScope', '$resource',
function ($rootScope, $resource) {
function ResourceSvcFactory (endpoint) {
// vvv problem is here vvv
var accountId = sessionStorage.getItem('engagedAsId');
var apiPath = (accountId != null)
? '/api/account/' + accountId + endpoint
: '/api' + endpoint;
var Resource = $resource(apiPath+':id/',{
// default params
id:''
},{
// custom actions
update: {method: 'PUT'}
});
return Resource;
}
return ResourceSvcFactory;
}]);
myapp.factory('AssetsResource', ['ResourceSvcFactory', function (ResourceSvcFactory) {
var endpoint = '/assets/';
var Resource = ResourceSvcFactory(endpoint);
return Resource;
}]);
I implement this in my Controller like this:
myapp.controller('AssetGroupListCtrl', [ 'AssetgroupsResource', function (AssetgroupsResource) {
var AssetGroups = AssetgroupsResource;
// ... rest of controller
}]);
When i run this it works fine. But, if i change the engaged status in the sessionStorage without a full reload, the instance in the controller does not pick up the new path.
Is there a way to 'refresh' the instance? ...automatically?
After hours of research, it appears that the fundamental flaw in what I'm trying to do in the question is this: I'm trying to use a 'singleton' as a 'class'. from the docs:
Note: All services in Angular are singletons. That means that the injector uses each recipe at most once to create the object. The injector then caches the reference for all future needs.
http://docs.angularjs.org/guide/providers
My work around was to create the $resource inside a method of a returned object. Here is an example:
MyApp.factory('AssetgroupsResource',
['$rootScope', '$resource',
function ($rootScope, $resource) {
return {
init: function () {
var accountId = sessionStorage.getItem('engagedAsId');
var apiPath = (accountId != null)
? '/api/account/' + accountId + endpoint
: '/api' + endpoint;
// default params
id:''
},{
// custom actions
});
return Resource;
}
}
}]);
This made it possible to build the object at the right time in the controller:
MyApp.controller('AssetGroupListCtrl', ['Assetgroups', function (Assetgroups) {
var Assetgroups = AssetgroupsResource.init();
// now I can use angular's $resource interface
}]);
Hope this helps someone. (or you'll tell me how this all could've been done in 3 lines!)
You can always call $scope.$apply(); to force an angular tick.
See a nice tutorial here: http://jimhoskins.com/2012/12/17/angularjs-and-apply.html
I think $resource uses promise which might be an issue depending on how you implement your factory in your controller.
$scope.$apply() can return an error if misused. A better way to make sure angular ticks is $rootScope.$$phase || $rootScope.$apply();.