I have been trying to dynamically change the array number of my expression. My initial state:
<p class="title text-center">{{data[0].title}}</p>
<p class="subtitle text-center">{{data[0].sub_title}}</p>
Data is just an array returned from an http request. What I want is that when I click or swipe an element on the page that it jumps to the second item in the data array, e.g.:
<p class="title text-center">{{data[1].title}}</p>
<p class="subtitle text-center">{{data[1].sub_title}}</p>
I have been trying to make an expression in an expression, but I think that that is very wrong. Also, I have tried adding a variable to the $scope in the controller:
$scope.update = function (whateverispassedinfromotherfunction){
var item = whateverispassedinfromotherfunction;
return "data["+whateverispassedinfromotherfunction+"].sub_title";
}
and then this in the HTML
<p class="subtitle text-center">{{update}}</p>
but that does not make any sense to Angular and to me neither :).
Anyone that knows a solution?
Make your current index a variable, initted to 0, and increment it. You can increment either by making a function on your controller that increments it, or just directly in a ng-click / ng-swipe.
<span ng-click="idx++"> <!-- init idx to 0 in your controller -->
<p class="title text-center">{{data[idx].title}}</p>
<p class="subtitle text-center">{{data[idx].sub_title}}</p>
</span>
Dylan's answer will work, but generally when you find the need for this kind of logic, you should try to wrap it up into a more general component.
For example, we'll call it Lense, as it's just a way of viewing one value at a time, given an collection of sequential values.
app.factory('Lense', function() {
return function(values) {
var lense = {};
lense.index = 0;
lense.next = function() {
lense.index += 1;
};
lense.previous = function() {
lense.index -= 1;
};
lense.value = function() {
return values[lense.index];
};
};
});
Now you have a class which can be injected in order to create a lense which contains all of the logic for you.
You can inject it and use it anywhere you need:
function MyController($scope, Lense) {
var data = [ { ... }, { ... }, { ... } ];
$scope.lense = Lense(data);
}
Then your views become a lot more declarative:
<p class="title text-center">{{lense.value().title}}</p>
<p class="subtitle text-center">{{lense.value().sub_title}}</p>
<a ng-click='lense.previous()'>Previous</a>
<a ng-click='lense.next()'>Next</a>
Not only that, but it is a lot easier to write unit tests for factories than it is for directives, as they involve no rendering on HTML.
Finally, it will be a lot easier to debug. Say you want to print the value of the current index every time the user clicks next, you can just add it to your lense factory.
lense.next = function() {
lense.index += 1;
console.log(lense);
};
If your logic is embedded in an expression, then there's no way to do this, because console isn't a property on the current $scope.
<div ng-click='index++ && console.log(index)'></div>
Related
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 have an app where I am running ng-repeat to display information. I need to then add classes to some of the generated elements after an Ajax call has been made.
I could do this easily with jQuery but I'm trying to stick to Angular/jqlite.
The problem I'm having is that I can get the element, but not as an object that addClass works on.
Here's what I've got so far:
angular.forEach($(".tile"), function(tile){
if(srv.free.indexOf(angular.element(tile.querySelector('.label')).text()) != -1){
tile.addClass("free");
}
});
The array srv.free contains a list of names, those name values are the same as the text value of the div with class .label, which is inside of .tile. So I need to loop through each .tile, check the text value of the child .label and if that text value is in the array srv.free, add the class .free to .tile.
The point I'm at, is that addClass is "undefined" because at this point, tile is just a string, not a jquery/jqlite object.
How do I add a class to that, or get to the object version?
Update
I have previously tried to use ng-class on the elements to update the class, but could not get them to update.
I have a service that has free in it, which is initially set to a blank array. After an Ajax call:
$http.get('/json/free.json').success(function(data){
srv.free = data;
});
Then, in my controller I have:
$scope.gsrv = globalService;
and in my ng-repeat:
<div class="col-xs-3 col-md-2" ng-repeat="Tile in list.tiles">
<div class="tile" id="{{$index}}" ng-class="{free:$.inArray(Tile.Name, gsrv.free)}" ng-click="main.changeView('/Tile/'+$index)">
<img src="http://placehold.it/256" ng-src="{{Tile.Icon}}" ng-class="{dis:!Tile.Stats}">
<div class="label">{{Tile.Name}}</div>
</div>
</div>
When that didn't work, I tried adding:
$scope.gsrv.free = globalService.free;
which did not change anything.
You state that the elemens are rendered in an ng-repeat. Therefore you can just use the ng-class directive to add a class based on some variable, something like the following:
<div ng-repeat="tile in tiles"
ng-class="{ free: tile.someVariable }"> // <-- adds a class 'free' when someVariable verifies to true
<label>{{ tile.someVariable }}</label>
</div>
UPDATE
You can add those variables to the tiles, after the ajax call:
$http.get('/json/free.json').success(function(data){
setTilesFree(data);
});
var setTilesFree = function (free) {
var tiles = $scope.list.tiles; // shortcut
for (var i = 0; i < tiles.length; i++) {
// If in 'free' array, set tile free = true
// This will update the DOM from this tile, adding the class 'free'
if (free.indexOf(tiles[i].Name) > -1) {
tiles[i].free = true;
} else {
tiles[i].free = false;
}
}
}
Then in your view:
<div ng-repeat="Tile in list.tiles">
<div ng-class="{free: Tile.free}">
<img src="http://placehold.it/256" ng-src="{{Tile.Icon}}" ng-class="{dis:!Tile.Stats}">
<div class="label">{{Tile.Name}}</div>
</div>
</div>
See this jsfiddle
You can return empty array from list and free services and populate them later. Since Angular runs digest cycle after every $http.get, $scope will be properly updated and binding will work as expected. The most tricky thing is to populate the same existing array (initially returned by service) instead of creating new one every time. This is required, because controller function will not run again every time digest is running and therefore new array instance will not be assigned to the $scope. Here is on of the possible solutions:
JavaScript
angular.module('app',[]).
factory('list', ['$http', function($http) {
var tiles = [];
return {
getList: function() {
if(!tiles.length) {
$http.get('data.json').then(function(res) {
res.data.forEach(function(item) {
tiles.push(item);
});
});
}
return {
tiles: tiles
};
}
}
}]).
factory('free', ['$http', function($http) {
var free = [];
return {
getFree: function() {
if(!free.length) {
$http.get('free.json').then(function(res) {
res.data.forEach(function(item) {
free.push(item);
});
});
}
return free;
}
}
}]).
controller('ctrl', ['$scope', 'list', 'free', function($scope, list, free){
$scope.list = list.getList();
$scope.free = free.getFree();
}]);
HTML
<div class="col-xs-3 col-md-2" ng-repeat="Tile in list.tiles">
<div class="tile" ng-class="{free: free.indexOf(Tile.Name) > -1}">
<img ng-src="{{Tile.Icon}}" ng-class="{dis:!Tile.Stats}">
<div class="label">{{Tile.Name}}</div>
</div>
</div>
Plunker
http://plnkr.co/edit/UEWnexi3wVU6eLN1BW2b?p=preview
Update:
http://plnkr.co/edit/vOHkLyCHQFoWdqaix4pH?p=preview
(the same example with static pre-populated list, and also service instead of factory)
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.
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.
I looked at the post jQuery: Loop iterating through numbered selectors? and it didn't solve my problem, and didn't look like it was truly an answer that works.
I have a list of <h3> tags that are titles to questions, and there are answers below in a <p>. I created classes for each Q & A like so:
<h3 class="sec1">Question:</h3><p class="view1">Answer...</p>
<h3 class="sec2">Question:</h3><p class="view2">Answer...</p>
<h3 class="sec3">Question:</h3><p class="view3">Answer...</p>
I used the following jQuery loop to reduce redundacy for my 21 questions.
$(document).ready(function () {
for (var i = 1; i < 21; i++) {
var link = ".sec" + i;
var content = ".view" + i;
$(link).click(function () {
$(content).toggle("fast");
});
}
});
But it isn't working for all Q & A sets, only the last one. i.e.: It works for the first set if I set the max value to 2 (only looping once). Please advise. Thanks
While I agree with #gaffleck that you should change your approach, I think it is worth while to explain how to fix the current approach.
The problem is that the click function does not get a copy of the content variable but instead has a reference to that same variable. At the end of the loop, the value is .view20. When any element is clicked it read that variable and gets back .view20.
The easiest way to solve this is to move the code into a separate function. The content variable within this function is a new variable for every call of the function.
function doIt(i){
var link = ".sec" + i;
var content = ".view" + i;
$(link).click(function () {
alert(content);
});
}
$(document).ready(function () {
for (var i = 1; i < 21; i++) {
doIt(i);
}
});
http://jsfiddle.net/TcaUg/2/
Notice in the fiddle, if you click on a question the alert has the proper number. Optionally, you could make the function inline, though I find the separate function in most cases to be a bit cleaner.
http://jsfiddle.net/TcaUg/1/
A much easier way to do this, would be this:
$(document).ready(function(){
$("h3").click(function(){
$(this).next("p").toggle("fast");
});
});
This is also safer in that you can add/remove questions and answers in the future and you won't have to update the function.
Wrap your questions in a more logical structure to create a proper scope for your questions-block:
<div id="questions">
<div class="question">
<h3 class="sec1">Question:</h3><p class="view1">Answer...</p>
</div>
<div class="question">
<h3 class="sec2">Question:</h3><p class="view2">Answer...</p>
</div>
<div class="question">
<h3 class="sec3">Question:</h3><p class="view3">Answer...</p>
</div>
</div>
Now iterate through it like this:
$(function() {
$('#questions .question h3').click(function(){
$(this).parent().find('.answer').toggle('fast');
});
});