Repeat an item in ng-repeat - angularjs

I have an array of objects whereby there is a need to allow the user to add an item that is a duplicate of another. It is triggered when they increase the quantity where a property called 'HasPers' is true.
The HTML markup looks like:
<td width="10%"><input type="number" value="{{productdetails.Quantity}}" ng-model="productdetails.Quantity" min="1" ng-change="change(productdetails)" /></td>
and the function is:
$scope.change = function (item) {
if (item.HasPers) {
item.Quantity = 1;
$scope.items.push(item);
}
};
Initially this had a problem in the repeater wherby it knew it was a duplicat object but this was solved by adding 'track by $index' in the repeater
<tr ng-repeat="productdetails in items track by $index">
http://mutablethought.com/2013/04/25/angular-js-ng-repeat-no-longer-allowing-duplicates/
but the item is clearly still associated with the original since when I change some properties on the original one they change on the duplicate one too. I really want to avoid typing out all of the properties again as in
How to add new key in existing array in angular js?
is there any way to create a new item that is identical to the one being passed in but is a different object?

try
$scope.change = function (item) {
if (item.HasPers) {
var copyItem=angular.copy(item);
copyItem.Quantity = 1;
$scope.items.push(copyItem);
}
};

Try by using angular.copy
$scope.change = function (item) {
if (item.HasPers) {
var newItem = angular.copy(item);
newItem.Quantity = 1;
$scope.items.push(newItem);
}
};

Related

using ng-click to update every cell in a table column

I'm new to Angular and I was trying to figure out how to have the data in one of the columns in a table I made convert from one number format to another when the user clicks on the top of the table cell. I've create a filter already, but i don't know how to call it so it effects all the cells in the table.
<tr ng-repeat="x in data">
<td>{{x.id}}</td>
<td>{{x.name}} </td>
<td>{{x.desc}}></td>
<td>{{x.number}}</td> <--- THIS IS WHAT I WANT TO CONVERT
</tr>
I'm not even sure where to start with this. I basically have a ng-click directive call "convert" which I've defined in the controller. I know that if I define a variable in the $scope (such as $scope.foo = "1") and then call the convert() function I can replace the value like this:
$scope.convert = function(){
$scope.foo = 2;
}
And then my table updates with that value. But what if every table cell in that column has a different value, and I basically want to run that value through a filter I've made.
Any suggestions on how to approach this?
You said you already have a filter?
Then just give your filter an argument 'numberFormat':
angular.
module('yourModule').
filter('yourFilter', function() {
return function(input, numberFormat) {
// convert the input according to the numberFormat
return filteredValue;
};
});
Then you can update the format in your convert() scope-method:
$scope.convert = function(){
$scope.numberFormat = 'long';
}
and pass it to your filter:
<td>{{x.number | yourFilter:numberFormat}}</td>
BTW:
Read about controllerAs - IMHO it is a better practice to store values at the controller rrather than directly on the scope.
Your function simply needs to update number property of the each object in the data array. You could do it like this:
$scope.convert = function() {
$scope.data.forEach(function(item) {
item.number = item.number + 2; // Convert number somehow
});
};
I assume click handler on table header of column as below
<th ng-click="convert()">column of number</th>
in the controller write the function as below
$scope.convert = function() {
$scope.data.forEach(function(obj) {
obj.number += 1;
})
//$scope.$apply() use this is template is not refreshed
}

ng-repeat not updating on update of array

I'm rendering data through an ng-repeat cycle. And i would like it to update as I update the array. From what i read this should happen automatically however this is not working. So what am i doing wrong?
html:
<tr ng-repeat="data in dataDifferenceArray">
<td>
{{data.name}}
</td>
<td>
{{data.startData}}
</td>
<td>
{{data.endData}}
</td>
<td>
{{data.differenceData}}
</td>
</tr>
Controller (this function is triggered on a button with ng-click):
$scope.getDifferences = function () {
$scope.dataDifferenceArray = [];
var i;
for (i = 0; i < 20 ;i++) {
$scope.dataDifferenceArray.push({
name : 30,
startData : 20,
endData : 2,
differenceData : 30
})
}
}
Console.log reveals that the array is updated correctly, however the table in my view does not change. I don't know what i'm doing wrong.
That's because you change the array reference in your method getDifferences.
To avoid that, us dot, for example with "controller as" syntax:
<div ng-controller="myController as c">
[...]
<tr ng-repeat="data in c.dataDifferenceArray">
<td>
{{data.name}}
</td>
<td>
{{data.startData}}
</td>
<td>
{{data.endData}}
</td>
<td>
{{data.differenceData}}
</td>
</tr>
[...]
If you want to understand how scopes work, i would advice this article : https://github.com/angular/angular.js/wiki/Understanding-Scopes#ngRepeat
Another solution could be :
$scope.getDifferences = function () {
$scope.dataDifferenceArray.length = 0; // here ----
var i;
for (i = 0; i < 20 ;i++) {
$scope.dataDifferenceArray.push({
name : 30,
startData : 20,
endData : 2,
differenceData : 30
})
}
}
but in this solution, you need to create the array outside (and only once) : $scope.dataDifferenceArray = [];
Edit2: My answer was not really clear, let's try to understand what is happening in deep:
Q: Why does the ng-repeat still has the reference REFERENCE1 ?
You have to remember that there is not only 1 scope in your template.
Eg: the ng-repeat directive create new scopes for each of the repeated elements, but we can still access to the parent scope in each child scope. Angular implemented this behavior using Prototype Inheritance: each child scope inherit the properties of its parent scope thanks to its prototype.
You can experiment how it is working by inspecting one on your child elements, then enter in the console: $($0).scope() (it will give you the scope of the selected element, $0 is the selected element (Chrome)). You can now see that there is the same object in $($0).scope().$parent and $($0).scope().__proto__, it is your parent scope.
But there is one problem with prototype inheritance: Let's say we have A = {}; B = {C: 1}, if A inherits from B then A.C == 1. But if we affect a new value A.C = 2, we did not change B, only A.
Angular expressions are evaluated using the current scope as this. So if we have something like ng-click="dataDifferenceArray = []" it is equivalent to this.dataDifferenceArray = [] with this being the scope of the element where ng-click is.
This problem is solved when you are using controller-as because it injects the controller in the scope and you will never directly affect a property to the scope.
Let's take back our example: A = {}; B = {C: {D: 1}}, if A inherits from B then A.C.D == 1. And now even if we affect a new value A.C.D = 2, we changed B also.
even when properly updating the array reference, I was having the same problem, ng-repeat was not rendering my changes. Finally I found the solution by calling to $scope.$apply();
$window.wizard.on("readySubmit", function () {
var newArray = GenesisFactory.GetPermissionsFromTemplate(GenesisFactory.SecurityModules, true);
$scope.NewSecurityProfileInstance.Permission.length = 0;
newArray.forEach(function (entry) {
$scope.NewSecurityProfileInstance.Permission.push(entry);
});
$scope.$apply();
});
I hope this can help.
I've done nothing but added this line "$scope.$apply()" after pushing the element, and I'm done.
For those who have an array being set within scope (i.e. $scope.$apply() causes an error if called after a push), but it is still not updating correctly on the DOM, my workaround has been to call this function immediately after the push/slice/array update:
function applyArray(container, key) {
setTimeout(function() {
var tempArray = container[key];
container[key] = [];
$scope.$apply();
container[key] = tempArray;
$scope.$apply();
}, 0);
}
By passing in container and key, where container in this case would be $scope and key would be "dataDifferenceArray" (note the quotes).
I have no idea why the DOM does not update correctly in some instances. It shows the correct number of elements, and it updates when a new element is added, it just doesn't update the child elements DOM correctly (i.e. the newly pushed element may take on the DOM view of the previous child). This could be due to how we redefine this to vm in Johnpapa's Angular Style Guide, but I'm not confident in that.
Actually it looks like your array is being set to empty inside the click function. This will work if you move your arrays scope outside the function.
Example
// Declared outside
$scope.dataDifferenceArray = [];
// Your function
$scope.getDifferences = function () {
var i;
for (i = 0; i < 20 ;i++) {
$scope.dataDifferenceArray.push({
name : 30,
startData : 20,
endData : 2,
differenceData : 30
});
}
};

Why my angularjs filter does not apply some time when the object property change

I have array of object say person which have two property name and hideMe. i have make a directive to add, remove person from array and use filter to hide the person using property hideMe.
service
app.factory("personService", function () {
var person = function () {
this.name = "";
this.hideMe = false;
};
var persons = [];
return {
add: function () {
persons.push(new person());
},
hide: function (index) {
persons[index].hideMe = true;
}
}
});
controller
app.controller("personCtrl", function ($scope, personService) {
$scope.model = personService;
});
HTML
Add New Person
<table>
<tr>
<td>Name </td>
<td>remove</td>
</tr>
<tr ng-repeat="person in model.persons | filter : { hideMe: false}" >
<td>
<input type="text" ng-model="person.name">
</td>
<td>Hide Me </td>
</tr>
</table>
when i hide the person by click on hideMe link some time it is not hide ????
here is the fiddle http://jsbin.com/kolaraliki/1/
The reason you are experiencing this error is because of the use of $index.
$index gives the index of the current item in the iterator ng-repeat, not the index of the source element. As soon as an item is removed from the array, the $digest triggers the ng-repeat to re-calculate, and $index of the items in the ng-repeat are re-calculated. Thus, if you have 3 items in your array, at 0,1,2 but item 1 is hidden, you would expect your ng-repeat would have 2 items, 0,2 but it actually has 0,1. clicking to hide 2 would actually try to hide 1 in the array, which is already hidden.
instead of using $index, try instead model.hide(model.persons.indexOf(person)) to get the actual array index instead of the iterator index.
I forked your jsbin with an example: http://jsbin.com/jecosofequ/2/edit
It is because you have already filtered only those persons who have hideMe value as true to be shown and by clicking link which calls hide() you are simply setting hideMe value as true for second time. You can simply fix this by changing that filter to be filter: { hideMe: false } in your ngRepeat and then it should work. Because it would show's only those objects which hideMe values are false.

How to ng-repeat into html table with multiple levels of json?

I have an object of social media stats. I'm trying to ng-repeat them into a table. Here's my plunker.
HTML:
<table>
<tr ng-repeat="(metric, metricData) in data">
<td>{{metric}}</td>
<td>{{metricData}}</td>
</tr>
</table>
Controller object:
$scope.data = { buzz:0,
Delicious:121,
Facebook:
{
like_count: "6266",
share_count: "20746"
},
GooglePlusOne:429,
LinkedIn:820,
Twitter:4074
};
I run into a problem when I get to the Facebook results. Within the <td> that entire object gets displayed (as it should be with how I have my code setup). But what I'd rather have happen is to repeat through that object and display the key and value in the cell.
I tried doing something looking to see if metricData is an object and doing some sort of ng-repeat on that. But I wasn't having luck with that. Any idea on how I can display the inner object (keys & value) within the cells?
You can define a scope function returning the type of metricData :
$scope.typeOf = function(input) {
return typeof input;
}
And then you can display it according to its type :
<tr ng-repeat="(metric, metricData) in data">
<td>{{metric}}</td>
<td ng-switch on="typeOf(metricData)">
<div ng-switch-when="object">
<div ng-repeat="(key, value) in metricData">
<span>{{key}}</span>
<span>{{value}}</span>
</div>
</div>
<span ng-switch-default>{{metricData}}</span>
</td>
</tr>
You can see it in this Plunker
Sounds like you'll need a specific directive that wires up children to be recursive, take a look at this example: Recursion in Angular directives
What you'd check on is if what you need to repeat is an object and not a value, then add the new element compile it, and start the process over again.
I'm assuming you want each of those values to have their own line but you don't explain exactly how you want it to work. I think the matter would best be handled by passing a clean version of what you want to the ng-repeat directive. I'm assuming you want two rows for facebook in your sample. You could create a filter to flatten the metrics so there are properties "Facebook_like_count" and "Facebook_share_count" (PLUNKER):
app.filter('flatten', function() {
function flattenTo(source, dest, predicate) {
predicate = predicate || '';
angular.forEach(source, function(value, key) {
if (typeof(value) == 'object') {
flattenTo(value, dest, predicate + key + '_');
} else {
dest[predicate + key] = value;
}
});
}
return function(input) {
var obj = {};
flattenTo(input, obj, '');
return obj;
}
});
Then your repeat can use the filter:
<tr ng-repeat="(metric, metricData) in data|flatten">

Adding a computed field to every element of an array in an AngularJS model

I'm pulling an array of users into my AngularJS model from a JSON datasource. This data is being rendered in a table, and I'd like to create a column that is computed from two values of the existing user object, without modifying my underlying data service.
// My model
function UserListCtrl($scope,$http) {
$http.get('users').success(function(data) {
$scope.users = data;
});
};
In my partial template, I know I can do something like this:
<tr ng-repeat="for user in users">
<td>{{user.data / user.count | number:2}}</td>
</td>
But I'd rather add that field into the model, so I can use it like so:
<td>{{user.amplification}}</td>
How do I add the "amplification" field to every user in my model?
As an aside, is it possible to use the orderBy filter on something like this:
<td>{{user.data / user.count | number:2}}</td>
You can eather:
Just after loading user do:
$http.get('users').success(function(data) {
$scope.users = data;
$scope.user.amplification() = function() { return $scope.user.data / $scope.user.count; }
});
And use as {{user.amplification()}}
Anywhere at controller:
$scope.$watch('user', function() {
$scope.userAmplification = $scope.user.data / $scope.user.count;
}, true);
$http.get
Or if user.data/count do not change, do same as 1. but staticly calculate:
$http.get('users').success(function(data) {
$scope.users = data;
$scope.user.amplification = $scope.user.data / $scope.user.count;
});
And OrderBy could be used on any expression (uncluding result of other filter)
If you don't need your amplicification() function to update when the data and count properties on your user update, you can do something like this in your controller:
$scope.users.forEach(function(user) {
user.amplification = function() {
return user.data / user.count;
};
});
Adding a second answer as I feel it's appropriate as it's distinct from my first one.
After a little looking around, I found the method I originally posted falls over if you try to add new rows dynamically, or new elements to the array which depend on the computed value. This is because the $scope.array.forEach() will only run when the controller is created.
The best way to solve this problem is to create a properly defined object which contains the options you want. e.g.
function Task(id, name, prop1, prop2) {
this.id = id;
this.name = name;
this.prop1 = prop1;
this.prop2 = prop2;
this.computedProperty = function () {
return this.prop1 + this.prop2;
};
}
This is far more flexible as each new object created will have the new property.
The only downside is that in your ajax success callback, you'll need to pass each of your users into your 'Users()' constructor.
What worked for me was to add a loop and add the property to each item in that loop. I used a property of the controller but I am sure you can use scope the way you are approaching it in the question.
function(result) {
self.list = result;
angular.forEach(self.list, function(item) {
item.hasDate = function() {
return this.TestDate != null;
}.bind(item); // set this context
});
}
Then in my markup I just used it like this.
<div ng-repeat...>
<div ng-show="item.hasDate()">This item has a date.</div>
</div>

Resources