Angular form disable control on valueChanges - javascript

I am trying to disable a form control when one of my form group value changes but the problem is that the method disable() and enable() also updates the form status, so I have an infinite loop.
#Component({
selector: 'my-app',
template: `
<form [formGroup]="questionForm">
<input type="text" formControlName="question" />
<input type="text" formControlName="other"/>
</form>
`,
})
export class App {
constructor(private fb: FormBuilder) {
this.questionForm = this.fb.group({
question: [''],
other: ['']
});
}
ngOnInit() {
this.questionForm.valueChanges.subscribe(v => {
if(v.question === 'test') {
this.questionForm.get('other').disable();
} else {
...enable()
}
})
}
}
How can I solve this problem?

Well, according to the official docs you could use an directive for custom validation, this could of course in your case could be applied with logic of checking what the user has input and then disable and enable the other field. There's quite a lot of code there, sooooo...
...You could also do a smaller hack if you do not want all that bunch of code. Let's call a function that checks what user has typed. We also need to bind this, so we can refer to the questionForm:
this.questionForm = this.fb.group({
question: ['', [this.checkString.bind(this)]],
other: ['']
});
Then the function:
checkString(control: FormControl) {
if(control.value == 'test') {
this.questionForm.get('other').disable()
}
// we need to check that questionForm is not undefined (it will be on first check when component is initialized)
else if (this.questionForm) {
this.questionForm.get('other').enable()
}
}
This seems to serve it's purpose.
Demo
Hope this helps! :)

I would first add a property isDisabled to your component, then I would use the [disabled] directive. This would solve your looping issue.
#Component({
selector: 'my-app',
template: `
<form [formGroup]="questionForm">
<input type="text" formControlName="question" />
<input type="text" formControlName="other" [disabled]="isDisabled" />
</form>
`,
})
export class App {
isDisabled = false;
constructor(private fb: FormBuilder) {
this.questionForm = this.fb.group({
question: [''],
other: ['']
});
}
ngOnInit() {
this.questionForm.valueChanges.subscribe(v => {
if (v.question === 'test') {
this.isDisabled = true;
} else {
this.isDisabled = false;
}
})
}
}

With reactive forms, you do it something like this:
question: {value: '', disabled: true }

Adding another answer to work around the reactive forms issue using two controls and an *ngIf directive. See https://github.com/angular/angular/issues/11324.
This solution may not be ideal as it requires that you add a second form control that will appear when the conditions for changing the isDisabled property are met. This means there are two controls to manage when submitting the form.
#Component({
selector: 'my-app',
template: `
<form [formGroup]="questionForm">
<input type="text" formControlName="question" />
<input *ngIf="isDisabled" type="text" formControlName="other" disabled />
<input *ngIf="!isDisabled" type="text" formControlName="other2" />
</form>
`
providers: [FormBuilder]
})
export class App {
isDisabled = true;
constructor(private fb: FormBuilder) {
this.questionForm = this.fb.group({
question: [''],
other: [''],
other2: ['']
});
}
ngOnInit() {
this.questionForm.valueChanges.subscribe(v => {
if(v.question === 'test') {
this.isDisabled = false;
} else {
this.isDisabled = true;
}
});
}
}
Check out or play with the plunker if you wish:
https://plnkr.co/edit/0NMDBCifFjI2SDX9rwcA

Related

Angular Reactive form custom validation is not triggering on touch

I have an angular reactive form with default Validation.required and a CustomValidation.
Inside the CustomValidation I intended to check if the control is touched, but somehow this is not working.
import {
CustomValidation
} from './CustomValidator';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
customForm: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.customForm = this.fb.group({
customInput: [
'', [Validators.required, CustomValidation.customEmailValidation()],
],
});
}
}
// CustomValidation.ts
import {
AbstractControl,
ValidationErrors,
ValidatorFn
} from '#angular/forms';
export class CustomValidation {
static customEmailValidation(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (control.touched && control.value.length === 0) {
console.log(control);
return {
customError: {
hasError: true,
errorType: 'empty field', // this can be any name
errorLabel: 'test',
},
};
}
return null;
};
}
}
<form [formGroup]="customForm">
<input formControlName="customInput" />
<button [disabled]="this.customForm.invalid">Submit</button>
</form>
I am expecting that the console.log inside the static method customEmailValidation will log the control object when the field will be touched. By touch I mean, I only click on the input.But that is not happening.
I also tried using updateOn
customInput: ['', {validators:[CustomValidation.customEmailValidation(),Validators.required],updateOn:"blur"}]
Please help me in understanding why it is not working.
Stackblitz Demo
At first, before you touch it, form.touched is false.
So thats the first value.
That value is being taken for the first time as form.touched
So you will get touched property as false.
If you intend to make it touched, just go with
this.form.markAsTouched() and then do the logic.
You are describing the focusin event, not the touched state.
From the Angular docs:
touched: boolean Read-Only True if the control is marked as touched.
A control is marked touched once the user has triggered a blur event
on it.
You are expecting this:
By touch I mean, I only click on the input.But that is not happening.
Also, the custom validator should be agnostic to the form input state. You can achieve what you want by binding to the focusin event and using that to flip the touched state. I don't understand why you would want to do this from a UX perspective, but hey.
Template
<form [formGroup]="customForm">
<input (focusin)="onFocusIn($event)" formControlName="customInput" />
<button [disabled]="this.customForm.invalid">Submit</button>
</form>
<span *ngIf="this.customForm.invalid && this.customForm.touched"
>VERY SERIOUS ERROR</span
>
Component
onFocusIn(event) {
this.customForm.markAsTouched();
console.log('focus');
}
Validator
static customEmailValidation(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (control.value.length === 0) {
console.log(control);
return {
// ...control.errors,
customError: {
hasError: true,
errorType: 'empty field', // this can be any name
errorLabel: 'test',
},
};
}
return null;
};
}
Stackblitz here.
You can read more about custom form controls here from the Angular docs.

How to check for state change in angular 4/6?

My task is to create an account information web page which consists of 4 pre-filled fields (given name, family name, username and email) and a common save button at the bottom. User can change any field by the respective field. I want save button to be enabled only if user changes any fields. Any method to track state changes? My code is as follows:
<mat-card-content>
<div class="form-group">
<mat-form-field class="simple-form-field-50">
<input matInput placeholder="Given name" name="givenName" formControlName="givenName">
</mat-form-field>
<mat-form-field class="simple-form-field-50">
<input matInput placeholder="Family name" name="familyName" formControlName="familyName">
</mat-form-field>
<br>
<mat-form-field>
<input matInput placeholder="Email" name="email" formControlName="email">
</mat-form-field>
<br>
<button
[disabled]="waiting"
class="simple-form-button"
color="primary"
mat-raised-button
type="submit"
value="submit">
Save
</button>
</div>
</mat-card-content>
My Code Output:
Since you're using a Reactive Form, you can use valueChanges on the FormGroup.
Since it is of type Observable, you can subscribe to it to set a variable of type boolean that will be used in the template to enable the button.
...
#Component({...})
export class AppComponent {
form: FormGroup;
disableButton = true;
ngOnInit() {
...
this.form.valueChanges.subscribe(changes => this.disableButton = false);
}
}
And in your template:
<form [formGroup]="form">
...
<button [disabled]="disableButton">Submit</button>
</form>
UPDATE:
If you want to disable it when values don't really change, check for the current value of the form with the previous value:
import { Component } from '#angular/core';
import { FormGroup, FormControl } from '#angular/forms';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
form: FormGroup;
disableButton = true;
userValue = {
firstName: 'John',
lastName: 'Doe',
email: 'john.doe#domain.com'
}
ngOnInit() {
this.form = new FormGroup({
firstName: new FormControl(),
lastName: new FormControl(),
email: new FormControl()
});
this.form.patchValue(this.userValue);
this.form.valueChanges.subscribe(changes => this.wasFormChanged(changes));
}
private wasFormChanged(currentValue) {
const fields = ['firstName', 'lastName', 'email'];
for(let i = 0; i < fields.length; i++) {
const fieldName = fields[i];
if(this.userValue[fieldName] !== currentValue[fieldName]) {
console.log('Came inside');
this.disableButton = false;
return;
}
}
this.disableButton = true;
}
}
NOTE: StackBlitz is updated accordingly.
Here's a Working Sample StackBlitz for your ref.
onChange(targetValue : string ){
console.log(targetValue );}
<input matInput placeholder="test" name="test" formControlName="testNM" (input)="onChange($event.target.value)">
This is called Dirty Check.
You may find this SO answer very useful:
https://stackoverflow.com/a/50387044/1331040
Here is the guide for Template-Driven Forms
https://angular.io/guide/forms
Here is the guide for Reactive Forms
https://angular.io/guide/reactive-forms
And here is the difference between two concepts
https://blog.angular-university.io/introduction-to-angular-2-forms-template-driven-vs-model-driven/
Hope these help.
I would do something like this:
form: FormGroup;
disableButton = true;
originalObj: any;
ngOnInit() {
this.form = new FormGroup({
control: new FormControl('Value')
});
this.originalObj = this.form.controls['control'].value; // store the original value in one variable
this.form.valueChanges.subscribe(changes => {
if (this.originalObj == changes.control) // this if added for check the same value
{
this.disableButton = true;
}
else {
this.disableButton = false;
}
}
);
}
WORKING EXAMPLE

How to make button disable after one click using angular2

I have a send Button, that contains 2 Api in it.
So, if the input box is empty then send the button is disabled.
Now i want 1 condition to work,
After giving Email-Id and click on save button, it must get disabled after one click.
If i edit again on input box then it must be enabled or it must be in disabled state.
Check this
<button (click)="generateEmailOtp(enterSms,enterEmail)" class="btn pull-right otp" [disabled]="buttonDisabled">Some Button</button>
buttonDisabled: boolean = false; //class variable
generateEmailOtp(enterSms,enterEmail) {
this.buttonDisabled = !this.buttonDisabled
// TO DO
// If any error/valdiation fails, again reset this.buttonDisabled = !this.buttonDisabled
}
If you are uisng reactive forms take a look at this stackblitz.
component.ts
import { Component } from '#angular/core';
import { FormGroup, Validators, FormBuilder } from '#angular/forms';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
public form: FormGroup;
public isThePreviousEmail: boolean;
private previousEmail: string;
constructor(private fb: FormBuilder) {
this.isThePreviousEmail = true;
this.buildForm();
this.form.valueChanges.subscribe((value) => {
if (this.previousEmail) {
this.isThePreviousEmail = value !== this.previousEmail;
}
});
}
public onSubmit(): void {
this.previousEmail = this.form.value.email;
this.isThePreviousEmail = false;
}
private buildForm(): void {
this.form = this.fb.group({
email: [null, Validators.compose([Validators.required, Validators.email])]
});
}
}
component.html
<form [formGroup]="form" (submit)="onSubmit()">
Email: <input formControlName="email">
<button type="submit" [disabled]="form.get('email').invalid || !isThePreviousEmail">Submit</button>
</form>.
I'm using reacctive form validation to disable the submit button. And a boolean variable which becomes false when user edit the input field.
[disabled]="form.get('email').invalid || !isThePreviousEmail"
and i'm subscrbing to form value changes like this.
this.form.valueChanges.subscribe((value) => {
if (this.previousEmail) {
this.isThePreviousEmail = value !== this.previousEmail;
}
});
And on the onSubmit method i'm setting the value to the previousEmail and making the isThePreviousEmail to false.
public onSubmit(): void {
this.previousEmail = this.form.value.email;
this.isThePreviousEmail = false;
}
This might help you to take an idea.
You can use HTML only for that :
<button
#submitButton
(click)="generateEmailOtp(enterSms,enterEmail); submitButton.disabled = true;"
class="btn pull-right otp"
*ngIf="isConfirmEmailOtp || isSms">Send OTP</button>
And to enable it again once the input changed :
<input
class="form-control col-lg-12"
type="email"
[(ngModel)]="enterEmail"
name="myEmail"
#myEmail="ngModel"
email
pattern="[a-zA-Z0-9.-_]{1,}#[a-zA-Z.-]{2,}[.]{1}[a-zA-Z]{2,}"
(input)="submitButton.disabled = false"
#emailOTP>

Angular 4 - Reactive Forms enable/disable not working

I am having trouble enabling and disabling a form controls in my application. It is probably because the form is being enabled/disabled in an asynchronous context.
Here is how the code is.
user.component.html
<form [formGroup]="form">
<input type="text" id="name" formControlName="name" />
<input type="text" id="age" formControlName="age" />
</form>
user.component.ts
ngOnInit() {
this._route.queryParams.subscribe(params => {
getUser(params.userId).subscribe(user => {
this._buildForm(user);
})
});
}
private _buildForm(user){
this.form = _fb.group({
name: [{value: user.name, disabled: user.someCondition}, Validators.required],
age: [{value: user.age, disabled: user.anotherCondition}, Validators.required]
})
}
When the first time the user is loaded upon param change, the controls do get enabled/disabled on the basis of their relevant conditions. When the params change subsequently, the state of the controls remain the same although the values are set appropriately.
I have tried different ways to fix this but no help. For example, I have tried the following at the end of the _buildForm method.
this.form.disable() //Doesn't do anything
this.form.controls.name.disable(); //Doesn't help
One thing that does work as expected is the following (but this isn't what is required).
<button (click)="form.disable()" value="Disable Form" />
<button (click)="form.enable()" value="Enable Form" />
What I feel is that the problem is due the fact the _buildForm() is being called from the asynchronous context (subscribe method).
How can I fix this problem?
UPDATE
Please note that the Observable subscription is triggered on the basis of the navigation as below.
this._router.navigate([], {
relativeTo: this._route,
queryParams: {userId: 10}
})
UPDATE 2
https://angular-cvf1fp.stackblitz.io
This is an easier way to understand my problem
You just need enable/disable methods of form control.
Here is stackblitz example. It works perfectly.
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
group;
isDisabled = true;
constructor(fb: FormBuilder) {
this.group = fb.group({
stuff: fb.control({value: 'stuff', disabled: this.isDisabled})
});
}
toggle() {
this.isDisabled = !this.isDisabled;
this.group.controls.stuff[this.isDisabled ? 'enable' : 'disable']();
}
}
You're right. The subscription is only fired once, so after submitting the form the user entity does not change anymore. To do it, add for instance an event handler to your form submit.
user.component.html
<form [formGroup]="form" (submit)="_updateForm()">
<input type="text" id="name" fromControlName="name" />
<input type="text" id="age" fromControlName="age" />
</form>
user.component.ts
private userId: any;
ngOnInit() {
this._route.queryParams.subscribe(params => {
this.userId = params.userId;
this._updateForm();
});
}
private _buildForm(user){
this.form = _fb.group({
name: [{value: user.name, disabled: user.someCondition}, Validators.required],
age: [{value: user.age, disabled: user.anotherCondition}, Validators.required]
});
}
// The update function has to be public to be called inside your template
public _updateForm(){
getUser(this.userId).subscribe(user => {
this._buildForm(user);
});
}
Create the formGroup directly in ngOnit w/controls disabled by default so you're sure it exists w/o worrying about async issues.
In this._route.queryParams.subscribe(...) you can use this.form.setValue({//set all control values}) or this.form.patchValue({//set individual controls}) to update the individual controls as needed/once available.
Use component instance variables for condition and someOtherCondition (or a user object as a whole if you want to separate your data model from the form's model - they don't have to be the same). Initialize these to "false", and refer to them instead for enabling/disabling the controls in the formGroup creation. You can also update those variables in the same .subscribe() block once the user data is in place, instead of waiting to access user data model directly.
This way your controls and formGroup exist, and are disabled, until the async data is available.
<form [formGroup]="form">
<input type="text" id="name" fromControlName="name" />
<input type="text" id="age" fromControlName="age" />
</form>
do you see a typo here?fromControlName should be formControlName
After that change toggling will work ;)
https://angular-rivxs5.stackblitz.io

Angular 4 - Using Asynchronous custom validators

I have the following code:
HTML:
<div [class]="new_workflow_row_class" id="new_workflow_row">
<div class="col-sm-6">
<label class="checkmark-container" i18n>New Workflow
<input type="checkbox" id="new-workflow" name="new-workflow" [(ngModel)]="new_checkbox" (click)="uncheckBox($event, 'edit')">
<span class="checkmark" id="new-workflow-checkmark" [class]="checkmark_class"><span id="new-workflow-checkmark-content"></span>{{checkmark_content}}</span>
</label>
<input type="text" *ngIf="new_checkbox" id="new_workflow_name" name="new_workflow_name" (keyup)="clearWorkflowError(true)" [(ngModel)]="new_workflow" placeholder="Enter Workflow Name">
<p [hidden]="!show_workflow_error && !workflowForm.errors" class="workflow-error" i18n>The workflow name already exists. Please use a different name.</p>
</div>
</div>
Component.ts:
duplicateWorkflowValidator(control: FormControl) {
console.log("this validator was called!");
clearTimeout(this.workflowTimeout);
return new Promise((resolve, reject) => {
if (this.new_workflow != '') {
this.workflowTimeout = setTimeout(() => {
this
.requestService
.findWorkflow(control.value)
.subscribe((results: any) => {
let data: any = results.data;
if (data.duplicate) {
resolve({ duplicateWorkflow: { value: control.value}})
}
else if (results.status == "OK") {
resolve(null);
}
})
;
}, 500);
}
else {
resolve(null);
}
})
}
Inside constructor for component.ts:
this.workflowForm = new FormGroup({
name: new FormControl(this.new_workflow, [
Validators.required,
], this.duplicateWorkflowValidator.bind(this))
});
I am trying to bind this asynchronous validator to the reactive form but it's not working. I want to use the duplicateWorkflowValidator inside workflowForm and have it trigger an error message when it finds a duplicate workflow.
How do I a) bind the validator to the reactive form properly, b) access the validator errors? Thanks in advance, hopefully this makes sense.
You are mixing template forms with reactive forms. Chose one approach. In the below example I am using reactive forms.
Try this simplified version. For demonstration purposes below the validator will fail when I type test, but succeed when I type anything else. You will need to change that to your service call.
https://angular-sjqjwh.stackblitz.io
Template:
<form [formGroup]="myForm">
<div>
<div>
<input type="text" formControlName="name">
<div *ngIf="myForm.controls.name.hasError('duplicateWorkflow')">
Workflow already exists!
</div>
{{ myForm.controls.name.hasError('duplicateWorkflow') | json }}
</div>
</div>
</form>
Component
import { Component } from '#angular/core';
import { FormControl, FormGroup, Validators, Form, FormBuilder } from '#angular/forms';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
workflowTimeout: number = 0;
myForm: FormGroup;
constructor(private fb: FormBuilder) {
this.myForm = this.fb.group({
name: new FormControl('',
[Validators.required],
this.duplicateWorkflowValidator.bind(this))
});
}
duplicateWorkflowValidator(control: FormControl) {
console.log("this validator was called!");
return new Promise((resolve, reject) => {
if (control.value === 'test') {
this.workflowTimeout = setTimeout(() => {
resolve({ duplicateWorkflow: { value: control.value } })
}, 500);
}
else {
resolve(null);
}
});
}
}

Categories