Angular service not exposing value as expected - angularjs

I'm having a problem getting at values in my service from the controller. My service looks like this:
angular.module('someApp').factory('someSvc', SomeSvc);
function SomeSvc($http) {
var colors = [];
function loadColors() {
return $http.get('SomeApi/GetColors')
.then(function (result) {
//colors = result.data.colors;//<-this doesn't work
//angular.copy(result.data.colors, colors);//<-this works
});
}
return {
loadColors: loadColors,
colors: colors
};
}
Then my controller might make a call like this:
someSvc.loadColors().then(function(){vm.colors = someSvc.colors;});
So, when I debug, if I set a breakpoint in the controller where the assignment to vm.colors is made, the colors property exposed on the someService object has just an empty array or any array with the expected values depending on which of the two commented-out lines I use in the service.
If I set a breakpoint in the service where the assignment to colors is made, the variable colors always has the expected values (e.g., let's say ["red", "yellow", "green"] is what comes back from the http call). So I can watch the controller trigger the http call, watch the value come back and get assigned to colors in the service, but then the controller just sees an empty array unless I do that angular.copy call.
Also, interestingly, if I change the service's return statement to look like this:
return {
loadColors: loadColors,
colors: function() {return colors;}
};
and then in the controller say vm.colors = someSvc.colors(); then that works just fine as well.
Why is this? Why isn't that array getting passed through?
UPDATE:
I've found that instead of the angular.copy() line, I can alternatively do this, and everything works as expected:
for (var i = 0; i < result.data.colors.length; i++) {
colors[i] = result.data.colors[i];
}
It seems to be that ASSIGNING the object is a problem, while modifying it is ok? Why is that?

This might work for ya. Guessing it's just a pointer issue maybe?
angular.module('someApp')
.factory('someSvc', function($http)
{
return {
colors: [],
loadColors: function()
{
var self = this;
return $http.get('SomeApi/GetColors').then(function (result)
{
self.colors = result.data.colors;
});
}
};
});

At the time you're calling return in your Factory, someSvc.colors is just the empty array - and the value is returned. Since Angular providers in general attempt to run only once, in future it doesn't actually check someSvc.colors again - just returns the initial value.
Wrapping it in a function means it runs the function every time, so it fetches the updated value.

Related

Value from $rootScope is not loaded properly

So I'm basically trying to get a property from my $rootScope when the page loads. I need this property so I can display the value in my form.
After testing this:
console.log("DEBUG $rootScope", $rootScope);
console.log("DEBUG $rootScope.localClient", $rootScope.localClient);
I've noticed that $rootScope contains a localClient property, but $rootScope.localClient is undefined. Why is this?
See console screen below.
Here is where I fill the localClient object
function setClient(client, tvaNumber) {
if (tvaNumber) {
if (angular.isUndefined($rootScope.localClient))
$rootScope.localClient = {};
$rootScope.localClient[tvaNumber] = client;
}
}
Try accessing it like this,
console.log("DEBUG $rootScope.localClient", $rootScope['localClient']);
You must make sure the attribute loaded before use it, because JavaScripte always pass a reference to an object. Or you can try console.log(JSON.parse(JSON.stringify($rootScope)) get the real value.
One example:
var a = {}; console.log(a);a.test = '1';

Trying to bind in ng-init a value. This value was set afler model loading, and 2 ajax calls

I explain myself,
The application i'm making right now come from a derivative of phonegap. I must absolutely waiting ajax answer to set de ng-init. I know i can do something like this ng-init = "ng-model = arr[0]" or i can use a variable for le position of the array like ng-init = "ng-model = arr[position]". That is simple in most case, but me because i'm working on a derivative phonegap i must call my function from a focus method with an ajax call, plus waiting after the result of another ajax call. See what i mean:
HTML select code:
<select id="usSelect" class="geotabFormEditField" ng-init="usCycle = usCyclesArr[usPos]" ng-model="usCycle" ng-options="x.descEn for x in usCyclesArr" ng-change="usSel()"}></select>
Function where i call my angular function to define which option must be selected. I must absolutely call it in this function.
focus: function (freshApi, freshState) {
freshApi.getSession(session => {
database = session.database;
freshApi.call('Get', {
typeName: 'User',
search: {
name: session.userName
}
}, function(user){
lang = user[0].language;
glScope.getHosRules(database);
}, function(){
//Oops can't get user
});
});
},
function called is getHosRule.
The angular array:
$scope.usCyclesArr = [{cycleId: 'America8Day', descEn: 'USA Property 70-hour/8-day', descFr: 'Propriété É.-U. 70 heures/8 jours'}, {cycleId: 'America7Day', descEn: 'USA Property 60-hour/7-day', descFr: 'Propriété É.-U. 60 heures/7 jours'}, {cycleId: 'America7DayBig', descEn: 'USA Property 60-hour/7-day (16-hour exemption)', descFr: 'Propriété É.-U. 60 heures/7 jours (exemption de 16 heures)'}, {cycleId: 'America7DayNo34h', descEn: 'USA Property 60-hour/7-day without 34-hour reset', descFr: ''}, ...];
function to get which option must be selected:
$scope.getHosRules = function(dat) {
console.log(dat, url);
$http({
method: 'GET',
url: $scope.urlPath + 'borderCross/' + dat + '/getHosRules',
responseType: 'json',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
}).then(function successCall(response) {
//console.log(response.data);
for(var i = 0; i < $scope.usCyclesArr.length; i++) {
//console.log($scope.usCyclesArr[i].cycleId);
if($scope.usCyclesArr[i].cycleId === response.data[0].us_id) {
$scope.usPos = i;
}
}
}, function errorCall() {
console.log('Unexpected error');
});
}
The problem is usPos get is value after ng-model is loaded and i must waiting for ajax answer, so it desn't working. I tried ng-init="{{usCycle = usCyclesArr[usPos]}}" It seem working but an error appear in the console and i can't take the risk on a professional app. So Anybody knows how can i bind usPos variable ?
From what I can understand from your question, you need to set the value of $scope.usCycle which will be shown as the selected value of dropdown.
Rather than setting the value in ng-init, why are you not setting in for loop:
for(var i = 0; i < $scope.usCyclesArr.length; i++) {
//console.log($scope.usCyclesArr[i].cycleId);
if($scope.usCyclesArr[i].cycleId === response.data[0].us_id) {
$scope.usCycle = usCyclesArr[i]; // <-- directly assigning value
}
}
Few points I would like to highlight:
You are facing issues because your ng-init works with AngularJS lifecycle where as your Business logic is triggered from focus function which is not at all connected with AngularJS. So, its a bad choice to mix code like this. To provide better UX, you can use spinner to make the user wait unless the $scope.usCycle value is assigned.
Rather than iterating through entire loop of for(var i = 0; i < $scope.usCyclesArr.length; i++) , you can break break the loop once you find the value. It will save extra looping cycles.
I found the way myself and it's simple. Refreshing my ng-model with the simple code below and removing ng-init attribute in html file.
$scope.usCycle = $scope.usCyclesArr[$scope.usPos];
Dropdown take right position.
Of course like Vivek said, breaking the loop when position is found is better.

AngularJS watch model value if model is not null

Simple question here.
I have this watch:
// Watch our model
$scope.$watch(function () {
// Watch our team name
return self.model.team.data.name;
}, function (name) {
console.log(name);
// if we have a name
if (name) {
// Store our model in the session
sessionStorage.designer = angular.toJson(self.model);
}
});
The team model is pull in from the database as a promise (hence the data) so when the watch first fires self.model.team has not been set so it is null.
How can I get my watch to either wait until it has been set or add a check into the return function of the watch?
Use a watch expression instead of a function. This will catch any errors with missing objects and return undefined.
// Watch our model
$scope.$watch('self.model.team.data.name', function (name) {
console.log(name);
// if we have a name
if (name) {
// Store our model in the session
sessionStorage.designer = angular.toJson(self.model);
}
});
There is no magic here - if one of the variables you are accessing could be null/undefined, then you cannot get its property if it's null/undefined. So, you have to guard against that:
$scope.$watch(
function(){
return (self.model.team && self.model.team.data.name) || undefined;
},
function(v){
// ...
});
The only "magic" is when you "$watch" for expressions, but the expressions need to be exposed on the scope. So, you could do:
$scope.model = self.model;
$scope.$watch("model.team.data.name", function(v){
// ...
});
But, really, you have to ask yourself why you need a $watch here to begin with. It seems to me that you are getting the team asynchronously once - it does not look like it will change except by maybe another async call. So, just handle that when you receive the data without the $watch:
someSvc.getTeam() // I made an assumption about a service that pulls the data from db
.then(function(team){
var name = team.data.name;
// if we have a name
if (name) {
// Store our model in the session
sessionStorage.designer = angular.toJson(self.model);
}
});
An unnecessary $watch is expensive - it is evaluated on every digest cycle, so, it's best to reduce the number of $watchers.

How do you tell when a view is loaded in extjs?

Im working on an extjs application. We're have a page that is for looking at a particular instance of an object and viewing and editing it's fields.
We're using refs to get hold of bits of view in the controller.
This was working fine, but I've been sharding the controller into smaller pieces to make it more managable and realised that we are relying on a race condition in our code.
The logic is as follows:
Initialise the controller
parse the url to extract the id of the object
put in a call to load the model with the given view.
in the load callback call the controller load method...
The controller load method creates some stores which fire off other requests for bits of information using this id. It then uses some of the refs to get hold of the view and then reconfigures them to use the stores when they load.
If you try and call the controller load method immediately (not in the callback) then it will fail - the ref methods return undefined.
Presumably this is because the view doesnt exist... However we aren't checking for that - we're just relying on the view being loaded by the time the server responds which seems like a recipe for disaster.
So how can we avoid this and be sure that a view is loaded before trying to use it.
I haven't tried rewriting the logic here yet but it looks like the afterrender event probably does what I want.
It seems like waiting for both the return of the store load and afterrender events should produce the correct result.
A nice little abstraction here might be something like this:
yourNamespace.createWaitRunner = function (completionCallback) {
var callback = completionCallback;
var completionRecord = [];
var elements = 0;
function maybeFinish() {
var done = completionRecord.every(function (element) {
return element === true
});
if (done)
completionCallback();
}
return {
getNotifier: function (func) {
func = func || function (){};
var index = elements++;
completionRecord[index] = false;
return function () {
func(arguments);
completionRecord[index] = true;
maybeFinish();
}
}
}
};
You'd use it like this:
//during init
//pass in the function to call when others are done
this.waiter = yourNamespace.createWaitRunner(controller.load);
//in controller
this.control({
'SomeView': {
afterrender: this.waiter.getNotifier
}
});
//when loading record(s)
Ext.ModelManager.getModel('SomeModel').load(id, {
success: this.waiter.getNotifier(function (record, request) {
//do some extra stuff if needs be
me.setRecord(record);
})
});
I haven't actually tried this out yet so it might not be 100% but I think the idea is sound

Angularjs promise not binding to template in 1.2

After upgrading to 1.2, promises returned by my services behave differently...
Simple service myDates:
getDates: function () {
var deferred = $q.defer();
$http.get(aGoodURL).
success(function (data, status, headers, config) {
deferred.resolve(data); // we get to here fine.
})......
In earlier version I could just do, in my controller:
$scope.theDates = myDates.getDates();
and the promises returned from getDates could be bound directly to a Select element.
Now this doesn't work and I'm forced to supply a callback on the promise in my controller or the data wont bind:
$scope.theDates = matchDates.getDates();
$scope.theDates.then(function (data) {
$scope.theDates = data; // this wasn't necessary in the past
The docs still say:
$q promises are recognized by the templating engine in angular, which means that in templates you can treat promises attached to a scope as if they were the resulting values.
They (promises) were working in older versions of Angular but in the 1.2 RC3 automatic binding fails in all my simple services.... any ideas on what I might be doing wrong.
There are changes in 1.2.0-rc3, including one you mentioned:
AngularJS 1.2.0-rc3 ferocious-twitch fixes a number of high priority
issues in $compile and $animate and paves the way for 1.2.
This release also introduces some important breaking changes that in some cases could break your directives and templates. Please
be sure to read the changelog to understand these changes and learn
how to migrate your code if needed.
For full details in this release, see the changelog.
There is description in change log:
$parse:
due to 5dc35b52, $parse and templates in general will no longer automatically unwrap promises. This feature has been deprecated and
if absolutely needed, it can be reenabled during transitional period
via $parseProvider.unwrapPromises(true) api.
due to b6a37d11, feature added in rc.2 that unwraps return values from functions if the values are promises (if promise unwrapping is
enabled - see previous point), was reverted due to breaking a popular
usage pattern.
As #Nenad notices, promises are no longer automatically dereferenced. This is one of the most bizarre decisions I've ever seen since it silently removes a function that I relied on (and that was one of the unique selling points of angular for me, less is more). So it took me quite a bit of time to figure this out. Especially since the $resource framework still seems to work fine. On top of this all, this is also a release candidate. If they really had to deprecate this (the arguments sound very feeble) they could at least have given a grace period where there were warnings before they silently shut it off. Though usually very impressed with angular, this is a big minus. I would not be surprised if this actually will be reverted, though there seems to be relatively little outcry so far.
Anyway. What are the solutions?
Always use then(), and assign the $scope in the then method
function Ctrl($scope) {
foo().then( function(d) { $scope.d = d; });
)
call the value through an unwrap function. This function returns a field in the promise and sets this field through the then method. It will therefore be undefined as long as the promise is not resolved.
$rootScope.unwrap = function (v) {
if (v && v.then) {
var p = v;
if (!('$$v' in v)) {
p.$$v = undefined;
p.then(function(val) { p.$$v = val; });
}
v = v.$$v;
}
return v;
};
You can now call it:
Hello {{ unwrap(world) }}.
This is from http://plnkr.co/edit/Fn7z3g?p=preview which does not have a name associated with it.
Set $parseProvider.unwrapPromises(true) and live with the messages, which you could turn off with $parseProvider.logPromiseWarnings(false) but it is better to be aware that they might remove the functionality in a following release.
Sigh, 40 years Smalltalk had the become message that allowed you to switch object references. Promises as they could have been ...
UPDATE:
After changing my application I found a general pattern that worked quite well.
Assuming I need object 'x' and there is some way to get this object remotely. I will then first check a cache for 'x'. If there is an object, I return it. If no such object exists, I create an actual empty object. Unfortunately, this requires you to know if this is will be an Array or a hash/object. I put this object in the cache so future calls can use it. I then start the remote call and on the callback I copy the data obtained from the remote system in the created object. The cache ensures that repeated calls to the get method are not creating lots of remote calls for the same object.
function getX() {
var x = cache.get('x');
if ( x == undefined) {
cache.put('x', x={});
remote.getX().then( function(d) { angular.copy(d,x); } );
}
return x;
}
Yet another alternative is to provide the get method with the destination of the object:
function getX(scope,name) {
remote.getX().then( function(d) {
scope[name] = d;
} );
}
You could always create a Common angular service and put an unwrap method in there that sort of recreates how the old promises worked. Here is an example method:
var shared = angular.module("shared");
shared.service("Common", [
function () {
// [Unwrap] will return a value to the scope which is automatially updated. For example,
// you can pass the second argument an ng-resource call or promise, and when the result comes back
// it will update the first argument. You can also pass a function that returns an ng-resource or
// promise and it will extend the first argument to contain a new "load()" method which can make the
// call again. The first argument should either be an object (like {}) or an array (like []) based on
// the expected return value of the promise.
// Usage: $scope.reminders = Common.unwrap([], Reminders.query().$promise);
// Usage: $scope.reminders = Common.unwrap([], Reminders.query());
// Usage: $scope.reminders = Common.unwrap([], function() { return Reminders.query(); });
// Usage: $scope.reminders.load();
this.unwrap = function(result, func) {
if (!result || !func) return result;
var then = function(promise) {
//see if they sent a resource
if ('$promise' in promise) {
promise.$promise.then(update);
}
//see if they sent a promise directly
else if ('then' in promise) {
promise.then(update);
}
};
var update = function(data) {
if ($.isArray(result)) {
//clear result list
result.length = 0;
//populate result list with data
$.each(data, function(i, item) {
result.push(item);
});
} else {
//clear result object
for (var prop in result) {
if (prop !== 'load') delete result[prop];
}
//deep populate result object from data
$.extend(true, result, data);
}
};
//see if they sent a function that returns a promise, or a promise itself
if ($.isFunction(func)) {
// create load event for reuse
result.load = function() {
then(func());
};
result.load();
} else {
then(func);
}
return result;
};
}
]);
This basically works how the old promises did and auto-resolves. However, if the second argument is a function it has the added benefit of adding a ".load()" method which can reload the value into the scope.
angular.module('site').controller("homeController", function(Common) {
$scope.reminders = Common.unwrap([], Reminders.query().$promise);
$scope.reminders = Common.unwrap([], Reminders.query());
$scope.reminders = Common.unwrap([], function() { return Reminders.query(); });
function refresh() {
$scope.reminders.load();
}
});
These were some good answers, and helped me find my issue when I upgraded angular and my auto-unwrapping of promises stopped working.
At the risk of being redundant with Peter Kriens, I have found this pattern to work for me (this is a simple example of simply putting a number of famous people's quotes onto a page).
My Controller:
angular.module('myModuleName').controller('welcomeController',
function ($scope, myDataServiceUsingResourceOrHttp) {
myDataServiceUsingResourceOrHttp.getQuotes(3).then(function (quotes) { $scope.quotes = quotes; });
}
);
My Page:
...
<div class="main-content" ng-controller="welcomeController">
...
<div class="widget-main">
<div class="row" ng-repeat="quote in quotes">
<div class="col-xs-12">
<blockquote class="pull-right">
<p>{{quote.text}}</p>
<small>{{quote.source}}</small>
</blockquote>
</div>
</div>
</div>
...

Resources