Restrict AngularJS ng-style - angularjs

I'm calculating containers heights as per viewport by ng-style through my custom method.
Everything works well, but it keeps calling the method even if the element is styled. I have a big amount of DOM elements that are need to be styled. That's why, I can't allow continuous execution for all elements. Please note, I can't use ng-class because each element contains different content. And can't use controller scope variable due to unlimited numbers of elements.
HTML:
<div class="myElement" ng-style="styleElement('myElement')">
...
...
</div>
Function:
$scope.styleElement = function (elementCls) {
var elementToSet = $('.'+elementCls+':visible');
if(elementToSet.length){
var winHeight = $( window ).height();
var eTop = elementToSet.offset().top;
if(eTop == 0){
var elemChilds = elementToSet;
var elemChildsLen = elemChilds.length;
for(var i=0;i<elemChildsLen;i++){
var elem = elemChilds[i];
var r = elem.getBoundingClientRect();
if(r.top != 0){
eTop = r.top;
i= elemChildsLen;
}
}
}
var nScrollHeight = winHeight - eTop - 20;
return {
'height': nScrollHeight + 'px',
'overflow-x': 'hidden',
'overflow-y': 'auto'
};
}
};
I've tried using a custom directive but binding DOM or writing a watcher isn't a preferable solution for me due to performance. Thanks in advance!

Use one time binding, which will only call styleElement once.
<div class="myElement" ng-style="::styleElement('myElement')">
...
...
</div>

Related

AngularJS ng repeat performance

I have a ng-repeat on my html view. The array it's repeating is populated each time a user selects a checkbox. It works like a filter way.
However, I am having issues with performance. It seems to create the DOM elements again and thus doing this there is a 1-1.5 second 'freeze' delay before the results are populated on the user interface again.
The array it's repeating isnt that big - probably around 50-60 entries. Each entry in the array has two objects which do have a lot of properties. Will this affect the performance? From what I've read it seems to be because it's creating the DOM elements again.
I have tried using track by $index, which speeds it up drastically but this causes problems on the div boxes I'm displaying. Text is on the wrong results, gets mixed up etc. I have also tried using track by ($index + item). No luck - same problem. I've also tried using track by item.id - but this has the same effect of not using track by - slow.
Is there anything I can do to optimize this? Or do I just bite the bullet?
Here is my code below:
<div ng-if="$ctrl.hasDataProcessed() && $ctrl.resultsAvailable()">
<div class="acca-builder-content">
<div class="acca-builder-header" style="border: 1px solid #1393ED;">{{"RESULTS" | translate}} ({{$ctrl.accaBuilderResultsCount}})</div>
<ul class="tips-list-group-matches">
<li ng-repeat="result in $ctrl.accaBuilderResults | orderBy: $ctrl.getSort" ng-class="{'match-has-link': $ctrl.canViewMatch(result.match)}" class="tip-list-group-match">
<tf-competition-header ng-if="result.match.CompMasterID" competition="result.match"></tf-competition-header>
<match-header match="result.match" tracking-screen="Tips"></match-header>
</li>
</ul>
</div>
</div>
</div>
And within the controller:
var buildAccaResultsFromFilter = function () {
var results = [];
var tips = ctrl.tips;
for (var i = 0; i < tips.length; i++) {
var tip = tips[i];
if(valueInFilter("COMPETITIONS", tip.match.CompID) &&
valueInFilter("DATES", tip.match.MatchDateConverted) &&
valueInFilter("SHOW", tip.tip.TipType)) {
results.push(tip);
}
}
if(results.length > 0) {
ctrl.accaBuilderResults = results;
ctrl.accaBuilderResultsCount = results.length;
ctrl.resultsFound = true;
} else {
clearAccaBuilderResults();
ctrl.resultsFound = false;
}
};
// Function called when a checkbox is clicked
ctrl.onCheckboxChange = function (option, item) {
item.checkState = !item.checkState;
if(item.checkState) {
addToFilter(option.optionKey, item.textKey);
}
else {
removeFromFilter(option.optionKey, item.textKey);
}
if(option.onChange) {
option.onChange(item.checkState, item.checkId);
}
if(ctrl.canBuildAccaResults()) {
buildAccaResultsFromFilter();
} else {
clearAccaBuilderResults();
}
};
// Checks if a value is present within the filter by it's key
var valueInFilter = function (filterKey, value) {
return ctrl.filter[filterKey].includes(value);
};
ctrl.resultsAvailable = function () {
return ctrl.accaBuilderResults && ctrl.accaBuilderResults.length > 0;
};
ctrl.hasDataProcessed = function () {
return ctrl.tips && ctrl.competitions;
};
Without seeing some code it is tough to optimize.
You can try and eliminate any ng-show and ng-hide and use ng-if only. It will remove a lot of watchers if you have those present.
Additionally you can use the syntax below in your directives if you don’t need two-way binding. This will also remove a watcher. The more watchers you can remove in ng-repeat, the better.
{{:: expression }}

How to pass a variable to css in AngularJS

I'm trying to make a virtual scroll and whenever the user scrolls down I need to add a negative top equal to the container height to each row. But of course this top property can vary depending of some factors like the user's screen resolution or browser window size.
So far this is what I got:
<div class="container" id="my-container">
<!--If it has the class row-scrolled the top property is applied-->
<div ng-repeat="(row) in virtualCollection"
ng-class="{'row-scrolled': controller.isScrolled}">
<!-- row properties -->
</div>
</div>
I have also thought about the idea of using ng-style but would override any style from my .css file.
Is there anyway to get the size/property of a DOM element...
// controller
var containerHeight = angular.element('#my-container')[0].clientHeight;
var cssProperty = '-' + containerHeight + 'px';
And then use it in an css?
// css
.row-scrolled {
top: cssProperty;
}
You can't pass variables from javascript to CSS since CSS is not a programming language but a style sheet language.
What you can do is manipulating specific elements with javascript.
Based on your code here is an example:
// controller
var containerHeight = angular.element('#my-container')[0].clientHeight;
var cssProperty = '-' + containerHeight + 'px';
var $$rowScrolled = document.querySelectorAll(".row-scrolled");
if ($$rowScrolled && $$rowScrolled.length > 0) {
for (var i = 0; i < $$rowScrolled.length; i++) {
var $rowScrolled = $$rowScrolled[i];
$rowScrolled.style.top = cssProperty;
}
}
With jQuery:
// controller
var containerHeight = angular.element('#my-container')[0].clientHeight;
var cssProperty = '-' + containerHeight + 'px';
var $rowScrolled = $(".row-scrolled");
if ($rowScrolled && $rowScrolled.length > 0) {
$rowScrolled.css("top", cssProperty);
}
You can not pass a variable to the CSS.
What you can do though is add the property directly using the ng-style tag:
<div class="container" id="my-container">
<!--If it has the class row-scrolled the top property is applied-->
<div ng-repeat="(row) in virtualCollection"
ng-style="{'top': controller.cssProperty}">
<!-- row properties -->
</div>

How to set the height of the body equals to the length of an array in Angular

I have this user list, which can grow dynamically if you add a new user. Now I want my whole body to be the length of the user list.
In my factory I determine the length of the array and now I need to know how to access/translate this to the view in angular. Something like this:
<div class="mainWrapper" id="mainView" style="width: {{gridSizeNG.x * 122}}px; height: {{gridSizeNG.y * the length of the user list }}">
...
</div>
Part of my factory code:
UserService.getUsers = function () {
$http.get("api/users") //your API url goes here
.success(function(dataFromServer){
//console.log('LOGGING DATADROMSERVER ', dataFromServer);
//UserService.userList = [];
/*dataFromServer.forEach(function(user, index, arr) {
UserService.userList.push(user);
})*/
var initials = function(name){
var d1 = name.split(" ")[0].charAt(0).toUpperCase();
var d2;
try
{
d2 = name.split(" ")[1].charAt(0).toUpperCase();
}
catch(e){
d2 = "";
}
return d1 + d2;
console.log('LOGGING INITIALS ', d1 + d2);
}
for (var i = 0; i < dataFromServer.length; i++) {
UserService.userList[i] = dataFromServer[i];
UserService.userList[i].initials = initials(UserService.userList[i].name)
};
console.log('#### logging lenght of the userlist ' , UserService.userList.length );
//here you should update the usersList from the server like this:
//UserService.usersList = dataFromServer;
return dataFromServer;
})
.error(function(errorFromServer){
//something went wrong, process the error here
console.log("Error in getting the users from the server");
})
};
return UserService;
})
As you can see from the image, the height of the body is now not dynamic
IMO the best approach here would be to use CSS to your advantage and have the size grow in accordance with the amount of elements you have. Which means (idealy) you don't need to set the height and width property in CSS, you only need to set the max-height and max-width properties, should the list grow more than the body.
If you do want to control the height and width using JS then use ng-style instead of style, keep in mind ng-style is considered a last resort in controlling styles, one should ideally use ng-class if possible.
With ng-style you will be able to use your scope variables to control the height and width.
Also try to use something like this, using your existing html code:
<div class="mainWrapper" id="mainView" style="width: {{gridSizeNG.x}} * 122 + 'px'; height: {{gridSizeNG.y}} * {{the length of the user list }} + 'px'">
...
</div>
Not sure if the above code will work, but ng-style will.

Looking for a better way to loop through and display one element at a time in AngularJS

I'm building an app that has animations in it and I need it to work better. I want to be able to go through a series of divs at a specified animation interval and display one at a time. And I want each series to have its own speed and its own divs.
Here's what I have so far (also copied below): http://jsfiddle.net/ollerac/shkq7/
Basically, I'm looking for a way to put the setInterval on a property of of the animatedBox so I can create a new animatedBox with custom properties. But every time I try to do this it breaks.
HTML
<div ng-app ng-controller="BoxController">
<div class="layer" ng-repeat="layer in animatedBox.layers" ng-style="{ 'backgroundColor': layer.color}" ng-show="layer == animatedBox.selectedLayer"></div>
</div>
JAVASCRIPT
function buildBox () {
return {
color: '#' + (Math.random() * 0xFFFFFF << 0).toString(16)
}
}
function BoxController ($scope) {
$scope.animatedBox = {
layers: [],
selectedLayer: null
};
for (var i = 0; i < 5; i++) {
$scope.animatedBox.layers.push(buildBox());
}
var i = -1;
setInterval(function () {
$scope.$apply(function() {
i++;
if (i < $scope.animatedBox.layers.length) {
$scope.animatedBox.displayThisLayer = $scope.animatedBox.layers[i];
} else {
i = 0;
$scope.animatedBox.selectedLayer = $scope.animatedBox.layers[i];
}
});
}, 500);
}
CSS
.layer {
width: 30px;
height: 30px;
position: absolute;
}
*Update*
Here's more along the lines of what I want to do:
updated jsFiddle: http://jsfiddle.net/ollerac/shkq7/2/
function buildBox () {
return {
color: '#' + (Math.random() * 0xFFFFFF << 0).toString(16)
}
}
function BoxController ($scope) {
$scope.animatedBox = {
layers: [],
selectedLayer: null,
selectedLayerIndex: -1,
updateSelectedLayer: function () {
var self = this;
if (self.layers.length) {
$scope.$apply(function() {
self.selectedLayerIndex++;
if (self.selectedLayerIndex < self.layers.length) {
self.selectedLayer = self.layers[self.selectedLayerIndex];
} else {
self.selectedLayerIndex = 0;
self.selectedLayer = self.layers[self.selectedLayerIndex];
}
});
}
}
};
for (var i = 0; i < 5; i++) {
$scope.animatedBox.layers.push(buildBox());
}
setInterval(function () {
$scope.animatedBox.updateSelectedLayer();
}, 500);
}
So now the object updates its own selectedLayer property. But I still need to call the setInterval that calls the update separately in order to get it to update. But I'd like the object to update itself and be completely independent. Can you think of a good way to do this because I'm really stuggling with it...
I guess this is more of a general javascript question, but I thought there might be an Angular way to handle this type of situation, like maybe using a directive or something would be appropriate.
Any suggestions would be much appreciated.
You are correct, I believe a directive is the right solution here. (This one was a fun one to work on, by the way. :)
When approaching a problem like this, I usually start by writing the HTML and controller that I'd wish I could write, if everything already worked. For this example, here's what I ended up with.
<div ng-controller="BoxController">
<div animated-boxes="colors"></div>
</div>
app.value('randomColor', function() {
var red = Math.floor(Math.random() * 255);
var green = Math.floor(Math.random() * 255);
var blue = Math.floor(Math.random() * 255);
return "rgb(" + red + "," + green + "," + blue + ")";
});
app.controller('BoxController', function($scope, randomColor) {
$scope.colors = [ randomColor(), randomColor() ];
});
Here, the controller is only responsible for setting some basic data on the scope--an array of colors; the DOM is very simple, only passing in that array to something called animated-boxes. randomColor has been moved into a service so it can be reused and tested more easily. (I also changed it a bit so it doesn't result in bad hex values.)
Now, the only part that doesn't already work is this thing called animated-boxes. Any time you want to interact with the DOM, or to trigger some behavior when an HTML attribute is used, we move to a directive.
We'll define our directive, injecting the $timeout service since we know we want to do timer-based stuff. The result of the directive will just be an object.
app.directive('animatedBoxes', function($timeout) {
return {
};
});
Since we want the directive to be self-contained and not mess up the outer scope in which it's contained, we'll give it an isolate scope (see the directive docs for more information, but basically this just means we have a scope that's not attached to the scope in which the directive lives except through variables we specify.)
Since we want to have access to the value passed in to the directive via the HTML attribute, we'll set up a bi-directional scope binding on that value; we'll call it colors.
app.directive('animatedBoxes', function($timeout) {
return {
scope: {
colors: '=animatedBoxes'
}
};
});
We'll give it a simple template that loops over colors and outputs one of our divs per each color. Our ng-show indicates that the div should only be shown if the scope value selected is equal to $index, which is the array index of the current iteration of the ng-repeat loop.
app.directive('animatedBoxes', function($timeout) {
return {
scope: {
colors: '=animatedBoxes'
},
template: "<div><div class='layer' ng-repeat='color in colors' " +
"ng-style='{backgroundColor: color}' ng-show='selected == $index'>" +
"</div></div>"
};
});
Now for the link function--the function that will handle the directive's logic. First, we want keep track of which box we're showing; in our ng-show, we used selected for this. We also want to keep track of how many boxes we have; we'll use $watch on our directive's scope to keep up with this.
link: function(scope, elem, attrs) {
scope.selected = 0;
var count = 0;
scope.$watch('colors', function(value) {
// whenever the value of `colors`, which is the array
// of colors passed into the directive, changes, update
// our internal count of colors
if (value) count = value.length;
else count = 0; // if `colors` is falsy, set count to 0
}, true); // `true` ensures we watch the values in the array,
// not just the object reference
}
Finally, we need to cycle through each box every so often. We'll do this with $timeout, which is a version of setTimeout that includes a scope $apply call (it does some other stuff, but we don't care about that now).
var nextBox = function() {
if (scope.selected >= count - 1) scope.selected = 0;
else scope.selected++;
// recursively use `$timeout` instead of `setInterval`
$timeout(nextBox, 500);
};
// kick off the directive by launching the first `nextBox`
nextBox();
If you put the entire directive so far together, you'll end up with this code (comments removed):
app.directive('animatedBoxes', function($timeout) {
return {
scope: {
colors: '=animatedBoxes'
},
template: "<div><div class='layer' ng-repeat='color in colors' " +
"ng-style='{backgroundColor: color}' ng-show='selected == $index'>" +
"</div></div>",
link: function(scope, elem, attrs) {
scope.selected = 0;
var count = 0;
scope.$watch('colors', function(value) {
if (value) count = value.length;
else count = 0;
}, true);
var nextBox = function() {
if (scope.selected >= count - 1) scope.selected = 0;
else scope.selected++;
$timeout(nextBox, 500);
};
nextBox();
}
};
});
A full working example, including comments and a little debugging area where you can see the value of colors and interact with it (so you can see how the directive responds to changes in the controller) can be found here: http://jsfiddle.net/BinaryMuse/g6A6Y/
Now that you have this, consider trying to apply this knowledge to allow the directive to have a variable speed by passing it in via the DOM, like we did with colors. Here's my result: http://jsfiddle.net/BinaryMuse/cHHKn/

AngularJS ng-Grid and ngGridFlexibleHeightPlugin not working as I expect

I am trying to use Angular ng-grid to show some dynamic content. The grid will have 0 - 25 rows. I want to have a minimum size and then have the ng-grid auto adjust the height as items get added to the array. For some reason the auto size stops after adding 6-7 items (it even seems to depend on your resolution).
Can anyone help? What am I missing? I have a Plunker below that shows the issue.
http://plnkr.co/edit/frHbLTQ68GjMgzC59eSZ?p=preview
If you are still having issues I would suggestion not using the ng-grid and create your layout directly in html (Use DIVs, column width formatting, styles, etc...). Then populate your new layout using your $scope. Between variables, models and ng-repeat you should be able to do everything you need.
The code bellow is a modification to the plugin. It works by checking number of items in the grid data and calculating the height based on that. The options passed in to the plugin are height of row and header height.
ngGridCustomFlexibleHeightPlugin = function (opts) {
var self = this;
self.grid = null;
self.scope = null;
self.init = function (scope, grid, services) {
self.domUtilityService = services.DomUtilityService;
self.grid = grid;
self.scope = scope;
var recalcHeightForData = function () { setTimeout(innerRecalcForData, 1); };
var innerRecalcForData = function () {
var gridId = self.grid.gridId;
var footerPanelSel = '.' + gridId + ' .ngFooterPanel';
var extraHeight = self.grid.$topPanel.height() + $(footerPanelSel).height();
var naturalHeight = (grid.data.length - 1) * opts.rowHeight + opts.headerRowHeight;
self.grid.$viewport.css('height', (naturalHeight + 2) + 'px');
self.grid.$root.css('height', (naturalHeight + extraHeight + 2) + 'px');
// self.grid.refreshDomSizes();
if (!self.scope.$$phase) {
self.scope.$apply(function () {
self.domUtilityService.RebuildGrid(self.scope, self.grid);
});
}
else {
// $digest or $apply already in progress
self.domUtilityService.RebuildGrid(self.scope, self.grid);
}
};
scope.$watch(grid.config.data, recalcHeightForData);
};
};
I find using this piece of code on the stylesheet solved my problem. I disabled the plugin but it works either way.
.ngViewport.ng-scope{
height: auto !important;
overflow-y: hidden;
}
.ngTopPanel.ng-scope, .ngHeaderContainer{
width: auto !important;
}
.ngGrid{
background-color: transparent!important;
}
I hope it helps someone

Resources