Angularjs controller required by directive can not be found in transclude content - angularjs

Recently I use angular to develop a directive, there is an directive which like ng-repeat to generate some records, I used transclude to implement it. but it raise an error that "Controller 'aArea', required by directive 'bSpan', can't be found!".
1. ModuleA code
var moduleA = angular.module("moduleA", []);
moduleA.directive("aArea", function () {
return {
restrict: 'E',
transclude:'element',
scope: {
amount:"="
},
template: '<div id=\"cc\" ng-transclude></div>',
controller: function ($scope,$element,$attrs) {
this.getData = function (data) {
return data + " is ok";
}
},
compile: function (tElement, attrs, linker) {
var parentElement = tElement.parent();
return {
pre: function () {
},
post: function (scope) {
linker(scope.$parent,function (clone,scope) {
parentElement.append(clone);
});
linker(scope.$parent, function (clone, scope) {
parentElement.append(clone);
});
linker(scope.$parent, function (clone, scope) {
parentElement.append(clone);
});
}
}
}
}
});
moduleA.directive("bSpan", function () {
return {
restrict: 'E',
scope: {
data: "=",
},
template: '<span style=\"background-color:gray;color:orange\">{{data}}</span>',
require: "^aArea",
link: function ($scope, $element, $attrs, controller) {
var data = "abc";
}
}
});
2. ModuleB COde
var moduleB = angular.module("moduleB", []);
moduleB.directive("myItem", function () {
return {
restrict: 'E',
scope: {
item: "=",
itemTemplate: '='
},
priority: 1000,
terminal:false,
template: '<ng-include src=\"itemTemplate\"/>',
controller: function ($scope, $element, $attrs) {
var data = "";
}
}
})
3. ModuleC Code
var moduleC = angular.module("moduleC", ["moduleA", "moduleB"]);
moduleC.controller("Ctr", function ($scope) {
$scope.item = {};
$scope.item.dataAmount = 1000;
$scope.item.templateUrl = "item-template.html";
})
4. Html Code
<body>
<div ng-app="moduleC">
<div ng-controller="Ctr">
<a-area>
<my-item item="item" item-template="item.templateUrl"></my-item>
</a-area>
</div>
</div>
</body>
5. template code
<div>
<span style="display:block">hello every one</span>
<b-span data="item.dataAmount"></b-span>
</div>

You should not use the transclude function (that you called linker) of the compile function - it is deprecated.
From $compile documentation:
Note: The transclude function that is passed to the compile function is deprecated, as it e.g. does not know about the right outer scope. Please use the transclude function that is passed to the link function instead.
Following this guidance (and a few other minor changes for the better), change the aArea directive as follows:
compile: function(tElement, tAttrs) {
// don't use the template element
//var parentElement = tElement.parent();
return function(scope, element, attrs, ctrls, transclude) {
transclude(function(clone, scope) {
element.after(clone);
});
transclude(function(clone, scope) {
element.after(clone);
});
transclude(function(clone, scope) {
element.after(clone);
});
};
}
In fact, you don't even need the transclude function at all and you don't need to do transclude: "element". You could just change to transclude: true and use <div ng-transclude> 3 times in the template.

Related

Angular directive calling another directive not working

In this plunk I have directive dir1 calling a method in directive dir2 as described here.
The problem is that the control object (scope.dir2Ctl) is empty in dir1 and I get TypeError: scope.dir2Ctl.call2 is not a function. Any ideas how to fix this?
HTML
<body ng-app="myModule" ng-controller="ctl">
<dir1 x1="1"></dir1>
</body>
Javascript
angular.module("myModule", [])
.controller('ctl', function($scope) {})
.directive('dir1', function ($timeout) {
return {
restrict: 'EA',
scope: {
x1: '='
},
template: '<p>x2 should be 2 = {{x2}} </p>' +
'<dir2 control="dir2Ctl"></dir2>',
link: function (scope, element, attrs) {
scope.dir2Ctl = {};
$timeout(function(){
console.log(scope.dir2Ctl)
scope.x2 = scope.dir2Ctl.call2();
},1000);
}
}
})
.directive('dir2', function () {
return {
restrict: 'EA',
scope: {
control: '='
},
template: '<p>some text in dir2</p>',
link: function (scope, element, attrs) {
scope.control = scope.control || {};
scope.control.call2 = function(){
return 2;
};
}
}
});

two-way data binding inside attribute directive that requires element directive

I have an element directive (e-dir) and an attribute directive (a-dir) on the same element:
<e-dir a-dir attr="msg"></e-dir>
I pass msg into e-dir's isolate scope via the attr attribute:
app.directive('eDir', function eDir($timeout) {
return {
restrict: 'E',
scope: {
attr: '='
}
};
});
In this way, msg is bound (two-ways) with $scope.attr (in EDirCtrl) or scope.attr (in e-dir's link function).
Is there a simple way I can achieve the same two-way data-binding inside a-dir's directive? Or would you recommend another, simpler approach?
The closest thing I've been able to come up with is to set eDirCtrl.attr = $scope.attr; inside e-dir's controller (EDirCtrl):
app.directive('eDir', function eDir($timeout) {
return {
restrict: 'E',
scope: {
attr: '='
},
controller: function EDirCtrl($scope) {
var eDirCtrl = this;
eDirCtrl.attr = $scope.attr;
},
controllerAs: 'eDirCtrl'
};
});
Then, have a-dir require e-dir, and access attr via e-dir's controller (eDirCtrl.attr):
app.directive('aDir', function aDir($timeout) {
return {
restrict: 'A',
require: 'eDir',
link: linkFn
};
function linkFn(scope, element, attrs, eDirCtrl) {
eDirCtrl.attr = 'eDirCtrl.attr';
}
});
But, it's not bound two-ways. As you can see this code snippet:
var app = angular.module('app', []);
app.controller('Ctrl', function Ctrl($scope) {
$scope.msg = 'initial message';
})
app.directive('eDir', function eDir($timeout) {
return {
restrict: 'E',
scope: {
attr: '='
},
template: '<div>$scope.attr: {{attr}}</div>'+
'<div>eDirCtrl.attr: {{eDirCtrl.attr}}</div>',
controller: function EDirCtrl($scope) {
var eDirCtrl = this;
eDirCtrl.attr = $scope.attr;
$timeout(function() {
$scope.attr = 'changing $scope.attr also changes msg';
}, 2000);
},
controllerAs: 'eDirCtrl'
};
});
app.directive('aDir', function aDir($timeout) {
return {
restrict: 'A',
require: 'eDir',
link: linkFn
};
function linkFn(scope, element, attrs, eDirCtrl) {
$timeout(function() {
eDirCtrl.attr = 'changing eDirCtrl.attr does not effect $scope.attr or msg';
}, 4000);
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="Ctrl">
msg: <input type="text" ng-model="msg"><br>
<e-dir a-dir attr="msg"></e-dir>
</div>
The reason the two-way binding isn't working is that attr is being bound to a string rather than an object. In JavaScript, primitives (booleans, numbers, strings) are immutable, so when you change one, the previous instance is discarded and a new one is used. This breaks Angular's two-way binding and any changes to scope.msg are not propagated through attr into the directive.
You can get this to work as expected by setting msg on an object e.g. scope.test.msg and binding attr to test (the object) rather than msg (a string).
I've updated your code snippet to do this:
var app = angular.module('app', []);
app.controller('Ctrl', function Ctrl($scope) {
$scope.test = {msg : 'initial message'};
})
app.directive('eDir', function eDir($timeout) {
return {
restrict: 'E',
scope: {
attr: '='
},
template: '<div>$scope.attr: {{attr.msg}}</div>'+
'<div>eDirCtrl.attr: {{eDirCtrl.attr.msg}}</div>',
controller: function EDirCtrl($scope) {
var eDirCtrl = this;
eDirCtrl.attr = $scope.attr;
$timeout(function() {
$scope.attr.msg = 'changing $scope.attr also changes msg';
}, 2000);
},
controllerAs: 'eDirCtrl'
};
});
app.directive('aDir', function aDir($timeout) {
return {
restrict: 'A',
require: 'eDir',
link: linkFn
};
function linkFn(scope, element, attrs, eDirCtrl) {
$timeout(function() {
eDirCtrl.attr.msg = 'changing eDirCtrl.attr does not effect $scope.attr or msg';
}, 4000);
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="Ctrl">
msg: <input type="text" ng-model="test.msg"><br>
<e-dir a-dir attr="test"></e-dir>
</div>

Dynamic controller

I have two nested directive and a few controllers and I want inject controller to second controller.
When I bind action to some button it work but list don't show up, some one know why?
Dynamic Controller directive
.directive("dynamicController", ["$compile", function($compile) {
return {
restrict: "A",
scope: {
dynamicController: "#"
},
compile: function(tElement, tAttrs) {
return {
pre: function preLink(scope, iElement, iAttrs, controller) {
iElement.attr("ng-controller", scope.dynamicController);
iElement.removeAttr("dynamic-controller");
$compile(iElement)(scope);
}
}
}
}
}])
V1: http://codepen.io/anon/pen/LVeaWo
V2: http://codepen.io/anon/pen/EjoJVx
[ EDIT ]
I almost do it but it's one more problem.
I have two directive:
.directive("wrapDirective", function() {
return {
restrict: "A",
template: "<div dynamic-controller=\"Ctr1\">" +
"<button ng-click='action()'>Click</button>" +
"<ul>" +
"<li ng-repeat=\"item in list\">{{item}}</li>" +
"</ul>" +
"</div>",
scope: {
controller: "#wrapDirective"
}
}
})
and
.directive("dynamicController", function($compile) {
return {
restrict: "A",
scope: true,
controller: "#",
name: "dynamicController"
}
})
The problem is this line <div dynamic-controller=\"Ctr1\"> in warpDirective
I can't do something like this <div dynamic-controller=\"{{controller}}\">
CodePen with both cases: http://codepen.io/anon/pen/EjoJXV
You should use require and link to get the controllers of parent directives.
See Creating Directives that Communicate.
.directive('myDirective', function() {
return {
require: '^ngController', // <-- define parent directive
restrict: 'E',
scope: {
title: '#'
},
link: function(scope, element, attrs, ctrl) { // <-- get the controller via the link function
ctrl.doSomething();
}
};
The reason behind your code is not working is, {{}} interpolation value is not evaluated in you pre link function. So by compiling ng-controller with not value in it is throwing an error. You should use iAttrs.$observe as you are evaluating expression inside {{}}.
Code
var dynamicControllerObserver = iAttrs.$observe('dynamicController', function(newVal, oldVal) {
wrapElement.attr("ng-controller", scope.dynamicController);
wrapElement.append(iElement.html());
console.log(wrapElement)
iElement.html("");
console.log(iElement)
iElement.append(wrapElement);
$compile(wrapElement)(scope);
dynamicControllerObserver(); //destruct observe
})
Working Codepen
I did it, really helpful was this post: Dynamic NG-Controller Name
I modified it to my needs:
.directive('dynamicCtrl', ['$compile', '$parse', function($compile, $parse) {
return {
restrict: 'A',
terminal: true,
scope: {
dynamicCtrl: "#"
},
link: function(scope, elem, attr) {
var initContent = elem.html();
var varName = getName(elem.attr('dynamic-ctrl'));
update();
scope.$watch("dynamicCtrl", function() {
update();
})
function update() {
var wrapper = angular.element("<div></div>");
wrapper.append(initContent);
var name = $parse(varName)(scope.$parent);
wrapper.attr('ng-controller', name);
elem.empty();
elem.append(wrapper);
$compile(wrapper)(scope);
}
function getName(attr) {
var startIndex = attr.lastIndexOf("{") + 1,
endIndex = attr.indexOf("}");
return attr.substring(startIndex, endIndex);
}
}
};
}])
http://codepen.io/anon/pen/xGYyqr

AngularJS - cannot access directive controller in another directive

I have two angularjs directives (extWindow and taskBar) and want to inject taskBar's controller into extWindow in order to access it's scope. Because they don't share the same scope I used
require : '^$directive'
syntax to include it.
Doing so I could get rid of the error 'Controller 'taskBar', required by directive 'extWindow', can't be found!' but TaskBarCtrl is still undefined in link(..) method of the extWindow directive.
Any suggestions how to fix it?
var mod = angular.module('ui', [])
.directive('taskBar', function() {
var link = function(scope, el, attrs) {
$(el).css('display', 'block');
$(scope.titles).each(function(i,t) {
el.append('<span>' + t + '</span>')
});
};
return {
scope: {},
restrict : 'E',
controller: function($scope, $element, $attrs) {
$scope.titles = [];
this.addTitle = function(title) {
$scope.titles.push(w);
};
this.removeTitle = function(title) {
$scope.titles = jQuery.grep(function(n,i) {
return title != n;
});
}
},
link: link
};
}).directive('extWindow', function() {
return {
scope: {},
require: '^?taskBar',
restrict: 'E',
transclude: true,
template: '<div class="ui-window">\
<div class="ui-window-header"><span>{{windowTitle}}</span><div class="ui-window-close" ng-click="close()">X</div></div>\
<div class="ui-window-content" ng-transclude></div>\
</div>',
link: function(scope, element, attrs, taskBarCtrl) {
scope.windowTitle = attrs['windowTitle'];
scope.close = function() {
$(element).css('display', 'none');
}
//taskBarCtrl is not recognized!!!
taskBarCtrl.addTitle(scope.windowTitle);
}
}
});
http://jsfiddle.net/wa9fs2nm/
Thank you.
golbie.
If you have a controller for your parent directive and you need something like.
this.scope = $scope;
this.attrs = $attrs;
And in your in you link function for the child you need something like
var Ctrl = ctrl || scope.$parent.tBarCtrl;
Here's a Plunker

How to use require option in directives

In documentation I can read next for the require option:
When a directive uses this option, $compile will throw an error
unless the specified controller is found. The ^ prefix means that this
directive searches for the controller on its parents (without the ^
prefix, the directive would look for the controller on just its own
element).
So I try to use it:
<div ng-sparkline></div>
app.directive('ngCity', function() {
return {
controller: function($scope) {}
}
});
app.directive('ngSparkline', function() {
return {
restrict: 'A',
require: '^ngCity',
scope: {},
template: '<div class="sparkline"><h4>Weather </h4></div>',
link: function(scope, iElement, iAttrs) {
// get weather details
}
}
});
But I have an error if my html have not ng-city attribute, so if I need controller of another directive - need to add exactly same attribute in html, but why (<div ng-sparkline ng-city="San Francisco"></div>)? And it looks on another directive's controller with this name (directive!!!) but not at controller with this name, is that true? Thanks. Just want to make it clear
With require you can get the controller of another (cooperating) directive. The controller in Angular is not semantically a function, but an object constructor, i.e. called essentially as var c = new Controller() (this is a simplification for the sake of clarity). Since the controller is an object, it can have properties and methods. By requiring the controller of another directive, you gain access to those properties/methods. Modifying your example to demonstrate:
app.directive('ngCity', function() {
return {
controller: function($scope) {
this.doSomething = function() {
...
};
}
}
});
app.directive('ngSparkline', function() {
return {
restrict: 'A',
require: '^ngCity',
scope: {},
template: '<div class="sparkline"><h4>Weather </h4></div>',
link: function(scope, iElement, iAttrs, ngCityController) {
// use the controller, e.g.
ngCityController.doSomething();
}
}
});
In your case, the city would be a property of the controller of the ngCity directive, exposed as a property. It will be read by the ngSparkline to know for which city the graph is about.
<b> added directives.js</b>
<code>
app.directive('ngSparkline', function () {
return {
restrict: 'A',
require: '^ngCity',
scope: {
ngCity: '#'
},
templateUrl: '/scripts/templates/tpl.html',
controller: ['$scope', '$http', function ($scope, $http) {
var url = "https://api.openweathermap.org/data/2.5/forecast/daily?mode=json&units=imperial&cnt=7&callback=JSON_CALLBACK&q=";
console.log(url + $scope.ngCity);
$scope.showTemp = function () {
$scope.getTemp($scope.ngCity);
};
$scope.getTemp = function (city) {
$http({
method: 'JSONP',
url: url + city
}).success(function(data) {
var weather = [];
angular.forEach(data.list, function(value){
weather.push(value);
});
$scope.weather = weather;
});
}
}],
link: function (scope, iElement, iAttrs, ctrl) {
scope.getTemp(iAttrs.ngCity);
scope.$watch('weather', function (newVal) {
if (newVal) {
var highs = [];
angular.forEach(scope.weather, function (value) {
highs.push(value.temp.max);
});
//chartGraph(iElement, highs, iAttrs);
}
});
}
}
}).directive('ngCity', function () {
return {
controller: function ($scope) {
//console.log("hello");
}
}
});
</code>
<b> and added tpl.htm</b>
<code>
<div class="sparkline">
<input type="text" data-ng-model="ngCity">
<button ng-click="showTemp()" class="btn1">Check {{ngCity}}</button>
<div style="color:#2743EF">{{weather}} ÂșC</div>
<div class="graph"></div>
</div>
</code>

Resources