How to Integrate reCAPTCHA in your Angular Forms
In this article, we are going to implement from scratch a reCAPTCHA directive for Angular applications including the server side with express js. Along the way, we will use features like custom form controls, async validators, zones and other fun stuff.
What is reCAPTCHA
reCAPTCHA is a free service that protects your website from spam and abuse.
We will follow the steps from the official documentation. I want the use of the directive to be as transparent as possible to the user, so that will be our final result:
The Client Side
Initialization
First, let’s create the directive.
We have three inputs, the public key, a config object for which we have defined an interface, and an optional user’s language.
Our next step is to define the onloadCallback.
This function will get called when all the dependencies have loaded.
Before we do this, let’s create a method that will insert the reCAPTCHA script.
The code is straightforward. Insert the Javascript resource, setting the onload
parameter to the name of our onload
callback function and the render
parameter to explicit
.
Now we can implement the onload
callback (reCaptchaLoad
).
When the onload
callback is executed, we can render the container as a reCAPTCHA widget by calling the grecaptcha.render()
method with the DOM element and the config object.
Type Definitions
At this point, typescript will yell at you because you don’t have type definitions for the grecaptcha
and the reCaptchaLoad
objects. A quick fix will be to do the following:
Create Custom Form Control
As you may know, if you want to create custom form control in Angular, you need to implement the ControlValueAccessor
interface. I’m not going to go into this topic because it requires an article of its own. You can read more about the subject here.
We have implemented the three methods that the ControlValueAccessor
require. In our case, we don’t care about the writeValue()
method, so we leave it empty.
Now let’s go back for a second. You remember we defined two callbacks in the final config
object?
'callback': this.onSuccess.bind(this),
'expired-callback': this.onExpired.bind(this)
callback
is the name of your callback function to be executed when the user submits a successful CAPTCHA response.
expired-callback
the name of your callback function to be executed when the recaptcha response expires and the user needs to solve a new CAPTCHA.
We need to notify the formControl
that it’s valid if we get the token from the onSuccess
function or that it’s invalid if the onExpired
function is called.
💣, surprise! We come across one of Angular’s unusual cases, in which Angular is unconscious and therefore does not run the change detection. Angular doesn’t know about our global callback. Therefore, we need to wrap our code in the zone.run()
method.
Running functions via
run
allows you to reenter Angular zone from a task that was executed outside of the Angular zone
Add Validators Programmatically
Remember when I said at first that I wanted everything to be as transparent as possible to the user? This is one of the cases.
We need to add the required
validation to the form control. We can accomplish this by injecting the NgControl
into our directive to get the control
instance.
But there is one problem, we can’t inject the NgControl
into our directive because we have implemented the directive as a ControlValueAccessor
and because of that Angular will throw an error:
Cannot instantiate cyclic dependency! NgControl
We can get around this problem by getting the NgControl
instance directly from the directive Injector
.
😎 Now we have the control instance and we can add the required
validator programmatically. We also need to call the updateValueAndValidity()
method because calling the setValidators()
doesn’t trigger any update or value change event.
The Server Side
When the user solves a reCAPTCHA, we also need to verify the token with the Google API to ensure that the token is valid. The required parameters are the secret key and the token. Let’s use express
for this task.
The response will be:
{
"success": true|false,
"challenge_ts": timestamp,
"hostname": string
"error-codes": [...]
}
In our case, we only care about the success
key. Let’s add an async validator that will send the HTTP request to our endpoint with the token
and will set the control
status based on the response. I’m going to use the DI for getting the endpoint from our consumer.
I already wrote a dedicated article on how to implement async validators in Angular, you can find it here. In a nutshell, if we have a success
response we are returning null
to notify the control
that it’s valid
otherwise we are returning the error
object.
We also need to add the validator to the directive providers
—
@Directive({
selector: '[nbRecaptcha]',
providers: [ ReCaptchaAsyncValidator ]
})
And to provide the endpoint in our signin
component —
@Component({
...
providers: [{
provide: RECAPTCHA_URL,
useValue: 'http://localhost:3000/validate_captcha'
}]
})
export class SigninComponent { }
The last thing is to trigger the validation in our reCAPTCHA directive.
When we get a valid token, we need to add the async validator to the control
validators that will trigger our HTTP request and based on the response will set the control status.
You can find the full source code with a few add-ons, here.
Follow me on Medium or Twitter to read more about Angular, Vue and JS!