Angular template inside reactive form - html

In our application I noticed lots of duplicate code in the HTML of our forms, most input elements have the same structure so I want to create a generic input component that we can use instead to keep the files clean.
The problem I currently have is that there is 1 form that has 2 nested formGroups inside, example:
this.addressForm = this.fb.group({
postalAddress: this.fb.group({
street: ["", [Validators.required]],
}),
visitorAddress: this.fb.group({
street: [""],
})
});
This leads to my new component's HTML to also have duplicate code due to some forms requiring a formGroupName.
<div [formGroup]="form" *ngIf="form && controlPath && controlName">
<div *ngIf="groupName" [formGroupName]="groupName">
<mat-form-field class="w-full">
<mat-label>{{ label }}</mat-label>
<input type="text" matInput [formControlName]="controlName" *ngIf="type === 'text'">
<input type="number" matInput [formControlName]="controlName" *ngIf="type === 'number'">
<mat-error *ngFor="let message of form.get(controlPath)?.errors?.['messages']">{{ message | i18n }}</mat-error>
</mat-form-field>
</div>
<div *ngIf="!groupName">
<mat-form-field class="w-full">
<mat-label>{{ label }}</mat-label>
<input type="text" matInput [formControlName]="controlName" *ngIf="type === 'text'">
<input type="number" matInput [formControlName]="controlName" *ngIf="type === 'number'">
<mat-error *ngFor="let message of form.get(controlPath)?.errors?.['messages']">{{ message | i18n }}</mat-error>
</mat-form-field>
</div>
The code above works fine but as mentioned, I would like to get rid of the duplicate code. I figured this would be a good case for a ng-template but it appears when using a template the nested controls can no longer find the surrounding FormGroup.
Example
<div [formGroup]="form" *ngIf="form && controlPath && controlName">
<div *ngIf="groupName" [formGroupName]="groupName">
<ng-content *ngTemplateOutlet="content"></ng-content>
</div>
<div *ngIf="!groupName">
<ng-content *ngTemplateOutlet="content"></ng-content>
</div>
<ng-template #content>
<mat-form-field class="w-full">
<mat-label>{{ label }}</mat-label>
<input type="text" matInput [formControlName]="controlName" *ngIf="type === 'text'">
<input type="number" matInput [formControlName]="controlName" *ngIf="type === 'number'">
<mat-error *ngFor="let message of form.get(controlPath)?.errors?.['messages']">{{ message | i18n }}</mat-error>
</mat-form-field>
</ng-template>
Error:
Has anyone encountered such a situation and if so, what would be a good way to go about this?
UPDATE
After Garbage Collectors answer I refactored my code to the following:
<div [formGroup]="form" *ngIf="form && controlPath && controlName">
<div *ngIf="groupName" [formGroupName]="groupName">
<ng-container [ngTemplateOutlet]="inputTemplate" [ngTemplateOutletContext]="{ templateForm: form, templateFormGroup: groupName, templateControlName: controlName }"></ng-container>
</div>
<div *ngIf="!groupName">
<ng-container [ngTemplateOutlet]="inputTemplate" [ngTemplateOutletContext]="{ templateForm: form, templateControlName: controlName }"></ng-container>
</div>
<ng-template #inputTemplate let-form="templateForm" let-groupName="templateFormGroup" let-controlName="templateControlName">
<div [formGroup]="form" [formGroupName]="groupName">
<mat-form-field class="w-full">
<mat-label>{{ label }}</mat-label>
<input type="text" matInput [formControlName]="controlName" *ngIf="type === 'text'">
<input type="number" matInput [formControlName]="controlName" *ngIf="type === 'number'">
<mat-error *ngIf="form.get(controlPath)?.errors?.['required']">{{ 'error.required' | i18n }}</mat-error>
<mat-error *ngIf="form.get(controlPath)?.errors?.['email']">{{ 'error.email' | i18n }}</mat-error>
<mat-error *ngIf="form.get(controlPath)?.errors?.['invalid']">{{ 'error.form.invalid' | i18n }}</mat-error>
<mat-error *ngIf="form.get(controlPath)?.errors?.['minlength']">{{ minLengthError }}</mat-error>
<mat-error *ngIf="form.get(controlPath)?.errors?.['pattern']">{{ patternError }}</mat-error>
<mat-error *ngFor="let message of form.get(controlPath)?.errors?.['messages']">{{ message | i18n }}</mat-error>
</mat-form-field>
</div>
</ng-template>
Though the template now has all variables correctly, it cannot find the control path for non-nested forms. I outputted controlName in the template as a test and it is correct. I expect the error occurring due to the formGroupName being null for non-nested forms.
Error:

As far as I remember, every isolated piece of code displaying form controls should have a reference to the form where those controls were defined.
Actions
Use ng-container instead of ng-content to include your templates
Pass form as a parameter to your template
Assign form parameter to [formGroup] attribute inside your template
Template
<ng-template #demoTemplate let-form="demoForm">
<div [formGroup]="form">
<!-- Insert the rest of your template here -->
</div>
</ng-template>
Main component using template
<div [formGroup]="form" *ngIf="form">
<ng-container
[ngTemplateOutlet]="demoTemplate"
[ngTemplateOutletContext]="{ demoForm: form }">
</ng-container>
</div>
Notice that attribute formGroup is added in both places, in the template and in the parent.

Use [formControl] instead of formControlName
You can pick nested variables via the get command, for instance form.get('Address.Street')
More explanations: https://stackoverflow.com/a/74668699/6019056

Related

input event not triggering for dynamic fields on pasting in Angular

I have a form with dynamically generated input fields. While pasting content to the fields I need a preview of the input contents on a div. I am using Angular 11.
Here is my .ts file
quickSendForm = this.fb.group({
parameters: this.fb.array([]),
})
Here is my .html file
<form [formGroup]="quickSendForm" #formDirective="ngForm" (ngSubmit)="submit()">
<div>
<mat-form-field>
<textarea matInput placeholder="To number" rows="5" maxlength="1300" name="toNumber" id="toNumber" formControlName="toNumber" ></textarea>
</mat-form-field>
</div>
<div *ngFor="let param of parameters.controls; let i=index" class="col-md-3">
<mat-form-field>
<input matInput type="text" placeholder="Parameter- {{ i + 1 }} " name="parameter" id="showCustomParameter{{ i + 1 }}" formControlName="parameter">
</mat-form-field>
</div>
<div>--input content here --</div>
I am getting the content of toNumber field but not getting for parameter field
<input> tag isn't properly closed. But I used template reference to show input value there. Could just as easily use formControl value though.
<mat-form-field>
<input matInput #myInput type="text" placeholder="Parameter- {{ i + 1 }} "
name="parameter" id="showCustomParameter{{ i + 1 }}"
formControlName="parameter">
</mat-form-field>
<div> {{ myInput.value }}</div>

How to trigger mat chip error validation?

I'm doing a error validation for a field with mat-chip, I'm able to pop up the error validation message only after user add a single mat-chip value, then remove the value. I want to make it where when user click on the field then go to a next field of the form without adding any value, it'll already trigger the error message. Below is my code snippet
HTML :
<form [formGroup]="positionForm">
<div fxLayout="column" fxLayoutAlign="space-between stretch">
<mat-form-field appearance="outline">
<mat-label>Title</mat-label>
<input matInput formControlName="title" type="text" placeholder="Title" required>
<mat-error *ngIf="positionForm.controls.title.invalid">This field is required.</mat-error>
</mat-form-field>
<mat-form-field class="chip-list" appearance="outline" (click)="formValue()">
<mat-label>Skills *</mat-label>
<mat-chip-list #chipSkills>
<mat-chip *ngFor="let skill of positionForm.get('skills').controls; let i = index" [selectable]="selectable"
[removable]="removable" (removed)="removeSkills(i)" (click)="formValue()">
{{skill.value}}
<mat-icon class="remove-chip" matChipRemove *ngIf="removable">cancel</mat-icon>
</mat-chip>
<input placeholder="Enter Skills..."
[matChipInputFor]="chipSkills"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="addOnBlur"
(matChipInputTokenEnd)="addEditSkill($event)" required>
</mat-chip-list>
<mat-error *ngIf="positionForm.controls.skills.errors">Atleast 1 skill need to be added </mat-error>
</mat-form-field>
</form>
TS :
title = new FormControl('', [Validators.required,this.customValidator.NoWhiteSpaceValidator]);
skills = new FormArray([], [Validators.required, this.validateArrayNotEmpty] );
validateArrayNotEmpty(chip: FormControl) {
if (chip.value && chip.value.length === 0) {
return {
validateArrayNotEmpty: { valid: false }
};
}
return null;
}

Angular 8 Material mat-error shows up on load by default

I am having this weird scenario. my mat-error shows up by default, without even touching the input field. Basically, when I load the page the error message is shown without any user interaction on the input field. I don't have any error messages on the browser console. Can anyone point out what did I miss?
This is my .ts
frmStep3: FormGroup;
matcher = new MyErrorStateMatcher();
// omited unrelated code
this.frmStep3 = this.fb.group({
courseDurationNum: [, [Validators.required, Validators.maxLength(3), Validators.minLength(1)]]
});
return {
frmStep3: this.frmStep3
};
This is my HTML
<form class="form-group-parent p-0" [formGroup]="frmStep3" (ngSubmit)="stepForward()">
<div class="form-group d-inline-block">
<label class="w-100">Choose your course duration<sup>*</sup></label>
<input [errorStateMatcher]="matcher" [maxLength]="3" type="text" class="float-left" matInput
formControlName="courseDurationNum" placeholder="Ex. 1 - 365 Day(s)" />
<mat-error class="w-100 float-left"
*ngIf="frmStep3.get('courseDurationNum').hasError('required') || frmStep3.get('courseDurationNum').invalid">
<strong>Duration</strong> must be between <strong>1 - 365</strong> day(s).
</mat-error>
</div>
</form>
I figured out, I missed wrapping them in from controls in <mat-form-field> tag. Correct code below.
<form class="form-group-parent p-0" [formGroup]="frmStep3" (ngSubmit)="stepForward()">
<div class="form-group d-inline-block">
<mat-form-field>
<label class="w-100">Choose your course duration<sup>*</sup></label>
<input [errorStateMatcher]="matcher" [maxLength]="3" type="text" class="float-left" matInput
formControlName="courseDurationNum" placeholder="Ex. 1 - 365 Day(s)" />
<mat-error class="w-100 float-left"
*ngIf="frmStep3.get('courseDurationNum').hasError('required') || frmStep3.get('courseDurationNum').invalid">
<strong>Duration</strong> must be between <strong>1 - 365</strong> day(s).
</mat-error>
</mat-form-field>
</div>
</form>

formGroup expects a FormGroup instance. Please pass one in. How to fix this error?

My HTML source code as follows,
<p>
<mat-form-field appearance="legacy" [formGroup]="form" (submit)="onFormSubmit()">
<mat-label>Add a List</mat-label>
<input [(ngModel)]="listName" matInput placeholder="Placeholder" formControlName="name" #nameInput fxFlex>
<mat-icon matSuffix class="check" (click)="onFormSubmit()">check</mat-icon>
<mat-icon matSuffix class="clear" (click)="closeForm()">clear</mat-icon>
</mat-form-field>
</p>
when running this code following error shows on the console,
ERROR Error: formGroup expects a FormGroup instance. Please pass one in.
Example:
<div [formGroup]="myGroup">
<input formControlName="firstName">
</div>
In your class:
this.myGroup = new FormGroup({
firstName: new FormControl()
});
how can I fix this error?
Typo ? You wrote:
[formGroup]="form"
It should be:
[formGroup]="myGroup"

Angular Material Input and select inside one form field in single row

I want a material input field and a material select in one line(inside one form field). To get it done I wrote the below code but it goes into two rows. How can I get this input and drop-down in one line?.
Expected result:
Frontend view
My html code :
<div fxLayout="column" class="mat-elevation-z8">
<mat-form-field class="p-1">
<input matInput placeholder="Search table..."
(keyup)="updateFilter($event)">
<mat-select name="ampm" [(ngModel)]="selectedtablesearch" (selectionChange)="updateFilter($event)">
<mat-option *ngFor="let draft_tblselect of draft_tblselects"
[value]="draft_tblselect.viewValue">{{draft_tblselect.viewValue}}</mat-option>
</mat-select>
</mat-form-field>
</div>
I've sorted this issue.
Code:
<div fxLayout="row" class="mat-elevation-z8">
<div fxFlex="80" class="p-2">
<mat-form-field class="w-100">
<input matInput placeholder="Search table..." (keyup)="updateFilter($event)">
</mat-form-field>
</div>
<div fxFlex="20" class="p-2">
<mat-form-field class="w-100">
<mat-select [(ngModel)]="selectedtablesearch" (selectionChange)="updateFilter($event)">
<mat-option *ngFor="let draft_tblselect of draft_tblselects"
[value]="draft_tblselect.viewValue">{{draft_tblselect.viewValue}}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>