ng-init not working inside link function of directive? - angularjs

According to this Plunkr, using ng-init in a directive won't execute a function assigned to scope inside link, but it will if I do it in controller.
Can anyone explain this?
app.js
var app = angular.module('app', []);
app.directive('linkDir', function(){
return {
restrict: 'E',
scope: true,
template: 'Link: <p ng-init="initLink()">{{ linkmsg }}</p>',
link: function(scope){
scope.initLink = function(){
scope.linkmsg = 'link function executed'
}
}
};
});
app.directive('controllerDir', function(){
return {
restrict: 'E',
scope: true,
template: 'Controller: <p ng-init="initController()">{{ controllermsg }}</p>',
controller: function($scope){
$scope.initController = function(){
$scope.controllermsg = 'controller function executed';
}
}
};
});
HTML
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.1/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body ng-app="app">
<link-dir></link-dir>
<controller-dir></controller-dir>
</body>
</html>

This has to due with how Angular's directives work. The linkage between the scope and the DOM element is a little bit complex to explain, but making a long story short, if you use controllers, the scope will be immediately created with the initController property, while if you use the link attribute, the scope will only be populated after it as been linked to the DOM element (this means initLink will be undefined during the ng-init).
In order to avoid these problems, don't use the ngInit directive, as stated on Angular's documentation:
The only appropriate use of ngInit is for aliasing special properties
of ngRepeat, as seen in the demo below. Besides this case, you should
use controllers rather than ngInit to initialize values on a scope.
Stick with Controllers if you need to init properties on the scope.

With newer versions of Angular you can work around this by adding an ng-if to the element the ng-init is on so it gets evaluated when the function is available:
template: 'Link: <p ng-if="initLink" ng-init="initLink()">{{ linkmsg }}</p>',
http://plnkr.co/edit/qGjPZ4P3OjIn8rU2cufQ?p=preview

Related

How to retrieve custom property in isolate scope?

I've read quite a lot examples on how to do binding in an isolate scope but not how to retrieve scope custom properties in template.
I think the fix must be very simple but I just don't figure out what's wrong here
<!doctype html>
<html ng-app="myApp">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0-rc.3/angular.js"></script>
</head>
<body>
<div my-directive>
</div>
<script>
angular.module('myApp', [])
.directive('myDirective', function() {
return {
restrict: 'A',
scope: {
data.myProperty:"1234"
},
template:'Inside myDirective, isolate scope: {{ data.myProperty }}'
};
})
</script>
</body>
</html>
Somehow, data.myProperty couldn't be reached.
You can't directly use and access bounded properties in bindings like you were doing data.myProperty:"1234". It will eventually result in error.
You have to pass custom property via attribute of your directive. Over here you can consider adding custom-data attribute, add mention the scope property name over it. So it would be passed to isolated scope directive.
Controller
$scope.data = {
myProperty: 'My Property Value'
}
Html
<div my-directive custom-data="data">
</div>
directive
angular.module('myApp', [])
.directive('myDirective', function() {
return {
restrict: 'A',
scope: {
customData: '=data'
//without alias it will look like below.
//customData: '='
},
template:'Inside myDirective, isolate scope: {{ data.myProperty }}'
//without alias template would look like below
//template:'Inside myDirective, isolate scope: {{ customData.myProperty }}'
};
})
Note: It seems like you are using older unstable version. If possible update angular to latest angularjs 1.7.x version, to find more features and performant angularjs. After 1.5.x version, you could also use < inside binding (customData: '<data') to keep one way data binding flow. Just to not confuse you I used =(two way data binding) for demo.

My directive uses isolate scope, but still fetches data from the controller's scope

I have created two directives, one with the controller passed in, and the other with the require passed it.
What I am trying to do it to just check how directives use isolated scopes and utilize the data of its own scope.
But my out put is strange.
Please follow the code below,
<!DOCTYPE html>
<html lang="en" ng-app="MyApp">
<head>
<title>Recipe 02 example 01</title>
<script type="text/javascript" src="D:\Rahul Shivsharan\JavaScript-Framework\AngularJS\angular.js"></script>
<script type="text/javascript" src="D:\Rahul Shivsharan\JavaScript-Framework\jQuery\jquery-1.11.1.js"></script>
<script type="text/javascript" src="D:\Rahul Shivsharan\JavaScript-Framework\BootstrapCSS\bootstrap-3.2.0-dist\js\bootstrap.js"></script>
<link rel="stylesheet" href="D:\Rahul Shivsharan\JavaScript-Framework\BootstrapCSS\bootstrap-3.2.0-dist\css\bootstrap.css"></link>
<link rel="stylesheet" href="D:\Rahul Shivsharan\JavaScript-Framework\BootstrapCSS\bootstrap-3.2.0-dist\css\bootstrap-theme.css"></link>
<script type="text/javaScript">
angular.module("MyApp",[]);
(function(){
angular.module("MyApp").controller("myCtrl",myCtrl);
angular.module("MyApp").controller("StudentCtrl",StudentCtrl);
angular.module("MyApp").directive("parentClass",parentClass);
angular.module("MyApp").directive("childClass",childClass);
function parentClass(){
var obj = {
restrict : 'EA',
controller : "StudentCtrl"
}
return obj;
};
function childClass(){
var obj = {
restrict : 'EA',
require : "^parentClass",
scope : {
array : '='
},
template : "<ol><li ng-repeat='student in array'>{{student}}</li></ol>",
link : function(scope,element,attrs,someCtrl){
}
}
return obj;
}
function myCtrl($scope){
$scope.studentList = ["Tom Cruise","Jammy Watson","Simon Colins"]
};
function StudentCtrl($scope){
$scope.studentList = ["Shahrukh Khan","Salmaan Khan","Amir Khan"]
};
})();
</script>
</head>
<body ng-controller="myCtrl">
<parent-class>
<child-class array="studentList"></child-class>
</parent-class>
<pre />
<ul>
<li ng-repeat="student in studentList">{{student}}</li>
</ul>
</body>
In the above code there are two directives 'parentClass' and 'childClass'.
'childClass' directive requires 'parentClass' as its parent directive, and hence 'childClass' uses the controller 'StudentCtrl'
What I am expecting in the output is
the directive 'child-class' should print the students in 'StudentCtrl' and the student outside the directives should be from 'myCtrl',
but what I am getting is both the students are from controller 'StudentCtrl',
When I do the code change in directive 'parentClass' as follows,
function parentClass(){
var obj = {
restrict : 'EA',
controller : "StudentCtrl" ,
scope : {
array : '='
}
}
return obj;
};
The output is all the students are from controller 'myCtrl' irrespective of the directives closing tag.
The links for the live demonstration is as follows
Link One
Link Two
What I am expecting is
the directive 'child-class' should output list of students present in controller 'StudentCtrl' and the student outside the directives should output from controller 'myCtrl'
Please solve my confusion in scopes also.
Thanks
In your parentClass directive, you do not specify a scope value. This means it defaults to false.
A false scope value means that the directive does not create a new scope, meaning it will use the scope that is closest (up the scope chain) to it.
In this particular case, the directive uses the scope of the myCtrl controller. As such, when you specify the controller: 'StudentCtrl', and inject the $scope object to set the studentList variable, you overwrite the value in the myCtrl controller - thereby giving you the unexpected results.
One way to resolve this is to put scope: true as an option to the parentClass directive, thereby forcing the directive to create a new scope and not overwrite its parent.

How can an Angular directive compile() function access an isolated scope?

I have the following directive:
angular.module("example_module", [])
.directive("mydirective", function() {
return {
scope: { data: "#mydirective" }
compile: function(element) {
element.html('{{example}}');
return function($scope) {
$scope.example = $scope.data + "!";
};
}
};
});
and the following HTML code:
<!DOCTYPE html>
<html ng-app="example_module">
<head>
<meta charset="utf-8">
<title>Example title</title>
<script src="lib/angular/angular.min.js"></script>
<script src="js/example.js"></script>
</head>
<body>
<div mydirective="Hello world"></div>
</body>
</html>
I would expect the directive to compile to Hello world!, but it compiles to an empty string instead. scope creates an isolated scope which seems impossible to reach for {{example}}.
I would like to know how the new HTML code created by compile() can access the link function $scope.
This doesn't work because {{example}} is being evaluated against the parent scope, which makes sense, since you are essentially changing the element before compilation to:
<div>{{example}}<div>
You can verify by replacing '$scope.example =' with '$scope.$parent.example =' (for demonstration purposes only - it's not a best practice to use $parent).
What you are really trying to do is something similar to transclusion, but there are very easier ways to do it. For instance:
angular.module("example_module", [])
.directive("mydirective", function() {
return {
restrict: 'A',
scope: { data: "#mydirective" },
template: '{{example}}',
compile: function(element) {
return function($scope) {
console.log($scope.data);
$scope.example = $scope.data + "!";
console.log($scope.example);
};
}
};
});
When you use a template, it replaces the content of the element the directive is applied to (unless you use replace: true, in which case it replaces the entire element), and the contents of the template are evaluated against the directive scope.
You could do what you are trying to do using the transclude parameter passed to compile (see the docs), but this has been deprecated so I wouldn't recommend going down that road.
Here's a Plunk where you can play with some variations.

Accessing parent controller data in combination with isolated scope in Angular

First the code, then the explanation:
index.html
<!DOCTYPE html>
<html>
<head>
<script data-require="angular.js#*" data-semver="1.3.7" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.7/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="app.js"></script>
</head>
<body ng-app="myApp">
<div ng-controller="myCtrl">
<my-directive data="1" />
<my-directive data="2" />
</div>
</body>
</html>
app.js
angular.module("myApp", []).directive("myDirective", function ($parent) {
return {
restrict: "E",
scope: {
data: "#",
},
template: function(element, attrs){
switch(attrs.data){
case '1':
return '<h3>'+ $parent.stringForDirective1 + '</h3>';
case '2':
return '<h3>'+ $parent.stringForDirective2 + '</h3>';
}
}
};
}).controller('myCtrl',function($scope){
$scope.stringForDirective1 = 'I was returned by the directive with HTML attribute data having the value 1.'
$scope.stringForDirective2 = 'I was returned by the directive with HTML attribute data having the value 2.'
});
Now for the explanation. If I were to set 'scope: false' on my directive, I could easily access the controller's data as the directive is positioned inside of its scope. However, from my understanding, in order to use any value from an HTML attribute with a custom directive, the entire directive must be put into an isolated scope.
I want to use an HTML attribute to return a template that uses the parent controller's data.
How do I get the benefits of the controller data when using 'scope: false' while being able to pass in a custom HTML attribute?
The $parent example does not work, I simply added it to show the way I've been thinking towards a solution, and I think it shows my intent clearly.
Not sure why you injected the $parent dependency. I hope that was the one you only used to show how you were thinking: The $parent example does not work, I simply added it to show the way I've been thinking towards a solution, and I think it shows my intent clearly.
Anyway, you don't need any of that. To make it all work, simply get rid of that dependency, don't concat the parent scope values into the template, but let Angular take care of it after it compiles the template (using the double curly braces for binding):
switch(attrs.data){
case '1':
return '<h3>{{$parent.stringForDirective1}}</h3>';
case '2':
return '<h3>{{$parent.stringForDirective2}}</h3>';
}
That will still look for scope.$parent, which is what you wanted.
See the fully working example here: http://plnkr.co/edit/pW5G2Yy4SelW5DxKBMqW?p=preview
Or here, as a snippet:
angular.module("myApp", [])
.directive("myDirective", function() {
return {
restrict: "E",
scope: {
data: "#",
},
template: function(element, attrs) {
switch (attrs.data) {
case '1':
return '<h3>{{$parent.stringForDirective1}}</h3>';
case '2':
return '<h3>{{$parent.stringForDirective2}}</h3>';
}
}
};
})
.controller('myCtrl', function($scope) {
$scope.stringForDirective1 = 'I was returned by the directive with HTML attribute data having the value 1.'
$scope.stringForDirective2 = 'I was returned by the directive with HTML attribute data having the value 2.'
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app="myApp">
<div ng-controller="myCtrl">
<my-directive data="1"></my-directive>
<my-directive data="2"></my-directive>
</div>
</body>
Note: had to specify the directive closing tag, since the shorthand version reads only the first directive (Plunker saying that the trailing solidus is not allowed on your directive element), see here: http://plnkr.co/edit/Qt0z0poU0ogoQq4C9a3n?p=preview
There are three possible modes of scope that a directive can have:
isolated scope (scope: {})
child scope (scope: true)
inherited scope (scope: false)
Depending on the needs of your directive, any one of these scope modes is valid.
If you want to create a directive with isolated scope, then you can pass in the models to your directive's isolated scope through the element's attributes:
scope: {
modelA: '=', // model binding
modelB: '#', // string binding
modelC: '&' // method binding in parent scope
}
Attributes
<div directive model-a="user" model-b="hello {{ user.name }}" model-c="addUser(user)"></div>
Example (not an ideal directive implementation but done to show how models can be passed to an isolated scope through attributes)
angular.module("myApp", []).directive("myDirective", function ($parent) {
return {
restrict: "E",
scope: {
data: "#",
stringForDirective1: '=?',
stringForDirective2: '=?'
},
template: '<h3 ng-if="data = '1'">{{stringForDirective1 }}</h3><h3 ng-if="data = '2'">{{stringForDirective2 }}</h3>'
};
}).controller('myCtrl',function($scope){
$scope.stringForDirective1 = 'I was returned by the directive with HTML attribute data having the value 1.'
$scope.stringForDirective2 = 'I was returned by the directive with HTML attribute data having the value 2.'
});
HTML
<body ng-app="myApp">
<div ng-controller="myCtrl">
<my-directive data="1" string-for-directive1="stringForDirective1" />
<my-directive data="2" string-for-directive2="stringForDirective2" />
</div>
</body>

AngularJS - how to override directive ngClick

I want to override directive ng-click: to some make some $rootscope changes before each execution of ng-click. How to do it?
Every directive is a special service inside AngularJS, you can override or modify any service in AngularJS, including directive
For example remove built-in ngClick
angular.module('yourmodule',[]).config(function($provide){
$provide.decorator('ngClickDirective', ['$delegate', function($delegate) {
//$delegate is array of all ng-click directive
//in this case first one is angular buildin ng-click
//so we remove it.
$delegate.shift();
return $delegate;
}]);
});
angular support multiple directives to the same name so you can register you own ngClick Directive
angular.module('yourmodule',[]).directive('ngClick',function (){
return {
restrict : 'A',
replace : false,
link : function(scope,el,attrs){
el.bind('click',function(e){
alert('do you feeling lucky');
});
}
}
});
check out http://plnkr.co/edit/U2nlcA?p=preview
I wrote a sample that removed angular built-in ng-click and add a customized ngClick
You can't override AngularJS built-in directives. However, you can define multiple directives with the same name and have them executed against the same element. By assigning appropriate priority to your directive, you can then control whether your directive runs before or after a built-in directive.
This plunker shows how to build an ng-click directive that executes before the built-in ng-click does. The code is also shown below. When clicking the link, the custom ng-click will run first, then the built-in ng-click does.
index.html
<!DOCTYPE html>
<html ng-app="app">
<head>
<script data-require="jquery#1.9.0" data-semver="1.9.0" src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.9.0/jquery.js"></script>
<script data-require="angular.js#1.0.7" data-semver="1.0.7" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.js"></script>
<script src="script.js"></script>
</head>
<body ng-controller="MyCtrl">
<a ng-click="alert()">Click me</a>
</body>
</html>
script.js
angular.module('app', [])
.directive('ngClick', function($rootScope) {
return {
restrict: 'A',
priority: 100, // give it higher priority than built-in ng-click
link: function(scope, element, attr) {
element.bind('click', function() {
// do something with $rootScope here, as your question asks for that
alert('overridden');
})
}
}
})
.controller('MyCtrl', function($scope) {
$scope.alert = function() {
alert('built-in!')
}
})

Resources