Angular Reactive form rendering issue breaking the form - javascript

So I am having a very weird problem with Angular reactive form. I asked my instructor, he couldn't figure it out so I have only one place I am wishing to get some help from. It's here.
So I am using a Angular form and the signup.component.html code snippet is:
<form [formGroup]="form" (submit)="onSaveUser()" *ngIf="!isLoading">
<mat-accordion class="accordion-headers-align">
<mat-expansion-panel [expanded]="1">
<mat-expansion-panel-header>
<mat-panel-title>Personal data</mat-panel-title>
<mat-panel-description>Type your personal details</mat-panel-description>
</mat-expansion-panel-header>
<mat-form-field>
<input matInput type="text" formControlName="name" placeholder="Name">
<mat-error *ngIf="form.get('name').invalid">Please enter your name</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput type="date" formControlName="dob" placeholder="DD/MM/YYYY">
<mat-error *ngIf="form.get('dob').invalid">Please enter your valid date of birth in form of DD/MM/YYYY</mat-error>
</mat-form-field>
and continue like that, ignore the accordion part pls.
Then my signup.component.ts file is:
import { Component, OnInit } from '#angular/core';
import { FormGroup, FormControl, Validators } from '#angular/forms';
import { ActivatedRoute, ParamMap } from '#angular/router';
#Component({
selector: 'app-signup',
templateUrl: './signup.component.html',
styleUrls: ['./signup.component.css']
})
export class SignupComponent implements OnInit {
isLoading = false;
form: FormGroup;
imagePreview: string;
constructor(public userService: UsersService, public route: ActivatedRoute) { }
ngOnInit() {
this.form = new FormGroup({
name: new FormControl(null, {validators: [
Validators.required,
Validators.minLength(3),
// tslint:disable-next-line:quotemark
Validators.pattern(some regex not of concern)
]}),
dob: new FormControl(null, {validators: [
Validators.required,
// tslint:disable-next-line:max-line-length quotemark
Validators.pattern(some regex not of concern)
]}),
and continued like this, nothing special. Just trying to map the form fields. So the form renders like this: The error I am getting in console is:
ERROR TypeError: "this.form is undefined, can't access property "get" of it".
ERROR Error: "formGroup expects a FormGroup instance. Please pass one in.
ERROR TypeError: "_co.form is undefined, can't access property "get" of it".
and I don't understand what is going wrong, I checked the documentation and everything, no help. I am guessing it's a possible bug so wanted to make sure.

I resolve with *ngIf="form", in this way form tag will be rendered only when form is ready.
<form [formGroup]="form" (submit)="onSaveUser()" *ngIf="form">

Related

Angular Material Mat-error Showing error for valid inputs, while validating with regex

Creating a signup form using angular material, It is displaying errors even if there is no error.
For a valid input it is displaying the error Invalid Input.
I've removed that mat-error condition the field is turning red while entering data.
The status inside the control is showing the field is valid on submit.
The error conditions are in in global constants file.
-Global const ts
export class globalConstants{
public static genericErr:string="Someting wnet wrong,Please try again";
public static usrname:string='[a-zA-Z0-9 ]*';
}
HTML-
<mat-form-field appearance="fill" fxFlex>
<mat-label>User Name</mat-label>
<input matInput formControlName="usrname" required>
<mat-error *ngif="signUpForm.controls.usrname.touched && signUpForm.controls.usrename.invalid">
<span *ngIf="signUpForm.controls.usrname.errors.required">User Name is Mandatory</span>
<span *ngIf="signUpForm.controls.usrname.errors.pattern">Ivalid Input</span>
</mat-error>
</mat-form-field>}
Signup Ts File:
import { globalConstants } from '../shared/globalConst';
signUpForm: any = FormGroup;
ngOnInit(): void {
this.signUpForm = this.formBuilder.group({usrname: ['Enter Username', [Validators.required, Validators.pattern(globalConstants.usrname)]],
Kindly assist with your expertise.
Please check the following code
input-error-state-matcher.ts
import { Component } from '#angular/core';
import {
FormGroup,
FormBuilder,
FormControl,
FormGroupDirective,
NgForm,
Validators,
} from '#angular/forms';
/** #title Input with a custom ErrorStateMatcher */
#Component({
selector: 'input-error-state-matcher-example',
templateUrl: './input-error-state-matcher-example.html',
styleUrls: ['./input-error-state-matcher-example.css'],
})
export class InputErrorStateMatcherExample {
userAddressValidations: FormGroup;
constructor(private formBuilder: FormBuilder) {}
ngOnInit() {
this.userAddressValidations = this.formBuilder.group({
firstName: [
'',
[
Validators.required,
Validators.minLength(4),
Validators.maxLength(20),
Validators.pattern('[a-zA-Z]+'),
],
],
});
}
}
input-error-state-matcher.html
<form class="example-form" [formGroup]="userAddressValidations">
<mat-form-field class="example-full-width">
<input matInput placeholder="First Name" formControlName="firstName">
<mat-error *ngIf="userAddressValidations.get('firstName').hasError('required')">
First Name is Required!
</mat-error>
<mat-error *ngIf="userAddressValidations.get('firstName').hasError('minlength')">
First Name should be atleast 4 characters long!
</mat-error>
<mat-error *ngIf="userAddressValidations.get('firstName').hasError('maxlength')">
First Name can be atmax n characters long!
</mat-error>
<mat-error *ngIf="userAddressValidations.get('firstName').hasError('pattern')">
First Name must follow this pattern!
</mat-error>
</mat-form-field>
</form>
working example is here

Angular ng-content not working with mat-form-field

My goal:
I'm trying to build a reusable mat-form-field with a clear button.
How I tried achieving my goal:
I created a "mat-clearable-input" component and used it like this:
<mat-clearable-input>
<mat-label>Put a Number here pls</mat-label>
<input matInput formControlName="number_form_control">
</mat-clearable-input>
mat-clearable-input.component.html
<mat-form-field>
<ng-content></ng-content>
</mat-form-field>
Expected result:
the ng-content tag takes the label and the input and puts them inside the mat-form-field tag.
Actual result:
Error: mat-form-field must contain a MatFormFieldControl.
at getMatFormFieldMissingControlError (form-field.js:226)
at MatFormField._validateControlChild (form-field.js:688)
at MatFormField.ngAfterContentChecked (form-field.js:558)
at callHook (core.js:2926)
at callHooks (core.js:2892)
at executeInitAndCheckHooks (core.js:2844)
at refreshView (core.js:7239)
at refreshComponent (core.js:8335)
at refreshChildComponents (core.js:6991)
at refreshView (core.js:7248)
It looks like I'm missing something and I'm not using correctly the ng-content tag.
I wasn't able to locate the documentation for the ng-content tag on the angular website.
Thank you for any help.
EDIT AFTER ANSWER BELOW
So I tried this suggested method:
export class MatClearableInputComponent implements OnInit {
#ContentChild(MatFormFieldControl) _control: MatFormFieldControl<any>;
#ViewChild(MatFormField) _matFormField: MatFormField;
// see https://stackoverflow.com/questions/63898533/angular-ng-content-not-working-with-mat-form-field/
ngOnInit() {
this._matFormField._control = this._control;
}
}
unfortunately, when I try to use this in a form it still fails with the error "Error: mat-form-field must contain a MatFormFieldControl."
Code where i try to use this component in a form:
<mat-clearable-input>
<mat-label>Numero incarico</mat-label>
<buffered-input matInput formControlName="numero"></buffered-input>
</mat-clearable-input>
Repro on stackblitz: https://stackblitz.com/edit/angular-material-starter-xypjc5?file=app/clearable-form-field/clearable-form-field.component.html
notice how the mat-form-field features aren't working (no outline, no floating label), also open the console and you'll see the error Error: mat-form-field must contain a MatFormFieldControl.
EDIT AFTER OPTION 2 WAS POSTED
I tried doing this:
<mat-form-field>
<input matInput hidden>
<ng-content></ng-content>
</mat-form-field>
It works, but then when i added a mat-label to my form field, like this:
<mat-clearable-input>
<mat-label>Numero incarico</mat-label>
<buffered-input matInput formControlName="numero"></buffered-input>
</mat-clearable-input>
the label is never floating and it's just staying there as a normal span the whole time.
So i tried assigning to the this._matFormField._control._label the content child with the label but that didn't work because _label is private and there is no setter for it.
It looks like I'm out of luck and this can't be done in Angular without going through a lot of effort.
If you have any further ideas feel free to fork the stackblitz and try!
Edit after #evilstiefel answer
the solution works only for native <input matInput>.
When I try replacing that with my custom input component, it doesn't work anymore.
Working setup:
<mat-form-field appClearable>
<mat-label>ID incarico</mat-label>
<input matInput formControlName="id">
</mat-form-field>
Same setup but with my custom "buffered-input" component (not working :( )
<mat-form-field appClearable>
<mat-label>ID incarico</mat-label>
<buffered-input matInput formControlName="id"></buffered-input>
</mat-form-field>
The console logs this error when I click on the clear button:
TypeError: Cannot read property 'ngControl' of undefined
at ClearableDirective.clear (clearable.directive.ts:33)
at ClearButtonComponent.clearHost (clearable.directive.ts:55)
at ClearButtonComponent_Template_button_click_0_listener (clearable.directive.ts:47)
at executeListenerWithErrorHandling (core.js:14293)
at wrapListenerIn_markDirtyAndPreventDefault (core.js:14328)
at HTMLButtonElement.<anonymous> (platform-browser.js:582)
at ZoneDelegate.invokeTask (zone-evergreen.js:399)
at Object.onInvokeTask (core.js:27126)
at ZoneDelegate.invokeTask (zone-evergreen.js:398)
at Zone.runTask (zone-evergreen.js:167)
Another solution is using a directive to implement the behaviour.
import {
AfterViewInit,
Component,
ComponentFactory,
ComponentFactoryResolver,
ContentChild,
Directive,
Injector,
Input,
Optional,
SkipSelf,
TemplateRef,
ViewContainerRef,
} from '#angular/core';
import { MatFormFieldControl } from '#angular/material/form-field';
#Directive({
selector: '[appClearable]'
})
export class ClearableDirective implements AfterViewInit {
#ContentChild(MatFormFieldControl) matInput: MatFormFieldControl<any>;
#Input() appClearable: TemplateRef<any>;
private factory: ComponentFactory<ClearButtonComponent>;
constructor(
private vcr: ViewContainerRef,
resolver: ComponentFactoryResolver,
private injector: Injector,
) {
this.factory = resolver.resolveComponentFactory(ClearButtonComponent);
}
ngAfterViewInit(): void {
if (this.appClearable) {
this.vcr.createEmbeddedView(this.appClearable);
} else {
this.vcr.createComponent(this.factory, undefined, this.injector);
}
}
/**
* This is used to clear the formControl oder HTMLInputElement
*/
clear(): void {
if (this.matInput.ngControl) {
this.matInput.ngControl.control.reset();
} else {
this.matInput.value = '';
}
}
}
/**
* This is the markup/component for the clear-button that is shown to the user.
*/
#Component({
selector: 'app-clear-button',
template: `
<button (click)="clearHost()">Clear</button>
`
})
export class ClearButtonComponent {
constructor(#Optional() #SkipSelf() private clearDirective: ClearableDirective) { }
clearHost(): void {
if (this.clearDirective) {
this.clearDirective.clear();
}
}
}
This creates a directive called appClearable and an optional Component for a fallback-layout. Make sure to add the component and the directive to the declarations-array of your module. You can either specify a template to use for providing the user-interface or just use the ClearButtonComponent as a one-size-fits-all solution. The markup looks like this:
<!-- Use it with a template reference -->
<mat-form-field [appClearable]="clearableTmpl">
<input type="text" matInput [formControl]="exampleInput">
</mat-form-field>
<!-- use it without a template reference -->
<mat-form-field appClearable>
<input type="text" matInput [formControl]="exampleInput2">
</mat-form-field>
<ng-template #clearableTmpl>
<button (click)="exampleInput.reset()">Marked-Up reference template</button>
</ng-template>
This works with and without a ngControl/FormControl, but you might need to adjust it to your use-case.
Update:
Option 1 does not work for new angular versions because #ViewChild() returns undefined in ngOnInit() hook. Another hack is to use a dummy MatFormFieldControl -
Option 2
<mat-form-field>
<input matInput hidden>
<ng-content></ng-content>
</mat-form-field>
Edit:
That error is thrown because MatFormField component queries the child content using #ContentChild(MatFormFieldControl) which does not work if you use nested ng-content (MatFormField also uses content projection).
Option 1 (deprecated)
Below is how you can make it work -
#Component({
selector: 'mat-clearable-input',
template: `
<mat-form-field>
<ng-content></ng-content>
</mat-form-field>
`
})
export class FieldComponent implements OnInit {
#ContentChild(MatFormFieldControl) _control: MatFormFieldControl<any>;
#ViewChild(MatFormField) _matFormField: MatFormField;
ngOnInit() {
this._matFormField._control = this._control;
}
}
Please checkout this stackBlitz. Also, there is this issue created in github already.
As of Angular 14 in 2022, the issue for the MatFormField has been closed against angular/angular#37319, without a solution. If you need this to work, the following seems to be the best solution that's possible now to use <ng-content> with mat-form-field:
#Component({
selector: 'my-input-wrapper',
template: `
<mat-form-field appearance="standard">
<ng-content></ng-content>
<!-- make sure this is destroyed so all bindings/subscriptions are removed-->
<input *ngIf="isBeforeViewInit$$ | async" hidden matInput />
</mat-form-field>
`,
});
class MyInputWrapper implement AfterViewInit {
isBeforeViewInit$$ = new BehaviorSubject(true);
#ContentChild(MatFormFieldControl) matFormControl: MatFormFieldControl<unknown>;
#ViewChild(MatFormField) matFormField: MatFormField;
ngAfterViewInit() {
// replace the reference to the dummy control
this.matFormField._control = this.matFormControl;
// force the form field to rebind everything to the actual control
this.matFormField.ngAfterContentInit();
this.isBeforeViewInit$$.next(false);
}
}

How to make a Value to lowercase in mat-form

I have a mat-form from where I want user to enter some values and then I want to submit those values.However I want one of the field to be converted to lowercase and then be submitted.How can I achieve that.
HTML Code:
<mat-form-field appearance="fill">
<mat-label>Hive Table</mat-label>
<input
matInput
formControlName="hiveTable"
/>
</mat-form-field>
Typescript Code:
this.generalInfoForm = new FormGroup({
hiveTable: new FormControl('', [Validators.required]),
});
What I have tried so far
<mat-label>Hive Table</mat-label>
<input
matInput
oninput="this.value = this.value.toLowerCase()"
(keyup.enter)="sendit($event.target.value)"
formControlName="hiveTable"
/>
</mat-form-field>
I used oninout and using keyup I did console.log and in console the values are getting converted tolowercase however on submitting it the value changes back to the way the user typed.
What am I doing wrong.
why not use in input
style="text-transform: lowercase"
Then, before submit you can (I imagine you has a function submit())
submit(form:FormGroup)
{
if (form.valid)
{
form.value.yourField=form.value.yourField.toLowerCase()
console.log(form.value)
}
}
You can use a Directive as shown below:
import { Directive, HostListener, ElementRef, Renderer2 } from '#angular/core';
#Directive({
selector: '[lowercase]'
})
export class LowercaseDirective {
constructor(private renderer: Renderer2, private elementRef: ElementRef) {}
#HostListener('keyup') onKeyUp() {
const nativeElement = this.elementRef.nativeElement;
nativeElement.value = nativeElement.value.toLowerCase();
}
}
And in your template, this needs to be defined as follows:
<input matInput ... lowercase/>
Please have a look at this StackBlitz

I am getting the following error: ERROR TypeError: Cannot read property 'invalid' of undefined

I have tried other solutions such as fixing form references but that didn't help me. Here is my form for a login
<form [formGroup]="longinForm" (ngSubmit)="onSubmit()">
<h1>Login Form</h1>
<div class="form-group" >
<label for="name" >Username</label>
<input type="text" name ="user" [(ngModel)]="loginuserdata.user"
#user="ngModel"
class="form-control" required >
<div *ngIf="submitted">
<div [hidden]="user.valid || user.pristine" class="alert alert-danger">
Username is required
</div>
</div>
</div>
<div class="form-group" >
<label for="pass" >Password</label>
<input type="text" name="pass" [(ngModel)]="loginuserdata.pass" #pass="ngModel"
class="form-control" required >
<div *ngIf="submitted">
<div [hidden]="pass.valid || pass.pristine" class="alert alert-danger">
Password is required
</div>
</div>
</div>
<button type="submit" class="btn btn-success">Submit</button>
</form>
also here is my login component
import { Component, OnInit, ViewChild } from '#angular/core';
import { NgForm, FormGroup } from '#angular/forms';
import { Router } from '#angular/router';
import { LoginService } from './login.service';
import { ILoginData } from './login-data';
#Component({
selector: 'app-loginform',
templateUrl: './loginform.component.html',
styleUrls: ['./loginform.component.css']
})
export class LoginformComponent implements OnInit {
loginForm:FormGroup;
loginuserdata : any[] = [];
error:string;
submitted=false;
constructor(private route: Router,private service:LoginService) { }
get f(){return this.loginForm.controls;}
ngOnInit() {
this.onSubmit();
}
onSubmit(){
if(this.loginForm.invalid){return;}//form invalid stop here
//form is valid do something
this.submitted=true;
if(this.f.user.value == "Admin" && this.f.pass.value == "Apassword"){
this.service.getAdmin()
.subscribe(data => this.loginuserdata = data)
this.route.navigateByUrl('admin');
err => this.error = err;
console.log(this.error);
}
}
}
If you guys need to see any other pieces of my code let me know please help me out I tried something similar to an answer solved to this but it didn't work for me.
You need to build the form controls, you have it defined but you need something like the following. You need to initialise the form group.
import { NgForm, FormGroup, FormBuilder } from '#angular/forms';
//Be sure to import formBuilder and inject through constructor. This will allow you
//to manage how you will build the form and add validation.
//Its a helper class provided by Angular to help us explicitly declare forms.
constructor( private formBuilder: FormBuilder ){}
public ngOnInit(): void {
this.loginForm = this.formBuilder.group({
username: ['', Validators.required],
password: ['', Validators.required]
});
This is where you can define the controls and the validators for the form group. (This example would just dictate that there needs to be a value for each in order to be valid. This should be built in the ngOnInit.
Then in your template you can remove the [(ngModel)] and make it look like the following. The value for the input will then be held within the control when you need to access it.
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<input type="text" name ="user" [formControlName]="'username'"
class="form-control" required>
<form>
Above would serve as an example for the login username / email.
Also as mentioned in the comments you have a typo in the form in the HTML.
This should then be valid when you are trying to access the valid and invalid properties of the from group. Comment if you have any trouble.
Here is the Angular documentation for reactive forms.
How about you do this.
ngAfterViewInit() {
this.onSubmit()
}
I think it was not yet defined at ngOninit.
Check Life Cycle.

Angular 5 FormBuilder errors undefined when invalid input

[As a Newbie tried to put this into plnkr, but couldn't; problems getting #angular/forms added to json.]
Purpose: to iron out things I need to know to do all my work in FormBuilder
HTML:
<input type="text"
formControlName="bucket"
value="A"
[(ngModel)]="AllData.bucket" />
// added to button to test: [disabled]="this.myTestForm.invalid || this.myTestForm.pristine"
<div><button
type="submit">submit</button></div>
</form>
<div *ngIf="result.length > 0 ">{{result}}</div>
<div *ngIf="myTestForm.errors === true ">ERRORS FOUND</div>
Running the app: shows the formbuilder in the ts below initializes the input field correctly and if I add the [disabled] expression commented above it disables button correctly.
Here's the ts:
import {Component, OnInit} from '#angular/core';
import {Validators, FormBuilder, FormGroup} from '#angular/forms';
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
myTestForm: FormGroup;
result: string;
AllData = { //// wired to ngModel
bucket: '12'
}
constructor(private fb: FormBuilder) {
this.result = '';
}
ngOnInit() {
this.myTestForm = this.fb.group({
bucket: ['starting value', [Validators.required, Validators.minLength(5)]]
<-- ngModel bucket above overrides this init value as expected -->
});
}
onSubmit(value: any) { // ways to show results
this.result = this.AllData.bucket;
// OR //
this.result = this.myTestForm.controls['bucket'].value;
// OR //
this.result = this.myTestForm.get('bucket').value;
}
}
The app inits with '12' in the input field as expected. No matter what I put into the textbox before pressing the submit button, devTools always shows the myTestForm 'error' property as undefined.
I'm expected errors to have some sort of string(s) based on the type of error(s) that is occurring.
Further, I scoured the web for ways to capture invalid fields as soon as the error occurs (of course for !pristine fields), but I couldn't get anything to work.
Any help will be much appreciated.
Thanks in advance,
Chuck
I have created a small demo to provide a suggestion on your approach
Do not use [(ngModel)] when your are using reactive forms approach, as ngModel will take precedence over the formControl and set its value to the control irrespective of formcontrol's value, that you have initialized.
<form [formGroup]="myTestForm" >
<input type="text"
formControlName="bucket"
value="A" />
<div><button
[disabled]="myTestForm.invalid || myTestForm.pristine"
type="submit" >submit</button></div>
</form>
To check form errors, use hasError() on controls
<div *ngIf="myTestForm.get('bucket').hasError('required')">Input is required</div>
<div *ngIf="myTestForm.get('bucket').hasError('minlength')">Min length should be 5</div>

Categories

Resources