Using deeply nested object from JSON in AngularJS - strange behavior - javascript

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

Related

Unit Testing a Controller with specific syntax

Whenever, I am testing a controller and have something like this in it.
$scope.isSomething = function (Item) {
return ItemCollection.someItem(Item.attachedItem);
};
giving error on karma console:
TypeError: undefined is not an object (evaluating 'Item.attachedItem')
I am simply calling the function from the test file like this:
scope.isSomething();
I need to mock the Item.attachedItem or I am missing something here.. Please Explain in details as this is happening in multiple files.. thanks in advance
Also, for this type of code
.controller('itemCtrl', function (itemCollection) {
var vm = this;
this.itemCollection= itemCollection;
itemCollection.someItem().then(function (Item) {
vm.pageUrl = Item.pageUrl;
vm.Item= Item.someItems;
});
});
Also, this is also part of the code for more broad view here it gives Item.pageUrl is not a object error
Refer angular unit testing docs
The ItemCollection being a service, you could inject a mock while initialising a controller using
var ItemCollection, ItemCrtl;
beforeEach(inject(function($controller, $rootScope) {
$scope = $rootScope.$new();
ItemCollection = jasmine.createSpyObj('ItemCollection', ['someItem']);
ItemCrtl = $controller('ItemCtrl', {
$scope: scope,
ItemCollection: ItemCollection
});
});
For Item, the method isSomething should take care of checking if Item is undefined before doing Item.attachedItem
Testing an aync block is tricky. someItem returns a promise. $q an angular service to which can be used create async functions while testing.
We need to resolve the deferred object to test the async task.
var ItemCollection, ItemCrtl, deferedObj;
beforeEach(inject(function($controller, $rootScope, $q) {
$scope = $rootScope.$new();
deferedObj = $q.defer();
ItemCollection = jasmine.createSpyObj('ItemCollection', ['someItem']);
ItemCollection.someItem.andReturn(deferedObj.promise);
ItemCtrl = $controller('ItemCtrl', {
$scope: scope,
ItemCollection: ItemCollection
});
});
it('sets page url', function() {
deferedObj.resolve({ pageUrl: 'http://url', someItems: [1,2,3] });
scope.$apply();
expect(ItemCtrl.pageUrl).toEqual('http://url');
});
you have to use mock Item data in test like this (assuming attachedItem value is boolean)
var item={attachedItem:true}
scope.isSomething(item)
$scope.isSomething = function (Item) {
if(!Item.attachedItem){
Item.attachedItem=YOUR_MOCK_VALUE;
}
return ItemCollection.someItem(Item.attachedItem);
};

How do I add result to my scope ng-click?

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?

How to set a variable in different controller in AngularJS?

I'd like to do simple notifications in angular. Here is the code I've written.
http://pastebin.com/zYZtntu8
The question is:
Why if I add a new alert in hasAlerts() method it works, but if I add a new alert in NoteController it doesn't. I've tried something with $scope.$watch but it also doesn't work or I've done something wrong.
How can I do that?
Check out this plnkr I made a while back
http://plnkr.co/edit/ABQsAxz1bNi34ehmPRsF?p=preview
I show a couple of ways controllers can use data from services, in particular the first two show how to do it without a watch which is generally a more efficient way to go:
// Code goes here
angular.module("myApp", []).service("MyService", function($q) {
var serviceDef = {};
//It's important that you use an object or an array here a string or other
//primitive type can't be updated with angular.copy and changes to those
//primitives can't be watched.
serviceDef.someServiceData = {
label: 'aValue'
};
serviceDef.doSomething = function() {
var deferred = $q.defer();
angular.copy({
label: 'an updated value'
}, serviceDef.someServiceData);
deferred.resolve(serviceDef.someServiceData);
return deferred.promise;
}
return serviceDef;
}).controller("MyCtrl", function($scope, MyService) {
//Using a data object from the service that has it's properties updated async
$scope.sharedData = MyService.someServiceData;
}).controller("MyCtrl2", function($scope, MyService) {
//Same as above just has a function to modify the value as well
$scope.sharedData = MyService.someServiceData;
$scope.updateValue = function() {
MyService.doSomething();
}
}).controller("MyCtrl3", function($scope, MyService) {
//Shows using a watch to see if the service data has changed during a digest
//if so updates the local scope
$scope.$watch(function(){ return MyService.someServiceData }, function(newVal){
$scope.sharedData = newVal;
})
$scope.updateValue = function() {
MyService.doSomething();
}
}).controller("MyCtrl4", function($scope, MyService) {
//This option relies on the promise returned from the service to update the local
//scope, also since the properties of the object are being updated not the object
//itself this still stays "in sync" with the other controllers and service since
//really they are all referring to the same object.
MyService.doSomething().then(function(newVal) {
$scope.sharedData = newVal;
});
});
The notable thing here I guess is that I use angular.copy to re-use the same object that's created in the service instead of assigning a new object or array to that property. Since it's the same object if you reference that object from your controllers and use it in any data-binding situation (watches or {{}} interpolation in the view) will see the changes to the object.

AngularJS function and $scope

I have function defined in my controller like this:
var getTransactions = function(transactionType, partnerId) {
return loadTexts.getTransactionTexts(transactionType, partnerId).then(function (res) {
$scope.loadedTexts = res.data.Transactions;
});
};
Another $scope function is calling it. Now, when I put console.log for res.data.Transactions, everything is logged as it has to be, but when I want to assign that data to $scope variable, it simply doesn't work. (After load function, $scope.loadedTexts is empty however)
$scope.load = function() {
getTransactions($scope.TransactionTypeId, $scope.selectedPartner.Id);
};
Any help?
CODE:
app.controller("textsController", function ($scope, $http, autoComplete, loadTexts) {
$scope.selectedPartner = { "Name": "", "Id": null };
$scope.loadedTexts = [];
.. other functions are here ...
var getTransactions = function(transactionType, partnerId) {
return loadTexts.getTransactionTexts(transactionType, partnerId);
};
$scope.load = function () {
getTransactions($scope.transactionType, $scope.selectedPartner.Id).then(function(res) {
$scope.loadedTexts = res.data.Transactions;
});
};
});
In the code you are showing everything looks fine to me. Note however, that you are storing a reference to Transactions within loadedTexts. Maybe the "Transactions" object res.Data.Transactions is somehow cleared elsewhere.
You could try to copy the object instead of assigning a reference (use e.g. angular.copy) to test if this is the cause.

Unset object property

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
});
});

Categories

Resources