<divng-controller="FruitController"ng-init="init()"><h4>Call controller method in default scope</h4><label>What's your favorite fruit(name can only contains letter)</label><inputtype="text"ng-model="newFruit"/><buttontype="button"add-fruit-method>validate and add</button><ul><ling-repeat="fruit in fruits track by $index"></li></ul></div>
This time, we want to validate the fruit name use typed in first, if it only contains letter, then it’s eligible to add in, otherwise nothing happen. The validation logic is defined in controller, we need to test the validation method is called inside our directive.
describe('directives',function(){beforeEach(module('myApp.directives'));describe('addFruitMethod',function(){var$scope,element;beforeEach(inject(function($rootScope,$compile){$scope=$rootScope;$scope.fruits=[];$scope.newFruit='apple';$scope.isValid=angular.noop;element=angular.element('<input type="text" name="fruit" id="fruitDefault" ng-model="newFruit"/><button type="button" add-fruit-method>validate and add</button>');$compile(element)($scope);}));it('should add valid fruit to fruit list when click button',function(){varisValid=spyOn($scope,'isValid').andReturn(true);element.filter('button').trigger('click');expect(isValid).toHaveBeenCalled();expect($scope.fruits[0]).toBe('apple');});it('should reject invalid fruit when click button',function(){varisValid=spyOn($scope,'isValid').andReturn(false);element.filter('button').trigger('click');expect(isValid).toHaveBeenCalled();expect($scope.fruits.length).toBe(0);});});});
It’s very similar with testing manipulate data on default scope, the only thing different is we spy on the validation method to verify it has been called, also we let the spy object return the corresponding result to execute each branch.
<divng-controller="FruitController"ng-init="init()"><h4>Model manipulation with isolated scope</h4><label>What's your favorite fruit</label><inputtype="text"ng-model="newFruit"/><buttontype="button"add-fruitfruits="fruits"new-fruit="newFruit">Add</button><ul><ling-repeat="fruit in fruits track by $index"></li></ul></div>
The above directive manipulate data in default scope, now we create a isolated scope for the directive, the test point change to verify the data on isolated scope.
123456789101112131415161718192021
describe('directives',function(){beforeEach(module('myApp.directives'));describe('addFruit',function(){var$scope,element;beforeEach(inject(function($rootScope,$compile){$scope=$rootScope;$scope.fruits=[];$scope.newFruit='apple';element=angular.element('<input type="text" ng-model="newFruit"/><button type="button" add-fruit fruits="fruits" new-fruit="newFruit">Add</button>');$compile(element)($rootScope);}));it('should add fruit to fruit list when click button',function(){element.filter('button').trigger('click');expect(element.scope().fruits[0]).toBe('apple');// use element.scope() to access isolated scopeexpect($scope.fruits[0]).toBe('apple');// also verify default scope is updated})});});
In the test, we setup the surrounding scope, then verify both the default scope and isolated scope are updated.(we use element.scope() to access the isolated scope).
<divng-controller="FruitController"ng-init="init()"><h4>Model manipulation with default scope</h4><label>What's your favorite fruit</label><inputtype="text"ng-model="newFruit"/><buttontype="button"add-fruit-default>Add</button><ul><ling-repeat="fruit in fruits track by $index"></li></ul></div>
Let’s try something advanced, we have a input box to type in a fruit name and a button, after click the button, the value of the input box will added into the fruit list.
How to write test for this directive? If the fruit list on the scope will have the fruit we passed in after we click the button, then we think it’s working well.
123456789101112131415161718192021
describe('directives',function(){beforeEach(module('myApp.directives'));describe('addFruitDefault',function(){var$scope,element;beforeEach(inject(function($rootScope,$compile){$scope=$rootScope;$scope.fruits=[];$scope.newFruit='apple';element=angular.element('<input type="text" name="fruit" id="fruitDefault" ng-model="newFruit"/><button type="button" add-fruit-default>Add</button>');$compile(element)($scope);}));it('should add fruit to fruit list when click button',function(){element.filter('button').trigger('click');expect($scope.fruits[0]).toBe('apple');});});});
Not like the first test code, this one needs to verify the data on scope, we need a real scope, also we need to initialize the state of the scope, after we compile the html fragment using the scope, interact with the DOM will affect the scope.
<div><h4>Basic Dom Manipulation</h4><inputtype="text"name="search"id="search"/><buttontype="button"clear-search>Clear</button></div>
We’re going to write a directive for the button, after click it, the search box wil be clear.
To demonstrate DOM manipulation, we’ll not set ngModel for the search box. Let’s write out the test first.
As a Java developer, I am familiar with test driven development with Java language, but for angular, I’m not, sometime it’s even hard to write angular test after the implementation is done.
So I’ll write a series of articles about how to test with angular, basically, I’d like to include the test strategy for directives, controllers and services, this article will begin with how to test angular directive.
Let’s check out the below examples form easy to hard.
This post is copied from stackvoerflow, check this for more details.
After writing a lot of directives, I’ve decided to use less isolated scope. Even though it is cool and you encapsulate the data and be sure not to leak data to the parent scope, it severely limits the amount of directives you can use together. So,
Isolated: a private sandbox
If the directive you’re going to write is going to behave entirely on its own and you are not going to share it with other directives, go for isolated scope. (like a component you can just plug it in, with not much customization for the end developer) (it gets very trickier when you try to write sub-elements which have directives within)
None: simple, read-only directives
If the directive you’re going to write is going to just make dom manipulations which has needs no internal state of scope, or explicit scope alterations (mostly very simple things); go for no new scope. (such as ngShow,ngMouseHover, ngClick, ngRepeat)
Child: a subsection of content
If the directive you’re going to write needs to change some elements in parent scope, but also needs to handle some internal state, go for new child scope. (such as ngController)
So keep in mind that don’t use isolated scope unless you have to.
Return false will prevent default browser behavior
I was blocked for whole afternoon by a very weird phenomenon, two radio buttons(with the same name), one of them can never be checked after you click it, at the last, I found out the root cause is the browser’s default behavior is prevented by the return false statement in directive.
Let’s see what a normal radio button group should be:
check the demo, click a radio button can make it checked.
Now let’s make some trouble, if we click a text input field, we want to show a console log say ‘input field clicked’, if other type input component is clicked, do nothing. let’s write a directive to handle this.
angular.module('Demo',[]).controller('DemoController',['$scope',function($scope){}]).directive('tellMe',[function(){return{link:function(scope,element,attr){element.bind('click',function(){vartarget=angular.element(event.target);if(!target.is(':text')){returnfalse;}else{console.log('input field clicked');}});}};}]);
Check the demo, you’ll see the radion button group is not functional well, one of them can never be checked, this is all because we use return false in directive.
After we replace return false with return, everything back to normal, check again here
I was trapped in angular directive this work, after struggled for hours, I noticed below traps in angular directive.
Directive with isolated scope will impact native angular directive
If your own directive has a isolated scope, then it will impact native angular directive, which means, sometime, ngModel, ngDisabled suddenly doesn’t work, because they’re impacted by your directive.
take below as an example:
We have a input field to type in a programming language, click the ‘Add’ button will add it into a list(as it’s a simple demo, so data validation is not concerned)
12345678910111213141516
<bodyng-app="DemoApp"><divng-controller="DemoController"> What's your favorite programming language (up to five):
<inputtype="search"ng-model="profile.newLanguage"/><inputtype="button"value="Add"add-languagelanguages="profile.languages"new-language="profile.newLanguage"/><div><ul><ling-repeat="language in profile.languages"></li></ul></div></div></body>
we put a directive addLanuage on the button, which will get the value in the input field and add it to language list, due to we need to operate the language list, so we use a isolated scope to access it inside the directive.
Now the new requirement comes, a user only allow to fill up to five programming languages, we need to disable the Add button after user have input 5 languages.
Seems a small change will fit the new requirement, ngDisabled should solve this.
As you can see from the code, we declare a new attribute needs-disabled which use reachThreshold() as it’s value, then we set needsDisabled to ng-disabled, the last thing is to declare the new attribute in directive’s scope, in this way, ngDisabled back again.
NO Multiple isolated scope
If you put more than one directive on a element, and each of them has a isolated scope, angular will fail and complain multiple isolated scope on one element.
I’ve been using angularjs for several months, at the beginning, i always think everything in jQuery way, since I’ve used jQuery heavily on all of my previous projects, but day by day
I became like angularjs, i realize the shortcoming of jQuery: you will not know who is responsile for the event happens on this element until you see the event binding in jQuery code;
lots of manipulation is based on css selector which is fragile; it’s also hard and tricky to transfer data between different element …
So I decide to write a series of articles to record what I’ve learned in the past months, these articles are not for beginners, I assume you have a basic concept with AngularJS, but still don’t know
how to write AngularJS in the right way. Let’s start with directives .
Directive Definition Object
A directive usually appears as a element/tag name or attribute, it’s used to add additional functionality to the element, like, <a toggle-background/>, the directive toggleBackground will switch the
background color of the current page once you click it, it’s more clear than implement the same logic with jQuery, you will understand what will happen when you see the directive on the link.
Let’s take a closer look to directives, below is a simple directive defination.
we can ignore the restrict property, by default a directive can appear as a attribute, let’s goes into the most important part in a directive: the link function .
The link function take three parameters: scope, elm, attr, the last two are easy to understand:
elm the jQuery object representation of the element which this directve blongs to
Take below code as an example, then most common use of directive is to do something against the element it decorated, in below example, we add autocomplete functionality for a input field.
123456
<bodyng-app="DemoApp"><div> What's your favorite programming language:
<inputtype="search"search-language/></div></body>
attr all the attributes on this element, it’s a map of attribute name and value, given <a type="text" some-directive/>, attr.type will return textattr are used to pass additional information to directive, it has the same functionallity with jQuery(element).attr('attrName'), but more convenient.
In below exmaple, we config the autocomplete dropdown is triggered at least user input 3 characters.
123456
<bodyng-app="DemoApp"><div> What's your favorite programming language:
<inputtype="search"search-languagemin-length="3"/></div></body>
Why we need scope for directive ? First, let’s devide all kinds of directves into two groups:
without scope
If what we do in this the directive is only DOM manipulation, then elm and attr is enough for use, we don’t need to declare a isolated scope.
with scope
If we need to manipulate angular model in directive, then we need to declare a scope.
directive which don’t need to use scope is easy to understand, usually we bind event listener on the link function, but directive which need scope requires more practice to master it, we also can
devide this kind of directives into two groups: manipulate model in controller and call method in controller
Manipulate Model in Controller
access model in controller need to establish a isolated scope, there are two ways:
@[attributeName] return the value of that attributeName, the value is a plain string, it’s the same as using attr.attributeName
=[attributeName] two way binding, first get the value of that attributeName, then evaluate the value in controller scope, changing the value in directive will reflect in controller scope
123456789101112131415161718
<bodyng-app="DemoApp"><divng-controller="DemoController"> Programming language (up to five):
<inputtype="search"ng-model="profile.newLanguage"><inputtype="button"value="Add"add-languagelanguages="profile.languages"new-language="profile.newLanguage"/><div><ul><ling-repeat="language in profile.languages"></li></ul></div></div></body>
In the above code, we establish a isolateds scope to setup a bridge with languages and newLanguage in controller’s scope, then we can manipulate them.
Call Method in Controller
&[attributeName] return the value of that attributeName, the value is a function reference which points to the a method whose name same as the value in controller.
we are two type of invocation of controller method: without parameters and with parameters.
without parameters
123456789101112131415161718
<bodyng-app="DemoApp"><divng-controller="DemoController"> Programming language (up to five):
<inputtype="search"ng-model="profile.newLanguage"><inputtype="button"value="Add"add-languagelanguages="profile.languages"send-signal-to-server="sendSignalToServer()"new-language="profile.newLanguage"/><div><ul><ling-repeat="language in profile.languages"></li></ul></div></div></body>
angular.module('DemoApp',[]).controller('DemoController',["$scope",function($scope){$scope.profile={};$scope.profile.languages=[];$scope.clearValue=function(){$scope.newLanguage="";};$scope.sendSignalToServer=function(){console.log('sending signal to server');};}]).directive('addLanguage',[function(){return{scope:{languages:'=',newLanguage:'=',sendSignalToServer:'&'},link:function(scope,ele,attr){ele.on('click',function(){scope.languages.push(scope.newLanguage);scope.sendSignalToServer();scope.$apply();});}};}]);
we want to call a methhod in controller which send some signal to server from our directive, we declare an attribute send-signal-to-server whose value is the method name is controller, as a result, scope.sendSignalToServer hold a reference to method sendSignalToServer() .
with parameters
Continue with the above example, we change the controller method sendSignalToServer() to accept two parameters, pass parameters from directive to controller is a lillte tricky.
12345678910111213141516171819
<bodyng-app="DemoApp"><divng-controller="DemoController"> Programming language (up to five):
<inputtype="search"ng-model="profile.newLanguage"><inputtype="button"value="Add"add-languagelanguages="profile.languages"send-signal-to-server="sendSignalToServer(param1, param2)"new-language="profile.newLanguage"/><div><ul><ling-repeat="language in profile.languages"></li></ul></div></div></body>
angular.module('DemoApp',[]).controller('DemoController',["$scope",function($scope){$scope.profile={};$scope.profile.languages=[];$scope.clearValue=function(){$scope.newLanguage="";};$scope.sendSignalToServer=function(param1,param2){console.log('sending signal to server',param1,param2);};}]).directive('addLanguage',[function(){return{scope:{languages:'=',newLanguage:'=',sendSignalToServer:'&'},link:function(scope,ele,attr){ele.on('click',function(){scope.languages.push(scope.newLanguage);scope.sendSignalToServer({"param1":"123","param2":"456"});scope.$apply();});}};}]);
as you can see, we need to define placeholder for the argument list in the attribute, and in the directive, when we are going to call that method, we need to construct a object which use the place holder as keys, and your real parameters as values.
If you’re patient enough to read to here, i belieave you’ve got a basic concept how to write directive in the right way. But keep in mind, don’t use too many directives on one element, it’s difficult to understand which directive is responsible for which functionality, thus increase the effort to maintain the code, also your directive could impact the native angular directive, so first try to use angular native directives(e.g. ng-click, ng-init), if it can not fit your requirements, write you own.