Angular 6 Nested FormArray - javascript

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/

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

How to use a global variable to enable or disable a button in Angular template driven forms?

<a #globalVar>
<input [(ngModel)] = "newTitle"
#newBlogTitle_l = "ngModel" >
<div *ngIf = 'newBlogTitle_l.value === "" ' >
globalVar.value = false
</div>
<button (click) = "onSave()" [disabled] = "globalVar" > Save </button>
If there is nothing in the input box, I expect the button to remain enabled.
What's the way to achieve that?
(This is just for learning purpose.)
It is not a good idea to modify values of variables in the template. It makes it hard to maintain in the long run. Variables modifications should be almost always done in the controller.
Besides, when there is a variable bound to [(ngModel)], you can use it directly to set the state of other elements. Try the following
Controller
export class AppComponent {
newTitle: string;
}
Template
<input [(ngModel)]="newTitle">
<button [disabled]="newTitle">Click me</button>
Working example: Stackblitz
I think you're going for an angular JS approach, where we can define global variables in HTML and set values. Why not just write the condition on the disabled attribute as shown below
<input [(ngModel)] = "newTitle"
#newBlogTitle_l = "ngModel" />
<div *ngIf = "newBlogTitle_l.value" >
test
</div>
<button (click) = "onSave()" [disabled] = "newBlogTitle_l.value" > Save </button>
https://stackblitz.com/edit/angular-vgxn4t?file=src/app/app.component.html

Angular input component duplicate ids

I'm creating a component for a toggle button. The styling is dependant on the input label, so I need to use id and for.
If I have multiple of these components on the page it will clearly mess up... What's the best way to deal with this? Random generate id through a function?
<div class="toggle-group checkbox-group-inline btn btn-link">
<input class="tgl tgl-light" id="toggle" type="checkbox" [checked]="value" (change)="stateChange($event.target.checked)">
<label class="tgl-btn" for="toggle"></label>
</div>
Use index on for:
<div *ngFor="let item of items; let i = index" class="toggle-group checkbox-group-inline btn btn-link">
<input class="tgl tgl-light" id="toggle_{{i}}" type="checkbox" [checked]="value" (change)="stateChange($event.target.checked)">
<label class="tgl-btn" for="toggle_{{i}}"></label>
</div>
This looks like a valid use case for a dynamic ids, as the correct way to associate labels with inputs is through ids.
To do so, add a randNum property to your class that contains a random number created on instantiation. Then, you can use this random number throughout your template to create unique identifiers like so:
Random Number (class property)
randNum = Math.floor(Math.random() * 1000000000);
Template
<div class="toggle-group checkbox-group-inline btn btn-link">
<input class="tgl tgl-light" [id]="'toggle' + randNum" type="checkbox" [checked]="value" (change)="stateChange($event.target.checked)">
<label class="tgl-btn" [for]="'toggle' + randNum"></label>
</div>
Now, if you have this component 10 times on the same page, those 10 instantiations of your component will have unique values of randNum, producing the desired effect.
Edit:
If you'd rather stay away from random numbers, you can ensure uniqueness by defining a static property as the basis for randNum like so:
static num = CustomComponent.num || 0;
randNum = ++CustomComponent.num;
In the code above, each component is guaranteed to have a unique randNum, as it is incremented for every instantiation.

ngModel cannot detect array changes correctly

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

Changing value of an input date picker with button angular

I have have an input field which is attached an Angular UI datepicker.
Below that, I added 2 buttons to change date, "day by day".
Here is the input part (just after in my page):
<p class="input-group">
<span class="input-group-addon hidden-xs">From</span>
<input type="text" class="form-control text-center" datepicker-popup="{{format}}" ng-model="parent.dtFrom" ng-change="refresh(0)" show-button-bar="false" is-open="opened1" max-date="maxDate" datepicker-options="dateOptions" ng-required="true" close-text="Close" readonly />
<span class="input-group-btn">
<button type="button" class="btn btn-default" ng-click="open($event, 'opened1')"><i class="glyphicon glyphicon-calendar"></i></button>
</span>
</p>
And here are my button to change it:
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 btn-group" role="group" >
<div class="text-center" >
<button type="button" class="btn btn-default btn-outline glyphicon glyphicon-chevron-left text-center previousdate" ng-click="addDayFrom(-1)"></button>
<button type="button" class="btn btn-default btn-outline glyphicon glyphicon-home text-center todaydate" ng-click="DateFromToday()"></button>
<button type="button" class="btn btn-default btn-outline glyphicon glyphicon-chevron-right text-center nextdate" ng-click="addDayFrom(1)" ng-show="parent.dtFrom < todate" ></button>
</div>
</div>
Here is how I init my dtFrom at the begin of my controller:
$scope.parent={
dtFrom : new Date(),
dtTo : new Date()
};
And this is my function for change dtFrom:
$scope.addDayFrom=function(day){
$scope.parent.dtFrom.setDate($scope.parent.dtFrom.getDate() + parseInt(day));
console.log($scope.parent.dtFrom);
};
It change value in $scope.parent.dtFrom but model isn't updated into input.
Example: Today we are 12/05/2015.
If I click one time on previous button, $scope.parent.dtFrom will be 11/05/2015.
And that's what is displayed all but the input.
I suppose that's a scope issue, but can't figure out where it is.
Have you any clue to fix this ?
Edit: Picture here : http://hpics.li/4e0fab6
Change your code from:
$scope.addDayFrom=function(day){
$scope.parent.dtFrom.setDate($scope.parent.dtFrom.getDate() + parseInt(day));
console.log($scope.parent.dtFrom);
};
To this:
$scope.addDayFrom=function(day){
$scope.parent.dtFrom = ($scope.parent.dtFrom.getDate() + parseInt(day));
console.log($scope.parent.dtFrom);
};
Looking at the Angular UI datepicker documentation, you can see that you don't need to access parent. I thought you were trying to access the parent scope, but actually you were accessing the parent attribute that you have set here:
$scope.parent={
dtFrom : new Date(),
dtTo : new Date()
};
Angular UI datepicker is watching by reference, as we can see in their source code, what makes perfect sense, since that's the most efficient watching strategy. To understand more about that, read Scope $watch Depths on Angular core documentation:
Scope $watch Depths
Dirty checking can be done with three strategies: By reference, by
collection contents, and by value. The strategies differ in the kinds
of changes they detect, and in their performance characteristics.
Watching by reference (scope.$watch (watchExpression, listener)) detects a change when the whole value returned by the
watch expression switches to a new value. If the value is an array or
an object, changes inside it are not detected. This is the most
efficient strategy.
Watching collection contents (scope.$watchCollection (watchExpression, listener)) detects changes that occur inside an
array or an object: When items are added, removed, or reordered. The
detection is shallow - it does not reach into nested collections.
Watching collection contents is more expensive than watching by
reference, because copies of the collection contents need to be
maintained. However, the strategy attempts to minimize the amount of
copying required.
Watching by value (scope.$watch (watchExpression, listener, true)) detects any change in an arbitrarily nested data
structure. It is the most powerful change detection strategy, but also
the most expensive. A full traversal of the nested data structure is
needed on each digest, and a full copy of it needs to be held in
memory.
That's why it wasn't detecting $scope.parent.dtFrom.setDate(newDate) usage, and the model wasn't being updated. So, just change that piece of code to $scope.parent.dtFrom = newDate, and it should work (as it has already worked with your global variable approach).
It's a bit of hack but, it works ... so I share it with you
When i start my app i create a local date variable that i change when i click on my button. when it's done, i do that :
$scope.parent.dtFrom = new Date(localDateVariable);
Not the best solution, but a solution.

Categories