I am writing some basic unit tests for my AngularJS app. I have some bindings on the UI with a scope variable inside my directive whichis populated on the completion of a promise.
HTML:
<div id="parent">
<div id="child" ng-repeat="l in aud">
// Other Stuff
</div>
</div>
Directive:
link: function(scope){
service.getArray().$promise.then(function(data){
scope.aud = data;
}
Test:
describe('my module', function () {
var $compile: ICompileService, $rootScope: IScope, directive: JQuery<HTMLElement>;
// Load the myApp module, which contains the directive
beforeEach(angular.mock.module('my-module'));
beforeEach(angular.mock.module(($provide) => {
$provide.service('service', () => {
return {
getArray: () => {
return Promise.resolve(
["item1", "item2"]
);
}
}
});
// Store references to $rootScope and $compile
// so they are available to all tests in this describe block
beforeEach(inject(($httpBackend: IHttpBackendService, _$compile_: ICompileService, _$rootScope_: IRootScopeService) => {
$compile = _$compile_;
$rootScope = _$rootScope_.$new();
directive = $compile('<my-directive></my-directive>')($rootScope)
$rootScope.$apply();
}));
describe('account-utility directive', function () {
it('account utility directive details panel is shown on click', function () {
let list = directive.find("parent"); // Finds this
let listItems = list.find("child"); // Cannot find this. Throws error.
console.log(list); // innerHTML still shows ngrepeat unsubstituted by divs
expect(listItems.length).toBe(2);
});
});
});
I debugged the whole thing and the promise is resolved and the data is assigned to the scope variable 'aud'. However seems like my copy of scope for the test and the app are different. Whats going on here?
beforeEach((done) => {
directive = $compile('<my-directive></my-directive>')($rootScope);
$rootScope.$digest();
setTimeout(() => {
$rootScope.$digest();
done();
});
});
Done helps you wait till all asynchronous tasks are picked up from the stack.
apply()
works too
When the Angular's promise is resolved you need to notify it so it will run its dirty check.
In order to do that you need to invoke $rootScope.apply() inside yours it clause.
Think of it like that, your $rootScope.apply() call in the before each clause invokes your directive's link function, the that function registers a Promise resolvment in Angulars queue, but it not got processed.
Related
For Angular 1, I have an AJAX call that is made in the controller (as it is dependent on another set of data that is returned during the route resolve) and once the response is back, the data is passed down to directives. Since the data doesn't come back until after the directives are compiled, the directives initially gets an undefined for that data passed from the controller and would only get the data if I am watching that scope value inside each of the directive. Is there a better way where I don't have to use $scope.$watch or any event listeners, such as $broadcast/$on? I don't want to exhaust the digest cycle with too much watchers.
Here's a mock structure:
<parent>
<directive1 data="manipulateDataReturnedFromAJAXCall"></directive1>
</parent>
//template for directive1
<div>
<directive2 ng-if="data" attr1="data.field1"></directive2>
<div>
What about using a callback in your http service.
//Controller
MyHTTPService.getData(function(data){
$scope.manipulateDataReturnedFromAJAXCall = data;
})
//MyHTTPService service
return{
getData : function(fct){
$http.get("url/to/data").then(function(response){
fct(response.data);
})
}
}
I am trying to show one button as in this Plunker
<div ng-show="showbtn"><button class="fix btn btn-success" ng-click="top()">To the top</button></div>
On scroll event, I have made $rootScope.$emit call and it is getting triggered too, but not sure why the $scope value is not getting changed inside the mainCtrl controller $scope. Is $scope inside $rootScope is different ?
The event handler (the function passed to $rootScope.$on) runs outside of Angular's normal digest cycle so you need to tell the parent scope that something has changed. You can use $apply to do so:
$rootScope.$on('scrolled',function(event,data){
$scope.$apply(function () {
$scope.showbtn = data.message;
});
});
Here's an updated Plunker.
I am trying to update a span's text after a call back function in Angular.
Here is my HTML:
<div ng-controller="onDragController">
<div id="draggableArea">
<div id="rectangle1" data-drag="true" jqyoui-draggable="{onDrag: 'dragCallback'}" data-jqyoui-options="{containment: '#draggableArea'}"></div>
</div>
<div>
<span ng-model="rectangleOne">{{rectangleOne.leftOffset}}</span>
</div>
</div>
And my controller is:
var App = angular.module('drag-and-drop', ['ngDragDrop', 'ui.bootstrap']);
App.controller('onDragController', function($scope) {
$scope.rectangleOne = {};
$scope.rectangleOne.leftOffset = 'ASDF';
$scope.dragCallback = function (event, ui) {
$scope.rectangleOne = {leftOffset: '12345'};
};
});
If I toss an alert in my callback function then I am seeing that the leftOffSet is updated, but on my HTML page the {{rectangleOne.leftOffset}} is staying the same.
What am I missing here...?
Use $apply in your dragCallback as follows:
$scope.dragCallback = function(event, ui) {
$scope.$apply(function() {
$scope.rectangleOne = {
leftOffset: '12345'
};
});
};
This will update your leftOffset in the scope. Here is a Plunker.
It might be helpful to better understand how Angular does two-way data binding. This blog post on how apply works, and why one would be motivated to use it, is pretty good.
In summary, for your changes to $scope.rectangleOne:
You can call $scope.$digest() every time you make a simple change like this. You need to do this so Angular knows to check if your bound data got updated.
Alternatively, you can use $scope.$apply, make the changes to $scope.rectangleOne inside a callback to $apply. It's doing the same thing, $apply ends up calling $digest() indirectly.
In code:
$scope.$apply(function() {
$scope.rectangleOne.leftOffset = '12345';
});
Hope that helps solve your problem, and add a bit of understanding to what's happening behind the scenes!
http://plnkr.co/edit/UfQJU661pQR0DMY3c61t?p=preview
I got above code from AngularJs site and only thing I have added a button to delete a Div where we have controller but after delete no destroy method called as I have put alert in Directive and Controller.
element.on('$destroy', function() {
alert('destroy directive interval');
$interval.cancel(stopTime);
});
and
$scope.$on('$destroy', function() {
alert('destroy controller interval');
// Make sure that the interval is destroyed too
$scope.stopFight();
});
please suggest.
Thanks
The main thing to be noticed
When element.remove() is executed that element and all of its children will be removed from the DOM together will all event handlers attached via for example element.on.
It will not destroy the $scope associated with the element.
So you need to manually trigger scope.$destroy();
First get the scope of element:-
var scope = angular.element(document.getElementById("mainDiv")).scope();
Second remove the element from dom:-
$('#mainDiv').remove();
Third destroy scope manually:-
scope.$destroy();
Plunker
You're doing it outside of angular's context.
<button id="btn" onclick="DeleteMainDiv()">DeleteDiv</button>
So in your DeleteMainDiv() function
function DeleteMainDiv() {
alert('Controller div going to remove');
//debugger;
var scope = angular.element(document.getElementById("mainDiv")).scope();
$('#mainDiv').remove();
scope.$destroy();
}
This will trigger the destroy functionality.
But I don't see a need of it. Angular will automatically run the $destroy event handler when the route changes or directive no longer required.
DEMO
Angular partial - HTML.
BaseCtrl
<div ng-controller="SelectTagCtrl">
<input type="text" ng-init="setTags(viewData['users'])" ui-select2="tagAllOptions" ng-model="tagsSelection" name="users" />
{{viewData['users']}} ECHOES CORRECTLY.
But undefined when passed inside ng-init callback.
</div>
<input type="text" class="span12" placeholder="Brief Description" name="description" value="{{viewData['description']}}">
ECHOES CORRECTLY.
Controller.js
function SelectTagCtrl(){
$scope.setTags = function(data){
// data is undfined when viewData['users'] is used. <-- PROBLEM
// correct when I pass some static string.
}
}
//POPULATING viewData to be used inside view partial.
function BaseCtrl(){
$http.get(url).success(function(data){
$scope.viewData = data.data || [];
$scope.view_type = $scope.viewData['view_type'];
$scope.fields = data.data.fields;
console.log($scope);
}).error();
}
Using timeout would be a workaround, instead I would take a $scope variable inside the controller to know if the ajax call has completed.
The problem is ng-init might get called before ajax completion.
I already had ui-if directive configured in my angular project, so I used it with the combination of $scope variable to get the things working.
<div ng-controller="SelectTagCtrl" ui-if="ajax_done">
<input type="text" ng-init="setTags(viewData['users'])" ui-select2="tagAllOptions" ng-model="tagsSelection" name="users" />
</div>
And inside controller,
$http.get(gurl + '.json').success(function(data,status){
// some stuff
$scope.ajax_done = true;
}).error();
Because of angular's magical two-way binding, the element will get updated. Now it sees that ajax request is completed, ui-if will get a truthy value and ng-init directive of the element will get a chance to execute its callback.
EDIT: ui-if was removed from Angular UI in favour of ng-if which is now built in.
Here are two different changes to your fiddle that appear to work.
Fiddle 1 - this version uses $scope.$apply(exp) as described in the documentation here and is useful when you are modifying angular bound data outside of the angular framework. In this example setTimeout is the culprit.
setTimeout(function(){
console.log("updateVal" );
$scope.$apply(function() {
$scope.updateVal2();
});
console.log($scope.tagsSelection);
},5000);
Fiddle 2 - this version uses angular's wrapper for setTimeout called the $timeout service.
$timeout(function(){
console.log("updateVal" );
$scope.updateVal2();
console.log($scope.tagsSelection);
},5000);