Writing test for angular directive with dynamic template - javascript

Here is the directive:
app.directive('templates',function() {
return {
restrict:'E',
templateUrl: function(s,e) {
switch (e.template) {
case 'temp1':
return 'temp1.html';
case 'temp2':
return 'temp1.htm2';
default:
// do nothing... ;
}
}
};
});
I can get it to compile in my test but i'm not sure how to test if the correct templates are being called

There is not much to test here. But as a feel good test you can just load a template into cache and do a test if specific element has been rendered or not as a feel good test.
Example:-
describe('templates', function () {
beforeEach(inject(function ($rootScope, $templateCache, $compile) {
// Set an arbitrary template to test
$templateCache.put('temp1.html', '<div class="test">Hello</div>');
element = angular.element("<templates template='temp1'></templates>");
$compile(element)(scope);
$rootScope.$digest();
}));
it('Should load template', function () {
expect(element.find('.test').length).toEqual(1); //Test if element has loaded template properly
expect(element.find('.test').text()).toEqual("Hello");
});
Demo
On a different note your directive can break if there is no template provided, it is required to return a template from templateurl function. Also you can make this simple directive more generic as well.
.directive('templates',function() {
return {
restrict:'E',
templateUrl: function(e,attr) {
return attr.template + ".html"
};
});
How ever, there is nothing to be tested here because you will just end up testing angular's templateUrl function evaluation.

Related

templateUrl function is not using explicit annotation and cannot be invoked in strict mode

I'm attempting to create a custom angular component that dynamically loads a template based on a templateUrl function. I currently get a templateUrl is not using explicit annotation and cannot be invoked in strict mode' error. Normally I understand that this error crops up when an injected service doesn't get properly annotated (https://docs.angularjs.org/error/$injector/strictdi). However, I am missing how this applies to templateUrl.
I'm using Angular 1.5.
Exact error message is -
angular.js:13550 Error: [$injector:strictdi] templateUrl is not using explicit annotation and cannot be invoked in strict mode
Component Code snippet:
angular.module('hive.triGrid')
.controller('TriGridCellController', ['$element', '$attrs', function ($element, $attrs) {
var $ctrl = this;
}])
.component('triGridCell', {
controller: 'TriGridCellController',
templateUrl: function($element, $attrs)
{
var type = $attrs.cellType;
if(type.toUpperCase() == "ICON")
{
return "components/grid/cellTemplates/iconCell.tpl.html";
}
else if(type.toUpperCase() == "CUSTOM")
{
return $attrs.cellTemplateUrl;
}
else
{
return "components/grid/cellTemplates/textCell.tpl.html";
}
},
//template:"<ng-include src='$ctrl.getTemplateUrl(z)'/>",
bindings: {
cellData:'<',
cellType: '<', //See triGridRow and triGrid for config JSON format.
}
});
EDIT:
Code after answer was applied:
templateUrl: ['$element', '$attrs', function($element, $attrs)
{
var type = $attrs.cellType;
if(type.toUpperCase() == "ICON")
{
return "components/grid/cellTemplates/iconCell.tpl.html";
}
else if(type.toUpperCase() == "CUSTOM")
{
return $attrs.cellTemplateUrl;
}
else
{
return "components/grid/cellTemplates/textCell.tpl.html";
}
}],
As said in this answer, $element and $attrs are injected into templateUrl function, not just passed as arguments. This is the difference between element parameter name (in link or compile functions) and $element local dependency in DI-enabled functions that Angular documentation emphasizes.
templateUrl function is invoked by injector in components, so any other services can be injected there as well, and it should be properly annotated.

Update UI based on change of a directive attribute in AngularJs

I am struggling with data binding in AngularJs.
I have the following piece of markup in .html file that includes the custom directive:
<my-directive ng-repeat="i in object" attr-1="{{i.some_variable}}"></my-directive>
Note: 'some-variable' is being updated every 10 seconds(based on the associate collection and passed to template through controller).
The directive's code includes:
myApp.directive('myDirective', function () {
scope: {
'attr-1': '=attr1'
which throws this exception because of the brackets in attr-1(see html code above).
It works though if I use read-only access(note at sign below):
myApp.directive('myDirective', function () {
scope: {
'attr-1': '#attr1'
I use scope.attr-1 in directive's HTML to show its value.
The problem is that with read-only access UI is not reflecting the change in attribute change.
I've found solution with $parse or $eval(couldn't make them work tho). Is there a better one there?
You'll need only two-way binding and I think $parse or $eval is not needed.
Please have a look at the demo below or in this fiddle.
It uses $interval to simulate your updating but the update can also come from other sources e.g. web socket or ajax request.
I'm using controllerAs and bindToController syntax (AngularJs version 1.4 or newer required) but the same is also possible with just an isolated scope. See guide in angular docs.
The $watch in the controller of the directive is only to show how the directive can detect that the data have changed.
angular.module('demoApp', [])
.controller('MainController', MainController)
.directive('myDirective', myDirective);
function MainController($interval) {
var self = this,
refreshTime = 1000; //interval time in ms
activate();
function activate() {
this.data = 0;
$interval(updateView, refreshTime);
}
function updateView() {
self.data = Math.round(Math.random()*100, 0);
}
}
function myDirective() {
return {
restrict: 'E',
scope: {
},
bindToController: {
data: '='
},
template: '<div><p>directive data: {{directiveCtrl.data}}</p></div>',
controller: function($scope) {
$scope.$watch('directiveCtrl.data', function(newValue) {
console.log('data changed', newValue);
});
},
controllerAs: 'directiveCtrl'
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.js"></script>
<div ng-app="demoApp" ng-controller="MainController as ctrl">
model value in ctrl. {{ctrl.data}}
<my-directive data="ctrl.data"></my-directive>
</div>
I've come to the following solution(in case somebody runs into the the same problem):
// Directive's code
myApp.directive('myDir', function () { return {
restrict: 'E',
templateUrl: function () {
return 'my-dir.html';
},
scope: {
'id': '#arId',
'x': '#arX',
'y': '#arY',
//....
},
link: function ($scope, element, attrs) {
// *** SOLUTION ***
attrs.$observe('arId', function (id) {
$scope.id = id;
});
//...
}
Update: somebody sent me this answer, they have the same problem and came up with a very similar if not exact same solution:
Using a directive inside an ng-repeat, and a mysterious power of scope '#'
It is useful to read because they explain what's the idea behind it.

Optional/multiple template URL in single directive Angular JS

Can I have a option between two template URL. Something like :
angular.factory('SAMPLE',function(){
return {
getnDetails:function($http){
return $http({
method: 'GET',
url: url
});
}
)};
angular.directive() {
controller : function($scope) {
SAMPLE. getnDetails().sucess(){
}.error() {
templateURL: "zbx.html"
}
templateUrl : "xyz.html"
}
such that when my http request is an error load entirely different template. What would be the best way to something like this.
ng-include is probably your friend here. A partial (hopefully syntactically correct) example:
angular('app').directive('coolDirective', function () {
return {
scope: {},
controller: function ($scope) {
$scope.template = 'b.html';
if ($scope.condition) {
$scope.template = 'a.html';
}
},
template: '<div ng-include="template"></div>'
}
});
Here's what I do to select different templates. The key is that templateUrl can be a function which accepts the element and the attributes.
In my HTML
<my-directive template="test-template.html"></my-directive>
In my Directive
.directive('myDirective', function () {
var path = '/app/templates/';
return {
restrict: 'E',
templateUrl: function(tElement, tAttrs) {
return path + tAttrs.template + '.html';
},
controller: function ($scope) {
// whatever...
}
};
})
I like this approach because it allows me to have a common directive for all the logic, but control from my HTML the template format to use to display the results.
According to the Angular Directive page.
Note: You do not currently have the ability to access scope variables
from the templateUrl function, since the template is requested before
the scope is initialized.
That said, the solution you were striving for was to open a totally different template if there was an error. In that case you'd want to use the $location to redirect to an error page, not simply load a different template. Or perhaps have an error handling directive in your main template which opens an error modal on any page.
Hope that info helps you or someone else.

AngularJS testing directives with templateUrl

I'm trying to use karma to test AngularJS directives. But I'm running into issues with templateUrls. Using technique described here, it gets even stranger. It seems to work as advertised and loads my template into the $templateCache, but that cache isn't being used by the directive. Here's some code:
This will work just fine
.directive('messageComposer', function($templateCache) {
return {
restrict: 'E',
template: $templateCache.get('partials/message_composer.html'),
replace: true,
link: function() {
console.log('hello world');
}
};
});
but as soon as I use a templateUrl, it fails to bind in the tests:
.directive('messageComposer', function() {
return {
restrict: 'E',
templateUrl: 'partials/message_composer.html',
replace: true,
link: function() {
console.log('hello world');
}
};
});
Anyone know what's going on here?
Here's my unit test setup:
var $scope;
var $compile;
beforeEach(function() {
module('partials/message_composer.html');
module('messageComposer');
inject(function(_$compile_, $rootScope) {
$scope = $rootScope.$new();
$compile = _$compile_;
});
});
it("works", function() {
$scope.message = {};
elem = angular.element("<message-composer message='message'></message-composer>")
$compile(elem)($scope);
console.log(elem);
expect(true).toBeDefined();
});
According to the url (http://tylerhenkel.com/how-to-test-directives-that-use-templateurl/), I believe you have run the following command:
npm install karma-ng-html2js-preprocessor --save-dev
Now when you are using the above preprocessor, then this preprocessor will convert HTML files into JS strings and will generate Angular modules. These modules, when loaded, puts these HTML files into the $templateCache and therefore Angular won't try to fetch them from the server.
Hope, the following files will clarify you better:
https://github.com/karma-runner/karma-ng-html2js-preprocessor
https://github.com/vojtajina/ng-directive-testing

Compile directives via service in angularjs

I'm trying to compile directive via angular service but unfortunately it doesn't work.
The idea is to show errors in popups.
I've modified $exceptionHandler service:
crm.factory('$exceptionHandler', function(popup) {
return function(exception) {
popup.open({message: exception});
}
});
The popup service looks like this:
crm.factory('popup', function ($document) {
return {
open: function (data) {
var injector = angular.element(document).injector(),
$compile = injector.get('$compile'),
template = angular.element('<popup></popup>');
// var ctmp = $compile(template.contents());
$compile(template.contents());
$document.find('body').append(template);
}
};
});
And I don't think that this was a good idea to hard code $compile service (but I haven't any ideas how to realize this in angular):
$compile = injector.get('$compile')
Popup directive:
crm.directive('popup', function () {
return {
restrict: 'E',
replace: true,
templateUrl: '/public/js/templates/common/popup.html',
link: function() {
console.log('link()');
},
controller: function () {
console.log('ctrl()');
}
};
});
May be there are some other ways to do this?
Thanks.
You can inject $compile directly into your service, also you're not quite using $compile correctly:
//commented alternative lines for allowing injection and minification since reflection on the minified code won't work
//crm.factory('popup', ['$document', '$compile', function ($document, $compile) {
crm.factory('popup', function ($document, $compile) {
return {
open: function (data) {
var template = angular.element('<popup></popup>'),
compiled = $compile(template);
$document.find('body').append(compiled);
}
};
});
//closing bracket for alternative definition that allows minification
//}]);

Categories

Resources