Angular Reactive Form Custom Validator for ORCID (plus Laravel rule)

Since I struggled a bit with putting this together, thought it might be useful to document it.

ORCID provides a handy guide to the Structure of the ORCID Identifier which includes a little function that calculates the 16th ‘checksum’ character of every ORCID id. This is a great way to check that a user hasn’t mis-typed/mis-copied their ORCID id into a form.

Our Angular validator for reactive forms is shown below:

import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
/*
* Validator for 16 digit code which uniquely identifies an ORDCID id
* Digit 16 is a chceksum for digits 1-15 - this validator checks that
* Note: use other validators to check for valid Url which contains
* https://orcid.org
*/
export function isORCIDValidator(control: AbstractControl): {[key: string]: any} | null {
if (control.value) { // don't check null values
// split url up on /
const urlParts: string[] = control.value.split('/');
// get last digit
const lastDigit = urlParts[urlParts.length - 1].slice(-1);
// let's get numbers from last part (removing any hyphens, etc)
const orcidDigits = urlParts[urlParts.length - 1].replace(/\D/g, '');
// strip last digit
const baseDigits = orcidDigits.slice(0, -1);
// get check digit - 0-9 or X
const generatedCheckDigit = generateCheckDigit(baseDigits).toString();
if (generatedCheckDigit !== lastDigit) {
return {'isORCID': {value: control.value}}; //return error
}
}
return null; //no errors
}
/**
* Generates check digit as per ISO 7064 11,2.
* https://support.orcid.org/knowledgebase/articles/116780-structure-of-the-orcid-identifier
*/
function generateCheckDigit(baseDigits: string) {
let total = 0;
for (let i = 0; i < baseDigits.length; i++) {
const digit = parseInt(baseDigits.charAt(i), 10); // base 10
total = (total + digit) * 2;
}
const remainder: number = total % 11;
const result: number = (12 - remainder) % 11;
return result === 10 ? 'X' : result;
}

All this really does is wrap that check digit generator from the ORCID website. To use this in a reactive form, the relevant bits of code are shown below:

...
import { isORCIDValidator } from '../../shared/index';
@Component({
selector: 'user-edit-component',
templateUrl: './user-edit.component.html',
styleUrls: ['./user-edit.component.css']
})
export class UserEditComponent implements OnInit {
@Input() user:User;
urlRegexPattern: string = 'https:\/\/orcid\.org\/.*'; // Regex pattern to ensure URL starts with https://orcid.org/
// TODO Regex pattern for 16digit part following this
/* Creating form */
userForm: FormGroup;
// naming form controls as class properties means we can refer to them by name in the html template - see: https://codecraft.tv/courses/angular/forms/model-driven-validation/
...
orcid: FormControl;
...
constructor(private userService: UserService) {
}
createFormControls() {
...
this.orcid = new FormControl(this.user.orcid,[Validators.pattern(this.urlRegexPattern),isORCIDValidator]);
...
}
createForm() {
this.userForm = new FormGroup({
...
orcid: this.orcid,
...
});
}
ngOnInit() {
this.createFormControls();
this.createForm();
}
}

If anyone can write me a bit of regex to check that the rest of the URL, after https://orcid.org/ is xxxx-xxxx-xxxx-xxxx, I’d be very grateful!

Finally, in the html (note am using Angular Material here), check for errors on the orcid FormControl (available like this  because of line 20 above) and display the approprioate <mat-error>.

<div fxLayout="row" fxLayoutAlign='center top'>
<div fxFlex class="form-container">
<h2>Edit your profile</h2>
<form [formGroup]="userForm">
...
<mat-form-field class="full-width" appearance="outline">
<mat-label>ORCID ID</mat-label>
<input id="orcid" matInput placeholder="https://orcid.org/xxxx-xxxx-xxxx-xxxx" formControlName="orcid">
<mat-error *ngIf="orcid.errors?.pattern && (orcid.dirty || orcid.touched)">ORCID ids start with https://orcid.org/</mat-error>
<mat-error *ngIf="orcid.errors?.isORCID && (orcid.dirty || orcid.touched)">Please check your id carefully - see <a href="https://support.orcid.org/knowledgebase/articles/116780-structure-of-the-orcid-identifier" target="_blank">Structure of the ORCID Identifier</a></mat-error>
</mat-form-field>
...
</form>
</div>
</div>

…and if you’re saving your ORCID ID in a Laravel api backend as I am, the same checksum validation as a Laravel Rule:

<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class Orcid implements Rule
{
/**
* Create a new rule instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
$urlParts = explode("/",$value);
// get last digit
$lastDigit = substr($urlParts[count($urlParts) - 1],-1);
// let's get numbers from last part (removing any hyphens, etc)
$orcidDigits = preg_replace('/\D/', '', $urlParts[count($urlParts) - 1]);
// strip last digit
$baseDigits = substr($orcidDigits, 0, -1);
// get check digit - 0-9 or X
$generatedCheckDigit = $this->generateCheckDigit($baseDigits);
return $generatedCheckDigit === $lastDigit;
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return 'The 16-digit number in the ORCID ID must have the correct checksum - please check it carefully';
}
/**
* Generates check digit as per ISO 7064 11,2 - see https://support.orcid.org/knowledgebase/articles/116780-structure-of-the-orcid-identifier
*
*/
private function generateCheckDigit($baseDigits) {
$total = 0;
for ($i = 0; $i < strlen($baseDigits); $i++) {
$digit = intval(substr($baseDigits,$i,1));
$total = ($total + $digit) * 2;
}
$remainder = $total % 11;
$result = (12 - $remainder) % 11;
if($result == 10) {
return "X";
} else {
return strval($result);
}
}
}
view raw Orcid.php hosted with ❤ by GitHub

…and in the update method of my UserController, the validation rules look like:

public function update(Request $request, $id)
{
$messages = [
...,
'orcid.unique' => 'This ORCID ID is already in use in our database',
'orcid.regex' => 'The ORCID ID is in the format https://orcid.org/xxxx-xxxx-xxxx-xxxx',
...,
];
//require more complex validation style to have unique ignore rule for ORCID
Validator::make($request->all(), [ //request->all() returns array required by Validator::make
...,
'orcid' => [
Rule::unique('users')->ignore($id),
'regex:/^http[s]?:\/\/orcid.org\/(\d{4})-(\d{4})-(\d{4})-(\d{3}[0-9X])$/', //from: https://github.com/pkp/pkp-lib/blob/master/classes/validation/ValidatorORCID.inc.php
new Orcid
],
...,
],$messages)->validate();

Note: the ignore clause on the unique rule would not be required in the store method as this user’s id and ORCID id would not be expected to already exist

Any suggestions for improvements gratefully received.