How to make directive use the controller specified in directive attribute? - angularjs

So I have a directive:
<directive data="user" templateUrl="./user.html" controller="UserController"></directive>
I want that directive to use the controller specified in "controller" attribute, as you see above.
Is it possible with AngularJS directives? Or should I do it other way, maybe with components?
My code currently looks like this:
app.directive('directive', function() {
var controllerName = "UserController"; // i want that to dynamicaly come from attribute
// check if controller extists:
var services = [];
app['_invokeQueue'].forEach(function(value){
services[value[2][0]] = true;
});
if (!services[controllerName]) controllerName = false;
return {
scope: { 'data' : '=' },
link: function (scope) {
Object.assign(scope, scope.data);
},
templateUrl: function(element, attr) {
return attr.templateurl;
},
controller: controllerName
}
});

You can do following (not exactly what you ask - it creates bunch of nested scopes, but should be sufficient):
.directive('directive', () => {
scope: { 'data' : '=' },
template: (elem, attrs) => {
return '<div ng-controller="' + attrs.controller + ' as vm"><div ng-include="' + attrs.template + '"></div></div>';
}
});
<directive data="user" templateUrl="./user.html" controller="UserController"></directive>
you may use $templateCache directly instead of ng-include
if you need controller/template/... to be dynamic, you need to observe/watch + dom manipulation + recompile stuff

Okay, so after analysing Petr's answer I post the working code using nested divs:
app.directive('directive', function() {
return {
scope: { 'data' : '=' },
link: function (scope) {
// this makes your fields available as {{name}} instead of {{user.name}}:
Object.assign(scope, scope.data);
},
template: function(element, attrs) {
var controllerName = attrs.controller;
var controllerString = controllerName + ' as vm';
// check if controller extists:
var services = [];
app['_invokeQueue'].forEach(function(value){
services[value[2][0]] = true;
})
if (!services[controllerName]) {
return '<div ng-include="\'' + attrs.templateurl + '\'"></div>';
} else {
return '<div ng-controller="' + controllerString + '"><div ng-include="\'' + attrs.templateurl + '\'"></div></div>';
}
}
}
});

Related

Two Way binding with isolated scope not working in AngularJs

I have written a directive for simple dropdown. On click of one value, I am calling a function and updating the value.
If I log 'scope.$parent.selectedItem' , I am able to see the value. But that is not updated in parent controller.
This is Directive code
app.directive('buttonDropdown', [function() {
var templateString =
'<div class="dropdown-button">'+
'<button ng-click="toggleDropDown()" class="dropbtn">{{title}}</button>'+
'<div id="myDropdown" ng-if="showButonDropDown" class="dropdown-content">'+
'<a ng-repeat="item in dropdownItems" ng-click="selectItem(item)">{{item.name}}</a>'+
'</div>'+
'</div>';
return {
restrict: 'EA',
scope: {
dropdownItems: "=",
selectedOption: '=',
title: '#'
},
template: templateString,
controller: function($scope,$rootScope,$timeout) {
$scope.selectedOption = {};
$scope.showButonDropDown = false;
$scope.toggleDropDown = function() {
$scope.showButonDropDown = !$scope.showButonDropDown;
};
$scope.$watch('dropdownItems', function(newVal,oldval){
if(newVal){
console.log(newVal);
}
});
$scope.selectItem = function(item){
console.log(item);
$scope.selectedOption = item;
}
},
link: function(scope, element) {
scope.dropdownItems = scope.dropdownItems || [];
window.onclick = function (event) {
if (!event.target.matches('.dropbtn')) {
scope.showButonDropDown = false;
}
console.log(scope.$parent);
}
}
}
}]);
This is my HTML
<button-dropdown title="Refer a Friend" dropdown-items='dropDownList' selected-option='selectedItem'></button-dropdown>
This is my controller code
$scope.$watch('selectedItem',function(newVal,oldVal){
if(newVal){
console.log("*** New Val** ");
console.log(newVal);
}
});
I didn't understand one thing.. If I print 'scope.$parent.selectedItem', I could see the value. but it is not updating in the controller. Didn't understand, what am I missing. Can anyone help on this. Thanks in advance.
Try in this way
1. Try to $emit the scope variable in directive.
2. get that in controller by using $on.
Directive:
$scope.$emit('selectedItem',scopeVariable);
Controller:
$scope.$on('selectedItem',function(event,newVal){
if(newVal){
// logic here
}
});

updating controller scope from directive

I have a problem updating a scope variable from a directive. my code looks something like this (simplified):
CONTROLLER
function newProjectController($scope) {
$scope.imageUploaded=false;
}
The directive should update the imageUploaded variable, which is then used to apply classes using ng-class function.
DIRECTIVE
.directive('ngThumb', ['$window', 'apiService', function($window, apiService) {
var helper = {
support: !!($window.FileReader && $window.CanvasRenderingContext2D),
isFile: function(item) {
return angular.isObject(item) && item instanceof $window.File;
},
isImage: function(file) {
var type = '|' + file.type.slice(file.type.lastIndexOf('/') + 1) + '|';
return '|jpg|png|jpeg|bmp|gif|'.indexOf(type) !== -1;
}
};
return {
restrict: 'A',
template: '<canvas/>',
bindToController: true,
link: function(scope, element, attributes) {
scope.$apply(function () {
scope.imageUploaded=true;
});
}
This is not happening and I don't know why...

Testing angularjs directive generating html code or not

I have a directive something like this which I want to test,
.directive('gridHeader', function() {
return {
restrict: 'A',
replace: true,
scope: false,
compile: function(tEle, tAttrs, transcludeFn) {
var h3 = tEle.find('h3');
var temp = h3.html();
temp = temp.replace('xxxx', tAttrs.gridHeader);
h3.html(temp);
},
template: '<div class="grid-header">' +
'<h3>Showing {{grid.data.records}} xxxx</h3>' +
'<div class="pull-right">' +
'</div>' +
'<div class="clearfix"></div>' +
'</div>'
}
})
I have tried something like this, but its not working
it('directive: should generate all required html elements', function() {
var items = angular.element('div.grid-header');
console.log(items.length); //always returning 0
expect(true).toBe(true);
});
I'm not a fan of checking directive HTML in unit tests (I feel that's better suited in e2e) however something like this should suffice...
Save a reference to the compiled element
var element;
...
inject(function($rootScope, $compile) {
var scope = $rootScope.$new();
scope.grid = { data: { records: 'records' } };
element = $compile('<div grid-header="foo"></div>')(scope);
$rootScope.$digest();
});
Then you can run tests against it
it('whatever', function() {
expect(element.hasClass('grid-header')).toBeTruthy();
expect(element.find('h3').text()).toEqual('Showing records foo');
});

Updating directive when scope variable changes in ng-grid?

I have a requirement for consuming an array of objects within ng-grid that are custom styled to look like a tag (similar to tags on here).
I have taken the approach of using a cellTemplate and have created a custom directive for this.
What is happening is when you sort, other columns change but the 'Tags' column does not, it stays as is, like the directive isn't getting updated.
Here is my directive:
app.directive('tag', function($compile){
return {
restrict: 'EA',
link: function(scope, element, attrs) {
attrs.$observe('tags', function(value) {
var array = JSON.parse(value);
var newHtml = '<ul>';
for(var i=0;i<array.length;i++)
{
newHtml += '<li>' + array[i].text + '</li>';
}
newHtml += '</ul>';
var e = $compile(newHtml)(scope);
element.replaceWith(e);
});
}
}
});
Here is a plunker: http://plnkr.co/edit/OxeUPaLLWtiCnvmgehnl
Thanks
Don't know if this fulfils all your requirements but you can change your tag directive to this:
app.directive('tag', function($compile){
var ddo = {
restrict: 'EA',
template: '<div><ul><li ng-repeat="tag in tags">{{tag}}</li></div>',
scope: { tags: "=tags" }
};
return ddo;
});
Or if you want to keep your code, just change the DOM first and compile it afterwards:
app.directive('tag', function($compile){
var ddo = {
restrict: 'EA',
scope: { tags: "#tags" },
link: function(scope, element, attrs) {
attrs.$observe('tags', function(value) {
var array = JSON.parse(value);
var newHtml = '<ul>';
for(var i=0;i<array.length;i++)
{
newHtml += '<li>' + array[i].text + '</li>';
}
newHtml += '</ul>';
element.html(newHtml);
$compile(newHtml)(scope);
});
}
};
return ddo;
});
Edit: Also, if all you want to do is change the layout, nothing stops you from calling ng-repeat in your cellTemplate:
cellTemplate: '<ul><li ng-repeat="val in row.entity.arr">{{val}}</li></ul>'}

Add dynamic transcluded content

I'm trying to accomplish the following scenario:
I've got the following directive:
return {
restrict: "AE",
replace: true,
transclude: true,
scope: {
chartData: "=",
groupId: "#",
groupName: "#"
},
template: "<div class=\"flex\" ng-transclude></div>",
compile: function(){
var charts = [];
return function (scope, element, attributes, controller, transcludeFn) {
_.forEach(scope.chartData, function (data) {
var id = "gauge-" + scope.groupId + '-' + data.name;
scope[id] = data.value; // gauge directive can now access this value
element.append("<div class=\"flex-spacer flex-column\"><div bv-gauge chart-title=\"" + data.name + "\" data-value=\"" + scope[id] + "\" series-name=\"" + scope.groupName + "\"></div></div>");
charts.push(id);
});
$compile(element.contents())(scope);
scope.$watch("chartData", function (newVal) {
_.forEach(scope.chartData, function (data) {
var id = "gauge-" + scope.groupName + '-' + data.name;
scope.$broadcast("chart:update", newVal);
});
});
}
}
};
As you can see I'm adding dynamically some content to the directive's element. It works nice but there is one issue; the isolated scope of inner directives ( i.e. bv-gauge) can see the outer scope only not the scope of bv-gauge-parent; As far as I know this is the default behaviour, which we can override by using the transclude function.
However, it seems that angular doesn't consider the dynamically added content as part of the transcluded content. So when I do the following:
transcludeFn(scope, function(clone, scope){
// clone doesn't have the content we added earlier
});
Is there any way to associate the scope of the transcluded content with the parent directive?

Resources