Angular not updating css after ng-repeat - javascript

I am having a problem updating css on an element in Angular. I have a div that contains elements generated by ng-repeat. The div containing these elements has a set height and has the ng-scrollbars directive attached to it. ng-scrollbars creates a nice scrollbar inside a div when the elements in the div exceed the height of the their container. To do this it creates nested internal divs. So in this case, if the height of the nested internal divs is larger than their container then a scrollbar is made so that you can scroll through the elements.
So lets say I add or remove items to the ng-repeat list. When this happens I would like additional css to be added to the top level div only if the internal div made by ng-scrollbars exceed the height of the top level div. (the height of the internal div is the actual height of the ng-repeat list because it contains the ng-repeat elements)
To do this I created two directives, one that broadcasts an event and one that receives it. The one that broadcasts the event is attached to every ng-repeat element and is broadcasted when the element is created or destroyed. This is to check when changes happen to the ng-repeat list.
The one that receives the broadcast is attached to the top level div container (the one that contains the div made by ng-scrollbar) and checks its height against the internal div made by ng-scrollbar, if the height of the internal div is greater than the top level div then add some css.
I'm sorry if this seems confusing, here's my code doing this with comments attached that I hope explain it better:
app.directive('gtScrollLb', function () {
return {
restrict: 'A',
link: function (scope, elem, attrs) { //elem is the top level container of the ng-repeat elements
var scrollDiv = elem.children()[0]; //this is an internal div created by ng-scrollbars
var anotherScrollDiv = $(scrollDiv).children("#" + $(scrollDiv).attr("id") + "_container"); //another internal div created by ng-scrollbars
var id = attrs.gtScrollLb;
scope.$on(id + "-repeat-change",
function(newVal, oldVal) {
//check if ng-scrollbar div that now contains the ng-repeat elements
//has a greater height than the top level container
if ($(anotherScrollDiv).height() >= elem.height()) {
elem.css("border-bottom", "1px solid darkgrey");
} else {
elem.css("border-bottom", "");
}
}
);
}
};
});
app.directive('gtRepeatable', function () {
return {
restrict: 'A',
link: function (scope, elem, attrs) {
var id = attrs.gtRepeatable;
scope.$emit(id + "-repeat-change"); //event that item has been added by ng-repeat
var parent = scope.$parent;
elem.bind("$destroy",function(){
parent.$broadcast(id + "-repeat-change");//event that item has been destroyed by ng-repeat
});
}
}
});
The problem is that the ng-repeat elements are added and destroyed before the height of their internal container is determined again. So the height being checked when the change event is fired is actually the old height of the internal container.
I need to somehow check the new height. I have been trying to solve this for a long time now, any help would be greatly appreciated.
Edit
In a nutshell, I need an even to fire or $watch to happen after ng-repeat has finished and the DOM has rendered. The problem I'm having here only happens because the event is fired before the DOM is rendered.
Edit 2
For #vittore who flagged this as a duplicate. If you had actually read my question in detail you would see that the one you proposed as the duplicate had nothing to do with my actual problem. There is no problem with ng-scrollbars here. ng-scrollbars is working fine. But it was necessary to include it in the description because it effects how I'm going about solving my problem.

There is a way to get event when last loop element was renderd. Check this link: AngularJS - Manipulating the DOM after ng-repeat is finished
Also here is exact question you have : ng-scrollbar is not working with ng-repeat
and important part:
$scope.$on('loopLoaded', function(evt, index) {
if (index == $scope.me.length-1) {
$scope.$broadcast('rebuild:me');
}
});

To get this to work I ended up using MutationObservers. I had to use two, one for attribute changes and one for childList changes.
MutationObservers should be used sparingly, they will slow down your app and cause serious bugs if left unchecked. I accidentally caused an infinite loop doing an async call.
It's best to only connect them right before the change to the DOM is made and have them disconnect themselves once they make the change.

Related

How to partially (re)apply bindings to descendants with knockout?

I created a custom knockout binding that wraps a given div in an expander. My custom binding moves the given content div to the contant-container div of the expander. After moving the content, the knockout bindings of the content child nodes would not work any more (e.g. click binding for a button inside the content div). Therefore I have to reapply the knockout bindings.
This works in most cases. However, If the content div contains for example a knockout foreach binding, reapplying the bindings means that some content is duplicated.
Example usage of the expander binding:
<div
data-bind="expander: { title: 'dataCollectionForms'}">
<div>
<div class="expander-content">
<button
data-bind="click: $root.addAction, text: 'Hinzufügen'"></button>
<div
data-bind="foreach: listOfButtons">
<button
data-bind="click: $root.buttonClickAction">
</button>
</div>
</div>
</div>
</div>
My code for moving the content div:
function moveExpanderContentToContentContainer($contentContainer, expanderContent) {
try {
//Move the expander content to the content container
//The content will not be cloned but moved. For more info see:
//https://api.jquery.com/append/
var $expanderContent = $(expanderContent);
$contentContainer.append($expanderContent);
$contentContainer.addClass('expander-content-container panel-body');
$expanderContent.addClass('expander-content');
ko.applyBindingsToDescendants(bindingContext, expanderContent);
} catch (appendException) {
var errorMessage = 'Could not append expander-content to expander-content-container.';
logger.logError(errorMessage, appendException, self, true);
}
}
If I remove the line
ko.applyBindingsToDescendants(bindingContext, expanderContent);
the click actions of my three buttons do not work any more:
If I keep the line, the click actions work but the buttons are duplicated:
=> How can I update the bindings of the moved content in a way that fixes
the click bindings and does not duplicate my buttons?
=> If this moving work flow does not work at all, what is a better way to create a custom knockout binding that wraps a given content in a collapsable expander?
I could find some related articles but no solution to my specific issue:
http://knockoutjs.com/documentation/custom-bindings-controlling-descendant-bindings.html
How to clear/remove observable bindings in Knockout.js?
ko.applyBindingsToNode vs. ko.applyBindingsToDescendants
Remove knockout js bindings on cloned element
https://github.com/knockout/knockout/issues/1821
I solved the issue by not moving the content div at all but building the expander around it.
My original strategy was to have a reusable expander view + view model and to move the content div from the original location to the newly composed expander view. Moving around the already bound content was no good idea, I guess.
The new strategy adapts the already existing divs and composes only the header for the expander.
Nevertheless thank you for your thoughts.
I use following code for recreating content of the passed DOM element (having view model + template selector):
function renderIntoElement(element, viewModel, templateSelector) {
templateSelector = templateName || "#myTemplateId";
var $element = $(element);
var templateHtml = $(templateSelector).text(),
$element.children().remove();
$element = $element.append(templateHtml),
ko.applyBindings(viewModel, $element.children()[0]);
}
Hope this helps.

Protractor check if element has children

I have a click event where child elements get appended to a parent element and then get removed on another click event. I want to test if those elements got removed from the parent. So is there something like
var container = element(by.css('.container'));
expect(container.length).toEqual(0);
that checks if there are any children elements?
There are special methods for checking if an element is present:
elm.isPresent();
parentElm.isElementPresent(childElm);
browser.isElementPresent(elm);
And here are the differences between them:
In protractor, browser.isElementPresent vs element.isPresent vs element.isElementPresent
Note that you can still find all elements inside a container and check the count:
var container = element(by.css('.container'));
expect(container.all(by.xpath("./*")).count()).toEqual(0);
Another alternative could be to check the inner HTML:
expect(container.getInnerHTML()).toEqual("");

What is the angular way of hiding all specific divs with the same class

I want to do a simple thing:
I have an app, which has certain divs that in need to show (Only the specific one) and hide if clicked somewhere outside of it (All with the specific class for example).
This is easy using jquery:
$('some-class').style('display','none') //psuedo code here
How should i do the same with angular js?
A specific reason to do so:
I want to build a simple select drop-down (I don't want to use one that exist, i want to understand this...), when i click one, it suppose to open a div right beneath the button, when i click outside of it, it suppose to close not only this one, but some other elements like it in the app.
one point worth mentioning: Not all my select boxes are pre-rendered, some are dynamically added (inside directive), so not all of the are in the $scope, but inside them directives which has isolated scope.
Its better to make directives for these kind of things:
Make a directive for toggleDisplay as following
app.directive('toggleDisplay', function() {
return function(scope, element) {
element.bind('click', function() {
element.toggleClass('unneeded-class'); // or something else
});
};
});
and then you can apply this directive to any element in the html:
<div toggle-display></div>
You can make a drop down logic or kindof accordion etc following this pattern
How do i listen to a click anywhere in the app, that when i click it
and "that" element is not the select box, close all the select boxes?
let suppose you have that opend/dispalyed div that you want to hide when you click outside of it . give it a class "divvy" and attach the following directive to its wrapping container:
app.directive('toggleFocus', function() {
return function(scope, element) {
element.bind('click', function() {
if(!element.hasClass('divvy'))
angular.element(document.querySelector('.divvy')).css({display:'none'})
});
};
});
html:
<div toggle-focus>
<div class="divvy"></div>
</div>
It's in the angular documentation: https://docs.angularjs.org/api/ng/directive/ngShow
<!-- when $scope.myValue is truthy (element is visible) -->
<div ng-show="myValue"></div>
<!-- when $scope.myValue is falsy (element is hidden) -->
<div ng-show="myValue" class="ng-hide"></div>
just attach ng-show="whateverValue" to each div you want to hide/show based on whether "whateverValue" is true/false

jquery not reacting to class changes?

So I've got this assignment I've got to do, it has to do with a christmas tree where you can click the branches and turn them into ornaments and whatnot.
When the tree is on, you should not be able to change the ornaments.
So far i've got this jquery:
$('.branch1, .ornament1, .light1').click(function()
{
alert("WARNING - Power off the tree first!");
die();
});
$('.branch, .ornament, .light').click(function()
{
this.className =
{
light : 'branch', branch: 'ornament', ornament: 'light'
}[this.className];
});
$('#treePowerButton').click(function()
{
$(".branch, .branch1").toggleClass("branch branch1");
$(".ornament, .ornament1").toggleClass("ornament ornament1");
$(".light, .light1").toggleClass("light light1");
$(".powerStatus, .powerStatus1").toggleClass("powerStatus powerStatus1");
});
$('#treeClearButton').click(function()
{
$(".ornament").toggleClass("ornament branch");
$(".light").toggleClass("light branch");
});
But when I turn the tree on and click a branch or ornament it doesn't throw the alert, it just makes the clicked item completely disappear. What am I doing wrong?
here's a jsfiddle: http://jsfiddle.net/a1smjgrv/
Refined Answer:
Try this jsfiddle: http://jsfiddle.net/a1smjgrv/5/. It will accomplish what you're looking for and is much simpler than trying to bind multiple click events on the same element.
Original Answer:
The problem is that when the page is loaded, there are not any 'branch1', 'ornament1', or 'light1' elements on the page, so the click event containing the alert() is not being bound to anything. There are only 'branch' elements on the page, so the click events for the branches are all that's bound.
When the tree is powered ON and the 'branch' elements are changed to be a 'branch1' class, the click event containing the className update are still attached to that element. So, when it tries to update the class name based on your object literal ({light: 'branch', branch: 'ornament', etc...}), it can't find a match for any of those because the class name of the given element is now 'branch1' instead of 'branch', so it's setting the class name on the element to 'undefined' (hence, the disappearance).
If you want a function to be bound to an element at any point in time that the element is rendered to the page, you'll need to bind it at the document level instead. Something like the following:
$(document).on('click', 'branch1', function() {
alert('SHAZAM!');
});

Out-of-the-flow DOM Manipulation

Got this from https://developers.google.com/speed/articles/javascript-dom
From what I understand, appending/removing elements causes reflow. As does changing class. But in the solution, you are appending and removing, thus, causing two times the number of reflows as the problem code. Of course, not all reflows are equal, so are class name change reflows more expensive than appending/removing reflows? What am I missing that makes the solution code more efficient than the problem code?
This pattern lets us create multiple elements and insert them into the
DOM triggering a single reflow. It uses something called a
DocumentFragment. We create a DocumentFragment outside of the DOM (so
it is out-of-the-flow). We then create and add multiple elements to
this. Finally, we move all elements in the DocumentFragment to the DOM
but trigger a single reflow.
The problem
Let's make a function that changes the className attribute for all
anchors within an element. We could do this by simply iterating
through each anchor and updating their href attributes. The problems
is, this can cause a reflow for each anchor.
function updateAllAnchors(element, anchorClass) {
var anchors = element.getElementsByTagName('a');
for (var i = 0, length = anchors.length; i < length; i ++) {
anchors[i].className = anchorClass;
}
}
The solution
To solve this problem, we can remove the element from the DOM, update
all anchors, and then insert the element back where it was. To help
achieve this, we can write a reusable function that not only removes
an element from the DOM, but also returns a function that will insert
the element back into its original position.
/**
* Remove an element and provide a function that inserts it into its original position
* #param element {Element} The element to be temporarily removed
* #return {Function} A function that inserts the element into its original position
**/
function removeToInsertLater(element) {
var parentNode = element.parentNode;
var nextSibling = element.nextSibling;
parentNode.removeChild(element);
return function() {
if (nextSibling) {
parentNode.insertBefore(element, nextSibling);
} else {
parentNode.appendChild(element);
}
};
}
Now we can use this function to update the anchors within an element
that is out-of-the-flow, and only trigger a reflow when we remove the
element and when we insert the element.
function updateAllAnchors(element, anchorClass) {
var insertFunction = removeToInsertLater(element);
var anchors = element.getElementsByTagName('a');
for (var i = 0, length = anchors.length; i < length; i ++) {
anchors[i].className = anchorClass;
}
insertFunction();
}
Suppose you want to change classes of 1 million elements.
Doing it directly would cause 1 million reflows -one for each class-.
But if you remove its parent from the DOM, change all classes, and insert it back, that's only 2 reflows -because changing elements outside the document doesn't cause reflow-.
So basically, removing and reinserting is more efficient if you have lots of elements. No need to do it if you only have a few.
So a document fragment lives 'in memory', not on the page. Manipulating that doesn't trigger any repaints/flows because the fragment is not visually represented anywhere. When you put it on the page, once you're done manipulating it, the browser knows its structure, classes, content, etc, so will only need to reflow/paint once.
In the first example, as you loop through the anchors and change the class name (presumably changing its style as well), it will immediately apply that class, find the new style, and repaint that link. Then do the same for the next one. This is slow.
By yanking it all out into memory and manipulating the DOM there, you only have one repaint/flow when you reinsert the parent wrapper element back into the page.
According to the solution:
To solve this problem, we can remove the element from the DOM, update
all anchors, and then insert the element back where it was.
So, in this case it will trigger 2 reflows (one for remove, and one for insert). So this solution applies when you want to modify more than 2 elements at a time.

Categories