This post is a continuation from Part 1, where we set up a test project with NgRx and ngrx-forms in our Angular application. For part 2, we will validate our form.
Synchronous Validation
Say you want to make sure the user has filled out every field in the order form. To validate an ngrx-forms form group, you must add a validating function to the reducer. This differs from reactive forms, which require validators at form creation.
// reducers.ts
import { updateGroup, validate } from 'ngrx-forms';
import { required } from 'ngrx-forms/validation';
const validateOrderForm = updateGroup<OrderFormState>({
name: validate(required),
address: validate(required),
phone: validate(required),
items: validate(required)
});
export function reducer(
state = initialState,
action: any // normally this would be a union type of your action objects
): GlobalState {
const orderForm = validateOrderForm(formGroupReducer(state.orderForm, action));
if (orderForm !== state.orderForm) {
state = {...state, orderForm};
}
switch (action.type) {
case ActionType.createOrderSuccess:
const orders = [...state.orders, action.order];
return {...state, orders, mostRecentOrder: action.order};
case ActionType.getOrdersSuccess:
return {...state, orders: action.orders};
case ActionType.clearOrderForm:
return {...state, orderForm: initialOrderFormState};
default:
return state;
}
}
The new reducer validates all the inputs we list in updateGroup()
. required
is one of ngrx-form’s built-in validators.
If an input fails validation, the form control will have an error attached to it. Here’s how to access that error and react in the template:
If a form control passes validation, errors is an empty object.
<!-- order.component.html -->
<p *ngIf="formState.controls.items.errors.required" class="info text-error">Please choose an item</p>
Custom validators
ngrx-forms comes with a lot of useful built-in validators, but sometimes you need something custom. Let’s add a validator so no one named Chris can use our app. Chrises, you know what you did.
// reducers.ts
// syntax is odd but copied from ngrx-forms’ implementation of required
interface NoChrisValidationError<T> {
actual: T | null | undefined;
}
declare module 'ngrx-forms/src/state' {
interface ValidationErrors {
noChris?: NoChrisValidationError<any>
}
}
const noChris = (name: string | null | undefined): ValidationErrors => {
const errors: ValidationErrors = {};
if (name && name.toLowerCase() === 'chris') {
errors.noChris = 'No one named Chris!'
}
return errors;
}
The important part is the custom validator function. The parameter should be typed as the form control value type or null
or undefined
. The function always returns a ValidationErrors
object. If the parameter is invalid, we add an error key to the ValidationErrors
object.
// from Angular source code
export declare type ValidationErrors = {
[key: string]: any;
};
To add the new validator to the form group, pass it as an additional argument to the validate()
function for the desired form control.
// reducers.ts
const validateOrderForm = updateGroup<OrderFormState>({
name: validate(required, noChris),
address: validate(required),
phone: validate(required),
items: validate(required)
});
<!-- order.component.html -->
<p *ngIf="formState.controls.name.errors.noChris" class="info text-error">No Chrises allowed!</p>
Asynchronous validators
An async validator is any validation that requires an async operation. For example, imagine a signup form for a website where users must have unique names. We might validate the username
form control through an HTTP request to the server to see if that name is free. That would require an async validator.
Async validators are a little tougher to implement in ngrx-forms. After reading the docs, the easiest way I found is to write them as effects.
Effects are impure operations that take place before your reducers run. For example, our order form component might dispatch an action to create a new order. That action would be intercepted and POSTed to our API in an effect. If the POST request passes, the newly created order is passed to our reducer for storage in the state. If it fails, it isn’t.
To demonstrate, let's install google-libphonenumber, a popular open source library for validating phone numbers. We are going to check users’ phone numbers to see if they are valid in the US.
We start with a function to validate phone numbers. google-libphonenumber actually runs synchronously, but this function will be async just to test async validators.
// phone-validator.ts
import {PhoneNumberUtil} from 'google-libphonenumber';
const phoneUtil = PhoneNumberUtil.getInstance();
async function isValidUSNumber(number: string): Promise<boolean> {
try {
const usNumber = phoneUtil.parse(number, 'US');
return phoneUtil.isValidNumberForRegion(usNumber, 'US');
} catch {
return false;
}
}
export default isValidUSNumber;
Now, in effects.ts:
// effects.ts
@Injectable()
export class OrderEffects {
@Effect()
submitOrder$ = this.actions$.pipe(
ofType<ReturnType<typeof createOrder>>(ActionType.createOrder),
mergeMap(action => {
return this.orderService.createOrder(action.order).pipe(
map((newOrder: Order) => ({ type: ActionType.createOrderSuccess, order: newOrder}))
)
})
);
@Effect()
getOrders$ = this.actions$.pipe(
ofType(ActionType.getOrders),
mergeMap(() => this.orderService.getOrders().pipe(
map((response: any) => ({ type: ActionType.getOrdersSuccess, orders: response.data }))
))
);
constructor(
private actions$: Actions,
private orderService: OrderService
) {}
}
We’ll add a new effect that listens for form control updates to our phone number input.
// effects.ts
import { Actions, Effect, ofType } from '@ngrx/effects';
import {ClearAsyncErrorAction, SetAsyncErrorAction, SetValueAction, StartAsyncValidationAction} from 'ngrx-forms';
import { from } from 'rxjs';
import isValidUSNumber from '../phone-validator';
...
@Effect()
validatePhoneNumber$ = this.actions$.pipe(
ofType(SetValueAction.TYPE),
filter((formControlUpdate: SetValueAction<string>) => formControlUpdate.controlId === 'order_form_id.phone'),
switchMap(formControlUpdate => {
const errorKey = 'validPhone'
return from(isValidUSNumber(formControlUpdate.value)).pipe(
map(validPhone => {
return validPhone ? new ClearAsyncErrorAction(formControlUpdate.controlId, errorKey) : new SetAsyncErrorAction(formControlUpdate.controlId, errorKey, true);
}),
startWith(new StartAsyncValidationAction(formControlUpdate.controlId, errorKey))
);
})
);
Let’s break down that operator chain:
- We listen to
this.actions$
to see actions as they come into the store. - We filter out all actions except those of type
SetValueAction
, which is ngrx-forms updating some form control. - We filter all ngrx-forms updates except those targeting the phone form control on our order form group.
- We create a new
Observable
representing an asynchronous validation of the new form control value. - If the form control value is valid, send a new action to the store clearing any phone validation error stored on the form control.
- If it is invalid, set a new async error on that form control. Async errors are like sync errors, but they are referenced slightly differently in the template.
- While the form control is being asynchronously validated, we tell the store that an async validation has started.
Basically, when the store is told to update the phone form control, we tell the store we are asynchronously checking its validity. When that check completes, we tell the store if it passed.
Last step: In the template, we display async errors if they exist.
<!-- order.component.html -->
<p *ngIf="formState.controls.phone.errors.$validPhone" class="info text-error">Invalid phone number</p>
Async errors on form controls are represented with a “$” prefix on form control objects.
Conclusion
That’s validation in ngrx-forms! A small but powerful library, especially if your application is already deeply invested in NgRx.