Within a unit test for an angular directive, I want to inspect the DOM that has been compiled:
var element = $compile("<div pr-sizeable='...'></div>")($rootScope);
var children = element[0].children; // HTMLCollection
expect(children.length).toBeGreaterThan(0); // that's fine
Especially, I want to check for the existence of a specific child element having an attribute pr-swipe-handler='right'.
I know I could iterate the children and their attributes-collections, but I'm sure there is a more tense solution.
Here's what I tried (refering to this answer of a similar post):
// TypeError: angular.element(...).querySelector is not a function
angular.element(children).querySelector("[pr-swipe-handler='right']")
// TypeError: children.querySelector is not a function
angular.element(children.querySelector("[pr-swipe-handler='right']"))
Here's a plunkr that should help you:
http://plnkr.co/edit/YRSgbsGCWGujhiOGV97z?p=preview
Code:
app.controller('MainCtrl', function($compile, $document, $scope) {
$scope.name = 'World';
var element = $compile('<div><p></p><div pr-swipe-handler="right"></div></div>')($scope);
console.log(element[0]);
console.log(element);
$document.append(element);
console.log(document.querySelectorAll('[pr-swipe-handler="right"]'))
});
You can not call querySelector on an element, but you can call it on document. You'll need to append the element to document in your tests, but you should be doing that anyway.
Peter Ashwell's answer showed me the right direction. Inspecting the HTMLCollection using the querySelector-api is possible only if the elements of the HTMLCollection are part of the actual DOM. Therefore it is necessary to append the compiled elements to it.
Thanks to that answer to a similar question, it turns out that the compiled element has to be appended to a concrete element in the DOM of the browser, such as body. $document is not enough:
var element = $compile('<div><p></p><div pr-swipe-handler="right"></div></div>')($scope);
var body = $document.find('body');
$document.append(element); // That's not enough.
//body.append(element); // That works fine.
var rightHandler = document.querySelectorAll('[pr-swipe-handler="right"]');
$scope.info = rightHandler.length + ' element(s) found.';
Refer to this plunk.
Related
I try do something with directive in angular, but I've some problem with $compile function in programmatically html element, call here "phe".
var phe = angular.element('<div style="background-color:orange">{{value}}</div>');
When I append "phe" after or before the directive's element, it work like a charm...
var $phe = $compile(phe)(scope);
element.after($phe);
but if I wrapped the directive element with this "phe" the $compile not work.
element.wrap($phe);
Somebody have some idea?
I have made a plunker http://plnkr.co/edit/0x2MmQ7WYmiNog0IEzTj?p=preview
it works if you change the compilation sequence... compile the element before placing it in the dom
var phe_b = angular.element('<div style="background-color:orange"> b {{value}}</div>');
var $phe_b = $compile(phe_b)(scope);
element.before($phe_b);
do same for after...
The reason it doesn't work with wrap is because wrap clones the DOM element. In other words, if you did:
var wrapper = angular.element("<div>");
element.wrap(wrapper);
console.log(wrapper[0] !== element[0].parentNode); // this would output "true"
So, the element that you compiled/linked is not the same that ends up in the DOM.
You could, of course, get the wrapping element (it's the return value of wrap) and $compile it, but you need to be careful not to re-compile/re-link certain directives that were applied on the current element (including the very same directive) and its children.
In this example of a directive unit test from the Angular docs:
describe('Unit testing great quotes', function() {
var $compile;
var $rootScope;
// Load the myApp module, which contains the directive
beforeEach(module('myApp'));
// Store references to $rootScope and $compile
// so they are available to all tests in this describe block
beforeEach(inject(function(_$compile_, _$rootScope_){
// The injector unwraps the underscores (_) from around the parameter names when matching
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('Replaces the element with the appropriate content', function() {
// Compile a piece of HTML containing the directive
var element = $compile("<a-great-eye></a-great-eye>")($rootScope);
// fire all the watches, so the scope expression {{1 + 1}} will be evaluated
$rootScope.$digest();
// Check that the compiled element contains the templated content
expect(element.html()).toContain("lidless, wreathed in flame, 2 times");
});
});
Can someone explain what ($rootScope) is doing in the element variable declaration in the it function.
I not sure what effect it has.
It is used to force a $digest cycle so that the element above it is compiled and rendered immediatly instead of waiting for an undetermined $digest cycle
The $compile function creates a function that grabs values from a scope variable to complete bindings.
When you call the function created by $compile with a scope variable, it replaces all bindings with a value on the scope you gave as an argument, and create a DOM element.
For example :
$rootScope.age = 15;
$scope.age = 52;
var element = $compile("<div>Tom is {{age}}</div>")($rootScope);
/* element is a div with text "Tom is 15" */
var element2 = $compile("<div>Tom is {{age}}</div>")($scope);
/* element2 is a div with text "Tom is 52" */
Compilation in Angular is done in two steps. $compile(template) does just the first half, where directives usually just transform the DOM (and other, more complicated stuff happens as well ;)), and returns a "linking function". The second part is done when the linking function is called with a particular scope as argument. In this part, directives can edit the scope, link behavior to DOM events, etc. More can be found in the official guide.
i have writted a directive to generate input fields from scope fields, everything is working fine except that the parent ng-form stays invalid even though the ng-form within directive is invalid.
this is how i am checking the state of the form:
<ng-form name="parentForm" class="form-horizontal">
<form-field ng-model="input.name" field="fields[0]" ng-change="changed(input.name)"></form-field>
<form-field ng-model="input.age" field="fields[1]"></form-field>
<pre> parent form: valid : {{parentForm.$valid}}</pre>
</ng-form>
and below is the link function
var linkFunction = function (scope, element, attrs) {
var fieldGetter = $parse(attrs.field);
var field = fieldGetter(scope);
var template = input(field, attrs); //genrate the template
element.replaceWith($compile(template)(scope)); //replace element with templated code
};
i guess the problem is that i need to compile the parent element rather than the element itself to get validations working, but not sure on how to do it
element.replaceWith($compile(template)(scope));
PLUNKER LINK
According to the docs on FormController, there is the $addControl() method that is used to:
Register a control with the form.
Input elements using ngModelController do this automatically when they are linked.
This gives us the hint that the "ng-modeled" elements will take care of everything in their link function as long as we give them the chance. Giving them the chance (in this context) means that they should be able to locate their parent ngForm element while linking.
You have been (quite small-heartedly) depriving them of this right, by first compiling and linking them and only then inserting them into the DOM (shame on you).
Once you know the cause, the solution is easy:
You need to first insert them into the DOM and then link them.
E.g.:
// Instead of this:
element.replaceWith($compile(template)(scope));
// Which is equivalent to this:
var linkFunc = $compile(template); // compiling
var newElem = linkFunc(scope); // linking
element.replaceWith(newElem); // inserting
// You should do this:
var newElem = angular.element(template); // creating
element.replaceWith(newElem); // inserting
$compile(newElem)(scope); // compiling + linking
// (could be done in 2 steps
// but there is no need)
See, also, this short demo.
i have seen different example for rending an element in the link function
example one:
var template = '<span><input type="text" ng-model="ngModel"></span>';
element.html(template);
$compile(element.contents())(scope);
example two:
var template = '<span><input type="text" ng-model="ngModel"></span>';
element.html(template);
var el = $compile(element.contents())(scope);
element.replaceWith(el);
i had tried 2-3 simple directives which works even without replacing the element. so what is the use case for the "element.replaceWith(el)". When is it necessary to user "element.replaceWith(el)" at the end of the link function?
Replacement is actually optional, and the final result won't be exactly the same:
Your first example: the element with your directive has the span as its only child
Your second example: the element with your directive is finally replaced with the span -> one level less in the DOM.
All is about what you want in the DOM at the end. If you consider the original container with the directive is a useless wrapper only declaring a component, you will want to unwrap the content.
I'm trying to get the service from my legacy code and running into a weird error with injector() returning undefined:
Check this plnkr
Also, I'm trying to set back the new property value back to the service, will that be reflected to the scope without the use of watch?
Thank you very much, any pointer or suggestion is much appreciated.
You're trying to get the element before the DOM has been constructed. It's basically the same issue as running javascript outside of a $(document ).ready(). So this line has no element to get:
var elem = angular.element($('#myCtr'));
Also, by the way, instead of using jQuery, another Angular option for doing the above is:
var elem = angular.element(document.querySelector('#myCtr'))
Angular provides an equivalent to $(document ).ready() called angular.element(document).ready() which we can use.
But you'll also need to grab scope and execute your change within a scope.$apply() so that Angular is aware that you've changed something that it should be aware of.
Combining the two we get:
angular.element(document).ready(function () {
var elem = angular.element($('#myCtr'));
//get the injector.
var injector = elem.injector();
scope= elem.scope();
scope.$apply(function() {
injector.get('cartFactory').cart.quantity = 1;
});
});
updated plnkr