Angular.js UNIT test directive - javascript

I'm trying to make a unit test for angular directive. So this is the code:
describe('Test', function () {
var element,
scope;
// load the service's module
beforeEach(module('app'));
beforeEach(inject(function ($compile, $rootScope) {
scope = $rootScope.$new();
element = angular.element('<div id="wow"></div>');
$compile(element)(scope);
element.scope().$apply();
}));
it('should', function () {
console.log(document.getElementById('wow'));
});
});
I got null in the console.
Of course this is just a test case. In real code getElementById is used by plugin which is wrapped with my directive, which is I'm trying to test.
So my question is: why the DOM element which is just compiled not be found by getElementById. If this is correct behavior, how can I avoid this mistake.
Thanks

You need to append the element in the document somewhere. Here's working plnk.
Only thing thing that I added:
angular.element(document.body).append(element);

The DOM element isn't found because jasmine never actually attaches it to the DOM. If you want to get the element, just refer to it as element. For example:
it('should', function() {
expect(element.attr('id')).toBe('wow');
});
You can attach the element to the DOM manually, but remember to remove it when you're done with your test (I use the afterEach blocks for this). Jasmine uses the same DOM for your entire test suite, so if you forget to remove an element, you could get false positives or other unexpected results in your tests from elements being in the DOM that you didn't expect to be there. In general, you shouldn't have to attach your element to the DOM unless you're testing certain interactions (blur works without attaching to the DOM, but focus doesn't, for example).

Related

Creating 2-way binding for elements created after initial compiliation

I created a directive that dynamically creates a form based on a json from the server. I'm trying to add ng-model attribute to the various input elements so that I'll be able to use the input values after the user has typed them in and clicked submit. The ng-model attribute seems to be added but 2-way databinding doesn't work.
EDIT: I'm calling buildForm from within the link function as seen below:
function link(scope, elem, attr, ctrl) {
//asyc request to the server, data here is a json object from the server
getMovieDataStructure({
onSuccess: (data) => {
scope.mdb = data;
buildForm(scope.mdb, elem);
},
onFail: (res) => {
console.log("ERROR getting it");
}
});
}
Here is some of the code from in the directive:
//mdb is an array of objects describing the form requirments
function buildForm(mdb, formElement) {
for(var i=0; i < mdb.length; i++) {
if(mdb[i].type == 'string') {
if(mdb[i].maxLength && mdb[i].maxLength > 1024) {
//if maxLength > 1024 put a text area instead
formElement.append(createTextArea({
id: mdb[i].fieldName,
placeholder: mdb[i].fieldName
}));
} else {
//add input field to the form
formElement.append(createTextInput({
id: mdb[i].fieldName,
placeholder: mdb[i].fieldName
}));
}
} else if(){
//some more cases
}
formElement.append("<br>");
}
//...some more code...
}
//one of the functions to create an input element
function createTextInput(data) {
var elem = angular.element("<input>");
elem.attr("type", "text");
elem.attr("id", data.id);
elem.attr("ng-model", data.id);
elem.attr("placeholder", data.placeholder);
return elem;
}
For example, a result of an input element on the html page could look like this:
<input placeholder="movie_name" ng-model="movie_name" id="movie_name" type="text"> </input>
And if I'll put the same tag directly to in the html file the 2-way binding works great.
What am missing here? Is there a better way to do this and I'm just overcomplicating things?
Somewhere after you update the form you will need to call $compile, otherwise angular will not be aware of your changes. See:
https://docs.angularjs.org/api/ng/service/$compile
Something to try would be to call $rootScope.apply() after you call the buildform method maybe. What may be happening is that you are making all these changes to the DOM after the digest cycle completes and angular won't know about your changes until the next cycle happens.
So in your case it will be:
buildForm(scope.mdb, elem);
scope.$apply();
Thing is digest loop needs to be called explicitly in your case cause angular is unaware of the change made.
USE:
buildForm(scope.mdb, elem);
scope.$apply();
OR
But there is a better way for using $apply:
scope.$apply(buildForm(scope.mdb,elem));
The difference is that in the first version, we are updating the values outside the angular context so if that throws an error, Angular will never know.
As wdanda mentioned, since the directive adds DOM elements, it needs to be compiled afterwards to let angular be aware of the changes
Short answer is that the line buildForm(scope.mdb, elem); has been changed to $compile(buildForm(scope.mdb, elem).contents())(scope); and '$compile' was added to the directive's list of dependencies.
Long explanation:
buildForm(scope.mdb,elem) returns the element of the directive (so actually adding $compile(elem.contents())(scope); after buildForm would be equivilant), .contents() on an angular wraped element returns all of that element children.
That means that $compile(buildForm(scope.mdb, elem).contents()) tells angular to compile all the children of the directive's element, after buildForm has added some elements to it (and which some of them have directives of their own.
The call for .contents() is important because:
we only compile .childNodes so that we don't get into infinite loop compiling ourselves
(from https://docs.angularjs.org/api/ng/service/$compile)
The $compile() function returns a linking function that needs to be called with a scope to link to. So adding (scope) at the end will call that returned function.
A more clear (though slightly less elegant) way to write that code, would be:
var element = buildForm(scope.mdb, elem); //buildForm returns an angular wraped element
var linking = $compile(element); // $compile returns a linking function
linking(scope); //linking is functions that takes a scope object
//and needs to be run after compilation

Angular: How to spy on $element.on

I have a directive which does something like this in its link function
angular.module('myApp')
.directive('barFoo', function() {
return {
restrict: 'E',
link: function (scope, element) {
element.on('click', ....);
}
};
});
Now I would like to verify in my unit test that it calls on correctly
element = angular.element('<bar-foo></bar-foo>');
$compile(element)(scope);
spyOn(element, 'on');
...
expect(element.on).toHaveBeenCalled();
It tells me the spy is not called. From what I've found on the web, angular/jquery creates a new wrapper around the DOM element every time. Meaning the the element inside my directive is not the same as the element in my spec file. Most likely (not verified) the element[0] probably are the same. I've also tried to spy on angular.element
var mockEl = { on: angular.noop };
spyOn(angular, 'element').andReturn(mockEl);
spyOn(mockEl, 'on');
but that seems to break more than it fixes (I also need functions like isloateScope for example).
Anyway, is there some easy way I can spy on the on function of the element used inside a directive?
Link function can be tested separately
element = angular.element('<bar-foo');
spyOn(element, 'on');
BarFooDirective[0].link(scope, element);
expect(element.on).toHaveBeenCalled();
if it is simple enough (stays away from attrs, required controllers, dependencies), otherwise the spec will cause more problems than it can solve.
Otherwise, it can be tested like it is done by the framework:
element = angular.element('<bar-foo');
expect(angular.element._data(element[0]).events.click).toBeDefined();
For real-world directive which may have more than one click listener defined in either itself or child directives it is not enough to make sure that listeners exist. Generally you may want to encapsulate internal functions, but anonymous click handler can also be exposed to scope for the purpose of testing:
element = angular.element('<bar-foo');
expect(angular.element._data(element[0]).events.click).toContain(scope.onClick);
Well, it's not necessary to test on method. You can test event bindings with the help of triggerHandler()
link: function (scope, element) {
element.on('click', 'something to heppen');
}
Test:
element = angular.element('<bar-foo></bar-foo>');
$compile(element)(scope);
$(element[0]).triggerHandler('click');
expect('something binded to click event to happen');
If you've compiled your element in in a beforeEach like this:
myEl = '<div my-el>MY EL</div>';
scope = $rootScope.$new();
directiveElement = $compile(myEl)(scope);
scope.$digest();
Try replacing mousedown with the event you are testing like this:
expect(directiveElement.on.mousedown).toBeDefined;

AngularJS: Using $compile on html that contains directives with templateurl

I have a legacy application that has some content inserted into the DOM via jQuery. I would like the legacy parts of the codebase to be responsible for compiling the html that it inserts into the DOM.
I can get it to compile the initial html using $compile, but any DOM elements added by a directive's template or templateUrl are not compiled, unless I call $scope.$apply() from within the directive itself.
What am I doing wrong here?
Link to fiddle: http://jsfiddle.net/f3dkp291/15/
index.html
<div ng-app="app">
<debug source='html'></debug>
<div id="target"></div>
</div>
application.js
angular.module('app', []).directive('debug', function() {
return {
restrict: 'E',
template: "scope {{$id}} loaded from {{source}}",
link: function($scope, el, attrs) {
$scope.source = attrs.source
if( attrs.autoApply ) {
// this works
$scope.$apply()
}
},
scope: true
}
})
// mimic an xhr request
setTimeout(function() {
var html = "<div><debug source='xhr (auto-applied)' auto-apply='1'></debug><br /><debug source='xhr'></debug></div>",
target = document.getElementById('target'),
$injector = angular.injector(['ng','app']),
$compile = $injector.get('$compile'),
$rootScope = $injector.get('$rootScope'),
$scope = angular.element(target).scope();
target.innerHTML = $compile(html)($scope)[0].outerHTML
// these do nothing, and I want to compile the directive's template from here.
$scope.$apply()
$scope.$root.$apply()
angular.injector(['ng','app']).get('$rootScope').$apply()
}, 0)
output
scope 003 loaded from html
scope 005 loaded from xhr (auto-applied)
scope {{$id}} loaded from {{source}}
Update: Solution works for directives with a template property, but not templateUrl
So, I should have been compiling dom nodes, not an HTML string. However, this updated fiddle shows the same failing behavior if the directive contains a templateUrl:
http://jsfiddle.net/trz80n9y/3/
As you probably realised, you need to call $scope.$apply() for it to update the {{bindings}} from the scope values.
But the reason you couldn't do it inside your async function was that you were compiling the HTML against the existing scope for #target, but then trying to append just the HTML. That won't work, because you need to have the compiled node in the DOM, either by appending the entire compiled node using jQuery's .append() or similar, or by setting the DOM innerHTML first, then compiling the node that is in the DOM. After that, you can call $apply on that scope and because the directive is compiled and in the DOM, it will be updated correctly.
In other words, change your async code as follows.
Instead of:
target.innerHTML = $compile(html)($scope)[0].outerHTML
$scope.$apply()
Change it to:
target.innerHTML = html;
$compile(target)($scope);
$scope.$digest();
Note that I did a $digest() instead of $apply(). This is because $apply() does a digest of every single scope, starting from the $rootScope. You only need to digest that one scope you linked against, so it is sufficient (and faster, for any reasonably sized app with lots of scopes) to just digest that one.
Forked fiddle
Update: Angular can compile strings and detached DOM nodes
I just checked, and the OP was actually correct in assuming that Angular can compile strings of HTML or detached DOM nodes just fine. But what you do need to do is make sure you actually append the compiled node to the DOM, not just the HTML. This is because Angular stores things like the scope and the binding information as jQuery/jQueryLite data on the DOM node*. Thus you need to append the whole node, with that extra information, so that the $digest() will work.
So an alternative way of having this work is to change the same portion of the OP's code as above to:
target.appendChild($compile(html)($scope)[0]);
$scope.$digest()
* Technically, it is stored in the internal jQuery data cache, with the cache key being stored on the DOM node itself.
Append the element to the target first, then compile it.
html = angular.element(html);
target = angular.element(target);
target.append(html);
html = $compile(html)($scope)
http://jsfiddle.net/f3dkp291/16/

Interact with compiled Angular directives HTML

I'm creating two Angular directives, fooContainer and foo, where the fooContainer will load one or more foo elements and render them inside its container.
Now I also want to attach some JavaScript events to the foo directives compiled HTML from the fooContainer directives link function since the container is supposed to be responsible for some things like dragging the compiled foo elements around.
Since $compile is asynchronous when compiling directives with templateUrl the HTML isn't available directly after calling $compile(html)(scope), however if I use a timeout for about a second the HTML is rendered in and I can interact with it.
This isn't optimal. Does $compile expose an event or promise I can use to be notified when all HTML is rendered?
Here is a Plunker that describes my problem, http://plnkr.co/edit/coYdRqCsysV4txSFZ6DI?p=preview
Approaches in order of preference:
1) Follow pattern of simmi simmi whenever you can and use angular (ng-...) approach. This is most reliable.
1.5) UPDATE: Liquinaut 'attribute directive' approach below seems valid - I've only used it in a quick demo POC and it worked fine. Assuming this survives more complex usage I would prefer over the watch/jquery option 2 below. Please note however that the $last is specifically for ng-repeat. If you are injecting a $compile chunk of non-repeating markup as per the OP then you need to drop the $last check. And to be clear this requires you to add the attribute post-render to the element you are waiting to render i.e. (per OP plunker)
var el = $compile('<foo><div class="hide" post-render>...
with directive:
.directive('postRender', function() {
return function(scope, element, attrs) {
//your code here
element.hide();
};
});
I've forked the original plunkr and generalized the approach to allow passing a generic callback: http://plnkr.co/edit/iUdbn6zAuiX7bPMwwB81?p=preview
NOTE: This approach only works for basic activities on the element compiled in. If your compile string has angular variable interpolation e.g. {{value}} and you rely on these being resolved in the callback it won't work. They are not resolved at this time. This is also true if the postRender directive is rewritten with an explicit link function. Option 2 below works fine for these cases as it pushes resolution to at least the next digest.
2) I've found watching the DOM very reliable even in very complex apps (although performance should as always be monitored). Replace your el.find('.hide').hide(); line with this block:
scope.$watch(
function() { return element.find('.hide').length > 0;},
function(newVal, oldVal) {
if (newVal) {
//DO STUFF
el.find('.hide').hide();
}
}
);
I wouldn't be comfortable using this in a tight loop but for one off usage on directive instantiation (assuming you aren't creating a 1000 of them!) it seems reasonable - I used this approach for ng/ui-slider integration etc
3) pixelbits approach also good architectural approach if you are building something more complex (and for reusable components) but beware the extra scope that gets created if you are using transclude (e.g. nested directives) it will be $$nextSibling that gets the 'emit'. See: here
BTW: if just want a quick way to do drag and drop see: http://codef0rmer.github.io/angular-dragdrop/#/
The directive fires a postRender event:
fooContainer.directive('postRender', function() {
return function(scope, element, attrs) {
if (scope.$last){
//your code here
}
};
});
Hope that helps!
http://plnkr.co/edit/f4924y6GW7rAMItqVR0L?p=preview
.directive('fooContainer', function($compile, $timeout) {
return {
link: function(scope, element, attributes) {
console.log('link called');
var el = $compile('<foo><div class="hide" >I should always be hidden.</div><div class="hideDelay" ng-show="visiblity">I should be hidden after 1 second.</div></foo>')(scope);
element.append(el);
scope.visiblity=false;
},
restrict: 'E',
template: '<div class="fooContainer"></div>'
}
});
why Dont you try using ng-show/ng-hide
You can safely attach events to the element in the directive's link function, but only for the directive's children. The directive's parent haven't been linked yet.
So within fooContainer's link function, you know that foo has already been compiled and linked, and it's safe to attach events.
If foo needs to be notified once fooContainer is linked, you can use some form of inter-directive communication. i.e. $scope.$emit. If you want to attach events to foo, you can also use a jquery selector inside fooContainer's link function.
According to Angular documentation:
templateUrl
Same as template but the template is loaded from the specified URL.
Because the template loading is asynchronous the compilation/linking
is suspended until the template is loaded
This means your template is already loaded by the time your link function executes. A promise object is not needed.

Is there a way to watch attribute changes triggered from outside the AngularJS world?

Iā€™m trying to understand interactions between the Angular world and the non-Angular world.
Given a directive that one declares like this:
<dir1 id="d1" attr1="100"/>
If code outside angular changes the directive this way:
$("#d1").attr("attr1", 1000);
How can the directive know that one of its attribute has changed?
It would be best to make this change inside the directive instead. If, for whatever reason, that's not possible, then there are a couple of options.
Outside the app, get a reference to any DOM element within the app. Using that reference, you can then get a reference to its scope. You could use your element with id d1. For example:
var domElement = document.getElementById('d1');
var scope = angular.element(domElement).scope();
Here are a couple of options:
Option 1
Modify the model instead of making a direct change to the view. In the link function, store the initial attribute value in a scope variable like:
scope.myvalue = attrs.attr1;
Then you can change the value outside the app (using the above reference to scope) like:
scope.$apply(function(){
scope.myvalue = 1000;
console.log('attribute changed');
});
Here is a fiddle
Option 2
If the view is manipulated directly with jQuery, I don't know of any use of $observe, $watch, or an isolate scope binding to the attribute that will work, because they all bind to the attribute expression itself, just once, when the link function is first run. Changing the value will cause those bindings to fail. So you'd have to $watch the attribute on the DOM element itself (rather than through attrs):
scope.$watch(function(){
return $(el).attr('attr1'); // Set a watch on the actual DOM value
}, function(newVal){
scope.message = newVal;
});
Then you can change the value outside the app (using the above reference to scope) like:
scope.$apply(function(){
$("#d1").attr("attr1",1000);
});
Here is a fiddle
Use a Web Components library like x-tags by Mozilla or Polymer by Google. This option works without maunally calling $scope.$apply every time the attribute changes.
I use x-tags because of their wider browser support. While defining a new custom tag (directive) you can set the option lifecycle.attributeChanged to a callback function, which will fire every time an argument is changed.
The official docs aren't very helpful. But by trial and error and diving into the code I managed to find out how it works.
The callback function's context (the this object) is the element itself. The one whose attribute has changed. The callback can take three arguments:
name ā€“ the name of the attribute,
oldValue and
newValue ā€“ these speak for themselves.
So now, down to business:
The code
This will watch the attribute for changes:
xtag.register('dir1', {
lifecycle: {
attributeChanged: function (attribute, changedFrom, changedTo) {
// Find the element's scope
var scope = angular.element(this).scope();
// Update the scope if our attribute has changed
scope.$apply(function () {
if (attribute == 'attr1') scope.style = changedTo;
});
}
}
});
The attributeChanged callback only fires when the arguments' values actually change. To get their initial values you need to scan the lot manually. The easiest way seems to be while defining the directive:
myApp.directive('dir1', function () {
return {
... ,
link: function (scope, element, attributes) {
scope.attr1 = element[0].getAttribute('attr1');
}
};
});

Categories

Resources