@a11y-ngx/a11y-required v1.0.4
Accessibility - Required / Invalid Component & Directives
Angular 4+ / Simple component that provides a red asterisk (by default) to complement any <label> tag on a form, plus Required & Invalid directives that provide aria-required and aria-invalid attributes to individual required form elements.
IMPORTANT: Inputs of type
radioorcheckboxgrouped under a<fieldset>(same "name" attribute), will require a different type of structure and validation. Only individual checkbox elements, with boolean values are allowed.
Introduction
The red asterisk helps the user to know that a form element is mandatory.
☑️ This is purely for visual purposes (it will be ignored by screen readers).
☑️ The red hex code color (
#EB0000) passess minimum color contrast of 4.5:1 for WCAG AA, considering a white background.
In addition to the asterisk, you need to add a11y-required and a11y-invalid directives to all mandatory form elements for screen reader users.
a11y-requiredwill add the attributearia-required="..."to the host element, to indicate screen readers that the control is required.a11y-invalidwill add the attributearia-invalid="..."andaria-describedby="..."to the host element, to complement screen reader users that the control is also invalid.
NOTE:
a11y-invalidcan be used on its own, for instance, if you have an optional input field but cannot have more than 15 characters then it's not required but it can be invalid (Check the HTML code block under Reactive Forms).
Installation
Install npm package:
npm install @a11y-ngx/a11y-required --saveImport
A11yRequiredModuleinto your module:
import { NgModule } from '@angular/core';
...
import { A11yRequiredModule } from '@a11y-ngx/a11y-required';
@NgModule({
declarations: [...],
imports: [
...
A11yRequiredModule
],
providers: [...],
bootstrap: [...]
})
export class AppModule { }📘 NOTE: Tested up to Angular 4
For Angular 4 use
Once installed, edit the file
a11y-ngx-a11y-required.metadata.jsonwithin the/node_modules/@a11y-ngx/a11y-requiredfolder and downgrade the value from theversionpropertyfrom
{"__symbolic":"module","version":4,"metadata":{...to
{"__symbolic":"module","version":3,"metadata":{...Done!
Usage of the Global Message & Asterisk
<a11y-required-message>will provide a generic global message about the required fields.<a11y-required>will provide a generic red asterisk to be placed with the required field.
Global Required Message
Add <a11y-required-message></a11y-required-message> at the top of your <form> (ideally) to add the message.
- By default it will render:
All elements with * are required - You can set your own custom message within the tags. Obligatorily, you also need to add
<a11y-required></a11y-required>as part of your message or the generic one will be rendered.
Example Code: Global Required Message
<a11y-required-message></a11y-required-message>
<a11y-required-message>All fields with a red asterisk are required</a11y-required-message>
<a11y-required-message>
<a11y-required></a11y-required> means required
</a11y-required-message>Example Output: Global Required Message
All elements with * are required
All elements with * are required
* means requiredRequired Asterisk
Add <a11y-required></a11y-required> within your <label> tag (usually) to add the red asterisk.
Example Code: Required Asterisk
<label for="...">
Your Name
<a11y-required></a11y-required>
</label>Example Output: Required Asterisk
Your Name *Changing the Default Asterisk
You can change the default asterisk by calling the default() method in the module and passing a new string:
@NgModule({
imports: [
A11yRequiredModule.default('(req)')
]
})Example Code: Custom Message
<a11y-required-message></a11y-required-message>
...
<label for="...">
Your Name
<a11y-required></a11y-required>
</label>Example Output: Custom Message
All elements with (req) are required
...
Your Name (req)Usage of the Required & Invalid directives
For the required directive, provide a11y-required to any required form control.
❗️ DO NOT use the
requiredHTML5 attribute, since some assistive technologies can say "required" twice.
For the invalid directive, provide a boolean to the attribute a11y-invalid on any form controls that are required or may be invalid at some point. In combination, you must provide the id value of the error message container in errorMsgId.
| Attribute | Type | Description |
|---|---|---|
a11y-required | none / boolean | For Reactive Forms, just the attribute (it will depend on the Validators).For Template-Driven Forms, you can set just the attribute (which means true by default) or pass a boolean value in case the control needs to change programmatically. |
a11y-invalid | boolean | Will indicate whether a control is invalid or not. |
errorMsgId | string | Provides the id of the error message container, to be associated to the form control in case of being invalid. |
⚠️ IMPORTANT: provide
errorMsgIdwith an unique and existingid. Having duplicate or non-existent IDs is an accessibility issue.
Template Variable
In order to have a cleaner code, you can use a template variable for the exported directives a11yRequired or a11yInvalid.
For example, if you need to programmatically set/remove the validator, you can make use of the template variable to show the asterisk, add an "invalid" classname and/or show the error message of that particular form element.
- For
a11yRequiredyou can make use ofisRequired,errorsorisInvalid(ifa11y-invalidis also in use). - For
a11yInvalidyou can make use ofisInvalid.
| Property | Returns | Description |
|---|---|---|
isRequired | boolean | Can be used in case of the Validator is programmatically set/removed. |
errors | object | Will provide the same errors object as the form control. |
isInvalid | boolean | Can be used in case of a11y-invalid directive is used. |
NOTE: You can import
A11yRequiredControlorA11yInvalidControlinterfaces to assign to a@ViewChild()template variable and make use of theisRequiredand/orisInvalidwithin your component.
import { A11yRequiredControl, A11yInvalidControl } from '@a11y-ngx/a11y-required';
@Component({ ... })
export class MyComponent {
...
@ViewChild('whichDogs') whichDogs: A11yRequiredControl;
@ViewChild('yourName') yourName: A11yInvalidControl;
myValidation(): void {
if (this.whichDogs.isRequired && this.whichDogs.isInvalid) { ... }
if (this.yourName.isInvalid) { ... }
}Reactive Forms
In order to work, besides adding a11y-required in the template's control, you must also set the Validators.required or Validators.requiredTrue to the form control in your code.
In the below example, we have a form with the next controls:
| Control | Type | Validators | Template Variable | Description |
|---|---|---|---|---|
username | text | required | a11yRequired | Required |
password | password | required & minLength(5) | a11yRequired | Required and it also needs a minimum length of 5 characters |
yourName | text | maxLength(15) | a11yInvalid | Not required, but can have up to 15 characters max (which makes it invalid if longer) |
likeDogs | checkbox | none | N/A | |
whichDogs | text | none or required | a11yRequired | Will be required if likeDogs is checked |
acceptTerms | checkbox | requiredTrue | a11yRequired | Required |
Explanation for Required:
When the user checks/unchecks
likeDogs, you must programmatically add/remove the validator required towhichDogscontrol.Once the directive detects a change on the validators, will return a
booleanin the template variableisRequiredproperty, which can be used to show/hide the asterisk within the label.Explanation for Invalid:
For each
a11y-invalidwe can set some conditions to each control, for instance if it wastouchedand if it'sinvalid.Once the directive detects a change, will return a
booleanin the template variableisInvalidproperty, which can be used to set an "invalid" class (for visual purposes) and show the error message, and not repeat the condition every time we have to use it.
Example Code: Reactive Forms
TypeScript
@Component({ ... })
export class MyComponent {
constructor(private fb: FormBuilder) { }
myForm = this.fb.group({
username: ['', Validators.required],
password: ['', [Validators.required, Validators.minLength(5)]],
yourName: ['', Validators.maxLength(15)],
likeDogs: [false],
whichDogs: [''],
acceptTerms: [false, Validators.requiredTrue]
});
likeDogs() {
const likeDogs = this.myForm.get('likeDogs').value;
const whichDogs = this.myForm.get('whichDogs');
if (likeDogs) {
whichDogs.setValidators([Validators.required]);
} else {
whichDogs.setValidators([]);
}
whichDogs.updateValueAndValidity();
}
}HTML
<form [formGroup]="myForm" (ngSubmit)="..." novalidate>
<div>
<a11y-required-message></a11y-required-message>
</div>
<div>
<label for="username">
Username
<a11y-required></a11y-required>
</label>
<input type="text" id="username" formControlName="username"
a11y-required #username="a11yRequired" [class.invalid]="username.isInvalid"
[a11y-invalid]="myForm.get('username').touched && myForm.get('username').invalid"
errorMsgId="username-error" />
<div id="username-error" *ngIf="username.isInvalid">
The username cannot be empty
</div>
</div>
<div>
<label for="password">
Password
<a11y-required></a11y-required>
</label>
<input type="password" id="password" formControlName="password"
a11y-required #password="a11yRequired" [class.invalid]="password.isInvalid"
[a11y-invalid]="myForm.get('password').touched && myForm.get('password').invalid"
errorMsgId="password-error" />
<div id="password-error" *ngIf="password.isInvalid">
<div *ngIf="password.errors.required">The password cannot be empty</div>
<div *ngIf="password.errors.minlength">The password must have at least {{ password.errors.minlength.requiredLength }} characters</div>
</div>
</div>
<div>
<label for="yourName">
Your Name
</label>
<input type="text" id="yourName" formControlName="yourName" aria-describedby="yourName-instructions"
#yourName="a11yInvalid" [class.invalid]="yourName.isInvalid"
[a11y-invalid]="myForm.get('yourName').touched && myForm.get('yourName').invalid"
errorMsgId="yourName-error" />
<div id="yourName-instructions">
Your name is optional, but must not exceed 15 characters.
</div>
<div id="yourName-error" *ngIf="yourName.isInvalid">
Your name cannot have more than 15 characters.
</div>
</div>
<div>
<input type="checkbox" id="likeDogs" formControlName="likeDogs" (change)="likeDogs()" />
<label for="likeDogs">
Do you like dogs?
</label>
</div>
<div>
<label for="whichDogs">
What breeds of dogs do you like?
<a11y-required *ngIf="whichDogs.isRequired"></a11y-required>
</label>
<input type="text" id="whichDogs" formControlName="whichDogs"
a11y-required #whichDogs="a11yRequired" [class.invalid]="whichDogs.isInvalid"
[a11y-invalid]="myForm.get('whichDogs').touched && myForm.get('whichDogs').invalid"
errorMsgId="whichDogs-error" />
<div id="whichDogs-error" *ngIf="whichDogs.isInvalid">
The dog's breeds cannot be empty
</div>
</div>
<div>
<input type="checkbox" id="acceptTerms" formControlName="acceptTerms"
a11y-required #acceptTerms="a11yRequired" [class.invalid]="acceptTerms.isInvalid"
[a11y-invalid]="myForm.get('acceptTerms').touched && myForm.get('acceptTerms').invalid"
errorMsgId="acceptTerms-error" />
<label for="acceptTerms">
I accept the terms and conditions
<a11y-required></a11y-required>
</label>
<div id="acceptTerms-error" *ngIf="acceptTerms.isInvalid">
You must accept the terms and conditions
</div>
</div>
<div>
<button type="submit" [disabled]="myForm.invalid">Submit</button>
</div>
</form>Template-Driven Forms
- You can just place the attribute to set the required value to
trueby default:<input type="text" a11y-required [(ngModel)]="...">, or - You can set a
booleanto the attribute to programmatically change the required status:<input type="text" [a11y-required]="inputRequired" [(ngModel)]="...">
NOTE: The
a11y-invaliddirective works the same way as in the reactive forms.
Example Code: Template-Driven Forms
TypeScript
@Component({ ... })
export class MyComponent {
likeDogs: boolean = false;
whichDogs: string = '';
}HTML
<form #myForm="ngForm" (ngSubmit)="..." novalidate>
<div>
<a11y-required-message></a11y-required-message>
</div>
<div>
<input type="checkbox" id="likeDogs" name="likeDogs" ngModel (change)="likeDogs = $event.target.checked">
<label for="likeDogs">
Do you like dogs?
</label>
</div>
<div>
<label for="whichDogs">
What breeds of dogs do you like?
<a11y-required *ngIf="whichDogsA11y.isRequired"></a11y-required>
</label>
<input type="text" id="whichDogs" name="whichDogs" ngModel #whichDogsModel="ngModel"
[a11y-required]="likeDogs" #whichDogsA11y="a11yRequired" [class.invalid]="whichDogsA11y.isInvalid"
[a11y-invalid]="whichDogsModel.touched && whichDogsModel.invalid"
errorMsgId="whichDogs-error" />
<div id="whichDogs-error" *ngIf="whichDogsA11y.isInvalid">
The dog's breeds cannot be empty
</div>
</div>
<div>
<button type="submit" [disabled]="myForm.invalid">Submit</button>
</div>
</form>