Validating workout names using async validators

Like custom validators, async validators inherit from the same Validator class too; but this time, instead of returning an object map, async validators return a Promise.

Let's look at the definition of the validator. Copy the definition of the validator from the GitHub (http://bit.ly/ng6be-6-1-remote-validator-directive-ts) folder and add it to the shared module folder. The validator definition looks as follows:

import { Directive, Input } from '@angular/core';
import { NG_ASYNC_VALIDATORS, FormControl } from '@angular/forms';

@Directive({
selector: '[abeRemoteValidator][ngModel]',
providers: [{ provide: NG_ASYNC_VALIDATORS, useExisting: RemoteValidatorDirective, multi: true }]
})
export class RemoteValidatorDirective {

@Input() abeRemoteValidator: string;
@Input() validateFunction: (value: string) => Promise<boolean>;

validate(control: FormControl): { [key: string]: any } {
const value: string = control.value;
return this.validateFunction(value).then((result: boolean) => {
if (result) {
return null;
}
else {
const error: any = {};
error[this.abeRemoteValidator] = true;
return error;
}
});
}
}

Do remember to export this directive from the shared module, allowing us to use it in the workout builder module.

Since we are registering the validator as a directive instead of registering using a FormControl instance (generally used when building forms with a reactive approach), we need the extra provider configuration setting (added in the preceding @Directive metadata) by using this syntax:

 providers:[{ provide: NG_ASYNC_VALIDATORS, useExisting: RemoteValidatorDirective,  multi: true }] 

This statement registers the validator with the existing async validators.

The strange directive selector, selector: `[abeRemoteValidator][ngModel]`, used in the preceding code will be covered in the next section, where we will build a busy indicator directive.

Before we dig into the validator implementation, let's add it to the workout name input. This will help us correlate the behavior of the validator with its usage.

Update the workout name input (workout.component.html) with the validator declaration:

<input type="text" name="workoutName" ... 
  abeRemoteValidator="workoutname"   [validateFunction]="validateWorkoutName">
Prefixing the directive selector
Always prefix your directives with an identifier (abe as you just saw) that distinguishes them from framework directives and other third-party directives.

Note: If the ngModelOptions, updateOn is set to submit, change it to blur.

The directive implementation takes two inputs: the validation key through directive property abeRemoveValidator, used to set the error key, and the validation function (validateFunction), called to validate the value of the control. Both inputs are annotated with the @Input decorator.

The input parameter @Input("validateFunction") validateFunction: (value: string) => Promise<boolean>;, binds to a function, not a standard component property. We are allowed to treat the function as a property due to the nature of the underlying language, TypeScript (as well as JavaScript).

When the async validation fires (on a change of input), Angular invokes the function, passing in the underlying control. As the first step, we pull the current input value and then invoke the validateFunction function with this input. The validateFunction returns a promise, which should eventually resolve to true or false:

  • If the promise resolves to true, the validation is successful, the promise callback function returns null.
  • If it is false, the validation has failed, and an error key-value map is returned. The key here is the string literal that we set when using the validator (a2beRemoteValidator="workoutname").

This key comes in handy when there are multiple validators declared on the input, allowing us to identify validations that have failed.

To the workout component next add a validation message for this failure too. Add this label declaration after the existing validation label for workout name:

<label *ngIf="name.control.hasError('workoutname')" class="alert alert-danger validation-message">A workout with this name already exists.</label> 

And then wrap these two labels inside a div, as we do for workout title error labels.

The hasError function checks whether the 'workoutname' validation key is present.

The last missing piece of this implementation is the actual validation function we assigned when applying the directive ([validateFunction]="validateWorkoutName"), but never implemented.

Add the validateWorkoutName function to workout.component.ts:

validateWorkoutName = (name: string): Promise<boolean> => {
if (this.workoutName === name) { return Promise.resolve(true); }
return this.workoutService.getWorkout(name).toPromise()
.then((workout: WorkoutPlan) => {
return !workout;
}, error => {
return true;
});
}

Before we explore what the preceding function does, we need to do some more fixes on the WorkoutComponent class. The validateWorkoutName function is dependent on WorkoutService to get a workout with a specific name. Let's inject the service in the constructor and add the necessary import in the imports section:

import { WorkoutService }  from "../../core/workout.service"; 
... 
constructor(... , private workoutService: WorkoutService) { 

Then declare variables workoutName and queryParamsSub:

private workoutName: string;
queryParamsSub: Subscription

And add this statement to ngOnInit:

this.queryParamsSub = this.route.params.subscribe(params => this.workoutName = params['id']); 

The preceding statement set the current workout name by watching (subscribing) over the observable route.params service. workoutName is used to skip workout name validation for an existing workout if the original workout name is used.

The subscription created previously needs to be clear to avoid memory leak, hence add this line to the ngDestroy function:

this.queryParamsSub.unsubscribe();

The reason for defining the validateWorkoutName function as an instance function (the use of the arrow operator) instead of defining it as a standard function (which declares the function on the prototype) is the 'this' scoping issue.

Look at the validator function invocation inside RemoteValidatorDirective (declared using @Input("validateFunction") validateFunction;):

return this.validationFunction(value).then((result: boolean) => { ... }); 

When the function (named validateFunction) is invoked, the this reference is bound to RemoteValidatorDirective instead of the WorkoutComponent. Since execute is referencing the validateWorkoutName function in the preceding setup, any access to this inside validateWorkoutName is problematic.

This causes the if (this.workoutName === name) statement inside validateWorkoutName to fail, as RemoteValiatorDirective does not have a workoutName instance member. By defining validateWorkoutName as an instance function, the TypeScript compiler creates a closure around the value of this when the function is defined.

With the new declaration, this inside validateWorkoutName always points to the WorkoutComponent irrespective of how the function gets invoked.

We can also look at the compiled JavaScript for WorkoutComponent to know how the closure works with respect to validateWorkoutName. The parts of the generated code that interest us are as follows:

function WorkoutComponent(...) { 
  var _this = this; 
  ... 
  this.validateWorkoutName = function (name) { 
    if (_this.workoutName === name) 
      return Promise.resolve(true); 

If we look at the validation function implementation, we see that it involves querying mLab for a specific workout name. The validateWorkoutName function returns true when a workout with the same name is not found and false when a workout with the same name is found (actually a promise is returned).

The getWorkout function on WorkoutService returns an observable, but we convert it into a promise by calling the toPromise function on the observable.

The validation directive can now be tested. Create a new workout and enter an existing workout name such as 7minworkout. See how the validation error message shows up eventually:

Excellent! It looks great, but there is still something missing. The user is not informed that we are validating the workout name. We can improve this experience.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.91.44