Cory Rylan

My name is , Google Developer Expert, Speaker, Software Developer. Building Design Systems and Web Components.

Follow @coryrylan
Angular

Preventing Attribute Reflection in Angular

Cory Rylan

- 4 minutes

Updated

This article has been updated to the latest version Angular 17 and tested with Angular 16. The content is likely still applicable for all Angular 2 + versions.

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 ids 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.

Duplicate id attributes in HTML

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.

Duplicate id attributes in HTML

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

View Demo Code   
Twitter Facebook LinkedIn Email
 

No spam. Short occasional updates on Web Development articles, videos, and new courses in your inbox.

Related Posts

Angular

Creating Dynamic Tables in Angular

Learn how to easily create HTML tables in Angular from dynamic data sources.

Read Article
Web Components

Reusable Component Patterns - Default Slots

Learn about how to use default slots in Web Components for a more flexible API design.

Read Article
Web Components

Reusable Component Anti-Patterns - Semantic Obfuscation

Learn about UI Component API design and one of the common anti-patterns, Semantic Obfuscation.

Read Article