By: David Barreto
Many enterprise applications use complex forms with even more complex validation rules. Out of the box, Angular offers a solution to handle forms including its validation. But even though the solution is feature rich, it can’t cover every possible use case. What if we want to count how many fields are invalid in any given section of our application? What if we want to validate a field depending on the value of another component’s field?
After building so many scalable applications, we’ve learned that it’s a good idea to use Redux, as we have written about earlier. It’s even a better idea if we also connect our forms, including validation, to Redux as well. But if we do this, how do we do validation? We can take a DIY approach, but it gets messy quickly. This is why ngrx-forms is such a great solution.
ngrx-forms
ngrx-forms
is a library that allow us to move the data and validation of our forms to Redux. It does that by re-imagining how forms should be implemented, but at the same time following a similar structure as Angular’s reactive form approach.
To show how to use this library and the benefits that we get by moving validation to the store, we are going to create a simple application that has two forms living in different components:
- Person: A simple form with
firstName
,lastName
andage
. - Config: A configuration form that sets the minimum value required for the person
age
field.
An example of how the applications behaves is shown in the image below:
Defining the Models
The first step is to define the models that we are going to be working with. In this case, we need to define models for Person
and Config
.
export interface Person {
firstName: string;
lastName: string;
age: number;
}
export interface Config {
minAge: number;
}
We then switch our attention to the shape of the state. In this case an interface that we have named AppState
.
import { FormGroupState } from 'ngrx-forms';
export interface AppState {
form: FormGroupState;
}
export interface RootForm {
person: Person;
config: Config;
}
Here we are modeling our application state as having a single property called form
. This property is of type FormGroupState<RootForm>
which means that it’s a form that’s going to store the data and validation for the models Person and Config. Even though the actual forms for both models are going to be shown in different components, we are going to combine all the forms into a single root form. This will allow us to do cross component validations.
Notice the name and structure of the FormGroupState?
Angular provides a similar class called FormGroup
that we can use to create forms as well. Both shapes are very similar as can be seen in the following code snippets:
Note: The code has been simplified for the sake of clarity, the actual type definition is slightly more complex
FormGroup (@angular)
class FormGroup extends AbstractControl {
value: any;
valid: boolean;
invalid: boolean;
errors: ValidationErrors;
controls: { [key: string]: AbstractControl };
...
}
FormGroupState (ngrx-forms)
interface FormGroupState extends AbstractControlState {
value: T;
isValid: boolean;
isInvalid: boolean;
errors: ValidationErrors;
controls: { [key: string]: AbstractControlState<T[key]> };
...
}
Our hard earned knowledge of Angular’s API is mostly transferable to ngrx-forms
.
Reducers
Because we only have one property in our state it is only logical that we are going to have one reducer.
export const rootReducer = {
form: formReducer,
};
Instead of creating the reducing function ourselves, we delegate that task to the library.
import { createFormGroupReducerWithUpdate, updateGroup, validate } from 'ngrx-forms';
import { required, greaterThanOrEqualTo } from 'ngrx-forms/validation';
export const formReducer = createFormGroupReducerWithUpdate({
person: updateGroup({
firstName: validate(required),
lastName: validate(required),
}),
config: updateGroup({
minAge: validate([required, greaterThanOrEqualTo(0)]),
}),
});
We use the function createFormGroupReducerWithUpdate
to define the validation rules of our form and we get back a reducer function that we can use in our rootReducer
. Notice that for the properties person and config we are using another function, updateGroup
. This function allows us to define those properties as nested forms. This is going to be important once we implement the components to display the form fields. This is because we are going to have a component displaying the fields for the Person
model and another for Config
. Therefore, it makes sense to treat them as nested forms that could be displayed independently.
The validation rules of every field is defined by the validate
function that expects a single or an array of validation functions. These functions are very simple and follow a similar logic as the built in Angular validation system. If the field is valid, the function should return null
and if the field is invalid it should return an error
object. Luckily for us, the library provides some basic validation functions that we can use out of the box like required
and greaterThanOrEqual
.
Normally, when creating a reducer, we define the initial value of the state but because we are delegating the creation of the actual reducer function to the library, where do we define the initial state of the reducer?
Initial State
To create the initial state of the application, we are going to delegate the process to the library once again.
import { createFormGroupState } from 'ngrx-forms';
export const initialFormState = createFormGroupState('form', {
person: {
firstName: '',
lastName: '',
age: null,
},
config: {
minAge: 21,
},
});
export const initialState: AppState = {
form: initialFormState,
};
Now that we have all the moving pieces of our redux store, it’s time to register our reducer in our app. To do so, we are going to need to import the module NgrxFormsModule
and StoreModule
.
import { StoreModule } from '@ngrx/store';
import { NgrxFormsModule } from 'ngrx-forms';
import { rootReducer, initialState } from './store';
@NgModule({
imports: [
NgrxFormsModule,
StoreModule.forRoot(rootReducer, { initialState }),
...
],
...
})
export class AppModule {}
Notice how we pass the initialState
object as the second argument of the StoreModule.forRoot
method. This is required as we cannot directly define the initial state at the reducer level, we have to do it at the application level.
At this point we are ready to use our forms in components.
Components
Our application consists in only three components, the RootComponent
the PersonComponent
and the ConfigComponent
. In the RootComponent
we define a very simple navigation system.
@Component({
selector: 'app-root',
template: `
<h1>NgRx Forms Example</h1>
<nav>
<a routerLink="/counter">Counter</a>
<a routerLink="/person">Person</a>
<a routerLink="/config">Config</a>
<nav>
<router-outlet></router-outlet>
`,
})
export class RootComponent {}
I’m not going to show the routing module as it’s not the focus of this article, but if you want, you can take a look at the file in the repo.Then we move to the PersonComponent
where we define our first form.
@Component({
selector: 'app-person',
template: `
<form novalidate [ngrxFormState]="personForm$ | async">
<label>First Name:</label>
<input type="text" [ngrxFormControlState]="(personForm$ | async).controls.firstName" />
<label>Last Name:</label>
<input type="text" [ngrxFormControlState]="(personForm$ | async).controls.lastName" />
<label>Age:</label>
<input type="number" [ngrxFormControlState]="(personForm$ | async).controls.age" />
</form>
`,
})
export class PersonComponent {
personForm$: Observable<FormGroupState<Person>>;
constructor(private store: Store<AppState>) {
this.personForm$ = store.pipe(
select(state => <FormGroupState<Person>>state.form.controls.person),
);
}
}
There are a couple of things to notice here. First, because our form lives in the store, we need to select the relevant FormGroupState
that we wish to use in this component. In this case, we grab the person
form group. The extra type cast define in the select is due to the lack of mapped conditional types in Typescript.
Second, instead of using any of the Angular tools to deal with forms, we are using directives from ngrx-forms
. In particular, we use the directive NgrxFormState
to define the form and NgrxFormControlState
to define the fields. These two directives are analogous to the Angular directives FormGroup
and FormControl
used for reactive forms.
Finally, we can use the property controls to access the individual FormControls
attached to a particular FormGroup
. This is similar to how Angular defines the form structure.
In the ConfigComponent
we follow a similar approach but starting with the config form group from our store.
@Component({
selector: 'app-config',
template: `
<form novalidate [ngrxFormState]="configForm$ | async">
<label>Min Age:</label>
<input type="number" [ngrxFormControlState]="(configForm$ | async).controls.minAge" />
</form>
`,
})
export class ConfigComponent {
configForm$: Observable<FormGroupState<Config>>;
constructor(private store: Store<AppState>) {
this.configForm$ = store.pipe(
select(state => <FormGroupState<Config>>state.form.controls.config),
);
}
}
We are ready to see our app in action:
Once again, following Angular’s footsteps, the library offers handy CSS classes that we can use to decorate our fields and the form itself based on its status. We can take advantage of that and define a simple global class to provide a visual cue to the user that the validation is failing.
input.ngrx-forms-dirty.ngrx-forms-invalid {
outline: 1px solid red;
}
With this simple change it is now easy to see the invalid state of our application forms.
That’s a half decent form, but is that all we get for the extra effort of learning a new library? Of course not, this is when the interesting part begins.
Taking Advantage of the Store
Now that the form values, state and validation live in the store we can cope with more complex requirements.
Suppose we want to count (and show) the number of fields that are invalid inside the PersonComponent
and ConfigComponent
. We could easily extract this information from the store using a selector. In order to build this selector, we have to take a look at how ngrx-forms stores error information.
Every form group has its own errors
object where the objects returned from the validators are stored. In the case of the person
form group, we only applied the requiredvalidator
for the fields firstName
and lastName
. So, when the field is empty, the error object { required: true }
is stored but when the field has any value (so it’s valid) that error object is removed. We can then build a selector that counts the number of invalid fields by inspecting the errors object.
@Injectable()
export class InvalidFieldsSelector {
personErrors$: Observable;
configErrors$: Observable;
constructor(private store: Store) {
this.personErrors$ = store.pipe(
select(state => countValidationErrors(state.form.controls.person)),
);
this.configErrors$ = store.pipe(
select(state => countValidationErrors(state.form.controls.config)),
);
}
}
function countValidationErrors(control: AbstractControlState): number {
return (control.isPristine) ? 0 : Object.keys(control.errors).length;
}
Notice that in our logic, we also check the property isPristine
to reset the counter to zero. We don’t want to count error messages if the user has not yet interacted with the nested form.
We can now make good use of this selector in the RootComponent
to show the number of invalid fields for both components.
@Component({
selector: 'app-root',
template: `
<h1>NgRx Forms Example</h1>
<nav>
<a routerLink="/person">Person ({{ personErrors$ | async }})</a>
<a routerLink="/config">Config ({{ configErrors$ | async }})</a>
<nav>
<router-outlet></router-outlet>
`,
})
export class RootComponent {
public personErrors$: Observable<number>;
public configErrors$: Observable<number>;
constructor(private invalidFieldsSelector: InvalidFieldsSelector) {
this.personErrors$ = invalidFieldsSelector.personErrors$;
this.configErrors$ = invalidFieldsSelector.configErrors$;
}
}
We are finally showing what’s possible when the validation is in our store state. Creating the same feature with Angular’s built in tools would have been very difficult to do since validation is tightly coupled to components. When we navigate, components are created and destroyed along its validation information. ngrx-forms
allow us to overcome this problem by decoupling validation from components.
With a small tweak, now we can even count all the validation errors in the application by traversing the validation information recursively.
@Injectable()
export class InvalidFieldsSelector {
appErrors$: Observable;
personErrors$: Observable;
configErrors$: Observable;
constructor(private store: Store) {
this.appErrors$ = store.pipe(
select(state => countValidationErrors(state.form)),
);
this.personErrors$ = store.pipe(
select(state => countValidationErrors(state.form.controls.person)),
);
this.configErrors$ = store.pipe(
select(state => countValidationErrors(state.form.controls.config)),
);
}
}
function countValidationErrors(control: AbstractControlState): number {
const subControl = (<FormGroupState>control).controls;
if (control.isPristine) {
return 0;
}
if (!subControl) {
return Object.keys(control.errors).length;
}
return Object.keys(subControl).reduce((errors, key) => {
return countValidationErrors(subControl[key]) + errors;
}, 0);
}</code class="language-js">
We have added a new observable in our selector to count the number of validation errors at the application level. We also modified the countValidationErrors
function to take into account the nested structure of our form. This is one of the benefits of grouping all of our forms into a root form as we did.
We can update our root component to make use of the new selector property.
@Component({
selector: 'app-root',
template: `
<h1>NgRx Forms Example ({{ appErrors$ | async }})</h1>
<nav>...<nav>
<router-outlet></router-outlet>
`,
})
export class RootComponent {
public appErrors$: Observable<number>;
...
constructor(private invalidFieldsSelector: InvalidFieldsSelector) {
this.appErrors$ = invalidFieldsSelector.appErrors$;
...
}
}
Now, the overall validation information is visible to the user.
Being able to count all the validation errors at the application level is not the only benefit of centralizing all the forms into a single root form. This pattern also will help us to do something that is virtually impossible to do with plain Angular forms: Cross component validation.
Cross Component Validation
So far, the field age
in the person
form group doesn’t have any validation applied. What if we now want to enforce the entered age to be greater or equal to the minAge
defined in the config
form group? We can do this by extending our formReducer
.
export const formReducer = createFormGroupReducerWithUpdate(
{
person: updateGroup({
firstName: validate(required),
lastName: validate(required),
}),
config: updateGroup({
minAge: validate([required, greaterThanOrEqualTo(0)]),
}),
},
{
person: (
person: AbstractControlState,
rootForm: FormGroupState,
) =>
updateGroup({
age: (age: AbstractControlState) => {
const minAgeValue = (<FormGroupState>rootForm.controls.config)
.controls.minAge.value;
return validate(minAge(minAgeValue), age);
},
})(cast(person)),
},
);
The createFormGroupReducerWithUpdate
accepts a second argument that augments the validation previously defined. In this case, we want to extend the validation of the personform
group so we pass an object with that key. The library requires a callback function that’s going to receive the control (person
) and its parent form (rootForm
). We then use (again) the function updateGroup
to define the extra validation rules for the field of this nested form. Because we have access to the rootForm
we can easily access the current value of the field minAge
of the config form group. With this information we can then apply the minAge
validation rule to the filed.
Wondering about the code cast(person)
? Don’t mind this too much, this is a piece of code to set the correct type and satisfy Typescript type inference process. This is probably a side effect of the lack of mapped conditional types discussed before. Suffice to know that this has no runtime impact on your code.
The minAge
validator is almost identical to the min validator that we used before with the exception of returning a different error object when the field is invalid.
export function minAge(minAge: number) {
return (value: number) => {
return value >= minAge ? null : { minAge: true };
};
}
We can see the final result below:
Conclusions
We have seen how powerful and convenient is to move our complete form system to the store. It allowed us to extract valuable information and create complex validations rules. By decoupling the validation from components, we can still access and display information about our forms even if the components used by the form are destroyed. It’s familiar API makes ngrx-form
easier to understand once you are familiarized with Angular’s FormGroup
and FormControl
directives.
I’m quite impressed with how feature complete this library is and how much information we have available in the store to play with. The seamless integration through directives that adds and removes CSS classes based on field and form states, makes styling a breeze.
There are of course downsides. The documentation, although extensive, is hard to understand at times. I also miss the declarative nature of “template-driven” forms that are not available with this library. With ngrx-forms
a reactive approach similar to the FormBuilder
is the only option. And, even though the API is very similar to Angular own form system, migrating an existing application to ngrx-forms
is not an easy task. It could require a lot of rework and probably a full rewrite of the application.
But at the end, my biggest regret is not have known about this library before. Centralizing all the form information into a single root form in our Redux store creates a myriad of possibilities. I just wish one day the Angular team will realize how important is to have a proper integration with Redux baked into the framework. For now, ngrx-forms
is a step in the right direction.
If you want to see the source code for the example app, feel free to go to the repo.
Leave a Reply