Problem
In Angular development, it is common to create reusable components that need to interact with the regular HTML DOM. In some cases you need to give attributes a unique name, as in the following cases:
- Forms: The attribute
for
in<label>
needs a reference to a uniqueid
of an input element. - Accessibility: Some elements need a specific labelling or further description via
aria-labelledby
oraria-describedby
. These references need point to a uniqueid
, too. While reusable components enhance maintainability and reduce code duplication, they can introduce a significant problem: duplicate IDs. When a component is instantiated multiple times within the same route or DOM, each instance would traditionally use the sameid
value, leading to duplication. This duplication can break the functionality of the HTML attributes relying on theseid
and violate HTML specifications, potentially leading to accessibility and usability issues.
Approach
To solve the problem of duplicated IDs by using reusable components, we have to generate a unique id
at runtime. This ensures that every component has its own unique id
to reference to. In the following example we take a look at a custom form input component shown in this Stackblitz.
Step 1: Setting Up the Component
<!-- custom-input.component.ts template -->
<label [attr.for]="inputId" class="form-label">{{ label() }}</label>
<input
[attr.id]="inputId"
[type]="type()"
[attr.aria-describedby]="hasError() ? errorId : undefined"
[formControl]="control()"
class="form-control">
We have a form component with a label and an input field. The for
attribute of the label needs to reference to the input's id
.
Step 2: Generating Unique IDs
// custom-input.component.ts
import ...;
let uniqueId = 0;
@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'custom-input',
template: `...`,
})
export class CustomInputComponent {
label = input.required<string>();
control = input.required<FormControl>();
type = input<string>('text');
inputId = `custom-input-${uniqueId++}`;
...
}
We use a variable outside of the component named uniqueId
. By initializing the component, the field inputId
uses and increments the value of uniqueId
. This ensures that no components will have the same value for inputId
.
Step 3: Using the Component
// main.ts
@Component({
selector: 'app-root',
standalone: true,
template: `
<h1>Angular Dynamic IDs</h1>
<form [formGroup]="form" class="container-md">
<custom-input label="Firstname" [control]="form.controls.firstname" />
<custom-input label="Lastname" [control]="form.controls.lastname" />
<button (click)="send()" [disabled]="form.invalid" class="btn btn-primary">Send Form</button>
</form>
`,
imports: [ReactiveFormsModule, CustomInputComponent],
})
export class App {
form = inject(FormBuilder).group({
firstname: ['', Validators.required],
lastname: ['', Validators.required],
});
send(): void {
alert(`Hello ${this.form.value.firstname} ${this.form.value.lastname}!`);
}
}
In the application we can now use our newly created form component. We do not have to worry about any duplicate IDs anymore! Our first custom input (Firstname) will now use the id="custom-input-0"
. Same with id="custom-input-1"
for our second custom input (Lastname).
Conclusion
By dynamically generating unique IDs for Angular components, you can safely reuse components across different parts of your application without running into issues of duplicate IDs. This method is essential for scenarios requiring strict ID uniqueness, such as labeling, accessibility attributes, or JavaScript DOM manipulation.
Summary of Steps:
- Define a counter outside the component.
- Combine a base name with the counter.
- Apply the generated ID to relevant attributes like
id
,for
,aria-labelledby
, etc.
Following these best practices ensures your Angular applications remain modular, accessible, and free from ID conflicts.
Top comments (3)
Hi Marcel Goldammer,
Top, very nice and helpful !
Thanks for sharing.
Hi, thanks for your article, I was wondering if this technique was working when the component is lazy loaded and/or imported in lazy loaded modules ?
Hi @jbuiquan, yes, it does work if you are using lazy loaded components.