Working of ng-repeat with dynamic data - angularjs

I have a query regarding angularjs, i.e. I have a table in which I have rows, but these rows are dynamically inserted by user. That means If I have a company having more than one owners then I allow them to add as many owners as they want by simply clicking on add button which is placed with the textboxes (i.e. name, email, number textboxes)
I have used 'ng-repeat' on a row so that the list of owners can be dynamically added one by one. While fetching values from the database it is working perfectly fine but when I am trying to add values by clicking 'add' button, It adds the new row with all the text boxes BUT WITH THE SAME VALUES as the previous row. I think it is because I have used ng-model to link the data.. but then that is the only option I can bind the data.. Do anyone have solution for this problem.? Please help me.
Thank you in advance
I am sharing the code sample for better understanding,
JSP code:
<tbody id="insertionRow">
<tr>
<th>#</th>
<th class="required">Name</th>
<th>Email</th>
<th>Phone No</th>
<th>Add</th>
<th>Delete</th>
</tr>
<tr data-ng-repeat="c in ctrl.client.clientOwnerVOList">
<td>{{$index + 1}}</td>
<td class="col-lg-3"><input type="Text"
class="form-control"
data-ng-model="c.clientOwnerName"
name="clientOwnerName{{$index + 1}}" id="Name">
</td>
<td class="col-lg-4"><input type="Email"
class="form-control"
data-ng-model="c.clientOwnerEmail"
name="clientOwnerEmail{{$index + 1}}" id="Email"></td>
<td class="col-lg-3"><input type="Text"
class="form-control"
data-ng-model="c.clientOwnerPhone"
name="clientOwnerPhone{{$index + 1}}" id="PhoneNo"></td>
<td>
<button type="button"
data-ng-click="insert();"
class="btn btn-sm btn-default">
<i class="fa fa-plus fa-lg"></i>
</button></td>
<td><button type="button"
onClick="$(this).closest('tr').remove();"
class="btn btn-sm btn-default">
<i class="fa fa-trash fa-lg "></i>
</button></td>
</tr>
</tbody>
AngularJS controller code:
$scope.insert = function(){
var tableRow ="<tr data-ng-repeat='c in ctrl.client.clientOwnerVOList'>"+
"<td>"+i+"</td>"+
"<td class='col-lg-3'><input type='Text' class='form-control' data-ng-model='c.clientOwnerName' name='clientOwnerName{{$index + 1}}' ></td>"+
"<td class='col-lg-4'><input type='Email' class='form-control' data-ng-model='c.clientOwnerEmail' name='clientOwnerEmail{{$index + 1}}'</td>"+
"<td class='col-lg-3'><input type='Text' class='form-control' data-ng-model='c.clientOwnerPhone' name='clientOwnerPhone{{$index + 1}}' ></td>"+
"<td><button type='button' data-ng-click='insert()' class='btn btn-sm btn-default'><i class='fa fa-plus fa-lg'></i></button></td>"+
"<td><button type='button' class='btn btn-sm btn-default' onClick=$(this).closest('tr').remove();><i class='fa fa-trash fa-lg '></i></button></td>"+
"</tr>";
var compiledString = $compile(tableRow)($scope);
$("#insertionRow").append(compiledString);
i++;
};
And the client Object is as follows
self.client= {
clientID:'',
clientName:'',
clientDescription:'',
clientAddressLine:'',
clientContactPersonPhone:'',
createdOn:'',
astUpdatedOn:'',
country:'',
state:'',
city:'',
isDeleted:'',
clientOwnerVOList: [
{
clientOwnerID:'',
createdOn:'',
isDeleted:'' ,
clientOwnerName:'',
clientOwnerPhone:'',
clientOwnerEmail:''
}
]
}

I tried to reproduce your problem in this plunker :
https://plnkr.co/edit/Eqw21bGNetGtHZIQZCYw?p=preview
I changed your insert function to create new clientOwnerVO.
I just push new object like this :
$scope.client.clientOwnerVOList.push({clientOwnerID:'',
createdOn:'',
isDeleted:'' ,
clientOwnerName:''
,clientOwnerPhone:''
,clientOwnerEmail:'',});
i++;
I hope i understand your problem

The insert function is pretty strange. AFAIT you are inserting some compiled HTML into the DOM. Angular's whole thing is for you to avoid doing this by hand. You should be changing ctrl.client.clientOwnerVOList directly by appending the required JS objects to it and then the change will be reflected by the DOM.
The code should look similar to this, plus-minus application specific logic which you know better.
$scope.append = function() {
self.client.clientOwnerVOList.push({
clientOwnerId: 'newid',
createdOn: 'today',
isDeleted: 'false',
clientOwnerName: 'foo',
clientOwnerPhone: 'bar',
clientOwnerEmail: 'foo#bar.com'
});
}
The values to populate the client info would be taken from other places (other models fields, or some sensible defaults etc.)
Basically, you have a bunch of data (self.client, and any other controller variables from the scope). The view is a function of this data, and you describe how to build the DOM through the html template which can access the fields from the scope. It's Angular's job to figure out how to do that. Whenever some sort of event happens, like a button press or network event etc., you update the data, and then Angular rebuild the DOM for you.

Related

On change ng-model value should be change

I have below code.
<td class="minwd100">
<select class="form-control" id="invoice_id" name="invoice_id" ng-options="invoice as invoice.invoice_number+ ' - '+invoice.client_name+' - '+invoice.invoice_converted_total for invoice in approved_invoices track by invoice.invoice_id" ng-model="approved_invoices.invoice_id" ng-change="changeInvoice(approved_invoices.invoice_id)"></select>
</td>
<td class="minwd100"><input type="text" class="form-control" id="payment_amount" ng-model="payment_data.payment_amount.value" name="payment_amount" placeholder="Enter {{form_data.payment_amount.label}}" ng-focus="form_data.payment_amount.error = ''" /></td>
<td class="minwd100"><input type="text" class="form-control" id="payment_conversion" ng-model="payment_data.payment_conversion_rate.value" name="payment_conversion" placeholder="Enter {{form_data.payment_conversion_rate.label}}" ng-focus="form_data.payment_conversion_rate.error = ''" /></td>
<td class="minwd100"><input type="text" class="form-control" id="payment_converted_amount" ng-model="payment_data.payment_converted_amount.value" name="payment_converted_amount" placeholder="Enter {{form_data.payment_converted_amount.label}}" ng-focus="form_data.payment_converted_amount.error = ''" ng-readonly="true"/></td>
<td class="wd80">
<div class="dt-options">
<a href ng-if="$index === form.payments.length - 1" ng-click="addContact()" title="Add Contact"><i class="fa fa-plus fa-lg"></i></a>
<a href ng-if="form.payments.length > 1" ng-click="removeContact($index)" title="Remove Contact"><i class="fa fa-trash-o fa-lg"></i></a>
</div>
</td>
<script>
$scope.changeInvoice = function (selectedItem){
$scope.payment_data.payment_amount.value = selectedItem.invoice_converted_total;
$scope.payment_data.payment_conversion_rate.value = selectedItem.invoice_conversion_rate;
$scope.payment_data.payment_converted_amount.value = selectedItem.invoice_converted_balance;
$scope.client_id = selectedItem.client_id;
$scope.payment_data.invoice_id.value = selectedItem.invoice_id;
};
$scope.addContact = function() {
$scope.form.payments.push({invoice_id: '', payment_amount: '', payment_conversion: '', payment_converted_amount: ''});
};
$scope.removeContact = function(index) {
$scope.form.payments.splice(index, 1);
};
</script>
To each change, value should be triggered for each input, But now whenever I add new row, its reflect old value itself please help. I have added image also please refere that.
What is the collection that you are iterating through?
"To each change, value should be triggered for each input, But now whenever I add new row, its reflect old value itself please help. I have added image also please refere that."
Are you saying that when you change one row every row changes? That is how I am understanding it. My guess is that all of the items in the collection are referencing the same object and you need to refer to the specific item within the collection.
I recently set up a model that had a form where you could add many associations so when adding a new association to the form I had to use a unique key to refer to the hash. I.e.:
$scope.addNewPartToAssembly = function(part) {
$scope.form.assembly.assembly_parts[part.id] = {
...
}
}
I'm just kind of guessing at your problem because to me it seems incomplete, maybe somebody else could resolve it with what's provided. Either way, hope that helps.

Custom validation via child control

So I'm working inside a directive that contains it's own form element and buttons, however all the controls must be transcluded through. The model for this particular view contains a property for total capacity, and a property that is a collection of compartments (separate entity). Each compartment has it's own capacity. I already have a function that will show an error on the view if/when the Total Capacity is not equal to the combined capacity of all compartments. The problem here is, since all my controls are transcluded through (and I'm not supposed to modify the parent directive) I have no clue if/how I can use that same function to mark the form as invalid to disable the save button. I was wondering if there is a solution (hopefully one that doesn't involve custom directives or services) that would allow me to set the parent form invalid if an expression returns true.
** UPDATE **
Sorry guys, I think I explained it backwards the first time. So this would be a good representation of what is going on in the html. (Also I haven't used stackoverflow much before this so bear with me)
edit-page-directive:
<div>
<form name="editForm">
<ng-transclude>
</ng-transclude>
<a class="btn btn-success">Save</a>
<a class="btn btn-danger">Cancel</a>
</form>
</div>
View for this particular edit:
<edit-page>
<uib-tabset>
<uib-tab>
<!--Total Capacity input-->
<input type="text" numeric="{min:1, format:'#,###.#'}" ng-model-options="{updateOn: 'blur'}" class="form-control" id="tcCapacity" name="tcCapacity" data-ng-required="true" ng-model="vm.dataContext.entity.TotalCapacity" />
<!--End Total Capacity-->
</uib-tab>
<uib-tab>
<table>
<tr><thead><th>...</th><th>Capacity</th><th>(Buttons for compartment add/remove)</th></thead></tr>
<tr ng-repeat="compartment in vm.dataContext.entity.TrailerConfigCompartments">
<td width="200">{{compartment.Sequence}}</td>
<!--Important input under this-->
<td><input type="text" numeric="{min:0, format:'#,###.#'}" class="form-control" ng-model="compartment.Capacity" data-ng-required="true" /></td>
<!--Important input above-->
<td align="right" style="padding-right:30px;">
<a class="btn" style="padding: .7em; color: black;" ng-click="vm.addCompartment(compartment.Sequence + 1)">
<span uib-tooltip="New compartment at sequence {{compartment.Sequence + 1}}" class="btn-edit" style='margin-left:5px'><span class="glyphicon glyphicon-plus" style="margin-top:3px"></span></span>
</a>
<a class="btn" style="padding: .7em; color: black;" ng-click="vm.removeCompartment(compartment)">
<span uib-tooltip="Remove compartment" class="btn-edit" style='margin-left:5px'><span class="glyphicon glyphicon-minus" style="margin-top:3px"></span></span>
</a>
</td>
</tr>
</table>
</uib-tab>
</uib-tabset>
</edit-page>
If I understand you correctly you have something like
HTML
<div data-ng-controller="FormController as vm">
<form class="foo form">
<input type="text"> // some inputs
<input type="text"> // some inputs
<transcluded-directive>
<button class="foo button-to-disable">Do something</button> // button that should be disabled
</transcluded-directive>
</form>
</div>
JS
.controller("FormController", function($scope) {
var vm = this;
vm.validateTotalCapacity = function () {
// validation stuff
}
});
So I think you can do something like:
HTML
<div data-ng-controller="FormController as vm">
<form class="foo form {{vm.validateTotalCapacity() ? '' : 'form-has-errors'}}" >
<input type="text"> // some inputs
<input type="text"> // some inputs
<transcluded-directive>
<button class="foo button-to-disable">Do something</button> // button that should be disabled
</transcluded-directive>
</form>
</div>
Look I put your form validator in <form class="foo form"> and make condition for error class
CSS
.form-has-errors .button-to-disable {
pointer-events: none;
cursor: default;
opacity: 0.5
// or your custom disabled styles
}
UPDATE
I see, but I believe you could try this:
HTML
<div>
<form name="editForm" class="{{editForm.$valid ? '' : 'form-has-errors '}}">
<ng-transclude>
</ng-transclude>
<a class="btn btn-success">Save</a>
<a class="btn btn-danger">Cancel</a>
</form>
</div>
So I realized I had misinterpreted the customValidation piece of angularjs. I thought any directive I'd have to create for validation would have to be added to the form element itself. Just as well I thought it would be alot harder to set up than it actually is.
For future reference:
1.) Create a directive and restrict it to an attribute
2.) Require ngModel for this directive
3.) Set up your link function:
link: function(scope, elem, attrs, ngModel) {....}
4.) Add a function to the $validators object of the control you want to validate. Do this INSIDE of your link function. Ex:
link: function(scope, elem, attrs, ngModel) {
ngModel.$validators.validationFn = function(value) {
//Where value is the current value of the control
//In my case, where I want to compare value to the combined value of other
//compartments I would send in whatever data I wanted via the scope property of
//this directive and compare the two in this function
}
}
5.) Return true if control is valid and vice versa
And that's it.
If you want to access this validator to display an error message just:
ng-show="vm.arbitraryInput.$error.validationFn"
Keep in mind that now if it returns true, then the input is invalid.

angular and asp.net mvc delete with post

I currently have the following grid on my page:
<tr ng-repeat="cartItem in vm.shoppingCart">
<td>{{cartItem.Title}}</td>
<td>
#using (Html.BeginForm("Delete", "MyController", FormMethod.Post))
{
#Html.AntiForgeryToken()
<input type="submit" value="Delete" id="DeleteBtn" name="DeleteBtn" class="btn blue" />
}
</td>
Our users have requested that the delete happen with a post to the server, so we setup a form in the grid, i'm not sure if that's the best way to go about it. also how would we pass in the Id (cartItem.Id) of the item to delete to the action? Is it common to have a form in each row like this? otherwise we can convince the users to do it through client side.

repeating a test condition iteratively in protractor test case not working

I have a create page in which there is a textbox and dropdown, I want to create a item for each dropdown value. After creation of single item it redirects to edit page from there we have to go to create page and create item with second dropdown value iteratively.
I am trying to iterate over the dropdown values and trying to create item under each condition but the test exists after the intial item creation and it does not create item for second dropdown value.
describe('When create case form is valid', function(){
it('Should be creating cases', function(){
browser.get("xyz.com");
browser.waitForAngular();
element(by.id("create")).click();
element.all(by.options('caseSubType.specID as caseSubType.specName for caseSubType in vm.newCaseSubTypes')).each(function(elem, index) {
elem.getText().then(function (text) {
elem.click();
element(by.model('vm.newCaseSummary')).sendKeys('test');
$('button[ng-disabled="!vm.hasClientIDForNewCase()"]').click();
browser.waitForAngular();
browser.get("xyz.com");
browser.waitForAngular();
element(by.id("create")).click();
})
})
});
});
HTML for the create page is:
<table>
<tr>
<td>Summary</td>
<td><input type="text" class="form-control input-sm" id="txtNewCaseSummary" ng-model="vm.newCaseSummary" ng-disabled="!vm.hasClientIDForNewCase()" name="caseSummary" required></td>
</tr>
<tr>
<td>Type</td>
<td>
<select class="form-control input-sm" ng-model="vm.newCaseSubTypeID" ng-options="caseSubType.specID as caseSubType.specName for caseSubType in vm.newCaseSubTypes" name="caseSubType">
</td>
</tr>
<tr>
<td>
<button id="create" type="submit" class="btn btn-primary" ng-disabled="!vm.hasClientIDForNewCase()">Create New Case</button>
</td>
</tr>
</table>
Can anyone suggest some other approach if this is not the correct approach or please provide a solution with current approach.
I would suggest doing it recursively :
var cases = element.all(by.options('caseSubType.specID');
createCases(cases, 0);
var createCases = function(cases, i) {
if (i = cases.length)
return;
browser.get("xyz.com");
//browser.waitForAngular(); NOT needed, if it's an angular page
element(by.id("create")).click();
cases[i].click();
element(by.model('vm.newCaseSummary')).sendKeys('test');
$('button[ng-disabled="!vm.hasClientIDForNewCase()"]').click();
createCases(cases, i + 1);
}

ng-form not available after manually deleting an item from collection of ng-repeat

Edit Here is the plunk for this question
I have a master detail form for an accounting transaction. The master portion just contains two fields name and Type. The detail portion can have two or more entries and each entry has AccountId, Debit and Credit fields. The form looks like
You can see that there is a delete button against each entry so if we have more than two entries we can delete any entry at random. The form html looks like following
<body data-ng-app="transactions">
<div data-ng-controller="transactionsController">
<form role="form" name="transactionForm" novalidate data-ng-submit="create()">
<div class="row">
<div class="col-md-2">
</div>
<div class="col-md-4">
<h2 class="form-login-heading">Create Transaction</h2>
<div data-ng-repeat="error in errors" class="alert alert-danger">
{{error[0]}}
</div>
<input type="text" name="name" class="form-control" placeholder="Name" data-ng-model="transaction.Name" required autofocus>
<div>
<span class="error" data-ng-show="transactionForm.name.$error.required && submitted">Please enter Name</span>
</div>
<input type="text" name="type" class="form-control" placeholder="Type" data-ng-model="transaction.Type" required>
<div>
<span class="error" data-ng-show="transactionForm.type.$error.required && submitted">Please enter Type</span>
</div>
<!--<input type="text" readonly name="number" class="form-control" placeholder="Number" data-ng-model="transaction.Number">
<input type="checkbox" data-ng-model="transaction.IsFinalized" /> <label>Finalize</label>-->
<table>
<tr>
<th>Account</th>
<th>Debit</th>
<th>Credit</th>
<th> </th>
</tr>
<tr data-ng-form="entryForm" data-ng-repeat="entry in transaction.Entries track by $index">
<td>
<input required name="accountId" data-ng-model="entry.AccountId" class="form-control" />
<span class="error" data-ng-show="entryForm.accountId.$error.required && submitted">Please select an account</span>
</td>
<td>
<input type="text" name="debit" data-ng-required="!entry.CreditAmount" class="form-control" placeholder="Debit" data-ng-model="entry.DebitAmount">
<span class="error" data-ng-show="entryForm.debit.$error.required && submitted">Debit is required</span>
</td>
<td>
<input type="text" data-ng-focus="checkAddRow($index)" name="credit" data-ng-required="!entry.DebitAmount" class="form-control" placeholder="Credit" data-ng-model="entry.CreditAmount">
<span class="error" data-ng-show="entryForm.credit.$error.required && submitted">Credit is required</span>
</td>
<td><button data-ng-show="transaction.Entries.length>2" class="btn btn-md btn-info " type="button" data-ng-click="deleteRow($index)">delete</button></td>
</tr>
<tr>
<td>Total</td>
<td><input readonly name="totalDebit" type="text" class="form-control" placeholder="Total Debit" data-ng-value="totalDebit()"></td>
<td><input readonly name="totalCredit" compare-to="totalDebit" type="text" class="form-control" placeholder="Total Credit" data-ng-value="totalCredit()"></td>
</tr>
<tr>
<td></td>
<td><b>Difference</b></td>
<td>
<input name="difference" readonly type="text" class="form-control" data-ng-value="difference()">
<!--<span class="error" data-ng-show="submitted && !differencezero">Difference should be 0</span>-->
</td>
</tr>
</table><br />
<button class="btn btn-md btn-info" type="submit">Create</button>
<button class="btn btn-md btn-info" data-ng-show="transaction.Entries.length<15" type="button" data-ng-click="addRow()">Add Row</button>
<div data-ng-hide="message == ''" class="alert alert-danger">
{{message}}
</div>
</div>
<div class="col-md-4">
</div>
<div class="col-md-2">
</div>
</div>
</form>
<style type="text/css">
.error {
color: red;
}
</style>
<pre>{{transactionForm.entryForm|json}}</pre>
</div>
I have the requirement that when focus is on Credit input of last entry the new entry should automatically added to the UI. I do it by using addRow and checkAddRow method on my controller. these methods are as follows
$scope.checkAddRow = function (index) {
if (index == $scope.transaction.Entries.length - 1) {
$scope.addRow();
}
}
$scope.addRow = function () {
entry = {
EntryTime: '',
DebitAmount: '',
CreditAmount: '',
AccountId: ''
};
$scope.transaction.Entries.push(entry);
console.log($scope.transactionForm);
}
$scope.deleteRow = function (index) {
$scope.transaction.Entries.splice(index, 1);
console.log($scope.transactionForm);
}
Again this part is just fine and works well. But I have another requirement that says that if last entry is not used it should not cause the form to invalidate. It should rather be removed from transaction.Entries collection and rest of the data should be saved normally. To achieve this, I have create function defined on $scope that looks like following
$scope.create = function () {
$scope.submitted = true;
if ($scope.transactionForm.entryForm && $scope.transactionForm.entryForm.$invalid && $scope.transactionForm.entryForm.$pristine) {
$timeout(function () {
$scope.deleteRow($scope.transaction.Entries.length - 1);
});
$timeout(function () {
console.log('From time out', $scope.transactionForm.$valid);
console.log($scope.transactionForm.$valid);
if (!$scope.transactionForm.$valid) return;
alert('data saved!');
console.log($scope.transactionForm);
//$scope.transactionForm.name.focus();
}, 200);
}
else {
if ($scope.transactionForm.$valid) {
alert('data saved 2');
}
}
}
You can see that what create function is doing. It is checking if entryForm (ng-form) is present in the main form (transactionForm) then it checks if entryForm is $invalid and $pristine if all these flags are true then, I delete the last entry from $scope.transaction.Entries and save the data (currently an alert to show data is saved) after $timeout. If I don't use timeout then the form is invalid so I have to wait for 200ms before I check the forms $valid flag after removing last row. But to my surprise when I remove last row from create function, there is no entryForm attached to the outer transactionForm. On the other hand If I delete entries using delete buttons present on UI, the entryForm is present inside the main transactionForm. Can anyone explain why is that. I have added <pre>{{transactionForm|json}}</pre> at the end to see when it is and when it is not available on main form. I have created a plunk to show what I mean. just add some data in two input fields of the master portion, enter some data in accountid field of both entries, when you reach the Credit input of second (last) entry, a new entry will be automatically added. Ignore that row and just push create button. The last entry will be removed and data will save but the entryForm will not be there anymore. I am not sure what I am doing wrong.
So, a problem here is that your definition of whether the form is valid or not depends on the state of the last row.
The last row could be of the following variety:
row fetched from the backend, but not new --> should only invalidate if not valid
row is new and $pristine --> should not invalidate
row is new, but $dirty (and still last) --> should only invalidate if not valid
You are trying to remove the last row and then re-evaluate the form for validity.
Approach it the other way - don't let the last row invalidate the form if it's in $pristine state:
Here's a simplified example:
<form name="transactionForm" ng-submit="submit()" novalidate>
<table>
<tr ng-form="entryForm" ng-repeat="transaction in transactions">
<td><input ng-model="transaction.account"
ng-required="transaction !== newLastEntry || entryForm.$dirty"></td>
<td><input ng-model="transaction.amount"
ng-required="transaction !== newLastEntry || entryForm.$dirty"
ng-focus="addEntryIfLast($last)" type="number"></td>
</tr>
</table>
</form>
Note $scope.newLastEntry here. It is set to the new empty (and last) entry. This happens when you add a new empty row:
function addEmptyEntry(){
$scope.newLastEntry = {};
$scope.transactions.push($scope.newLastEntry);
}
And so, ng-required is applied only if row is NOT new last OR otherwise $dirty.
Then, on submit, you can remove the last entry if it's in $pristine state and indeed the new last (as opposed to whatever existed before):
$scope.submit = function(){
var itemsToSubmit = angular.copy($scope.transactions);
if ($scope.transactionForm.$invalid) return;
if ($scope.transactionForm.entryForm &&
$scope.transactionForm.entryForm.$pristine &&
$scope.transactions[$scope.transactions.length - 1] === $scope.newLastEntry) {
itemsToSubmit.splice(itemsToSubmit.length - 1);
}
console.log(JSON.stringify(itemsToSubmit));
};
plunker

Resources