How to force validation for disabled property? - html

I have a form element as follows:
<input type="text" id="country" formControlName="Country" />
I have form group as follows:
this.myForm = this.formbuilder.group({
'Country': [{ value: this.user.Country, disabled: this.SomeProperty===true}, [Validators.required]],
});
When it is disabled when this.SomeProperty is true, required validator does not work at all and Save button is enabled.
<button type="submit" [disabled]="!myForm.valid">Save</button>
Is there anyway to force validation for disabled attribute?
The reason the required property can be null is because of some data issue or data got migrated from different source without the required values which is beyond my control.

Hmmm.. Generally, Angular Reactive Forms will not trigger validators for disabled form fields.
I can suggest two ways for you to get around this situation:
1) Writing a custom validator on your FormGroup to manually check the values for each individual Form Control.
this.myForm = this.formbuilder.group({
'Country': [{ value: this.user.Country, disabled: this.SomeProperty===true}, [Validators.required]],
// other FormControls
}, { validator: GroupValidator.checkEveryField() });
Then, create a new Validator class that checks for an empty Country FormControl. If it is empty, it will return the validation error.
import { AbstractControl } from '#angular/forms';
export class GroupValidator {
static checkEveryField(control: AbstractControl) {
const { Country } = control.value;
if (!Country) {
return { 'invalidCountry': true };
} else {
return null;
}
}
}
2) Writing a method that checks the values of the incoming data before you patch the values to your FormGroup.
this.someService.getData().subscribe(response => {
if (!response.user.Country) {
// trigger invalid validation response
} else {
this.myForm.patchVaue(response.user);
}
});

Related

Angular addValidator function doesn't immediately work, only after entering value into field and deleting it after

I'm using a reactive form with some fields and a toggle button:
export class DashboardComponent implements OnInit {
reminderForm: FormGroup;
public emailReminderType : boolean;
ngOnInit(): void
{
this.emailReminderType = false;
this.reminderForm = this.formBuilder.group({
Message: ["", Validators.required],
EmailAddress: [""],
DateWhenToSend: [""],
TimeWhenToSend: [""],
});
}
and on clicking on toggle element, an event fires that sets a boolean value to either true or false. In this case, when it sets is to true, the email field needs to have validation.
HTML:
<mat-slide-toggle id="switchRemindEmail" (click)="onCheckboxEmailChange($event)">Via Email</mat-slide-toggle>
TS:
onCheckboxEmailChange(e) {
if (this.emailReminderType === false) {
this.emailReminderType = true;
this.reminderForm.get("EmailAddress").addValidators(Validators.required);
} else {
this.emailReminderType = false;
this.reminderForm.get("EmailAddress").clearValidators();
}
}
and finally, the form submit button:
<button mat-raised-button color="primary" type="submit" [disabled]="reminderForm.valid && emailReminderType ? false : true">Remind me!</button>
The problem is, once I click on toggle, the submit button is still enabled even though the Email input field is empty. If I write something into the field and then delete it, only THEN becomes the button disabled.
What am I doing wrong? Why isn't the validation taking effect immediately after setting validation with 'addValidators' ?

Angular 8+ two way binding textarea in reactive forms

I want to bind my textarea input.
HTML:
<form [formGroup]="formGroup">
<custom-element formControlName="firstName"</custom-element>
<textarea formControlName="question" rows="6" placehoder"some text"></textarea>
</form>
TS:
formGroup = this.formBuilder.group({
firstName: [{value: null, disabled: true}],
question:[null]
});
get firstNameControl(): AbstractControl {return this. formGroup.get('firstName');}
get questionControl(): AbstractControl {return this. formGroup.get('question');}
dosomething(): void {
// fill firstname form api work fine
// I can read the value with this.firstNameControl.value
}
Tried to bind textarea but no working solution yet.
message: string;
this.questionControl.valueChanges.subscribe(
val => {
this.message= val;
console.log('questiontext' + this.message);
}
);
or
this.questionControl.valueChanges.pipe(
debounceTime(500),
distinctUntilChanged(),
tap(message => this.message = message),
tap(data => console.log(data))
).subscribe();
if I add [(ngModel)]="message" to the textarea and remove formCotroleName it works.
But than you are combining two way of presenting the form which is not the way to go.
How can I two way bind a textarea or other form element with reactive forms?

How to detect values changes after form builder and patchValue called?

I have this create and edit screen both in separated component. So in order to pump data in edit screen, I need to use patchValue and patch data with all the responses get back from API.
So after patching all values I need to disable the Update button for better user experiences. Then whenever users trigger changes on the form , I will enable the Update button again.
The problem now is I wish to disable the button in form.valueChanges but seems failed because patchValue will trigger valueChanges once so is there anyway I can disable my button after all data been patched?
The boolean that control the button is isFormNoChanges stated below
constructor(
private readonly router: Router,
private readonly formBuilder: FormBuilder,
private readonly agentService: AgentService,
) {
this.agentService.getAgentMLRDetails().then((data: AgentList) => {
this.agentDetails = data;
this.injectValue(this.agentDetails);
})
}
ngOnInit() {
this.onChanges();
}
injectValue(data: AgentMLRList) {
this.mlrForm.patchValue({
entity: data.entityCode,
agentCode: data.agentCode,
productCode: data.product.productCode,
coverageCode: data.coverage.coverageCode,
startDate: data.startDate,
endDate: data.endDate
})
}
onChanges() {
this.mlrForm.valueChanges
.subscribe(data => {
this.isFormNoChanges = false
})
}
Hey do this and use skip operator as below :-
this.mlrForm.valueChanges
.pipe(skip(1)).subscribe(data => {
this.isFormNoChanges = false
})
or use the solution i mentioned in comments above.
or use patchValue like
patchValue({
entity: data.entityCode,
agentCode: data.agentCode,
productCode: data.product.productCode,
coverageCode: data.coverage.coverageCode,
startDate: data.startDate,
endDate: data.endDate
}, { onlySelf: true, emitEvent: false })

Cannot Bind Dynamic Data in Component Angular 8

Error when component loading dynamic
DynamicBuilderComponent.ngfactory.js:198 ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ng-pristine: true'. Current value: 'ng-pristine: false'.
Problem
after binding json in select2data to select2 component Angular throw exception.
component code
#Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'select2',
Imported changeDetection in component.
template: `
<div [formGroup]="form">
<ng-container>
<ng-select2
[data]="select2data"
[options]="options"
[width]="500"
[formControlName]="field.code"
(keyup)="changed($event.target.value)">
</ng-select2>
</ng-container>
</div>`
})
select2 component class
export class Select2Component implements OnInit {
#Input() field: any = {};
#Input() form: FormGroup;
public exampleData: Array<Select2OptionData>;
public options: Options;
public value: string[];
select2data: any;
public selected: string;
constructor(public cl: Services,private cd: ChangeDetectorRef) {
this.options = {
width: '258',
multiple: true,
tags: false
};
}
Problem Area After Binding subscribe data in ng select2 component
changed(search: any) {
//call service pass search text to service
return this.cl.searchFunc(search).subscribe(
res1 =>
this.select2data = res1.data;
this.cd.markForCheck(); // marks path
}
}
},
error => {
console.log('error = ', error);
});
}
}
i tried to print this.select2data in console.log its return me json.
Vendor.js
function expressionChangedAfterItHasBeenCheckedError(context, oldValue, currValue, isFirstCheck) {
var msg = "ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '" + oldValue + "'. Current value: '" + currValue + "'.";
if (isFirstCheck) {
msg +=
" It seems like the view has been created after its parent and its children have been dirty checked." +
" Has it been created in a change detection hook ?";
}
return viewDebugError(msg, context);
}
Great Article
https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html
Reference
Expression ___ has changed after it was checked
any suggestion is most welcome.
I believe that you put your component select2 inside another component which contains a form which you then pass to select2 for create another <form> tag, is that correct? I mean do you have something like that?
<form [formGroup]="form">
<!-- Some code -->
<select2 [field]="something" [form]="form"></select2>
</form>
If so, then your select2 component SHOULD NOT contain re-declaration of form, it should not contain anything related to forms at all. It should be a form control. Please read a post by Netanel Basal on how to create custom form controls. You will need to create ControlValueAccessor for your select2 and wire it up to Angular forms through a custom provider.
The issue you're facing is that since you include form object twice in the DOM data changes are propagated twice as well and you run into issues. There should be only one reference to a specific instance of FormGroup in your templates.
Solution that worked
#Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'select2',
export class Select2Component implements OnInit {
constructor(public cl: Services,private cd: ChangeDetectorRef) {
this.options = {
width: '258',
multiple: true,
tags: false
};
}
Binding function
changed(search: any) {
//call service pass search text to service
return this.cl.searchFunc(search).subscribe(
res1 =>
this.select2data = res1.data;
this.cd.markForCheck(); // marks path
this.cd.detectChanges();
}
}
},
error => {
console.log('error = ', error);
});
}

Angular 4 : How to use custom validator with data from back-end

In a form, the button submit becomes enable only when the form is valid. A particular input contains datalist with data from back-end. I want to return the form invalid if data filled by the user is not in the datalist. So, i need a custom validator which check if user data is equal to data from back end.
Data from back-end is a list of objects listOfArticles which contains reference numbers and other data.
So, I try to create a function for custom validator in the same component file, but it doesn't work for 2 reasons :
I don't know how to retrieve the path of my listOfArticles "refer.refNumber.input"
I get error in my console : "Cannot read property 'length' of undefined".
Function for custom validator :
export function ValidateRefNumber(control: AbstractControl) {
for (let refer of ArbologistiqueComponent.listOfArticles) {
if (control.value == refer.refNumber.input) {
return true;
}
}
return null;
}
My entire component.ts :
import { Component, OnInit } from '#angular/core';
import 'rxjs/add/operator/switchMap';
import { ManagementArbologistiqueService } from "../management-arbologistique.service";
import { ActivatedRoute, Params } from '#angular/router';
import { FormGroup, FormControl, FormBuilder, FormArray, Validators } from '#angular/forms';
#Component({
selector: 'app-arbologistique',
templateUrl: './arbologistique.component.html',
styleUrls: ['./arbologistique.component.css']
})
export class ArbologistiqueComponent implements OnInit {
private reponseTest: String;
private listOfArticles :Array<Object>
private pathDownload: any;
private myFormGroup: FormGroup;
fileToUpload: File = null;
private buttonSubmitEnabled: boolean = false;
constructor(public fb: FormBuilder, private managementArbo: ManagementArbologistiqueService, private route: ActivatedRoute) { }
ngOnInit() {
this.myFormGroup = this.fb.group({
itemRows: this.fb.array([this.initItemRows()])
})
this.myFormGroup.valueChanges.subscribe(x => this.buttonSubmitEnabled = false);
this.getListBdd();
}
initItemRows() {
return this.fb.group({
... //other fields
refNb: ['',[Validators.required, ValidateRefNumber]],
... //other fields
})
}
addRow(index: number) {
console.log("functionAddRow called");
const control = <FormArray>this.myFormGroup.controls['itemRows'];
control.insert(index, this.initItemRows());
}
deleteRow(index: number) {
console.log("functionDeleteRow called");
const control = <FormArray>this.myFormGroup.controls['itemRows'];
control.removeAt(index);
}
sendForm() {
this.buttonSubmitEnabled=true;
console.log("functionExportCalled");
this.route.params.subscribe((params: Params) => {
let subroute = "exportation";
this.managementArbo.postProducts(subroute, JSON.stringify(this.myFormGroup.value))
.subscribe(
res => { this.reponseTest = res; console.log('reponse:' + res); }
,
err => console.log(err),
() => console.log('getProducts done'));
});
}
getListBdd() {
this.route.params.subscribe((params: Params) => {
let subroute = "getRefNumber";
this.managementArbo.getProducts(subroute)
.subscribe(
res => { this.listOfArticles = res; console.log('reponse:' + res); }
,
err => console.log(err),
() => console.log('getProducts done'));
});
}
get refNb() {
return this.myFormGroup.get('itemRows.refNb');
}
}
export function ValidateRefNumber(control: AbstractControl) {
for (let refer of ArbologistiqueComponent.listOfArticles) {
if (control.value == refer.refNumber.input) {
return true;
}
}
return null;
}
input with datalist (component.html) :
<input list="refNumbers" formControlName="refNb" type="text" name="article" maxlength="8" size="15" required title="8 characters" />
<datalist id="refNumbers">
<option *ngFor="let ref of listOfArticles">{{ref.refNumber.input}}</option>
</datalist>
I will provide you an answer with a simple example, then you can transform it as you need.
Lets say we have this list of articles:
articles = [
{
id: 1,
content: "test 123"
},
{
id: 2,
content: "test 345"
}
];
And we want to check if the user type in the input text one of the id's of the articles, otherwise, the form is invalid.
We have this little piece of HTML:
<div *ngFor="let article of articles">
{{article | json}}
</div>
<form [formGroup]="cForm" (submit)="submitForm(cForm.value)" novalidate>
<input formControlName="article" type="text" name="article" required />
<input type="submit" value="submit" [disabled]="!cForm.valid">
</form>
So I just print the array for debug, and we have input which has formControlName of article. and we have a button which is disable if the form is invalid.
Init the formGroup:
this.cForm = this._fb.group({
article: [
null,
Validators.compose([Validators.required, matchValues(this.articles)])
]
});
So we have a formGroup with simple validation of required, and our new validator named matchValues.
export const matchValues = (valuesToCheck: any[]): ValidatorFn => {
return (control: AbstractControl): { [key: string]: boolean } => {
const controlValue = control.value;
let res = valuesToCheck.findIndex(el => el.id === +controlValue);
console.log(res);
return res !== -1 ? null : { matched: true };
};
};
I simply wrap it in a function which received the array of articles, and I'm trying to find an article id which is matching the value of the input text. If I dont find any, I'm returning an object:
{ matched: true }
which is the error of the control, so you can access this error with 'matched' same as you access required or minlength or any other error.
I hope its clear enough and gives you a nice kick off to solve your a bit more complicated problem.
--UPDATE--
To make it more clear:
findIndex searching in the array for the first item that match the condition and return the index of the item or -1 if nothing found, in this case, I've check if the value of the input from the user match any ID from the list. so as long as my res is not equal to -1, thats mean that I found an item that match so the validation pass, but if res === -1 there is an error, so I send new object with the error name matched so you can handle it later. anyway in this case, I didn't find any item that matched the id, so its an error.
--UPDATE2--
You got some trouble, So I've included a working example.
Check this out