Error: No controller in directive - angularjs

Folks,
I am an AngularJS newbie and I am trying to create a basic HTML hierarchy bound to a model hierarchy using Angular. My top-level module looks like this:
angular.module('fooApp', ['ngResource', 'myList']) which is declared as ng-app="fooApp" at the root div for my page.
I then have HTML that looks like this in my page:
<my-item-list/>
I have a second module that looks like this:
var myList = angular.module('myList', []);
myList.directive('myItemList', function factory() {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/assets/partials/my-item-list.html',
replace: false,
transclude: false,
restrict: 'E',
scope: false,
controller: function($scope, $element, $attrs, $transclude) {
// No Op
$scope.foo = 'bar';
},
compile: function compile(tElement, tAttrs, transclude) {
return {
pre: function preLink(scope, iElement, iAttrs, controller) {
// No Op
},
post: function postLink(scope, iElement, iAttrs, controller) {
// No OP
}
}
},
link: function postLink(scope, iElement, iAttrs) {
// No Op
}
};
return directiveDefinitionObject;
});
The partial HTML template just contains a table and an ng-repeat on a tag. The template renders just fine, but my JavaScript console contains :
Error: No controller: myItemList
at Error (<anonymous>)
at h (http://localhost:9000/assets/javascripts/angular.min.js:41:458)
at i (http://localhost:9000/assets/javascripts/angular.min.js:43:415)
at http://localhost:9000/assets/javascripts/angular.min.js:48:191
at http://localhost:9000/assets/javascripts/angular.min.js:94:307
at h (http://localhost:9000/assets/javascripts/angular.min.js:78:33)
at http://localhost:9000/assets/javascripts/angular.min.js:78:266
at Object.e.$eval (http://localhost:9000/assets/javascripts/angular.min.js:88:347)
at Object.e.$digest (http://localhost:9000/assets/javascripts/angular.min.js:86:198)
at Object.e.$apply (http://localhost:9000/assets/javascripts/angular.min.js:88:506) <my-item-list>
I intend for the nesting to increase (e.g. replace the in the HTML partial template with another directive and I really need to figure out what I'm doing wrong. It feels like I'm doing some kind of newbie mistake. Whenever I add a 'require' attribute to my directive definition, I get the same "no controller" error message, but with the name of whatever I required (even if I try and require ngRepeat or the page's master controller).

It looks like a bug in Angular: http://github.com/angular/angular.js/issues/1903
Does your directive really need a controller? I would say 9 times out of 10 directives do not need their own controller (mostly they just need link).
When you remove controller from the directive, the error appears to go away.
Edit: More specifically, it appears that the problem arises when you combine controller and compile. Any other combination (controller+link, compile+link, or any by themselves) works fine.

Found a solution for this...
Directives should be nested, example,
<directive-parent>
<directive-child></directive-child>
</directive-parent>
Check it our here:
https://groups.google.com/d/msg/angular/SRKL0wZtSew/6sToIKLkRHQJ

Related

How to use an AngularjS View (.html file) without using it's tags

Inside a big AngularJS application I have a new HTML template file and a controller for it, and I'd like to build a layout the designer gave me using this temporary view, since I'd like to be able to call some data from the $scope object.
I also created a new route for it so that I have a clean working space.
But I don't want to include it in the main index.html file, like so:
<my-new-template></my-new-template>
I'd just like to start using it without having to include this HTML element anywhere, is this possible? This is the controller so far:
.directive('portfolio', [
function () {
return {
templateUrl: "views/temporary-view.html",
scope: {
data: "="
},
link: function (scope, element, attrs, ctrl) {
scope.stuff = 'stuff';
}
};
}])
The view:
<nav class="portfolio-view">
{{stuff}}
</nav>
Thanks for helping a noob like me! :)
In your directive, you can change the restrict option to change how the directive is called in the HTML. There are 4 options for this. I found this in the AngularJS documentation for directives:
restrict
String of subset of EACM which restricts the directive to a specific directive declaration style. If omitted, the defaults (elements and attributes) are used.
E - Element name (default): <my-directive></my-directive>
A - Attribute (default): <div my-directive="exp"></div>
C - Class: <div class="my-directive: exp;"></div>
M - Comment: <!-- directive: my-directive exp -->
By default, it uses EA, so as an Element (the way you do not want to call it in HTML) or an attribute.
If you wish to change it to, say, class for example, then change your directive definition to:
.directive('portfolio', [
function () {
return {
restrict: 'C',
templateUrl: "views/temporary-view.html",
scope: {
data: "="
},
link: function (scope, element, attrs, ctrl) {
scope.stuff = 'stuff';
}
};
}])
and you can call it as so:
<div class="portfolio"></div>
I think this is what you mean, and I hope it helps!
So I just changed .directive to .controller and posted it alongside the other controllers in the app and not the directives... I guess I was confused with that!
.controller('PortfolioView', ["$scope",
function ($scope) {
$scope.stuff = 'stuff';
}])

AngularJS $parser not being called when dynamically adding the directive

So what i'm trying to achieve is to be able to add a directive that contains a parser through another directive.
When directly adding the parser directive on an html element it works completely fine. the parser directive i currently use:
.directive('parseTest', [
function () {
return {
restrict: 'A',
require: ['ngModel'],
link: {
post: function (scope, element, attributes, ctrls) {
var controller = ctrls[0];
controller.$parsers.unshift(function (value) {
var result = value.toLowerCase();
controller.$setViewValue(value);
controller.$render();
return result;
})
}
}
}
}
])
Now when i add this directive through another directive the parser never gets called weirdly enough. The directive that generated the parsetest directive:
.directive('generateTest', ['$compile',
function ($compile) {
return {
restrict: 'A',
compile: function (elem, attrs) {
elem.attr('parse-test', '');
elem.removeAttr('generate-test');
var linkFn = $compile(elem);
return function (scope, element, attr) {
linkFn(scope);
}
}
}
}
])
The following works fine:
<input class="form-control col-sm-6" ng-model="model.parsetest" parse-test/>
The following doesn't work (While the generated result html is the same)
<input class="form-control col-sm-6" ng-model="model.generateTest" generate-test />
So my question is how can i get the parser working when it is in a dynamicly added directive?
Note, i already tried the solution to a similar issue from this question, but that doesn't work for me.
EDIT: Here is a plnkr that demonstrates the issue, both fields have the parse-test directive applied to it that should make the value in the model lowercase, but it only works for the one that is not dynamically added as shown in the console logs.
So I've found the solution, so for anyone that stumbles on the same issue here it is.
The 2 small changes have to made to the directive that generates the directive that contains a parser or formatter.
First of set the priority of the directive to a number higher or equal as 1. Secondly put terminal on true. Those 2 settings seem to resolve the issue.
The problem probably lies in that the default execution of nested directives makes it so that the parser and formatters get inserted slightly to late which is why we need to make sure the directive gets generated first thing before anything else.
This is just an assumption of why it works tho, if anyone else has an explanation it would be great :)
As for the code, the directive that generates another directive should look something like:
directive('generateTest', ['$compile',
function ($compile) {
return {
restrict: 'A',
terminal: true,
priority: 1,
compile: function (elem, attrs) {
attrs.$set('parseTest', '');
attrs.$set('generateTest', undefined);
var linkFn = $compile(elem);
return function (scope, element, attr) {
linkFn(scope);
}
}
}
}
])

How to access parent's controller function from within a custom directive using *parent's* ControllerAs?

I'm in need of building a transformation directive that transforms custom directives into html.
Input like: <link text="hello world"></link>
should output to: <a class="someclass" ng-click="linkClicked('hello world')"></a>
linkClicked should be called on the parent controller of the directive.
It would have been very easy if I was the one responsible for the html holding the 'link' directive (using isolated scope), but I'm not. It's an as-is input and I have to figure a way to still do it.
There are countless examples on how to do similar bindings using the default scope of the directive, but I'm writing my controllers using John Papa's recommendations with controllerAs, but don't want to create another instance on the controller in the directive.
This is what I have reached so far:
(function () {
'use strict';
angular
.module('app')
.directive('link', link);
link.$inject = ['$compile'];
function link($compile) {
return {
restrict: 'E',
replace: true,
template: '<a class="someclass"></a>',
terminal: true,
priority: 1000,
link: function (scope, element, attributes) {
element.removeAttr('link'); // Remove the attribute to avoid indefinite loop.
element.attr('ng-click', 'linkClicked(\'' + attributes.text + '\')');
$compile(element)(scope);
},
};
}
})();
$scope.linkClicked = function(text){...} in the parent controller works.
element.attr('ng-click', 'abc.linkClicked(..)') in the directive (where the parent's controllerAs is abc) - also works.
The problem is I don't know which controller will use my directive and can't hard-code the 'abc' name in it.
What do you suggest I should be doing?
It's difficult to understand from your question all the constraints that you are facing, but if the only HTML you get is:
<link text="some text">
and you need to generate a call to some function, then the function must either be:
assumed by the directive, or
conveyed to the directive
#1 is problematic because the user of the directive now needs to understand its internals. Still, it's possible if you assume that a function name is linkClicked (or whatever you want to call it), and the user of your directive would have to take special care to alias the function he really needs (could be done with "controllerAs" as well):
<div ng-controller="FooCtrl as foo" ng-init="linkClicked = foo.actualFunctionOfFoo">
...
<link text="some text">
...
</div>
app.directive("link", function($compile){
return {
transclude: "element", // remove the entire element
link: function(scope, element, attrs, ctrl){
var template = '<a class="someclass" ng-click="linkClicked(\'' +
attrs.text +
'\')">link</a>';
$compile(template)(scope, function(clone){
element.after(clone);
});
}
};
});
Demo
#2 is typically achieved via attributes, which isn't possible in your case. But you could also create a sort of "proxy" directive - let's call it onLinkClick - that could execute whatever expression you need:
<div ng-controller="FooCtrl as foo"
on-link-click="foo.actualFunctionOfFoo($data)">
...
<link text="some text">
...
</div>
The link directive now needs to require: "onLinkClick":
app.directive("link", function($compile){
return {
transclude: "element", // remove the entire element
scope: true,
require: "?^onLinkClick",
link: function(scope, element, attrs, ctrl){
if (!ctrl) return;
var template = '<a class="someclass" ng-click="localClick()">link</a>';
scope.localClick = function(){
ctrl.externalFn(attrs.text);
};
$compile(template)(scope, function(clone){
element.after(clone);
});
}
};
});
app.directive("onLinkClick", function($parse){
return {
restrict: "A",
controller: function($scope, $attrs){
var ctrl = this;
var expr = $parse($attrs.onLinkClick);
ctrl.externalFn = function(data){
expr($scope, {$data: data});
};
},
};
});
Demo
Notice that having a link directive would also execute on <link> inside <head>. So, make attempts to detect it and skip everything. For the demo purposes, I used a directive called blink to avoid this issue.

Why is the post link function executed before the transcluded child link functions?

The timing of (pre/post)link functions in AngularJS are well defined in the documentation
Pre-linking function
Executed before the child elements are linked. Not safe to do DOM
transformation since the compiler linking function will fail to locate
the correct elements for linking.
Post-linking function
Executed after the child elements are linked. It is safe to do DOM
transformation in the post-linking function.
and this blog post clearly illustrates this expected order.
But this order does not seem to apply when using ng-transclude and nested directives.
Here is an example for a dropright element (See the Plunkr)
<!-- index.html -->
<dropright>
<col1-item name="a">
<col2-item>1</col2-item>
<col2-item>2</col2-item>
</col1-item>
<col1-item name="b">
...
</col1-item>
</dropright>
// dropright-template.html
<div id="col1-el" ng-transclude></div>
<div id="col2-el">
<!-- Only angularJS will put elements in there -->
</div>
// col1-item-template.html
<p ng-transclude></p>
// col2-item-template.html
<div ng-transclude></div>
The dropright looks like
The directives write a log in the console when their link and controller functions are called.
It usually displays:
But sometimes (after few refreshes), the order is not as expected:
The dropright post-link function is executed before the post-link function of its children.
It may be because, in my particular case, I am calling the dropright controller in the children's directives (See the Plunkr)
angular.module('someApp', [])
.directive('dropright', function() {
return {
restrict: 'E',
transclude: 'true',
controller: function($scope, $element, $attrs) {
console.info('controller - dropright');
$scope.col1Tab = [];
$scope.col2Tab = [];
this.addCol1Item = function(el) {
console.log('(col1Tab pushed)');
$scope.col1Tab.push(el);
};
this.addCol2Item = function(el) {
console.log('(col2Tab pushed)');
$scope.col2Tab.push(el);
};
},
link: {
post: function(scope, element, attrs) {
console.info('post-link - dropright');
// Here, I want to move some of the elements of #col1-el
// into #col2-el
}
},
templateUrl: 'dropright-tpl.html'
};
})
.directive('col1Item', function($interpolate) {
return {
require: '^dropright',
restrict: 'E',
transclude: true,
controller: function() {
console.log('-- controller - col1Item');
},
link: {
post: function(scope, element, attrs, droprightCtrl) {
console.log('-- post-link - col1Item');
droprightCtrl.addCol1Item(element.children()[0]);
}
},
templateUrl: 'col1-tpl.html'
};
})
.directive('col2Item', function() {
var directiveDefinitionObject = {
require: '^dropright',
restrict: 'E',
transclude: true,
controller: function() {
console.log('---- controller - col2Item');
},
link: {
post: function(scope, element, attrs, droprightCtrl) {
console.log('---- post-link - col2Item');
droprightCtrl.addCol2Item(element.children()[0]);
}
},
templateUrl: 'col2-tpl.html'
};
return directiveDefinitionObject;
});
Is there any clean way to execute the link function of a directive after all the link functions of its children while using transclusion?
This is my theory - its not the transclude aspect that is causing the sequence issue but rather the template being a templateUrl. The template needs to be resolved before the post link function get to act on it - hence we say post link function is safe to do DOM manipulation. While we are getting 304s for all the 3 templates - we do have to read them and it ultimately resolves the template promise.
I created a plunker with template instead of templateUrl to prove the corollary. I have hot refresh/plunker Stop/Run many times but I always get link - dropright at the end.
Plunker with template instead of templateUrl
I don't pretend to understand the compile.js code fully. However it does appear that in
compileTemplateUrl function $http.success() resolves the template and then on success the applyDirectivesToNode function is called passing in postLinkFn.
https://github.com/angular/angular.js/blob/master/src/ng/compile.js
This may be just weirdness with Plunker. I tried copying the files to my local IIS, and was not able to replicate the issue.

Why AngularJS directive compiling not support nested directives

In my understanding, $compile should be able to support nested directive compilation/link, but we came across an issue that the compilation/link is incomplete - only the outermost directive got rendered into DOM, and the issue only reproduced when both below conditions are true:
The inner directive template is loaded via templateUrl (e.g. async manner)
The compilation is triggered outside Angular context.
I wrote a jsfiddler to demo it, part code listed below, for complete case http://jsfiddle.net/pattern/7KjWP/
myApp.directive('plTest', function($compile){
return {
restrict :'A',
scope: {},
replace: true,
template: '<div>plTest rendered </div>',
link: function (scope, element){
$('#button1').on('click', function(){
var ele;
ele = $compile('<div pl-shared />')(scope);
console.log('plTest compile got : '+ ele[0].outerHTML);
// scope.$apply();
element.append(ele);
});
}
};
});
myApp.directive('plShared', function($compile, $timeout){
return {
restrict: 'A',
scope: {},
replace: true,
link: function (scope, element){
// comment out below line to make render success
//$timeout(function(){});
var el = $compile('<div pl-item></div>')(scope);
console.log('plShared compile got:' + el[0].outerHTML);
element.append(el);
}
};
});
myApp.directive('plItem', function($timeout){
return {
restrict: 'A',
scope:{},
template:'<div>plItem rendered <div pl-avatar/></div>',
link: function(scope){
}
};
});
myApp.directive('plAvatar', function(){
return {
restrict: 'A',
scope: {}
, templateUrl: 'avatar.html'
// ,template: 'content of avatar.html <div pl-image></div>'
};
});
Interestingly, i can workaround the issue by either calling scope.$apply() somewhere after compile() call (line 27)
or adding $timeout(function(){}) call into the link func of one of inner directive (line 41). Is this a defect or by design?
$(foo).on(bar, handler) is a jQuery event, which means AngularJS does not know the specifics of it, and will not (can not) run an apply-digest cycle after it to process all the bindings.
scope.$apply was made for this, and as you rightly say, fixes it. The rule of thumb is: if you implement UI functionality in an AngularJS application using other libraries (specifically: outside of apply-digest cycles), you must call scope.$apply yourself.
HTH!
After element.append(el), try to compile again as you have just modified the DOM.
You could try something such as $compile(element)(scope); or $compile(element.contents())(scope);.
As said before me, I would also change the event handler as follows :
$('#button1').on('click', function(){
scope.$apply( function(){
//blablalba
});
});
Also, justa piece of advice in case you would want to minify your code, I would declare the compile dependency using the following syntax :
.directive('directiveName',['$service1',''$service2,...,'$compile', function($service1, $service2,...,$compile){
//blablabla
}]}

Resources