AngularJS dynamically add elements to ng-repeat template - angularjs

I have an array called 'variables' that I turn into a table.
<tr ng-repeat="variable in variables">
<td>
<textarea ng-model="variable.extras" ...>{{variable.extras}}</textarea>
</td>
I then have a function numberOfRowsForChoiceVariable that looks at variable.extras and determines how many lines it has, doing things like removing blank lines, etc... So now on the next <td> element I want to have as many <select> elements as returned by the call to numberOfRowsForChoiceVariable(variable).
So if they put 4 lines into variable.extras, then I want to display 4 select elements inside the next <td>
I'm thinking I need to use some type of $watch() to enable that, but I'm not sure how to use that within the ng-repeat scope.

Use Case
Please let me know if I have misunderstood your question. Here's what I understand from your question.
User enters text into the textarea
System counts the number of lines
Number of lines will create that number of select element. [edit: updated solution to meet this requirement]
Solution
You can use a filter to create a list from your line counting.
Controller
Based on your requirements, this controller uses regular expressions to count the number of lines. I haven't tested this on a Unix system, but it works fine on Windows. The lines function will return the number of lines from a given string input.
function ctrl($scope) {
$scope.variables = [{extras: ""}, {extras: ""}, {extras: ""}];
$scope.lines = function (input) {
if (angular.isString(input)) {
return input.split(/\r?\n/).length;
}
return 0;
};
}
Filter
A filter is required to create a temporary array of a given length. This is a common component from functional programming.
var app = angular.module('app', []);
app.filter('range', function () {
return function (input, total) {
total = parseInt(total);
input = Array.apply(null, Array(total))
.map(function(value, index) {
return index;
});
return input;
};
});
View
Using this view, the <select> can be dynamically created next to the textarea. I've shortened the variable names so it could fit within this screen, but don't do this, it's bad form.
<div ng-app='app' ng-controller="ctrl">
<table>
<tr ng-repeat="v in variables">
<td>
<textarea ng-model="v.extras">
{{v.extras}}
</textarea>
</td>
<td>
<select ng-repeat="n in [] | range:lines(v.extras)">
</select>
</td>
</tr>
</table>
</div>
Demo
You can access a demo from this jsFiddle.

Related

How to add a new record and not update previous record in angularjs

How do I add a new record and have angular "forget" the new item added after I click the corresponding button.
The ability to add a record works and when I add the new record it display in the table I have, correctly, However my problem is that when I go to add a second record ie I remove the previous input and type something else. This results in the newly added record below to also change.
In short after I add an item to my array I want angular to forget it. How do I accomplish this.
In my controller I have this
(function () {
var app = angular.module("mainApp");
var ordersController = function ($scope,$filter, ordersService,customerService) {
$scope.orders = [];
$scope.addOrder = function (newOrder) {
$scope.orders.push(newOrder);
}
app.controller("ordersController", ["$scope","$filter", "ordersService","customerService", ordersController]);
}());
in my html I have this
<div><table>
<tr>
<td><input ng-model="item.quantity" type="text" /></td>
<td><button type="button" ng-click="addOrder(item)">Add Line Item</button></td>
</tr>
</table></div>
<div>
<table>
<tr ng-repeat="order in orders track by $index">
<td>
{{ order.quantity }}
</td>
</tr>
</table>
</div>
The problem might be that you are adding the newOrder object to the array while Angular keeps it's model bound to that object. Try using Angular's copy functionality like this (assuming that you are using Angular 1):
$scope.addOrder = function (newOrder) {
var copiedOrder = angular.copy(newOrder);
$scope.orders.push(copiedOrder);
}
The documentation for the copy method can be found here.

Update Totals in ng-repeat

Please take a look at this fiddle
What I'm trying to do is to calculate/update totals for each column inside a nested ng-repeat:
<tr ng-repeat="cust in customers">
<td>{{cust.name}}</td>
<td ng-repeat="book in books">
<p ng-init="index=getIndex(book.id, cust.id)"></p>
<input ng-model="qtys[index].qty">
</td>
</tr>
<tr>
<td>Total</td>
<td ng-repeat="book in books">
<input type=text value="{{sumQty(book.id)}}">
</td>
</tr>
I have 2 problems that I'm trying to solve:
The input boxes are now showing the correct values from my array. I'm trying to use ng-init to get the index from the array first based on the custId and bookId I pass in:
<p ng-init="index=getIndex(book.id, cust.id)"</p>
<input ng-model="qtys[index].qty">
I guess I'm not sure how to correctly bind the ng-model
The totals are not working. But I guess I need to solve the #1 problem before I can get this to work.
Please help. Thanks.
You got it almost right. The problem is in your getIndex method - the return that you do there does not return from getIndex, it just exits the function that you passed to forEach. If you modify it like this, it will work
$scope.getIndex = function(bookId, custId) {
var index = null;
angular.forEach($scope.qtys , function(item, idx) {
if (item.bookId == bookId && item.custId == custId) {
index = idx;
};
});
return index;
};
scope.sumQty seems unfinished (it takes a list as parameter, but you are passing an id to it). One way of fixing it would be to make it accept the id instead
$scope.sumQty = function(id) {
var total=0;
angular.forEach($scope.qtys , function(item){
if(item.bookId == id) {
total+= parseInt(item.qty);
}
});
return total;
}

Weird bug: AngularJS can search single fields but not multiple fields

I am writing an app using Angular 1.3. I want to search through a table by any word/field; one search box for everything.
Currently I am able to get my search box to search through a specific field fine by doing:
<label>Search: <input ng-model="searchKeyword.title"></label>
or
<label>Search: <input ng-model="searchKeyword.author"></label>
Those work fine. However, when I try to search all the fields at the same time by doing:
<label>Search: <input type = "text" ng-model="searchKeyword"></label>
It doesn't work at all.
This is my code on the table:
<label>Search: <input ng-model="searchKeyword"></label>
<table ng-repeat="post in posts | orderBy: sort | filter: searchKeyword">
<tr>
<td> {{post.title}} </td>
<td> {{post.author}} </td>
<td> {{post.content}} </td>
<td> {{post.date | date: "d/M/yyyy"}} </td>
</tr>
</table>
And this is inside my main controller:
$scope.posts = posts.posts;
$scope.searchKeyword = "";
Please tell me what exactly is causing this weird bug. Thank you.
Its because ng-repeat create a new scope and cannot write in to a primitive property of its parent scope.but can write in to an object of its parent scope.
A scope (prototypically) inherits properties from its parent scope.
this post explains it well.What you want here is a custom filter check the ng-modal against each property in your object.
. filter('customFilter', function() {
return function(product,query) {
var out = [];
angular.forEach(product,function(value,key){
if(value['author'].indexOf(query)>-1 || value['title'].indexOf(query)>-1)
{
out.push(value)
}
})
return out;
}
})
Here is a working example
i found this post very late. Anyway my suggestion may helpful to other viewer.
There is a solution for weird bug in weird way.
I found this post is very useful for use of multiple filters in controller with example of big data table search.
(after reading the above linked post)
But you dont need to include multiple watcher and multiple input fields in every column of table, for your scenario you will just need the below code change after static json declaration.
$scope.$watch('searchKeyword', function(val) {
$scope.items = $filter('filter')($scope.items2, val); });
considering searchKeyword is the ng-model of input field to search.

Angular ng-checked if item is in list

I have two lists of items:
all_todos
my_todos
I want to create a directive that will update the check boxes of the todos if the my todos are updated anywhere else in the application.
<tr ng-repeat="todo in all_todos">
<td>
<input type="checkbox" ng-checked="_.findIndex(my_todos, function(t){return t.id == todo.id;}) > -1"/>
</td>
<td>
{{todo.title}}
</td>
</tr>
Angular doesn't like the starting curly brace in the ng-checked. I want to avoid writing a $scope.watch and placing a property on the array if possible.
try to create a function in your controller which take the content of your actual ng-checked directive and call it in the ng-checked.
you should have something like that
// your controller
angular.module('..').
controller('..', function() {
$scope.test = function() {
return _.findIndex(my_todos, function(t){return t.id == todo.id;}) > -1;
}
})
and in your html
<.... ng-checked="test()" />
i think the declaration of the function in your directive is the key of your issue

duplicates in a repeater are not allowed angular

I'm trying to create a form like below, this using ng-repeat directive in angular and it whenever I created a new row complains
"Duplicates in a repeater are not allowed.".
While I understand the solution for this is by putting "track by $index", however it causes another issue, which clicking delete on one row deletes the value of other field. So I suspect that track by index is OK for static text but not input form. So how to use ng-repeat correctly for my case?
My jsfiddle : demo.
Edit : I do aware that json array of object will solve my issue ( because for object angular create $$hashKey ) and already implemented this for most of my other module. But I am actually expecting some fix that can be done without really change my json array of string. Sorry for not being clear.
My current code :
HTML
<div class="row-fluid spacer10">
<a ng-click="addAKA()" class="btn btn-primary spacer5 left30"><i class="icon-plus icon-white"></i> Add New Alias</a>
</div>
<div class="row-fluid spacer10"></div>
<div class="row-fluid spacer5" ng-repeat="item in aliasList track by $index">
<input type="text" class="span6 left30" ng-model="item">
<button class="btn btn-danger" ng-click="deleteAKA($index)">delete</button>
<BR/>
</div>
Javascript
$scope.addAKA = function ()
{
if($scope.aliasList == null)
{
$scope.aliasList = [];
}
$scope.aliasList.push("");
$scope.aliasjson = JSON.stringify($scope.aliasList);
}
$scope.deleteAKA = function (idx)
{
var aka_to_delete = $scope.aliasList[idx];
$scope.aliasList.splice(idx, 1);
$scope.aliasjson = JSON.stringify($scope.aliasList);
}
I would guess this is caused when there are more than one empty strings in the list.
If this is the case, it is caused because any two empty strings are equals in JS and Angular repeater does not allow duplicate values (as clearly stated in the message). This is a valid decision as they have to relate an object in the list with its DOM tree to minimize DOM manipulation.
A solution would be to insert simple objects containing the string in the model:
$scope.addAKA = function () {
...
$scope.aliasList.push({value:""});
...
};
And adjust your template:
<input type="text" class="span6 left30" ng-model="item.value">
Since all new objects are different, your problem should be solved.
See a fiddle where a filter is implemented to transform the model back to a list of strings.
When you type in a new created input, your list stays the same. Angular on any list change will update the view (ng-repeat) and remove all new stored text. Therefore we need to add ng-change to update our list on any input change
Add ng-change="change(i, $index) to your item and it should work
HTML
<div ng-controller='ctrl'>
<ol>
<li ng-repeat='i in list track by $index'>
<input type='text' ng-model='i' ng-change="change(i, $index)"></input>
<button ng-click='deleteItem($index)'>Delete</button>
</li>
</ol>
<button ng-click='addItem()'>Add</button>
<div>ITEM: {{list | json}}</div>
</div>
Javascript
angular.module("app", []).controller("ctrl", function ($scope) {
$scope.list = ["one","two"];
$scope.addItem = function ()
{
$scope.list.push("");
};
$scope.deleteItem = function (idx)
{
var item_to_delete = $scope.list[idx];
$scope.list.splice(idx, 1);
};
$scope.change = function (item, idx)
{
$scope.list[idx] = item;
};
});
See fixed Demo in DEMO
Yes, pushing more than one empty string will result in ng-repeat complaining.
In addition, you can also try:
if ($scope.aliasList.indexOf(VALUE_TO_ADD) === -1) {
...
}

Resources