Preventing Attribute Reflection in Angular
When it comes to building Angular applications eventually its likely that you will need to pass an id
value of some kind into a component via an Input
. Using the @Input
decorator, this is pretty straightforward and allows us to pass data down to child components. However, when passing id
s specifically, we can run into some interesting edge cases and how id
attributes behave in the browser.
Let's take a look at an example. I have a custom switch input that uses some CSS magic to make a checkbox look like a switch/toggle. I plan on wrapping this functionality up into a component.
Looking at the HTML for this component it would look something like this,
<form [formGroup]="myForm" (ngSubmit)="submit()">
<label for="switch-1">Switch 2</label>
<app-switch formControlName="mySwitch" id="switch-1"></app-switch>
<button>Submit</button>
</form>
Ideally, for my switch component, I want to be able to assign a label to the input that way if the user clicks the label it focuses my switch as well as provides accessibility to my switch component.
To assign a label to HTML inputs we typically use the for
attribute and give it an id
reference to the input it belongs to. In this case, that id
is switch-1
. Our switch input needs to take that id
as an Input
so it can assign it to the checkbox in its template.
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-switch',
template: `
<div (click)="switch()" class="switch">
<input type="checkbox" class="switch-input" [id]="id">
<span class="switch-label" data-on="On" data-off="Off"></span>
<span class="switch-handle"></span>
</div>
`,
styleUrls: ['./switch.component.css']
})
export class SwitchComponent {
@Input() id = '';
switch() { ... }
}
This component has more functionality than just this but for now, let's focus on our id
Input
property. Notice how we take in the id
as an @Input
. With that input, we set the id
of the checkbox. Now our label and checkbox internally have the same id
and everything should work. Well not quite. If we look at the HTML generated by Angular, we see something is not quite right.
Notice there is a duplicate id="switch-1"
. The switch-1
value is on the app-switch
element and the checkbox element. Because of this, we break the label functionality. In HTML id
attributes on the same page must be unique. If not unique then the label does not know which element it should associate to. This behavior breaks the functionality of being able to click the label to focus the switch as well as any accessibility benefits of the label. Luckily there is a way to fix this duplication with a little extra work.
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-switch',
template: `
<div (click)="switch()" class="switch">
<input type="checkbox" class="switch-input" [id]="id">
<span class="switch-label" data-on="On" data-off="Off"></span>
<span class="switch-handle"></span>
</div>
`,
styleUrls: ['./switch.component.css']
})
export class SwitchComponent {
@HostBinding('attr.id')
externalId = '';
@Input()
set id(value: string) {
this._ID = value;
this.externalId = null;
}
get id() {
return this._ID;
}
private _ID = '';
switch() { ... }
}
To fix the duplicate id
attribute we need to delete it once it has been passed in via the @Input
. Angular by default will "reflect" or render the values passed into our @Input()
properties into the dom as an HTML attribute. This usually is for convenience for things like CSS selectors and other HTML behavior, but this case causes issues.
@HostBinding('attr.id')
externalId = '';
@Input()
set id(value: string) {
this._ID = value;
this.externalId = null;
}
get id() {
return this._ID;
}
private _ID = '';
The @HostBinding()
allows us to bind a value to the host element, the host element being app-switch
. In this case, we are setting the id attribute (attr.id
). Our id
@Input
property we change to become a setter so every time the id
input changes it sets the private _ID
to hold the value and then sets the HostBinding
property externalId
to null
. By setting the host binding property to null
Angular removes the attribute from the DOM solving our duplicate id issue.
To see the full working example check out the link below. If you want a more in depth tutorial on how to make a custom Angular Form Control checkout my blog post "Angular Custom Form Controls with Reactive Forms and NgModel".