Problems with Angular Dynamic Reactive Form - html

I'm trying to create a dynamic reactive form. The user has the ability to choose between either a text (type = 1) input or img (type = 2) input. According to his choice, the right input is being added. - He can add as much input field as he wants.
I've never really used reactive forms before, hence this question.
The code below adds a control according to what module the user has chosen, but for instance adding a textarea only displays a textarea with [object Object] inside - clicking makes it disappear.
Additionally I haven't figured out yet how to submit the form's input. Logging form on submit returns the form, but without the textarea's input.
That's what I have so far:
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div *ngFor="let module of form.get('modules').controls; let i = index" class="position-relative" [style.padding-bottom.rem]="paddingValue" (mouseover)="showPaddingInput = true" formArrayName="modules">
<div class="padding-input position-absolute d-flex justify-content-center" *ngIf="showPaddingInput === true" [style.height.rem]="paddingValue">
<input type="text" class="align-self-center text-center padding-input-field" [value]="paddingValue" (input)="changePadding(padding.value)" #padding>
</div>
<div class="text" *ngIf="module.value.type === 1">
<textarea class="flow-text" placeholder="Some Text..." rows="3" [formControlName]="i"></textarea>
</div>
<div class="img-container" *ngIf="module.value.type === 2">
<div class="custom-file align-self-center">
<input type="file" id="i" class="custom-file-input" [formControlName]="i" (change)="handleFileInput($event.target.files)">
<label class="custom-file-label" for="i"></label>
</div>
</div>
</div>
<button class="btn btn-dark">Submit</button>
</form>
export class CreateCaseCmsComponent implements OnInit {
form: FormGroup;
constructor(private caseService: CasesService) { }
addModule(type) {
if (type === 1) {
const control = new FormControl({type: 1}, Validators.required);
(this.form.get('modules') as FormArray).push(control);
} else if (type === 2) {
const control = new FormControl({type: 2}, Validators.required);
(this.form.get('modules') as FormArray).push(control);
}
}
ngOnInit() {
this.form = new FormGroup({
modules: new FormArray([])
});
}
onSubmit() {
console.log(this.form);
}
}

the first argument to a form control is it's value, so you're setting the initial value as an object and that's why it's showing [object Object] in the text box... that's what you get if you call .toString() on an object, you need to instantiate them like this:
const control = new FormControl('', Validators.required);
or something like that... this affects how you're building your template, so you probably need something more like:
const group = new FormGroup({
type: new FormControl(1),
value: new FormControl('', Validators.required)
});
and add that group to your array and access it like:
<div class="text" *ngIf="module.get('type').value === 1" [formGroupName]="i">
<textarea class="flow-text" placeholder="Some Text..." rows="3" formControlName="value"></textarea>
</div>

Related

Getting error after upgrading from Angular 8 to 12 - "Property 'controls' does not exist on type 'AbstractControl' "

I had a fully functioning code in Angular 8. I decided to upgrade from 8 to Angular 12.
I have a dynamic reactive form. The form is of question answer format.
Based on who is login in the questions change. The answer is of Yes/No format. Based on who is login in and what answer option is chosen textbox dropdown and checkbox or all the there mentioned appreas.
Here is my code for the form
Component.ts
constructor(
private router: Router,
private datePipe: DatePipe,
private route: ActivatedRoute,
private formBuilder: FormBuilder) {
this.dynamicForm = this.formBuilder.group({
questions: new FormArray([])
});
this.CreateForm();
}
get f() { return this.dynamicForm.controls; }
get t() { return this.f.questions as FormArray; }
CreateForm() {
if (this.formdetail.questionList) {
// CREATE Questions
if (racialQues !== this.formdetail.questionList[i].quetext && this.formdetail.questionList[i].quetext !== auditQues) {
this.t.push(this.formBuilder.group({
quesIndex: [i + 1],
...
controlName: [this.formdetail.questionList[i].selectedAns, [Validators.required]]
}));
} else if (this.formdetail.questionList[i].quetext === auditQues) {
this.t.push(this.formBuilder.group({
quesIndex: [i + 1],
......
selectedValue: [this.formdetail.auditTrail.selectedValue, this.reasonValidator],
}));
} else if (this.formdetail.questionList[i].quetext === racialQues) {
this.t.push(this.formBuilder.group({
quesIndex: [i + 1],
......
selectedAuxAns: [this.formdetail.questionList[i].val, [this.auxAnsValidator]]
}));
}
}
}
Here is the html
<form [formGroup]="dynamicForm">
<!-- Question Start -->
<div *ngFor="let ticket of t.controls; let i = index">
<div [formGroup]="ticket" class="form-row">
<div class="input-group col-md-12" style="padding: .15em .5em">
<div class="input-group-prepend col-md-10" style="padding: 0; margin: 0;">
<label class="input-group-text w-15">{{i + 1}}</label>
<label class="input-group-text w-100" style="text-align: left; white-space: normal;">{{ticket.controls.name.value}}</label>
</div>
<select formControlName="controlName" class="form-control col-md-2" style="height:auto !important;"
[ngClass]="{ 'is-invalid': submitted && ticket.controls.controlName.errors }"
(change)="onChange($event.target.value, i)">
<option [ngValue]="null"> --Select-- </option>
<option *ngFor="let ansItem of formdetail.questionList[i].answerList" [ngValue]="ansItem" >{{ansItem.anstext}}</option>
</select>
<div *ngIf="submitted && ticket.controls.controlName.errors" class="invalid-feedback" style="height:auto !important;"
[ngClass]="{ 'input-group-append': submitted && ticket.controls.controlName.errors,
'col-md-1': submitted && ticket.controls.controlName.errors }">
<div *ngIf="ticket.controls.controlName.errors.required">Required</div>
</div>
.
.
.
.
</div>
/div>
</div>
<!-- Question End -->
</form>
Now I am getting error in "ticket.control"
Property 'controls' does not exist on type 'AbstractControl'.
I am not sure why I am getting this error after upgrade.
I have tried ticket.['control'] or ticket.get('controlName') as suggested by other article of similar error in stackoverflow but nothing works
Here is the snapshot of error
Any help will be appreciated.
Thanks
your t is of type FormArray, FormArray.controls array is of type AbstractControl[], thus, each ticket at
<div *ngFor="let ticket of t.controls; let i = index">
line is of type AbstractControl, which does not have controls property as is seen in TS error in your screenshot. TS does not know that each ticket actually is of type FormGroup (set here this.t.push(this.formBuilder.group({...}) and which has controls property).
So you can try to add another getter tControls:
get f() { return this.dynamicForm.controls; }
get t() { return this.f.questions as FormArray; }
get tControls() { return this.t.controls as FormGroup[]; }
and use it within *ngFor:
<div *ngFor="let ticket of tControls; let i = index">
<div [formGroup]="ticket" class="form-row">
...
Please note, that now [formGroup] will also be properly filled with FormGroup and not AbstarctControl
One of your Angular upgrades probably added
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"strictInputTypes": true,
}
to your tsconfig.json file, you can verify it just by setting these values to false and rebuilding your app, but I recommend to leave these strict checks.
This is a generic answer (so generic code) to help fixing such an issue.
Let's suppose you have a loop through a field (call it elements) of type FormArray. This field is part of a form of type FormGroup
In your ts logic code (logic implementation) you should have something like:
form = new FormGroup({
//.. other fields
elements: new FormArray([]),
});
In your template (html file), you need to toop through the elements field:
<li
*ngFor="let element of elements.controls">
{{ element.value }}
</li>
In order to tell angular cli that elements has a property of controls (so of type FormArray), you need to format it in your ts logic code as the following (implement a getter for elements field):
get elements() {
return this.form.get('elements') as FormArray;
}
'controls' isn't a property of an AbstractControl. instead you can use your form group, so 'dynamicForm.controls.controlName'

Reactive form angular using FormArrayName

In my form I need to add phones, I have seen that in the database you are saving a string array with the phone numbers that I added, but the moment the value returned from the database is set to the form, only the first array value is shown .
I would like to display all array values that contain number of phones in string format.
component.html:
<div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="start">
<div formArrayName="phones" fxFlex="30">
<div *ngFor="let phone of phones.controls; index as i">
<mat-form-field>
<input matInput [formControlName]="i" placeholder="Phone Number">
</mat-form-field>
</div>
</div>
<div fxFlex="30">
<button mat-raised-button class="blue" (click)="addPhone()">Add</button>
</div>
</div>
component.ts:
this.myForm = this._formBuilder.group({
phones: new FormArray([new FormControl('')]),
});
this.values$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((values) => {
// values = {phones: ['999999', '88888', '77777']}
if (values) {
this.myForm.patchValue(values, { emitEvent: false });
}
});
get phones(): FormArray { return this.myForm.get('phones') as FormArray; }
addPhone(): void { this.phones.push(new FormControl('')); }
Even returning more than one value inside the array only displays the first array value on my form, what am I doing wrong?
patchValue won't create any form controls for you, so when you call patchValue it only sets the value for the first form control in your form array, because you only created one.
You have to call addPhone for each phone number or create form inside the describe when you already know how many form controls there should be, but you still need to create as many form controls as number of elements in your array.
this.myForm = this._formBuilder.group({
phones: new FormArray(),
});
this.values$
.pipe(takeUntil(this._unsubscribeAll))
.subscribe((values) => {
if (values && values.phones) {
phones.forEach(phone => this.addPhone(phone));
}
});
get phones(): FormArray { return this.myForm.get('phones') as FormArray; }
addPhone(phone = ''): void { this.phones.push(new FormControl(phone)); }

Angular required validation does not let form valid until input is edited

I have a below html where I have implemented HTML5 required property:
<div class="col-sm-3">
<label>{{question.placeholder}}<span *ngIf="question.required">*</span></label>
</div>
<div class="col-sm-9">
<mat-form-field style="width:100%">
<input matInput [placeholder]="question.placeholder" [id]="question.key" [maxLength]="question.maxLength" [formControlName]="question.key"
type="email" [required]="question.required"[readonly]="question.readonly" [value]="question.value" (keyup)="checkValue($event, question)"
(paste)="checkPasteValue($event, question)">
<mat-hint align="end">{{question.value ? question.value.length : 0}} / {{question.maxLength}}</mat-hint>
</mat-form-field>
</div>
It is an email field.
[required]="question.required"
Below is a form button which needs to enabled when form is valid.
<button mat-raised-button class="md-primary md-raised pull-right" color="primary" style="margin-right:10px;" *ngIf="isEditMode" [disabled]="!dataEntryForm.valid"
(click)="updateDataEntry($event)">UPDATE</button>
button does not enable until input is dirty, whereas it is pre filled. So it should be satisfied for required. So what should be used in such case?
EDIT
Form is pre-filled by using service:
editSectionDataItem(item: any) {
var me = this;
// / var decodedItem = this.decodeDataArray([item]);
// console.log(item, this.originalData);
var decodedItem = this.originalData.filter(function (originalItem) {
return originalItem.id == item.id;
});
console.log(decodedItem, item);
var mission = {
widgetConfig: this.widgetConfig,
settings: this.settings,
fields: this.fields,
isEditMode: true,
data: flattenJSON(decodedItem[0])
}
console.log(mission);
this._widgetService.missionToOpenDataEntryForm(mission);
}
You should use [(ngModel)] instead of [value].
[(ngModel)] is basically what gives Angular the validation properties of the form. Without it, you can't track the validation state of the input (at least on a template-driven form).
More info about this on the Angular docs, here and here

Angular 5 [object HTMLDivElement]

When I click the button, I'm sending the 'copyRow' element to the 'copyItem' method. I'm equalizer the 'copyItem' element with the 'item' variable in the 'Typescript' file.
This 'item in the html file' variable when I want to show '[object htmldivelement]' I'm getting as output.
create.component.html
<div class="questions">
<div class="form-group copy-row" #copyRow>
<label>Question</label>
<input type="text" class="form-control" placeholder="Question title">
</div>
{{ item }}
</div>
<button type="button" class="btn btn-primary" (click)="copyItem(copyRow)">Add</button>
create.component.ts
item;
constructor() { }
ngOnInit() {
}
copyItem(row) {
this.item = row;
}
EDIT
My aim is to do a survey project.
When I click on the 'Add' button, the same '#copyRow' element will show in the {{ item }} section. However, I get an output like the second link.
1: http://prntscr.com/j1ncp1
2: http://prntscr.com/j1nd19
I'm not sure what you want to achieve with this but this is the explanation of what is happening in your code.
#copyRow is a reference to the HTML element & in this case it is a div element. So when you're passing the reference using copyItem function, you are actually passing an HTML element.
Putting these things together, the copyItem method gets following signature -
public item: HTMLElement;
public copyItem(row: HTMLElement): void {
this.item = row;
//this is how you get inner HTML
console.log(row.innerHTML);
//this is to get inner text
console.log(row.innerText);
}
This is the reason why you are getting [object HTMLDivElement] in the template for item binding (you are trying to display an object).
You can simply use {{item.innerHTML}} or {{item.innerText}} to display the inner content of selected HTML element.
Let me know if I'm missing anything.
EDIT - Alternative Way (Binding in Template)
If you are not doing additional stuff in the component, the binding can be as simple as assigning the HTML element reference directly to the item property in template itself -
<div class="questions">
<div class="form-group copy-row" #copyRow>
<label>Question</label>
<input type="text" class="form-control" placeholder="Question title">
</div>
{{ item?.innerHtml }}
{{ item?.innerText }}
</div>
<button type="button" class="btn btn-primary" (click)="item = copyRow">Add</button>
EDIT 2 (as per discussion in comments)
Try this template to iterate over same HTML on button click -
<div class="questions">
<ng-container *ngFor="let item of items">
<div class="form-group copy-row">
<label>Question</label>
<input type="text" class="form-control" placeholder="Question title" />
</div>
</ng-container>
<button type="button" class="btn btn-primary" (click)="items = items || []; items.push(1);">Add</button>
Just initialise your items array as -
public items: Array<number> = [1];
I hope this helps :)
Use ViewChild and ElementRef
import { Component, ViewChild, ElementRef } from '#angular/core'
#ViewChild('item')
item: ElementRef;
constructor() { }
ngOnInit() {
}
copyItem() {
// this.item -> now you have the reference of the element
}

check if emails match on blur

I'm trying to check if email field and confirm email field match each other. That is, the user types in their email and then they have to confirm it again. I want the match/validation to happen on blur (when the user presses enter or the textfield loses focus).
Here's my ts file:
import {Component, OnInit} from '#angular/core';
import {User} from './user.interface';
import {FormBuilder, FormGroup, ValidatorFn} from '#angular/forms';
#Component({
selector: 'my-email',
templateUrl: '/app/components/profile/email.component.html',
styleUrls:['styles.css'],
})
export class EmailComponent implements OnInit {
public user : User;
Form : FormGroup;
ngOnInit() {
// initialize model here
this.user = {
Email: '',
confirmEmail: ''
}
if(this.Form.valid) {
this.displayErrors = false;
}
}
constructor(fb: FormBuilder, private cookieService: CookieService, private router: Router) {
this.Form = fb.group({
email: [''],
confirmEmail: ['']
},
{
validator: this.matchingEmailsValidator('email', 'confirmEmail')
});
}
save(model: User, isValid: boolean) {
// call API to save customer
//save email
}
matchingEmailsValidator(emailKey: string, confirmEmailKey: string): ValidatorFn {
return (group: FormGroup): {[key: string]: any} => {
let email = group.controls[emailKey];
let confirmEmail = group.controls[confirmEmailKey];
if (email.value !== confirmEmail.value) {
return {
mismatch: true
};
}
};
}
}
Here's my view:
<form [formGroup]="Form" novalidate (ngSubmit)="Form.valid && save(Form.value, Form.valid)">
<div class="container-fluid">
<div id = "container" class="contain" style="text-align: center">
<div>
<fieldset class="form-group">
<label id = "rounded" class="item item-input .col-md-6 .col-md-offset-3">
<input class="email-address-entry form-control" name="email" type="email" placeholder="name#domain.com" formControlName="email" pattern="^(\\w|[0-9.!#$%&’*+/=?^_\`{|}~-])+#(\\w|[0-9-])+(?:‌​[.](\\w|[0-9-])+)*$"/>
</label>
<p class="Reenter-your-email">Reenter your email to confirm</p>
<label id = "rounded" class="item item-input">
<input class="email-address-entry form-control" (blur)="displayErrors=true" name="confirmEmail" type="email" placeholder="name#domain.com" formControlName="confirmEmail" validateEqual="email"/>
</label>
</fieldset>
</div>
<div>
<label class="entry-invalid">
<p *ngIf="displayErrors && !Form.get('email').valid">The email you entered does not match.</p>
</label>
</div>
<div (click)="Form.get('email').length > 0 ? save(Form.value, Form.valid) : NaN" class="{{ Form.get('email').length > 0 ? 'container-fluid anchorBottomHighlight' : 'container-fluid anchorBottom'}}">
<label class="footerLabel">Confirm</label>
</div>
</div>
</div>
</form>
Currently, with the way it's set up, the validation occurs but it does not get cleared when the correct match is input. I'm wondering how I can setup my view correctly? So the validation message is shown/hidden when the correct match is set and not.
Also it seems like Form.get('email').length > 0 is never greater than 0 / never hit, so my label doesn't toggle to be clickable.
I'm using Angular 2 and reactive forms.
It looks that you're mixing the two form syntaxes: template-driven forms and model-driven forms.
Since you're declaring a form model in your class with FormBuilder, I'm assuming you want a model-driven form.
This means your fields don't need attributes like [(ngModel)] or #EmailAddress.
Instead of that:
<input type="email" [(ngModel)]="user.EmailAddress" required #EmailAddress="ngModel">
Write this:
<!-- Now I'm using `formControlName` to bind the field to the model -->
<!-- Its value must match one of the names you used in the FormBuilder -->
<input type="email" formControlName="email">
ALL of your validators must be declared in the FormBuilder. Not just matchingEmailsValidator, but also required:
this.Form = fb.group({
email: ['', Validators.required],
confirmEmail: ['', Validators.required]
},
{
validator: this.matchingEmailsValidator('email', 'confirmEmail')
});
Now you can access a field with the following syntax:
// In the class
this.Form.get('email').value
this.Form.get('email').errors
<!-- In the template -->
{{ Form.get('email').value }}
{{ Form.get('email').errors }}
You can use these syntaxes to display errors. For example:
<input type="email" formControlName="email">
<p *ngIf="Form.get('email').dirty && Form.get('email').errors.required">
This field is required.
</p>
In the example above, I am displaying an error message if the email field has been touched (i.e. the user tried to enter something) AND the required error is present.
You can also verify that your validation rules are enforced by inspecting the form's markup with your browser's dev tools. Angular should have added classes like .ng-invalid or .ng-valid to the <input> tags that have validation rules.
Finally, regarding your question to check email match on blur. You can't postpone Angular's validation, it will happen in real-time (as the user types). But you could wait for the blur event to display errors.
Combining this last advice with my previous example, here's how you could should an error if the email field is empty AND it has lost focus (blur event):
<input type="email" formControlName="email" (blur)="displayErrors=true">
<p *ngIf="displayErrors && Form.get('email').dirty && Form.get('email').errors.required">
This field is required.
</p>
UPDATE(01-FEB-2017) after Euridice posted this Plunkr:
You still have wayyyyy to much validation code in your template. Like I said, ALL VALIDATORS should be declared IN THE FORM MODEL (with the FormBuilder). More specifically:
The pattern="..." attribute in the email field should be replaced with Validators.pattern() in the form model.
What is the validateEqual="email" attribute in the confirmEmail field? You're not using that anywhere.
The main problem is your test to display the error message: *ngIf="displayErrors && !Form.get('email').valid && Form.get('email').error.mismatch".
First of all, the property is errors with an "s", not error.
Also, your custom validator is setting the error on the form itself, NOT on the email field. This means you should retrieve your mismatch custom error from Form.errors.mismatch, NOT Form.get('email').errors.mismatch.
Here's the updated, working Plunkr: https://plnkr.co/edit/dTjcqlm6rZQxA7E0yZLa?p=preview