ngModel cannot detect array changes correctly - javascript

The component model:
private SomeArray = [{ key: "Initial" }];
User can add/remove items dynamically:
addField() {
this.SomeArray.push({ key: Math.random().toString() });
}
removeField(index: number) {
this.SomeArray.splice(index, 1);
}
Template markup:
<div class="col-xs-12">
<button (click)="addField()" type="button">Add</button>
</div>
<div *ngFor="let field of SomeArray; let i = index;">
<input [(ngModel)]="field.key" #modelField="ngModel" [name]=" 'SomeArray['+i+'].key' " type="text" class="form-control" required />
<div [hidden]="modelField.pristine || !(modelField.errors && modelField.errors.required)" class="alert alert-danger">
Required error
</div>
<button (click)="removeField(i)" class="btn btn-danger">Remove</button>
</div>
This works untill user removes any item from SomeArray. If I add some two items initially:
and remove the one with 1 index:
then after adding another item Angular treat it as item has both 0 and 1 index (the new item "occupies" both two inputs):
(item with key 0.1345... is not displayed)
It's worth to noting items of SomeArray are as expected, but data binding fails. What can be the reason of it?
Update: Thanks to the comments of #Stefan Svrkota and #AJT_82 it's known for me the issue can be resolved by adding [ngModelOptions]="{standalone: true}" to the needed input. But I couldn't stop thinking about the reason of the issue in my cause, without setting standalone option (there is unique value for each name attribute so it's excepted nothing wrong here).
Finally I have found that behavior occurs when input elements are into <form> tag only - Plunker sample here (enclosing of template with form tag is the reason that issue).
Any ideas of this behavior?

The reason why it happens is ngFor mixes name properties when you delete some item.
When you use ngModel inside form each ngModel control will be added to form controls collection.
Let's see what happens if we have added three items and clicked on Remove the second
1) Step1 - SomeArray[1].key exists in collection controls
2) Step2 - SomeArray[1].key has been removed from controls collection
3) Step3 - Html looks like
4) Step4 We are adding a new item
So formGroup returns existing item.
How we can solve it?
1) Don't wrap our controls in form tag
2) Add ngNoForm attribute to form
<form ngNoForm>
3) Use
[ngModelOptions]="{standalone: true}
With all three solutions above:
We can remove [name] property binding
We can't use the built in Form group validation
4) Use trackBy for ngFor
template.html
<div *ngFor="let field of SomeArray; let i = index; trackBy: trackByFn">
component.ts
trackByFn(i: number) {
return i;
}
Plunker Example
This way our built in form will work properly

Related

How to solve Angular FormArray and mat-select options linked to input fields?

I have a scenario where the user can open mat-select and choose an option from the dropdown. Upon his selection a hidden form will be displayed for him to fill it. Everything was okay with static (one) template. Now, we decided to make the mat-select and the corresponding templates linked to the options inside Angular formArray.
I have two problems that I am facing right now:
1- All mat-selects in all the generated forms listen to the same event (user selected option. That is to say, when you change mat-select in index 0 of the formArray the mat-select in index 1 react to your changes.
2- All the generated forms appear with the template linked to the option that the user chose from the 1st selection (index 0 in the formArray).
There are no syntax errors.
I guess the problem stemming from a global variable that I defined to decide to display one of the forms or not: (isPremiumAmountSelected in code below). I do not like to split my component to use #Input and #Output. I need a solution that builds upon what is shared below. (There are many related stuff depends on this).
isPremiumAmountSelected: boolean = false;
FixedProductPrice: Boolean = undefined;
OneFactorPricingCarValue: Boolean = undefined;
MultiFactorPricingCarValueCarModel: Boolean = undefined;
ToggledPremiumAmountTypeDropDownOptions(data){
this.isPremiumAmountSelected = true;
if(Object.is(data.value, 1)){
this.FixedProductPrice = true;
this.OneFactorPricingCarValue = false;
this.MultiFactorPricingCarValueCarModel = false;
}if(Object.is(data.value, 2)){
this.OneFactorPricingCarValue = true;
this.FixedProductPrice = false;
this.MultiFactorPricingCarValueCarModel = false;
}if(Object.is(data.value, 3)){
this.MultiFactorPricingCarValueCarModel = true;
this.FixedProductPrice = false;
this.OneFactorPricingCarValue = false;
}
}
<div formArrayName="formArray" *ngFor="loop goes here; let i = index">
<div [formGroupName]="i">
<mat-form-field appearance="outline" *ngIf="options">
<mat-label>Available Types</mat-label>
<mat-select
(ngModel)="selectedPremiumAmountOptionValue"
(onSelectionChange)="ToggledPremiumAmountTypeDropDownOptions($event)"
formControlName="control">
<mat-option *ngFor="let option of options"
(click)="ToggledPremiumAmountTypeDropDownOptions(option)"
[value]="option?.value">
{{option?.viewValue}}
</mat-option>
</mat-select>
</mat-form-field>
<span *ngIf="isPremiumAmountSelected">
<div *ngIf="FixedProductPrice"> Template 1 goes here</div>
<div *ngIf="OneFactorPricingCarValue"> Template 2 goes here</div>
<div *ngIf="MultiFactorPricingCarValueCarModel"> Template 3 goes here</div>
</span>
</div>
</div>
UPDATE
I was able to solve it few hours after posting my question. My approach was similar to #Eliseo answer below. Probably more straightforward.
Step one: Delete the TS method and golabl variable
These two should be deleted: ToggledPremiumAmountTypeDropDownOptions() method and the isPremiumAmountSelected variable. The other three vars should remain.
Step two: Adjust the ngIf logic in the template
To prevent the problem of multi-listeners to the same event I access the value of the selected option in every iteration *ngIf is inside it. (delete the span that hosts the isPremiumAmountSelected and put the logic below for every conditional template you have) Also, do not forget to clear the methods on the mat-select (It should be normal without any method).
Something like the following for the *ngIfs that will hide and display the templates conditionally:
*ngIf="productCreationForm.get('formArrayName')['controls'][i].get('mat-select-formControlName').value === 'x'"
I do this with every conditional template I want to display.
That is to say I am verifying the value in the template. And with this i get rid of the global var isPremiumAmountSelected which will be true always in every iteration based on the first selection.
Hope this was illustrative. The approach of #Eliseo below is feasible and correct I think.
In general to mannage a FormArray of FormGroups that belong to a FormGroup you has a getter to get the formArray
get myFormArray()
{
return this.myForm.get('myFormArray') as FormArray
}
<form [formGroup]="myFormGroup">
<!--use formArrayNem-->
<div formArrayName="myFormArray">
<!--see that we use the "getter" before and iterate over "controls"-->
<!--see the [formGroupName]="i" IN the own loop-->
<div *ngFor="let group of myFormArray.controls;let i=index"
[formGroupName]="i">
<!--here you use formControlName to input the control, e.g.-->
<input formControlName="control">
<!--to know the value of control you can use, e.g.-->
<div *ngIf="formArray.value[i].control==2">is two</div>
<!-- or -->
<div *ngIf="formArray.at(i).value.control==2">is two</div>
<!-- or -->
<div *ngIf="formArray.at(i).get('control').value==2">is two</div>
</div>
</div>
</form>
You needn't use three variables to know is if fixedProduction, onFactor or multipleFactor. If you want to use three variables, you need use tre arrays, but you can see that is unnecesary also is unnecesary the (onSelectionChange) event.
NOTE: You're using (ngModel)' -not exist (ngModel)- if you want to say [ngModel]` remember that if you're using FormArrays or FormControl, you should NOT use ngModel-,
NOTE2: In my answer I use some like
form=new FormGroup({
formArray:new FormArray([this.getGroup(),this.getGroup()])
})
get formArray()
{
return this.form.get('formArray') as FormArray
}
getGroup()
{
return new FormGroup({
control:new FormControl(),
fixedPrice:new FormControl(),
oneFactor:new FormControl(),
})
}
I don't know what are you using

Angular 6 Nested FormArray

I'm trying to make a form with an array of fields generating by clicking '+' using Angular Reactive Forms. However, when I try to use [formControlName]="index" in child component it doesn't work.
Firstly it was saying that formControlName should have a parent with FormGroup I passed it and added. Now it says Cannot find control name with unspecified name attribute. In the Angular docs it's said that one able to use FormArray with [formControlName] and index since there is no names.
This is what I have
https://stackblitz.com/edit/angular-runxfe
I have fixed your issues. You needed to pass the formGroupName here:
<app-site-form-feature
*ngFor="let feature of features.controls; let last = last; let index = index; let first = first" [formGroupName]="index"
[last]="last" [index]="index" [first]="first"
(addFeature)="addFeature()" (removeFeature)="removeFeature(index, first, last)">
</app-site-form-feature>
HTML of the child component will look like this:
<div class="row no-gutters form-group">
<input type="text" class="form-control px-2 col-10">
<span class="btn btn-outline-danger" (click)="removeFeature.emit(index, first, last)">-</span>
<span class="btn btn-success" *ngIf="last" (click)="addFeature.emit($event,index)">+</span>
</div>
Take a look to this stacblitz https://stackblitz.com/edit/angular-v8zkz2
You can also reference this guide:
https://alligator.io/angular/reactive-forms-formarray-dynamic-fields/

Avoiding a large amount of hidden elements for in place editing with angularjs

I have a rather large list with todo items for which I want to add in place editing for:
[ ] first todo
[ ] second todo
[ ] third todo
after clicking on the second todo, the text of the second todo can be edited but you can also set some properties on the todo:
[ ] first todo
-----------------------------------------------------------
| [ ] second todo__________________ |
| due: __/__/____ |
| assigned: ______________ |
| |
| [save] [cancel] |
-----------------------------------------------------------
[ ] third todo
Now I can do something like:
<div ng-repeat="todo in todos">
<div ng-show="!doedit">
<input type="checkbox"> {{todo.title}}
</div>
<div ng-show="doedit" class="boxed">
<input type="checkbox"><input type="text" ng-model="todo.title"><br>
<input type="date" ng-model="todo.due"><br>
<input type="text" ng-model="todo.assigned"><br>
<button ng-click="doedit = false">save</button>
</div>
</div>
This should work (ignoring how the cancel button should work) but if I have a large todo list (100+ items) it will create a large amount of hidden elements which are probably never used but still bound to variables.
Is there a better way to do this? I looked at angular-xeditable which seems to dynamically add elements but it only works for simple input elements.
Or is having a large amount of hidden elements not an issue in angular?
ng-if removes the element from DOM entirely if it evaluates to false, including any watchers. For example:
<div ng-if="doEdit"></div>
This post explains some of the differences between ng-if and ng-show.
Having large amounts of hidden elements is not an issue only with the js but also with the DOM itself. you should not be doing that.
What can be done instead is
1. Using ng-if - keep a state of all the element lets say isEdited after the user clicks on the todo. Set the variable / state isEdited = !isEdited. This would set it to true if it is not already. Then inside your DOM write HTML using ng-if. Something like this
<div class="to-do-item"></div><br>
<div ng-if="isEdited">
<!--UI for the edited part-->
</div>
Since ng-if does not render the html till the condition is true. There wont be too much load on the DOM.
2. If the template for editing the to do item is same for all the todo items. You should prefer using ng-include with ng-if. This way the template would be downloaded the first time you try to edit the element. Then you can cache it so not network lag. Also there is no extra hidden html. Whatever needs to be on the page is on the page and is not hidden.
Wrapping all of that editing section into one container with an ng-if would reduce the number of internal watches down to one for the whole section when it wasn't active

AngularJS: ng-if outside ng-repeat breaks ng-repeat

AngularJS Verion: 1.3.8
JSFiddle: http://jsfiddle.net/uYFE9/4/
I've been working on a small AngularJS application, and ran into a bit of a problem. I have an ng-repeat on a page, which fills in the contents of a form. The amount of items in the form is defined by a dropdown bound to a model, and populated using ng-options. Something like:
<select id="testAmount" ng-model="selectedItem" ng-options="item.name for item in items"></select>
<form role="form" name="testForm" ng-if="!complete">
<div ng-repeat="i in getNumber(selectedItem.number) track by $index">
{{$index}}
</div>
</form>
Complete is set to false in the beginning, and hitting a Next button will toggle complete and hide the form and dropdown. A Back button will then toggle complete back, and show the form again.
The problem I'm having is with the ng-if on the select (and previously, I had the form wrapped in a div with the same ng-if - same problem). The ng-repeat no longer updates when the select dropdown is changed. Removing the ng-if on the select restores the ng-repeat to working order.
I'm wondering if there's something strange I'm doing with the nesting here, or if it's actually a bug? You can test it out on the JSFiddle linked above. The $index should be printed the number of times on the dropdown, but isn't.
Interestingly enough - when debugging the problem on my local machine, having FireBug open fixed the issue.
This is because of ng-if creating a child scope and how prototypical inheritance works with primitives. In this case, the primitive is selectedItem that you are setting by the <select>, but is actually being set on the child scope and shadows/hides the parent scope property.
In general you should always use a dot (.) with ng-models:
$scope.selection = {selectedItem: undefined};
And in the View:
<div ng-if="!complete">
<select ng-model="selection.selectedItem"
ng-options="item.name for item in items"></select>
</div>
ng-if is causing you some scoping issues (which messes with the binding).
Here is an updated jsfiddle that you could use as a work around. Essentially, this example wraps another div around the items that you want to end up hiding. And then adds a next function so that the same scope is affected during the click that sets complete to true.
HTML:
<div ng-app="test">
<div ng-controller="TestCtrl">
<div ng-if="!complete">
<div>
<label for="testAmount">Amount:</label>
<select id="testAmount" ng-model="selectedItem" ng-options="item.name for item in items"></select>
</div>
<form role="form" name="testForm">
<div ng-repeat="i in getNumber(selectedItem.number) track by $index">
{{$index + 'hi'}}
</div>
<button class="btn btn-default" value="Next" title="Next" ng-click="next()">Next</button>
</form>
</div>
<div ng-if="complete">
</div>
</div>
</div>
JS:
angular.module('test', [])
.controller('TestCtrl', function($scope) {
$scope.complete = false;
$scope.items = [
{ name: '2', number: 2 },
{ name: '3', number: 3 },
{ name: '4', number: 4 }
];
$scope.selectedItem = $scope.items[0];
$scope.getNumber = function (number) {
return new Array(number);
};
$scope.next = function() {
$scope.complete = true;
};
})
I believe the problem is with your select statement inside an ng-if the selectedItem is never getting set. If you just don't want to show that dropdown when !complete change it to an ng-show and it works fine.
<div ng-show="!complete">
As to WHY the ng-model is not being bound inside the ng-if, I don't really know but it does make some sense in that you are trying to do a conditional bind which is a bit screwy

How to add number of new html elements based on the number entered into ng-model

I am new to angular. I am trying to add icons/input tags based on value entered into the model.
e.g. i have a model seat
<input ng-model ="seat" > //lets say user enters 3
i want to dynamically generate three models as
<input ng-model="seat.seat1">
<input ng-model="seat.seat2">
<input ng-model="seat.seat2">
Thanks in advance ..
Initial $scope.seats in Controller:
$scope.seats = [];
And add below code to Template:
<input ng-model="seats.length">
<input ng-repeat="seat in seats track by $index" ng-model="seats[$index]">
when change the seats.length to 3, it will add null to array temporarily.
$scope.seats // [null, null, null]
So must to use track by $index to avoid same value issue
Demo on plnkr here
Use ng-repeat and repeat the loop till the model value and inside that create the models.

Categories