How to handle tree like structure in angularjs? - angularjs

I am trying to implement a hierarchical structure in the form of a tree in a complex angular.js web app. I am using nested ng-repeats to render the structure but I am getting significant performance related issues in IE 10 and minor performance issues in chrome. Data that will be used will contain as many as 5,000 entries at the final level.
Based on my research I think following could be the reasons behind it:
Large number of watcher elements.
To tackle this I have already implemented one time data binding and number of watchers is not that high.
Browser repaint time:
ng-repeat adds the elements to the DOM one by one. This could result in overloading of browser engine to render complex HTML multiple number of times causing large amount of lags.
To tackle this I have applied lazy loading sort of technique by rendering the child only when one node is collapsed. Still I am experiencing an observable delay in rendering of nodes where the number of nodes to be rendered is large.
CSS classes:
I tried implementing the tree structure by stripping off all the classes from node elements. This resulted in significant improvement but removing classes is not really an option. Also if I give inline style to the elements then it also results in better performance.
Performance issues of Angular material:
Angular material is the integral part of my web app. After looking into the issues submitted by angular material users for large amount of ng-repeats to which fixes have already been rolled out. But upgrading to latest version didn't help either.
Please refer to this image for the table design. Template used for the creation of tree is as follows:
<li ng-repeat="item in ::item.childLevelDetails" >
<div >
<a ng-click="toggle(this)" class="icon icon-stream-add-2">
<span></span>
</a>
<div class="unitTextDiv">{{::item.title}}</div>
</div>
<ol ng-include= "'tree_node'">
</li>
Request you to suggest any possible solutions to this problem.

angular-ui-grid can be considered as one option. It uses virtualization and renders only the rows that are visible. So, it performs well with huge number of rows too. It comes with a great documentation and examples http://ui-grid.info/docs/#/tutorial
Refer to the grouping example to make sure that this helps for your use case http://ui-grid.info/docs/#/tutorial/209_grouping

Time to use stick with this potion and finalize according to black magic!

You can try this recursive sample I did for you.
Using ng-if for showing/hidding element will reduce the amount of watches.
Here find the Fiddler
var myApp = angular.module('myApp',[]);
myApp.controller('MyCtrl', ['$scope','$timeout', 'getWatchCount' , function ($scope ,$timeout, getWatchCount ){
$scope.tree = [
{title:'Element level 1',
elements: [
{ title: 'Element level 1.1'},
{ title: 'Element level 1.2',
elements: [
{ title: 'Element level 1.2.2'},
{ title: 'Element level 1.2.2'},
]}
]},
{title:'Element level 2'}
]
//NEXT CODE ONLY USED FOR COUNTING WATCHES//
$scope.countWatches = function(){
$scope.numberOfWatches = getWatchCount();
}
$timeout(function(){$scope.countWatches()} , 0 );
// I return the count of watchers on the current page.
function getWatchCount() {
// Keep track of the total number of watch bindings on the page.
var total = 0;
// There are cases in which two different ng-scope markers will actually be referencing
// the same scope, such as with transclusion into an existing scope (ie, cloning a node
// and then linking it with an existing scope, not a new one). As such, we need to make
// sure that we don't double-count scopes.
var scopeIds = {};
// AngularJS denotes new scopes in the HTML markup by appending the classes "ng-scope"
// and "ng-isolate-scope" to appropriate elements. As such, rather than attempting to
// navigate the hierarchical Scope tree, we can simply query the DOM for the individual
// scopes. Then, we can pluck the watcher-count from each scope.
// --
// NOTE: Ordinarily, it would be a HUGE SIN for an AngularJS service to access the DOM
// (Document Object Model). But, in this case, we're not really building a true AngularJS
// service, so we can break the rules a bit.
angular.forEach(
document.querySelectorAll( ".ng-scope , .ng-isolate-scope" ),
countWatchersInNode
);
return( total );
// ---
// PRIVATE METHODS.
// ---
// I count the $watchers in to the scopes (regular and isolate) associated with the given
// element node, and add the count to the running total.
function countWatchersInNode( node ) {
// Get the current, wrapped element.
var element = angular.element( node );
// It seems that in earlier versions of AngularJS, the separation between the regular
// scope and the isolate scope where not as strong. The element was flagged as having
// an isolate scope (using the ng-isolate-scope class); but, there was no .isolateScope()
// method before AngularJS 1.2. As such, in earlier versions of AngularJS, we have to
// fall back to using the .scope() method for both regular and isolate scopes.
if ( element.hasClass( "ng-isolate-scope" ) && element.isolateScope ) {
countWatchersInScope( element.isolateScope() );
}
// This class denotes a non-isolate scope in later versions of AngularJS; but,
// possibly an isolate-scope in earlier versions of AngularJS (1.0.8).
if ( element.hasClass( "ng-scope" ) ) {
countWatchersInScope( element.scope() );
}
}
// I count the $$watchers in the given scope and add the count to the running total.
function countWatchersInScope( scope ) {
// Make sure we're not double-counting this scope.
if ( scopeIds.hasOwnProperty( scope.$id ) ) {
return;
}
scopeIds[ scope.$id ] = true;
// The $$watchers value starts out as NULL until the first watcher is bound. As such,
// the $$watchers collection may not exist yet on this scope.
if ( scope.$$watchers ) {
total += scope.$$watchers.length;
}
}
}
}]);
myApp.factory(
"getWatchCount",
function() {
// I return the count of watchers on the current page.
function getWatchCount() {
var total = 0;
// AngularJS denotes new scopes in the HTML markup by appending the
// class "ng-scope" to appropriate elements. As such, rather than
// attempting to navigate the hierarchical Scope tree, we can simply
// query the DOM for the individual scopes. Then, we can pluck the
// watcher-count from each scope.
// --
// NOTE: Ordinarily, it would be a HUGE SIN for an AngularJS service
// to access the DOM (Document Object Model). But, in this case,
// we're not really building a true AngularJS service, so we can
// break the rules a bit.
angular.element( ".ng-scope" ).each(
function ngScopeIterator() {
// Get the scope associated with this element node.
var scope = $( this ).scope();
// The $$watchers value starts out as NULL.
total += scope.$$watchers
? scope.$$watchers.length
: 0
;
}
);
return( total );
}
// For convenience, let's serialize the above method and convert it to
// a bookmarklet that can easily be run on ANY AngularJS page.
getWatchCount.bookmarklet = (
"javascript:alert('Watchers:'+(" +
getWatchCount.toString()
.replace( /\/\/.*/g, " " )
.replace( /\s+/g, " " ) +
")());void(0);"
);
return( getWatchCount );
}
);
ul{
list-style-type: none;
}
li{
font-size:13px;
}
.arrow{
width: 0;
height: 0;
border-top: 7px solid transparent;
border-bottom: 7px solid transparent;
border-right: 7px solid transparent;
cursor: pointer;
margin-left: 5px;
border-left: 7px solid #000;
display: inline-block;
transition:all 0.3s;
}
.arrow.expand {
transform: rotate(45deg);
transform-origin: 20% 50%;
margin-top: 0;
}
.arrow.none {
border-left: 7px solid #ccc;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp" ng-controller="MyCtrl" >
<p>
<strong>Watch Count:</strong> {{ numberOfWatches }}
</p>
<script type="text/ng-template" id="elementTree">
<li>
<div class="arrow"
ng-class="{expand:element.isOpen,none:!element.elements}"
ng-click="$apply(element.isOpen = !element.isOpen) ; countWatches()">
</div>
{{element.title}}
</li>
<div ng-if="element.isOpen">
<ul
ng-repeat="element in element.elements"
ng-include="'elementTree'">
</ul
</div>
</script>
<ul ng-repeat="element in tree"
ng-include="'elementTree'">
</ul>
</div>

Related

Customizing Day Cell content in FullCalendar

I am using fullcalendar with react. I am trying to customize the dayGrid view. According to the Content Injection docs for react I can use custom content for the rendering of both the date and the header cells. The dayCellContent "hook" states that:
Generated content is inserted inside the inner-most wrapper of the day cell. It does not replace the cell.
I've provided an implementation for the dayCellContent and noticed that my content gets injected into the following structure:
<td class="fc-daygrid-day fc-day fc-day-wed fc-day-past rot_time-off_day-cell" data-date="2021-04-07">
<div class="fc-daygrid-day-frame fc-scrollgrid-sync-inner">
<div class="fc-daygrid-day-top">
<a class="fc-daygrid-day-number">
...custom content goes here
</a>
</div>
<div class="fc-daygrid-day-events"></div>
<div class="fc-daygrid-day-bg"></div>
</div>
</td>
Now, the problem is that this structure lets you insert content ONLY in the upper right corner of the date cell due to the positioning of the element. Furthermore, it is in an anchor element.
Example:
function renderDayCell(dayCellContent: DayCellContentArg) {
return (
<div>
{dayCellContent.dayNumberText}
</div>
);
}
Is there a clean way to customize the whole content of the cell somehow? I've seen a couple of sites using fullcalendar that have their content inserted directly into the td. Not sure if this is version dependent or they're using the alternative JS approach based on domNodes or html. I am using version 5.6.0 of fullcalendar.
I had the same requirement although not using React. I solved it using a manual manipulation of the DOM elements as suggested above. I have used jQuery for the select and manipulation. It is posted here in case anyone would like to see an example of how this can be achieved using DOM manipulation.
I implemented dayCellContent to make the day-cell DOM element easily identifiable by wrapping it in a span, with a unique id attribute based on the day of year number:
dayCellContent: function(info, create) {
const element = create('span', { id: "fc-day-span-"+info.date.getDayOfYear() }, info.dayNumberText);
return element;
},
This dayCellContent implementation makes no visible difference to the calendar but makes it easier to identify the elements to be modified in the DOM.
I then implemented dayCellDidMount to do the DOM manipulation by finding the appropriate cells and selecting their parent’s parent:
dayCellDidMount: function(info) {
let element = "<div style='position: absolute; left: 4px; top: 4px;'><a href='https://www.w3schools.com/'>TEST-"+info.dayNumberText+"</a></div>";
$('#fc-day-span-'+info.date.getDayOfYear()).parent().parent().prepend(element);
},
In this case I have just put a link to w3c in the top left of the cell with test text which also includes the day number. It results in cells that look like this:
Clearly the CSS could be improved and should be moved out to the CSS definitions but it illustrates the point.
Warning: This approach makes assumptions about the DOM structure that FullCalendar generates. The generated HTML may change in future versions of the product which could invalidate it. If you go this way then be careful when doing a FullCalendar update.
Note that the getDayOfYear function is from the ext-all.js library. Any way of uniquely identifying the day will work.
ngAfterViewInit(){
// Your CSS as text
var styles =.fc td, .fc th { vertical-align: top; padding: 0; height: 100px; } a{ color:#3d1cba; }
let styleSheet = document.createElement("style");
styleSheet.innerText = styles;
document.head.appendChild(styleSheet);
let arrTD = document.querySelectorAll('td.fc-timeline-slot');
let arrTR= document.querySelectorAll('td.fc-timeline-lane.fc-resource');
let arrInject= document.querySelectorAll('td.fc-timeline-lane.fc-resource>div.fc-timeline-lane-frame');
console.log(arrTR);
let k=-1;
arrTR.forEach(eachTR => {
let i=1;
let str = '';
k++;
let data_resource_id= eachTR.getAttribute('data-resource-id');
console.log(data_resource_id);
arrTD.forEach(eachTD => {
let k=100*(i-1);
i=i+1;
let data_date= eachTD.getAttribute('data-date');
console.log(data_date);
let data_resource_id= eachTR.getAttribute('data-resource-id');
console.log(data_resource_id);
str = str + '<span data-date="'+data_date+'" data-resource-id="'+data_resource_id+'" class="plus_icon" style="position:relative;top: 0px; left: '+k+'px !important;width:500px;height:500px;z-index:3;-moz-border-radius:100px;border:1px solid #ddd;-moz-box-shadow: 0px 0px 8px #fff;">+</span>';
});
arrInject[k].innerHTML=str;
});
let elementList = this.elRef.nativeElement.querySelectorAll('span.plus_icon');
for(let i=0;i<elementList.length;i++){
elementList[i].addEventListener('click', this.plusClick.bind(this));
}
}

Error: ngRepeat:dupes Duplicate Key in Repeater (need track by $index)

https://plnkr.co/edit/bpFi5WuojpNO2rh5vF3T?p=preview
See the README in the Plunker for the following explaination:
I would like the "INJECT NEW" button to create a blank input under
the one that was clicked, not at the end.
The reason they are getting added at the end is because of :
<div ng-repeat="problem in problems track by $index">
The track by $index is breaking the injection.
If I take out the track by $index then I get the error:
https://docs.angularjs.org/error/ngRepeat/dupes?p0=problem%20in%20problems&p1=object:171&p2={%22key%22:null,%22component%22:null,%22$$hashKey%22:%22object:171%22}
How can I have the inject functionality but not get the error?
Can change the method like below and can get it working :
$scope.addMotFault = function(idx) {
if ($scope.problems.length > 1) {
// Now more than one item, we need to
// inject the additional one under the clicked item
// this index + 1
problemPrototype.key = idx + 1;
$scope.problems.splice(idx + 1, 0, angular.copy(problemPrototype));
} else {
// Only one item, so just push new problem
// no need to "inject"
problemPrototype.key = 0;
$scope.problems.push(angular.copy(problemPrototype));
}
};
html:
<div ng-repeat="problem in problems" style="border: 1px #ccc solid; margin:5px; padding: 5px">
It would work i believe.
As per your question I am sending you the required answer.
Please find the attached link and go through the code. you will find the solution.
https://plnkr.co/edit/nhqAnD1hSKuIs9HTsT30?p=preview
you can use ng-if in place $first and can check on the basis on length.
<button ng-click="removeMotFault($index)" ng-if="problems.length > 1">REMOVE</button>

Strange behaviour of angular bootstrap collapse

I faced with strange behaviour of uib-collapse.
Let's assume I have a list of elements and i want each of them to be collapsed. Also i want to refresh its content periodically depend on something.
For example: i have some items and each of them have description which consists of some sections. I can pick item and description sections should be populated with item's description content. The problem is that each time i refresh its content, some sections are collapsing (despite the fact i set uib-collapse to false)
My controller:
var i = 0;
$scope.sections = [0,1,2];
$scope.next = function(nextOffset) {
i+=nextOffset;
$scope.sections = [i, i+1, i+2]
}
My template:
<button ng-click="next(1)" style="margin-bottom: 10px">Next item</button>
<button ng-click="next(2)" style="margin-bottom: 10px">Next next item</button>
<button ng-click="next(3)" style="margin-bottom: 10px">Next next next item</button>
<div ng-repeat="section in sections">
<div uib-collapse="false">
<div class="well well-lg">{{ section }}</div>
</div>
</div>
So when i click first button, only one section does transition. When i click second, 2 section do transition and click to third button leads to all section transition.
See plunkr
Any ideas?
UPD: if $scope.sections is array of object, not of primitives, then all sections have transition in each of 3 cases. It is so ugly...
You are not refreshing the existing content, you are adding new arrays each time, which will make ng-repeat remove the old DOM elements and insert new ones.
If you try with track by $index you will see the difference:
<div ng-repeat="section in primitiveSections track by $index">
Demo: http://plnkr.co/edit/hTsVBrRLa8nWXhaqfhVK?p=preview
Note that track by $index might not be the solution you want in your real application, I just used it for demonstration purposes.
What you probably need is to just modify the existing objects in the array.
For example:
$scope.nextObject = function(nextOffset) {
j += nextOffset;
$scope.objectSections.forEach(function (o, i) {
o.content = j + i;
});
};
Demo: http://plnkr.co/edit/STxy1lAUGnyxmKL7jYJH?p=preview
Update
From the collapse source code:
scope.$watch(attrs.uibCollapse, function(shouldCollapse) {
if (shouldCollapse) {
collapse();
} else {
expand();
}
});
When a new item is added the watch listener will execute, shouldCollapse will always be false in your case so it will execute the expand function.
The expand function will always perform the animation:
function expand() {
element.removeClass('collapse')
.addClass('collapsing')
.attr('aria-expanded', true)
.attr('aria-hidden', false);
if ($animateCss) {
$animateCss(element, {
addClass: 'in',
easing: 'ease',
to: {
height: element[0].scrollHeight + 'px'
}
}).start().finally(expandDone);
} else {
$animate.addClass(element, 'in', {
to: {
height: element[0].scrollHeight + 'px'
}
}).then(expandDone);
}
}
If this is the intended behavior or not I don't know, but this is the reason why it happens.
this is a comment on the original ui-bootstrap library: (and the new uib prefixed directive doesn't comply this comment.)
// IMPORTANT: The height must be set before adding "collapsing" class.
Otherwise, the browser attempts to animate from height 0 (in
collapsing class) to the given height here.
use the deprecated "collapse" directive instead of new "uib-collapse" until it gets fixed.

How to mix ngClass Map Syntax and Boolean Operators

How can I use ngClass map syntax with the && and ! boolean operators?
<div class="tbTileContent"
ng-class=
"{'highlight': ((obj.Count == 1 &&
setActionType != 1 && setActionType != 20 )},[obj.BackgroundColor,obj.ColumnColor]">
Does not work. Is this possible some other way?
Use equality checks with reference to the rootScope to simplify debugging, and use the developer tool to check CSS specificity:
.foo { color: red; }
.bar { color: blue; }
.baz { color: green; }
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script>
function bar($rootScope)
{
$rootScope.conditionA = true;
$rootScope.conditionB = function() { return true; };
}
angular.module('foo',[]);
angular.module('foo').run(bar);
</script>
<div ng-app="foo">
<div class="existing" ng-class="{baz: conditionA && conditionB(), foo: conditionA, bar: conditionB()}">
Hi
</div>
</div>
Run blocks are the closest thing in Angular to the main method. A run block is the code which needs to run to kickstart the application. It is executed after all of the services have been configured and the injector has been created. Run blocks typically contain code which is hard to unit-test, and for this reason should be declared in isolated modules, so that they can be ignored in the unit-tests.
References
AngularJS Developer Guide: Modules - Run Blocks
AngularJS Source: ngClassSpec.js - 'should support adding multiple classes conditionally via a map of class names to boolean'
Angular.js Source: parseSpec.js - 'should give higher precedence to equality than to the logical and operator'

AngularJs get count of element by class name

How can I count element by class name in angularJs?
I have tried with:
$scope.numItems = function() {
$window.document.getElementsByClassName("yellow").length;
};
Plunkr: http://plnkr.co/edit/ndCqpZaALfOEiYieepcn?p=preview
You have defined your function correctly, but made a mistake in showing its results: it should have been...
<p>{{numItems()}}</p>
... instead of plain {{numItems}}. You want to display the return value of the function, and not the function itself (that's meaningless), that's why you should follow the standard JS syntax for a function invocation.
Note that you can send arguments into this expression too: for example, I've rewritten that method like this:
$scope.numItems = function(className) {
return $window.document.getElementsByClassName(className).length;
};
... and then made three different counters in the template:
<p>Yellow: {{numItems('yellow')}}</p>
<p>Green: {{numItems('green')}}</p>
<p>Red: {{numItems('red')}}</p>
Plunker Demo.
But here's the real problem: numItems() result, used in one View, is based on DOM traversal - in other words, on another View. Not only that goes against Angular philosophy in general, it tends to break. In fact, it DOES break since this commit, as old as 1.3.0:
Now, even when the ngAnimate module is not used, if $rootScope is in
the midst of a digest, class manipulation is deferred. This helps
reduce jank in browsers such as IE11.
See, changes in classes are applied after digest - and that's after numItems() is evaluated, hence the delay in demo mentioned by #Thirumalaimurugan.
A quick-and-dirty solution is using another attribute for selector in numItems (in this plunker, it's data-color). But I would strongly advise you against it. The proper approach would be adding the data rendered by numItems() -using component into the model. For example:
app.js
// ...
var colorScheme = {
'toggle': {true: 'yellow', false: 'red'},
'toggle2': {true: 'green', false: 'red'},
'toggle3': {true: 'green', false: 'red'},
'toggle4': {true: 'red', false: 'green'}
};
$scope.getColor = function getColor(param) {
return colorScheme[param][$scope[param]];
};
$scope.countColor = function(color) {
return Object.keys(colorScheme).filter(function(key) {
return colorScheme[key][$scope[key]] === color;
}).length;
};
index.html
<p ng-class="getColor('toggle')">{{name}}</p>
<!-- ... -->
<p ng-repeat="color in ['Yellow', 'Green', 'Red']"
ng-bind="color + ' ' + countColor(color.toLowerCase())">
Demo.

Resources