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)
Related
I have an ng-repeat listing items with a button inside, which updates certain info from this item into the DB and then retrieves the new object.
I'm trying to show a loading gif while this happens, but so far the gif appears on all items of the ng-repeat, as I'm using a $scope variable.
Like so:
<div ng-repeat="fair in allfairs">
<img ng-show="loading" src="../images/loading.gif">
<a ng-click="deactivate(fair)" ng-if="!loading">Deactivate</a>
</div>
And in the controller:
$scope.deactivate = function(fair){
$scope.loading = true;
var $promisedb=$http.post('databaseconnect/updatefair.php',$scope.activated);
$promisedb.then(function (data) {
$scope.loading = false;
});
};
How could I achieve the same but only for the particular item that I'm clicking on without affecting the whole array?
Assign the item to a scope variable instead and compare item to that scope variable in ng-if
$scope.deactivate = function(fair){
$scope.loadingItem = fair;
var $promisedb=$http.post('databaseconnect/updatefair.php',$scope.activated);
$promisedb.then(function (data) {
$scope.loadingItem = false
});
};
View
<a ng-click="deactivate(fair)" ng-if="loadingItem != fair">Deactivate</a>
You can chain the img element and a element together via $index of the array allfairs,and you must change the type of loading to object {},when you click the a element,you should add a item to loading,and you can take the $index as the key of item.The loading gif which has a $index will be affected by same $index which has clicked on a element. The code looks like below:
<div ng-repeat="fair in allfairs">
<img ng-show="loading[$index]" src="../images/loading.gif">
<a ng-click="deactivate(fair,$index)" ng-if="!loading[$index]">Deactivate</a>
</div>
$scope.loading = {};
$scope.deactivate = function(fair,index){
$scope.loading[index] = true;
var $promisedb=$http.post('databaseconnect/updatefair.php',$scope.activated);
$promisedb.then(function (data) {
$scope.loading[index] = false;
});
};
This is a little difficult to explain since I can't extract the code that I'm having the most difficulty with. The best I can do is a simple fiddle of what I'm trying to accomplish: https://jsfiddle.net/yLkukw5p/
HTML:
<div ng-app = "myApp" ng-controller = "parentController" ng-switch = "properties.selectedMethod">
<div ng-controller = "childController" ng-switch-when = "id">
<a ng-click = "survey()">
Change div
</a>
</div>
<div ng-switch-when = "date">
div changed
</div>
</div>
JS:
var app = angular.module('myApp', []);
app.factory('vars', function() {
var properties = {};
properties.selectedMethod = 'id';
function setselectedMethod(string){
properties.selectedMethod = string;
}
return {
properties : properties,
setselectedMethod : setselectedMethod
};
});
app.controller('parentController', function($scope, vars) {
$scope.properties = vars.properties;
$scope.setSearchMethod = function(method){
vars.setselectedMethod(method);
}
});
app.controller('childController', function($scope, $rootScope, $http, vars) {
$scope.properties = vars.properties;
$scope.survey = function() {
vars.setselectedMethod("date");
}
});
Basically, I want to be able to change the variable value in a factory shared between child and parent controllers. The only hiccup I'm running into is that in my case, the child div is dynamically generated, and that seems to be the only thing different between the fiddle and my code. I have some JavaScript that adds this DOM:
<div onclick = angular.element('#anotherdiv').scope().setSearchMethod('id');> More Info </div>
where anotherdiv is a div within the childController. When I click this div, I know by debugging that it runs the code in the vars factory, but it doesn't update other values? I'm using the "dot" trick so I would think the variables are references and not "shadowing" as some other posts suggested. Any thoughts?
EDIT: Updated the fiddle to be more accurate: https://jsfiddle.net/yLkukw5p/1/
It looks like the onclick function using angular.element is the one causing trouble, but I don't know how to work around it.
I'm trying to make an event fire whenever a filtered collection is changed. The filtered list is attached to the non-filtered list in ng-repeat.
<tr ng-repeat="item in $scope.filtered = (vm.list | filter:vm.searchText) | limitTo:vm.limit:vm.begin">
And here's my event I want to fire:
$scope.$watchCollection('filtered', function () {
alert($scope.filtered.length);
}, true);
It fires once when the page first loads, before my ajax call populates vm.list, so the alert says 0, but then it should fire again after vm.list gets populated, and every time a change to vm.searchText causes a change to $scope.filtered, but it's not.
I also tried making the $watchCollection method like this:
$scope.$watchCollection('filtered', function (newList, oldList) {
alert(newList.length);
});
But that had the same result.
I also tried doing as is suggested here, and it ended up like this:
<tr ng-repeat="item in catchData((vm.list | filter:vm.searchText)) | limitTo:vm.limit:vm.begin">
$scope.catchData = function (filteredData) {
alert(filteredData.length);
return filteredData;
}
That seemed like it fixed it at first. It now fired when the API call populated the list, and fired again whenever the searchText caused the filtered list to change. Unfortunately it made it so changing the begin option on the limitTo filter no longer worked. Changing the limit option still worked, but not the begin. Changing the begin does still work with the $watchCollection method.
Does anyone have any ideas?
When you create some variables in view, it added as property to current scope. So, in your case you create $scope.filtered, and this added to current scope.
To get it in watch, you just need use same declaration
$scope.$watchCollection('$scope.filtered', function () {
console.log($scope.$scope.filtered.length)
}
But better not use variable name like $scope, so as not to confuse them with angular variables.
so, you can change it ro simple: filtered
angular.module('app', [])
.controller('ctrl', function($scope) {
$scope.$watchCollection('$scope.filtered', function(nval) {
if(!nval) return; //nval - new value for watched variable
console.log('as $scope.filtered in view', $scope.$scope.filtered.length);
}, true);
$scope.$watchCollection('filtered', function(nval) {
if(!nval) return; //nval - new value for watched variable
console.log('as filtered in view', $scope.filtered.length);
}, true);
})
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<input type="text" data-ng-model="search" />
<h3>as $scope.filtered</h3>
<div ng-repeat="item in $scope.filtered = ([11,12,23]| filter:search)">item_{{item}} from {{$scope.filtered}}</div>
<h3>as filtered</h3>
<div ng-repeat="item in filtered = ([11,12,23]| filter:search)">item_{{item}} from {{filtered}}</div>
</div>
you will want to use a function to return the filtered list and set object equality to true.
$scope.$watch(function () {
return $scope.filtered;
}, function (newList) {
alert(newList.length);
}, true);
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>
I have broken this problem down into it's simplest form. Basically I have a directive that, for the demo, doesn't yet really do anything. I have a div with the directive as an attribute. The values within the div, which come from an object array, are not displayed. If I remove the directive from the div, they are displayed OK. I am clearly missing something really obvious here as I have done this before without any problems.
Here's the Plunk: http://plnkr.co/edit/ZUXD4qW5hXvB7y9RG6sB?p=preview
Script:
app.controller('MainCtrl', function($scope) {
$scope.tooltips = [{"id":1,"warn":true},{"id":2,"warn":false},{"id":3,"warn":true},{"id":4,"warn":true}];
});
app.directive("cmTooltip", function () {
return {
scope: {
cmTooltip: "="
}
};
});
HTML
<div ng-repeat="tip in tooltips" class="titlecell" cm-tooltip="true">
A div element: {{ tip.id }}
</div>
<br><br>
Just to prove it works without the directive:
<div ng-repeat="tip in tooltips" class="titlecell">
A div element: {{ tip.id }}
</div>
There is a hack to make it working in earlier versions of angular by making use of transclusion, like that:
app.directive("cmTooltip", function () {
return {
scope: {
cmTooltip: "="
},
transclude: true,
template : '<div ng-transclude></div>'
};
});
PLNKR
As by Beyers' comment above and below, the behaviour the question is about no longer exists in at least 1.2.5
To be clearer; this has nothing to do with ng-repeat, you can remove it and there still will be no tip ( or tooltips ).
See this question on what the = and other configs mean and what it is doing for you.
Basically for your situation when you use = the scope of the directive will be used in the underlying elements, you no longer have your controller's scope. What this means for you is that there is no {{ tip.id }} or not even tip. Because the directive doesn't supply one.
Here's a plunker that demonstrates what you can do with it.
Basically all i did was
app.directive("cmTooltip", function () {
return {
scope: {
cmTooltip: "="
},
link: function($scope){ // <<
$scope.tip = { id: 1 }; // <<
} // <<
};
});
This creates the tip object on the scope so it has an id.
For your situation you would probably just not use = and look at this question for your other options depending on what you want.
In my opinion this isn't the way to go.
I would use Objects.
JS code:
function tooltip(id,warn){
this.id = id;
this.warn = warn;
}
tooltip.prototype.toString = function toolToString(){
return "I'm a tooltip, my id = "+this.id+" and my warn value = "+this.warn;
}
$scope.tooltips = [new tooltip(1,true),new tooltip(2,false),new tooltip(3,true),new tooltip(4,true)];
HTML:
<div ng-repeat="tip in tooltips" class="titlecell">
A div element: {{ tip.toString() }}
</div>