I'm trying to follow John Papa's Controller Activation Process where the data that a controller needs is loaded at the constructor of the controller. However, my page is triggering a lot of errors due to Angular trying to bind to a null object until the data is loaded.
Example
Controller initializes and performs an AJAX call to fetch information about a movie. Meanwhile, the HTML tries to load information about the movie's director.
Controller.js
class MovieController {
constructor(movieHttp) {
this.movieHttp = movieHttp;
this.movie = null;
activate();
}
activate() {
this.movieHttp.getMovie(...).then(movieData => { this.movie = movieData; });
}
}
index.html
...
<span ng-bind="vm.movie.actor.name"></span>
...
Is there a nice way of resolving this issue other than changing the ng-bind into a function call, and then doing an if (!this.movie) { return; }?
You can set an empty movie object on your ctrl (or better yet a service), and update it when the ajax calls returns:
class MovieController {
constructor(movieHttp) {
this.movieHttp = movieHttp;
this.movieData = {
movie: {
actor: {
name: ''
},
title: ''
}
};
activate();
}
activate() {
this.movieHttp.getMovie(...).then(movieData => { this.movieData.movie = movieData;})
}
}
and your html:
<span ng-bind="vm.movieData.movie.actor.name"></span>
it could be annoying if the object is very complex however you set it only once and than you can easily figure out what's the retured object structure is (again a better place to store it would be a service).
another solution is using the router resolve property which forces the promise to be reolved before the controller is instantiated (and injecting the resolved object as a parameter to your controller)
ui-router has a convinent way for that (https://github.com/angular-ui/ui-router/wiki), but angular's default router also has this option (https://docs.angularjs.org/api/ngRoute/provider/$routeProvider)
Related
Currently for loading a minimum of 100 customer booking records, We have used Angular factory as InitializationFactory for having the data in hand before the page loads and this factory is registered in Routes. This factory makes use of customerService to talk to relevant API. This whole request takes round about 5-7seconds to complete. We have the requirement of applying filters on top of the fetched data.
Considering page performance in mind, We now want to fetch only 10 records with dynamic filters applied over it. Next set of records would be retrieved based on a click event. Now, in order to do so, I need to inject customerService into CustomerController as well.
So, by design, is it a good approach to inject customerService into factory and then into controller for further data retrieval?
Code Snippet:
Route.js
.state("app.customer.dashboard",
{
parent: "parent",
controller: "CustomerBookingController",
resolve: {
customerBookingListModel: [
"CustomerInitializationFactory",
function (customerInitializationFactory) {
return customerInitializationFactory.initializeCustomerBookingResult();
}
]
}
CustomerInitializationFactory.js
function CustomerInitializationFactory(customerService, $translate, bookingConstants) {
return {
initializeCustomerAccountResult: function() {
var params = "xyz";
var promise = customerService.getCustomerBookings(params)
.then(function(bookingResponse) {
var customerBookings = constructCustomerBookings(bookingResponse);
return customerBookings;
});
return promise;
} };
CustomerBookingController.js
function CustomerBookingController(customerBookingListModel,customerService???) {
$scope.LoadMoreBookings = function(params) {
var bookings = customerService.getCustomerBookings(params);
};
I am currently using Angular 1.5. I am using ui-router as my primary navigation mechanism. I am leveraging Angular components.
I understand that I can use .resolve on my states to instantiate services which are then passed down through my component hierarchy (mostly using one-way bindings).
One of my components is called literatureList and is used in more than one route/state. The literatureList component makes use of a specific service called literatureListService. literatureListService is only used by literatureList. literatureListService takes a while to instantiate, and uses promises etc.
In each of the .state definitions then I need to have a .resolve that instantiates literatureListService. This means that I need to refer to this literatureListService in each of the .state.resolve objects. This doesn't seem very DRY to me.
What I'd really like to do is remove the literatureListService references from the .state.resolve objects and 'resolve' the service from 'within' the literatureList component itself.
How do I code a 'resolve-style' mechanism within the literatureList component that will handle the async/promise nature of literatureListService? What is best practice for doing this?
Code snippets follow:
state snippets:
$stateProvider.state({
name: 'oxygen',
url: '/oxygen',
views: {
'spofroot': { template: '<oxygen booklist="$resolve.literatureListSvc"></oxygen>' }
},
resolve:{
literatureListSvc: function(literatureListService){
return literatureListService.getLiterature();
}
}
});
$stateProvider.state({
name: 'radium',
url: '/radium',
views: {
'spofroot': { template: '<radium booklist="$resolve.literatureListSvc"></radium>' }
},
resolve:{
literatureListSvc: function(literatureListService){
return literatureListService.getLiterature();
}
}
});
literatureListService:
angular.module('literature')
.factory('literatureListService',function($http,modelService){
// Remember that a factory returns an object, whereas a service is a constructor function that will be called with 'new'. See this for a discussion on the difference: http://blog.thoughtram.io/angular/2015/07/07/service-vs-factory-once-and-for-all.html
console.log('literatureListService factory is instantiating - this will only happen once for each full-page refresh');
// This is a factory, and therefore needs to return an object containing all the properties that we want to make available
var returnObject = {}; // Because this is a factory we will need to return a fully-formed object (if it was a service we would simply set properties on 'this' because the 'context' for the function would already have been set to an empty object
console.log('service instantiation reporting that modelManager.isDataDirty='+modelService.isDataDirty);
// The getLiterature method returns a promise, and therefore can only be consumed via a promise-based mechanism
returnObject.getLiterature = function($stateParams){
console.log('literatureService.getLiterature will now return a promise (via a call to $http)');
return $http({method: 'GET', url: 'http://localhost:3000/literature/'});
};
return returnObject;
});
oxygen component html:
<div>
This is the OXYGEN component which will now render a literature list, passing in bookList.data as books
<literature-list books="$ctrl.booklist.data"></literature-list>
</div>
oxygen component js
angular.module('frameworks')
.component('oxygen',{
templateUrl:"frontend/framework/frameworks/oxygenComponent.html",
controller:function($http){
var $ctrl = this;
console.log('Hello from the oxygen component controller with literatureListSvc='+$ctrl.booklist); // Bound objects NOT YET AVAILABLE!!!!!!
this.$onInit = function() {
//REMEMBER!!!! - the bound objects being passed into this component/controller are NOT available until just before the $onInit event fires
console.log('Hello from the oxygen component controller onInit event with bookList='+JSON.stringify($ctrl.booklist));
};
}
,bindings:{ // remember to make these lowercase!!!
booklist:'<'
}
});
literatureList component html:
<div>
{{$ctrl.narrative}}
<literature-line ng-repeat="literatureItem in $ctrl.books" wizard="fifteen" book="literatureItem" on-tut="$ctrl.updateItemViaParent(itm)">555 Repeat info={{literatureItem.title}}</literature-line>
</div>
literatureList component js
angular.module('literature')
.component('literatureList',{
templateUrl:'frontend/literature/literatureListComponent.html',
//template:'<br/>Template here33 {{$ctrl.listLocalV}} wtfff',
// controller:function(literatureListService){
controller:function(){//literatureListService){
var $ctrl=this;
this.narrative = "Narrative will unfold here";
this.updateItemViaParent = function(book){
this.narrative = 'just got notified of change to book:'+JSON.stringify(book);
};
this.$onInit = function(){
console.log('literatureList controller $onInit firing with books='+JSON.stringify($ctrl.books));
};
this.$onChanges = function(){
console.log('literatureList controller $onChanges firing');
};
},
bindings: {
books:'<'
}
});
As JavaScript in reference based, you can crete object in your service and access it in all three controllers that you need.
For Example:
function serviceA() {
var vm = this;
vm.testObject = {};
vm.promise1().then(function(response) {
vm.testObject = response;
})
}
function ControllerA($scope, serviceA) {
$scope.testA = service.testObject;
}
In this case, as soon as the promise is resolved, all the controllers will get the value of the response and can be used in the partials respecively
I want to display view once the list of content is retrieved from the database display it in the view.
On a high level what I am doing now works more or less but upon first access the previous results appears to be cached or still saved in my storage service.
This is what the service looks like.
function StorageService() {
var opportunities;
var getOpportunities = function() {
return opportunities;
}
var setOpportunities = function(data) {
opportunities = data;
}
return {
getOpportunities: getOpportunities,
setOpportunities: setOpportunities
}
}
So when I click on the tab to getOpportunities I go direct to the view first and the load the data.
$scope.getAllOpportunities = function() {
$state.go("app.opportunities");
communicationService.getOpportunitiesAll().then(function(data) {
StorageService.setOpportunities(data);
if (!data.length) {
$("#noShow").html("There are no opportunities");
}
}, function(error) {})
}
once the view is rendered I retrieve the results and bind it to the view.
controller('OpportunitiesController', function($scope) {
$scope.$on("$ionicView.afterEnter", function(scopes, states) {
$scope.opportunities = StorageService.getOpportunities();
});
What I am doing here also feels a bit wrong.
Is there a better way or a way that I can improve on the existing.
I want to load the view and the replace the loader with the data once the data is ready.
Thanks in advance
You should resolve the promise in the route, using the resolve property. That way, the data will always be available when the controller is instantiated.
https://toddmotto.com/resolve-promises-in-angular-routes/
Unless the resource is huge and you want to show som loading animation while getting the data. Then it would probably be more proper to just get the data in the controller.
controller('OpportunitiesController', function($scope) {
communicationService.getOpportunitiesAll().then(function(response){
$scope.opportunities = response;
})
});
html:
<span ng-if="!opportunities">Getting stuff</span>
<span ng-if="opportunities">Stuff fetched</span>
Also, there is no use to have getter and setters in the service. Javascript objects are passed by reference so you can just expose the property directly.
I have an AngularJs app with a master details that I have changed to use
ui-router.
Previously, I had a SelectionService with a plain JavaScript observer that was
used by the master to notify selection and by the details to update himself. The
selection was just a technical identifier, so that the DetailsController has
to get the item from my BackendService which is basically a local cache.
Now with ui-router when an item is selected into the master, I go the details
state and the same flow remains (use the technical id to get details from backend).
My problem is that into the previous version all updates made on the details
where automagically updated on the master. But that is broken with the ui-router
version, probably because the MasterController and DetailsController don't
share the same scope.
So, my question is : How do you ensure that a list of items is updated when one
item is changed. Do you rely on some AngularJs functionalities (then how) or do
you use a classic events mechanism with $scope.$broadcast and $scope.$on ?
Edit, after more investigations
I have read some articles that are clearly against the usage of AngularJs events ($scope.$broadcast, $scope.$emit and $scope.$on) and recommand a custom event bus/aggregator.
But I would like to avoid that and thus rely on the AngularJs digest lifecycle. However what is suggest by #Kashif ali below is what I have but my master is not updated when the details changes.
angular
.module('masterDetails')
.service('BackendService', function($q){
var cache = undefined;
var Service = {}
Service.GetImages = function() {
var deferred = $q.defer();
var promise = deferred.promise;
if ( !cache ) {
// HTTP GET from server .then(function(response){
cache = JSON.parse(response.data);
deferred.resolve(cache);
});
} else {
deferred.resolve(cache);
}
return promise;
}
Service.GetImage = function(key) {
return GetImages().then(function(images){
return images[key];
});
}
Service.SetImage = function(key, updated) {
GetImages().then(function(images){
images[key] = updated;
// TODO; HTTP PUT to server
});
}
})
.controller('MasterController', function(BackendService){
var vm = this;
vm.items = [];
vm.onSelect = onSelect;
init();
function init() {
BackendService.GetImages().then(function(images){
// images is a map of { key:String -> Image:Object }
vm.items = Object.keys(images).map(function(key){
return images[key];
});
});
}
function onSelect(image) {
$state.go('state.to.details', {key: image.key});
}
})
.controller('DetailsController', function(BackendService, $stateParams){
var vm = this;
vm.image = undefined;
init();
function init() {
BackendService.GetImage($stateParams.key).then(function(image){
vm.image = image;
}).then(function(){
// required to trigger update on the BackendService
$scope.$watch('[vm.image.title, vm.image.tags]', function(newVal, oldVal){
BackendService.SetImage(vm.image.key, vm.image);
}, true);
});
}
});
Edit, this is due to the states
So, when I open the app on #/images the images state start. Then I select one image to go to the images.edit state and everything works well, the master is updated when details changes.
However if I start on #/images/:key which is the images.edit state, then the master ignore all changes mades on the master.
You can rely on both the solution you have mentioned
1.You can achieve this using factories in angularjs
Factories/services are the singleton objects that is shared along the the app:
example:
angular.module("app",[]).factory('myfactory',function(){
var data;
{
getData:getData,
setData:setData
};
function setData(data1)
{
data=data1;
}
function getData()
{
return data;
}
}
).controller('myclrl1',function($scope,myfactory){
}).controller('myclrl2',function($scope,myfactory){
});
you can inject these controller in different views and can access singleton factory(all controller will share the same object) "myfactory" in both controller using getter and setter
you can use $scope.$broadcast and $scope.$on to make nested contollers to communicate with each other
you can find the detailed Example over here.
$scope.$broadcast and $scope.$on
hope that would be helpful
Regards
For the sake of understanding suppose I have an AngularJS application that has similar data as Stackoverflow so that it:
is using the usual ngRoute/$routeProvider
has a userService that returns favourite and ignore tag lists of the logged in user - both lists are fetched at the same time and request for them is a promise that when resolved caches these lists
has a view that displays a list of questions with a QuestionsController that provides its model (similar to Stackoverflow)
QuestionsController makes a request for questions and then depends on cached tag lists to mark them appropriately
As the recommended guideline when controllers rely on other async data we should offload those to route resolve part so when controllers are being instantiated those promises are already resolved. Therefore I offload tag list fetching to it so both lists are ready and injected into the controller. This all works as expected.
The additional feature of my questions list view is that when a user clicks a tag displayed on questions it automatically adds this tag to favourite list (or off of it when that tag is already part of favourite list).
Route configuration
...
.when({
templateUrl: "...",
controller: "QuestionsController as context",
resolve: {
tags: ["userService", function(userService) {
return userService.getMyTags();
}]
}
})
.when(...)
...
Controller pseudo code
QuestionsController.prototype.markQuestions = function() {
this.model.questions.forEach(function(q, idx) {
// "myTags" is resolve injected dependency
q.isFavourite = q.tags.any(myTags.favourite);
q.isIgnored = q.tags.any(myTags.ignored);
});
};
QuestionsController.prototype.toggleTag = function(tag) {
var self = this;
// change tag subscription
tagService
.toggleFavourite(tag)
.then(function() {
// re-mark questions based on the new set of tags
self.markQuestions();
});
};
The problem
When the view displays, all questions are loaded and correctly marked as per provided tag lists. Now when a user clicks on a specific tag and that tag's favourite status gets changes my controller's dependency should get automatically updated.
How can I do that since my controller is already instantiated and had tag lists injected during instantiation?
I would like to avoid loading those lists manually within my controller because in that case I should do the same during instantiation and reuse the same functionality and not have it in two places (route resolve and inside controller).
So long as your "resolved" variable is referring to the same object used elsewhere, they are one and the same.
So, if your userService.getMyTags is conceptually like the following:
.factory("userService", function($timeout){
var tags = [/*...*/];
return {
getMyTags: function(){
return $timeout(function(){ return tags; }, 500);
},
addTag: function(newTag){
tags.push(newTag);
}
}
});
Then any reference to tags anywhere would get the changes:
.controller("ViewCtrl", function($scope, tags){
$scope.tags = tags; // tags is "resolved" with userService.getMyTags()
})
.controller("AddTagCtrl", function($scope, userService){
$scope.addTag = function(newTag){
userService.addTag(newTag); // changes will be reflected in ViewCtrl
}
}
plunker, to illustrate