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

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]};

Related

Angular : set reactive form with dynamic looping inputs

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

How to convert ngbDatepicker date format to string onSubmit() of form?

I am trying to convert ngbDatepicker date format to string before I call my backend API to save the data because it allows date in string format only. I tried converting it by using submittedData.MaturityDate.toString(); and submittedData.RemitDate.toString(); but it's not working.
I currently have this form in my component.html:
<div class="form-group row">
<div class="col-sm-6">
<label for="MaturityDate">Maturity Date</label>
<div class="input-group">
<input formControlName="MaturityDate" class="form-control" placeholder="yyyy-mm-dd" name="dp1"
ngbDatepicker #dp1="ngbDatepicker" [ngClass]="{ 'is-invalid': submitted && f.MaturityDate.errors }">
<div class="input-group-append">
<button class="btn btn-outline-secondary calendar" (click)="dp1.toggle()" type="button"></button>
</div>
<div *ngIf="submitted && !!f.MaturityDate.errors" class="invalid-feedback">
<div *ngIf="!!f.MaturityDate.errors.required">Maturity date is required</div>
</div>
</div>
</div>
<div class="col-sm-6">
<label for="RemitDate">Remittance Date</label>
<div class="input-group">
<input formControlName="RemitDate" class="form-control" placeholder="yyyy-mm-dd" name="dp2"
ngbDatepicker #dp2="ngbDatepicker" [ngClass]="{ 'is-invalid': submitted && f.RemitDate.errors }">
<div class="input-group-append">
<button class="btn btn-outline-secondary calendar" (click)="dp2.toggle()" type="button"></button>
</div>
<div *ngIf="submitted && !!f.RemitDate.errors" class="invalid-feedback">
<div *ngIf="!!f.RemitDate.errors.required">Remittance date is required</div>
</div>
</div>
</div>
</div>
Upon submission of this form, I'll trigger the following in my component.ts:
onSubmit() {
this.submitted = true;
// stop here if form is invalid
if (this.accountPayableForm.invalid) {
return;
}
let submittedData = this.accountPayableState;
submittedData = this.accountPayableForm.value;
// **TRYING TO CONVERT INPUT TO STRING BUT NOT WORKING
submittedData.MaturityDate.toString();
submittedData.RemitDate.toString();
submittedData.OwningAccountKey = this.currentAccount[0].accountId;
console.log(submittedData);
this.loading = true;
this.accountPayableService
.submitAccountPayable(submittedData)
.pipe(first())
.subscribe(
(data: AccountPayable) => {
this.alertService.success(
'Success! Account payable created with reference ID: ' + data.accountPayableID,
true
);
this.loading = false;
this.submitted = false;
this.accountPayableForm.reset();
document.querySelector('.my-scroll').scrollTo(0, 0);
},
error => {
document.querySelector('.my-scroll').scrollTo(0, 0);
this.alertService.error('Submission failed, please try again');
this.loading = false;
}
);
}
This is the submitAccountPayable() function in my service.ts:
submitAccountPayable(accountPayable: AccountPayableState) {
return this.http.post(this.issueAccountPayableUrl, accountPayable);
}
My MaturityDate and RemitDate attributes in my AccountPayableState model are in string format so I thought they'd be automatically mapped to string format when I call this function. Here's my AccountPayableState model:
export interface AccountPayableState {
OwningAccountKey: string;
BuyerAccount: string;
BuyerCode: number;
SupplierAccount: string;
SupplierCode: number;
PaymentReference: string;
MaturityDate: string;
RemitDate: string;
PaymentAmount: number;
PaymentCurrency: string;
PurchaseOrderNumber: string;
}
This is my payload in the console for a sample submission, apparently they are not in string type even though I have specifically use .toString(). Please see MaturityDate and RemitDate below:
BuyerAccount: "asdasdasd"
BuyerCode: "123123"
MaturityDate: {year: 2019, month: 10, day: 18}
OwningAccountKey: "e273b89f-c828-41ad-873d-a13bbe63d7c5"
PaymentAmount: "123123123"
PaymentCurrency: "USD"
PaymentReference: "asdasda"
PurchaseOrderNumber: "asdasda"
RemitDate: {year: 2019, month: 10, day: 10}
SupplierAccount: "asdasdasda"
SupplierCode: "123123123"
Any advice?
You could just transform your date with a function;
private dateToString = (date) => `${date.year}-${date.month}-${date.day}`;
onSubmit() {
this.submitted = true;
// stop here if form is invalid
if (this.accountPayableForm.invalid) {
return;
}
let submittedData = this.accountPayableState;
submittedData = this.accountPayableForm.value;
submittedData.MaturityDate = this.dateToString(submittedData.MaturityDate);
submittedData.RemitDate= this.dateToString(submittedData.RemitDate);
submittedData.OwningAccountKey = this.currentAccount[0].accountId;
...
The object returned from ngbDatepicker is in its default format, so you would need to either manually convert in your onSubmit or in the model, or look at using a custom date adapter https://ng-bootstrap.github.io/#/components/datepicker/examples#adapter

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 Material MatChipList - how to use it on a Dynamic FormArray?

StackBlitz
Here is my FormArray (variants):
this.productGroup = this.fb.group({
name: '',
variants: this.fb.array([
this.fb.group({
type: '',
options: ''
})
])
})
I'm using MatChips to store a string array. This array needs to be passed to options:
<div formArrayName="variants" *ngFor="let item of productGroup.controls['variants'].controls; let i = index;">
<div [formGroupName]="i">
<div class="row">
<mat-form-field class="col-12">
<input formControlName="type">
</mat-form-field>
</div>
<div class="row">
<mat-form-field class="col-12">
<mat-chip-list #chipList>
<mat-chip *ngFor="let opt of typesOptions" [selectable]="true"
[removable]="true" (removed)="removeOpt(opt)">
{{opt}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input placeholder="Conjunto de opções deste Tipo"
formControlName="options"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addOpt($event)">
</mat-chip-list>
</mat-form-field>
</div>
</div>
<div class="row">
Add Variants
Remove Variants
</div>
</div>
Here is the methods:
// Dynamic Methods
addItem(): void {
this.variantsArray = this.productGroup.get('variants') as FormArray;
this.variantsArray.push(this.fb.group({
type: '',
options: ''
}));
}
removeItem(index: number) {
this.variantsArray.removeAt(index);
}
// MatChip Methods
addOpt(item: number, event: MatChipInputEvent): void {
const input = event.input;
const value = event.value;
// Add our fruit
if ((value || '').trim()) {
this.typesOptions.push(value.trim());
}
// Reset the input value
if (input) {
input.value = '';
}
}
removeOpt(opt: string): void {
const index = this.typesOptions.indexOf(opt);
if (index >= 0) {
this.typesOptions.splice(index, 1);
}
I'm successfully adding dynamic fields to my variants formArray. However MatChipList is the same for every dynamic field. I need to make MatChipList dynamic as well. Is there a way to achieve this? Like changing <mat-chip-list #chipList+i> or something like that.
EDIT: StackBlitz
I'm not sure the dom reference variable #chiplist is the issue. It looks like the matChipList is backed by the typesOptions array, but you only have one array. So every time you add a matChipList component it is still being backed by the same array as all the others. You need to have an array of typesOptions, an array of arrays. Then when you addItem, you also push a new sub array to typesOptions (and similarly remove one for removeItem).
I haven't coded this up, just a suggestion from looking at the code.
Edit - coded up a solution based on James's stackblitz.
https://stackblitz.com/edit/angular-3od6rd-jkidxf
Note I haven't looked in detail at how the delete variant holds together, ideally I'd probably like to use a key/value pair to track the variant options using the dom input element id as the key (which is in the MatChipInputEvent), instead of relying on the outer loop index.
Some code from the stackblitz:
export class ChipsOverviewExample {
productGroup: FormGroup;
variantsArray: FormArray;
typesOptionsArray: string[][] = [];
readonly separatorKeysCodes: number[] = [ENTER, COMMA];
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.productGroup = this.fb.group({
name: '',
variants: this.fb.array([
this.fb.group({
type: '',
options: ''
})
]),
});
this.typesOptionsArray.push([]);
}
saveProduct(form: FormGroup) {
console.log(form);
}
// Add new item to FormArray
addItem(): void {
this.variantsArray = this.productGroup.get('variants') as FormArray;
this.variantsArray.push(this.fb.group({
type: '',
options: ''
}));
this.typesOptionsArray.push([]);
}
removeItem(index: number) {
this.variantsArray.removeAt(index);
}
addOpt(event: MatChipInputEvent, index: number): void {
const input = event.input;
const value = event.value;
// Add our fruit
if ((value || '').trim()) {
this.typesOptionsArray[index].push(value.trim());
}
// Reset the input value
if (input) {
input.value = '';
}
}
removeOpt(opt: string, index: number): void {
const optIndex = this.typesOptionsArray[index].indexOf(opt);
if (optIndex >= 0) {
this.typesOptionsArray[index].splice(optIndex, 1);
}
}
}
try to make the formGroup as a new component and input formGroup to it(not formGroupName).
<div formArrayName="variants" *ngFor="let item of productGroup.controls['variants'].controls; let i = index;">
<variant [varientGroup]="item"><varient>
<div class="row">
Add Variants
Remove Variants
</div>
</div>
varient component.html
<div [formGroup]="varientGroup">
<div class="row">
<mat-form-field class="col-12">
<input formControlName="type">
</mat-form-field>
</div>
<div class="row">
<mat-form-field class="col-12">
<mat-chip-list #chipList>
<mat-chip *ngFor="let opt of typesOptions" [selectable]="true"
[removable]="true" (removed)="removeOpt(opt)">
{{opt}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input placeholder="Conjunto de opções deste Tipo"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addOpt($event)">
</mat-chip-list>
</mat-form-field>
</div>
</div>
in varient.component.ts
#Input()varientGroup: FormGroup

How to add data to collection in an object Angular 5+?

I am trying to add data to my collection inside an object from my TypeScript code. I am successfully getting the name and type from the view in the html binding So I was wondering how I can edit the this.newList.socialData model via code before adding the new list to my database.
Html:
<mat-form-field>
<input matInput placeholder="List Name" [(ngModel)]="newList.name" name="name" type="text" class="form-control rounded-0" required>
</mat-form-field>
<mat-form-field>
<input matInput placeholder="List Type" [(ngModel)]="newList.type" name="type" type="text" class="form-control rounded-0" required>
</mat-form-field>
<button (click)="addList()" type="button" class="btn btn-primary float-right">Create New</button>
Declaration:
newList: List = {} as List
TypeScript:
addList() {
let tt = {} as SocialData;
//tt.socialId = 'jj'
//this.newList = {} as List;
// I would like to add test data to socialData here
this.newList.socialData.push(tt);
this.listService.addList(this.newList)
.subscribe(
res => {
this.fetchLists();
},
err => console.log(err)
)
}
Model:
export class List {
name: String;
type: String;
inputValue: String;
socialData: [SocialData]
}
export class SocialData {
socialId: String
}
I guess to just want to add a new item to the socialData array.
You need to make 2 changes in your code:
1. Declaration
export class List {
name: String;
type: String;
inputValue: String;
// socialData: [SocialData]; // This is wrong
socialData: SocialData[]; // declares a type of array
constructor() {
this.socialData = []; // Initialize the array to empty
}
}
2. Creating the instance:
// let tt = {} as SocialData; // This will just cast an empty object into List
newList: List = new List(); // Will create an instance of List
Just make these changes and the code should work.

Categories