1.0.4 • Published 3 years ago

@a11y-ngx/a11y-required v1.0.4

Weekly downloads
-
License
MIT
Repository
-
Last release
3 years ago

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 or checkbox 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 attribute aria-required="..." to the host element, to indicate screen readers that the control is required.
  • a11y-invalid will add the attribute aria-invalid="..." and aria-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

  1. Install npm package:

    npm install @a11y-ngx/a11y-required --save

  2. 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 the version property

from

{"__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.

AttributeTypeDescription
a11y-requirednone / booleanFor 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-invalidbooleanWill indicate whether a control is invalid or not.
errorMsgIdstringProvides 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 existing id. 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 of isRequired, errors or isInvalid (if a11y-invalid is also in use).
  • For a11yInvalid you can make use of isInvalid.
PropertyReturnsDescription
isRequiredbooleanCan be used in case of the Validator is programmatically set/removed.
errorsobjectWill provide the same errors object as the form control.
isInvalidbooleanCan be used in case of a11y-invalid directive is used.

NOTE: You can import A11yRequiredControl or A11yInvalidControl interfaces to assign to a @ViewChild() template variable and make use of the isRequired and/or isInvalid 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:

ControlTypeValidatorsTemplate VariableDescription
usernametextrequireda11yRequiredRequired
passwordpasswordrequired & minLength(5)a11yRequiredRequired and it also needs a minimum length of 5 characters
yourNametextmaxLength(15)a11yInvalidNot required, but can have up to 15 characters max (which makes it invalid if longer)
likeDogscheckboxnoneN/A
whichDogstextnone or requireda11yRequiredWill be required if likeDogs is checked
acceptTermscheckboxrequiredTruea11yRequiredRequired

Explanation for Required:

When the user checks/unchecks likeDogs, you must programmatically add/remove the validator required to whichDogs control.

Once the directive detects a change on the validators, will return a boolean in the template variable isRequired 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 was touched and if it's invalid.

Once the directive detects a change, will return a boolean in the template variable isInvalid 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>
1.0.8

3 years ago

1.0.7

3 years ago

1.0.4

3 years ago

1.0.3

3 years ago

1.0.2

3 years ago

1.0.1

3 years ago

1.0.0

3 years ago