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.
Related
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.
We are using Angular 1.4.2 and I am trying to take a count value from a directive using ng-click, pass it to a function, then pass it up to the parent controller. After some effort it is working in a plunker, but unfortunately when I tried to move this functionality back into the main code, I'm not able to get a controller to bind to the isolated scope.
Should be simple, but I've tried injecting the current controller into the directive and trying to create a new controller, but nothing happens when I press click on the button.
Here is the code:
TEMPLATE:
<!DOCTYPE html>
<html>
<head>
<script data-require="angular.js#1.4.2" data-semver="1.4.2" src="https://code.angularjs.org/1.4.2/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body>
<div id="app" ng-app="app">
<div ng-controller="mainCtrl">
<my-directive ctrl-fn="ctrlFn(count = count + 10)"></my-directive>
</div>
</div>
</body>
</html>
SCRIPT:
var app = angular.module('app', []);
app.controller('mainCtrl', function($scope){
$scope.count = 0;
$scope.ctrlFn = function() {
console.log('In mainCtrl ctrlFn!');
//$scope.count += 10; Old hardcoded value.
console.log("count is: " + JSON.stringify($scope.count));
//Call service here
};
});
app.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
'ctrlFn' : '&'
},
template: "<div><button ng-click='ctrlFn()'>Click Here</button></div>",
link: function(scope, element, attributes) {
scope.ctrlFn(count);
}
};
});
Here is the template code I'm trying to modify in the main code base:
<div>
<div layout="row">
<results-loader ctrl-fn="ctrlFn(count = count + 10)"></results-loader>
<md-button class="md-raised md-primary md-button" ng-click="ctrlFn()" flex>Click Me</md-button>
</div>
</div>
and here is where I use an existing controller in my directive as a parent controller. It's defined in a route, rather than ng-controller and is already used for this view.
myresultsCtrl.$inject = ['$scope', ' myService'];
/* #ngInject */
function myresultsCtrl($scope, myService) {
$scope.count = 0;
etc...
however, it apparently isn't bound properly as I never hit the directive or this function with ng-click.
Like I said I tried adding a new controller to the template, then I tried injecting an existing controller into the directive, but neither worked. I took out the template from the directive and tried to put the ng-click directly into the template with ctrl-fn, but I wasn't sure how to wire up the click with the call to the ctrl-fn attribute of the directive with both in the template? The idea here is to move the template into it's own html file and reference it from the directive, as in: template: "myFile.html. I'm trying to enscapsulate as much as possible to make this into a reusable component.
I haven't worked much with custom directives.
Here is the direct link to the plunker.
I don't know your exact assignment, but rethink your architecture. Why do you want to count the value in the directive? In your simple case it would be better to count the value in a service or in the controller, not a directive.
Injecting controller in the directive is anti-angular pattern. You need to rethink your intentions. It is better to pass the number to the directive, make your calculations there and send your number back to the controller. It would work without extra code, because of the two-way data binding. Here a fork of your plunker:
http://plnkr.co/edit/KpB6bv5tHvXSvhErLcEp?p=preview
Main part is the definiton of the directive:
<div><span>{{count}}</span><br /><button ng-click='myFunction()'>Calculate</button></div>
I prefer not to answer my own questions as that can have a negative appearance, but in this case, I don't know if I could have explained the question well enough to get the right answer. What was throwing me off was integrating my working plunker code into our existing code base.
Where I got stuck was how to setup the controller properly for this use case. Though I declared my controller, in the module and injected the controller as it was already bound to that view, I needed to define the controller in the directive itself. Once I did all three together, everything started working.
Here are some code snippets:
angular.module('directiveName', [])
.directive('directiveName', directiveName)
.controller('injectedCtrl', injectedCtrl)
etc...
var directive = {
restrict: 'E',
scope: {
'ctrlFn' : '&'
},
template: "<div><button ng-click='ctrlFn()'>Click Here</button></div>",
controller: "injectedCtrl",
link: function(scope, element, attributes) {
scope.ctrlFn(); //This will pass to the parent controller: injectedCtrl, where $scope resides.
}
}
return directive;
}
injectedCtrl.$inject = ['$scope', 'myService'];
/* #ngInject */
function injectedCtrl($scope, myService) {
$scope.ctrlFn = function() {
//do something
}
etc...
HTML CODE:
When the button is clicked from the directive, ctrlFn here is a reference that is under my directives isolated scope in the DDO. This takes the external function that was passed in, namely: ctrlFn() which can then be invoked from the directive to call the parent controller.
<div layout="row">
<results-loader ctrlFn="ctrlFn()"></results-loader>
</div>
I'm posting this to help someone else that might have this use case as it was not easy to figure this out.
Dah Wahlins post goes into this subject in greater detail: Creating Custom AngularJS Directives Part 3 - Isolate Scope and Function Parameters and what helped to get my thinking straightened out.
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>
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
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