I have the following code:
<div ng-hide="items.length == 0">
<li ng-repeat="item in items" ng-hide="item.hide == true">
{{ item.name }} <hide-button />
</li>
</div>
Imagine that I have 10 items. When I click the button, I set the item.hide to true and the item disappear. When I hide all 10 items, I want to hide the main div. What's the best way to achieve this with AngularJS?
An approach might be to use a function like this:
$scope.checkItems = function () {
var total = $scope.items.length;
for (var i = 0; i < $scope.items.length; ++i) {
if ($scope.items[i].hide) total--;
}
return total === 0;
};
and change the ngHide attribute on the outer div:
ng-hide="checkItems()"
jsfiddle: http://jsfiddle.net/u6vkf6bt/
Another approach is to declare a $scope.allHidden variable and watch over the array like this:
$scope.$watch('items', function (newItems) {
var all = true;
for (var i = 0; i < newItems.length; ++i) {
if (newItems[i].hide !== true) all = false;
}
$scope.allHidden = all;
}, true);
This is checking when anything inside the array is changed, and check if all the hide attributes are set to true.
Then set it on the ngHide attribute:
ng-hide="allHidden"
jsfiddle: http://jsfiddle.net/u6vkf6bt/1/
Among the two, I would choose the first approach, because deep watching may cause performance issues:
This therefore means that watching complex objects will have adverse
memory and performance implications.
From here, under $watch(watchExpression, listener, [objectEquality]); when objectEquality is set to true.
Yet another approach would be updating the counter of hidden items whenever a single item is hidden. Of course this would require the hiding code to be placed within your controller, like this:
$scope.allHidden = false;
// if items can have hide = true property set already from the start
var hiddenItems = $scope.items.filter(function (item) {
return item.hide;
}).length;
// or if they are all initially visible, a straightforward approach:
// var hiddenItems = 0;
$scope.hideItem = function (item) {
item.hide = true;
hiddenItems++;
if (hiddenItems === $scope.items.length) {
$scope.allHidden = true;
}
}
and the hiding would need to be done like this:
<li ng-repeat="item in items" ng-hide="item.hide == true">{{ item.name }}
<button ng-click="hideItem(item)">hide</button>
</li>
jsfiddle: https://jsfiddle.net/723kmyou/1/
Pros for this approach
no need to iterate over the full items array when doing the "should main div be hidden?" check -> possibly better performance
Cons for this approach
you need to place the hideItem function inside your controller
A couple of reasons why I didn't like the other solutions:
Time complexity is always O(n). instead of counting how many items are visible/hidden - a number we do not care about at all in this problem - we should just ask if there's at least one visible. better performance.
Not reusable enough.
My preferred solution is to actually look for the first visible item:
http://plnkr.co/edit/FJv0aRyLrngXJWNw7nJC?p=preview
angular.module('MyApp',[]);
angular.module('MyApp').directive('myParent', function(){
return {
restrict: 'A',
link:function(scope, element){
scope.$watch(function(){
return element.find('>:visible:first').length > 0;
}, function( visible ){
if ( visible ){
element.show();
}else{
element.hide();
}
})
}
}
});
<div my-parent>
<div class="hidden">one child</div>
<div class="hidden">another child</div>
</div>
<div my-parent>
<div class="hidden">third child</div>
<div>another child</div>
</div>
You can even expand this by passing a selector to 'my-parent' attribute to be used in the directive. allows to better specify which items to look for.
I like this approach as it has almost no assumptions.
This can be used for any scenario where children might be hidden. not necessarily ng-repeat.
However the watch might be costly as we're using a :visible selector.
another - assuming ng-repeat is involved - is to watch the value on the scope - http://plnkr.co/edit/ZPVm787tWwx9nbJTNg1A?p=preview
app.directive('myParent', function(){
return{
restrict: 'A',
link: function(scope, element,attrs){
scope.$watch(function(){
try{
value = scope.$eval(attrs.myParent);
var i = value.length-1;
for ( ; i >= 0; i-- ){
if ( value[i].hide != false ){
return true;
}
}
}catch(e){
return false
}
return false;
}, function( newValue ){
if ( !!newValue ) { element.show(); } else { element.hide();}
})
}
}});
<ul my-parent="awesomeThings">
<li ng-repeat="thing in awesomeThings">{{thing.value}}</li>
</ul>
The watch here might be less costly, not sure, but it doesn't handle the dom, so there's a good chance for that..
While this is very similar to other solutions, it is implemented as a highly reusable directive, and looks for the first visible item.
In my solutions I also assume jquery is available.
There are many other ways to implement this if required:
AngularJS: element.show() in directive not working
regarding watch performance, not sure which way is better. I am using a function to evaluate the watched value and I am not sure if this is the best way to handle it.
Related
In my controller I have a $scope.var=[]
when i click a button, this array gets filled with content.
so the $scope.var.length is changing from 0 to 1 for example.
When the array gets filled I want to show a div, so it has a
ng-if="condition".
This condition is saved in the controller as well:
$scope.controller = false.
Now I want that condition to be changed, when the array get filled:
$scope.$watch('var.length', function(){$scope.condition = true;});
Now (important!), there is a second option to show the div: a button with
ng-click="condition = !condition"
In the test, the ng-click is working perfectly, and the condition is changing between true and false, but when i fill the var = [] with content, the $watch-method isn´t working.
Thanks for help and tipps :)
//EDIT
html:
<div class="row">
<div ng-click="condition= !condition">
<span class="glyphicon clickable" ng-class="{'glyphicon-down': !condition, 'glyphicon-up': condition}" ></span>
</div>
<div class="slideToggle" ng-if="condition">
Text hier
</div>
</div>
You can just watch var itself instead of var.length
Check this out for an example : http://jsfiddle.net/Lzgts/577/
The code -
$scope.$watch('var', function(newVal, oldVal){
console.log(newVal);
if(newVal.length > 0 ){
$scope.condition = true;
}else{
$scope.condition = false;
}
});
This should do the trick for you.
Alternatively, you can watch var.length but you still need some if/else logic inside to check if the newVal is actually 0 or not. Something like this :
$scope.$watch('var.length', function(newVal, oldVal){
if(newVal === 0){
$scope.condition = false;
} else {
$scope.condition = true;
}
});
Such a code may lead to several problems.
You can inspect my code:
plnkr
$scope.$watch('var.length', function(newE) {
if (newE > 0) {
$scope.condition = true;
}
});
The way your var changing your array, if they are equal you have to reinitialize it. Not a very smart idea but in other way you must ensure that you array filling creates new array and angular could see that.
to give it a possibility to change a scope you have to use $timeout in my case, so be care to not get trapped in $apply loop.
Okay I did it:
change the ng-click="condition = !condition"
to ng-click="reverse()"
and in the controller you define the function:
$scope.reverse= function(){
$scope.condition= !$scope.condition;
};
Then the condition is changing as well, because it´s in the same scope now.
I'm trying to figure out how to display up to 10 options from a suggestions array and then give the user the ability to select the option, with that option now becoming the search query?
Html and AngularJS:
<ul class="suggestions" ng-show="showAutocomplete">
<li ng-repeat="suggestion in autocomplete.suggestions" ng-show="suggestion.options.length > 0" ng-mousedown="searchForSuggestion()"><small>Searching —</small>
{{suggestion.options[0].text}}//how to display up to 10 options
</li>
</ul>
Suggestions array
$scope.autocomplete = {
suggestions: []
};
$scope.showAutocomplete = false;
JS:
$scope.searchForSuggestion = function() {
$scope.searchTerms = $scope.autocomplete.suggestions[0].options[0].text;
$scope.search();
$scope.showAutocomplete = false;
};
More JS:
var getSuggestions = function(query) {
searchService.getSuggestions(query).then(function(es_return) {
var suggestions = es_return.suggest.phraseSuggestion;
if (suggestions.length > 0) {
$scope.autocomplete.suggestions = suggestions;
}
else {
$scope.autocomplete.suggestions = [];
}
if (suggestions.length > 0) {
$scope.showAutocomplete = true;
}
else {
$scope.showAutocomplete = false;
}
});
};
Suggestions is an array mapped to the $scope.autocomplete object.
I'm using PhraseSuggestion from ES and options is an array within the PhraseSuggestion object. Text is the actual option from the options array within the PhraseSuggestion object.
I hope that makes it a bit clearer - I'm a bit new to AngularJS and still learning it.
UPDATE:
I should be able to include the functionality that I'm seeking in the getSuggestions function, right? Just adjusting the if statements a bit... then all it really comes down to is the display, how to display up to 10 suggestions in the options array
I think you might be looking to use a nested ng-repeat with a limitTo filter placed on it.
I've put together a plunker showing how you can use limitTo with an ng-click on your populated options here http://plnkr.co/edit/8Mh....
If selecting an option alters suggestions, that should be fine and update the view accordingly.
Hope that helps you out!
I recently inherited an asp.net project that uses Angular, which is very new to me, so I apologize in advance for any rudimentary questions or assumptions.
The markup / js below results in an endless number of the following error:
10 $digest() iterations reached. Aborting!
Angular version 1.2.27
I have the following markup (showing only relevant parts for brevity).
<div id="RecentContentGrid" ng-controller="RecentContentCtrl" ng-cloak>
<ul>
<li ng-repeat="item in items" ng-class="item.contentType.toLowerCase() && getItemClass(item)" ng-switch on="item.contentType">
<a href="{{item.url}}" class="content clearfix" title="{{item.displayName}}" ng-switch-default>
<img ng-src="{{getThumbUrlBySize(item, 320)}}?mh=320" />
</a>
</li>
</ul>
</div>
My issue is with the "ng-src="{{getThumbUrlBySize(item, 320)}}" part. This calls a method in the controller, which in turn calls a web service to get a image based on the specified height:
$scope.getThumbUrlBySize = function(item, size){
VideoThumbnail.query({ embedCode : item.embedCode, maxHeight: size }, function (data) {
return data.Content;
});
}
The controller also has the following watch methods:
// Watch Methods
$scope.$watch('params.category', function (newVal, oldVal) {
if (typeof(newVal) == 'string') {
$scope.params.perPage = $scope.total_items;
}
$scope.items = [];
});
$scope.$watchCollection('params', function () {
var items = [];
$q.all(_.compact([fetchArticles(), fetchVideos()])).then(function (data) {
items = _.flatten(data);
if (items.length == $scope.total_items) {
items = $filter('orderBy')(items, 'CreatedAt').reverse();
if (typeof(ad_content) != 'undefined' && ad_content.length > 0 && $scope.ads_served == false) {
items = injectAds(items);
}
for (i = 0; i < items.length; i++) {
items[i].cssClass = "block-" + (i + 1);
}
// Append scope items
$scope.items = $scope.items.concat(items);
}
else {
$scope.messages.push("No more items");
}
});
});
My question is how do I get a dynamic image url based on the specific item property and the passed in value for the size? As I mentioned, Angular is very new to me, so I'd appreciate specific details.
Oh, and I should that that the controller is used for many parts of the site, and that's why the size is passed in on the specific module, rather than at the scope level. The maxHeight variable will change based on where this module is used.
Thank you very much.
There are a couple of issues with your code I can see:
the function getThumbUrlBySize does not return anything. Therefore the markup {{getThumbUrlBySize(item, 320)}}?mh=320 fails to interpolate, leaving img tags with empty src attribute.
VideoThumbnail.query seems to be asynchronous. Even if it returned a Promise object, the markup wouldn't be interpolated with the resolved value.
The VideoThumbnail.query's callback does not actually do anything with the value it's passed (assuming that the method itself doesn't do anything with the value returned from its callback - which is unlikely)
None of these problems seems to cause an infinite $digest loop (from the code you've posted I'd suspect the injectAds function), however they prevent your code from working properly ;)
The easiest way I can imagine right now is to replace the for loop in $watchCollection handler with the following:
angular.forEach(items, function(item, i) {
item.cssClass = "block-" + (i + 1); // this comes from the original for loop
VideoThumbnail.query({ embedCode : item.embedCode, maxHeight: 320 }, function (data) {
item.thumbnail = data.Content + "?mh=320";
/*
VideoThumbnail.query accepts a callback instead of returning a Promise,
so we have to tell AngularJS to "refresh" when the asynchronous
job is done:
*/
$scope.$apply();
});
});
and the img markup:
<img ng-src="{{item.thumbnail}}" />
I wouldn't call this solution perfect, but it should work :)
I hope this hasn't been answered anywhere else. I am trying to create a rule with ng-class that:
displays firstTagClass if the item is $first in the repeater index
is not displayed when the item is the only item in the repeater
that doesn't use scope.objects.length (because scope.objects is not an array but an object in this specific case).
USE CASE
Can be a great starting point to crop avatar pictures and hold them in the same container.
SO FAR...
I completed the first and third objectives but I am stuck with the second. I tried to add a second condition !$last but it doesn't work.
<div
ng-repeat="object in objects"
ng-if="$index < 4 && object.id !== me.id"
ng-class="{firstTagClass: $first && !$last}">
</div>
Is there a way to achieve all of my goals through ng-class or should I build a custom directive?
Thanks in advance for your help.
Hadrien
UPDATE
I am adding a Codepen: http://codepen.io/anon/pen/ZYaBjW
I found an answer making use of a directive. You can check the solution by removing the 3rd object in $scope.objects here: http://codepen.io/anon/pen/LEOopR
HTML
I added the block-style directive and used the class attribute instead of ng-class since I chose to use element.removeClass() in my link function. It seemed more logical to remove a class because my intention was to make it the exception and not the rule.
<div ng-repeat="object in objects"
class="firstTagClass"
ng-if="$index < 4 && object.id !== me.id"
position="{{index}}"
block-style>
{{object.id}}
</div>
JS
The details of the blockStyle directive:
restrict: 'A',
link: function(scope, el, attrs){
var objects = scope.objects;
var me = scope.me;
var userCount = Object.keys(objects).length;
var position = parseInt(attrs.position);
//assign hasMe attribute for objects collection with me object
if(objects){
for(var prop in objects){
if(objects[prop].id === me.id){
objects.hasMe = true;
}
}
}
//handle particular case of 1 object displayed
if(userCount < 2 || userCount < 3 && objects.hasMe === true){
el.removeClass('firstTagClass');
}else if(position > 0){
el.removeClass('firstTagClass');
}
} // end of link function
I have a JSON structure which represents as hierarchical elements.
It looks like the following:
{
"url":"http://docsetups.json",
"partnerId":1,
"fieldDefs":
[
{"roleName":"Make","roleId":1,
"children":[{"roleName":"Invoice Number","roleId":11}]
},
{"roleName":"Model","roleId":2,
"children":[
{"roleName":"Manufacturer","roleId":21},
{"roleName":"EquipmentCode","roleId":22},
{"roleName":"EquipmentSSN","roleId":23}
]
}
]
}
Plunker
I've have created a plunker at: http://plnkr.co/edit/betBR2xLmcmuQR1dznUK?p=preview
I am using ng-repeat to display this in elements as a hierarchy of elements like the following:
When I click on either element the entire structure expands and looks like the following:
The code which renders the DOM is nice and easy and looks like the following:
<div class="headerItem"
ng-class="{focus: hover}"
ng-mouseenter="hover = true"
ng-mouseleave="hover = false"
data-ng-click="vm.onClick(item.roleName)"
data-ng-repeat="item in vm.documentSetups.fieldDefs">{{item.roleName}}
<div class="subItem" ng-show="vm.isVisible"
data-ng-repeat="subItem in item.children">[ ] {{subItem.roleName}}
</div>
</div>
vm.isVisible
The thing to focus on here is the subitem which has the ng-show="vm.isVisible" so that it only displays if that value is true.
Show Only The Subitem of the Clicked Parent
However, I'd like to only display the subitem when its parent item is clicked -- instead of showing all subitems like it does now. Can someone offer a good way to do this? I'm hoping to do it without a directive, because I am interested in whether or not this is possible without a directive or if the code is terribly convoluted in that case.
If you have a solution which includes creating a directive, please keep it as simple as possible. Thanks.
I think you should define a flag for every item which determine if the item is open.
Then you pass the item itself into handler:
data-ng-click="vm.onClick(item)
after that - you simply need to invert isOpen flag:
function onClick(item)
{
item.isOpen = !item.isOpen;
}
The whole view snippet:
<div class="headerItem"
ng-class="{focus: hover}"
ng-mouseenter="hover = true"
ng-mouseleave="hover = false"
data-ng-click="vm.onClick(item)" data-ng-repeat="item in vm.documentSetups.fieldDefs">{{item.roleName}}
<div class="subItem" ng-show="item.isOpen" data-ng-repeat="subItem in item.children">[ ] {{subItem.roleName}}</div>
</div>
The plunker: http://plnkr.co/edit/N8mUZaVfmLpnlW4kxzSr?p=preview
#Oleksii You're answer is very close and it did inspire me to develop the following answer so I appreciate your input and I did upvote you. However, there's a bit more to it than what you gave me.
View Solution at Plunker
I forked the previous plunker and you can see the final solution at:
http://plnkr.co/edit/QvyHlLh83bEyvlNkskYJ?p=preview
No Directive Required
Now I can click either or both element and they expand independently. Here's the sample output:
It took a bit of thinking, but what I did first was create a new type which holds a roleName (consider it unique) and a isVisible boolean. I call that type visibleItem and it looks like this:
var visibleItem = function (roleName){
this.isVisible = false;
this.roleName = roleName;
};
After that I created an array to hold all the visibleItems (1 for each node):
var visibleItems = [];
Now when I load the json I go ahead and create 1 visibleItem object for each node and push it into the visibleItems array.
$http.get('items.json')
.success(function(data, status, header, config) {
vm.documentSetups=data;
for (var x = 0; x < vm.documentSetups.fieldDefs.length; x++)
{
visibleItems.push(new visibleItem(vm.documentSetups.fieldDefs[x].roleName));
}
})
They are "keyed" by their roleName (consider it unique).
Next, I had to write two helper methods (setVisibleItem and getVisibleItem)
function setVisibleItem(roleName)
{
for (var x = 0; x < visibleItems.length;x++)
{
if (visibleItems[x].roleName == roleName)
{
visibleItems[x].isVisible = !visibleItems[x].isVisible;
}
}
}
function getVisibleItem(roleName)
{
for (var x = 0; x < visibleItems.length;x++)
{
if (visibleItems[x].roleName == roleName)
{
return visibleItems[x].isVisible;
}
}
return false;
}
Wire Up The Helper Methods
Finally, I wire up the setVisibleItem to the ng-click of the element and I wire up the getVisibleItem to the ng-show directive.
data-ng-click="vm.onClick(item.roleName)"
data-ng-repeat="item in vm.documentSetups.fieldDefs">{{item.roleName}}
<div class="subItem" ng-show="vm.getVisibleItem(item.roleName)"
data-ng-repeat="subItem in item.children">[ ] {{subItem.roleName}}</div>
</div>
Summary Of How It Works
Basically each of those just iterates through the list and checks to insure if the roleName sent in matches the roleName of the item. If it does it sets or gets the value.
Solved Without a Directive and Not Bad
It's a lot more work than you think it'll be, but I didn't have to implement a directive and the code is still fairly basic.