Angular Components does not work with Promise and Service - angularjs

I have a really strange problem with Angular Components calling a service.
For example, i have a simple service with some mockup data as an array inside. The i add two methods, one synchron and one asynchron which returns a promise (if i correct understood).
Now a have a angular component which is well loaded in a example application.
On the frontend i have two buttons, one for each method in the service.
If i press now the button "btn1" the list is well loaded, all works fine.
If i now press the button "btn2" i see in the console that the service returns all data correctly, but the list in the frontend will not be loaded.
Service
var myItems = [
{id: 1, name: "item1"},
{id: 2, name: "item2"}
];
function ItemsService($q) { // inject the $q service
this.$q = $q;
}
ItemsService.prototype = {
loadItems: function () {
return myItems;
},
loadItemsAsync: function () {
var $qme = this.$q; // if i do not this, "this.$q" is undefined
return this.$q.resolve(myItems);
}
};
module.exports = {
ItemsService: ItemsService
}
Controller
function Foo(ItemsService) {
this.itemsService = ItemsService;
}
Foo.prototype = {
loadItems: function () {
this.items = this.itemsService.loadItems();
},
loadItemsAsync: function () {
this.itemsService.loadItemsAsync().then(
function (response) {
this.items = response;
console.log('data->' + this.items + ' -> ' + this.items[0].name);
},
function (error) {
console.log('error->' + error); // ignore
}
);
}
};
Component HTML
<section>
<button id="btn1" ng-click="$ctrl.loadItems()">Load</button>
<button id="btn2" ng-click="$ctrl.loadItemsAsync()">Load Async</button>
<ul>
<li ng-repeat="item in $ctrl.items">{{item.id}} // {{item.name}}</li>
</ul>
</section>
In future i want to replace the code in the service method of "loadItemsAsync" with a service call, but actually nothing works.
Future planned service method code
loadTreasuriesAsync: function () {
var $qme = this.$q;
return this.$http.get(this.url).then(
function (response) {
return $qme.resolve(response.data);
},
function (error) {
throw error;
}
);
}
I also tried this service call, but it also returns a promise object.
loadItems: function () {
return this.$http.get(this.url).then(function (response) {
return response.data;
});
},
Can anyone help me finding a solution?

this.items = response;
this does not exist anymore in the context. Try to save the context previously either by an outside variable (i.e. _this) or use arrow functions if you have those available.

Related

Angular - view not updating after model changed after http request

I am writing an app with Angular 1.5, Ionic 1.3, and Cordova. Right now I am working on a part where the user will push a button, the app will store the time and geolocation in the localStorage and then post the data back to the server and update the view. The issue I am having is that in one situation the view is not updating after a model change.
I have tried a few things: $timeout, $rootScope, $q and nothing seems to fix the issue. It only happens on iOS and not on Android. I am also using this library to help with the geolocation process: https://github.com/dpa99c/cordova-diagnostic-plugin
I am aware of the 3rd party library issue where it may not be a part of the Angular digest cycle but I have wrapped it with $q and no luck: https://www.searchenginepeople.com/blog/top-5-technical-issues-large-sites-angularjs.html
I am posting the pseudo code here.
view.html
<ion-view cache-view="false">
<ion-content>
<span>{{ data.text }}</span>
<span>{{ data.date }}</span>
<span>{{ data.isSync }}</span>
<button ng-click="showPopup()">Click</button>
</ion-content>
</ion-view>
controller.js
(function () {
'use strict';
angular
.module('myApp')
.controller('ViewController', ViewController);
ViewController.$inject = ['$scope', 'GeoDataModelService'];
function ViewController($scope, GeoDataModelService) {
$scope.data = GeoDataModelService.value;
$scope.showPopup = GeoDataModelService.showPopup();
}
})();
service.js
(function () {
'use strict';
angular
.module('myApp')
.factory('GeoDataModelService', GeoDataModelService);
GeoDataModelService.$inject = [
...
];
function GeoDataModelService(
...
) {
//data model
var dataModel = {
isSync: true,
text: null,
date: null
};
return {
value: dataModel,
showPopup: showPopup
};
function showPopup() { //gets called
$ionicPopup.confirm({
title: 'clock now ?',
buttons: [{
text: 'CONTINUE',
type: 'button-positive',
onTap: function () {
geoData();
}
}, {
text: 'CANCEL',
type: 'button-default'
}]
});
};
function geoData() {
getLocationServicesStatus()
.then(geoServiceSuccessful)
.catch(function(err) {});
};
function geoServiceSuccessful() { //gets called
DataModelService.createRecord();
sendDataToServerAfterGeoData();
}
function getLocationServicesStatus() {
console.log(' getLocationServicesStatus');
var deferred = $q.defer();
//this is outside of angular
cordova.plugins.diagnostic.isLocationAvailable(
function (available) {
if (available) {
deferred.resolve(true);
} else {
deferred.reject(false);
}
}, function (error) {
deferred.reject(false);
}
);
return deferred.promise;
}
function updateDataModel(source) {
console.log('source ', source); //this one is not null
if (source != null) {
dataModel.text = source.text;
dataModel.date = source.date;
console.log(JSON.stringify(dataModel)); //correct
}
}
function sendDataToServerAfterGeoData() {
//if offline just skip the server post
if (!navigator.onLine) {
// trigger digest cycle
$timeout(function () {
updateModelAfterRecord(); //this one works fine
}, 0);
return;
}
var clockins = DataModelService.load(); //load from local storage
console.log(' * * * * * HERE WE GO * * * * * ');
//this service returns an http promise
DataModelService
.sendLocalDataToService(clockins)
.then(sendDataToServerAfterGeoDataSuccess)
.then(getClockDataToServerAfterGeoSuccess)
.catch(handleSendDeviceDataToServerFail);
};
function sendDataToServerAfterGeoDataSuccess() {
console.log(' sendDataToServerAfterGeoDataSuccess ');
//this service returns an http promise
return DataModelService.getDataModelFromServer();
}
function getClockDataToServerAfterGeoSuccess(response) {
console.log(' getClockDataToServerAfterGeoSuccess ', response);
console.log('1 dataModel: ', dataModel);
// $timeout not working here
// $rootScope.asyncEval not working either
// $rootScope.$apply threw an error
console.log(' 2 dataModel: ', dataModel); //correct
dataModel.isSync = true;
updateDataModel(response); //goes through this code
console.log('3 dataModel: ', dataModel); //correct
console.log(' 4 dataModel: ', dataModel); //correct
console.log('5 dataModel: ', dataModel); //correct
return response; //tried to leave this out - no effect
}
function handleSendDeviceDataToServerFail(error) {
console.log('handleSendDeviceDataToServerFail ', error);
var clockins = DataModelService.load();
dataModel.isSync = false;
updateDataModel(clockins); //this works
}
function updateModelAfterRecord() {
dataModel.isSync = false;
var data = DataModelService.load();
updateDataModel(data);
}
}
})();
I added a watcher to see if the data is changing:
$scope.$watch('data.text', function(newVal, oldVal) {
console.log(' new val', newVal); //this is correct
});

`this` is undefined in Angular filter function used by ng-repeat

I'm trying to filter an an array in the dom that uses ng-repeat. The filter works when I hard code the return, but when I try to use this, it gives me the error
Cannot read property minHeight of undefined.
So for some reason this is undefined, but it works in other functions in the controller. I'm not sure why it doesn't work in the filter.
I'm sure I'm missing something ridiculously simple, but I cannot find it
Controller
export class ItemComponent {
/*#ngInject*/
constructor($http) {
this.$http = $http;
this.minHeight = 0;
}
filter(row){
console.log(this.minHeight);
return row.minHeight > this.minHeight;
}
HTML
ng-repeat="item in itemCtrl.items | filter: itemCtrl.filter"
Transpiled Code (from app.bundle.js in browser)
var ItemsComponent = exports.ItemsComponent = function () {
/*#ngInject*/
ItemsComponent.$inject = ['$http'];
function ItemsComponent($http) {
_classCallCheck(this, ItemsComponent);
this.$http = $http;
this.minHeight = 0;
}
_createClass(ItemsComponent, [{
key: '$onInit',
value: function $onInit() {
this.loadItems();
}
}, {
key: 'loaditems',
value: function loadItems() {
var _this = this;
this.$http.get('/api/items').then(function (res) {
_this.items = res.data;
console.log(_this.items);
}).catch(function (err) {
console.log(err);
});
}
}, {
key: 'filter',
value: function filter(row) {
console.log(this.minHeight);
return row.minHeight > 3;
}
}]);
return ItemsComponent;
}();
I'm pretty sure this is not the best approach, but since there is no answer and I could solve my problem this way, I will share it with you.
There is no this context in the filter function when called on ng-repeat (why?), so I solved it binding the context and getting the controller context from the this context.
HTML
ng-repeat="item in itemCtrl.items | filter: itemCtrl.filter.bind(this)"
Controller
filter(row) {
console.log(this.itemCtrl.minHeight);
}
Looks like you figured this out, but if you didn't want to bind in the template file, and I prefer to put as little as possible in the template file, you could bind in your class definition in the constructor.
For example:
export class ItemComponent {
/*#ngInject*/
constructor($http) {
this.$http = $http;
this.minHeight = 0;
// arrow function will bind the 'this' you need, or you could use '.bind' instead
this.filter = (row) => {
console.log(this.minHeight);
return row.minHeight > this.minHeight;
}
}

Update $http return data to html view

I am getting my data form angular service
.service('SelectedMemberDetail', function ($http) {
return {
get : function(id) {
return $http.get('/api/members/'+ id);
},
create : function(id) {
return $http.post('/api/members/'+ id);
},
delete : function(id) {
return $http.delete('/api/members/'+ id);
}
}
})
This is the controller function which calling that service.
$scope.show = function (user_id) {
$scope.selectedMember = SelectedMemberDetail.get(user_id);
}
And i am trying to get view in html like this
<h2>
<span class="glyphicon glyphicon-user mleft_10 icon_big"></span>
{{ selectedMember.firstName[0] }}
</h2>
<div>
<p class="clear_margin"><b>Address: </b>
{{ selectedMember.address[0] }}
</p>
<p><b>Contact: </b>{{ selectedMember.contact[0] }}</p>
</div>
I checked, the service function is returning json data, which is here,
_id: "552386cb880611101168ab4b"
address: ["pata nai", "text"]
contact: ["23456", "tel"]
email: ["anoop#email", "email"]
firstName: ["Anoop", "text"]
lastName: ["singh", "text"]
I am not able to see the updated data on browser. What's wrong with my code?
Use promise return by $http and then assign value.It is not showing anything because you are directly assigning promise to the assignment it will not show anything in the view.
SelectedMemberDetail.get(user_id).then(function(response){
$scope.selectedMember = response.data
});
SelectedMemberDetail.get(user_id).success(function(response){
$scope.selectedMember = response.data
});
There is two way to capture promise return from $http one is then and another is success.
Look at the Doc.
When you use $http.get it returns a promise not the data itself. You need to do this:
$scope.show = function (user_id) {
SelectedMemberDetail.get(user_id)
.success(function (result) {
$scope.selectedMember = result;
});
}
see more here:
https://docs.angularjs.org/api/ng/service/$http
I like this paradigm:
.service('SelectedMemberDetail', function ($http) {
return {
get : function(id) {
var member = {};
member.$promise = $http.get('/api/members/'+ id)
.success(function(data) {
angular.copy(data, member);
});
return member;
}
}
})
The get method returns a reference to the member right away. When the $http call completes successfully, it uses angular.copy to assign the member's properties, while preserving the reference.
That way you can still do this (main use case):
$scope.show = function (user_id) {
$scope.selectedMember = SelectedMemberDetail.get(user_id);
}
The member also has a $promise property, so if you need to handle success or error (edge use case):
$scope.selectedMember = SelectedMemberDetail.get(user_id);
$scope.selectedMember.$promise.success(function(data) {
// do something else with member
});

Should I return collection from factory to controller via function?

I'm struggling with choosing the correct way of accessing collection located inside service from controller. I see two options, both have ups and downs:
Returning function from service that returns collection:
Service:
app.factory('healthService', function () {
var healths = [{},{},{},{}];
function updateHealths() {
healths = [...];
}
return {
getHealths : function() {
return healths;
},
update : function () {
updateHealths();
}};
});
Controller:
$scope.healths = healthService.getHealths;
$scope.update = healthService.update;
View:
ng-repeat = "health in healths()"
ng-click = "update()" '
I'm not sure about efficiency here- how often will healths() be evaluated?
Giving the possibility to access collection directly from controller:
Service:
app.factory('healthService', function () {
return {
healths : [{},{},{},{}],
update :function() {
this.healths = [...];
}
});
Controller:
$scope.healthService = healthService;
View:
ng-repeat = "health in healthService.healths" '
ng-click = "healthService.update()"
Which one is better, faster? Any other tips?
Why not try wrapping your collection in an object (which acts as a service state) to allow binding to occur on the object, rather than by exposing functions. For example:
app.factory('healthService', function() {
var state = {
healths: [...]
};
return {
getState: function() { return state; },
updateHealths: function() { state.healths = [...]; }
};
});
Then inside your controller:
$scope.healthState = healthService.getState();
Then to reference your healths from your html, use:
<div ng-repeat="health in healthState.healths"></div>

Variables between factories in angular.js

I'm currently learning angular and have hit a roadblock.
The second factory (shown below) makes an http request like this: http://example.com/api/get_post/?post_id=7129&callback=JSON_CALLBACK');
I want the post ID to be a variable. So depending on which blog title is clicked, I can pass the correct variable into that http request.
In other words, I guess I want to take a result from the first factory (blogAPIservice) and use it in the second factory.
Makes sense??
<!-- FACTORIES -->
angular.module('blogApp.services',[])
.factory('blogAPIservice',function($http) {
var blogAPI = [];
var blogs = $http.jsonp('http://example.com/api/get_recent_posts/?count=10&callback=JSON_CALLBACK');
blogs.success(function(data) {
$.each(data.posts, function(i, blog) {
var fromNow = moment(blog.date).fromNow();
blogAPI.push({
url: blog.url,
title: blog.title,
excerpt: blog.excerpt,
date : fromNow,
id: blog.id
})
});
});
var factory = {};
factory.getBlogs = function () {
return blogAPI;
};
return factory;
})
.factory('singlePostService',function($http) {
var singleAPI = [];
var postID = '7129';
var singlePost = $http.jsonp('http://example.com/api/get_post/?post_id=7129&callback=JSON_CALLBACK');
singlePost.success(function(data) {
singleAPI.push({
title: data.post.title,
content: data.post.content
})
});
var factory = {};
factory.getSinglePost = function () {
return singleAPI;
};
return factory;
})
And here are the controllers:
angular.module('blogApp.controllers', [])
.controller('resultsController',function($scope, blogAPIservice) {
$scope.keywordFilter = null;
$scope.blogs = [];
init();
function init() {
$scope.blogs = blogAPIservice.getBlogs();
}
function grabID() {
$(this).attr('rel');
}
})
.controller('singlePostController',function($scope, singlePostService) {
$scope.keywordFilter = null;
$scope.singlePost = [];
init();
function init() {
$scope.singlePost = singlePostService.getSinglePost();
}
})
And finally the markup:
<li ng-repeat="blog in blogs">
{{ blog.title }}
</li>
You can inject the first service into the second one like this:
.factory('singlePostService',function($http, blogAPIservice) {
//Do something with blogAPIservice
}
For more information about depenency injection read the docs

Resources