how to add directive attribute to element in link function? - angularjs

As we known, the angular have performance if there are more than 2000 records in the page, because only some of the records need add some behavior , so I would prefer to dynamically add attribute to element according model value in link function, as there will be less watches.
So I use $compile to recompile the element like below:
mainApp.directive("popoverSetting", function ($compile) {
return {
restrict: "A",
link: function (scope, element, attrs) {
if (scope.item.isTrue) {
element.attrs("ns-popover-trigger", "mouseenter");
element.attrs("ns-popover-timeout", "0.01");
$compile(element)(scope);
}
}
}
})
Because there are about 1000 records, so the speed is very slow, is there some other way to add attribute and compile quickly? although there only 5 records need to add these attribute, it still increase about twofold time than before.

The problem with trying to do this is that nested compiles are slow (an understatement), especially when you are talking thousands of them. It just isn't the way to go.
I could see where some may consider this ugly, and depending on your needs, this solution may NOT work for you, but it does get everything compiled pretty quickly, with the attributes applied. I ended up building a compiler directive that would generate markup based on the dataset, adding the necessary attributes if isTrue was true, and then compiling the whole thing once. The ugly part is that it doesn't generate bindings. So if you need to change something or update something, you'll have to recompile the whole thing.
It's running right now at about 200-300ms to compile the whole thing with 2000 items in the dataset.
Plunker
Directives:
app.directive('nsPopoverTrigger', function($timeout) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
element.on(attrs.nsPopoverTrigger, function() {
console.log('popover triggered')
})
}
}
})
app.directive('compiler', function($compile) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var i = 0, template = '<ul>'
scope.generateMarkup = function() {
for (i; i < scope.data.length; i++) {
if(scope.data[i].isTrue) {
template = template + "<li ns-popover-trigger='mouseenter' ns-popover-timeout='0.01'>" + scope.data[i].key + "</li>"
} else {
template = template + '<li>' + scope.data[i].key + '</li>'
}
}
var $template = angular.element(template)
$compile($template)(scope)
element.append($template)
}
}
}
})

because I don't remove the directive so it makes the angular compile element over and over again. so i need to remove the directive or set a flat to determine if it is necessary to compile again.
function groupPopoverDirective($compile) {
return {
restrict: "A",
scope:true,
link: function ($scope, $element, $attrs) {
$scope.groupPopoverData = $scope.$eval($attrs.groupPopoverData);
if ($scope.groupPopoverData.isTrue) {
if ($element.attr("ns-popover")) {
return;
}
$element.attr("ns-popover", "true");
$element.attr("ns-popover-template", "popover-template.html");
$element.attr("ns-popover-trigger", "mouseenter");
$element.attr("ns-popover-placement", "right|top");
$element.attr("ns-popover-mouse-relative", "x");
$element.attr("ns-popover-theme", "ns-popover-tooltip-theme");
$element.attr("ns-popover-timeout", "0.01");
//element.removeAttr('popover-info');
var linkFn = $compile($element);
var pp = linkFn($scope);
}
}
};
};

Related

AngularJS $parser not being called when dynamically adding the directive

So what i'm trying to achieve is to be able to add a directive that contains a parser through another directive.
When directly adding the parser directive on an html element it works completely fine. the parser directive i currently use:
.directive('parseTest', [
function () {
return {
restrict: 'A',
require: ['ngModel'],
link: {
post: function (scope, element, attributes, ctrls) {
var controller = ctrls[0];
controller.$parsers.unshift(function (value) {
var result = value.toLowerCase();
controller.$setViewValue(value);
controller.$render();
return result;
})
}
}
}
}
])
Now when i add this directive through another directive the parser never gets called weirdly enough. The directive that generated the parsetest directive:
.directive('generateTest', ['$compile',
function ($compile) {
return {
restrict: 'A',
compile: function (elem, attrs) {
elem.attr('parse-test', '');
elem.removeAttr('generate-test');
var linkFn = $compile(elem);
return function (scope, element, attr) {
linkFn(scope);
}
}
}
}
])
The following works fine:
<input class="form-control col-sm-6" ng-model="model.parsetest" parse-test/>
The following doesn't work (While the generated result html is the same)
<input class="form-control col-sm-6" ng-model="model.generateTest" generate-test />
So my question is how can i get the parser working when it is in a dynamicly added directive?
Note, i already tried the solution to a similar issue from this question, but that doesn't work for me.
EDIT: Here is a plnkr that demonstrates the issue, both fields have the parse-test directive applied to it that should make the value in the model lowercase, but it only works for the one that is not dynamically added as shown in the console logs.
So I've found the solution, so for anyone that stumbles on the same issue here it is.
The 2 small changes have to made to the directive that generates the directive that contains a parser or formatter.
First of set the priority of the directive to a number higher or equal as 1. Secondly put terminal on true. Those 2 settings seem to resolve the issue.
The problem probably lies in that the default execution of nested directives makes it so that the parser and formatters get inserted slightly to late which is why we need to make sure the directive gets generated first thing before anything else.
This is just an assumption of why it works tho, if anyone else has an explanation it would be great :)
As for the code, the directive that generates another directive should look something like:
directive('generateTest', ['$compile',
function ($compile) {
return {
restrict: 'A',
terminal: true,
priority: 1,
compile: function (elem, attrs) {
attrs.$set('parseTest', '');
attrs.$set('generateTest', undefined);
var linkFn = $compile(elem);
return function (scope, element, attr) {
linkFn(scope);
}
}
}
}
])

How to make sibling directives communication work( communication between certain specific directive)

All:
Suppose I have two directives( dir1 and dir2) , which are both isolated scope. From some posts, I learnt that I need to use "require" to get scope of the other directive, but there is one question confused me so much:
Suppose I use ng-repeat generated a lot of dir1 and dir2, how do I know in certain dir1, which specific dir2's controller scope is required( cos there are many dir2 and in my understanding, all those scopes in dir2 controller are independent to each other)?
For example:
app.directive("dir1", function(){
var counter = 0;
return {
restrict:"AE",
scope:{},
template: "<button class='dir1_btn'>highlight dir2_"+(couter++)+" </button>",
link:function(scope, EL, attrs){
EL.find("button.dir1_btn").on("click", function(){
// How to let according dir2 know this click?
})
}
}
})
app.directive("dir2", function(){
var counter = 0;
return {
restrict:"AE",
scope:{},
template: "<span class='dir2_area'>This is dir2_"+(couter++)+" </span>",
link:function(scope, EL, attrs){
// Maybe put something here to listening the event passed from dir1?
}
}
})
And the html like( for simple purpose, I just put 2 of each there, actually it will generated by ng-repeat) :
<dir1></dir1>
<dir2></dir2>
<dir1></dir1>
<dir2></dir2>
Consider this just like the switch and light, dir1 is the switch to open(by change background-color) according light (dir2).
In actual project, what I want to do is angularJS directive version
sidemenu and scrollContent, each item in sidemenu is a directive,
click it will make according content(another directive) to auto scroll
to top.
I wonder how to do this? I know this is easy in jQuery, just wondering how to hook this into AngularJS data-driven pattern.
Thanks
The most important thing to note here is that I think you want to use ng-class Since you are creating both directives in an ng-repeat, I assume you are iterating over a list of objects (even if they are two separate ng-repeats, if you iterate over the same list of objects in both it will work. JQuery should not be necessary)? Attach an ngClass object to each object you iterate over, put it on an ng-class attribute in your dir2, then give dir1 access to change it. ngClass provides animation hooks if you want to animate the transition. The rest of my answer may help, though I would like to redo it now that I thought of ng-class. I have to get back to work for now though. I'll watch for feedback and try to answer quickly if you have questions.
I think there are probably a few ways to better accomplish what you are trying to do. It is not clear why both of your directives need to have isolate scopes. As I use angular more I find that though isolating a scope is a powerful technique, it is best to avoid over using it.
As for the require directive property, this post explains how to make directives communicate via their controllers very well.
I have two possible suggestions for you.
Make it one directive
Why can't you just put the templates into one?
Or if as I assume there is some reason they need to be separate, you could consider just sharing an object between them.
<div ng-repeat='obj in sharedDirObjs'>
<dir1 shared-dir-obj='obj'></dir1>
<dir2 shared-dir-obj='obj'></dir2>
</div>
app.controller('ctrl', function() {
$scope.sharedDirObjs = [obj1, obj2, obj3]
});
app.directive("dir1", function(){
var counter = 0;
return {
restrict:"AE",
scope:{sharedDirObj : '='},
template: "<button class='dir1_btn' ng-click='clickFn()'>highlight dir2_"+(couter++)+" </button>",
link:function(scope, EL, attrs){
var dir1vars...
scope.clickFn = function(){
// dir1 logic...
scope.sharedDirObj.dir2.clickFn(dir1vars...);
};
}
}})
app.directive("dir2", function(){
var counter = 0;
return {
restrict:"AE",
scope:{sharedDirObj : '='},
template: "<span class='dir2_area'>This is dir2_"+(couter++)+" </span>",
link:function(scope, EL, attrs){
scope.sharedDirObj.dir2 = {};
scope.sharedDirObj.dir2.clickFn(dir1vars...) {
// access to dir2 vars
};
}
}})
Similarly, you could create a service that holds an array of objects that are shared by injecting the service and indexed using the $index from the ng-repeat, or you could use an id system as PSL suggests. Note that the solution I describe above could work with isolate scope, or without it using scope.$eval(attr.sharedObj); on either or both of your directives. The answer to this question provides a solid runthrough of when and why to use isolated scope. In any case it would likely be best not to pipe functions through a shared object as I am showing above, and timing issues would need to be dealt with. Better would be to store properties on the object and set a scope.$watch on them in your dir2.
You may have to use some sort of strategy. Some kind of identifier hook up. Clearly you cannot use require(to require the controller of a directive and you don't have any also it can only look up to ancestors or itself not siblings). For example you could add an id attribute and a for attribute and target the element with a selection based on specific attribute value and fire an event. With this position of related element does not matter.
Your directive could look like:
<dir1 dir-for="id1"></dir1>
<dir2 dir-id="id1"></dir2>
<dir1 dir-for="id2"></dir1>
<dir2 dir-id="id2"></dir2>
and simple implementation:
.directive("dir1", function($document) {
var counter = 0;
return {
restrict: "AE",
scope: {
dirFor: '#'
},
template: "<button class='dir1_btn' ng-click='handleClick()'>highlight dir2_({{dirFor}}) </button>",
link: function(scope, EL, attrs) {
var $target = angular.element(
$document[0].querySelector('[dir-id="' + scope.dirFor + '"]'))
.contents().scope();
var clicked = false;
scope.handleClick = function() {
clicked = !clicked;
targetScope.$broadcast("SWITCH_CLICKED", clicked);
}
scope.$on('$destory',function() {
$target = null;
}
}
}
})
app.directive("dir2", function() {
var counter = 0;
return {
restrict: "AE",
scope: {
dirId: '#'
},
template: "<span class='dir2_area' ng-class=\"{true:'on', false:'off'}[status]\">This is dir2_{{dirId}}</span>",
link: function(scope, EL, attrs) {
console.log(scope.$id);
scope.status = false;
scope.$on('SWITCH_CLICKED', function(e, data) {
scope.status = data;
});
}
}
});
Demo
var app = angular.module('app', []).controller('ctrl', angular.noop);
app.directive("dir1", function($document) {
var counter = 0;
return {
restrict: "AE",
scope: {
dirFor: '#'
},
template: "<button class='dir1_btn' ng-click='handleClick()'>highlight dir2_({{dirFor}}) </button>",
link: function(scope, EL, attrs) {
var $target = angular.element($document[0].querySelector('[dir-id="' + scope.dirFor + '"]')).contents();
var clicked = false;
scope.handleClick = function() {
clicked = !clicked;
$target.scope().$broadcast("SWITCH_CLICKED", clicked);
}
scope.$on('$destroy',function() {
$target = null;
});
}
}
})
app.directive("dir2", function() {
var counter = 0;
return {
restrict: "AE",
scope: {
dirId: '#'
},
template: "<span class='dir2_area' ng-class=\"{true:'on', false:'off'}[status]\">This is dir2_{{dirId}}</span>",
link: function(scope, EL, attrs) {
scope.status = false;
scope.$on('SWITCH_CLICKED', function(e, data) {
scope.status = data;
});
}
}
})
.on{
color:green;
}
.off{
color:blue;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<dir1 dir-for="id1"></dir1>
<dir2 dir-id="id1"></dir2>
<dir1 dir-for="id2"></dir1>
<dir2 dir-id="id2"></dir2>
</div>
I have used $document[0].querySelector('[dir-id="' + scope.dirFor + '"]')).contents().scope() to get hold of the scope, similarly you could do .controller to get hold of the controller instance as well. Current example is doing an absolute selection(with document), you could as well make it relative.

Angularjs directive to make any element work like a link

I tried to write a directive making an arbitrary element work like a link. Something like
<span goto="overview">Overview</span>
to be translated into something like
<span ng-click="$location.path('#/overview')">Overview</span>
or whatever will work. As a bonus I'd like the link to work, even when the current URL is already .../#overview.1
This sound pretty trivial, but I'm stuck with something like
.directive('goto', function($location) {
return {
template: function(element, attrs) {
// Here `attrs` should be used but how?
var ref = "#/" + element.attr('goto');
// This is a mystery to me.
// I guess, transclusion is the way to go, but how exactly?
var elementContent = getEverythingBelowElementButHow();
// I doubt `$location` will be visible on the invocation site.
return "<span ng-click='$location.path(" +
ref + "'>" +
elementContent +
"</span>";
}
};
})
I'm perfectly sure, I'm doing it all wrong. I'm nearly sure it's all unnecessary complicated. However, I'm hoping to be able to learn something from the answers.
1I found myself repeatedly clicking on such a link in order to get to the "pristine overview state" and wondering that nothing happens. Obviously, the browser ignores links to the current location, but there could be a way around this?
You can add click event to directive dynamically.
angular.module('ExApp')
.directive('goto', function($location) {
return {
restrict: 'A',
scope: {
//scope variables
},
link: function(scope, element, attr) {
element.on('click', function() {
$location.path('/overview');
scope.$apply();
});
}
}
});
plunker : http://plnkr.co/edit/n6VbZnAVzM4y0p8dbmvf?p=preview
Take a look at this example:
app.directive('goto', function($location){
function link(scope, element, attr){
scope.gotoLink = function(){
alert(scope.goto);
//$location.path(scope.goto);
};
}
return {
restrict: 'A',
transclude: true,
scope:{ goto: '#' },
template: '<span ng-click="gotoLink()"><span ng-transclude></span></span>',
link: link
};
});
You can play with it here (plnkr)

Angular directive for generating directives

I have a screen in my app which should show a list of components. Now, components can be different, showing different HTML content and with varying behaviour.
So, what I need is to do an ngRepeat, pass an object in each iteration, and according to some param in this object generate different directive.
This is the code that I had in mind - in the view:
<component ng-repeat="data in dataSet" config="data"></component>
Directive itself:
myApp.directive('component', function($compile) {
return {
restrict: 'E',
template: function(el, attrs) {
switch(attrs.config.type) {
case 'numeric':
return '<numeric config="data">this is numeric</numeric>';
case 'bar':
return '<another config="data"></another>';
}
}
};
});
Most of this is actually working, but the issue here that I couldn't get to fix is that the directive which is generated through template does not see the value of the config attribute. In other words, I want to pass the object from component to the directive inside, but that's not working.
One more approach I tried but ended up having the same issues is:
myApp.directive('component', function($compile) {
return {
restrict: 'E',
link: function(scope, el, attrs) {
var newElement;
switch(scope.config.type) {
case 'numeric':
newElement = $compile("<numeric config='data'>this is numeric</numeric>")(scope);
break;
case 'bar':
newElement = $compile("<another config='data'></another>")(scope);
break;
}
el.append( newElement );
}
};
});
Another thing here is that some of the generated directives will be the ones I pulled online, so ideally my solution should not involve modifying these.
I'm open to other approaches.

Angular Directive Different Template

I have a directive myDirective with variable type. If I run <my-directive type="X"> I want the directive to use templateUrl: x-template.html.
If I do <my-directive type="Y"> I want the directive to use templateUrl: y-template.html.
This is my current directive.
app.directive('myDirective', function() {
var myDirective = {
templateUrl: 'X-template.html',
restrict: 'E',
scope: {
type: '='
},
};
return myDirective;
});
I read thru stackoverflow and angular documentation but have not found anything that I need.
I am now trying to do something along the lines of:
if ($scope.type === 'X') {
templateUrl: 'X-template.html',
}
else if ($scope.type === 'Y') {
templateUrl: 'Y-template.html',
}
But do not know where to do it.
Do you guys know if this is possible and how?
Angular will accept a function as the template option, so you could do something like so:
.directive('myDirective', function () {
return {
templateUrl: function (tElement, tAttrs) {
if (tAttrs) {
if (tAttrs.type === 'X') {
return 'X-template.html';
}
if (tAttrs.type === 'Y') {
return 'Y-template.html';
}
}
}
}
});
For more info, see the documentation for the $compile service.
You can work around this issue using ng-include inside compile:
app.directive('myDirective', function() {
return {
restrict: 'E',
compile: function(element, attrs) {
element.append('<div ng-include="\'' + attrs.type + '-template.html\'"></div>');
}
}
});
fiddle
If you're willing to live on the bleeding edge with a build on the 1.1.x code path (note the warning attached to every 1.1.x build notes entry so I don't dilute this answer by repeating it again here), you're in luck--this very feature was just added in the 1.1.4 release on April 3rd. You can find the release notes for 1.1.4 here and the feature's task log includes a Jasmine test that demonstrates how to use the new functionality.
If you're more conservative and are using a 1.0.x release, then you won't be able to accomplish this as easily, but it can be done. Mark Rajcok's solution looks like it would fit your requirements as-stated, but I would just add a few additional notes:
Aside from its 1.1.4 release, compile-time directives don't support modification at runtime.
As of 1.1.4, you can safely modify the attributes of compile-time directives, but only from another compile-time directive.
You may want to consider replaceWith() instead of append() since <my-directive> is not a standard-defined HTML element type.
If your X and Y templates contain additional directives, I don't think you'll be able to pass attributes on <my-template> through to the root element of your template so easily.
A directive with replace: true will transfer attributes from the source element to its replacement root, but I do not think that ngInclude will do the same from is host to the root of the included template.
I also seem to recall that ngInclude does not require that its template have exactly one root element.
You could perhaps preserve attributes on a replacement parent by using replaceWith() instead of append() and wrapping the <div ng-include=""> tag within a <div></div>. The outer <div> could hold attributes and would still be accessible after the <div ngInclude> element replaced itself with loaded content.
Be aware that ngInclude creates a new scope. This subjects you to a flashing yellow klaxons warning about the dangers of primitive scope models. For more information, see this fine page from Angular's GitHub depot.
I can propose another alternative for those on 1.0.x, but it involves a fair amount of code. It's a more heavy-weight operation, but it has the upside of not only being able of switching between templates, but full-fledged directives as well. Furthermore, its behavior is more readily dynamic.
app.directive('myDirective', function() {
return {
restrict: 'E',
replace: true,
templateUrl: 'partials/directive/my-directive.html',
link: function(scope, element, attrs, ctrl) {
// You can do this with isolated scope as well of course.
scope.type = attrs.type;
}
}
);
my-directive.js
<div ng-switch on="{{type}}">
<div ng-switch-where="X" ng-include="X-template.html"></div>
<div ng-switch-where="Y" ng-include="Y-template.html"></div>
</div>
my-directive.html
This is my version for optionally overriding a default template
templateUrl: function (elem, attrs) {
if (attrs.customTemplate) {
return '/path/to/components/tmpl/' + attrs.customTemplate + '.html';
} else {
return '/path/to/components/tmpl/directive.html';
}
}
e.g on a directive
<div my-directive custom-template="custom"></div>
I solve this problem so:
app.directive("post", function ($templateCache, $compile) {
function getTemplate(mode) {
switch (mode) {
case "create":
return "createPost.html";
case "view":
return "viewPost.html";
case "delete":
return "deletePost.html";
}
}
var defaultMode = "view";
return {
scope: {},
restrict: "AE",
compile: function (elem, attrs, transclude) {
return function ($scope, $element, $attr) {
function updateTemplate() {
$element.html("");
$compile($templateCache.get(getTemplate($scope.mode)).trim())($scope, function (clonedElement, scope) {
clonedElement.appendTo($element);
});
}
$scope.mode = $attr.mode || defaultMode;
$scope.$watch("mode", updateTemplate);
}
}
}
});
It's probably not the best way to do this, but I have no extra scope.
Ok, this might help someone here :-)
To inject your custom attr into your link or controller function use the following.
I'm at work right now but will post a fiddle later if I get a chance :-)
.directive('yourDirective', function() {
return {
restrict: 'EA',
template: '<div></div>', // or use templateUrl with/without function
scope: {
myAttibute: '#myAttr' // adds myAttribute to the scope
},
link: function(scope) {
console.log(scope.myAttibute);
}
}
}
// HTML ""
// Console will output "foo"

Resources