Angular : set reactive form with dynamic looping inputs - javascript

I've to set reactive form validation for a form where the inputs are made with data looping :
my form builder would look like this :
constructor(private formBuilder: FormBuilder) {
this.userForm = this.formBuilder.group({
'inputOne': ['', [Validators.required]],
'inputOne': ['', [Validators.required]],
...
'inputN': ['', [Validators.required]]
});
}
and my template would look like this :
<form [formGroup]="userForm">
<div class="form-group" *ngFor="let item of items; let i=index;">
<label for="lastName">{{item.name}}</label>
<input class="form-control" name="lastName" id="lastName" type="text" [formControlName]="item.name">
</div>
</form>
where items are loaded dynamically from my my backend
How to populate controls dynamically in Angular reactive forms?

sounds like you want a form array here, not a group...
constructor(private formBuilder: FormBuilder) {
// build an empty form array
this.userForm = this.formBuilder.array([]);
}
// call this whenever you need to add an item to your array
private addItem(item) {
// build your control with whatever validators / value
const fc = this.formBuilder.control(i.lastName, [Validators.required]);
this.userForm.push(fc); // push the control to the form
}
// call this function whenever you need to reset your array
private resetFormArray(items) {
this.userForm.clear(); // clear the array
items.forEach(i => this.addItem(i)); // add the items
}
<form [formGroup]="userForm">
<div class="form-group" *ngFor="let item of items; let i=index;">
<label for="lastName">{{item.name}}</label>
<input class="form-control" name="lastName" id="lastName" type="text" [formControlName]="i">
</div>
</form>
notice you're using the index for the form control name here

Related

How to use subscribe in Angular with an array of inputs and how to sum their values?

I have a reactive angular form. The form is a simple scoreboard. I would like to be able to add the rounds up together to make up a score. So when a round is finished and the score for that round is entered, it will sum up all the total rounds into that players score.
This is what I have so far:
form.component.html
<section [formGroup]="board">
<table class="table table-bordered" formArrayName="scoreboard">
<tr>
<th colspan="2">Scoreboard:</th>
<th width="150px">
<button type="button" (click)="addPlayer()" class="btn btn-primary">
Add Additional Players
</button>
</th>
</tr>
<tr
*ngFor="let quantity of scoreboard().controls; let i = index"
[formGroupName]="i"
>
<td>
Name :
<input type="text" formControlName="name" class="form-control" />
</td>
<td>
Round1
<input type="text" formControlName="round1" />
Round2
<input type="text" formControlName="round2" />
</td>
<td>
Score:
<input type="text" formControlName="score" class="form-control" />
</td>
</tr>
</table>
{{ this.board.value | json }}
</section>
form.component.ts
import { Component } from '#angular/core';
import { FormBuilder, FormGroup, FormArray } from '#angular/forms';
#Component({
selector: 'app-basic-form',
templateUrl: './basic-form.component.html',
styleUrls: ['./basic-form.component.css']
})
export class BasicFormComponent {
board: any;
constructor (private fb: FormBuilder) {
this.board = this.fb.group({
scoreboard: this.fb.array([this.game(), this.game()])
});
}
scoreboard() : FormArray {
return this.board.get("scoreboard") as FormArray
}
game(): FormGroup {
return this.fb.group({
name: '',
score: '',
round1: [''],
round2: ['']
})
}
addPlayer() {
this.scoreboard().push(this.game());
}
onSubmitForm () {
console.log(this.board.value);
}
}
Really just starting to learn Angular and wanted to try something on my own. If you could be detailed or show me somewhere I can learn additional information about the solution that would be great!
You can listen to your group's controls value changes, and update the score accordingly. Your form array is made of list of form groups (the return value of game function), these groups have controls which holds the values for their respective index, so when you create a form group, you can listen to round1 and round2 changes, and update score accordingly. For example, what I did below is using combineLatest function to merge both value changes observables (round1 + round2) and then update the score accordingly. So now every new form group pushed to your form array will have its own value change listener and will update the score.
game(): FormGroup {
const gameGroup = this.fb.group({
name: '',
score: '',
round1: [''],
round2: ['']
});
combineLatest(gameGroup.get('round1').valueChanges,gameGroup.get('round2').valueChanges)
.subscribe(([r1,r2]) => gameGroup.get('score').setValue(Number(r1) + Number(r2)));
return gameGroup;
}

Save form data in modal class that contains an array [Angular]

How can I save form data that contains an array.so I faced an issue which is the adresses is null whereas other data like city region aren't null. someone here have an idea ?!
Iuser.interface
export interface UserData {
id: string;
email: string;
adresses: string[];
}
Component.ts
onSubmit() {
const {adresse1, ...rest} = this.form;
const userData: UserData = {...rest, adresses: [adresse1]};
this.userservice.updateProfile(userData, this.currentUser.id).subscribe(
data => {
console.log(data);
this.isSuccessful = true;
},
err => {
this.errorMessage = err.error.message;
}
);
}
component.html
<div class="form-group">
<label>Address</label>
<input type="text" class="form-control" placeholder=""
name="adresse"
[(ngModel)]="form.adresse"
required
#adresse="ngModel">
</div> <!-- form-group end.// -->
<div class="form-row">
<div class="form-group col-md-6">
<label>Region</label>
<input type="text" class="form-control"
name="region"
[(ngModel)]="form.region"
required
#region="ngModel">
</div> <!-- form-group end.// -->
You are reading adresse1 from this.form inside onSubmit() method which is not a part of your form object,your address is actually stored inadresse property.Please update your code within submit block as
const {adresse, ...rest} = this.form;
const userData: UserData = {...rest, adresses: [adresse]};

Angular 6 Reactive Forms : How to set focus on first invalid input

Under my Angular 6 app , i'm using Reactive Forms .
My purpose is when submitting , i want to set focus on first invalid input when error.
My form looks like this :
<form [formGroup]="addItemfForm " (ngSubmit)="onSubmitForm()">
<div class="form-inline form-group">
<label class="col-md-2 justify-content-start">
Libellé du pef
<span class="startRequired mr-1"> *</span>
</label>
<input type="text" maxlength="100" formControlName="libellePef" class="col-md-6 form-control" placeholder="saisie obligatoire" [ngClass]="{ 'is-invalid': submitted && formFiels.libellePef.errors }" />
<div *ngIf="submitted && formFiels.libellePef.errors" class="col invalid-feedback">
<div class="col text-left" *ngIf="formFiels.libellePef.errors.required">Libellé du pef est requis.</div>
</div>
</div>
<div class="form-inline form-group">
<label class="col-md-2 justify-content-start">
Code Basicat
<span class="startRequired mr-1"> *</span>
</label>
<input type="text" maxlength="100" formControlName="codeBasicat" class="col-md-3 form-control" placeholder="saisie obligatoire" [ngClass]="{ 'is-invalid': submitted && formFiels.codeBasicat.errors }" />
<div *ngIf="submitted && formFiels.codeBasicat.errors" class="col invalid-feedback">
<div class="text-left" *ngIf="formFiels.codeBasicat.errors.required">Code Basicat est requis.</div>
</div>
</div>
<div class="form-inline form-group">
<label class="col-md-2 justify-content-start">
Nom de l'application
<span class="startRequired mr-1"> *</span>
</label>
<input type="text" maxlength="100" formControlName="nomApplication" class="col-md-6 form-control" placeholder="saisie obligatoire" [ngClass]="{ 'is-invalid': submitted && formFiels.nomApplication.errors }" />
<div *ngIf="submitted && formFiels.nomApplication.errors" class="col invalid-feedback">
<div class="text-left" *ngIf="formFiels.nomApplication.errors.required">Nom de l'application est requis.
</div>
</div>
</div>
</form>
Under my TS file , my form config looks like this :
this.addItemfForm = this.formBuilder.group({
libellePef: ['', Validators.required],
codeBasicat: ['', Validators.required ],
nomApplication: ['', Validators.required ],
urlCible: [''],
modeTransfert: [''],
});
I've tried the autofocus directive but that didn't work
Suggestions?
Use below code in your submit.
for (const key of Object.keys(this.addressForm.controls)) {
if (this.addressForm.controls[key].invalid) {
const invalidControl = this.el.nativeElement.querySelector('[formcontrolname="' + key + '"]');
invalidControl.focus();
break;
}
}
this.addressForm will be your FormGroup.
We don't even need directive here.
My Answer is inspired from yurzui's answer here. I'm using the logic from his answer to get the nativeElement of a particular FormControl by using it's FormControl.
This is the logic that does that:
const originFormControlNameNgOnChanges = FormControlName.prototype.ngOnChanges;
FormControlName.prototype.ngOnChanges = function () {
const result = originFormControlNameNgOnChanges.apply(this, arguments);
this.control.nativeElement = this.valueAccessor._elementRef.nativeElement;
return result;
};
Now, the form's errors field would be null even though it's fields are invalid. So to get the exact first field that's invalid, we'll have to loop through all the fields and check for validity for each of them. I can write this logic in the onSubmitForm() method. Something like this:
onSubmitForm() {
const fieldsToCheck = [
'codeBasicat',
'libellePef',
'nomApplication'
];
for (let i = 0; i < fieldsToCheck.length; i++) {
const fieldName = fieldsToCheck[i];
if (this.addItemfForm.get(fieldName).invalid) {
( < any > this.addItemfForm.get(fieldName)).nativeElement.focus();
break;
}
}
}
I've deliberately used for instead of Array.forEach as I wanted to break from the loop.
Hopefully this should do the trick for you.
Here's a Working Sample StackBlitz for your ref.
I did that using directives. So My form would look like this:
<form [formGroup]="userForm" (submit)="saveData()" appFocus >
...
</form>
and the code for the directive itself:
import { Directive, HostListener, Input, ElementRef } from '#angular/core';
import { NgForm } from '#angular/forms';
#Directive({
selector: '[appFocus]'
})
export class FocusDirective {
constructor(private el: ElementRef) { }
#Input() formGroup: NgForm;
#HostListener('submit', ['$event'])
public onSubmit(event): void {
if ('INVALID' === this.formGroup.status) {
event.preventDefault();
const formGroupInvalid = this.el.nativeElement.querySelectorAll('.ng-invalid');
(<HTMLInputElement>formGroupInvalid[0]).focus();
}
}
}
However this solution is incomplete as there is a lot of corner cases that have to be considered. For example what if the first element is radio button group. Dispatching focus event will automatically mark the filed. Second not every element to which angular ads ng-invalid will be an input.
We can set focus on first invalid input simply by just write this code in the submit() of the form.
if(this.form.invalid)
{
// Got focus to the error field
let invalidFields = [].slice.call(document.getElementsByClassName('ng-invalid'));
invalidFields[1].focus();
}
Try this:
import { Directive, HostListener, ElementRef} from '#angular/core';
#Directive({
selector: '[focusFirstInvalidField]'
})
export class FocusFirstInvalidFieldDirective {
constructor(private el: ElementRef) { }
#HostListener('submit')
onFormSubmit() {
const invalidElements = this.el.nativeElement.querySelectorAll('.ng-invalid');
if (invalidElements.length > 0) {
console.log(invalidElements[0]);
invalidElements[0].focus();
}
}
}
Remember to debug, see if element 0 is not your own form as it happened to me, so see right what field it is reporting as the first and put the position right.
This option does not work for me, but I managed to fix it by changing the code as follows:
#Directive({
selector: '[appErrorFocusin]'
})
export class ErrorFocusinDirective {
selectorToFocus : String = 'textArea,mat-select,select,input,button';
constructor(private el: ElementRef,
#Inject(DOCUMENT) private document: Document) { }
#Input() formGroup: NgForm;
#HostListener('submit', ['$event'])
public onSubmit(event): void {
if ('INVALID' === this.formGroup.status) {
event.preventDefault();
const formGroupInvalid = this.el.nativeElement.querySelectorAll('.ng- invalid,:not(.mat-badge-hidden).mat-badge');
let elementToOffset = this.getElementToOffset(formGroupInvalid[0]);
this.document.documentElement.scrollTop = elementToOffset.offsetTop;
this.setFocusOnError(elementToOffset);
}
}
getElementToOffset(element : any){
let defaultElement = element;
while (!(element.parentElement instanceof HTMLFormElement)){
if (element.parentElement){
element = element.parentElement;
}else{
return defaultElement;
}
}
return element;
}
setFocusOnError(elementToOffset : any){
let listaElementos = elementToOffset.querySelectorAll(this.selectorToFocus);
if (listaElementos.length>0){
listaElementos[0].focus();
}
}
}
A continuation to #Avinash's answer, instead of making it
querySelector('[formcontrolname="' + key + '"]');
We can add id's in the HTML to the input and simply make that as this:
querySelector('#'+ key +'.ng-invalid');

Angular 4 : Check if radio button selected in a reactive form

I have a reactive form with 4 inputs type radio and 4 inputs type text.
I want to display dynamically inputs text following the radio buttons
selected. For example, if button radio 1 selected, i want to display
its corresponding input text 1.
I encounter 2 problems :
The first radio button is not selected by default (even with attribute checked) and all the radio buttons can be selected at the same time.
All the inputs text are displayed, i don't know with a *ngIf how to
check if the radio button corresponding to the input is selected.
component.html :
<section class="CreateItem" *ngIf="formGroupItemSelection">
<form (ngSubmit)="addItem()" [formGroup]="formGroupItemSelection">
<input formControlName="refNumber" type="radio" value="refNumber" checked> ref number
<br>
<input formControlName="partNumber" type="radio" value="partNumber"> part number
<br>
<input formControlName="itemName" type="radio" value="itemName"> item name
<br>
<input formControlName="manufacturerName" type="radio" value="manufacturerName">manufacturer name
<br>
<div *ngIf="formGroupItemSelection.controls.refNumber.valid">
<input list="refNumbers" formControlName="refNumberSelected" type="text" name="refNumberSelected">
<datalist id="refNumbers">
<option *ngFor="let ref of listOfItems">{{ref.refNumber.input}}</option>
</datalist>
</div>
<div *ngIf="formGroupItemSelection.controls.partNumber.valid">
<input list="partNumbers" formControlName="partNumberSelected" type="text" name="partNumberSelected">
<datalist id="partNumbers">
<option *ngFor="let ref of listOfItems">{{ref.partNumber.input}}</option>
</datalist>
</div>
<div *ngIf="formGroupItemSelection.controls.itemName.valid">
<input list="itemsName" formControlName="itemNameSelected" type="text" name="itemNameSelected">
<datalist id="itemsName">
<option *ngFor="let ref of listOfItems">{{ref.itemDesignation.input}}</option>
</datalist>
</div>
<div *ngIf="formGroupItemSelection.controls.manufacturerName.valid">
<input list="manufacturersName" formControlName="manufacturerNameSelected" type="text" name="manufacturerNameSelected">
<datalist id="manufacturersName">
<option *ngFor="let ref of listOfItems">{{ref.manufacturerName.input}}</option>
</datalist>
</div>
<button type="submit [disabled]="!formGroupItemSelection.valid">Valider</button>
</form>
</section>
component.ts :
import { Component, OnInit } from '#angular/core';
import {FormControl, FormGroup, FormBuilder, Validators} from '#angular/forms'
import { ManagementArbologistiqueService } from '../../management-arbologistique.service';
import { ActivatedRoute, Params } from '#angular/router';
import { matchValuesRefNumber, matchValuesPartNumber, matchValuesItemName, matchValuesManufacturerName } from '../Validators/validators';
#Component({
selector: 'app-item-selection',
templateUrl: './item-selection.component.html',
styleUrls: ['./item-selection.component.css']
})
export class ItemSelectionComponent implements OnInit {
formGroupItemSelection:FormGroup;
listOfItems = [];
constructor(public fb:FormBuilder,private managementArbo: ManagementArbologistiqueService, private route: ActivatedRoute) { }
ngOnInit() {
this.getListBdd();
}
initializeForm() {
this.formGroupItemSelection = this.fb.group({
refNumber : '',
partNumber: '',
itemName: '',
manufacturerName: '',
refNumberSelected:[
null,
Validators.compose([Validators.required, matchValuesRefNumber(this.listOfItems)])
],
partNumberSelected:[
null,
Validators.compose([Validators.required, matchValuesPartNumber(this.listOfItems)])
],
itemNameSelected: [
null,
Validators.compose([Validators.required, matchValuesItemName(this.listOfItems)])
],
manufacturerNameSelected:[
null,
Validators.compose([Validators.required, matchValuesManufacturerName(this.listOfItems)])
]
})
}
getListBdd() {
this.route.params.subscribe((params: Params) => {
let subroute = "getRefNumber";
this.managementArbo.getProducts(subroute)
.subscribe(
res => {
this.listOfItems = res; console.log('bdd:' + res);
this.initializeForm();
},
err => console.log(err),
() => console.log('getProducts done'));
});
}
addItem() {
}
1.-NOT use "checked". just when you create the form give the correct value. And the correct value is not "true"
this.formGroupItemSelection = this.fb.group({
refNumber : ['refNumber'], //NOT is true or false
partNumber: [],
itemName: []
....
})
2.-When we make a *ngIf into a reactive form use myform.get('mycontrol'), e.g.
<div *ngIf="formGroupItemSelection.get('partNumber').valid">...</div>
I solved my problem !
Firstly, I found important information in the angular doc to answer my first question :
"To use radio buttons in a template-driven form, you'll want to ensure that radio buttons in the same group have the same name attribute. Radio buttons with different name attributes do not affect each other.".
Then, in the case of a reactive form :
"When using radio buttons in a reactive form, radio buttons in the same group should have the same formControlName. You can also add a name attribute, but it's optional."
So, i gave the same formControlName at each radio button and the mention "checked" works now.
Otherwise, to answer my second question, I identified each radio buttons following the "value" property of input, and checked if radio buttons selected :
<div *ngIf="formGroupItemSelection.controls.radioBoutton.value==='refNumber'">
Hope it can help !
initializeForm() {
this.formGroupItemSelection = this.fb.group({
refNumber : new FormControl(true),
partNumber: new FormControl(false),
itemName: new FormControl(false),
manufacturerName: '',
refNumberSelected:[
null,
Validators.compose([Validators.required, matchValuesRefNumber(this.listOfItems)])
],
partNumberSelected:[
null,
Validators.compose([Validators.required, matchValuesPartNumber(this.listOfItems)])
],
itemNameSelected: [
null,
Validators.compose([Validators.required, matchValuesItemName(this.listOfItems)])
],
manufacturerNameSelected:[
null,
Validators.compose([Validators.required, matchValuesManufacturerName(this.listOfItems)])
]
})
//This will set the radio button checked no need to add checked attribute you can set cheked dynamically
this.formGroupItemSelection.get('refNumber ').setValue(true);
this.formGroupItemSelection.get('partNumber').setValue(true);
this.formGroupItemSelection.get('itemName').setValue(true)
}

Angular 4 array of ngModels - how to handle the name attribute

Here's my class:
interface IHobby {
name: string;
type: string;
}
class User {
constructor(public name: string, public hobbies: IHobby[]) { }
}
Now I'm trying to do a template form in Angular 4. I've noticed that in order to display the hobbies, I need to iterate through them. So, here's my HTML:
<div *ngFor="let h of user.hobbies; let i = index">
#{{i}}<br />
<input [(ngModel)]="h.name" name="hobby_name[{{i}}]" /> <br /><br />
<input [(ngModel)]="h.type" name="type[{{i}}]" />
<br /><br />
<button class="btn btn-warn" (click)="remove(i)">Remove</button>
<br /><br />
</div>
While this works, I'm not quite sure if:
I'm doing this the correct Angular 4 way - should I use ngModel for each item in the array?
Is it OK to define names as name="hobby_name[{{i}}]"? Because name="hobby_name_{{i}}" works as well. What is the correct, HTML5 and Angular 4 way?
Here's a working sample: https://plnkr.co/edit/chOsfhBThD3dA9FFqNPX?p=preview
You don't really need to use name attribute since you're binding the inputs directly to the model. If you want to see instant changes in your model, you can keep using [(ngModel)], but if you want to update your model on form submit only, then you should not use ngModel. I would do a reactive form like this:
<div [formGroup]="form">
<div formArrayName="hobbiesFormArray">
<div *ngFor="let h of user.hobbies; let i = index" [formGroupName]="i">
#{{i}}<br />
<input formControlName="name" /> <br /><br />
<input formControlName="type" />
<br /><br />
<button class="btn btn-warn" (click)="remove(i)">Remove</button>
<br /><br />
</div>
</div>
</div>
And then in your component I would do something like:
form: FormGroup;
constructor(fb: FormBuilder) {
this.form = this.fb.group({
name: '',
hobbies: this.fb.array([])
});
}
ngOnChanges() {
this.form.reset({
name: this.user.name
});
this.setHobbies(this.user.hobbies);
}
get hobbiesFormArray(): FormArray {
return this.form.get('hobbiesFormArray') as FormArray;
};
setHobbies(hobbies: IHobby[]) {
const hobbiesFormArray = this.fb.array(hobbies.map(hobby => this.fb.group(hobby)));
this.form.setControl('hobbiesFormArray', this.fb.array());
}
Then in your onSubmit method I would convert the form model back to the user object:
const formModel = this.form.value;
const newHobbies: IHobby[] = formModel.hobbiesFormArray
.map(() => (hobby: IHobby) => Object.assign({}, hobby))
const newUser: User = {
name: formModel.name as string,
hobbies: newHobbies
};
And save newUser object with whatever means you have.

Categories