Angular ReactiveForms, despite their problems, are a powerful tool for encoding form validation rules reactively.
Single Source of Truth for Validation RulesYour backend code should be the single source of truth for validation rules. Of course we should validate input in the UI for a better user experience.
Likely we are either implementing the same rules from the same spec, or copying what has been implemented in the API, or layers behind that. We should be asking ourselves, Where should the single source of truth for validation rules live? It probably shouldn't be in the Angular app. We can eliminate this manual duplication by generating Angular ReactiveForms from OpenAPI/Swagger specs, rather than hand coding them.
This eliminates bugs where the validation rules between the UI and API fall out of sync -- or copied incorrectly from the backend to the frontend. When new validation rules change, just rerun the command to generate the reactive form from the updated OpenAPI spec.
This works very well in conjunction with Rest API proxies generated using the openapi-generator.
PrerequisiteIf your projects do not meet following prerequisite, you can still use the Quick Start.
In order for this to work with your Rest API you will need to have your backend provide a well formed Swagger (OpenAPI 2) or OpenAPI 3 spec that includes model-metadata for validation expressed as type
, format
, pattern
, minLength
, maxLength
, etc. Frameworks such as SwashbuckleCore and SpringFox (and many others) do this for you based on metadata provided using attributes or annotations.
This quick start uses a hosted swagger spec, so if you can still go through it whether or not your API exposes the required model-metadata.
First, let's create a new app.
npm i -g @angular/cli
ng n example
cd example
Enter fullscreen mode Exit fullscreen mode
Second, install the generator into your Angular project as a dev dependancy.
npm install --save-dev @verizonconnect/ngx-form-generator
Enter fullscreen mode Exit fullscreen mode
Third, update your package.json
scripts
to include a script to generate the form. This way when the API changes we can easily rerun this script to regenerate the form.
{
. . .
"scripts": {
. . .
"generate:address-form": "ngx-form-generator -i https://raw.githubusercontent.com/verizonconnect/ngx-form-generator/master/demo/swagger.json -o src/app/"
},
. . .
}
Enter fullscreen mode Exit fullscreen mode
Now run the script.
npm run generate:address-form
Enter fullscreen mode Exit fullscreen mode
Within your src/app
you will now have a new generated file based on the name of the OpenAPI title
property, in this case myApi.ts
. You can change this using the -f
argument and provide the filename you like.
We can now import the form into a component and expose it to the template.
import { Component } from '@angular/core';
import { addressModelForm } from './myApi'; // <- import the form
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
addressForm = addressModelForm; // <- expose form to template
}
Enter fullscreen mode Exit fullscreen mode
We will add Angular Material to our project to provide styles for our form in this example. Of course there is no dependency on any CSS or component library.
ng add @angular/material
Enter fullscreen mode Exit fullscreen mode
In the module lets import ReactiveFormModule
, MatFormFieldModule
and MatInputModule
.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ReactiveFormsModule } from '@angular/forms'; // <- ESM imports start
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
ReactiveFormsModule, // <- NgModule imports start
MatFormFieldModule,
MatInputModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode
We will build the form. For demonstration purposes only need the first field.
<form [formGroup]="addressForm">
<mat-form-field>
<mat-label>First Name</mat-label>
<input matInput formControlName="firstName">
<mat-error *ngIf="addressForm.controls.firstName.invalid">This field is invalid</mat-error>
</mat-form-field>
</form>
Enter fullscreen mode Exit fullscreen mode
We can now run this with ng serve
and see our single field form.
An invalid name can be entered into the field and we will see this validated based on the validation rules from exposed to the Angular application through the OpenAPI spec.
Here we can enter a valid name and see that the validation updates.
If this were a real RestAPI we could add the rest of our form fields and go.
Isolate Generated Code into a LibraryWe can improve this by putting the generated form into its own library within the Angular workspace. The benefits of this are:
Create a new library
ng g lib address-form
Enter fullscreen mode Exit fullscreen mode
We can now remove the scaffolded component, service and module from the lib.
rm -fr projects/address-form/src/lib/*
Enter fullscreen mode Exit fullscreen mode
We are placing only generated code into this library. We do not want to create unit tests for generated code. Tests should live with the code generator itself. So lets get rid of the unit test support files.
rm projects/address-form/karma.conf.js
rm projects/address-form/tsconfig.spec.json
rm projects/address-form/src/test.ts
Enter fullscreen mode Exit fullscreen mode
We don't need to lint generated code, so lets get rid of tslint.json
rm projects/address-form/tslint.json
Enter fullscreen mode Exit fullscreen mode
We need to remove the references to the test and lint files in the workspace angular.json
. Open angular.json
and find "address-form": {
property. Remove the "test": {
and "lint": {
sections.
It should then look something like this.
"address-form": {
"projectType": "library",
"root": "projects/address-form",
"sourceRoot": "projects/address-form/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"options": {
"tsConfig": "projects/address-form/tsconfig.lib.json",
"project": "projects/address-form/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "projects/address-form/tsconfig.lib.prod.json"
}
}
}
}
}
Enter fullscreen mode Exit fullscreen mode
In package.json
we need to add a script to build our lib as well as update the path where we generate the form.
"generate:address-form": "ngx-form-generator -i https://raw.githubusercontent.com/verizonconnect/ngx-form-generator/master/demo/swagger.json -o projects/address-form/src/lib",
"build:libs": "ng build address-form"
Enter fullscreen mode Exit fullscreen mode
Now lets generate the form into the lib.
npm run generate:address-form
Enter fullscreen mode Exit fullscreen mode
Now replace all of the exports in proects/address-form/src/public-api.ts
with:
export * from './lib/myApi';
Enter fullscreen mode Exit fullscreen mode
Build the lib with:
npm run build:libs
Enter fullscreen mode Exit fullscreen mode
We can remove the old generated myApi.ts
rm src/app/myApi.ts
Enter fullscreen mode Exit fullscreen mode
Last, update the import of the form
import { addressModelForm } from 'address-form';
Enter fullscreen mode Exit fullscreen mode
Generating forms will allow you to keep your validation rules in sync with the backend. Like generating proxies, this will greatly reduce integration bugs that occur by trying to manually implement backend to UI data contracts.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4