Today, I encountered a problem with angular routing after successful validation of Angular reactive form. All I want to achieve is validate passwords fields in my form and in case success redirect toward another component ('/customers')
or in case that passwords are not the same we should do nothing and give a chance to the user to retype proper passwords again.
RegistrationComponent.ts
@Component({
selector: 'app-registration',
templateUrl: './registration.component.html',
styleUrls: ['./registration.component.css']
})
export class RegistrationComponent implements OnInit {
registerForm: FormGroup;
submitted = false;
email$: Observable<string>;
constructor(private formBuilder: FormBuilder, private customerService: CustomerService, private router: Router) {
}
ngOnInit() {
this.registerForm = this.formBuilder.group({
username: ['', Validators.required],
email: ['', Validators.required],
name: ['', Validators.required],
surname: ['', Validators.required],
phoneNumber: ['', Validators.required],
nip: ['', Validators.required],
password: ['', Validators.required],
confirmPassword: ['', Validators.required],
}, {
validator: MustMatch('password', 'confirmPassword')
});
}
get form() {
return this.registerForm.controls;
}
onSubmit() {
this.submitted = true;
const registeredCustomer: RegistrationForm = {
username: this.registerForm.controls.username.value,
email: this.registerForm.controls.email.value,
name: this.registerForm.controls.name.value,
surname: this.registerForm.controls.surname.value,
phoneNumber: this.registerForm.controls.phoneNumber.value,
password: this.registerForm.controls.password.value,
confirmPassword: this.registerForm.controls.confirmPassword.value,
};
this.email$ = this.customerService
.register(registeredCustomer)
.pipe(map(customer => customer.email));
if (this.registerForm.invalid) {
return;
} else {
setTimeout(() => this.router.navigate((['/customers'])), 5000);
}
alert('User successfully registered' + JSON.stringify(this.registerForm.value));
}
}
RegistrationComponent.html
<div class="jumbotron">
<div class="container">
<div class="row">
<div class="col-md-6 offset-md-3">
<h3>Fill in the form below to complete the registration process </h3>
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label>Username</label>
<input type="text" formControlName="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && form.username.errors }" />
<div *ngIf="submitted && form.username.errors" class="invalid-feedback">
<div *ngIf="form.username.errors.required">Username is required</div>
</div>
</div>
<div class="form-group">
<label>Name</label>
<input type="text" formControlName="name" class="form-control" [ngClass]="{ 'is-invalid': submitted && form.name.errors }" />
<div *ngIf="submitted && form.name.errors" class="invalid-feedback">
<div *ngIf="form.name.errors.required">Customer name is required</div>
</div>
</div>
<div class="form-group">
<label>Surname</label>
<input type="text" formControlName="surname" class="form-control" [ngClass]="{ 'is-invalid': submitted && form.surname.errors }" />
<div *ngIf="submitted && form.surname.errors" class="invalid-feedback">
<div *ngIf="form.surname.errors.required">Customer surname is required</div>
</div>
</div>
<div class="form-group">
<label>Phone Number</label>
<input type="text" formControlName="phoneNumber" class="form-control" [ngClass]="{ 'is-invalid': submitted && form.phoneNumber.errors }" />
<div *ngIf="submitted && form.phoneNumber.errors" class="invalid-feedback">
<div *ngIf="form.phoneNumber.errors.required">Phone number is required</div>
</div>
</div>
<div class="form-group">
<label>Email</label>
<input type="text" formControlName="email" class="form-control" [ngClass]="{ 'is-invalid': submitted && form.email.errors }" />
<div *ngIf="submitted && form.email.errors" class="invalid-feedback">
<div *ngIf="form.email.errors.required">Email is required</div>
<div *ngIf="form.email.errors.email">Email must be a valid email address</div>
</div>
</div>
<div class="form-group">
<label>Password</label>
<input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && form.password.errors }" />
<div *ngIf="submitted && form.password.errors" class="invalid-feedback">
<div *ngIf="form.password.errors.required">Password is required</div>
<div *ngIf="form.password.errors.minlength">Password must be at least 6 characters</div>
</div>
</div>
<div class="form-group">
<label>Confirm Password</label>
<input type="password" formControlName="confirmPassword" class="form-control" [ngClass]="{ 'is-invalid': submitted && form.confirmPassword.errors }" />
<div *ngIf="submitted && form.confirmPassword.errors" class="invalid-feedback">
<div *ngIf="form.confirmPassword.errors.required">Confirm Password is required</div>
<div *ngIf="form.confirmPassword.errors.mustMatch">Passwords must match</div>
</div>
</div>
<div class="form-group">
<button class="btn btn-primary">Register</button>
</div>
</form>
</div>
</div>
</div>
</div>
I was based on this tutorial: Registration tutorial
but after small modification, my current solution is not working properly.
I was trying to replace this
setTimeout(() => this.router.navigate((['/customers'])), 5000);
infront of:
if (this.registerForm.invalid) {
return;
}
and checking if boolean value submitted = false
but it didn't yield the desired result.
Thank you in advance for all your help.
Edit: Stackblitz link for application.
We've been working at work on a library to handle forms easily and with also more type safety (for both TS and HTML!). This library is called ngx-sub-form
and you can find it here: https://github.com/cloudnc/ngx-sub-form
Using ngx-sub-form
, I've made a demo of your app that you can try here:
https://stackblitz.com/edit/angular-genpiv
Now, some explanations.
First, we want to define a proper interface for your form (type safety for the win!):
export interface RegistrationForm {
username: string;
email: string;
name: string;
surname: string;
phoneNumber: string;
nip: string;
password: string;
confirmPassword: string;
}
Then we create all the components we need for the app:
- app/app.component.ts
- app/registration/registration.component.ts
- app/registration/registration-form/registration-form.component.ts
- app/customers.component.ts
Now we need to define the routing for our app. Our app.module.ts
should look like the following:
const ROUTES: Routes = [
{
path: '',
redirectTo: '/registration',
pathMatch: 'full'
},
{
path: 'registration',
component: RegistrationComponent
},
{
path: 'customers',
component: CustomersComponent
},
];
@NgModule({
imports: [BrowserModule, ReactiveFormsModule, RouterModule.forRoot(ROUTES)],
declarations: [AppComponent, RegistrationFormComponent, RegistrationComponent, CustomersComponent],
bootstrap: [AppComponent]
})
export class AppModule { }
And app.component.html
should simply be <router-outlet></router-outlet>
.
RegistrationComponent
will act as a smart component. It will inject the router and just wait for the form to be sent. That component doesn't want to be aware of the form itself, just the object that has been sent.
@Component({
selector: 'app-registration',
templateUrl: './registration.component.html',
styleUrls: ['./registration.component.css']
})
export class RegistrationComponent {
constructor(private router: Router) {}
public register(registrationForm: RegistrationForm): void {
// here you probably want to inject a `RegistrationService` instead of the `Router`
// and call a method that will make an XHR call to your backend and on success
// would do that `navigate` from the service too
// for the sake of simplicity here I'm just doing the navigate directly
this.router.navigate(['/customers']);
}
}
And its HTML:
<app-registration-form (register)="register($event)"></app-registration-form>
RegistrationFormComponent
will be the component responsible to handle the form. That component is the only one that needs to use ngx-sub-form
library.
@Component({
selector: 'app-registration-form',
templateUrl: './registration-form.component.html',
styleUrls: ['./registration-form.component.css']
})
export class RegistrationFormComponent extends NgxSubFormComponent<RegistrationForm> {
@Output() register: EventEmitter<RegistrationForm> = new EventEmitter();
protected getFormControls(): Controls<RegistrationForm> {
return {
username: new FormControl(null, [Validators.required]),
email: new FormControl(null, [Validators.required]),
name: new FormControl(null, [Validators.required]),
surname: new FormControl(null, [Validators.required]),
phoneNumber: new FormControl(null, [Validators.required]),
nip: new FormControl(null, [Validators.required]),
password: new FormControl(null, [Validators.required]),
confirmPassword: new FormControl(null, [Validators.required]),
}
}
protected getFormGroupControlOptions(): FormGroupOptions<RegistrationForm> {
return {
validators: [
formGroup => {
if (formGroup.value.password !== formGroup.value.confirmPassword) {
return {
passwordsMustMatch: true,
};
}
return null;
},
],
};
}
public onSubmit(): void {
this.register.emit(this.formGroupValues)
}
}
Things to notice here:
export class RegistrationFormComponent extends NgxSubFormComponent<RegistrationForm>
we're extending NgxSubFormComponent
and we pass our form interface. That will provide us a lot utilities and also some type safety
The method getFormControls
expects you to basically provide an object to create your form. I think it's self explanatory because it looks like the object you'd pass when creating a FormGroup
getFormGroupControlOptions
is a hook provided by NgxSubFormComponent
that allows you to set validators or asyncValidators at the FormGroup
level
Finally, the onSubmit
method is the one that'll be called when the user clicks on the Register
button (once the form is fully valid)
Now, the last missing piece is the form HTML (for simplicity I'll only display in the response the first field and the password check because everything in between is pretty much the same)
<div class="jumbotron">
<div class="container">
<div class="row">
<div class="col-md-6 offset-md-3">
<h3>Fill in the form below to complete the registration process</h3>
<form [formGroup]="formGroup" (ngSubmit)="onSubmit()">
<div class="form-group">
<label>Username</label>
<input type="text" [formControlName]="formControlNames.username" class="form-control" [ngClass]="{ 'is-invalid': formGroupErrors?.username }" />
<div *ngIf="formGroupErrors && formGroupControls.username.touched" class="invalid-feedback">
<div *ngIf="formGroupErrors?.username?.required">Username is required</div>
</div>
</div>
...
<div class="form-group">
<label>Confirm Password</label>
<input type="text" [formControlName]="formControlNames.confirmPassword" class="form-control" [ngClass]="{ 'is-invalid': formGroupErrors?.confirmPassword }" />
<div *ngIf="formGroupErrors && formGroupControls.confirmPassword.touched" class="invalid-feedback">
<div *ngIf="formGroupErrors?.confirmPassword?.required">Confirm Password is required</div>
<div *ngIf="formGroupErrors?.formGroup?.passwordsMustMatch">Passwords must match</div>
</div>
</div>
<div class="form-group">
<button class="btn btn-primary" [disabled]="formGroup.invalid">Register</button>
</div>
</form>
</div>
</div>
</div>
</div>
Things to notice from the HTML:
<form [formGroup]="formGroup"
we haven't defined the formGroup
ourselves. It's been created by NgxSubFormComponent
<input type="text" [formControlName]="formControlNames.username"
the formControlNames
property is also defined by NgxSubFormComponent
and its main purpose is to bring some type safety! If you try to put formControlNames.random
you'll get a run time error, but also a Typescript error when compiling with AOT!
<div *ngIf="formGroupErrors?.username?.required">
the property formGroupErrors
is also provided by NgxSubFormComponent
and gives you access to the form errors. Best part is that it works with nested sub forms and is also type safe! Even though here we don't have any sub form, I invite you to check the Github page of ngx-sub-form
to learn more about that
The app is now fully ready and has a lot of type safety! Any refactor impacting the main interface will also require you to update the form (TS + HTML) otherwise Typescript will throw errors around.
Here's how it looks like:
Then once it's valid:
And then when we click on register:
Don't forget to check out the live example here: https://stackblitz.com/edit/angular-genpiv
Edit:
If you want to go further, I've just published a blog post to explain a lot of things about forms and ngx-sub-form here https://dev.to/maxime1992/building-scalable-robust-and-type-safe-forms-with-angular-3nf9
To activate the routing you must inject the routing module in the app.module.ts (ie root module). Also define the path for the route in your routing module file as :
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{path: 'customers' , component: CustomersComponent}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Aslo don't forgrt to include <router-outlet></router-outlet>
tag because its where the child view will be loaded after the routing is successfull.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.