I'm having problems with a form that working on.
I'm using MaterializeCSS with Angular and in order to make sure the select will be properly initiated i'm using the following method:
ngAfterViewInit() {
const selArray = this.select.toArray();
selArray.forEach(el => {
M.FormSelect.init(el.nativeElement);
});
}
That's working fine, my problem is that I'm using FormArray and when I create a new FormControl dynamically, the select doesn't work. It's like it's not initialized.
I added the code above inside my addResident() method and it doesn't work but if I have to add again, then it works.
Here is video of the error:
https://youtu.be/v1CHkmJtzCo
Here is the code:
#ViewChildren('select') select: QueryList<ElementRef>;
ngAfterViewInit() {
const selArray = this.select.toArray();
selArray.forEach(el => {
M.FormSelect.init(el.nativeElement);
});
const dpArray = this.datePicker.toArray();
dpArray.forEach(element => {
M.Datepicker.init(element.nativeElement);
});
}
addResident() {
(this.hostForm.get('residents') as FormArray).push(
new FormGroup({
firstName: new FormControl(''),
lastName: new FormControl(''),
birthDate: new FormControl(''),
action: new FormControl('Insert'),
residentID: new FormControl(0),
relationship: new FormControl('')
})
);
const selArray = this.select.toArray();
selArray.forEach(el => {
M.FormSelect.init(el.nativeElement);
});
}
I would suggest to use Angular Material to avoid problems with MaterializeCSS.
https://material.angular.io/
Material Design components for Angular
Angular Material: Forms
Angular Material: Form-Field
Have a look at the Form-Field examples:
<div class="example-container">
<mat-form-field>
<input matInput placeholder="Input">
</mat-form-field>
<mat-form-field>
<textarea matInput placeholder="Textarea"></textarea>
</mat-form-field>
<mat-form-field>
<mat-select placeholder="Select">
<mat-option value="option">Option</mat-option>
</mat-select>
</mat-form-field>
</div>
Related
I am trying to create a form in which once an option is selected in the first field, this selection gives the options of the second field, which is a mat-autocomplete, also when value from the mat-autocomplete is selected, it is filled with mat-chips.
The problem is that I am not able to make the second field disabled until the first field is filled.
I have tried both different ways, like the indicated by the console warning:
"It looks like you're using the disabled attribute with a reactive form directive. If you set disabled to true
when you set up this control in your component class, the disabled attribute will actually be set in the DOM for
you. We recommend using this approach to avoid 'changed after checked' errors.
Example:
// Specify the `disabled` property at control creation time:
form = new FormGroup({
first: new FormControl({value: 'Nancy', disabled: true}, Validators.required),
last: new FormControl('Drew', Validators.required)
});
// Controls can also be enabled/disabled after creation:
form.get('first')?.enable();
form.get('last')?.disable();"
I tried the response of this question too:
Angular mat-autocomplete disable input FormControl not working
But in the end I had to do it like this (that give me the warning):
HTML
<div class="modal-descriptors" [formGroup]="filterValueForm">
<mat-form-field appearance="outline" class="form-field">
<mat-label>tag</mat-label>
<mat-select (selectionChange)="onFilterChange($event)" formControlName="filterSelectCtrl" required>
<mat-option *ngFor="let filter of filters" [value]="filter.code">{{ filter.code }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="modal-descriptors">
<mat-label>condition</mat-label>
<mat-select formControlName="filterConditionCtrl" required>
<mat-option value="true">{{ apply$ | async }}</mat-option>
<mat-option value="false">{{ notApply$ | async }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="descriptors">
<mat-label>{{ values }}</mat-label>
<mat-chip-list #toChipList required>
<mat-chip
class="descriptors_label"
*ngFor="let filterValue of selectedFiltersValue"
[selectable]="selectable"
[removable]="removable"
(removed)="removeSelectedFilterValue(filterValue)">
{{ filterValue.value }}
<mat-icon class="iconicon" matChipRemove *ngIf="removable">close</mat-icon>
</mat-chip>
<input type="text"
matInput
#filterValueInput
formControlName="filterValueCtrl"
[matAutocomplete]="autoTo"
[matChipInputFor]="toChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="addOnBlur"
(matChipInputTokenEnd)="add($event)"
[disabled] = "isFilterValueSelected"
>
</mat-chip-list>
<mat-autocomplete #autoTo="matAutocomplete" (optionSelected)="selected($event)">
<mat-option *ngFor="let filterValue of filteredValues" [value]="filterValue">{{ filterValue.value }}</mat-option>
</mat-autocomplete>
</mat-form-field>
TS
ngOnInit() {
this.filterValueForm = this.fb.group({
filterSelectCtrl: [null, Validators.required],
filterConditionCtrl: [null, Validators.required],
filterValueCtrl: [{value: '', disabled: true}, Validators.required], /*this object inside array do nothing*/
})
};
get isFilterValueSelected() {
return !this.filterValueForm.get('filterSelectCtrl').value ? true : false;
}
Does anyone know what is wrong or knows a better way to do it?
Disable the second field on the formGroup declaration.
this.filterValueForm = this.fb.group({
firstField: ['', Validators.required],
secondField: [{value: '', disabled: true}, Validators.required],
})
Then, when you select an option from the autocomplete, you are calling the selected($event) function from your ts. In this function, you can enable the second field like this:
selected() {
\*Do whatever logic you want to do when an autocomplete element is selected*\
this.filterValueForm.controls['secondField'].enable();
}
Edit:
You are overriding the ts configuration on the html with the line
<input type="text"
matInput
#filterValueInput
formControlName="filterValueCtrl"
[matAutocomplete]="autoTo"
[matChipInputFor]="toChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="addOnBlur"
(matChipInputTokenEnd)="add($event)"
[disabled] = "isFilterValueSelected" <---
>
I found a solution.
Dont know why but when creating the formGroup like this dont work:
filterValueCtrl: [{value: '', disabled: true}, Validators.required]
And after try some other solutions I found that one:
this.filterValueForm.controls.filterValueCtrl.disable();
But dont know why dont work if its used after create the formGroup that its inside ngOnInit()
So I made method that its triggered when the mouse is over the <mat-form-field>
TS:
enableOrDisableField(){
this.filterValueForm.get('filterSelectCtrl').value ? this.filterValueForm.controls.filterValueCtrl.enable() : this.filterValueForm.controls.filterValueCtrl.disable();
}
HTML:
<mat-form-field appearance="outline" (mouseenter)="enableOrDisableField()">
...
...
<mat-form-field>
Maybe its not the best solution but its working and no warning at console...so I think its fine for me.
Hope can help someone, and tanks to all who have tryed to help.
I am trying to implement a reactive angular form where either A or B has to be entered. A is a unique id and B is a set of values which identify the id. Now I try to validate a Form that is valid if either A is entered or B is entered including all the required values. I found several solutions that implement this behavior based on FormFields but was not able to get it working with the group of values.
<form class="container" [formGroup]="myForm" (ngSubmit)="onSubmit()">
<mat-form-field class="w-1/2">
<mat-label>ID</mat-label>
<input matInput type="number" formControlName="id">
</mat-form-field>
<div class="grid grid-cols-3 gap-4" formGroupName="innerGroup">
<mat-form-field>
<mat-label>First Name</mat-label>
<input matInput type="number" formControlName="firstName">
</mat-form-field>
<mat-form-field>
<mat-label>Last Name</mat-label>
<input matInput type="number" formControlName="lastName">
</mat-form-field>
</div>
</form>
My first idea was to override the default validator for the form but I could not figure out how to do that. Not even sure if it would be possible. I was trying to adjust https://stackoverflow.com/a/48714721 to work in my scenario but I had no idea how to get it to work because of the additional complexity with the inner form group.
Using angular 14 I was able to produce a similar result to what you are describing I am not sure it will 100% solve your issue however it might help.
Basically what I did was create a validator function that is to be applied at the group level. This validator will check the valid state of any given controls be they a FormGroup or a FormControl. However, this alone will not solve the problem as if you have a form group angular will see that any underling control or group that is invalid will also invalidate the parent. So what I did was call .disable() on any control that was being checked by the validator function. This will disable the UI element and disable validation checking by angular allowing the parent to be considered valid when one of the children is valid but the other is invalid effectively creating a one and only one validator.
My specific example I was trying to get the OnlyOneValidator to work for a MatStepper.
Validator
export function onlyOneValidator(controlKeys: string[]) {
return (control: AbstractControl): ValidationErrors | null => {
let countOfValidControls = 0;
for (let key of controlKeys) {
const controlToCheck = control.get(key);
if (controlToCheck === null || controlToCheck === undefined) {
throw new Error(`Error: Invalid control key specified key was ${key}`);
}
countOfValidControls += controlToCheck?.valid ? 1 : 0;
}
if (countOfValidControls !== 1) {
// the count is not exactly one
return {
onlyOneValid: {
actualValidCount: countOfValidControls,
expectedValidCount: 1
}
};
}
return null;
};
}
Controller
#Component({
selector: "app-equipment-creation-page",
templateUrl: "./equipment-creation-page.component.html",
styleUrls: ["./equipment-creation-page.component.scss"],
})
export class EquipmentCreationPageComponent implements OnInit, OnDestroy {
public categories = [null, "Tools", "Vehicles"];
constructor(private _formBuilder: FormBuilder) {}
public categoryInformationGroup = this._formBuilder.group({
existingCategory: this._formBuilder.group({
category: new FormControl(null, [ Validators.required ])
}),
newCategory: this._formBuilder.group({
name: new FormControl("", [Validators.required]),
description: new FormControl("", [Validators.required])
})
}, {
validators: [
onlyOneValidator(["existingCategory", "newCategory"])
],
});
public ngOnDestroy(): void {
this.subscriptions.forEach(sub => {
sub.unsubscribe();
});
}
private subscriptions: Subscription[] = [];
public ngOnInit(): void {
this.subscriptions.push(this.categoryInformationGroup.controls.existingCategory.statusChanges.pipe(
tap((status: string) => {
if (status === "VALID") {
this.categoryInformationGroup.controls.newCategory.disable();
} else {
this.categoryInformationGroup.controls.newCategory.enable();
}
})
).subscribe());
this.subscriptions.push(this.categoryInformationGroup.controls.newCategory.statusChanges.pipe(
tap((status: string) => {
if (status === "VALID") {
this.categoryInformationGroup.controls.existingCategory.disable();
} else {
this.categoryInformationGroup.controls.existingCategory.enable();
}
})
).subscribe());
}
}
Template
<form [formGroup]="categoryInformationGroup.controls.existingCategory">
<mat-form-field>
<mat-label>Apply to existing category?</mat-label>
<mat-select formControlName="category">
<mat-option *ngFor="let category of categories" [value]="category">
{{ category ?? "None" }}
</mat-option>
</mat-select>
</mat-form-field>
</form>
OR
<form [formGroup]="categoryInformationGroup.controls.newCategory">
<mat-form-field>
<mat-label>Create New Category</mat-label>
<input matInput formControlName="name" placeholder="Name">
<mat-error *ngIf="categoryInformationGroup.controls.newCategory.controls.name.hasError('required')">This field
is required
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Create New Category</mat-label>
<input matInput formControlName="description" placeholder="Description">
<mat-error *ngIf="categoryInformationGroup.controls.newCategory.controls.description.hasError('required')">
This field is required
</mat-error>
</mat-form-field>
</form>
Hopefully this helps or at least gives you some ideas about how to approach this. If anyone else has any thoughts on this please let me know I would love to find a better way to do this.
I have written below code so far to display the dropdowns. But selected value and options are not appearing in the dropdowns. Where I am doing wrong in the below code.
Component code
testForm: FormGroup;
get ctrls() {
return this.testForm.get('ctrls') as FormArray;
}
data = [{
initial: 'hen',
options: ['hen', 'duck', 'parrot']
},
{
initial: 'lion',
options: ['lion', 'tiger', 'cheetah']
}
];
constructor(private router: Router, private formBuilder: FormBuilder) {
this.testForm = this.formBuilder.group({
ctrls: this.formBuilder.array([])
});
for (let i = 0; i < this.data.length; i++) {
this.ctrls.push(this.formBuilder.control(this.data[i]));
}
}
Template code
<form [formGroup]="testForm">
<div formArrayName="ctrls" *ngFor="let animals of ctrls.controls; let i = index">
<mat-form-field>
<mat-select [formControlName]="i" [(value)]="animals.value.initial">
<mat-select-trigger>{{ animals.value.initial }}</mat-select-trigger>
<mat-option *ngFor="let animal of animals.value.options" [value]="animal">{{ animmal }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</form>
Please guide me in the correct way...
Please find the stackblitz to check the issue
Instead of using the options from the controller, use the options from data.
Also, you need to initialise your formControl using the this.data[i].initial, since the data is the string, not the object.
I also changed the html a bit, using the controller instead of controllerName, since you already have a reference to it. I aslo removed the [value]-attribute in the mat-select because you are setting the value in the component. Using [value] is better suited for template driven instead of reactive forms, and using both may cause side effects.
html:
<form [formGroup]="testForm">
<div *ngFor="let animals of ctrls.controls; let i = index">
<mat-form-field>
<mat-select [formControl]="animals">
<mat-option *ngFor="let animal of data[i].options" [value]="animal">{{ animal }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</form>
And in the ts file:
ngOnInit() {
this.testForm = this.formBuilder.group( {
ctrls : this.formBuilder.array( [] )
});
for( let i = 0; i < this.data.length; i++ ) {
this.ctrls.push( this.formBuilder.control( this.data[ i ].initial ) );
}
}
https://stackblitz.com/edit/angular-material-design-q3wbah
EDIT:
After reading the comments, wanting to have an object as the data in the formControl, I would change the data, using something like this instead.
data = [
{ options : [
{name:'hen', icon: 'someIcon'},
{name:'duck', icon: 'asd'},
{name: 'parrot', icon: 'asdasd'} ] }
];
then, if you need to put in the correct initial object, I would match the name with your initial object, so you get the correct pointer. I have only selected the first element in the example below.
Then, you can use the <mat-select-trigger> as you initially wanted I believe.
https://stackblitz.com/edit/angular-material-design-new-data
You have a typo
animmal
Additionally, you should move the code form the constructor to the ngOnInit method
Check out: https://stackblitz.com/edit/angular-material-design-m491ic?file=src%2Fapp%2Fapp.component.ts
Next, I believe you mixed up the formControls. I slightly changed the whole logic, hopefully you can use it.
Basically, the options need to go out of the formcontrol
<mat-form-field>
<mat-select
[formControl]="animals"
>
<mat-option
*ngFor="let animal of data[i].options; let j = index"
value="{{animal}}"
>{{ animal }}</mat-option>
</mat-select>
</mat-form-field>
And I changed the formgroup definition
this.testForm = this.formBuilder.group( {
ctrls : this.formBuilder.array( [] )
} );
for( let i = 0; i < this.data.length; i++ ) {
this.ctrls.push( new FormControl({value: this.data[i].initial, disabled: false}) );
}
I want to check whether the dropdown is empty.
Need to show the required message and
If not empty, enable the submit button.
If empty, disable the submit button. Below is my html
Below is my html
<form [formGroup]="myForm" (ngSubmit)="save()" >
<mat-form-field>
<mat-select formControlName="name" placeholder="Element List" (selectionChange)="elementSelectionChange($event)" required>
<mat-option *ngFor="let element of Elements" [value]="element.name">
{{ element.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="myForm.hasError('required', 'name')">Please choose an name</mat-error>
</mat-form-field>
<mat-form-field>
<mat-select formControlName="symbol" placeholder="Symbol List" required>
<mat-option *ngFor="let element of selectedElementSymbol" [value]="element.symbol">
{{ element.symbol }}
</mat-option>
</mat-select>
<mat-error *ngIf="myForm.hasError('required', 'symbol')">Please choose an symbol</mat-error>
</mat-form-field>
<div mat-dialog-actions>
<button mat-button (click)="onNoClick()">Cancel</button>
<button type="submit" mat-button cdkFocusInitial>Add</button>
</div>
</form>
below is my component
export class DialogOverviewExampleDialog {
myForm: FormGroup;
symbol = new FormControl('', Validators.required);
name = new FormControl('', Validators.required);
constructor(
public dialogRef: MatDialogRef<DialogOverviewExampleDialog>,
#Inject(MAT_DIALOG_DATA) public data: any,
private formBuilder: FormBuilder) {
this.myForm = this.formBuilder.group({
name: [this.name],
symbol: [this.symbol],
});
}
save() {
console.log(this.myForm.value);
}
}
updated demo here
You are currently assigning formcontrols to your formcontrol, whereas you want to assign value to your form controls. Below you are assigning form control name to formcontrol name:
WRONG:
name = new FormControl('', Validators.required);
this.myForm = this.formBuilder.group({
'name': [this.name, Validators.required],
// ...
});
so instead, just declare name, do what you want with the value, then assign that value to your form control...
CORRECT:
name: string;
this.myForm = this.formBuilder.group({
'name': [this.name, Validators.required],
// ...
});
Then just add [disabled]="!myForm.valid" on your submit button.
As for the other question, by default Angular material doesn't show error message unless the field has been touched, so you need to have a custom error state matcher that shows the error even when field is not touched (if that is what you want):
import {ErrorStateMatcher} from '#angular/material/core';
export class MyErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
return !!(control.invalid);
}
}
and in your TS, declare a error state matcher:
matcher = new MyErrorStateMatcher();
and use in template:
<mat-select formControlName="name" ... [errorStateMatcher]="matcher">
Here's your
StackBlitz
To make the submit button disabled (link)
<button type="submit" [disabled]="!myForm.valid" mat-button cdkFocusInitial>Add</button>
In order to check whether the dropdown is empty or not, you need to make the form fields required like
this.myForm = this.formBuilder.group({
'name': [this.name, Validators.required],
'symbol': [this.symbol, [Validators.required]]
});
Inorder to show the error highlight you need to add an ngClass in the templete like.
[ngClass]="{'error': myForm.controls.name.valid == false}"
You have to insert the validator into the form builder object.
Have a quick look at:
https://angular.io/guide/reactive-forms#validatorsrequired
this.heroForm = this.fb.group({
name: ['', [Validators.required] ],
});
As for the button:
<button type="submit" [disabled]="!form.valid" mat-button cdkFocusInitial>Add</button>
I would create a form with the possibility to add inputs dynamically
I found a question about the same problem in angular 2 but I can't make it working in my exemple
Here's my component ts file :
export class AjoutProjetComponent implements OnInit {
isLinear = false;
firstFormGroup: FormGroup;
secondFormGroup: FormGroup;
constructor(private _formBuilder: FormBuilder) {}
ngOnInit() {
this.secondFormGroup = this._formBuilder.group({
pers: [this._formBuilder.array([this.createItem()])]
});
}
createItem(): FormGroup {
return this._formBuilder.group({
name: ['', Validators.required]
poste: ['', Validators.required],
});
}
addItem(): void {
const control = < FormArray > this.secondFormGroup.controls['pers'];
control.push(this.createItem());
}
}
then HTML file
<mat-step [stepControl]="secondFormGroup">
<form [formGroup]="secondFormGroup">
<ng-template matStepLabel>Constituez votre équipe</ng-template>
<div formArrayName="pers">
<mat-form-field *ngFor="let control of secondFormGroup.controls.pers.controls; let i= index">
<input matInput placeholder="Nom collaborateur" formControlName="name" required>
</mat-form-field>
</div>
</form>
</mat-step>
<div>{{secondFormGroup.value | json}}</div>
When I click in my favorite icon I get this error :
ERROR TypeError: control.push is not a function at AjoutProjetComponent.addItem
How can I make adding dynamically inputs working ?
UPDATE
I have updated my html code so that I could print two inputs but when I run my code I get this error now
ERROR Error: Cannot find control with path: 'pers -> name'
You did not declare your FormArray properly. You use arrays only to initialize simple FormControls, not FormGroups or FormControls, change to :
this.secondFormGroup = this._formBuilder.group({
pers: this._formBuilder.array([this.createItem()]) // remove opening and closing brackets
});
To see the inputs added dynamically to the html, you need to use an ngFor loop. I think you somewhat misunderstood the usage of formArrayName, which only adds context to the template to use with FormArrays. Try this:
<ng-container formArrayName="pers">
<input placeholder="Address"
*ngFor="let control of secondFormGroup.controls.pers.controls"
[formControl]="control.controls.name" required />
</ng-container>
And read more about FormArrayName directive here