Angular - binding to protected properties

Angular - binding to protected properties

Before Angular@14 we could only bind public properties in views. Even though, the properties were meant to be private to the component, we had to make them public. This led to exposing component internals to the consumer. Now, we can make use of typescript's protected access modifier to prevent users from accessing internals and still be able to bind those properties in the view.

Public properties

Before Angular@14, if you tried binding protected property in the view, the compiler would complain that the property is not accessible outside class.

In this example, I'm using color setter to prepend a value with the @ symbol. It's assigned to the colorWithHash property and displayed in the view.

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-public-test',
  standalone: true,
  template: `<span>{{ colorWithHash }}</span>`,
})
export class PublicTestComponent {
  colorWithHash: string | undefined;

  @Input() set color(color: string) {
    this.colorWithHash = `@${color}`;
  }
}

Take a look at the IDE auto-completion - both properties are available.

Since both setter and colorWithHash are public we can access them directly, i.e. by using @ViewChild decorator. Now, I can override internal value not meant to be accessed from the outside.

import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { PublicTestComponent } from './public-test.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [PublicTestComponent],
  template: `<app-public-test [color]="'red'" #publicTestComponent></app-public-test>`,
})
export class AppComponent implements AfterViewInit {
  @ViewChild('publicTestComponent') publicTestComponent!: PublicTestComponent;

  ngAfterViewInit(): void {
    this.publicTestComponent.color = 'green';
    this.publicTestComponent.colorWithHash = '-1-2-';
  }
}

Protected properties

The component body is almost identical to the previous one. The only difference is the colorWithHash property is now protected.

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-protected-test',
  standalone: true,
  template: `<span>{{ colorWithHash }}</span>`,
})
export class ProtectedTestComponent {
  protected colorWithHash: string | undefined;

  @Input() set color(color: string) {
    this.colorWithHash = `@${color}`;
  }
}

Take a look at the IDE auto-completion - only the color setter is available.

Now, I can't access protected properties and have to use only public ones, which is an expected behavior. The protected value is properly interpolated in the view.

import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { ProtectedTestComponent } from './protected-test.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ProtectedTestComponent],
  template: `<app-protected-test [color]="'red'" #protectedTestComponent></app-protected-test>`,
})
export class AppComponent implements AfterViewInit {
  @ViewChild('protectedTestComponent') protectedTestComponent!: ProtectedTestComponent;

  ngAfterViewInit(): void {
    this.protectedTestComponent.color = 'white';
  }
}

Footnote

Please do not confuse Typescript's private / protected keywords with the Javascript's native private fields prefixed with the # symbol. These are completely different topics. Nevertheless, there are still ways to access protected or private members. You can always cheat the compiler or iterate through object properties. Protected fields are meant to hide the class internals and remove users' uncertainty about whether to use a certain property or not.