Angular Maintain List of Directives in Order - javascript

I want to maintain a list of directives on a page in the order they appear in the dom. I know directives are created (link function called) in order, and I can append them to an array when being linked, but how do I handle dynamic pages (ajax, ngRepeats, etc..). Currently every time I need to use the array I broadcast an event to get the directives in order.
gatherDirectives: ->
all = []
$rootScope.$broadcast 'roleCall', (dir) -> all.push dir
all
But I'd rather have directives register and unregister when being created and removed to be more efficient. Something like what is discussed on AngularJS directive - setting order for multiple directive elements (not priority for directives, but priority for the elements), but that can handle dynamically added/removed directives. How can this be done without gathering the directives each time?

"I want to maintain a list of directives on a page in the order they appear in the dom."
Can these methods help you?
Priority
AngularJS finds all directives associated with an element and processes it. This option tells angular to sort directives by priority so a directive having higher priority will be compiled/linked before others. The reason for having this option is that we can perform conditional check on the output of the previous directive compiled. In the below example, I want to add btn-primary class only if a div has btn class on it.
<div style='padding:100px;'>
<div primary btn>Random text</div>
</div>
Please note that the default priority if not set will be zero. In this example, btn directive will be executed before primary. Play with the demo!
App.directive('btn', function($timeout) {
return {
restrict: 'A',
priority: 1,
link: function(scope, element, attrs) {
element.addClass('btn');
}
};
});
App.directive('primary', function($http) {
return {
restrict: 'A',
priority: 0,
link: function(scope, element, attrs) {
if (element.hasClass('btn')) {
element.addClass('btn-primary');
}
}
};
});
Terminal
As per the official documentation, If set to true then the current priority will be the last set of directives which will execute on an element. It holds true unless you use custom directives in conjunction with built-in directives having priority set on them such as ngRepeat, ngSwitch, etc. Instead all custom directives having a priority greater than or equal the current priority will not be executed in this case.
In the below example, first has a higher priority than second – which has terminal set to true. And if you set the lower priority to first – It will not be executed at all. But in case of no-entry directive, it will not be executed even though it has a higher priority than ng-repeat. Is it a bug? Is it because of transclusion used in ng-repeat? Need to dig in…
<div first second></div>
<ul>
<li ng-repeat="item in ['one', 'two', 'three']" no-entry>{{item}} </li>
</ul>
App.directive('first', function() {
return {
restrict: 'A',
priority: 3,
link: function(scope, element, attrs) {
element.addClass('btn btn-success').append('First: Executed, ');
}
};
});
App.directive('second', function() {
return {
restrict: 'A',
priority: 2,
terminal: true,
link: function(scope, element, attrs) {
element.addClass('btn btn-success').append('Second: Executed ');
}
};
});
App.directive('noEntry', function() {
return {
restrict: 'A',
priority: 1001,
link: function(scope, element, attrs) {
element.append('No Entry: Executed ');
}
};
});

Related

When using ngRepeat track by dont update DOM on move

For my particular application, I want to be able to use an ng-repeat on a custom directive and track by a property of the object.
The problem I'm running into is that I am intentionally not wanting my custom directive to be rendered in the DOM where it is written. I want to take the template (including the directive element) and some variables that exist in it's parent scope and have them compiled and appended inside some markup that is being generated by a 3rd party library.
This way I can write something like:
<my-directive ng-repeat="item in items track by item.id"
ng-click="someCtrl.doSomethingWithAService();">
</my-directive>
And have this code appended elsewhere where it functions the same way.
The collection that is being repeated over can change frequently, but I don't want to recreate the appended content, and want to be able to remove it when the scope of each directive instance is destroyed.
I've tried as many approaches as I can think of, and some have been "good enough". The main issue I'm running into now is that when the collection is updated, the tracked items that didn't change end up being added to the DOM where the markup was originally written.
So on first load, everything works. Update collection: directive logic doesn't run again and the element is appended back to where the ng-repeat was.
Any ideas?
Edit:
window.angular.module('my.module')
.directive('myDirective', ['myService',
function (myService) {
return {
restrict: 'E',
scope: true,
replace: true,
templateUrl: '/path/to/template.tpl.html',
compile: function (tElement, tAttributes) {
tElement.removeAttr('ng-repeat');
return {
post: function (scope, iElement, iAttributes, controller, transcludeFn) {
iElement.remove();
var item = myService.addItem(scope, iElement[0].outerHTML);
scope.$on('$destroy', function () {
// remove item added in service
});
}
};
}
};
}]);
The directive essentially looks like this.
Service:
window.angular.module('my.module')
.factory('myService', ['$compile', function ($compile) {
function addItem(scope, template) {
var element = angular.element(document.querySelector('some-selector'));
element.html('').append($compile(template)(scope));
}
return {
addItem: addItem
};
}]);

Directives are not updated when ng-repeat changes

I have a directive that is repeated using ng-repeat. Consider this code:
<my-drctv todos="todos" ng-repeat="todos in todoGroups track by todos[0].id"></my-drctv>
todoGroups is a two dimensional array, that contains grouped todos.
This is my directive:
angular.module('myModule').directive('myDrctv', function() {
return {
restrict: 'E',
scope: {
todos: "="
},
replace: true,
templateUrl: '<div>{{firstTodo.description}}</div>',
link: function link(scope, element, attrs, ctrl) {
console.log("myDrctv: link fn");
scope.firstTodo = scope.todos[0];
}
};
});
This lists my todos (the template is simplified) and works so far.
Now the problem: When todoGroups is re-fetched from my server and set to a new value with $scope.todoGroups = myService.fetch() inside my controller, my directives aren't updated, because the link function of my directive isn't called a second time.
It seems that this problem occurs only, when the length of the list returned by the server is the same as before and only the "content" of my todos are changed (but I'm not sure)
Does angular re-use my directives for performance reasons? I always thought, it would destroy every element inside ng-repeat and insert new elements (in my case a directive)
How can I trigger a second call to my link function of my directive?

Angular directives - How to select template based on attribute values?

I am developing a widget where I want to render some messages/text one after another. I want to change the template of the message based on the type of message.
my current directive setup is as follows
directive('cusMsgText', function(){
return {
restrict: 'E',
template:function(elements, attrs){
return '<div></div>';
},
link: function($scope, iElm, iAttrs, controller) {
//add children to iElm based on msg values in $scope
}
};
});
The directive is used as follows
<div ng-repeat="(key, value) in chatUser.msg">
<data-cus-msg-text msg="value.type"></data-cus-msg-text>
</div>
Now my question are -:
Is it possible to return one of multiple strings (templates) from
template function itself based on the actual value of attribute
msg. I tried accessing attrs.msg in template function and it
return value.type.
If not then, Is it good to manipulate template under linker or I
need to move it to compile function?
To render a different template based on value.type you can use the ng-switch statement:
<div ng-switch="value.type">
<div ng-switch-when="type1">
//...template for type 1 here...
</div>
<div ng-switch-when="type2">
//...template for type 2 here...
</div>
</div>
Also, if I understood your second question: manipulation of the uncompiled directive should be done in the compile function, all the manipulation which occurs after compilation should go in the link function.
Docs for ngSwitch
EDIT: +1 to Sebastian for understanding what you wanted. However, what he is proposing is essentially reinventing the wheel, since it is essentially compiling and inserting the template manually (which is what ngSwitch does for you). Also, you can access the attributes you put on your directive through the attrs argument of the link function.
In the template function you don't have access to the scope of your directive. If you want to control what gets rendered you can do this using conditional logic (e.g. ng-switch) in a global template as suggested by simoned or use a link function:
.directive('cusMsgText', function($compile) {
return {
restrict: 'E',
scope: {
msg: '=',
item: '='
},
link: function(scope, element, attrs) {
templates = {
x: '<div>template x {{item.name}}</div>',
y: '<div>template y {{item.name}}</div>'
};
var html = templates[scope.msg];
element.replaceWith($compile(html)(scope));
}
};
});

Outer AngularJS directive doesn't build inner directive correctly

Using AngularJS 1.0.8, I'm trying to create some reusable directives to create a situation where a web developer can code a single "top-level" directive with a number of attributes, and this directive, in turn, has a template containing other directives, which themselves might contain other directives etc.
The problem I'm having is making the "inner" templates aware of the top level attributes. I thought this would be a universal problem, but it didn't look, from my research, that anyone else was asking this.
I created this Plunker to show the problem:
<!DOCTYPE html>
<html ng-app="outerInnerDirectivesApp">
<head>
<title>Outer/Inner Directives</title>
</head>
<body>
<div>Single level directive follows:</div>
<single-level-directive single-level-id="single123"></single-level-directive>
<div>Outer/inner directive follows (Expecting "outer123"):</div>
<outer-directive outer-id="outer123"></outer-directive>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js"></script>
<script src="app.js"></script>
<script src="directives.js"></script>
</body>
</html>
In the Plunker,
single-level-directive works and is, I think, a standard way to display data.
outer-directive and inner-directive aren't working.
What I expected to happen with these was
(i) outerDirective compiles/links to produce the html
<inner-directive inner-id="outer123"></inner-directive>
and then
(ii) innerDirective compiles/links to produce html
<div>outer123</div>
But at step (ii) I get
<inner-directive inner-id="" class="ng-isolate-scope ng-scope">
<div class="ng-binding"></div>
</inner-directive>
so an empty div is generated by innerDirective.
In fact, if I change outer-template.html to be
<div>{{outerId}}<div>
then the value displays correctly, so it looks like scope.outerId is available at the correct point, but Angular isn't happy about me trying to use it in the way I am.
Is this a reasonable thing to expect Angular to do? If so, what am I missing? If not, then what do you think would be a sensible alternative way to build up more complex screens from simple sets of directives?
If you are going to design directives with isolated scope, I would suggest using the isolated scope to define the type of attribute you want to use:
outerInnerApp.directive("outerDirective", function() {
return {
restrict: "E",
scope: {
outerId: '#'
},
link: function(scope, element, attrs) {
},
templateUrl: "outer-template.html"
};
});
outerInnerApp.directive("innerDirective", function() {
return {
restrict: "E",
scope: {
innerId: '='
},
link: function(scope, element, attrs) {
},
templateUrl: "inner-template.html"
};
});
Here is a working plunker.
Your outer directive is using the value that is defined in the attribute. So, to pass the value into the isolated scope, we can use #. The inner scope is then binding a variable through. So, we can use = to set up a bound attribute.
Had some more thoughts about this. Having used AngularJS a bit more, I'm not sure that I want to bind to the scope (using the "="). In fact, I can get the original Plunkr to work by making these changes:
outerInnerApp.directive("outerDirective", function() {
return {
restrict: "E",
scope: {
//add outerId here
outerId: "#"
},
link: function(scope, element, attrs) {
//remove scope assignment here
//scope.outerId = attrs.outerId;
},
templateUrl: "outer-template.html"
};
});
outerInnerApp.directive("innerDirective", function() {
return {
restrict: "E",
scope: {
//add innerId here
innerId: "#"
},
link: function(scope, element, attrs) {
//remove scope assignment here
//scope.innerId = attrs.innerId;
},
templateUrl: "inner-template.html"
};
});
What I don't understand at the moment is why there is a different between, say,
innerId:"#"
and setting the value of scope in the link function
link: function(scope, element, attrs) {
scope.innerId = attrs.innerId;
}
When I find out why it behaves differently I'll post back.

Angular Directive to Directive call

If you have a directive that you're using multiple times on a page how can 1 directive communicate with another?
I'm trying to chain directives together in a parent child relationship. When directive A is clicked i want to filter Directive B to only have the children of the selected item in Directive A. In this case there may be infinite number of directives and relationships on the page.
Normally i would have Directive A call a filter method on each of it's children, and each child calls it's child to continue filtering down the hierarchy.
But i can't figure out if calling methods from 1 directive to another is possibe.
Thanks
It sounds like you are looking for a directive controller. You can use the require: parameter of a directive to pull in another directive's controller. It looks like this:
app.directive('foo', function() {
return {
restrict: 'A',
controller: function() {
this.qux = function() {
console.log("I'm from foo!");
};
},
link: function(scope, element, attrs) {
}
};
});
app.directive('bar', function() {
return {
restrict: 'A',
require: '^foo',
link: function(scope, element, attrs, foo) {
foo.qux();
}
};
});
From the angular docs, here are the symbols you can use with require and what they do.
(no prefix) - Locate the required controller on the current element.
? - Attempt to locate the required controller, or return null if not found.
^ - Locate the required controller by searching the element's parents.
?^ - Attempt to locate the required controller by searching the element's parents, or return null if not found.
Here's a jsbin of my example. http://jsbin.com/aLikEF/1/edit
Another option that may work for what you need is to have a service that each directive sets up a watch on and can manipulate. For example, directive1 may watch a property in the service and respond to changes and also setup a button that can change that property. Then, directive2 can also watch and change the service, and they will respond to one another however you set that up. If you need a jsbin of that also, just let me know.
I hope this helps!
You could try putting all of the data into a service that the directives can each reference.
Something like:
app.factory('selectedStuffService', function(){
var allItems = [];
var selectedItems = [];
function addSelectedItem(item){
selectedItems.push(item);
}
return {
allItems: allItems,
selectedItems: selectedItems,
addSelectedItem: addSelectedItem
}
}
Interactions in directive A change the values in the selectedItems array and directive B can bind to it. You can easily add other methods to the service to filter/manipulate the items as needed and any directive that uses the service should be able to update based on changes made by other directives.

Categories

Resources