@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
radio
orcheckbox
grouped 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-required
will add the attributearia-required="..."
to the host element, to indicate screen readers that the control is required.a11y-invalid
will add the attributearia-invalid="..."
andaria-describedby="..."
to the host element, to complement screen reader users that the control is also invalid.
NOTE:
a11y-invalid
can 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 --save
Import
A11yRequiredModule
into 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.json
within the/node_modules/@a11y-ngx/a11y-required
folder and downgrade the value from theversion
propertyfrom
{"__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 required
Required 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
required
HTML5 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
errorMsgId
with 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
a11yRequired
you can make use ofisRequired
,errors
orisInvalid
(ifa11y-invalid
is also in use). - For
a11yInvalid
you 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
A11yRequiredControl
orA11yInvalidControl
interfaces to assign to a@ViewChild()
template variable and make use of theisRequired
and/orisInvalid
within 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 towhichDogs
control.Once the directive detects a change on the validators, will return a
boolean
in the template variableisRequired
property, which can be used to show/hide the asterisk within the label.Explanation for Invalid:
For each
a11y-invalid
we can set some conditions to each control, for instance if it wastouched
and if it'sinvalid
.Once the directive detects a change, will return a
boolean
in the template variableisInvalid
property, 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
true
by default:<input type="text" a11y-required [(ngModel)]="...">
, or - You can set a
boolean
to the attribute to programmatically change the required status:<input type="text" [a11y-required]="inputRequired" [(ngModel)]="...">
NOTE: The
a11y-invalid
directive 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>