AngularJS - Pass $index or the item itself via ng-click? - angularjs

Say I have
<div ng-repeat="item in scope.allItems">
<div ng-click="doSomething(...)"></div>
</div>
If I want to do something with item upon click, which one is a better option?
Option 1
Use ng-click="doSomething($index) and have:
$scope.doSomething = function($index) {
var myItem = $scope.allItems[$index];
}
Option 2
Use ng-click="doSomething(item) and have:
$scope.doSomething = function(myItem) {
// Do whatever
}

If you are just doing something to the item, pass that in. This way the function doesn't need to know which array the item belongs to:
$scope.addColor = function(car) {
car.color = 'red';
};
If, on the other hand, you need to modify the array I prefer to pass in $index and save having to loop through the array looking for a match:
$scope.deleteCar = function(index, cars) {
cars.splice(index, 1);
};

If you want use filter for scope.allItems, use Option 2 - because using filters change element $index.
If you don't use filter,you can use Option 1.
IMHO Option 2 more easy and useful than Option 1, so i already use Option 2.

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
}

How make related selects in Angular?

I have two select list HTML.
How I can make easy them related? I mean when I select option from first select, some options from second select are shown to hidden?
Selects lists HTML created by PHP.
There are a few ways using angular. One way is to use the ng-change:
<select ng-model="selectedItem1" ng-options="item in items1" ng-change="update(selectedItem1)"></select>
and then have the update(selectedItem1) function update your items2 list. And vice versa for your items2 drop down.
$scope.update = function(selectedItem1) {
$scope.items2 = // logic to filter items 2 based on selectedItem1
}
<select ng-model="selectedItem2" ng-options="item in items2" ng-change="updateSet1(selectedItem2)"></select>
Alternatively, you could use a $watch function, to watch selectedItem1 and update items2 list in this function.
If you need to use a custom filter, see here:
https://docs.angularjs.org/tutorial/step_09
angular.module('myModule', []).filter('items2', function() {
return function(selectedItem1, items2) {
for (var i = 0; i < items2.length; i++) {
// return items you want
}
};
});
Then in your controller, include this filter as dependency items2Filter by appending Filter and you can update $scope.items2 like so:
$scope.items2 = items2Filter($scope.selectedItem1, $scope.items2);

Angular nested ng-repeat filter items matching parent value

I am passing in 2 arrays to my view. I would like my nested loop to only display where it's parent_id value matches the parent.id. Eg.
arr1 = {"0":{"id":326,"parent_id":0,"title":"Mellow Mushroom voucher","full_name":"Patrick","message":"The voucher says $10 Voucher; some wording on the printout says, \"This voucher is valid for $20 Pizza\" but my purchase price or amount paid also says $20. Shouldn't that be $10","type":"Deals"}};
arr2 = {"0":{"id":327,"parent_id":326,"title":"Re: Mellow Mushroom voucher","full_name":"Patrick Williams","message":"Some message here","type":null};
...
<div data-ng-repeat = "parent in arr1">
<span>{{parent.title}}<span>
<div data-ng-repeat="child in arr2 | only-show-where-child.parent_id == parent.id">
<li>{{child.body}}</li>
</div>
</div>
Is this possible/best practice in angular of should I be filtering the object in node before passing it into angular? Thank you!
There are a couple of ways you could do it... You could create a function to return just the children:
$scope.getChildren = function(parent) {
var children = [];
for (var i = 0; i < arr2.length; i++) {
if (arr2[i].parent_id == parent.id) {
children.push(arr2[i]);
}
}
return children;
};
html:
<div ng-repeat="child in getChildren(parent)">
You could define a filter to do the same thing:
myApp.filter('children', function() {
return function(input, parent) {
var children = [];
for (var i = 0; i < input.length; i++) {
if (input[i].parent_id == parent.id) {
children.push(input[i]);
}
}
return children;
};
});
html:
<div ng-repeat="child in arr2|children:parent">
Both of those methods will execute every digest cycle though. If you have a large list of elements you would definitely want to improve performance. I think the best way would be to pre-process those results when you get them, adding a children array to each object in arr1 with only its children (here using array.filter instead of for loop and array.forEach):
arr1.forEach(function(parent) {
parent.children = arr2.filter(function(value) {
return value.parent_id === parent.id;
};
});
Then in the html you are already working with the parent so you can repeat over its children property:
<div ng-repeat="child in parent.children">
Instead of using filters, data-ng-if can achieve the same result.
<div data-ng-repeat="parent in arr1">
<span>{{parent.title}}<span>
<div data-ng-repeat="child in arr2" data-ng-if="child.parent_id == parent.id">
<li>{{child.body}}</li>
</div>
</div>
The solution depends on how often arrays are changed and how big arrays are.
The fist solution is to use filter. But in this case it would be called at least twice (to make sure that result is "stabilized" - selected same elements).
Other solution is to $watch by yourself original array and prepare "view" version of it injecting children there. Personally I would prefer the second as more explicit.
However if you can reuse "find-the0child" filter in other parts of your application you can go with first one - AngularJS will re-run filter only after original array modified.
If needed I can provide here an example of implementation of one of these options - add the comment to answer.

Table filter by predicate

I made a jsfiddle to show what is my problem.
The fisrt part is working in a partial way. See line number 15. I put the predicate in the filter (predicate is l_name) by hand and is working. The table is filtered by Last Name column.
<tr ng-repeat="item in items | filter:{l_name:myInput}">
The second part of the sample is not working when I use the select (model named mySelect2) to choose the predicate where I'm going to filter (see line number 36).
What I'm trying to do is use the select to choose the column by predicate and the input to filter in that column.
<tr ng-repeat="item in items | filter:{mySelect2:myInput2}">
Am I missing something or the binding of the select (mySelect2) must update the filter on the table?
Thanks for the help!
PS: type jo in the input.
Here's a fiddle with some options: http://jsfiddle.net/jgoemat/tgKkD/1/
Option 1 - Search on multiple fields
You can use an object on your model ('search' here) as your filter and separate input boxes for l_name and f_name. This allows you not only to filter on either, but filter on both:
any: <input ng-model="search.$"/><br/>
l_name: <input ng-model="search.l_name"/><br/>
f_name: <input ng-model="search.f_name"/><br/>
<!-- skipping code -->
<tr ng-repeat="item in items|filter:search">
Option 2 - Use a function on your controller
The built-in filter can take a function as an argument that should return true if the object should be included. This function takes the object to be filtered as its only argument and returns true if it should be included. Html:
<tr ng-repeat="item in items|filter:filterFunc">
controller function:
$scope.filterFunc = function(obj) {
// property not specified do we want to filter all instead of skipping filter?
if (!$scope.mySelect)
return obj;
if (obj[$scope.mySelect].toLowerCase().indexOf($scope.myInput.toLowerCase()) >= 0)
return obj;
return false;
};
Option 3 - Create a custom filter
This filter function will take the whole list as an argument and return the filtered list. This does require you to create an angular module and specify it in the ng-app tag like ng-app="MyApp"Html:
<tr ng-repeat="item in items|MyFilter:mySelect:myInput">
Code:
var app = angular.module('MyApp', []);
app.filter('MyFilter', function() {
return function(list, propertyName, value) {
console.log('MyFilter(list, ', propertyName, ', ', value, ')');
// property not specified do we want to filter all instead of skipping filter?
if (!propertyName)
return list;
var newList = [];
var lower = value.toLowerCase();
angular.forEach(list, function(v) {
if (v[propertyName].toLowerCase().indexOf(lower) >= 0)
newList.push(v);
});
return newList;
}
});
Option 4: ng-show
The built-in filter filter expressions don't let you use any expression, but ng-show does so you can just limit visible items like so:
<tr ng-show="item[mySelect].toLowerCase().indexOf(myInput.toLowerCase()) >= 0 || !mySelect" ng-repeat="item in items">
I think option 1 is easy and flexible. If you prefer your drop-down + field UI then I think option 3 is the most useful, and you can re-use it as a dependency in other apps like this:
var app = angular.module("NewApp", ["MyApp"]);
I would just name it something better like 'filterByNamedProperty'. Option 2 is easy but it is tied to your controller. Option 4 is messy and I wouldn't use it.
What about using a custom filter? Users concatenate the property with the criteria (e.g. last:jo). In the filter, split on the colon, and use the first part as the property name and the second part as the criteria.
You may pass scope variables to your filters:
<tr ng-repeat="item in items | filter:myScopeVariable">
This means that you may define your filter object in controller and it will be used by the filter:
$scope.$watch('mySelect2', function(val){
$scope.myScopeVariable = {};
$scope.myScopeVariable[val] = $scope.myInput2;
});
$scope.$watch('myInput2', function(val){
$scope.myScopeVariable = {};
$scope.myScopeVariable[$scope.mySelect2] = $scope.myInput2;
});
Demo Fiddle

create objects on the fly with angularjs

Problem 1:
I came across this issue where in I need to create objects on the fly based on a loop.
Example:
angular.forEach([0,1,2], function (index) {
$scope.cc + index = buildMeProto();
});
Am I approaching this the wrong way? Can I create $scope.element based on an index?
Problem 2:
I also notice that in the HTML if you do something like:
<div ng-repeat="black in blacks">
<lightbox name="black+$index" />
</div>
you can't append an index to an object, in this case 'black' is an object and index is 0, 1,2 etc.
Is there a each way to piggy ride the index to create or invoke elements?
Thanks
Problem 1
If you want to create a $scope property on the fly you need to use the [] notation.
angular.forEach([0,1,2], function (index) {
$scope["cc" + index] = buildMeProto();
});
Problem 2
You could call a function on the scope that would augment the object by adding a property.
$scope.addIndex = function(person, index){
person.id = index;
};
Example jsfiddle.

Resources