Trying to test a directive but I'm at a loss. Basically I don't know how to set the test up and I can't find any examples to step by step me. Can someone provide an explanation of how this should be set up? Right now the error I'm getting is
TypeError: 'undefined' is not an object (evaluating 'current.name')
directives.directive('convenienceNav', function(){
return {
restrict: 'A',
template: '<button class="btn btn-success" ui-sref=" {{$state.current.name}}.add"><i class="fa fa-user-plus"></i>Add {{$state.current.params.singular_title}}</button>'
};
});
describe('directive: convenienceNav', function() {
var element, scope, stateParams, state;
beforeEach(module('app.directives'));
beforeEach(module('ui.router'));
beforeEach(inject(function($rootScope, $compile, $state) {
scope = $rootScope.$new();
stateParams = {'api_resource_name': 'people'};
state = $state;
state.current.name = 'admin.people';
// state.current.params.singular_title = 'Person';
element ='<div convenience-nav></div>';
element = $compile(element)(scope);
scope.$digest();
}));
it('should have state.current.name = admin.people', function(){
expect(element.html()).toBe('<button class="btn btn-success" ui-sref="admin.people.add"><i class="fa fa-user-plus"></i>Add Person</button>');
});
});
Below are the examples of how you can test angular directives and routes navigation(ui router) in jasmine.
Button with name
var namedButtonModule = angular.module('namedButtonModule', []);
namedButtonModule.directive('namedButton', function() {
return {
restrict: 'AE',
scope: {
name: "=?"
},
template: '<button class="btn btn-success">I am {{name}} Button</button>',
controller: function($scope) {
$scope.name = $scope.name || 'simple';
}
};
});
describe('directive: namedButton', function() {
beforeEach(module('namedButtonModule'));
beforeEach(inject(function($rootScope, $compile) {
this.$rootScope = $rootScope;
this.$compile = $compile;
this.testContainer = document.getElementById('test-container');
this.compileDirective = function(template, scope) {
var element = this.$compile(template)(scope);
this.testContainer.appendChild(element[0]);
scope.$digest();
return element;
}
}));
afterEach(function() {
this.testContainer.innerHTML = '';
});
it('button should show default name', function() {
var template = '<div named-button></div>';
var scope = this.$rootScope.$new();
var element = this.compileDirective(template, scope);
expect(element.text()).toBe('I am simple Button');
});
it('button should show passed name to the scope', function() {
var template = '<div named-button name="inputName"></div>';
var scope = this.$rootScope.$new();
scope.inputName = "Angular Test";
var element = this.compileDirective(template, scope);
expect(element.text()).toBe('I am Angular Test Button');
});
});
<!-- jasmine -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine-html.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/boot.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine.min.css" rel="stylesheet" />
<!-- angular -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.15/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.15/angular-mocks.js"></script>
<div id="test-container"></div>
Navigation Button
on clicking button, app will redirect to that state.
var navigationButtonsModule = angular.module('navigationButtonsModule', ['ui.router']);
navigationButtonsModule.config(['$stateProvider',
function($stateProvider) {
$stateProvider.state('home', {
url: '/home',
templateUrl: 'home.html'
});
}
]);
navigationButtonsModule.directive('navigationButton', function() {
return {
restrict: 'AE',
scope: {
state: "=?",
name: "=?"
},
template: '<button class="btn btn-success" ui-sref="{{state}}">Go to {{name}}</button>',
};
});
describe('directive: navigationButton', function() {
beforeEach(module('navigationButtonsModule'));
beforeEach(inject(function($rootScope, $compile, $state, $templateCache, $timeout) {
this.$rootScope = $rootScope;
this.$compile = $compile;
this.$state = $state;
this.$templateCache = $templateCache;
this.$timeout = $timeout;
this.testContainer = document.getElementById('test-container');
this.compileDirective = function(template, scope) {
var element = this.$compile(template)(scope);
this.testContainer.appendChild(element[0]);
scope.$digest();
return element;
}
}));
afterEach(function() {
this.testContainer.innerHTML = '';
});
it('navigation button should show passed name and ui-sref state', function() {
var template = '<navigation-button state="state" name="name"></navigation-button>';
var scope = this.$rootScope.$new();
scope.state = 'home';
scope.name = 'Home';
var element = this.compileDirective(template, scope);
expect(element.text()).toBe('Go to Home');
expect(element.find('button').attr('ui-sref')).toBe('home');
});
it('will show home href', function() {
expect(this.$state.href('home')).toEqual('#/home');
});
it('on button click browser should go to home state', function() {
var template = '<navigation-button state="state" name="name"></navigation-button>';
var scope = this.$rootScope.$new();
scope.state = 'home';
scope.name = 'Home';
var element = this.compileDirective(template, scope);
// mimicking home.html template
this.$templateCache.put('home.html', '');
this.$timeout(function() {
element.find('button')[0].click();
});
this.$timeout.flush();
this.$rootScope.$digest();
expect(this.$state.current.name).toBe('home');
});
});
<!-- jasmine -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine-html.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/boot.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine.min.css" rel="stylesheet" />
<!-- angular -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.15/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.15/angular-mocks.js"></script>
<div id="test-container"></div>
JSFiddle
I have kept replicated code as it is, so it can be easy to read and understand
Related
How to expose directive methods without using $broadcast or '=' between modules?
Using $broadcast (events) if there are multiple directives all will be notified. It cannot return value too.
Exposing directive's function by html attribute I think it is not that best that Angular has to offer.
Angular Bootstrap UI do it using services (I guess): It have a service named "$uibModal".
You can call a function "$uibModal.open()" of Modal Directive by injecting $uibModal service.
Is that the right way?
An example of a directive that registers its API with a service:
app.service("apiService", function() {
var apiHash = {};
this.addApi = function (name,api) {
apiHash[name] = api;
};
this.removeApi = function (name) {
delete apiHash[name];
};
this.getApi = function (name) {
return apiHash[name];
};
});
app.directive("myDirective", function (apiService) {
return {
restrict: 'E',
scope: {},
template: `<h1>{{title}}</h1>`,
link: postLink
};
function postLink(scope, elem, attrs)
var name = attrs.name || 'myDirective';
var api = {};
api.setTitle = function(value) {
scope.title = value;
};
apiService.addApi(name, api);
scope.$on("$destroy", function() {
apiService.removeApi(name);
});
}
});
Elsewhere in the app, the title of the directive can be set with:
apiService.getApi('myDirective').setTitle("New Title");
Notice that the directive registers the api with a name determined by the name attribute of the directive. To avoid memory leaks, it unregisters itself when the scope is destroyed.
Update
How could I use it from a controller?
app.controller('home', function($scope,apiService) {
$scope.title = "New Title";
$scope.setTitle = function() {
apiService.getApi('mainTitle').setTitle($scope.title);
};
})
<body ng-controller="home">
<my-directive name="mainTitle"></my-directive>
<p>
<input ng-model="title" />
<button ng-click="setTitle()">Set Title
</button>
</p>
</body>
The DEMO
angular.module('myApp', [])
.service("apiService", function() {
var apiHash = {};
this.addApi = function(name, api) {
apiHash[name] = api;
};
this.getApi = function(name) {
return apiHash[name];
};
})
.directive("myDirective", function(apiService) {
return {
restrict: 'E',
scope: {},
template: `<h1>{{title}}</h1>`,
link: postLink
};
function postLink(scope, elem, attrs) {
var name = attrs.name || 'myDirective';
var api = {};
api.setTitle = function(value) {
scope.title = value;
};
apiService.addApi(name, api);
scope.$on("$destroy", function() {
apiService.addApi(name, null);
});
}
})
.controller('home', function($scope,apiService) {
$scope.title = "New Title";
$scope.setTitle = function() {
apiService.getApi('mainTitle').setTitle($scope.title);
};
})
<script src="//unpkg.com/angular/angular.js"></script>
<body ng-app="myApp" ng-controller="home">
<my-directive name="mainTitle"></my-directive>
<p>
<input ng-model="title" />
<button ng-click="setTitle()">Set Title
</button>
</p>
</body>
.factory('myService', [function() {
return {
charCount: function(inputString) {
return inputString.length;
}
}
}])
this service exposes function charCount();
in your directive you have to inject it like this
.directive('testDirective', ['myService', function(myService) {
return {
restrict: 'A',
replace: true,
template: "<div>'{{myTestString}}' has length {{strLen}}</div>",
link: function($scope, el, attrs) {
$scope.myTestString = 'string of length 19';
$scope.strLen = myService.charCount( $scope.myTestString );
}
}
}])
and, of course call it
$scope.strLen = myService.charCount( $scope.myTestString );
<html>
<style>
#out {
width:96%;
height:25%;
padding:10px;
border:3px dashed blue;
font-family: monospace;
font-size: 15px;
}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.min.js"></script>
<script>
var APP = angular.module('MYAPP', []);
APP.controller('main', ['$scope', '$element', '$compile', 'myService', function($scope, $element, $compile, myService) {
$scope.test = 'my Test Controller';
$scope.directiveTest = "directive test";
var testSvc = myService.charCount($scope.test);
$scope.showTestDir = true;
}])
.directive('testDirective', ['myService', function(myService) {
return {
restrict: 'A',
replace: true,
template: "<div>'{{myTestString}}' has length {{strLen}}</div>",
link: function($scope, el, attrs) {
$scope.myTestString = 'string of length 19';
$scope.strLen = myService.charCount( $scope.myTestString );
}
}
}])
.factory('myService', [function() {
return {
charCount: function(inputString) {
return inputString.length;
}
}
}])
.filter('toUpper', function() {
return function(input) {
return input.toUpperCase();
}
})
.filter('toLower', function() {
return function(input) {
return input.toLowerCase();
}
})
;
</script>
<body ng-app="MYAPP">
<div id="out" ng-controller="main">
{{test}} - not filtered
<br/>
{{test|toUpper}} - filtered toUpper
<br/>
{{test|toLower}} - filtered toLower
<br/>
<br/>
<div test-directive ng-if="showTestDir"></div>
</div>
</body>
</html>
I have an angular directive detectFocus as :
app.directive("detectFocus", function ($focusTest, $location, $rootScope) {
return {
restrict: "A",
scope: {
onFocus: '&onFocus',
onBlur: '&onBlur',
},
link: function (scope, elem) {
$rootScope.$on('$locationChangeSuccess', function (event, newUrl, oldUrl) {
return;
});
elem.on("focus", function () { console.log("focus");
scope.onFocus();
$focusTest.setFocusOnBlur(true);
});
elem.on("blur", function () { console.log("blur");
scope.onBlur();
if($focusTest.getFocusOnBlur())
elem[0].focus();
});
}
}
});
this directive check two event focus and blur, so is there any way to check location change from this directive.
Try binding a listener on Angulars built in $locationChangeSuccess event. This event is fired every time your app has finished changing a location.
Your link function could look somehow like this.
link: function($rootScope) {
$rootScope.$on('$locationChangeSuccess', function (event, newUrl, oldUrl) {
console.log('Changed from ', oldUrl, ' to ', newUrl);
});
}
Try adding a watch on $location.path()
myModule.directive('highlighttab', ['$location', function(location) {
return {
restrict: 'C',
link: function($scope, $element, $attrs) {
var elementPath = $attrs.href.substring(1);
$scope.$location = location;
$scope.$watch('$location.path()', function(locationPath) {
(elementPath === locationPath) ? $element.addClass("current") : $element.removeClass("current");
});
}
};
}]);
adding watch variable is not much a recomented process since it will increase the load of the application. Instead you can use the $locationChangeSuccess event in angular js.
<!DOCTYPE html>
<html>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.0/angular-route.min.js"></script>
<script>
var app = angular.module('routeApp', ['ngRoute']);
app.config(['$routeProvider', function ($routeProvider) {
$routeProvider.when('/addStudent', {
template: '<div>Add Student</div>',
controller: 'addStudentController'
})
.when('/viewStudent', {
template: '<div>View Student</div>',
controller: 'viewStudentController'
})
.otherwise({
redirectTo: '/'
});
}]);
app.directive('activeLink', ['$location', function (location) {
return {
restrict: 'A',
link: function (scope, element, attrs, controller) {
var clazz = attrs.activeLink;
var path = attrs.href;
path = path.substring(1); //hack because path does bot return including hashbang
scope.location = location;
scope.$on('$locationChangeSuccess', function () {
console.log('$locationChangeSuccess changed!', new Date());
});
}
};
}]);
app.controller('addStudentController', function ($scope) {
$scope.message = "This is message from add student controller";
});
app.controller('viewStudentController', function ($scope) {
$scope.message = "This is message from view student controller";
});
app.controller('pageController', function ($scope, $location) {
$scope.GoTo = function (URl) {
$location.path('/' + URl);
};
});
</script>
<body ng-app="routeApp" ng-controller="pageController">
<h2>Sample Application</h2>
Add Student
View Student
home
<div ng-view></div>
</body>
</html>
inject $rootScope and use it like this :
$rootScope.$on('$stateChangeStart', function(evt, to, params) {
//do your stuff here
}
Furthermore if you use elem.on('focus/blur') you must call scope.$apply() in the even callback if you want angular to detect the changes.
When unit testing angular directives, how can I grab an instance of the directive controller and assert certain data bindings on the controller?
function myDirective() {
return {
restrict: 'E',
replace: true,
templateUrl: 'tpl.html',
scope: { },
controller: function($scope) {
$scope.text = 'Some text';
}
};
}
angular
.module('example')
.directive('myDirective', [
myDirective
]);
unit test
describe('<my-directive>', function() {
var element;
var $compile;
var $scope;
beforeEach(module('example'));
beforeEach(function() {
inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$scope = _$rootScope_.$new();
});
});
describe('basic functionality', function() {
beforeEach(function() {
element = $compile('<my-directive></my-directive')($scope);
$scope.$digest();
});
it('should bind the correct text', function() {
//?
});
});
});
element = $compile('<my-directive></my-directive')($scope);
$scope.$digest();
ctrl = element.controller('myDirective');
Call element.controller with $scope like element.controller($scope). Some proof of concept bellow.
angular
.module('example', [])
.directive('myDirective', [
function myDirective() {
return {
restrict: 'E',
replace: true,
//template: ['<div>{{ text }}</div>'].join(''),
scope: {},
controller: function($scope) {
$scope.text = 'Some text';
}
};
}
]);
describe('<my-directive>', function() {
var element;
var $compile;
var $scope;
beforeEach(module('example'));
beforeEach(function() {
inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$scope = _$rootScope_.$new();
});
});
describe('basic functionality', function() {
beforeEach(function() {
element = $compile('<my-directive></my-directive')($scope);
$scope.$digest();
});
it('should bind the correct text', function() {
expect(element.controller($scope).scope().$$childTail.text).toEqual('Some text')
});
});
});
<link href="//safjanowski.github.io/jasmine-jsfiddle-pack/pack/jasmine.css" rel="stylesheet" />
<script src="//safjanowski.github.io/jasmine-jsfiddle-pack/pack/jasmine-2.0.3-concated.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular-mocks.js"></script>
i have simple directive that create a text, after click to add buttom.
and after click to each directive remove and destroy directive properly.
but i need delete all directive after selected.
for example i clicked to add button for 5 step and result same bellow
Directive content
Directive content
Directive content
Directive content
Directive content
i need click to item 2 then remove and destroy scope of item 3,4,5
another question is , can i delete directive by spsephic id ?
<body ng-app="app">
<div ng-controller="MainController">
<button ng-click="Stage()">{{stage}}</button>
<div class="my-directive-placeholder"></div>
</div>
</body>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script>
var app = angular.module('app', []);
app.controller('MainController', function($scope, $compile, $element){
$scope.stage = 'Add';
var childScope;
$scope.Stage = function(){
childScope = $scope.$new();
var el = $compile( "<b my-directive></b>" )(childScope);
$('.my-directive-placeholder').append(el);
}
})
app.directive('myDirective', function($interval){
return {
template: 'Directive content<br>',
link: function(scope, element, attrs){
element.on('click', function () {
scope.$destroy();
element.remove();
});
scope.$on('$destroy', function(){
console.log('destroid');
});
}
}
});
</script>
https://jsfiddle.net/0vucwqrc/
Instead creating html on compile time, may be its better to have dedicated directive for this like, See if this fits your requirment
angular.module('MyApp', [])
angular.module('MyApp')
.controller("DirectivePageController",
function() {
var self = this;
self.fields = [{
Name: 'Directive1'
}];
self.newField = function() {
self.fields.push({
Name: ('Directive' + (self.fields.length + 1))
});
};
self.removeField = function(field) {
var index = self.fields.indexOf(field);
if (index >= 0) {
self.fields.splice(index, 1);
}
};
})
.controller("appDirectiveController", ['$scope', '$attrs',
function($scope, $attrs) {
var self = this;
var directiveScope = $scope.$parent;
self.options = directiveScope.$eval($attrs.model);
self.onOk = function() {
alert(JSON.stringify(self.options) + ' button clicked');
}
}
])
.directive('appDirective', function($compile) {
return {
transclude: true,
template: '<div ng-click="dirCtrl.onOk()" type="">{{type|uppercase}}</div>',
scope: {
index: '#',
type: '#'
},
restrict: 'E',
replace: true,
controller: 'appDirectiveController',
controllerAs: 'dirCtrl',
}
})
<script src="https://code.angularjs.org/1.4.8/angular.js"></script>
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<body>
<div ng-app="MyApp">
<div ng-controller="DirectivePageController as pageCtrl">
<span>Add Button</span>
<div ng-repeat="field in pageCtrl.fields track by $index">
<app-directive type="{{field.Name}}" model="field">
</app-directive>
Remove
</div>
</div>
</div>
</body>
</html>
Today I'm trying to develop a popover directive. I don't know why the ng-repeat inside the styles-select directive wich is insered in the popover after click doesn't work(<- Edited it works now)... And I want to get the value of "selectedStyles" in my controller "MyController" without passing it through the directive.
var app = angular.module('MyApp', []);
app.controller('MyController', ['$scope', function($scope) {
$scope.selectedStyles = [];
$scope.$watch('selectedStyles', function(newValue, oldValue) {
console.log(newValue);
});
}]);
app.directive('popover', ['$compile', '$templateCache', function($compile, $templateCache) {
return {
restrict: 'A',
scope: {
header: '#header',
template: '=template'
},
link: function(scope, element) {
element[0].onclick = function (event) {
var popover = document.createElement('div'),
header = document.createElement('h4'),
content = document.createElement('p');
header.textContent = scope.header;
content.innerHTML = $templateCache.get(scope.template);
popover.appendChild(header);
popover.appendChild(content);
document.body.appendChild($compile(popover)(scope)[0]);
scope.$apply();
}
}
};
}]);
app.directive('stylesSelect', ['$compile', '$filter', function($compile, $filter) {
return {
restrict: 'E',
scope: {
selectedStyles: '=selectedStyles'
},
template: '<div ng-repeat="s in styles"><label><input type="checkbox" ng-model="s.selected" ng-change="selectStyle()" /> {{s.label}}</label></div>',
link: function(scope, element) {
scope.styles = [
{label: 'Hipster', selected: false},
{label: 'Hip-Hop', selected: false},
{label: 'Punk', selected: false}
];
scope.selectStyle = function() {
scope.selectedStyles = $filter('filter')(scope.styles, {selected: true});
};
}
}
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.1/angular.min.js"></script>
<div ng-app="MyApp">
<div ng-controller="MyController">
{{test}}
<button popover template="'popoverContent.html'" header="Select your styles" type="button">Show Popover</button>
<script type="text/ng-template" id="popoverContent.html">
<styles-select selected-styles="selectedStyles"></styles-select>
</script>
</div>
</div>
It gonna make me crazy... Please Help lol
Thank you
Instead of changing values in different scopes, try to use a service with a promise. This way the popover service is more reusable in your application.
var app = angular.module('MyApp', []);
app.controller('MyController', ['$scope', 'popover',
function($scope, popover) {
$scope.selectedStyles = [];
$scope.showStylesSelect = function() {
popover.show({
templateUrl: 'popoverContent.html',
scope: {
header: 'Select your style',
styles: [{
label: 'Hipster',
selected: false
}, {
label: 'Hip-Hop',
selected: false
}, {
label: 'Punk',
selected: false
}]
}
}).then(function(result) {
$scope.selectedStyles = result.selectedStyles;
});
};
$scope.$watch('selectedStyles', function(newValue, oldValue) {
console.log(newValue);
});
}
]);
app.factory('popover', ['$rootScope', '$q', '$compile', '$templateCache',
function($rootScope, $q, $compile, $templateCache) {
function showPopover(options) {
var defer = $q.defer(),
scope = $rootScope.$new(),
popover = document.createElement('div'),
header = document.createElement('h4'),
content = document.createElement('p');
angular.extend(scope, options.scope || {});
scope.close = function() {
popover.parentNode.removeChild(popover);
defer.resolve(scope);
};
header.textContent = options.header || '';
content.innerHTML = $templateCache.get(options.templateUrl);
popover.appendChild(header);
popover.appendChild(content);
document.body.appendChild($compile(popover)(scope)[0]);
return defer.promise;
}
return {
show: showPopover
}
}
]);
app.directive('stylesSelect', ['$filter',
function($filter) {
return {
restrict: 'E',
scope: false,
template: '<div ng-repeat="s in styles"><label><input type="checkbox" ng-model="s.selected" ng-change="selectStyle()" /> {{s.label}}</label></div><button ng-click="close()">close</button>',
link: function(scope) {
scope.selectStyle = function() {
scope.selectedStyles = $filter('filter')(scope.styles, {
selected: true
});
};
}
}
}
]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.1/angular.min.js"></script>
<div ng-app="MyApp" class="ng-scope">
<script type="text/ng-template" id="popoverContent.html">
<styles-select selected-styles="selectedStyles"></styles-select>
</script>
<div ng-controller="MyController" class="ng-scope ng-binding">
<button ng-click="showStylesSelect()">Show Popover</button>
</div>
</div>