Angular changeable form according to inputs - html

I'm using a reactive form. I need to add/remove an input that appears in it according to some other input. Here's a simplified scenario of the issue:
Asking the user to select an option from a list. If their desired option is not there, there is an open input where they can write. If they do choose an option from the select, the input must disappear. If they do not select an option, the input must be there and it must be required.
Here's the code I made which 1) doesn't work 2) feels like it's fairly ugly and could be made in some other way.
Template:
<form [formGroup]="whateverForm" (ngSubmit)="onSubmit()">
Choose an option:
<select
formControlName="option"
(change)="verifySelection($event)">
<option value=''>None</option>
<option value='a'>Something A</option>
<option value='b'>Something B</option>
</select>
<br>
<div *ngIf="!optionSelected">
None of the above? Specify:
<input type="text" formControlName="aditional">
</div>
<br>
<br>
Form current status: {‌{formStatus}}
</form>
Code:
export class AppComponent {
whateverForm: FormGroup;
formStatus: string;
optionSelected = false;
ngOnInit() {
this.whateverForm = new FormGroup({
'option': new FormControl(null, [Validators.required]),
'aditional': new FormControl(null, [Validators.required])
});
this.whateverForm.statusChanges.subscribe(
(status) => {
this.formStatus = status;
}
);
}
verifySelection(event: any) {
if (event.target.value !== '') {
this.optionSelected = true;
this.whateverForm.get('aditional').clearValidators();
this.whateverForm.get('option').setValidators(
[Validators.required]);
} else {
this.optionSelected = false;
this.whateverForm.get('option').clearValidators();
this.whateverForm.get('aditional').setValidators(
[Validators.required]);
}
}
}

Instead of using an event, I used an observable in one of the fields. The exact solution to the problem I proposed is here.
And I solved it using what I found here (they are basically the same thing, but I included mine for completion).

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'

Change behaviour of enter key in a phone - Angular 5

I am working with inputs but I am not really sure about how is the configuration of the navigation done (I guess that it are predefined behaviours).
I am not in the last input the enter key goes to the next one. This one is working as I want.
Nevertheless, when I am on the last input, when I press enter, it automatically clicks on the next button.
This is what I am trying to avoid. Is there any way to change this behaviour? Just to close the keyboard or to click on another button?
I have tried with keyup.enter and it pseudo works. It calls to the method but also clicks on the next button
HTML
<input
type="text"
class="form-control"
id="validationCustomSurname"
placeholder="e.g. Lopez"
required
(keyup.enter)="onNavigate(1, 'forward')"
[(ngModel)]="values.store.surname"
name="surname"
/>
This method should work on a phone, so I guess that keydown is not an option since $event.code does not give me any code in the phone.
Some time ago I make a directive see stackblitz that you apply in a div (or in a form) in the way
<form [formGroup]="myForm" (submit)="submit(myForm)" enter-tab>
Each input or button add a reference variable #nextTab like
<input name="input1" formControlName="input1" #nextTab/>
<button type="button" #nextTab/>
</form>
The directive use ContentChildren to add a keydown.enter to all the components that have #nextTab to focus to the next control
export class EnterTabDirective {
#ContentChildren("nextTab") controls: QueryList<any>
nextTab
constructor(private renderer: Renderer2, private el: ElementRef) {
}
ngAfterViewInit(): void {
this.controls.changes.subscribe(controls => {
this.createKeydownEnter(controls);
})
if (this.controls.length) {
this.createKeydownEnter(this.controls);
}
}
private createKeydownEnter(querycontrols) {
querycontrols.forEach(c => {
this.renderer.listen(c.nativeElement, 'keydown.enter', (event) => {
if (this.controls.last != c) {
let controls = querycontrols.toArray();
let index = controls.findIndex(d => d == c);
if (index >= 0) {
let nextControl = controls.find((n, i) => n && !n.nativeElement.attributes.disabled && i > index)
if (nextControl) {
nextControl.nativeElement.focus();
event.preventDefault();
}
}
}
})
})
}
Here's a very simple approach, with just a few lines of code:
First, in your Template when you dynamically create your Input elements: 1. populate the tabIndex attribute with a unique number, 2. populate a super-simple custom "Tag" Directive with the same unique number as the tabIndex, and 3. set up a Keydown "Enter" event listener:
Template:
<ng-container *ngFor="let row in data">
<input tabindex ="{{row[tabCol]}}" [appTag]="{{row[tabCol]}}" (keydown.enter)="onEnter($event)" . . . />
</ng-container>
In your component, your super-simple event-listener onEnter():
#ViewChildren(TagDirective) ipt!: QueryList<ElementRef>;
onEnter(e: Event) {
this.ipt["_results"][(<HTMLInputElement>e.target).tabIndex%(+this.ipt["_results"].length-1)+1].el.nativeElement.focus();
}
Note: The modulus (%) operation is just to make sure that if you're at the last Input, you'll get cycled back to the first input.
Super-simple, bare-minimum "Tag" Directive
import { Directive, ElementRef, Input } from '#angular/core';
#Directive({
selector: '[appTag]'
})
export class TagDirective {
#Input('appTag') id: number;
constructor(public el: ElementRef) { }
}
There's probably even a way to get rid of the "Tag" `Directive altogether and make it even more simple, but I haven't had time to figure out how to do that yet . . .

Setting default value of HTML select in a child component with data from API call

I have a multi-component form that I want to pre-fill with data from an API call. The data populates correctly for every field except for the selected value for the select tag. The value is correct, it is just not displaying the correct option inside of the select. The rest of the form, made of input fields, fill in with the correct data.
The child component takes in a form group and that is what fills in the fields.
I have tried .setValue, .patchValue, [(ngModel)], etc. I cannot get the default value to display correctly. What am I doing wrong?
child-component.html
<select [attr.id]="reasonCode" formControlName="reasonCode" placeholder="Reason Code" style="max-width: 100% !important"
no-padding selected>
<option value="" >Please select reason code</option>
<option *ngFor="let reasonCode of reasonCodesService.reasonCodes">
{{reasonCode}}
</option>
</select>
child-component.ts
#Input() newReqForm: FormGroup;
parent-component.html
<div *ngFor="let control of newForm.controls['requisitionItem'].controls; let i = index" style="border: 1px solid black; border-radius: 10px; margin-top: 10px;">
<edit-items (remove)='onRemove($event)' [newReqForm]="newForm.controls.requisitionItem.controls[i]" style="padding-bottom: 10px"
[index]='i'></edit-items>
</div>
parent-component.ts
ngOnInit() {
this.employeeService.loadEmployees();
this.reasonCodesService.loadReasonCodes();
this.itemReqId = +this.route.snapshot.paramMap.get('ReqId');
this.reqService.getRequisition(this.itemReqId).subscribe(response => {
this.editReq = response;
console.log("EDIT REQ VVV");
console.log(this.editReq);
this.editRI = this.editReq.requisitionItem;
this.newForm = this.fb.group({
employee: [this.editReq.employee, Validators.required],
job: [this.editReq.job, Validators.compose([Validators.pattern("^[0-9]+$")])],
requisitionItem: this.fb.array([
])
});
this.arrayControl = <FormArray>this.newForm.controls['requisitionItem'];
this.editRI.forEach(item => {
let newItem = this.fb.group({
item: [item.item, Validators.required],
quantity: [item.quantity, Validators.compose([Validators.required, Validators.pattern("^[0-9]+$")])],
reasonCode: [item.reasonCode],
operation: [item.operation],
})
this.arrayControl.push(newItem);
this.setValidators()
});
for (let i = 0; i < this.arrayControl.length; i++) {
this.arrayControl.controls[i].get('reasonCode').setValue(this.editRI[i].reasonCode);
}
this.setValidators();
console.log(this.editReq);
this.newReqForm = this.newForm;
});
}
My node/Angular info:
Angular CLI: 6.0.8
Node: 8.11.2
OS: win32 x64
EDIT
child-component.ts
isSelected(reasonCode) {
if (reasonCode == this.rc) {
return reasonCode
}
}
You are missing a binding on selected of the selected option (see here). You can set the value of the select using reactive forms but this does not actually select an option.
In other words, you need to add something like [selected]="isSelected(reasonCode)" to option:
<option *ngFor="let reasonCode of reasonCodesService.reasonCodes" [selected]="isSelected(reasonCode)">
{{reasonCode}}
</option>
You can implement isSelected for example by comparing its parameter with the value obtained from the FormGroup reference.

Nothing selected in Select by default - how to fix that?

I'm using Angular 5 and I have a simple Select:
<select class="ui fluid dropdown" formControlName="device">
<option *ngFor="let device of getSenderDevices()" [ngValue]="device">{{device.Name}}</option>
</select>
My problem is the fact that by default nothing is selected, but I'd like the first option to be selected. I saw many threads like this, the solution that I thought would work, does not:
<select class="ui fluid dropdown" formControlName="device">
<option *ngFor="let device of getDevices(); let i = index" [ngValue]="device" [selected]="i==0">{{device.Name}}</option>
</select>
I also found some advices to use compareWith directive - but I wasn't able to understand how it works.
Maybe the problem is caused by getDevices(), which returns data with some delay, because the data is fetched from external server. In the beginning select has to be empty, because the data is not ready yet. When it arrives however, I'd like the select to show that by auto-selecting first option.
Not use a function getDevices() and not use [selected] in .html
//I imagine you have a serviceData that return an observable
export class DataService {
constructor(private httpClient HttpClient) {}
public getDevices():Observable<any>
{
return this.httpClient.get("....");
}
}
constructor(private fb:FormBuilder,private myservice:ServiceData){}
ngOnInit()
{
this.myService.getDevices().subscribe(res=>{
this.devices=res;
this.createForm();
})
//If getDevices() simple return an array, not from service
// this.devices=getServices();
// this.createForm();
}
createForm()
{
this.myForm=this.fb.group({device:this.device[0]})
}
<form [formGroup]="myForm">
<select class="ui fluid dropdown" formControlName="device">
<!--NOT use [selected], when "device" get the value, the select show the value-->
<!--I use the variables "devices"--->
<option *ngFor="let device of devices; let i = index"
[ngValue]="device">{{device.Name}}</option>
</select>
</form>

Angular 5 FormGroup reset doesn't reset validators

I have a form on my page and when I call FormGroup.reset() it sets the forms class to ng-pristine ng-untouched but FormControl.hasError(...) still returns truthy. What am I doing wrong here?
Template
<form [formGroup]="myForm" (ngSubmit)="submitForm(myForm)">
<mat-form-field>
<input matInput formControlName="email" />
<mat-error *ngIf="email.hasError('required')">
Email is a required feild
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput type="password" formControlName="password" />
<mat-error *ngIf="password.hasError('required')">
Password is a required feild
</mat-error>
</mat-form-field>
<button type="submit">Login</button>
</form>
Component
export class MyComponent {
private myForm: FormGroup;
private email: FormControl = new FormContorl('', Validators.required);
private password: FormControl = new FormControl('', Validators.required);
constructor(
private formBuilder: FormBuilder
) {
this.myForm = formBuilder.group({
email: this.email,
password: this.password
});
}
private submitForm(formData: any): void {
this.myForm.reset();
}
}
Plunker
https://embed.plnkr.co/Hlivn4/
It (FormGroup) behaves correctly. Your form requires username and password, thus when you reset the form it should be invalid (i.e. form with no username/password is not valid).
If I understand correctly, your issue here is why the red errors are not there at the first time you load the page (where the form is ALSO invalid) but pop up when you click the button. This issue is particularly prominent when you're using Material.
AFAIK, <mat-error> check the validity of FormGroupDirective, not FormGroup, and resetting FormGroup does not reset FormGroupDirective. It's a bit inconvenient, but to clear <mat-error> you would need to reset FormGroupDirective as well.
To do that, in your template, define a variable as such:
<form [formGroup]="myForm" #formDirective="ngForm"
(ngSubmit)="submitForm(myForm, formDirective)">
And in your component class, call formDirective.resetForm():
private submitForm(formData: any, formDirective: FormGroupDirective): void {
formDirective.resetForm();
this.myForm.reset();
}
GitHub issue: https://github.com/angular/material2/issues/4190
In addition to Harry Ninh's solution, if you'd like to access the formDirective in your component without having to select a form button, then:
Template:
<form
...
#formDirective="ngForm"
>
Component:
import { ViewChild, ... } from '#angular/core';
import { NgForm, ... } from '#angular/forms';
export class MyComponent {
...
#ViewChild('formDirective') private formDirective: NgForm;
constructor(... )
private someFunction(): void {
...
formDirective.resetForm();
}
}
After reading the comments this is the correct approach
// you can put this method in a module and reuse it as needed
resetForm(form: FormGroup) {
form.reset();
Object.keys(form.controls).forEach(key => {
form.get(key).setErrors(null) ;
});
}
There was no need to call form.clearValidators()
Add the property -
#ViewChild(FormGroupDirective) formGroupDirective: FormGroupDirective;
and use this instead of this.myForm.reset();
this.formGroupDirective.resetForm();
This will reset the error display and also do the job of form.reset(). But the form, along with the fields, will still show ng-invalid class
Check this answer for more details - https://stackoverflow.com/a/56518781/9262627
The below solution works for me when trying to reset specific form controller in form group -
this.myForm.get('formCtrlName').reset();
this.myForm.get('formCtrlName').setValidators([Validators.required, Validators.maxLength(45), Validators.minLength(4), Validators.pattern(environment.USER_NAME_REGEX)]);
this.myForm.get('formCtrlName').updateValueAndValidity();
form.reset() won't work on custom form control like Angular Material that's why the function is not working as expected.
My workaround for this is something like this
this.form.reset();
for (let control in this.form.controls) {
this.form.controls[control].setErrors(null);
}
this.form.reset() the issue with this is that it will reset your formcontrol values but not the errors so you need to reset them individually by this line of code
for (let control in this.form.controls) {
this.form.controls[control].setErrors(null);
}
With this you don't need to use FormGroupDirective which is a cleaner solution for me.
Github issue: https://github.com/angular/angular/issues/15741
I found that after calling resetForm() and reset(), submitted was not being reset and remained as true, causing error messages to display. This solution worked for me. I found it while looking for a solution to calling select() and focus() on an input tag, which also wasn't working as expected. Just wrap your lines in a setTimeout(). I think setTimeout is forcing Angular to detect changes, but I could be wrong. It's a bit of a hack, but does the trick.
<form [formGroup]="myFormGroup" #myForm="ngForm">
…
<button mat-raised-button (click)="submitForm()">
</form>
submitForm() {
…
setTimeout(() => {
this.myForm.resetForm();
this.myFormGroup.reset();
}, 0);
}
resetForm() {
this.myFormGroup.reset();
this.myFormGroup.controls.food.setErrors(null);
this.myFormGroup.updateValueAndValidity();
}
UPDATE FROM 2021 - ANGULAR 11.2
The fact to use a [formGroup]="form and a #formDirective="ngForm" directly into the HTML function is not a good practise. Or maybe you would prefer to use #ViewChild, and do it directly from your .ts. Actually, the problem don't come from Angular, but Material.
If you take a look at their GitHub, you will see this :
/** Provider that defines how form controls behave with regards to displaying error messages. */
#Injectable({providedIn: 'root'})
export class ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
return !!(control && control.invalid && (control.touched || (form && form.submitted)));
}
}
The form will keep its submitted state. So you just have to delete the last part of the function.
Here is my solution (tested and working). I have a Material Module, into I've implemented this :
export class ShowOnInvalidTouchedErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl): boolean {
return !!(control && control.invalid && control.touched);
}
}
#NgModule({
providers: [
{
provide: ErrorStateMatcher, useClass: ShowOnInvalidTouchedErrorStateMatcher
}
],
exports: [
MatSnackBarModule,
MatTabsModule,
...
]
});
If you want to use this ErrorStateMatcher on only one form, it's possible. Please see this Material example. This is the same principle.
I had no luck with resetting the form directive. But You can also change the input state to pending to do that as well.
this.myForm.get("email").reset();
this.myForm.get("password").reset();
To anyone whom this may help, I am running Angular 9.1.9 and I didn't want to reset the form/controls just the overall validity of the form so I just ran:
this.registerForm.setErrors(null);
...where registerForm: FormGroup and that reset the form errors, leading to:
this.registerForm.valid
...returning true.
The same can be done for controls:
this.registerForm.get('email').setErrors(null)
As soon as the form is touched, these errors are re-evaluated anyway so if that's not good enough, you may need to have a boolean flag to further pin-down exactly when you want to start showing/hiding error UI.
I did not need to touch the directive in my case.
I was also having the same set of problems. My problem was that i was using mat-form-field and formGroup. After resetting the form submitted flag was not resetting.
So, the solution that worked for me is, putting a directive of ngForm along with formGroup and passing onSubmit(form). Added
#ViewChild('form') form;
in component and then I used
this.form.resetForm();
Nothing from above worked for me (Angular 7.2, Angular Material 7.3.7).
Try to pass with submit method an event on view:
<form [formGroup]="group" (ngSubmit)="onSubmit($event)">
<!-- your form here -->
</form>
Then use it to reset currentTarget and your form afterwards:
public onSubmit(event): void {
// your code here
event.currentTarget.reset()
this.group.reset()
}
Simple fix: use button with type="reset" and function submitForm() together
<form [formGroup]="MyForm" (ngSubmit)="submitForm()">
<input formControlName="Name">
<mat-error>
<span *ngIf="!tunersForm.get('Name').value && tunersForm.get('Name').touched"></span>
</mat-error>
<button type="reset" [disabled]="!MyForm.valid" (click)="submitForm()">Save</button>
</form>