---
title: "Angular: disabled状態を持つカスタムコントロールとSignals"
slug: 9b110500fe16
created_time: 2024-07-10T03:05:00.000Z
last_edited_time: 2025-06-11T08:36:00.000Z
category: Tech
tags:
  - Angular
  - Signals
  - Forms
published: true
locale: ja
---
Angularでカスタムコントロールとして機能するコンポーネントを実装する場合、そのコントロールに `disabled` プロパティがある場合、Signals で実装するのは少し工夫が必要になる。端的に言えば `input()` と `model()` だけでは実現できない。

## disabled の実装

次のようなテンプレートで用いられる `<app-checkbox>` コンポーネントを想定する。このコンポーネントはHTML標準の`<input>`によるチェックボックスと同じくコントロールを不活性にする `disabled` プロパティを持つとする。また、`<input>` と同じく `disabled` 属性によって不活性にすることもできるとする。さらに、AngularのForms APIによってフォームコントロールの `disable()` メソッドから不活性にもできるとする。この3つの要件を満たせるカスタムコントロールを作ることを考える。

```typescript
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [AppCheckbox, ReactiveFormsModule],
  template: `
  <div>
    <app-checkbox disabled />
    <app-checkbox [disabled]="true" />
    <app-checkbox [formControl]="formControl" /> 
  </div>
  `,
})
export class App {
  formControl = new FormControl(false);

  constructor() {
    this.formControl.disable();
  }
}
```

Angular v18.0の現状では、この要件すべてを満たすには `input()`や`model()`だけではなく`@Input()` デコレータを使った実装が必要になる。具体的には次のような実装がSignalを使った最も簡素なものになるだろう。不活性化状態を保持するのはプライベートフィールドの`#disabled` で、WritableなSignalである。そして、`@Input()`デコレータを付与したセッターによって`#disabled`に受け取った値をセットしており、同様に`ControlValueAccessor`として`setDisabledState`メソッドでも受け取った値をセットしている。そして`transform: booleanAttribute`設定によって `disabled` 属性でも不活性化できる。

```typescript
@Component({
  selector: 'app-checkbox',
  standalone: true,
  template: `
  <label>
    <input type="checkbox" 
      #input 
      [checked]="checked" 
      (change)="onInputChange(input.checked)" 
      [disabled]="disabled"> 
    <span [style.textDecoration]="disabled ? 'line-through' : 'unset'">checkbox<span>
  <label>
  `,
  providers: [
    { provide: NG_VALUE_ACCESSOR, useExisting: AppCheckbox, multi: true },
  ],
})
export class AppCheckbox implements ControlValueAccessor {
  readonly #disabled = signal(false);
  
  @Input({ transform: booleanAttribute })
  set disabled(value: boolean) {
    this.#disabled.set(value);
  }
  get disabled(): boolean {
    return this.#disabled();
  }

  setDisabledState(isDisabled: boolean) {
    this.#disabled.set(isDisabled);
  }
}
```

ここからはなぜこのような実装が必要になるのかを説明する。

### Input Signalは読み取り専用である

`@Input()` デコレータに代わるコンポーネントのインプット宣言方法として `input()` 関数が導入されたが、この関数が返すInput Signalはコンポーネント内部からは読み取り専用である。したがって、`setDisabledState`メソッドが実装できなくなる。よって、`disabled`にInput Signalは使えない。

```typescript
export class AppCheckbox implements ControlValueAccessor {
  readonly disabled = input(false, { transform: booleanAttribute });

  setDisabledState(isDisabled: boolean) {
    this.disabled.set(isDisabled); // <-- ERROR!!
  }
}
```

### Model Inputは変換できない

 `input()` 関数と違ってコンポーネント内部で書き込み可能なものとして`model()`関数も導入されているが、こちらの場合は `disabled` 属性による不活性化を可能にするための transform オプションを持たない。よって、`disabled`にModel Inputも使えない。

```typescript
export class AppCheckbox implements ControlValueAccessor {
  readonly disabled = model(false, { 
    transform: booleanAttribute // <-- ERROR!!
  });

  setDisabledState(isDisabled: boolean) {
    this.disabled.set(isDisabled);
  }
}
```

なぜModel Inputが値の変換をサポートしていないのかについては、GitHubのIssueでAngularチームのテクニカルリードであるAlexからコメントがされている。双方向バインディングのメンタルモデルからすると、親からバインディングした値と内部で保持される値が違うというのは混乱を招きやすいということが主な理由だ。

https://github.com/angular/angular/issues/55166#issuecomment-2032150999

以上の理由から、現状のSignal APIでは`@Input()`デコレータを使わずに冒頭の3つの要件を満たすことはできない。もちろん `disabled` 属性によって不活性化できる要件を無視すれば`model()`で満足できるが、Signalsで統一された実装のために要件を妥協するかといわれればそれは選ばないだろう。`@Input()`デコレータを使っていても現状では非推奨化もされていないし、状態の保持がSignal化されていればそれだけでパフォーマンスの最適化やZoneless化には寄与するわけだから、特に何も失うことはない。

また、現状のSignalsとForm APIsが噛み合っていないことについてはAngular開発ロードマップの中で高い優先度で取り組まれているので、今回説明したワークアラウンドについてはそのうち不要になるだろう。それまではこのやり方が無難だと思われる。