I have a function inside a controller and I'm confused as to how to update a $scope variable from inside a .then function.
Heres my function:
searchSpotify(query, $scope) {
this.Spotify.search(query,'track').then(function (data) {
console.log(data.tracks.items); // this is working
$scope.result = data;
});
}
In the console log I receive this error:
TypeError: Cannot set property 'result' of undefined
Do I use $scope.$apply somehow?
EDIT:
Here's the entire controller for context
(function() {
class SelectionController {
constructor(User, groupService, selectionService, songService, $scope, $routeParams, Spotify) {
this.groupId = $routeParams.groupId;
this.selectionId = $routeParams.selectionId;
this.groupService = groupService;
this.selectionService = selectionService
//this.selections = this.selectionService.getSelections();
this.songService = songService;
this.searchResults;
this.$scope = $scope;
this.Spotify = Spotify
}
searchSpotify(query) {
this.Spotify.search(query, 'track').then(function(data) {
console.log(data.tracks.items);
$scope.result = data;
});
}
addSong(name, artist, album) {
alert('called from selection controller');
this.songService.addSong(this.selectionId, name, artist, album);
}
createSelection(name, description) {
this.selectionService.createSelection(this.groupId, name, description);
}
deleteSelection(selection) {
this.selectionService.deleteSelection(selection);
}
}
angular.module('songSelectionApp')
.controller('SelectionController', SelectionController);
})();
Save reference to $scope:
searchSpotify(query) {
var $scope = this.$scope;
this.Spotify.search(query, 'track').then(function(data) {
console.log(data.tracks.items);
$scope.result = data;
});
}
Or use arrow functions:
searchSpotify(query) {
this.Spotify.search(query, 'track').then((data) => {
console.log(data.tracks.items);
this.$scope.result = data;
});
}
.bind(this) should also work, as another answers suggest.
Regarding $scope.$apply: you need it only when you want to change a $scope outside of a $digest, for example when you use external (non-angular) libraries/functions, like WebSocket, or jquery event listeners. Untill Spotify is an angular service/factory - you don't need $scope.$apply
From docs:
$apply() is used to execute an expression in angular from outside of the angular framework. (For example from browser DOM events, setTimeout, XHR or third party libraries).
this.Spotify.search(query,'track').then(function (data) {
console.log(data.tracks.items); // this is working
this.result = data;
}.bind($scope));
Should be all you need really. You are basically telling the internal scope what this should really be
You should use
this.Spotify.search(query,'track').then(function (data) {
console.log(data.tracks.items); // this is working
this.$scope.result = data;
}.bind(this));
Related
This is a relatively simple piece of code that calls a service and returns some data. I need to set the $scope with the result of the data. Is there an easy way to set this data to the scope without resorting to to binding the scope to the function in the then clause?
Angular Code
(function () {
var app = angular.module('reports', []);
var reportService = function($http, $q) {
var service = {};
service.getMenuData = function() {
var deffered = $q.defer();
$http.get('/Report/MenuData').success(function(data) {
deffered.resolve(data);
}).error(function(data) {
deferred.reject("Error getting data");
});
return deffered.promise;
}
return service;
};
reportService.$inject = ['$http', '$q'];
app.factory('reportService', reportService);
var reportMenuController =
function ($scope, $http, reportService) {
$scope.getMenuData = function(e) {
reportService.getMenuData().then(function(data) {
// Need to set the $scope in here
// However, the '$scope' is out of scope
});
}
};
reportMenuController.$inject = ['$scope', '$http', 'reportService'];
app.controller('ReportMenuController', reportMenuController);
})();
Markup
<div>
<div ng-controller="ReportMenuController">
<button ng-click="getMenuData()">Load Data</button>
</div>
</div>
There is absolutely no problem to set the $scope from within the function passed to then(). The variable is available from the enclosing scope and you can set your menu data to one of its fields.
By the way: You should consider to use then() instead of success() for your http request. The code looks much nicer because then() returns a promise:
service.getMenuData = function() {
return $http.get('/Report/MenuData').then(function(response) {
return response.data;
}, function(response) {
deferred.reject("Error getting data");
});
}
success() is deprecated by now.
I didn't notice the small detail missing in the plunker where my code was different.
(function () {
...
var reportMenuController =
function ($scope, $http, reportService) {
$scope.getMenuData = getMenuData;
function getMenuData(e) {
reportService.getMenuData().then(function(data) {
// Now I have access to $scope
});
}
};
...
})();
Notice the changes to the two lines as below:
$scope.getMenuData = getMenuData;
function getMenuData(e) {
This also begs a small question which is, "Why is it okay to set getMenuData to the $scope before it is declared?
So. I have simple controller and service:
angular
.module('field', [])
.controller('FieldController', function(FieldService) {
var vm = this;
vm.name = FieldService.getName();
})
.service('FieldService', function() {
var name = 'John'
this.getName = function() {
return name;
};
this.setName = function(newName) {
name = newName;
};
})
;
Then i have some $interval in anotherService, that getting data every 1 second and calling FieldService.setName:
.service('UpdateService', function($http, FieldService) {
$interval(function() {
$http.get('/somUrl')
.then(function(response) {
FieldService.setName(response.name);
});
});
})
But it won't change my HTML.
If i switch from primitive to object in returning value getName, then it's working.
Is there another approach? I personally think, that this structure i created is bad, but can't understand how it should be done.
JavaScript is always pass-by-value, but when your variable is an object, the 'value' is actually a reference to the object. So in your case, you are getting a reference to the object, not the value. So when the object changes, that change isn't propagated like a primitive would be.
Your code seems a bit incorrect, too. You are setting the value of response.name to FieldService.setName, which is actually a function.
If you want to use the getter/setter approach you have listed, then you could use events to let the controller know that name has changed.
.service('UpdateService', function($http, FieldService, $rootScope) {
$interval(function() {
$http.get('/somUrl')
.then(function(response) {
FieldService.setName(response.name);
$rootScope.$broadcast('nameChanged', {
name : response.name
});
});
});
})
.controller('FieldController', function(FieldService, $scope) {
var vm = this;
vm.name = FieldService.getName();
$scope.$on('nameChanged', function (evt, params) {
vm.name = params.name;
});
})
Another way to accomplish this is to use a $scope.$watch on the service variable in the controller:
.controller('FieldController', function($scope, FieldService) {
$scope.name = FieldService.getName();
$scope.$watch(function () {
return FieldService.getName();
}, function (newVal, oldVal) {
if (newVal !== oldVal) {
$scope.name = newVal;
}
});
})
I would move my $interval function inside a controller and then just update a $scope attribute every second. Then Angular will take care of the rendering.. Or you must also use an $interval function in your controller which gets the service content (ie FieldService.getName) every second.
I would use it this way:
angular
.module('field', [])
.controller('FieldController', function($scope, FieldService) {
$scope.name = function(){
FieldService.getName();
};
})
.service('FieldService', function() {
var name = 'John'
this.getName = function() {
return name;
};
this.setName = function(newName) {
name = newName;
};
});
Use name() in your html to see the update value.And your other service:
.service('UpdateService', function($http, FieldService) {
$interval(function() {
$http.get('/somUrl')
.then(function(response) {
FieldService.setName(response.name);
});
}, 1000);
})
There are numerous ways in which you can achieve this. No way is the best way. Depends on person to person.
Hope this helps.
There are several ways to solve that problem.
1) Move the $interval to controller.Then you will have a variable, which holds that data and you can bind it in view
2) You can use AngularJs Events.$rootScope will help you to send signal and catch it wherever you want.
If you want more info about this solutions, you can see it here:
http://www.w3docs.com/snippets/angularjs/bind-value-between-service-and-controller-directive.html
This is my controller:
function TestCtrl ($scope, sharedProperties, $resource){
var userDataProfile = sharedProperties.getUserDataProfile();
var userCreatedBoard = $resource('/boards?owner=:owner');
userCreatedBoard.query({'owner':userDataProfile.id}, function (result) {
console.log(result);
});
}
Now problem is sharedProperties.setUserDataProfile() is called after calling a 3rd party service and its async. Hence, when the TestCtrl is bound, userDataProfile is effectively null. How do I handle this situation so that after the sharedProperties.setUserDataProfile() is called and variable has been assigned a value from 3rd party service, then only my controller should get bound?
I think you want to look at resolve:
http://www.youtube.com/watch?v=Kr1qZ8Ik9G8
This allows you to load all your data before instantiating your controller and firing a routeChangeSuccess event.
In the angular docs.
It does not make sense to pass variables into an Angular.js controller like that, that is what the Scope variable is meant for.
Try this
window.activeTestCtrls = [];
window.sharedProperties = [];
function TestCtrl ($scope, $resource){
if (window.sharedProperties) this.createUserData(window.sharedProperties)
window.activeTestCtrls.push[this];
function createUserData(sharedProperties) {
var userDataProfile = sharedProperties.getUserDataProfile();
var userCreatedBoard = $resource('/boards?owner=:owner');
userCreatedBoard.query({'owner':userDataProfile.id}, function (result) {
console.log(result);
}
});
// and when you get your sharedproperties
function gotSharedProperties(sharedProperties) {
$.each(activeTestControllers, function(index,value) {
activeTestControllers[index].createUserData(sharedProperties)})
}
}
I have a provider:
AdviceList.provider('$adviceList',function(){
this.$get = function ($rootScope,$document,$compile,$http,$purr){
function AdviceList(){
$http.post('../sys/core/fetchTreatments.php').success(function(data,status){
this.treatments = data;
console.log(this.treatments); // the correct object
});
this.adviceCategories = [
// available in the controller
];
}
return{
AdviceList: function(){
return new AdviceList();
}
}
}
});
Further, i have this controller:
AdviceList.controller('AdviceListCtrl',function($scope,$adviceList){
var adv = $adviceList.AdviceList();
$scope.treatments = adv.treatments; // undefined
});
Why is it, that the controller's $scope.treatments stays undefined, this.treatments inside the provider however, is filled correctly? Also, adviceCategories is available in my controller.
The call you get teatment is async in nature so the results may not have been populated when you try to assign them.
So here
var adv = $adviceList.AdviceList();
$scope.treatments = adv.treatments; //The treatments would only get filled after the server call is over.
You need to rewrite the code in a way that you assign it to your scope property on the success callback.
I will recommend you to simplify your code
1) Use simple factory method of angular instead of provider
2) return a promise to avoid using callbacks
AdviceList.service('adviceList', function ($http) {
return {
adviceList: function () {
return $http.post('../sys/core/fetchTreatments.php');
}
}
});
AdviceList.controller('AdviceListCtrl', function ($scope, $adviceList) {
adviceList.AdviceList().then(function (data) {
$scope.treatments = data //set value to data when data is recieved from server
});
});
I'm trying to understand how AngularJS sees an object from a deeply nested JSON. Here's an example plunker. The data comes from service and is assigned to $scope.data. The javascript code seems to want me to declare every level of the object first before usage, but referencing a deep level within object from the view HTML always works, and using the deep level in a function kinda works. It's rather inconsistent.
I'm not sure if my understanding of $scope is lacking, or if this has something to do with promise objects. Advise please?
HTML
<body ng-controller="MainCtrl">
Referencing nested obj in view works:
{{data.level1.level2}}
<br>
Using nested obj within declared scope var doesn't work:
{{nestedObj}}
<br>
Using nested obj in a function works but throws TypeError:
{{getLen()}}
</body>
Javascript
var app = angular.module('app', []);
app.factory('JsonSvc', function ($http) {
return {read: function(jsonURL, scope) {
$http.get(jsonURL).success(function (data, status) {
scope.data = data;
});
}};
});
app.controller('MainCtrl', function($scope, JsonSvc) {
JsonSvc.read('data.json', $scope);
// Using nested obj within declared scope var doesn't work
// Uncomment below to break whole app
// $scope.nestedObj = $scope.data.level1.level2;
// Using nested obj in a function works but throws TypeError
// Declaring $scope.data.level1.level2 = [] first helps here
$scope.getLen = function () {return $scope.data.level1.level2.length};
});
JSON
{
"level1": {
"level2": [
"a",
"b",
"c"
]
}
}
Your $http request is asynchronous.
app.controller('MainCtrl', function($scope, JsonSvc) {
JsonSvc.read('data.json', $scope);
//$scope.data.level1.level2 doesn't exist yet at this point in time
//and throws an exception
$scope.nestedObj = $scope.data.level1.level2;
//$scope.data.level1.level2 doesn't exist yet at this point in time
//and throws an exception
//once Angular does dirty checking this one will work since the
//$http request finished.
$scope.getLen = function () {
return $scope.data.level1.level2.length
};
});
Since you have three scope objects that rely on that data it would be best to assign those in the call back.
app.factory('JsonSvc', function ($http) {
return {read: function(jsonURL, scope) {
$http.get(jsonURL).success(function (data, status) {
scope.data = data;
scope.nestedObj = scope.data.level1.level2;
scope.getLen = function () {
return scope.data.level1.level2.length;
};
});
}};
});
If you do not want to set it all up on the call back, you could also use $broadcast() and $on()
app.factory('JsonSvc', function ($http, $rootScope) {
return {
read: function (jsonURL, scope) {
$http.get(jsonURL).success(function (data, status) {
scope.data = data;
$rootScope.$broadcast("jsonDone");
});
}
};
});
app.controller('MainCtrl', function ($scope, JsonSvc) {
JsonSvc.read('data.json', $scope);
$scope.name = "world";
$scope.$on("jsonDone", function () {
$scope.nestedObj = $scope.data.level1.level2;
$scope.getLen = function () {
return $scope.data.level1.level2.length;
};
});
});
Ray, another option is to return the $http.get call since its a promise and use the .then() function to declare $scope.nestedObj or anything else you want to do with data once it returns.
Here's my example: http://plnkr.co/edit/GbTfJ9
You can read more about promises in Angular here: http://docs.angularjs.org/api/ng.$q