Accessing parent controller data in combination with isolated scope in Angular - angularjs

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>

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.

Build template string inside directive

I'm trying to build a string of HTML code inside the directive and then use that as the directive's template.
I tried this but it doesn't work.
myApp.directive('myDirective', [function() {
return {
restrict: 'E',
scope: {
data: '=' // this is the data that will shape the HTML
},
template: str,
controller: function($scope){
$scope.str = ****BUILD THE STRING HERE USING 'data'****
}
};
}]);
Then I tried passing the app's scope to the directive, but got an error again
myApp.directive('myDirective', ['$scope', function($scope) {
var str = ****BUILD THE STRING HERE USING $scope.data****
return {
restrict: 'E',
template: str
};
}]);
I'm confused about how I can do this. In the first example the string is built correctly and when I do
template: {{str}}
I see the string but obviously, it just shows up as text on the page and it's not interpreted as HTML code.
Is there a way to access either myApp's or the directive controller's scope within the return statement of the directive?
I could build the string outside of the directive, but even if I do that I still can't access myApp's scope within the directive.
Directives, by nature have access to the outer scope, if you don't strictly define it with an inner scope (or isolated scope). Example:
angular.module('app',[]).controller('ctrl', function($scope) {
$scope.example = {
message: "Hello world"
};
}).directive("myDirective", function() {
return {
template: '<strong>{{example.message}}</strong>'
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<div my-directive></div>
</div>
As you can see in the example above - The directive "knows" the outer scope values without you need to actually inject it.
Now, you can create an isolated scope and by doing this you don't limit yourself to a given scope:
angular.module('app',['ngSanitize']).controller('ctrl', function($scope) {
$scope.example = {
html: "<strong>Hello world</strong>"
};
}).directive("myDirective", function() {
return {
scope: {
data: '='
},
template: '<div ng-bind-html="data"></div>'
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular-sanitize.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<div my-directive data="example.html"></div>
</div>
By including ngSanitize I an able to use the ngBindHtml directive and pass an HTML structure to the directive inner scope.
Strictly speaking what you're trying to do can be achieved by using ng-if to output html that uses different directives based on the data count in your controller. This is better 'separation of concerns', as it means you're moving your presentation logic into the view where it belongs, and your controller is then concerned with your business logic including a data count variable (which you can then use in the ng-if).
If using this approach, you'll want to put your data retrieval into a service that the controller uses, and use the same controller for both directives.

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.

ng-init not working inside link function of directive?

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

Why I can't access the right scope?

html:
<!doctype html>
<html>
<head>
</head>
<body>
<div ng-app="project">
<div ng-controller="mainCtrl">
{{ property1 }}
<br />
{{ property2 }}
<div class="ts" d-child property1="{{ property1 }}cloud" property2="property2">
property1: {{ property1 }}
<br />
property2: {{ property2 }}
</div>
</div>
</div>
</body>
</html>
js:
angular.module('project', [])
.controller('mainCtrl', function($scope) {
$scope.property1 = 'ss';
$scope.property2 = 'dd';
});
angular.module('project')
.directive('dChild', function() {
return {
restrict: 'A',
scope: {
property1: '#',
property2: '='
},
link: function(scope, element, attrs) {
}
// template: '<input type="text" ng-model="property1" />'
}
})
I thought the property1: {{ property1 }} would be "property1: sscloud",but it turns out to be "ss",as if it still refers to the scope of the mainCtrl controller, shouldn't it be refer the scope of the d-child directive?
if I use template in the directive,it does refer to the right scope and shows 'sscloud',anyone can tell me why?
When angular compiles an element with isolated scope it has some rules:
If directives has no template property (or templateUrl), the inner content is attached to the parent scope. Actually before this commit, inner contents were getting the isolated scope. check your example to confirm it works on versions less than 1.2
If directives do have a template property then it would override the inner content (unless trancluded).
Only when you use a transclusion, then the inner content is attached to a sibling scope (non isolated).
The reason why angular works this way is to let reusable components be loosely coupled, and not have any side effects on your application.
Directives without isolate scope do not get the isolate scope from an isolate directive on the same element (see important commit).
Directive's template gets the isolated scope anyways.
If you want to alter this behavior you can pass the isolated scope to the tranclusion function like so:
angular.module('project')
.directive('dChild', function() {
return {
restrict: 'A',
transclude: true,
scope: {
property1: '#',
property2: '='
},
link: function(scope, element, attrs, ctrl, linker) {
linker(scope, function(clone, scope){
element.append(clone)
})
}
}
})
I highly recommend you to see these tutorials:
Angular.js - Transclusion basics
Angular.js - Components and containers
And to read more:
Access directive's isolate scope from within transcluded content
https://github.com/angular/angular.js/wiki/Understanding-Scopes
I'm not quite sure about this, I'm pretty sure it all has to do with when each {{}} is evaluated, and when the scope of the directive becomes isolated. My suggestion is to place the content in a template, as it seems to be working when doing so.
If you want to read more about the difference of of "#" and "=" in directive scopes, here's the best text I've found about it.
What is the difference between '#' and '=' in directive scope in AngularJS?
I think you have to use the transclude option.
In fact, as AngularJS docs says :
What does this transclude option do, exactly? transclude makes the contents of
a directive with this option have access to the scope outside of the directive
rather than inside.
Because of the Directives isolated scope that you created
More docs at:
http://docs.angularjs.org/guide/directive

Resources