Problem
Dynamically add the ng-bind attribute through a custom directive to be able to use ng-bind, ng-bind-html or ng-bind-html-unsafe in a custom directive with out manually adding to the template definition everywhere.
Example
http://jsfiddle.net/nstuart/hUxp7/2/
Broken Directive
angular.module('app').directive('bindTest', [
'$compile',
function ($compile) {
return {
restrict: 'A',
scope: true,
compile: function (tElem, tAttrs) {
if (!tElem.attr('ng-bind')) {
tElem.attr('ng-bind', 'content');
$compile(tElem)
}
return function (scope, elem, attrs) {
console.log('Linking...');
scope.content = "Content!";
};
}
};
}]);
Solution
No idea. Really I can not figure out why something like the above fiddle doesn't work. Tried it with and with out the extra $compile in there.
Workaround
I can work around it might adding a template value in the directive, but that wraps the content in an extra div, and I would like to be able to that if possible. (See fiddle)
Second Workaround
See the fiddle here: http://jsfiddle.net/nstuart/hUxp7/4/ (as suggested by Dr. Ikarus below). I'm considering this a workaround for right now, because it still feels like you should be able to modify the template before you get to the linking function and the changes should be found/applied.
You could do the compiling part inside the linking function, like this:
angular.module('app').directive('bindTest', ['$compile', function ($compile) {
return {
restrict: 'A',
scope: true,
link: {
post: function(scope, element, attrs){
if (!element.attr('ng-bind')) {
element.attr('ng-bind', 'content');
var compiledElement = $compile(element)(scope);
}
console.log('Linking...');
scope.content = "Content!";
}
}
};
}]);
Let me know how well this worked for you http://jsfiddle.net/bPCFj/
This way seems more elegant (no dependency with $compile) and appropriate to your case :
angular.module('app').directive('myCustomDirective', function () {
return {
restrict: 'A',
scope: {},
template: function(tElem, tAttrs) {
return tAttrs['ng-bind'];
},
link: function (scope, elem) {
scope.content = "Happy!";
}
};
});
jsFiddle : http://jsfiddle.net/hUxp7/8/
From Angular directive documentation :
You can specify template as a string representing the template or as a function which takes two arguments tElement and tAttrs (described in the compile function api below) and returns a string value representing the template.
The source code tells all! Check out the compileNodes() function and its use of collectDirectives().
First, collectDirectives finds all the directives on a single node. After we've collected all the directives on that node, then the directives are applied to the node.
So when your compile function on the bindTest directive executes, the running $compile() is past the point of collecting the directives to compile.
The extra call to $compile in your bindTest directive won't work because you are not linking the directive to the $scope. You don't have access to the $scope in the compile function, but you can use the same strategy in a link function where you do have access to the $scope
You guys were so close.
function MyDirective($compile) {
function compileMyDirective(tElement) {
tElement.attr('ng-bind', 'someScopeProp');
return postLinkMyDirective;
}
function postLinkMyDirective(iScope, iElement, iAttrs) {
if (!('ngBind' in iAttrs)) {
// Before $compile is run below, `ng-bind` is just a DOM attribute
// and thus is not in iAttrs yet.
$compile(iElement)(iScope);
}
}
var defObj = {
compile: compileMyDirective,
scope: {
someScopeProp: '=myDirective'
}
};
return defObj;
}
The result will be:
<ANY my-directive="'hello'" ng-bind="someScopeProp">hello</ANY>
Related
I'm building a directive that needs to initialize some data based on values passed into it through the scope. The problem is that when I try and initialize the data in the link function, the passed in value isn't available yet. Is there anyway to only run the initialization when the passed in value is available? I thought about using a watch as in the following code but it seems messy (and doesn't seem to work anyway).
.directive('etMemberActivitySummary', [function () {
return {
restrict: 'E',
templateUrl: '<div>My template</div>',
transclude: false,
scope: {
memberModel: '='
},
link: function(scope, element, attrs, controller) {
var watcher = scope.$watch(
function() {
return scope.memberModel
},
function(value) {
console.log(value);
if (value != null) {
console.log('Watch');
console.log(value);
watcher();
// Perform initialization based on scope.memberModel here
}
});
}
}
}])
Is there a correct way to do this? If it helps, the passed in value is in itself retrieved from a web service.
Update 1
Turns out that if I put an ng-if="ctrl.memberModel" on the directive usage like the following and get rid of all the watch stuff, it works. Is this the best way to do this?
<et-member-activity-summary member-model="ctrl.memberModel" ng-if="ctrl.memberModel"></et-member-activity-summary>
Say that I have a straight forward directive:
angular
.module('myApp')
.directive('myDiv', ['MyService1', 'MyService2',
function(MyService1, MyService2) {
return {
restrict: 'E',
link: function(scope, element, attrs) {
scope.myVars = MyService1.generateSomeList();
MyService2.runSomeCommands();
// Do a lot more codes here
// Lines and Lines
// Now run some embedded function here
someEmbeddedFunction();
function someEmbeddedFunction()
{
// More embedding
// This code is really getting nasty
}
}
}
}
]);
The code above has so much indentation and crowded that at least to me, it is very hard to read and unenjoyable to work with.
Instead, I want to move the link and someEmbeddedFunction out and just call them. So something along the lines of this:
function link(scope, element, attrs, MyService1, MyService2)
{
scope.myVars = MyService1.generateSomeList();
MyService2.runSomeCommands();
// Do a lot more codes here
// Lines and Lines
// Now run some embedded function here
someEmbeddedFunction();
}
function someEmbeddedFunction()
{
// This is much nicer and less tabbing involved
}
angular
.module('myApp')
.directive('myDiv', ['MyService1', 'MyService2',
function(MyService1, MyService2) {
return {
restrict: 'E',
link: link // This is line that I need to get it to work
}
]);
The problem is that MyService1 and MyService2 are not passed to the link function (i.e. if I only had a link function with scope, element, attrs then the code above would work just fine). How can I pass those variables as well?
I tried to call the function as link: link(scope, element, attrs, MyService1, MyService2) but then it says scope, element, attrs are undefined.
Note I realize that the someEmbeddedFunction can right now be moved out without a problem. This was just for demonstrating purposes.
Edit
The only way I can think to get this to work is call the link function from the directive this way:
link: function(scope, element, attrs) {
link(scope, element, attrs, MyService1, MyService2);
}
As you observed, the only way to call your non-standard link function is to do so manually within a "standard" link function.
i.e.
link: function(scope, element, attrs) {
link(scope, element, attrs, MyService1, MyService2);
}
This is because the link function doesn't get injected like other functions in Angular. Instead, it always gets the same series of arguments (regardless of what you call the function parameters):
The scope
The element (as an angular.element() instance)
The attrs object
An array or single controller instance that you required
A transclude function (if your directive uses transclusion)
Nothing else.
I use this scheme to keep it simple & readable:
var awesomeDir = function (MyService, MyAnotherService) {
var someEmbeddedFunction = function () {
MyService.doStuff();
};
var link = function ($scope, $elem, $attrs) {
someEmbeddedFunction();
};
return {
template: '<div>...</div>',
replace: true,
restrict: 'E',
link: link
};
};
awesomeDir.$inject = ['MyService', 'MyAnotherService'];
app.directive('awesomeDir', awesomeDir);
I have two directive, I want to count how much sons in father tag and display the counter in index.html as shown after JS files, the problem is that I didn't get the number correctly
module.directive('directiveFather', function () {
return {
restrict: 'E',
scope: {},
controller: function ($scope) {
$scope.AllCounter = 0;
this.addCounter = function () {
$scope.AllCounter = $scope.AllCounter + 1;
}
}
}
})
module.directive('directiveSon', function () {
return {
restrict: 'E',
scope: {},
require: '^directiveFather',
link: function(scope, element, attrs, fatherCtrl){
fatherCtrl.addCounter();
}
}
}
})
<directive father>
<directive son>
</directive son>
{{AllCounter}}
</directive father>
If you use an isolate scope, then the elements inside directiveFather will be bound to the parent scope (thus AllCounter won't be directly available in the view - although it will hold the correct value).
You can either change scope: {} to scope: true/false in directiveFather (see demo 1) or
"manually" compile, link and append the HTML to the directiveFather element (see demo 2).
(There is, also, Wildhoney's suggestion of using transclusion and template.)
Basically, you don't need any of that for the approach to work - they are only necessary to demonstrate that everything works (by displaying the counter in the view), but even without appending the counter, the addChild() function gets called as expected.
That said, after fixing the element names (<directive-father> and <directive-son>), it works as expected.
See, also, these short demos: demo 1, demo 2.
Let's say we have some nested directives:
<big-poppa>
<baby-bird></baby-bird>
</big-poppa>
And let's say that big-poppa wants to create a component that all of his children directives can share. It would be nice to put it in the controller, but this component needs the DOM, so it needs to be build in the link function.
Then let's say the baby-bird component wants to read from component. Maybe it wants to listen to events from it, maybe send it a command or two. The challenge is that controllers fire down the dom (first parent, then child), and post-link methods fire the other direction, so the execution order looks like this:
bigPoppa controller
babyBird controller
babyBird link
bigPoppa link
The fact that the parent's link method fires after the child's is the cause of an intra-directive communication challenge for me. I want the parent to build the shared DOM component, but DOM construction should happen in a link function. The parent therefore builds the component after any children
I can solve this with a timeout (gross), or a promise (complex/non-idiomatic?). Here is the fiddle:
http://jsfiddle.net/8xF3Z/4/
var app = angular.module('app',[]);
app.directive('bigPoppa', function($q){
return {
restrict: 'E',
controller: function($scope){
console.log('bigPoppa controller');
var d = $q.defer()
$scope.bigPoppaLinkDeferred = d
$scope.bigPoppaLink = d.promise
},
link: function(scope, el, attrs){
console.log('bigPoppa link');
scope.componentThatNeedsDom = { el: el, title: 'Something' };
scope.bigPoppaLinkDeferred.resolve()
}
}
});
app.directive('babyBird', function(){
return {
restrict: 'E',
controller: function(){ console.log('babyBird controller'); },
link: function(scope, el, attrs, bigPoppaController){
console.log('babyBird link');
// console.log('poppa DOM component', scope.componentThatNeedsDom); // Not yet defined, because the parent's link function runs after the child's
// setTimeout(function(){ console.log('poppa DOM component', scope.componentThatNeedsDom); }, 1); // Works, but gross
scope.bigPoppaLink.then(function(){
console.log('poppa DOM component', scope.componentThatNeedsDom);
}); // works, but so complex!
}
}
});
console.log(''); // blank line
Lots of background here, but my question is this simple:
Is there a clean way to do behavior in a child directive after a parent's directive has run its post-link function?
Maybe a way of using priority, or the pre and post link methods?
Another way of achieving this is to use plain Angular scope events to communicate from the parent linking function to the child.
var app = angular.module('app',[]);
app.directive('bigPoppa', function($q){
return {
restrict: 'E',
link: function(scope, el, attrs){
scope.$broadcast('bigPoppa::initialised', {el: el, title: 'Something'});
}
}
});
app.directive('babyBird', function(){
return {
restrict: 'E',
link: function(scope, el, attrs) {
scope.$on('bigPoppa::initialised', function(e, componentThatNeedsDom) {
console.log('poppa DOM component in bird linking function', componentThatNeedsDom);
});
}
}
});
This can be seen working at http://jsfiddle.net/michalcharemza/kerptcrw/3/
This way has the benefits:
Has no scope watchers
Instead of depending on knowledge of the order of controller/pre-link/post-link phases, it uses a clear message send/receive paradigm, and so I would argue is easier to understand and maintain.
Doesn't depend on behaviour being in the pre-link function, which isn't that typical, and you have to be mindful to not put in behaviour that modifies the DOM in it.
Doesn't add variables to the scope hierarchy (but it does add events)
Based on experiments, and asking correction if I am wrong, I have found Angular runs its compile phase in the following order:
1. compile methods of all directives both parent and child, run in flat order
2. parent controller
3. parent pre-link
4. (all actions of children directives)
5. parent post-link (AKA regular `link` function)
The public gist of this experiment is here: https://gist.github.com/SimpleAsCouldBe/4197b03424bd7766cc62
With this knowledge, it seems like the pre-link callback on the parent directive is a perfect fit. The solution looks like this:
var app = angular.module('app',[]);
app.directive('bigPoppa', function($q){
return {
restrict: 'E',
compile: function(scope, el) {
return {
pre: function(scope, el) {
console.log('bigPoppa pre');
scope.componentThatNeedsDom = { el: el, title: 'Something' };
}
};
}
}
});
app.directive('babyBird', function(){
return {
restrict: 'E',
link: function(scope, el, attrs, bigPoppaController){
console.log('babyBird post-link');
console.log('bigPoppa DOM-dependent component', scope.componentThatNeedsDom);
}
}
});
http://jsfiddle.net/a5G72/1/
Thanks to #runTarm and this question for pointing me in the pre-link direction.
There are 2 patterns that you can use to achieve what you want
You can have code in a child linking function that reacts to changes in a parent directive's controller, by requireing the parent directive's controller, and creating a $watcher on some value in it.
If you need run something in the parent linking function, and only then change a value in its controller, it is possible for a directive to require itself, and access the controller from the linking function.
Putting these together in your example becomes:
var app = angular.module('app',[]);
app.directive('bigPoppa', function($q){
return {
restrict: 'E',
require: 'bigPoppa',
controller: function($scope) {
this.componentThatNeedsDom = null;
},
link: function(scope, el, attrs, controller){
controller.componentThatNeedsDom = { el: el, title: 'Something' };
}
}
});
app.directive('babyBird', function(){
return {
restrict: 'E',
require: '^bigPoppa',
link: function(scope, el, attrs, bigPoppaController){
scope.$watch(function() {
return bigPoppaController.componentThatNeedsDom
}, function(componentThatNeedsDom) {
console.log('poppa DOM component in bird linking function', componentThatNeedsDom);
});
}
}
});
Which can be seen at http://jsfiddle.net/4L5bj/1/ . This has the benefits over your answer that it doesn't depend on scope inheritance, and doesn't pollute the scope with values that are only used by these directives.
I am developing a directive which shows and hides it's contents based on a click event (ng-click) defined in it's template. On some views where the directive is used I'd like to be able to know if the directive is currently showing or hiding it's contents so I can respond to the DOM changes. The directive has isolated scope and I am trying to notify the parent scope when the directive has been "toggled". I'm attempting to accomplish this by passing a callback function to the directive where it is used that can be called when the directive's state changes i.e hides or shows
I'm not sure how to correctly implement this being that the state of the directive (hidden or shown) is stored in the directive's isolated scope and is determined after the ng-click. Therefore I need to call the parent scope's function from within the directive and not from withing the view.
This will make WAAY more sense with an example. Here is a plunked demonstrating what I'd like to do:
http://plnkr.co/edit/hHwwxjssOKiphTSO1VIS?p=info
var app = angular.module('main-module',[])
app.controller('MainController', function($scope){
$scope.myValue = 'test value';
$scope.parentToggle = function(value){
$scope.myValue = value;
};
});
app.directive('toggle', function(){
return {
restrict: 'A',
template: '<a ng-click="toggle();">Click Me</a>',
replace: true,
scope: {
OnToggle: '&'
},
link: function($scope, elem, attrs, controller) {
$scope.toggleValue = false;
$scope.toggle = function () {
$scope.toggleValue = !$scope.toggleValue;
$scope.OnToggle($scope.toggleValue)
};
}
};
});
I'm relatively new to Angular. Is this a bad idea to begin with? Should I be using a service or something rather than passing around function refs?
Thanks!
Update
You can also use & to bind the function of the root scope (that is actually the purpose of &).
To do so the directive needs to be slightly changed:
app.directive('toggle', function(){
return {
restrict: 'A',
template: '<a ng-click="f()">Click Me</a>',
replace: true,
scope: {
toggle: '&'
},
controller: function($scope) {
$scope.toggleValue = false;
$scope.f = function() {
$scope.toggleValue = !$scope.toggleValue;
$scope.toggle({message: $scope.toggleValue});
};
}
};
});
You can use like this:
<div toggle="parentToggle(message)"></div>
Plunk
You could bind the function using =. In addition ensure the property name in your scope and tag are matching (AngularJS translates CamelCase to dash notation).
Before:
scope: {
OnToggle: '&'
}
After:
scope: {
onToggle: '='
}
Furthermore don't use on-toggle="parentToggle({value: toggleValue})" in your main template. You do not want to call the function but just passing a pointer of the function to the directive.
Plunk