- Angular
Angularコンポーネントの作り方|standaloneとimportsの基本
Angularのコンポーネントとは、@Component デコレーターを付けたTypeScriptのクラスに、テンプレート(HTML)とスタイル(CSS)を組み合わせた画面の部品です。 ng generate component 名前 で作り、使う側のコンポーネントの imports に入れて、テンプレートにセレクター(タグ名)を書けば表示されます。現在の Angular では、コンポーネントは何も指定しなくても standalone(NgModule に登録しなくても単体で使える形式)です。
この記事では、Angular 22.2.1 で新規プロジェクトを作り、すべてのコードをビルドして Chromium で表示した結果を載せています。ng g c が生成する4つのファイルを1つずつ読み解き、@Component の全プロパティを一覧にしました。imports に入れ忘れたときの「NG8001: is not a known element」「NG8002」などのビルドエラーも実際に出して、直し方を示します。インラインテンプレートと外部ファイルの違い、:host とスタイルのカプセル化(スタイルの効く範囲を部品の中に閉じる仕組み)の3モードも、同じ条件で比べました。
確認環境: Linux(x64)/Node.js 24.21.0/Angular CLI 22.2.1・Angular 22.2.1/TypeScript 6.0.3/Vitest 5.0.3/Playwright 1.56.0(Chromium)。出力中のパスは /home/taro/ に置き換えています。
Angularコンポーネントの早見表
| やりたいこと | 書き方 | 補足 |
|---|---|---|
| コンポーネントを作る | ng g c user-card |
user-card/ に .ts・.html・.css・.spec.ts の4ファイル |
| 1ファイルで作る | ng g c badge -t -s --skip-tests |
テンプレートとスタイルを .ts の中に書く |
| 別のコンポーネントで使う | imports: [UserCard] + テンプレートに <app-user-card /> |
imports に入れないと NG8001 |
| タグ名を決める | selector: 'app-user-card' |
接頭辞 app は ng new --prefix で変更できる |
| HTML を外部ファイルにする | templateUrl: './user-card.html' |
template と同時には書かない |
| HTML を直接書く | template: `...` |
短いテンプレート向き |
| CSS を外部ファイルにする | styleUrl: './user-card.css' |
複数なら styleUrls: [...] |
| CSS を直接書く | styles: `...` |
文字列または文字列の配列 |
| 親から値を受け取る | name = input.required<string>() |
渡し忘れると NG8008 |
| 親へイベントを送る | selected = output<string>() |
親は (selected)="..." で受ける |
| 自分自身(ホスト要素)を装飾する | :host { display: block; } |
条件付きは :host(.active) |
| ホスト要素にクラスやイベントを付ける | host: { '[class.pressed]': '...' } |
デコレーターの host プロパティ |
| スタイルの効く範囲を変える | encapsulation: ViewEncapsulation.None |
既定は Emulated |
| NgModule 形式にする | standalone: false |
既定は true(書かなくてよい) |
覚えることは3つです。「クラスに @Component を付ける」「使う側の imports に入れる」「テンプレートにセレクターを書く」。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Angularコンポーネントの作り方|standaloneとimportsの基本」
ng g cが生成する4つのファイルを読む
プロジェクトのフォルダーで次のコマンドを実行します(ng g c は ng generate component の短縮形)。
ng g c product-list
CREATE src/app/product-list/product-list.css (0 bytes)
CREATE src/app/product-list/product-list.spec.ts (568 bytes)
CREATE src/app/product-list/product-list.ts (208 bytes)
CREATE src/app/product-list/product-list.html (27 bytes)
product-list.ts(クラスとデコレーター)
import { Component } from '@angular/core';
@Component({
imports: [],
selector: 'app-product-list',
styleUrl: './product-list.css',
templateUrl: './product-list.html',
})
export class ProductList {}
| 行 | 意味 |
|---|---|
@Component({...}) |
このクラスがコンポーネントであることと、その設定を Angular に伝える |
imports: [] |
このテンプレートの中で使う、ほかのコンポーネント・ディレクティブ・パイプを並べる場所 |
selector: 'app-product-list' |
テンプレートに書くタグ名。<app-product-list /> と書いた場所にこの部品が入る |
styleUrl |
この部品専用の CSS ファイル |
templateUrl |
この部品の HTML ファイル |
export class ProductList {} |
状態(プロパティ)と処理(メソッド)を書くクラス |
standalone: true はどこにも書かれていません。 公式 API リファレンスでは standalone の既定値が true とされており、書かなくても standalone として扱われます。クラス名は ProductListComponent ではなく ProductList、ファイル名は product-list.component.ts ではなく product-list.ts で生成されます。
product-list.html(テンプレート)
<p>product-list works!</p>
生成直後は1行だけです。ここに表示したい HTML を書き、{{ 式 }} の形でクラスの値を埋め込みます。
product-list.css(スタイル)
0バイトの空ファイルです。ここに書いた CSS は、この部品のテンプレートの中だけに効きます(後半の「カプセル化」で実際に確かめます)。
product-list.spec.ts(テスト)
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductList } from './product-list';
describe('ProductList', () => {
let component: ProductList;
let fixture: ComponentFixture<ProductList>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProductList],
}).compileComponents();
fixture = TestBed.createComponent(ProductList);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
| 部分 | 意味 |
|---|---|
TestBed.configureTestingModule({ imports: [ProductList] }) |
テスト用の環境にこのコンポーネントを読み込む。ここも imports |
TestBed.createComponent(ProductList) |
コンポーネントを1つ作り、操作用の fixture を受け取る |
fixture.componentInstance |
クラスのインスタンス |
await fixture.whenStable() |
描画が落ち着くまで待つ |
it('should create', ...) |
「作れること」だけを確かめる最小のテスト |
@Componentのプロパティ一覧
公式 API リファレンス(angular.dev/api/core/Component)に載っているプロパティの一覧です。上の4つ(selector・imports・template/templateUrl・styles/styleUrl)で、ほとんどの部品は書けます。
| プロパティ | 型 | 役割 | 既定値・補足 |
|---|---|---|---|
selector |
string | テンプレートでこの部品を表す CSS セレクター | タグ名のほか、属性・クラスのセレクターも使える |
imports |
配列 | テンプレートで使うコンポーネント・ディレクティブ・パイプ | standalone のときに使う |
template |
string | インラインのテンプレート | templateUrl とどちらか一方 |
templateUrl |
string | テンプレートファイルのパス | 相対パスで書く |
styles |
string または string の配列 | インラインの CSS | ― |
styleUrl |
string | CSS ファイル1つのパス | ― |
styleUrls |
string の配列 | CSS ファイル複数のパス | ― |
changeDetection |
ChangeDetectionStrategy | 変更検知(画面を更新するタイミングの判断)の方式 | 既定は OnPush |
encapsulation |
ViewEncapsulation | スタイルのカプセル化の方式 | 既定は Emulated |
standalone |
boolean | NgModule なしで使えるか | 既定は true。NgModule に入れるときだけ false |
host |
オブジェクト | ホスト要素へのプロパティ・属性・イベントの割り当て | '[class.x]'・'(click)' など |
providers |
Provider の配列 | この部品と子孫に提供するサービス | ― |
viewProviders |
Provider の配列 | この部品のビュー内の子にだけ提供するサービス | ― |
hostDirectives |
配列 | ホスト要素に適用する standalone ディレクティブ | ― |
inputs |
配列 | 入力プロパティをデコレーター側で列挙する | 通常は input() で足りる |
outputs |
string の配列 | 出力プロパティをデコレーター側で列挙する | 通常は output() で足りる |
queries |
オブジェクト | 子要素の取得(クエリー)をデコレーター側で設定する | ― |
exportAs |
string | テンプレート変数に割り当てるときの名前 | ― |
animations |
配列 | アニメーションの trigger() 定義 |
― |
preserveWhitespaces |
boolean | テンプレートの余分な空白を残すか | ― |
schemas |
配列 | 未知の要素・属性を許可するスキーマ | CUSTOM_ELEMENTS_SCHEMA など |
jit |
true | AOT コンパイラーの対象から外し、実行時にコンパイルする | ― |
コンポーネントを作って親から使う
ここからは、会員カードを表示する UserCard を作り、ルートコンポーネント App から使います。3つのファイルと App をコピーすれば、そのまま動きます。
ng g c user-card
src/app/user-card/user-card.ts
import { Component, computed, input, output } from '@angular/core';
@Component({
imports: [],
selector: 'app-user-card',
styleUrl: './user-card.css',
templateUrl: './user-card.html',
})
export class UserCard {
readonly name = input.required<string>();
readonly points = input(0);
readonly selected = output<string>();
protected readonly rank = computed(() => (this.points() >= 100 ? 'ゴールド' : 'レギュラー'));
protected select(): void {
this.selected.emit(this.name());
}
}
src/app/user-card/user-card.html
<article class="card">
<h2>{{ name() }}</h2>
<p>ポイント: {{ points() }}({{ rank() }})</p>
<button type="button" (click)="select()">選ぶ</button>
</article>
src/app/user-card/user-card.css
:host {
display: block;
border: 1px solid #ccc;
padding: 8px;
}
:host(.active) {
border-color: rgb(0, 128, 0);
}
h2 {
color: rgb(0, 0, 255);
}
使う側の src/app/app.ts と src/app/app.html です。import 文でクラスを読み込み、imports 配列に入れる、の2か所がそろって初めてテンプレートで使えます。
import { Component, signal } from '@angular/core';
import { UserCard } from './user-card/user-card';
@Component({
imports: [UserCard],
selector: 'app-root',
styleUrl: './app.css',
templateUrl: './app.html',
})
export class App {
protected readonly title = signal('shop-app');
protected readonly picked = signal('なし');
}
<h1>{{ title() }}</h1>
<app-user-card name="佐藤" [points]="120" class="active" (selected)="picked.set($event)" />
<app-user-card name="鈴木" />
<p id="picked">選択中: {{ picked() }}</p>
| テンプレートの書き方 | 意味 |
|---|---|
name="佐藤" |
文字列をそのまま渡す |
[points]="120" |
式の結果(ここでは数値 120)を渡す |
(selected)="picked.set($event)" |
子の output を受け取る。$event に emit() した値が入る |
class="active" |
ホスト要素にふつうの HTML 属性として付く |
<app-user-card ... /> |
中身がなければ自己終了タグで書ける |
ビルドして Chromium で開き、表示と計算後のスタイルを読み取った結果です。
1枚目: 佐藤 / ポイント: 120(ゴールド) / 選ぶ 枠線 rgb(0, 128, 0) h2 の色 rgb(0, 0, 255)
2枚目: 鈴木 / ポイント: 0(レギュラー) / 選ぶ 枠線 rgb(204, 204, 204)
App の h1 の色: rgb(0, 0, 0)
クリック前: 選択中: なし
1枚目の「選ぶ」をクリック後: 選択中: 佐藤
points を渡さなかった2枚目は既定値の 0 になり、class="active" を付けた1枚目だけ枠線が緑になりました。UserCard の CSS に書いた h2 { color: blue } は、App の h1 には影響していません。
importsを入れ忘れたときのエラーと直し方
NG8001とNG8002を再現する
App の imports を空にして(imports: [])ビルドすると、次のエラーが出ました。
✘ [ERROR] NG8001: 'app-user-card' is not a known element:
1. If 'app-user-card' is an Angular component, then verify that it is included in the '@Component.imports' of this component.
2. If 'app-user-card' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@Component.schemas' of this component to suppress this message. Find more at https://next.angular.dev/errors/NG8001 [plugin angular-compiler]
/home/taro/shop-app/src/app/app.html:2:0:
2 │ <app-user-card name="佐藤" [points]="120" class="active" (selecte...
╵ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Error occurs in the template of component App.
✘ [ERROR] NG8002: Can't bind to 'points' since it isn't a known property of 'app-user-card'.
1. If 'app-user-card' is an Angular component and it has 'points' input, then verify that it is included in the '@Component.imports' of this component.
2. If 'app-user-card' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@Component.schemas' of this component to suppress this message.
3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@Component.schemas' of this component. Find more at https://next.angular.dev/errors/NG8002 [plugin angular-compiler]
同じビルドで、次のエラーも一緒に出ました。app-user-card がコンポーネントとして認識されないため、(selected) がふつうの DOM イベント扱いになり、$event の型が Event になったためです。
✘ [ERROR] TS2345: Argument of type 'Event' is not assignable to parameter of type 'string'. [plugin angular-compiler]
3種類のエラーが出ていますが、原因は1つ(imports の入れ忘れ)です。 メッセージの「verify that it is included in the '@Component.imports' of this component」のとおり、直し方は次の2行です。
// 直す前
import { Component, signal } from '@angular/core';
@Component({
imports: [],
// ...
})
// 直した後
import { Component, signal } from '@angular/core';
import { UserCard } from './user-card/user-card'; // 1. クラスを読み込む
@Component({
imports: [UserCard], // 2. imports に入れる
// ...
})
コンポーネントまわりのエラー早見表
imports を正しくしたうえで、ほかの書き間違いも1つずつ試しました。エラー文はすべて実際のビルド出力です。
| エラー | 出したときの書き方 | 原因 | 直し方 |
|---|---|---|---|
NG8001: 'app-user-card' is not a known element |
imports: [] のまま <app-user-card> を使う |
imports の入れ忘れ、またはセレクターの打ち間違い | imports にクラスを入れる。タグ名を selector と見比べる |
NG8002: Can't bind to 'points' since it isn't a known property of 'app-user-card'. |
同上 | imports の入れ忘れ | 同上 |
NG8002: Can't bind to 'pointz' since it isn't a known property of 'app-user-card'. |
[pointz]="120" |
入力名の打ち間違い | input() の名前に合わせる |
NG8008: Required input 'name' from component UserCard must be specified. |
<app-user-card /> |
input.required の値を渡していない |
name="..." を渡す |
NG8004: No pipe found with name 'price'. |
{{ 1200 | price }} |
パイプを imports に入れていない | imports にパイプのクラスを入れる |
NG2011: The component 'Badge' appears in 'imports', but is not standalone and cannot be imported directly. It must be imported via an NgModule. |
子に standalone: false を書く |
NgModule 形式の部品を直接 imports した | standalone: false を外す。または NgModule 経由で読み込む |
NG2008: Could not find template file './user-card.htm'. |
templateUrl のパス違い |
ファイル名・拡張子の打ち間違い | 相対パスを確認する |
NG8113: Highlight is not used within the template of App(警告) |
使っていないものを imports に入れる |
余分な imports | 配列から外す |
ビルドエラーにならない入れ忘れもあります。 属性セレクターのディレクティブ(<p appHighlight> など)は、imports に入れなくてもビルドが通りました。appHighlight がただの HTML 属性として扱われるためです。「エラーは出ないのに効かない」ときも、まず imports を見直してください。
インラインテンプレートと外部ファイルの違い
-t(--inline-template)と -s(--inline-style)を付けると、1ファイルにまとまったコンポーネントが生成されます。
ng g c badge -t -s --skip-tests
CREATE src/app/badge/badge.ts (192 bytes)
import { Component } from '@angular/core';
@Component({
imports: [],
selector: 'app-badge',
styles: ``,
template: ` <p>badge works!</p> `,
})
export class Badge {}
これを小さなバッジに書き換えます。バッククォートで囲むので、HTML も CSS も複数行で書けます。
import { Component, input } from '@angular/core';
@Component({
selector: 'app-badge',
template: `<span class="badge">{{ label() }}</span>`,
styles: `
:host {
display: inline-block;
}
.badge {
background: rgb(255, 215, 0);
padding: 2px 6px;
}
`,
})
export class Badge {
readonly label = input('NEW');
}
App の imports に Badge を足し、テンプレートに次の2行を書いて表示しました。
<app-badge />
<app-badge label="SALE" />
1つ目: NEW display: inline-block 背景 rgb(255, 215, 0)
2つ目: SALE display: inline-block 背景 rgb(255, 215, 0)
| 比べる点 | インライン(template / styles) |
外部ファイル(templateUrl / styleUrl) |
|---|---|---|
| ファイル数 | 1つ | 3つ(+テスト) |
| 生成コマンド | ng g c badge -t -s |
ng g c user-card |
| 向いている大きさ | テンプレートが数行〜十数行の小さな部品 | 行数が多い、HTML と CSS を分けて編集したい部品 |
| 一覧性 | クラスと見た目を1画面で読める | 役割ごとにファイルが分かれる |
| 動作 | 同じ | 同じ |
| 組み合わせ | template + styleUrl のように片方だけインラインにもできる |
― |
どちらで書いても、ビルド後の動作は同じです。「スクロールせずに読めるならインライン、それ以上なら外部ファイル」を目安にすると決めやすくなります。
:hostとスタイルのカプセル化を比べる
:hostでコンポーネント自身を装飾する
コンポーネントのタグ(<app-user-card> そのもの)を ホスト要素と呼びます。ホスト要素は自分のテンプレートの外側にあるので、ふつうのセレクターでは指定できません。そこで :host を使います。
:host { display: block; } /* ホスト要素そのもの */
:host(.active) { border-color: green; } /* ホスト要素に active クラスがあるとき */
先ほどの UserCard で読み取った値です。
| 対象 | 読み取った値 |
|---|---|
1枚目(class="active")の display |
block |
| 1枚目の枠線の色 | rgb(0, 128, 0)(:host(.active) が効いている) |
| 2枚目の枠線の色 | rgb(204, 204, 204)(:host の #ccc) |
カスタム要素の display は、指定しなければインラインです。 幅や余白を持たせたい部品は、:host { display: block; } を最初に書いておくとレイアウトが安定します(ng g c 名前 --display-block で自動的に入ります)。
カプセル化の3モードを同じ条件で比べる
encapsulation を変えた3つのコンポーネントを1ファイルに用意し、それぞれ p 要素に別々のスタイルを当てます。
// src/app/enc.ts
import { Component, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-enc-emulated',
template: `<p>Emulated の p</p>`,
styles: `p { color: rgb(255, 0, 0); }`,
})
export class EncEmulated {}
@Component({
selector: 'app-enc-none',
template: `<p>None の p</p>`,
styles: `p { text-decoration: underline; }`,
encapsulation: ViewEncapsulation.None,
})
export class EncNone {}
@Component({
selector: 'app-enc-shadow',
template: `<p>ShadowDom の p</p>`,
styles: `p { font-weight: 700; }`,
encapsulation: ViewEncapsulation.ShadowDom,
})
export class EncShadow {}
App の imports に3つを足し、テンプレートに App 自身の p も置きます。
<p id="outer">App の p</p>
<app-enc-emulated />
<app-enc-none />
<app-enc-shadow />
Chromium で各 p の計算後のスタイルを読み取った結果です。
| どの p か | 文字色(Emulated が赤を指定) | 下線(None が指定) | 太さ(ShadowDom が 700 を指定) | p に付いた属性 |
|---|---|---|---|---|
| App の p | 黒 rgb(0, 0, 0) |
あり | 400 | id |
| Emulated の p | 赤 rgb(255, 0, 0) |
あり | 400 | _ngcontent-ng-c2462001973 |
| None の p | 黒 | あり | 400 | なし |
| ShadowDom の p | 黒 | あり | 700 | なし(シャドウツリーの中) |
読み取れることは3つです。
- Emulated(既定):赤は自分の
pだけに効きました。Angular が要素に_ngcontent-ng-c...という属性を付け、CSS をp[_ngcontent-ng-c2462001973] { color: rgb(255, 0, 0); }に書き換えてheadに入れているためです。ホスト要素には_nghost-ng-c...が付き、:hostは[_nghost-ng-c503633429]に書き換えられていました。 - None:下線はページ中のすべての
pに効きました。p { text-decoration: underline; }がそのまま全体のスタイルとして入るためです。今回は ShadowDom のコンポーネントの中にも届いていました(シャドウルートの中に、アプリの各コンポーネントのスタイルが5つ入っていることを確認しました)。 - ShadowDom:太字は自分の
pだけに効きました。pはシャドウツリーの中に作られ、外からquerySelector('app-enc-shadow p')で探しても見つかりません(結果はnull)。
| モード | 仕組み | スタイルが外へ出るか | 選ぶ場面 |
|---|---|---|---|
Emulated(既定) |
属性を付けてセレクターを書き換える | 出ない | 通常はこれ |
None |
何もしない(全体のスタイルになる) | 出る | 全体に効かせたいスタイルを部品側に置くとき |
ShadowDom |
ブラウザーの Shadow DOM を使う | 出ない | Web 標準の仕組みで区切りたいとき |
公式ドキュメント(Styling components)は、Emulated について「コンポーネントの外で定義したグローバルなスタイルは、Emulated の内側の要素にも影響しうる」と説明しています。今回、None の下線が Emulated の p にも付いたのはこのためです。同じページには、より厳密に区切る ExperimentalIsolatedShadowDom というモードと、カプセル化を越えるための ::ng-deep は新しく使うことを強く勧めない、という説明も載っています。
hostプロパティと属性セレクターで既存の要素を拡張する
セレクターはタグ名だけでなく、属性でも書けます。公式ドキュメント(Component selectors)によると、使えるのはタグ名・属性・クラスのセレクターと :not、それらの連結とカンマ区切りで、子孫結合子などは使えません。接頭辞に ng は使わないことも案内されています。
次の例は、appPrimary 属性を付けた button 要素だけをコンポーネントにします。host プロパティで、ホスト要素(button 自身)にクラス・属性・クリック処理を割り当てています。
// src/app/primary-button.ts
import { Component, input, signal } from '@angular/core';
@Component({
selector: 'button[appPrimary]',
template: `<ng-content /> <small>({{ count() }})</small>`,
styles: `
:host {
background: rgb(25, 118, 210);
color: rgb(255, 255, 255);
}
:host(.pressed) {
background: rgb(13, 71, 161);
}
`,
host: {
'[class.pressed]': 'count() > 0',
'[attr.aria-label]': 'label()',
'(click)': 'count.update(n => n + 1)',
},
})
export class PrimaryButton {
readonly label = input('送信');
protected readonly count = signal(0);
}
// src/app/app.ts
import { Component } from '@angular/core';
import { PrimaryButton } from './primary-button';
@Component({
imports: [PrimaryButton],
selector: 'app-root',
template: `
<button appPrimary label="注文を送信">送信する</button>
<button>ふつうのボタン</button>
`,
})
export class App {}
実行結果です。
クリック前
1つ目: 送信する(0) class="" aria-label="注文を送信" 背景 rgb(25, 118, 210)
2つ目: ふつうのボタン class="" aria-label なし 背景 rgb(239, 239, 239)
クリック後
1つ目: 送信する(1) class="pressed" aria-label="注文を送信" 背景 rgb(13, 71, 161)
2つ目: ふつうのボタン class="" aria-label なし 背景 rgb(239, 239, 239)
host のキー |
働き |
|---|---|
'[class.pressed]': 'count() > 0' |
式が真のときホスト要素に pressed クラスを付ける |
'[attr.aria-label]': 'label()' |
ホスト要素の属性に値を入れる |
'(click)': '...' |
ホスト要素のイベントを受け取る |
<ng-content /> は、タグの間に書かれた中身(ここでは「送信する」)を差し込む場所です。属性セレクターにすると button 本来の機能(フォーカスやキーボード操作)をそのまま使えるので、ボタンやリンクを拡張するときに向いています。
入力があるコンポーネントのテストを書く
input.required を持つコンポーネントを、入力を渡さずにテストで作ると次のエラーになりました。必須の入力に値が入っていないためです。
FAIL shop-app src/app/user-card/user-card.spec.ts > UserCard > 名前とランクを表示する
Error: NG0950: Input "name" is required but no value is available yet. Find more at https://v22.angular.dev/errors/NG0950
fixture.componentRef.setInput() で入力を渡すと通ります。完成形は次のとおりです。
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserCard } from './user-card';
describe('UserCard', () => {
let fixture: ComponentFixture<UserCard>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserCard],
}).compileComponents();
fixture = TestBed.createComponent(UserCard);
fixture.componentRef.setInput('name', '佐藤');
fixture.componentRef.setInput('points', 120);
await fixture.whenStable();
});
it('名前とランクを表示する', () => {
const el = fixture.nativeElement as HTMLElement;
expect(el.querySelector('h2')?.textContent).toBe('佐藤');
expect(el.querySelector('p')?.textContent).toContain('ゴールド');
});
it('ボタンで selected が発火する', () => {
let picked = '';
fixture.componentInstance.selected.subscribe((v) => (picked = v));
(fixture.nativeElement as HTMLElement).querySelector('button')!.click();
expect(picked).toBe('佐藤');
});
});
ng test --watch=false
Test Files 8 passed (8)
Tests 10 passed (10)
(プロジェクト内のほかの生成済みテストも含めた件数です。)
コンポーネントを作るときのチェックリスト
| 確認すること | 見る場所 |
|---|---|
使う側の imports に入れたか |
親コンポーネントの @Component |
タグ名が selector と一致しているか |
親のテンプレートと子の selector |
input.required の値を渡しているか |
親のテンプレート |
[名前] の入力名が input() のプロパティ名と一致しているか |
親のテンプレートと子のクラス |
パイプやディレクティブも imports に入れたか |
親コンポーネントの @Component |
ホスト要素に display を指定したか |
子の CSS の :host |
| スタイルを全体に効かせる必要が本当にあるか | encapsulation(既定の Emulated のままでよいか) |
テストで必須入力を setInput() しているか |
.spec.ts |
よくある質問
Q. standalone: true は書いたほうがよいですか?
A. 書かなくて構いません。22.2.1 の ng g c は standalone を書かずに生成し、そのまま imports に入れて使えました。公式 API リファレンスでも既定値は true です。standalone: false を書くのは、NgModule の declarations に入れる場合だけです。
Q. imports と TypeScript の import 文は何が違いますか?
A. import 文は「このファイルでそのクラス名を使えるようにする」TypeScript の機能、imports 配列は「このテンプレートでその部品を使えるようにする」Angular の設定です。両方が必要で、imports 配列に入れ忘れると NG8001 になります。
Q. 生成されたコンポーネントに changeDetection が書かれていませんが、どの方式になりますか?
A. 公式 API リファレンスでは changeDetection の既定が OnPush とされています。ng g c 名前 --change-detection Eager で生成すると、changeDetection: ChangeDetectionStrategy.Eager が明記された形になりました。状態を signal() や input() で持てば、OnPush のままで画面が更新されます(この記事の例はすべて既定のままです)。
Q. styleUrl と styleUrls はどちらを使いますか?
A. CSS ファイルが1つなら styleUrl(文字列)、複数なら styleUrls(配列)です。ng g c は styleUrl で生成します。
まとめ
- コンポーネント=
@Componentを付けたクラス+テンプレート+スタイル。ng g c 名前で4ファイルが生成されます。 - standalone が既定なので、
standalone: trueは書かずに、使う側のimportsに入れるだけで使えます。 - 「NG8001: is not a known element」「NG8002」が出たら、まず
importsを確認します。属性セレクターのディレクティブは入れ忘れてもエラーにならない点に注意します。 - インライン(
template・styles)と外部ファイル(templateUrl・styleUrl)は動作が同じ。部品の大きさで選びます。 :hostでホスト要素を装飾し、最初にdisplayを決めておくとレイアウトが安定します。- カプセル化は既定の
Emulatedが基本。Noneにしたスタイルはページ全体に効くことを、計算後のスタイルで確認しました。 - 必須入力のあるコンポーネントのテストは
setInput()で値を渡します。
参考・出典
- Angular 公式 API リファレンス「Component」(angular.dev/api/core/Component)
- Angular 公式ドキュメント「Styling components」(angular.dev/guide/components/styling)
- Angular 公式ドキュメント「Component selectors」(angular.dev/guide/components/selectors)
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Angularコンポーネントの作り方|standaloneとimportsの基本」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。