Recursive angular directive with large data - angularjs

I have a angular directive to generate nested list structure. However when i get large data, browser gets stuck & is very slow. If it was only ng-repeat i could have used limitTo but this is a recursive template. Any suggestion please.
http://jsfiddle.net/L97o5swa/14/
treeModule.directive('tmTree', function() {
return {
restrict: 'E', // tells Angular to apply this to only html tag that is <tree>
replace: true, // tells Angular to replace <tree> by the whole template
scope: {
t: '=src',
fetchChildren: '&fetchChildren',
selectNode : '&selectNode' // create an isolated scope variable 't' and pass 'src' to it.
},
controller : function($scope){
console.log('aaa');
},
template: '<ul><branch ng-repeat="c in t.children" src="c" fetch-children="fetchChildren()" select-Node="selectNode({node :child})" ng-class="c.expandChildren ? \'\':\'collapsed\' "></branch></ul>' ,
link: function(scope, element, attrs) {
}
};
});
treeModule.directive('branch', function($compile) {
return {
restrict: 'E', // tells Angular to apply this to only html tag that is <branch>
replace: true, // tells Angular to replace <branch> by the whole template
scope: {
b: '=src',
fetchChildren: '&fetchChildren', // create an isolated scope variable 'b' and pass 'src' to it.
selectNode : '&selectNode'
},
controller : function($scope,$element){
} ,
template: '<li class="treeNode"><div class="wholerow"></div><span id="chevron-right" class="glyphicon glyphicon-chevron-right" ></span><a ng-click="selectNode({child : b})">{{ b.text }}</a></li>',
link: function(scope, element, attrs) {
//// Check if there are any children, otherwise we'll have infinite execution
var has_children = angular.isArray(scope.b.children);
var parent = scope.b;
//// Manipulate HTML in DOM
if (has_children) {
element.append($compile( '<tm-tree src="b" fetch-children="fetchChildren()" select-Node="selectNode({node:child})" ></tm-tree>')(scope) );
// recompile Angular because of manual appending
//$compile(element.contents())(scope);
}
var chevronRight = angular.element(element.children()[1]);
chevronRight.on('click',function(event) {
event.stopPropagation();
chevronRight.toggleClass('glyphicon-chevron-right');
chevronRight.toggleClass('glyphicon-chevron-down');
if(has_children){
element.toggleClass('collapsed');
if(scope.b.children.length == 0) {
}
}
});
}
};
});

Hard to tell based on the piece of code you posted. But my initial instinct is that you shouldn't be using so many jqLite references like element and append. You should handle more of this functionality within the template itself using ngRepeat (ie. if (has_children) { element.append...) and ngClick (ie. chevronRight.on('click'...). jqLite operations are expensive.

Related

Adding directives to an element using another directive

I am trying to create a directive that adds some html code but also adds additional attributes/directives.
Using the code below, an ng-class attribute is indeed added, but it seems angular does not recognize it as a directive anymore. It is there, but it has no effect.
How can I get it to work? Thanks.
The Angular code:
angular.module('myModule', [])
.directive('menuItem', function () {
return {
restrict: 'A',
template: '<div ng-if="!menuItem.isSimple" some-other-stuff>{{menuItem.name}}</div>'
+'<a ng-if="menuItem.isSimple" ng-href="{{menuItem.link}}">{{menuItem.name}}</a>',
scope: {
menuItem: '='
},
compile: function (element, attrs) {
element.attr('ng-class', '{active : menuItem.isActivated()}');
}
}
});
And the html:
<li menu-item="menuItem" ng-repeat="menuItem in getMenuItems()" />
EDIT:
The solution by #Abdul23 solves the problem, but another problem arises: when the template contains other directives (like ng-if) these are not executed. It seems the problem just moved.
Is it possible to also make the directives inside the template work?
Or perhaps insert the html using the compile function instead of the template parameter. Since I want a simple distinction based on some value menuItem.isSimple (and this value will not change), perhaps I can insert only the html specific to that case without using ng-if, but how?
You need to use $compile service to achieve this. See this answer.
For your case it should go like this.
angular.module('myModule', [])
.directive('menuItem', function ($compile) {
return {
restrict: 'A',
template: '<a ng-href="{{menuItem.link}}">{{menuItem.name}}</a>',
scope: {
menuItem: '='
},
compile: function (element, attrs) {
element.removeAttr('menu-item');
element.attr('ng-class', '{active : menuItem.isActivated()}');
var fn = $compile(element);
return function(scope){
fn(scope);
};
}
}
});

How to get values resolved in template without double curly brackets

I am using directive and template. My template is as follows
<div>
<a data-my-dir="item" data-attr1="true"
data-attr2="{{itemParentId}}"
data-attr3="{{item.id}}">
</a>
</div>
Here due to curly braces watches are added and it is affecting my performance. I don't want watches as I am not going to modify attr2 or attr3.
I want to directly resolved value here only.
We can use bindonce directive where we don't want watches where we can use bo-text="xyz" instead, but here I want to pass values as attr to my custom directive.
Inside my directive's link function I am binding click event with element as follows
link: function (scope, element, attrs) {
element.bind('click', function (event) {
var myAttr1 = attrs.attr1;
var myAttr2 = attrs.attr2;
}
}
So due to those watches in template on attr1 and attr2 I am getting these values resolved in click event.
What are the alternatives?
One time Binding
Seems like a good use case for one time binding (if you are using angular 1.3+)
<div>
<a data-my-dir="item"
data-attr1="true"
data-attr2="{{::itemParentId}}"
data-attr3="{{::item.id}}">
</a>
</div>
the directive would look like
angular.module('app', [])
.directive("myDir", function() {
return {
restrict: "A",
scope: {
"attr1": "#",
"attr2": "#",
"attr3": "#",
},
template: '<span> {{attr1}} {{attr2}} {{attr3}}</span>'
};
})
Demo
http://plnkr.co/edit/GJCZmb9CTknZZbcRHN7s?p=preview
You could use data-attr2="itemParentId" directly but for that you need to use = as currently you are using # option of directive.
app.directive('myDir', function(){
return {
scope: {
dataAttr1: '=', //or '=dataAttr1'
dataAttr2: '=' //or '=dataAttr2'
},
link: function(scope, element, attrs){
console.log(scope.dataAttr1);
console.log(scope.dataAttr2);
}
}
})

angularjs directive - get element bound text content

How do you get the value of the binding based on an angular js directive restrict: 'A'?
<span directiverestrict> {{binding}} </span>
I tried using elem[0].innerText but it returns the exact binding '{{binding}}' not the value of the binding
.directive('directiverestrict',function() {
return {
restrict:'A',
link: function(scope, elem, attr) {
// I want to get the value of the binding enclosed in the elements directive without ngModels
console.log(elem[0].textContent) //----> returns '{{binding}}'
}
};
});
You can use the $interpolate service, eg
.directive('logContent', function($log, $interpolate) {
return {
restrict: 'A',
link: function postLink(scope, element) {
$log.debug($interpolate(element.text())(scope));
}
};
});
Plunker
<span directiverestrict bind-value="binding"> {{binding}} </span>
SCRIPT
directive("directiverestrict", function () {
return {
restrict : "A",
scope : {
value : '=bindValue'
},
link : function (scope,ele,attr) {
alert(scope.value);
}
}
});
During the link phase the inner bindings are not evaluated, the easiest hack here would be to use $timeout service to delay evaluation of inner content to next digest cycle, such as
$timeout(function() {
console.log(elem[0].textContent);
},0);
Try ng-transclude. Be sure to set transclude: true on the directive as well. I was under the impression this was only needed to render the text on the page. I was wrong. This was needed for me to be able to get the value into my link function as well.

why does the mouse click in parent fires twice when a directive is injected during compile

I have a directive which needs to plug in an additional directive depending
on some model value. I am doing that in the [edit] pre-link phase of the directive.
The parent directive sets up a host of
methods and data which the child uses. Therefore I have child directive with
scope:true.
On the outer (or, parent) directive there is a click handler method. When I
click this, it gets fired twice. I want to know why and how. Presently the
only way I know how to stop that is by calling event::stopImmediatePropagation()
but I have a suspicion I am doing something wrong here.
template usage
<dashboard-box data-box-type="column with no heading">
<div class="card">
<div
ng-click="show($event)"
class="box-title item item-divider ng-binding is-shown"
ng-class="{'is-shown':showMe}">
<span class="box-title-string ng-binding">A/R V1</span>
</div>
<div class="box-content item item-text-wrap" ng-show="showMe">
<!-- plug appropriate dashbox here - in dashboard-box compile -->
<dashbox-column-with-no-heading>
<div>
SOME DATA...
</div>
</dashbox-column-with-no-heading>
</div>
</div>
</dashboard-box>
In the directive for dashboard-box:
scope: {
boxType: "#"
},
pre: function preLink(scope, iElement, iAttrs, controller) {
// some crazy hardcoding - because find by tagname...replaceWith throws in ionic
var parent_div = angular.element(angular.element(iElement.children()[0]).children()[1]);
if (!parent_div) {
return; // programmer error in template??
}
var html_tag_name = d_table_type_to_directive_name.xl[iAttrs.boxType];
parent_div.append(angular.element( "<" + html_tag_name + ">" ));
$compile(iElement.contents())(scope); // angular idiom
}
In the controller for dashboard-box:
$scope.show = function($e){
$log.log("dashboard box show(), was=", $scope.showMe, $e);
if ($e) $e.stopImmediatePropagation(); // <<<<<<<<<<< without this, double-hits!!
$scope.showMe = ! $scope.showMe;
// etc
};
In the directive for dashbox-column-with-row-heading:
restrict: "E",
scope: true,
templateUrl: "dashbox-column-with-row-heading.tpl.html"
controller: function(){
// specialized UI for this directive
}
I am using ionicframework rc-1.0.0 and angularjs 1.3.13.
What is happening here is that you are double-compiling/linking the ng-click directive: 1) first time Angular does that in its compilation phase - it goes over the DOM and compiles directives, first dashboardBox, then its children, together with ngClick), and 2) - when you compile with $compile(element.contents())(scope).
Here's a reduced example - demo - that reproduces your problem:
<foo>
<button ng-click="doFoo()">do foo</button>
</foo>
and the foo directive is:
.directive("foo", function($compile) {
return {
scope: true,
link: {
pre: function(scope, element, attrs, ctrls, transclude) {
$compile(element.contents())(scope); // second compilation
scope.doFoo = function() {
console.log("foo called"); // will be called twice
};
}
}
};
});
What you need to do instead is to transclude the content. With transclusion, Angular compiles the content at the time that it compiles the directive, and makes the content available via the transclude function.
So, instead of using $compile again, just use the already-compiled - but not yet linked (until you tell it what it should be linked to) - contents of the directive.
With the foo example below, this would look like so:
.directive("foo", function($compile) {
return {
scope: true,
transclude: true,
link: {
pre: function(scope, element, attrs, ctrls, transclude) {
transclude(scope, function(clone, transcludedScope){
var newEl = createNewElementDynamically();
$compile(newEl)(transcludedScope); // compile just the newly added content
// clone is the compiled-and-now-linked content of your directive's element
element.append(newEl);
element.append(clone);
});
scope.doFoo = function() {
console.log("foo called");
};
}
}
};
});

Binding To Element Text In AngularJS

Is it possible to bind to the text of an element without actually dropping into the link function?
<blink>Text Here or {{ controllerText() }}</blink>
// add a namespace for custom directives
angular.module('mydirectives', []);
angular.module('mydirectives').directive('blink', function() {
return {
restrict: 'E',
template: '<marquee scrollamount="100%">{{ can i do it here? }} </marquee>',
scope: {
// can i do it here?
}
};
});
So this is done with transclusion which merges the content of the original element with the template. The ng-transclude tag in the template is required to get it to work.
<blink>Bring the blink back<blink>
// add a namespace for custom directives
angular.module('mydirectives', []);
angular.module('mydirectives').directive('blink', function() {
return {
restrict: 'E',
transclude: true,
template: '<marquee scrollamount="100%" ng-transclude></marquee>'
}
});
You absolute can.
scope: {
text: '='
}
This adds a text attribute to the isolate scope that is linked to the value of the text attribute from the element.
So you need to change the html slightly to:
<blink text="fromController"></blink>
And then add that fromController attribute in the enclosing controller.
Here's a (very annoying) fiddle.

Resources