← 解説「@Input / @Output / @ViewChild — デコレータ時代の部品API」に戻る
サンプルコードで身につける: @Input / @Output / @ViewChild — デコレータ時代の部品API
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1新旧対訳表 — コンポーネントAPI一覧
デコレータAPIと関数APIの対応を一覧にします。テンプレート側の書き方([]や())は新旧で同じなので、変わるのはクラス側だけです。
// ── クラス側の対訳(テンプレート側は新旧共通)──
//
// 旧(プロパティ) 新(シグナル)
// @Input() name = ''; name = input('');
// @Input({ required: true }) input.required<number>()
// @Output() saved = saved = output<string>();
// new EventEmitter<string>();
// @ViewChild('el') el; el = viewChild('el');
// @ContentChild(Cmp) c; c = contentChild(Cmp);
//
// 読み方の切り替え:
// 旧 → this.name(そのまま)、変更検知は ngOnChanges
// 新 → this.name()(シグナル)、変更検知は computed / effect
2ngOnChanges — 入力の変化を受け止める旧作法
デコレータ版の@Input()はシグナルではないので、値の変化にcomputedで反応できません。代わりにngOnChangesフックがSimpleChangesという変更履歴を受け取ります。旧コードの頻出パターンです。
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({ selector: 'app-user-detail', template: '...' })
export class UserDetailComponent implements OnChanges {
@Input() userId = 0;
user?: User;
// 入力が変わるたびに呼ばれる。changes には変更前後の値が入る
ngOnChanges(changes: SimpleChanges) {
if (changes['userId']) {
console.log(
changes['userId'].previousValue, // 変更前
changes['userId'].currentValue, // 変更後
);
this.loadUser(this.userId); // IDが変わったら取得し直す
}
}
}
// 現代版: userId = input(0) にして
// user = resource({ params: () => ({ id: this.userId() }), ... })
3EventEmitter の正体は Observable
EventEmitterはRxJSのSubjectを継承しているため、emitで流した値をpipeで加工したりsubscribeしたりできます。凝った旧コードではこの性質を使った書き方に出会うことがあります。
import { Component, Output, EventEmitter } from '@angular/core';
import { debounceTime } from 'rxjs';
@Component({
selector: 'app-search-box',
template: '<input (input)="onInput($any($event.target).value)">',
})
export class SearchBoxComponent {
@Output() search = new EventEmitter<string>();
constructor() {
// EventEmitter は Observable でもあるので pipe できる。
// こういう「自分で自分を購読する」旧コードに出会うことがある
this.search.pipe(debounceTime(300)).subscribe((q) => {
console.log('検索実行:', q);
});
}
onInput(value: string) {
this.search.emit(value);
}
}
4@ViewChild の時差と static オプション
デコレータ版のViewChildは「いつから値が入っているか」という時差の罠が有名でした。ngAfterViewInitまで待つのが基本形です。viewChild()シグナルがこの悩みをどう消したかも対比します。
import {
Component, ViewChild, ElementRef, AfterViewInit,
} from '@angular/core';
@Component({
selector: 'app-focus-input',
template: '<input #nameInput>',
})
export class FocusInputComponent implements AfterViewInit {
@ViewChild('nameInput') nameInput?: ElementRef<HTMLInputElement>;
constructor() {
// ここではまだ undefined!(ビューが未構築)
}
ngAfterViewInit() {
// ビュー構築後のこのフックでやっと使える
this.nameInput?.nativeElement.focus();
}
}
// 現代版: nameInput = viewChild<ElementRef>('nameInput') にして
// effect(() => this.nameInput()?.nativeElement.focus());
// → シグナルなので「値が入ったら動く」が自然に書ける
5混在コンポーネントを読んでみる
移行途中のコードでは新旧APIが1つのクラスに同居します。実際にありそうな混在コンポーネントで、どの行がどちらの世代かを判定する練習をしましょう。目印はデコレータの有無です。
@Component({ selector: 'app-order-row', template: '...' })
export class OrderRowComponent {
// 旧: デコレータ → ただのプロパティ。this.order で参照
@Input() order!: Order;
// 新: 関数 → シグナル。this.highlighted() で参照
highlighted = input(false);
// 旧: EventEmitter
@Output() removed = new EventEmitter<number>();
// 新: output()
selected = output<number>();
// 混在時の注意: computed から読めるのはシグナルだけ。
// this.order を computed に入れても変化に追従しない
label = computed(() =>
this.highlighted() ? '★ 注目の注文' : '注文',
);
}