feat(ja-JP): add skill sub-reference translations (angular, remotion, etc.)

Translated 85 skill sub-reference files to achieve full parity with
the English source:

- skills/angular-developer/references/ — 35 files (all references)
- skills/remotion-video-creation/rules/ — 28 files (all rules)
- skills/tinystruct-patterns/references/ — 5 files
- skills/openclaw-persona-forge/references/ — 6 files
- skills/skill-comply/prompts/ — 3 files
- skills/lead-intelligence/agents/ — 4 files
- skills/brand-voice/references/ — 1 file
- skills/frontend-slides/ — 2 files
- hooks/memory-persistence/README.md — 1 file

English source parity: 0 missing files (excluding rules/zh/, internal
docs, and experimental examples absent from zh-CN)
This commit is contained in:
Claude
2026-05-18 06:15:16 +09:00
parent 63624426c8
commit 174e31b3fc
85 changed files with 8409 additions and 0 deletions

View File

@@ -0,0 +1,160 @@
# Angular アニメーション
Angularで要素をアニメーションさせる場合は、**まず `package.json` でプロジェクトのAngularバージョンを確認してください**。
モダンなアプリケーション(**Angular v20.2以上**)では、`animate.enter``animate.leave` を使ったネイティブCSSを優先してください。古いアプリケーションでは、非推奨の `@angular/animations` パッケージを使用する必要がある場合があります。
## 1. ネイティブCSSアニメーションv20.2以上 推奨)
モダンなAngularは `animate.enter``animate.leave` を提供し、要素がDOMに追加・削除される際にアニメーションさせます。これらは適切なタイミングでCSSクラスを適用します。
### `animate.enter` と `animate.leave`
要素に直接使用して、enter・leaveフェーズ中にCSSクラスを適用します。Angularはアニメーション完了時にenterクラスを自動的に削除します。`animate.leave` の場合、Angularはアニメーションが完了するまで要素をDOMから削除しません。
`animate.enter` の例:
```html
@if (isShown()) {
<div class="enter-container" animate.enter="enter-animation">
<p>The box is entering.</p>
</div>
}
```
```css
/* トランジションを使用する場合は開始スタイルを定義してください */
.enter-container {
border: 1px solid #dddddd;
margin-top: 1em;
padding: 20px;
font-weight: bold;
font-size: 20px;
}
.enter-container p {
margin: 0;
}
.enter-animation {
animation: slide-fade 1s;
}
@keyframes slide-fade {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
```
_注意: `animate.leave` は削除される子要素に追加できます。_
### イベントバインディングとサードパーティライブラリ
`(animate.enter)``(animate.leave)` にバインドして、関数を呼び出したりGSAPなどのJSライブラリを使用することができます。
```html
@if(show()) {
<div (animate.leave)="onLeave($event)">...</div>
}
```
```ts
import { AnimationCallbackEvent } from '@angular/core';
onLeave(event: AnimationCallbackEvent) {
// カスタムアニメーションロジックをここに記述
// 重要: Angularが要素を削除できるよう、完了時に必ず animationComplete() を呼び出してください!
event.animationComplete();
}
```
## 2. 高度なCSSアニメーション
CSSは高度なアニメーションシーケンスのための強力なツールを提供しています。
### 状態とスタイルのアニメーション
プロパティバインディングを使用して要素のCSSクラスを切り替え、トランジションをトリガーします。
```html
<div [class.open]="isOpen">...</div>
```
```css
div {
transition: height 0.3s ease-out;
height: 100px;
}
div.open {
height: 200px;
}
```
### Autoハイトのアニメーション
`css-grid` を使用してautoハイトへのアニメーションが可能です。
```css
.container {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s;
}
.container.open {
grid-template-rows: 1fr;
}
.container > div {
overflow: hidden;
}
```
### スタッガリングと並列アニメーション
- **スタッガリング**: リスト内のアイテムに異なる値の `animation-delay` または `transition-delay` を使用します。
- **並列**: `animation` ショートハンドで複数のアニメーションを適用します(例:`animation: rotate 3s, fade-in 2s;`)。
### プログラムによる制御
標準のWeb APIを使用してアニメーションを直接取得します
```ts
const animations = element.getAnimations();
animations.forEach((anim) => anim.pause());
```
## 3. レガシーアニメーションDSL非推奨
古いプロジェクトv20.2以前、または `@angular/animations` が既に大量に使用されているプロジェクトでは、コンポーネントメタデータDSLを使用します。
**重要:** レガシーアニメーションと `animate.enter`/`leave` を同じコンポーネント内で混在させないでください。
### セットアップ
```ts
bootstrapApplication(App, {
providers: [provideAnimationsAsync()],
});
```
### トランジションの定義
```ts
import {signal} from '@angular/core';
import {trigger, state, style, animate, transition} from '@angular/animations';
@Component({
animations: [
trigger('openClose', [
state('open', style({opacity: 1})),
state('closed', style({opacity: 0})),
transition('open <=> closed', [animate('0.5s')]),
]),
],
template: `<div [@openClose]="isOpen() ? 'open' : 'closed'">...</div>`,
})
export class OpenClose {
isOpen = signal(true);
}
```

View File

@@ -0,0 +1,410 @@
# Angular Aria
Angular Aria`@angular/aria`は、一般的なWAI-ARIAパターンを実装するヘッドレスでアクセシブルなディレクティブのコレクションです。これらのディレクティブは、キーボードインタラクション、ARIA属性、フォーカス管理、スクリーンリーダーのサポートを処理します。
**AIエージェントとしての役割は、HTMLの構造とCSSスタイリングを提供すること**であり、ディレクティブが複雑なアクセシビリティロジックを処理します。
## ヘッドレスコンポーネントのスタイリング
Angular Ariaコンポーネントはヘッドレスであるため、デフォルトのスタイルは付属していません。ディレクティブが自動的に適用するARIA属性や構造クラスに基づいて、CSSで各状態をスタイリングする**必要があります**。
CSSでターゲットとする一般的なARIA属性
- `[aria-expanded="true"]` / `[aria-expanded="false"]`
- `[aria-selected="true"]`
- `[aria-disabled="true"]`
- `[aria-current="page"]`(ナビゲーション用)
---
**重要**: このパッケージを使用する前に、パッケージマネージャーでインストールする必要があります。プロジェクトにインストールされていることを確認してください。必要な場合は `npm install @angular/aria` でインストールしてください。
## 1. アコーディオン
関連するコンテンツを展開・折りたたみ可能なセクションに整理します。
**使用場面:** アコーディオンは、コンテンツの多いページでスクロールを減らすために、ユーザーが一度に1つずつ展開できる論理的なグループにコンテンツを整理するレイアウトコンポーネントです。FAQ、長いフォーム、または情報の段階的な開示に使用しますが、プライマリナビゲーションや、ユーザーが複数のコンテンツセクションを同時に表示する必要があるシナリオには使用しないでください。
**インポート:** `import { AccordionContent, AccordionGroup, AccordionPanel, AccordionTrigger } from '@angular/aria/accordion';`
**ディレクティブ:** `ngAccordionGroup``ngAccordionTrigger``ngAccordionPanel``ngAccordionContent`(遅延ロード用)。
```ts
@Component({
selector: 'app-cmp',
imports: [AccordionContent, AccordionGroup, AccordionPanel, AccordionTrigger],
template: `...`,
styles: [],
})
export class App {
protected readonly title = signal('angular-app');
}
```
```html
<div ngAccordionGroup [multiExpandable]="false">
<div class="accordion-item">
<button ngAccordionTrigger panelId="panel-1" class="accordion-header">
Section 1
<span class="icon"></span>
</button>
<div ngAccordionPanel panelId="panel-1" class="accordion-panel">
<ng-template ngAccordionContent>
<p>Lazy loaded content here.</p>
</ng-template>
</div>
</div>
</div>
```
**スタイリング戦略:**
トリガーの `[aria-expanded]` 属性をターゲットにしてアイコンを回転させ、パネルの表示をスタイリングします。
```css
.accordion-header[aria-expanded='true'] .icon {
transform: rotate(180deg);
}
/* パネルディレクティブがDOM削除を処理しますが、トランジションをスタイリングできます */
.accordion-panel {
padding: 1rem;
border-top: 1px solid #ccc;
}
```
---
## 2. リストボックス
オプションのリストを表示するための基本的なディレクティブです。可視の選択リスト(ドロップダウンではない)に使用します。
**使用場面:** 可視の選択可能なリスト(単一または複数選択)。
**インポート:** `import {Listbox, Option} from '@angular/aria/listbox';`
**ディレクティブ:** `ngListbox``ngOption`
```ts
@Component({
selector: 'app-cmp',
imports: [Listbox, Option],
template: `...`,
styles: [],
})
export class App {
protected readonly title = signal('angular-app');
}
```
```html
<!-- 水平または垂直方向 -->
<ul ngListbox [(values)]="selectedItems" orientation="horizontal" [multi]="true">
<li ngOption value="apple" class="option">Apple</li>
<li ngOption value="banana" class="option">Banana</li>
</ul>
```
**スタイリング戦略:**
選択状態には `[aria-selected="true"]`、フォーカスされたアイテムには `:focus-visible` または `[data-active]` をターゲットにしますAngular Ariaはroving tabindexまたはactivedescendantを使用します
```css
.option {
padding: 8px;
cursor: pointer;
}
.option[aria-selected='true'] {
background: #e0f7fa;
font-weight: bold;
}
/* フォーカス状態はariaで管理されます */
.option:focus-visible {
outline: 2px solid blue;
}
```
---
## 3. コンボボックス、セレクト、マルチセレクト
これらのパターンは、`ngCombobox` とポップアップ内の `ngListbox` を組み合わせます。
- **コンボボックス**: テキスト入力 + ポップアップ(オートコンプリートに使用)。
- **セレクト**: 読み取り専用コンボボックス + 単一選択リストボックス。
- **マルチセレクト**: 読み取り専用コンボボックス + 複数選択リストボックス。
**使用場面:** コンボボックスは、テキスト入力をポップアップと同期させる低レベルのプリミティブディレクティブで、オートコンプリート、セレクト、マルチセレクトパターンの基盤となるロジックとして機能します。カスタムフィルタリング、独自の選択要件、または標準の文書化されたコンポーネントから逸脱した特殊な入力-ポップアップ連携を構築する場合に特に使用してください。
**インポート:**
```
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';
import {Listbox, Option} from '@angular/aria/listbox';
```
**ディレクティブ:** `ngCombobox``ngComboboxInput``ngComboboxPopupContainer``ngListbox``ngOption`
```html
<!-- 例:標準セレクト -->
<div ngCombobox [readonly]="true">
<button ngComboboxInput class="select-trigger">
{{ selectedValue() || 'Choose an option' }}
</button>
<ng-template ngComboboxPopupContainer>
<ul ngListbox [(values)]="selectedValue" class="dropdown-menu">
<li ngOption value="option1">Option 1</li>
<li ngOption value="option2">Option 2</li>
</ul>
</ng-template>
</div>
```
**スタイリング戦略:**
ポップアップコンテナをコンテンツの上に浮かぶドロップダウンのように見せるスタイリングをしますCDK Overlayと組み合わせることが多い
```css
.select-trigger {
width: 200px;
padding: 8px;
text-align: left;
}
.dropdown-menu {
list-style: none;
padding: 0;
margin: 0;
border: 1px solid #ccc;
background: white;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
```
---
## 4. メニューとメニューバー
アクション、コマンド、コンテキストメニュー用(フォーム選択には使用しない)。
**使用場面:** メニューバーは、インターフェース全体に渡って持続するデスクトップスタイルのアプリケーションコマンドバー(例:ファイル、編集、表示)を構築するための高レベルのナビゲーションパターンです。完全な水平キーボードサポートを備えた論理的なトップレベルカテゴリへの複雑なコマンドの整理に最適ですが、シンプルなスタンドアロンアクションリストや水平スペースが制約されるモバイルファーストのレイアウトには避けてください。
**インポート:** `import {MenuBar, Menu, MenuContent, MenuItem} from '@angular/aria/menu';`
**ディレクティブ:** `ngMenuBar``ngMenu``ngMenuItem``ngMenuTrigger`
```html
<!-- メニューバーの例 -->
<ul ngMenuBar class="menubar">
<li ngMenuItem value="file">
<button ngMenuTrigger [menu]="fileMenu">File</button>
</li>
</ul>
<ul ngMenu #fileMenu="ngMenu" class="menu">
<li ngMenuItem value="new">New</li>
<li ngMenuItem value="open">Open</li>
</ul>
```
**スタイリング戦略:**
メニューバーにはflexboxを使用します。トリガーの状態に基づいてサブメニューを表示・非表示にします。
```css
.menubar {
display: flex;
gap: 10px;
list-style: none;
padding: 0;
}
.menu {
background: white;
border: 1px solid #ccc;
padding: 5px 0;
}
.menu li {
padding: 5px 15px;
cursor: pointer;
}
```
---
## 5. タブ
1つのパネルのみが表示される層状のコンテンツセクション。
**使用場面:** タブコンポーネントは、関連するコンテンツを明確なナビゲート可能なセクションに整理し、ユーザーがページを離れることなくカテゴリやビューを切り替えられるようにします。設定パネル、複数トピックのドキュメント、ダッシュボードに理想的ですが、順次ワークフローステッパーや7〜8セクションを超えるナビゲーションには避けてください。
**インポート:** `import {Tab, Tabs, TabList, TabPanel, TabContent} from '@angular/aria/tabs';`
**ディレクティブ:** `ngTabs``ngTabList``ngTab``ngTabPanel``ngTabContent`
```html
<div ngTabs>
<ul ngTabList class="tab-list">
<li ngTab value="profile" class="tab-btn">Profile</li>
<li ngTab value="security" class="tab-btn">Security</li>
</ul>
<div ngTabPanel value="profile" class="tab-panel">
<ng-template ngTabContent>Profile Settings</ng-template>
</div>
<div ngTabPanel value="security" class="tab-panel">
<ng-template ngTabContent>Security Settings</ng-template>
</div>
</div>
```
**スタイリング戦略:**
タブボタンの `[aria-selected="true"]` をターゲットにします。
```css
.tab-list {
display: flex;
border-bottom: 2px solid #ccc;
list-style: none;
padding: 0;
}
.tab-btn {
padding: 10px 20px;
cursor: pointer;
border-bottom: 2px solid transparent;
}
.tab-btn[aria-selected='true'] {
border-bottom-color: blue;
font-weight: bold;
}
.tab-panel {
padding: 20px;
}
```
---
## 6. ツールバー
関連するコントロールをグループ化します(テキストフォーマットなど)。
**使用場面:** ツールバーは、頻繁にアクセスされる関連コントロールを1つの論理コンテナにグループ化するための組織コンポーネントです。テキストフォーマットやメディアコントロールなど、繰り返しアクションが必要なワークフローのキーボード効率矢印キーナビゲーションと視覚的構造を強化するために最適です。
**インポート:** `import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar';`
**ディレクティブ:** `ngToolbar``ngToolbarWidget``ngToolbarWidgetGroup`
```html
<div ngToolbar class="toolbar">
<div ngToolbarWidgetGroup [multi]="true" role="group" aria-label="Formatting">
<button ngToolbarWidget value="bold" class="tool-btn">B</button>
<button ngToolbarWidget value="italic" class="tool-btn">I</button>
</div>
</div>
```
**スタイリング戦略:**
ツールバー内の `[aria-pressed="true"]`(トグルボタン用)または `[aria-checked="true"]`(ラジオグループ用)をターゲットにします。
```css
.toolbar {
display: flex;
gap: 5px;
padding: 8px;
background: #f5f5f5;
}
.tool-btn {
padding: 5px 10px;
border: 1px solid #ccc;
}
.tool-btn[aria-pressed='true'],
.tool-btn[aria-checked='true'] {
background: #ddd;
}
```
---
## 7. ツリー
階層データを表示します(ファイルシステム、ネストされたナビゲーションなど)。
**使用場面:** ツリーコンポーネントは、ファイルシステム、組織図、複雑なサイトアーキテクチャなど、深くネストされた階層データ構造のナビゲーションと表示のために設計されています。ユーザーがブランチを展開・折りたたむ必要がある複数レベルの関係に特に使用しますが、フラットリスト、データテーブル、またはシンプルな選択メニューには避けてください。
**インポート:** `import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';`
**ディレクティブ:** `ngTree``ngTreeItem``ngTreeGroup`
```html
<ul ngTree class="tree">
<li ngTreeItem value="documents">
<span class="tree-label">Documents</span>
<ul ngTreeGroup class="tree-group">
<li ngTreeItem value="resume">Resume.pdf</li>
</ul>
</li>
</ul>
```
**スタイリング戦略:**
`[aria-expanded]` をターゲットにして子を表示・非表示にするか、シェブロンアイコンを回転させます。ネストされたグループに `padding-left` を使用して階層を表示します。
```css
.tree,
.tree-group {
list-style: none;
padding-left: 20px;
}
.tree-label::before {
content: '> ';
display: inline-block;
transition: transform 0.2s;
}
li[aria-expanded='true'] > .tree-label::before {
transform: rotate(90deg);
}
```
## 8. グリッド
矢印キーでナビゲーションを可能にする、セルの双方向インタラクティブコレクションです。
**使用場面:** データテーブル、カレンダー、スプレッドシート、インタラクティブ要素のレイアウトパターン。
**ディレクティブ:** `ngGrid``ngGridRow``ngGridCell``ngGridCellWidget`
```html
<table ngGrid [multi]="true" [enableSelection]="true" class="grid-table">
<tr ngGridRow>
<th ngGridCell role="columnheader">Name</th>
<th ngGridCell role="columnheader">Status</th>
</tr>
<tr ngGridRow>
<td ngGridCell>Project A</td>
<td ngGridCell [(selected)]="isSelected">
<button ngGridCellWidget (activated)="onActivate()">Active</button>
</td>
</tr>
</table>
```
**スタイリング戦略:**
選択されたセルには `[aria-selected="true"]`、アクティブなセルには `:focus-visible`roving tabindexまたは コンテナの `[aria-activedescendant]` をターゲットにします。
```css
.grid-table {
border-collapse: collapse;
}
[ngGridCell] {
padding: 8px;
border: 1px solid #ddd;
}
[ngGridCell][aria-selected='true'] {
background: #e3f2fd;
}
/* フォーカス状態はroving tabindexで管理されます */
[ngGridCell]:focus-visible {
outline: 2px solid #2196f3;
outline-offset: -2px;
}
```
## エージェントへの一般的なルール
1. **これらの特定のAriaパターンを実装する際は、`<select>` などのネイティブHTML要素を使用しないでください**`ng*` ディレクティブを使用してください。
2. **CSSは手動で処理してください**: `Angular Aria` はスタイルを提供しません。ディレクティブが自動的に切り替えるネイティブARIA属性`aria-expanded``aria-selected` などをターゲットにしたCSSを記述する必要があります。
3. **遅延ロード**: 重いコンテンツパネルの遅延レンダリングを確保するために、`ng-template` 内で提供されている構造ディレクティブ(`ngAccordionContent``ngTabContent`)を常に使用してください。

View File

@@ -0,0 +1,86 @@
# エージェント向け Angular CLI ガイド
Angular CLI`ng`)は Angular ワークスペースを管理するための主要ツールです。プロジェクト構造を変更したり Angular 固有の依存関係を追加したりする際は、手動でのファイル作成や汎用の `npm` コマンドよりも、常に CLI コマンドを優先して使用してください。
## 1. 依存関係の管理
**Angular ライブラリには `npm install` ではなく必ず `ng add` を使用してください。** `ng add` はパッケージのインストールに加え、初期化スキーマティクス(例:`angular.json` の設定、ルートプロバイダーの更新)も実行します。
```bash
ng add @angular/material
ng add tailwindcss
ng add @angular/fire
```
アプリケーションとその依存関係を更新するには(コードマイグレーションが自動的に実行されます):
```bash
ng update @angular/core@<latest or specific version> @angular/cli<latest or specific version>
```
## 2. コード生成(`ng generate` または `ng g`
Angular の規約に準拠し、必要な設定ファイルが自動的に更新されるよう、コード生成には必ず CLI を使用してください。
| 対象 | コマンド | 備考 |
| :------------- | :-------------------- | :--------------------------------------------------------------------------------------------- |
| コンポーネント | `ng g c path/to/name` | コンポーネントを生成します。要求に応じて `--inline-style``-s`)または `--inline-template``-t`)を使用してください。 |
| サービス | `ng g s path/to/name` | `@Injectable({providedIn: 'root'})` サービスを生成します。 |
| ディレクティブ | `ng g d path/to/name` | ディレクティブを生成します。 |
| パイプ | `ng g p path/to/name` | パイプを生成します。 |
| ガード | `ng g g path/to/name` | 関数型ルートガードを生成します。 |
| 環境設定 | `ng g environments` | `src/environments/` を生成し、ファイル置換を含む `angular.json` を更新します。 |
_注意:単一のルート定義を生成するコマンドはありません。コンポーネントを生成した後、`app.routes.ts` 内の `Routes` 配列に手動で追加してください。_
## 3. 開発サーバーとプロキシ
ホットモジュール置換HMRを使用してローカル開発サーバーを起動します
```bash
ng serve
```
### バックエンド API プロキシ
開発中に API リクエストをプロキシするには(例:`/api` をローカルの Node サーバーにリルーティング):
1. `src/proxy.conf.json` を作成します:
```json
{
"/api/**": {"target": "http://localhost:3000", "secure": false}
}
```
2. `angular.json` の `serve` ターゲット以下を更新します:
```json
"serve": {
"builder": "@angular/build:dev-server",
"options": { "proxyConfig": "src/proxy.conf.json" }
}
```
## 4. アプリケーションのビルド
アプリケーションを出力ディレクトリ(デフォルト:`dist/<project-name>/browser`)にコンパイルします。最新の Angular は esbuild ベースの `@angular/build:application` ビルダーを使用します。
```bash
ng build
```
- `ng build` はデフォルトでプロダクション設定を使用し、Ahead-of-TimeAOTコンパイル、ミニファイ、ツリーシェイキングを有効にします。
- `--configuration` オプションを使用して `angular.json` で定義された特定の設定を対象にできます:`ng build --configuration=staging`。
## 5. テスト
- **ユニットテスト**設定されたテストランナーKarma または Vitestでユニットテストを実行するには `ng test` を実行します。
- **エンドツーエンドE2Eテスト**`ng e2e` を実行します。E2E フレームワークが設定されていない場合、CLI がインストールを促しますCypress、Playwright、Puppeteer など)。
## 6. デプロイ
アプリケーションをデプロイするには、まずデプロイメントビルダーを追加してからデプロイコマンドを実行します:
```bash
# Firebase の例
ng add @angular/fire
ng deploy
```

View File

@@ -0,0 +1,59 @@
# コンポーネントハーネスを使用したテスト
コンポーネントハーネスは、テストでコンポーネントを操作するための標準的かつ推奨される方法です。コンポーネントの内部 DOM 構造の変更からテストを保護することで、壊れにくく読みやすいテストを実現するユーザー中心の API を提供します。
## なぜハーネスを使用するのか?
- **堅牢性:** コンポーネントの内部 HTML や CSS クラスをリファクタリングしてもテストが壊れません。
- **可読性:** テストは DOM クエリ(`fixture.nativeElement.querySelector(...)`)を通じてではなく、ユーザーの観点からのインタラクション(例:`button.click()``slider.getValue()`)を記述します。
- **再利用性:** 同じハーネスをユニットテストと E2E テストの両方で使用できます。
Angular Material はライブラリ内のすべてのコンポーネントにテストハーネスを提供しています。
## ユニットテストでのハーネスの使用
`TestbedHarnessEnvironment` はユニットテストでハーネスを使用するためのエントリーポイントです。
### 例:`MatButtonHarness` を使用したテスト
```ts
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {MatButtonHarness} from '@angular/material/button/testing';
import {MyButtonContainerComponent} from './my-button-container.component';
describe('MyButtonContainerComponent', () => {
let fixture: ComponentFixture<MyButtonContainerComponent>;
let loader: HarnessLoader;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyButtonContainerComponent, MatButtonModule],
}).compileComponents();
fixture = TestBed.createComponent(MyButtonContainerComponent);
// コンポーネントのフィクスチャ用のハーネスローダーを作成する
loader = TestbedHarnessEnvironment.loader(fixture);
});
it('should find a button with specific text', async () => {
// "Submit" というテキストを持つ MatButton のハーネスをロードする
const submitButton = await loader.getHarness(MatButtonHarness.with({text: 'Submit'}));
// ハーネス API を使用してコンポーネントを操作する
expect(await submitButton.isDisabled()).toBe(false);
await submitButton.click();
// ... アサーション
});
});
```
### 主要なコンセプト
1. **`HarnessLoader`**:ハーネスインスタンスを検索して作成するためのオブジェクトです。`TestbedHarnessEnvironment.loader(fixture)` を使用してコンポーネントのフィクスチャ用のローダーを取得します。
2. **`loader.getHarness(HarnessClass)`**:最初にマッチするコンポーネントのハーネスインスタンスを非同期で検索して返します。
3. **`HarnessClass.with({ ... })`**:多くのハーネスは静的な `with` メソッドを提供し、`HarnessPredicate` を返します。これにより、テキスト、セレクター、無効状態などのプロパティに基づいてコンポーネントをフィルタリングして検索できます。テストしたいコンポーネントを正確に対象にするために常にこれを使用してください。
4. **ハーネス API** ハーネスインスタンスを取得したら、そのメソッド(例:`.click()``.getText()``.getValue()`)を使用してコンポーネントを操作します。これらのメソッドは非同期操作と変更検出の待機を自動的に処理します。

View File

@@ -0,0 +1,91 @@
# コンポーネントスタイリング
Angular コンポーネントは、カプセル化とモジュール性を実現するために、テンプレートに特定のスタイルを定義できます。
## スタイルの定義
スタイルはインラインまたは別ファイルで定義できます。
```ts
@Component({
selector: 'app-photo',
// インラインスタイル
styles: `
img {
border-radius: 50%;
}
`,
// または外部ファイル
styleUrl: 'photo.component.css',
})
export class Photo {}
```
## ビューエンカプセレーション
すべてのコンポーネントには、スタイルのスコープを決定するビューエンカプセレーション設定があります。
| モード | 動作 |
| :------------------------------ | :-------------------------------------------------------------------------------------------- |
| `Emulated`(デフォルト) | 一意の HTML 属性を使用してスタイルをコンポーネントにスコープします。グローバルスタイルは引き続き影響する可能性があります。 |
| `ShadowDom` | ブラウザのネイティブ Shadow DOM API を使用してスタイルを完全に分離します。 |
| `None` | カプセル化を無効にします。コンポーネントスタイルはグローバルになります。 |
| `ExperimentalIsolatedShadowDom` | コンポーネントのスタイルのみが適用されることを厳密に保証します。 |
### 使用方法
```ts
import { ViewEncapsulation } from '@angular/core';
@Component({
...,
encapsulation: ViewEncapsulation.None,
})
export class GlobalStyled {}
```
## 特殊セレクター
### `:host`
コンポーネントのホスト要素(コンポーネントのセレクターに一致する要素)を対象にします。
```css
:host {
display: block;
border: 1px solid black;
}
```
### `:host-context()`
先祖の何らかの条件に基づいてホスト要素を対象にします。
```css
/* 先祖に 'theme-dark' クラスがある場合にスタイルを適用する */
:host-context(.theme-dark) {
background-color: #333;
}
```
### `::ng-deep`
特定のルールのビューエンカプセレーションを無効にし、子コンポーネントに「漏れ」させます。
**注意Angular チームは `::ng-deep` の使用を強く推奨しません。** これは後方互換性のためにのみサポートされています。
## テンプレート内のスタイル
コンポーネントのテンプレートで直接 `<style>` 要素を使用できます。ビューエンカプセレーションのルールは引き続き適用されます。
```html
<style>
.dynamic-class {
color: red;
}
</style>
<div class="dynamic-class">Hello</div>
```
## 外部スタイル
CSS での `<link>``@import` の使用は外部スタイルとして扱われます。**外部スタイルはエミュレートされたビューエンカプセレーションの影響を受けません。**

View File

@@ -0,0 +1,117 @@
# コンポーネント
Angular コンポーネントはアプリケーションの基本的な構成要素です。各コンポーネントは、振る舞いを持つ TypeScript クラス、HTML テンプレート、CSS セレクターで構成されます。
## コンポーネントの定義
`@Component` デコレーターを使用してコンポーネントのメタデータを定義します。
```ts
@Component({
selector: 'app-profile',
template: `
<img src="profile.jpg" alt="Profile photo" />
<button (click)="save()">Save</button>
`,
styles: `
img {
border-radius: 50%;
}
`,
})
export class Profile {
save() {
/* ... */
}
}
```
## メタデータオプション
- `selector`:テンプレート内でこのコンポーネントを識別する CSS セレクター。
- `template`:インライン HTML テンプレート(小さなテンプレートに推奨)。
- `templateUrl`:外部 HTML ファイルへのパス。
- `styles`:インライン CSS スタイル。
- `styleUrl` / `styleUrls`:外部 CSS ファイルへのパス(複数可)。
- `imports`:このコンポーネントのテンプレートで使用されるコンポーネント、ディレクティブ、パイプのリスト。
## コンポーネントの使用
コンポーネントを使用するには、利用側コンポーネントの `imports` 配列に追加し、テンプレートでセレクターを使用します。
```ts
@Component({
selector: 'app-root',
imports: [Profile],
template: `<app-profile />`,
})
export class App {}
```
## テンプレート制御フロー
Angular は条件レンダリングとループに組み込みブロックを使用します。
### 条件レンダリング(`@if`
コンテンツを条件付きで表示するには `@if` を使用します。`@else if``@else` ブロックを含めることができます。
```html
@if (user.isAdmin) {
<admin-dashboard />
} @else if (user.isModerator) {
<mod-dashboard />
} @else {
<standard-dashboard />
}
```
**結果のエイリアス**:式の結果を保存して再利用できます。
```html
@if (user.settings(); as settings) {
<p>Theme: {{ settings.theme }}</p>
}
```
### ループ(`@for`
`@for` ブロックはコレクションを反復処理します。`track` 式はパフォーマンスと DOM 再利用のために**必須**です。
```html
<ul>
@for (item of items(); track item.id; let i = $index, total = $count) {
<li>{{ i + 1 }}/{{ total }}: {{ item.name }}</li>
} @empty {
<li>No items to display.</li>
}
</ul>
```
**暗黙的変数**`$index``$count``$first``$last``$even``$odd`
### コンテンツの切り替え(`@switch`
`@switch` ブロックは値に基づいてコンテンツをレンダリングします。厳密等価(`===`)を使用し、**フォールスルーはありません**。
```html
@switch (status()) { @case ('loading') { <app-spinner /> } @case ('error') { <app-error-msg /> }
@case ('success') { <app-data-grid /> } @default {
<p>Unknown status</p>
} }
```
**網羅的型チェック**`@default never;` を使用して、ユニオン型のすべてのケースが処理されることを確認します。
```html
@switch (state) { @case ('on') { ... } @case ('off') { ... } @default never; // 'standby' のような新しい
// 状態が追加された場合にエラーになる }
```
## 核心的コンセプト
- **ホスト要素**:コンポーネントのセレクターに一致する DOM 要素。
- **ビュー**:ホスト要素内のコンポーネントのテンプレートによってレンダリングされた DOM。
- **スタンドアロン**デフォルトでコンポーネントはスタンドアロンですAngular 19 以降、`standalone: true` がデフォルト)。古いバージョンでは `standalone: true` を明示するか、コンポーネントを `NgModule` の一部にする必要があります。
- **コンポーネントツリー**Angular アプリケーションはコンポーネントのツリーとして構成され、各コンポーネントは子コンポーネントをホストできます。
- **コンポーネントの命名**:プロジェクトがその命名設定を使用するように構成されていない限り、コンポーネントクラスに `Component` サフィックスを追加しないでくださいAppComponent

View File

@@ -0,0 +1,97 @@
# サービスの作成と使用
Angular のサービスは、複数のコンポーネントや他のサービスがアクセスする必要があるデータフェッチ、ビジネスロジック、または状態管理を扱う再利用可能なコードです。
## サービスの作成
Angular CLI を使用してサービスを生成できます:
```bash
ng generate service my-data
```
または、TypeScript クラスを手動で作成して `@Injectable()` でデコレートすることもできます。
```ts
import {Injectable} from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class BasicDataStore {
private data: string[] = [];
addData(item: string): void {
this.data.push(item);
}
getData(): string[] {
return [...this.data];
}
}
```
### `providedIn: 'root'` オプション
ほとんどのサービスには `providedIn: 'root'` の使用が推奨されます。これにより Angular は以下を行います:
- アプリケーション全体に対して**シングルインスタンス(シングルトン)を作成**します。
- `providers` 配列に列挙しなくても**どこからでも自動的に利用可能**にします。
- **ツリーシェイキングを有効**にし、サービスが実際にどこかで注入されている場合にのみ最終的な JavaScript バンドルに含まれるようにします。
## サービスの注入
サービスを作成したら、`inject()` 関数を使用してコンポーネント、ディレクティブ、または他のサービスに注入できます。
### コンポーネントへの注入
```ts
import {Component, inject} from '@angular/core';
import {BasicDataStore} from './basic-data-store.service';
@Component({
selector: 'app-example',
template: `
<div>
<p>Data items: {{ dataStore.getData().length }}</p>
<button (click)="dataStore.addData('New Item')">Add Item</button>
</div>
`,
})
export class Example {
// サービスをクラスフィールドとして注入する
dataStore = inject(BasicDataStore);
}
```
### 別のサービスへの注入
サービスはまったく同じ方法で他のサービスを注入できます。
```ts
import {Injectable, inject} from '@angular/core';
import {AdvancedDataStore} from './advanced-data-store.service';
@Injectable({
providedIn: 'root',
})
export class BasicDataStore {
// 別のサービスを注入する
private advancedDataStore = inject(AdvancedDataStore);
private data: string[] = [];
getData(): string[] {
// このサービスと注入されたサービスのデータを結合する
return [...this.data, ...this.advancedDataStore.getData()];
}
}
```
## 高度なサービスパターン
`providedIn: 'root'` がほとんどのシナリオをカバーしていますが、以下のような場合に他の方法が必要になることがあります:
- **コンポーネント固有のインスタンス**:コンポーネントがサービスの独立したインスタンスを必要とする場合、コンポーネントの `@Component({ providers: [MyService] })` 配列で直接提供します。
- **ファクトリープロバイダー**:動的な作成のため。
- **値プロバイダー**:設定オブジェクトを注入するため。

View File

@@ -0,0 +1,69 @@
# データリゾルバー
データリゾルバーはルートがアクティブになる前にデータをフェッチし、コンポーネントがレンダリング時に必要なデータを持っていることを保証します。
## リゾルバーの作成
`ResolveFn` 型を実装します。
```ts
export const userResolver: ResolveFn<User> = (route, state) => {
const userService = inject(UserService);
const id = route.paramMap.get('id')!;
return userService.getUser(id);
};
```
## ルートの設定
`resolve` キーの下にリゾルバーを追加します。
```ts
{
path: 'user/:id',
component: UserProfile,
resolve: {
user: userResolver
}
}
```
## 解決されたデータへのアクセス
### 1. `ActivatedRoute` 経由(従来の方法)
```ts
private route = inject(ActivatedRoute);
data = toSignal(this.route.data);
user = computed(() => this.data().user);
```
### 2. コンポーネント入力経由(モダンな方法)
`provideRouter``withComponentInputBinding()` を有効にすると、解決されたデータを `@Input` または `input()` に直接渡せます。
```ts
// app.config.ts
provideRouter(routes, withComponentInputBinding());
// component.ts
user = input.required<User>();
```
## エラーハンドリング
リゾルバーが失敗するとナビゲーションがブロックされます。
- グローバルな処理には `withNavigationErrorHandler` を使用します。
- リゾルバー内で `catchError` を使用して `RedirectCommand` やフォールバックデータを返します。
```ts
return userService
.get(id)
.pipe(catchError(() => of(new RedirectCommand(router.parseUrl('/error')))));
```
## ベストプラクティス
- **軽量に保つ**:重要なデータのみをフェッチします。
- **フィードバックを提供する**:リゾルバーが完了するまで UI は古いページに留まるため、ナビゲーション中にグローバルなローディングバーを表示するためにルーターイベントをリッスンします。

View File

@@ -0,0 +1,67 @@
# ルートの定義
ルートは特定の URL パスに対してどのコンポーネントをレンダリングするかを定義するオブジェクトです。
## 基本設定
`Routes` 配列にルートを定義し、`appConfig``provideRouter` を使用して提供します。
```ts
// app.routes.ts
export const routes: Routes = [
{path: '', component: HomePage},
{path: 'admin', component: AdminPage},
];
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes)],
};
```
## URL パス
- **静的**:完全な文字列に一致します(例:`'admin'`)。
- **ルートパラメーター**:コロンが前置された動的セグメント(例:`'user/:id'`)。
- **ワイルドカード**`**` を使用して任意の URL に一致します。「見つかりません」ページに有用です。**常に配列の最後に配置してください。**
## マッチング戦略
Angular は**先勝ち**戦略を使用します。具体的なルートは具体性の低いルートより前に記述する必要があります。
## リダイレクト
`redirectTo` を使用して一つのパスを別のパスに向けます。
```ts
{ path: 'articles', redirectTo: '/blog' },
{ path: 'blog', component: Blog },
```
## ページタイトル
アクセシビリティのためにルートにタイトルを関連付けます。タイトルは静的または動的(`ResolveFn` またはカスタム `TitleStrategy` 経由)にできます。
```ts
{ path: 'home', component: Home, title: 'Home Page' }
```
## ルートデータとプロバイダー
- **静的データ**`data` プロパティを使用してメタデータを付加します。
- **ルートプロバイダー**`providers` 配列を使用して特定のルートとその子に依存関係をスコープします。
## ネスト(子)ルート
`children` プロパティを使用してサブビューを定義します。親コンポーネントには `<router-outlet />` が必要です。
```ts
{
path: 'product/:id',
component: Product,
children: [
{ path: 'info', component: ProductInfo },
{ path: 'reviews', component: ProductReviews },
],
}
```

View File

@@ -0,0 +1,72 @@
# 依存性プロバイダーの定義
Angular は依存性注入DIシステムに依存関係を提供する自動的な方法と手動の方法を提供します。
## 自動プロビジョン
サービスを提供する最も一般的な方法は、`@Injectable()``providedIn: 'root'` を使用することです。
### InjectionToken
非クラスの依存関係(設定オブジェクト、関数、プリミティブ)には `InjectionToken` を使用します。`InjectionToken` は自動的に提供することもできます。
```ts
import {InjectionToken} from '@angular/core';
export interface AppConfig {
apiUrl: string;
}
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config', {
providedIn: 'root',
factory: () => ({apiUrl: 'https://api.example.com'}),
});
```
## 手動プロビジョン
`providedIn` のないサービス、特定のコンポーネント用の新しいインスタンスが必要な場合、またはランタイム値を設定する場合に `providers` 配列を使用します。
```ts
@Component({
providers: [
// { provide: LocalService, useClass: LocalService } の省略形
LocalService,
// useClass: 実装を切り替える
{provide: Logger, useClass: BetterLogger},
// useValue: 静的な値を提供する
{provide: API_URL_TOKEN, useValue: 'https://api.example.com'},
// useFactory: 値を動的に生成する
{
provide: ApiClient,
useFactory: (http = inject(HttpClient)) => new ApiClient(http),
},
// useExisting: エイリアスを作成する
{provide: OldLogger, useExisting: NewLogger},
// multi: 同じトークンに複数の値を配列として提供する
{provide: INTERCEPTOR_TOKEN, useClass: AuthInterceptor, multi: true},
],
})
export class Example {}
```
## プロバイダーのスコープ
- **アプリケーションブートストラップ**グローバルシングルトン。HTTP クライアント、ロギング、アプリ全体の設定に使用します。
- **コンポーネント/ディレクティブ**:分離されたインスタンス。コンポーネント固有の状態やフォームに使用します。サービスはコンポーネントが破棄されると破棄されます。
- **ルート**:特定のルートでのみロードされる機能固有のサービス。
## ライブラリパターン:`provide*` 関数
ライブラリ作者は設定をカプセル化するためにプロバイダー配列を返す関数をエクスポートする必要があります:
```ts
export function provideAnalytics(config: AnalyticsConfig): Provider[] {
return [{provide: ANALYTICS_CONFIG, useValue: config}, AnalyticsService];
}
```

View File

@@ -0,0 +1,120 @@
# 依存性注入DIの基礎
依存性注入DIは、アプリケーションの異なる部分に機能を「注入」することで、コードを整理して共有するための設計パターンです。これにより、コードの保守性、スケーラビリティ、テスト容易性が向上します。
## Angular における DI の仕組み
コードが Angular の DI システムと相互作用する主な方法が 2 つあります:
1. **プロビジョン(提供)**:値(オブジェクト、関数、プリミティブ)を DI システムで利用可能にする。
2. **インジェクション(注入)**DI システムにそれらの値を要求する。
Angular のコンポーネント、ディレクティブ、サービスは自動的に DI に参加します。
## サービス
**サービス**はアプリケーション全体でデータと機能を共有する最も一般的な方法です。`@Injectable()` でデコレートされた TypeScript クラスです。
### サービスの作成
`@Injectable` デコレーターで `providedIn: 'root'` オプションを使用すると、アプリケーション全体で利用可能なシングルトンサービスになります。これはほとんどのサービスに推奨されるアプローチです。
```ts
import {Injectable} from '@angular/core';
@Injectable({
providedIn: 'root', // どこでも利用可能なシングルトンにする
})
export class AnalyticsLogger {
trackEvent(category: string, value: string) {
console.log('Analytics event logged:', {category, value});
}
}
```
サービスの一般的な用途:
- データクライアントAPI 呼び出し)
- 状態管理
- 認証と認可
- ロギングとエラーハンドリング
- ユーティリティ関数
## 依存関係の注入
依存関係を要求するには Angular の `inject()` 関数を使用します。
### `inject()` 関数
`inject()` 関数を使用してサービス(または提供された他のトークン)のインスタンスを取得できます。
```ts
import {Component, inject} from '@angular/core';
import {Router} from '@angular/router';
import {AnalyticsLogger} from './analytics-logger.service';
@Component({
selector: 'app-navbar',
template: `<a href="#" (click)="navigateToDetail($event)">Detail Page</a>`,
})
export class Navbar {
// クラスフィールド初期化子を使用して依存関係を注入する
private router = inject(Router);
private analytics = inject(AnalyticsLogger);
navigateToDetail(event: Event) {
event.preventDefault();
this.analytics.trackEvent('navigation', '/details');
this.router.navigate(['/details']);
}
}
```
### `inject()` はどこで使用できるか?(インジェクションコンテキスト)
`inject()` は**インジェクションコンテキスト**内でコードが実行されている場合に呼び出せます。最も一般的なインジェクションコンテキストは、コンポーネント、ディレクティブ、またはサービスの構築時です。
`inject()` を呼び出せる有効な場所:
1. **クラスフィールド初期化子**(推奨)
2. **コンストラクター本体**
3. **ルートガードとリゾルバー**(インジェクションコンテキスト内で実行される)
4. プロバイダーで使用される**ファクトリー関数**
```typescript
import {Component, Directive, Injectable, inject, ElementRef} from '@angular/core';
import {HttpClient} from '@angular/common/http';
// 1. コンポーネント内(フィールド初期化子とコンストラクター)
@Component({
/*...*/
})
export class Example {
private service1 = inject(MyService); // 有効なフィールド初期化子
private service2: MyService;
constructor() {
this.service2 = inject(MyService); // 有効なコンストラクター本体
}
}
// 2. ディレクティブ内
@Directive({
/*...*/
})
export class MyDirective {
private element = inject(ElementRef); // 有効なフィールド初期化子
}
// 3. サービス内
@Injectable({providedIn: 'root'})
export class MyService {
private http = inject(HttpClient); // 有効なフィールド初期化子
}
// 4. ルートガード(関数型)内
export const authGuard = () => {
const auth = inject(AuthService); // 有効なルートガード
return auth.isAuthenticated();
};
```

View File

@@ -0,0 +1,56 @@
# エンドツーエンドE2Eテスト
実際のブラウザでの重要なユーザージャーニーをカバーするために E2E テストを使用します。Cypress や Playwright など、Angular ワークスペースですでに設定されているフレームワークを優先してください。
## E2E テストの実行
プロジェクト固有のコマンドについては `package.json``angular.json` を確認してください。一般的なパターンには以下があります:
```shell
npm run e2e
pnpm e2e
ng e2e
```
アプリをビルドまたはサーブする必要がある場合は、並列テストエントリポイントを新たに作成するのではなく、既存のプロジェクトスクリプトを使用してください。
## テスト構造
- E2E スペックは `cypress/e2e/``e2e/` など、設定されたテストフレームワークに近い場所に保管してください。
- 再利用可能なログイン/セットアップヘルパーはフレームワークのサポートディレクトリに配置してください。
- フィクスチャは明示的に小さくして、各テストが依存するユーザー状態を説明できるようにしてください。
### Cypress の例
```typescript
describe('Login flow', () => {
it('redirects to dashboard on valid credentials', () => {
cy.visit('/login');
cy.get('[data-cy=email]').type('user@example.com');
cy.get('[data-cy=password]').type('password123');
cy.get('[data-cy=submit]').click();
cy.url().should('include', '/dashboard');
});
});
```
### Playwright の例
```typescript
import {expect, test} from '@playwright/test';
test('redirects to dashboard on valid credentials', async ({page}) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', {name: 'Sign in'}).click();
await expect(page).toHaveURL(/dashboard/);
});
```
## ベストプラクティス
- アクセシブルなロケーター(`getByRole``getByLabel`)または安定した `data-*` 属性を優先してください。
- CSS クラス、DOM の深さ、または偶発的なテキストに依存するセレクターは避けてください。
- 任意のスリープではなく、特定の UI 状態、ルート、またはネットワークレスポンスを待機してください。
- スモークテストは短く保ち、完全なワークフローカバレッジは最も価値の高いパスに限定してください。

View File

@@ -0,0 +1,83 @@
# `effect` と `afterRenderEffect` によるサイドエフェクト
Angularにおける**エフェクト**とは、追跡している1つ以上のシグナル値が変化するたびに実行される処理です。
## `effect` を使うべき場面
エフェクトは、シグナルの状態を命令的な非シグナルAPIに同期させるために使います。
**有効なユースケース:**
- アナリティクスのログ記録。
- `localStorage``sessionStorage` への状態の同期。
- `<canvas>` やサードパーティのチャートライブラリへのカスタムレンダリングの実行。
**重要なルール: 状態の伝播にエフェクトを使わないこと。**
2つのシグナルを同期させるためにエフェクト内でシグナルの `.set()``.update()` を呼び出している場合、それは誤りです。`ExpressionChangedAfterItHasBeenChecked` エラーや無限ループを引き起こします。**状態の派生には常に `computed()` または `linkedSignal()` を使用してください。**
## 基本的な使い方
エフェクトは変更検出プロセス中に非同期で実行されます。常に少なくとも1回は実行されます。
```ts
import { Component, signal, effect } from '@angular/core';
@Component({...})
export class Example {
count = signal(0);
constructor() {
// エフェクトはインジェクションコンテキスト(例: コンストラクター)内で作成する必要があります
effect((onCleanup) => {
console.log(`Count changed to ${this.count()}`);
const timer = setTimeout(() => console.log('Timer finished'), 1000);
// クリーンアップ関数は次回実行前または破棄時に実行されます
onCleanup(() => clearTimeout(timer));
});
}
}
```
## `afterRenderEffect` によるDOM操作
標準の `effect` はAngularがDOMを更新する_前_に実行されます。シグナルの変化に基づいてDOMを手動で検査または変更する必要がある場合例: サードパーティUIライブラリの統合は、`afterRenderEffect` を使用してください。
`afterRenderEffect` はAngularがDOMのレンダリングを完了した後に実行されます。
### レンダーフェーズ
リフロー(強制レイアウトスラッシング)を防ぐため、`afterRenderEffect` はDOMの読み取りと書き込みを特定のフェーズに分割することを強制します。
```ts
import { Component, afterRenderEffect, viewChild, ElementRef } from '@angular/core';
@Component({...})
export class Chart {
canvas = viewChild.required<ElementRef>('canvas');
constructor() {
afterRenderEffect({
// 1. DOMから読み取る
earlyRead: () => {
return this.canvas().nativeElement.getBoundingClientRect().width;
},
// 2. DOMに書き込む前フェーズの結果を受け取る
write: (width) => {
// writeフェーズではDOMを読み取らないこと。
setupChart(this.canvas().nativeElement, width);
}
});
}
}
```
**利用可能なフェーズ(この順序で実行されます):**
1. `earlyRead`
2. `write`(ここでは読み取らないこと)
3. `mixedReadWrite`(可能な限り避けること)
4. `read`(ここでは書き込まないこと)
_注意: `afterRenderEffect` はクライアントでのみ実行され、サーバーサイドレンダリングSSR中は実行されません。_

View File

@@ -0,0 +1,43 @@
# 階層的インジェクター
Angularの依存性注入システムは階層的であり、サービスをアプリケーションの異なるレベルにスコープできます。
## インジェクター階層の種類
1. **`EnvironmentInjector` 階層**: `@Injectable({ providedIn: 'root' })` またはブートストラップ時の `ApplicationConfig.providers` で設定されます。これらはグローバルシングルトンです。
2. **`ElementInjector` 階層**: 各DOM要素で暗黙的に作成されます。`@Component()` または `@Directive()``providers` または `viewProviders` 配列で設定されます。
## 解決ルール
依存関係が要求されると、Angularは2つのフェーズで解決します:
1. リクエスト元のコンポーネント/ディレクティブからルート要素まで、**`ElementInjector`** ツリーを上方向に検索します。
2. 見つからない場合、最も近い環境インジェクターからルートまで、**`EnvironmentInjector`** ツリーを検索します。
3. それでも見つからない場合、エラーをスローします(オプションとしてマークされている場合を除く)。
## 解決モディファイアー
`inject()` のオプションオブジェクトを使用して、Angularが依存関係を検索する方法を変更できます:
- **`optional`**: 依存関係が見つからない場合、エラーをスローする代わりに `null` を返します。
- **`self`**: 現在の `ElementInjector` のみをチェックします。親ツリーを検索しません。
- **`skipSelf`**: 現在の要素をスキップして、親の `ElementInjector` から検索を開始します。
- **`host`**: ホストコンポーネントのビュー境界に達したら検索を停止します。
```ts
@Component({...})
export class Example {
// 見つからない場合はクラッシュせずnullを返す
optionalService = inject(MyService, { optional: true });
// このコンポーネントのプロバイダーをスキップして親を参照する
parentService = inject(ParentService, { skipSelf: true });
}
```
## `providers` と `viewProviders`
コンポーネントレベルでサービスを提供する場合:
- **`providers`**: サービスはコンポーネント、そのビュー(テンプレート)、および**プロジェクトされたコンテンツ**`<ng-content>`)で利用可能です。
- **`viewProviders`**: サービスはコンポーネントとそのビューで利用可能ですが、プロジェクトされたコンテンツからは**利用できません**。コンシューマーから渡されたコンテンツからサービスを分離するために使用します。

View File

@@ -0,0 +1,80 @@
# コンポーネントのホスト要素
**ホスト要素**とは、コンポーネントのセレクターに一致するDOM要素です。コンポーネントのテンプレートはこの要素の内部にレンダリングされます。
## ホスト要素へのバインディング
`@Component` デコレーターの `host` プロパティを使用して、プロパティ、属性、スタイル、イベントをホスト要素にバインドします。これはレガシーデコレーターより**推奨されるアプローチ**です。
```ts
@Component({
selector: 'custom-slider',
host: {
'role': 'slider', // 静的属性
'[attr.aria-valuenow]': 'value', // 属性バインディング
'[class.active]': 'isActive()', // クラスバインディング
'[style.color]': 'color()', // スタイルバインディング
'[tabIndex]': 'disabled ? -1 : 0', // プロパティバインディング
'(keydown)': 'onKeyDown($event)', // イベントバインディング
},
})
export class CustomSlider {
value = 0;
disabled = false;
isActive = signal(false);
color = signal('blue');
onKeyDown(event: KeyboardEvent) {
/* ... */
}
}
```
## レガシーデコレーター
`@HostBinding``@HostListener` は後方互換性のためにサポートされていますが、新しいコードでは使用を避けてください。
```ts
export class CustomSlider {
@HostBinding('tabIndex')
get tabIndex() {
return this.disabled ? -1 : 0;
}
@HostListener('keydown', ['$event'])
onKeyDown(event: KeyboardEvent) {
/* ... */
}
}
```
## バインディングの競合
コンポーネント(ホストバインディング)とコンシューマー(テンプレートバインディング)の両方が同じプロパティにバインドする場合:
1. **静的 vs 静的**: インスタンス(コンシューマー)のバインディングが優先されます。
2. **静的 vs 動的**: 動的バインディングが優先されます。
3. **動的 vs 動的**: コンポーネントのホストバインディングが優先されます。
## ホスト属性のインジェクト
`inject` 関数と `HostAttributeToken` を使用して、構築時にホスト要素から静的属性を読み取ります。
```ts
import {Component, HostAttributeToken, inject} from '@angular/core';
@Component({
selector: 'app-btn',
template: `<ng-content />`,
})
export class AppButton {
// `{ optional: true }` でインジェクトしない限り、'type'がない場合エラーをスローします
type = inject(new HostAttributeToken('type'));
}
```
使用例:
```html
<app-btn type="primary">Click Me</app-btn>
```

View File

@@ -0,0 +1,63 @@
# インジェクションコンテキスト
`inject()` 関数はコードが**インジェクションコンテキスト**内で実行されている場合にのみ使用できます。
## インジェクションコンテキストが利用可能な場所
インジェクションコンテキストは以下の場所で自動的に利用可能です:
1. DI によってインスタンス化されるクラス(`@Injectable``@Component``@Directive``@Pipe`)の**フィールド初期化子**。
2. DI によってインスタンス化されるクラスの**コンストラクター**。
3. `useFactory` または `InjectionToken` 設定で指定された**ファクトリー関数**。
4. Angular によって実行される**関数型 API**(例:関数型ルートガード、リゾルバー、インターセプター)。
```ts
@Component({...})
export class Example {
// 有効:フィールド初期化子
private router = inject(Router);
constructor() {
// 有効:コンストラクター
const http = inject(HttpClient);
}
onClick() {
// 無効:インジェクションコンテキストではない
// const auth = inject(AuthService);
}
}
```
## `runInInjectionContext`
インジェクションコンテキスト内で関数を実行する必要がある場合(動的コンポーネントの作成やテストでよく必要になります)、`runInInjectionContext` を使用します。これには既存のインジェクター(`EnvironmentInjector` または `Injector` など)へのアクセスが必要です。
```ts
import {Injectable, inject, EnvironmentInjector, runInInjectionContext} from '@angular/core';
@Injectable({providedIn: 'root'})
export class MyService {
private injector = inject(EnvironmentInjector);
doSomethingDynamic() {
runInInjectionContext(this.injector, () => {
// ここで inject() を使用することが有効になる
const router = inject(Router);
});
}
}
```
## `assertInInjectionContext`
ユーティリティ関数が有効なコンテキストから呼び出されることを保証するために `assertInInjectionContext` を使用します。そうでない場合は明確なエラーをスローします。
```ts
import {assertInInjectionContext, inject, ElementRef} from '@angular/core';
export function injectNativeElement<T extends Element>(): T {
assertInInjectionContext(injectNativeElement);
return inject(ElementRef).nativeElement;
}
```

View File

@@ -0,0 +1,101 @@
# 入力Inputs
入力により、親コンポーネントから子コンポーネントにデータを流すことができます。Angular はモダンなアプリケーションにはシグナルベースの `input` API の使用を推奨しています。
## シグナルベースの入力
`input()` 関数を使用して入力を宣言します。これは `InputSignal` を返します。
```ts
import {Component, input, computed} from '@angular/core';
@Component({
selector: 'app-user',
template: `<p>User: {{ name() }} ({{ age() }})</p>`,
})
export class User {
// デフォルト値を持つオプションの入力
name = input('Guest');
// 必須の入力
age = input.required<number>();
// 入力はリアクティブなシグナル
label = computed(() => `Name: ${this.name()}`);
}
```
### テンプレートでの使用
```html
<app-user [name]="userName" [age]="25" />
```
## 設定オプション
`input` 関数は設定オブジェクトを受け取ります:
- **エイリアス**:テンプレートで使用されるプロパティ名を変更します。
- **トランスフォーム**:コンポーネントに到達する前に値を変更します。
```ts
import { input, booleanAttribute } from '@angular/core';
@Component({...})
export class CustomButton {
// エイリアスの例
label = input('', { alias: 'btnLabel' });
// 組み込みヘルパーを使用したトランスフォームの例
disabled = input(false, { transform: booleanAttribute });
}
```
## モデル入力(双方向バインディング)
双方向データバインディングをサポートする入力を作成するには `model()` を使用します。
```ts
@Component({
selector: 'custom-counter',
template: `<button (click)="increment()">+</button>`,
})
export class CustomCounter {
value = model(0);
increment() {
this.value.update((v) => v + 1);
}
}
```
### 使用方法
```html
<!-- シグナルによる双方向バインディング -->
<custom-counter [(value)]="mySignal" />
<!-- プレーンプロパティによる双方向バインディング -->
<custom-counter [(value)]="myProperty" />
```
## デコレーターベースの入力(@Input
レガシー API は引き続きサポートされていますが、新しいコードには推奨されません。
```ts
import { Component, Input } from '@angular/core';
@Component({...})
export class Legacy {
@Input({ required: true }) value = 0;
@Input({ transform: trimString }) label = '';
}
```
## ベストプラクティス
- **シグナルを優先する**:より良いリアクティビティと型安全性のために `@Input()` ではなく `input()` を使用します。
- **必須入力**:ビルド時エラーを得るために必須データには `input.required()` を使用します。
- **純粋なトランスフォーム**:入力トランスフォーム関数は純粋で静的に解析可能であることを確認します。
- **衝突を避ける**:標準の DOM プロパティと衝突する入力名は使用しないでください(例:`id``title`)。

View File

@@ -0,0 +1,60 @@
# `linkedSignal` による依存状態の管理
`linkedSignal` 関数を使うと、別の状態と本質的に連動した書き込み可能な状態を作成できます。入力や他のシグナルから導出されたデフォルト値を持ちつつ、ユーザーが独立して変更できる状態に最適です。
ソース状態が変化すると、`linkedSignal` は新たに計算された値にリセットされます。
## 基本的な使い方
ソースに基づいて再計算するだけでよい場合は、計算関数を渡してください。`linkedSignal``computed` のように機能しますが、得られるシグナルは書き込み可能です(`.set()``.update()` を呼び出せます)。
```ts
import { Component, signal, linkedSignal } from '@angular/core';
@Component({...})
export class ShippingMethodPicker {
shippingOptions = signal(['Ground', 'Air', 'Sea']);
// 最初のオプションをデフォルト値とする。
// shippingOptions が変化すると、selectedOption は新しい最初のオプションにリセットされる。
selectedOption = linkedSignal(() => this.shippingOptions()[0]);
changeShipping(index: number) {
// このシグナルは手動で更新することもできる!
this.selectedOption.set(this.shippingOptions()[index]);
}
}
```
## 高度な使い方:以前の状態を考慮する
ソース状態が変化したとき、ユーザーの手動選択がまだ有効であれば保持したい場合があります。その場合は、`source``computation` を持つオブジェクト構文を使います。
`computation` 関数は、ソースの新しい値と、以前のソース値および以前の `linkedSignal` 値を含む `previous` オブジェクトを受け取ります。
```ts
interface ShippingMethod { id: number; name: string; }
@Component({...})
export class ShippingMethodPicker {
shippingOptions = signal<ShippingMethod[]>([
{id: 0, name: 'Ground'}, {id: 1, name: 'Air'}, {id: 2, name: 'Sea'}
]);
selectedOption = linkedSignal<ShippingMethod[], ShippingMethod>({
source: this.shippingOptions,
computation: (newOptions, previous) => {
// 新たに読み込まれたオプションにユーザーが以前選択した
// オプションが含まれていれば、そのまま選択を保持する。
// そうでなければ、最初のオプションにリセットする。
return newOptions.find(opt => opt.id === previous?.value.id) ?? newOptions[0];
}
});
}
```
### `linkedSignal` と `computed` と `effect` の使い分け
- `computed` を使う:状態が他の状態から**厳密に**導出されており、手動で更新すべきでない場合。
- `linkedSignal` を使う:状態は他の状態から導出されているが、ユーザーが**必ず**オーバーライドまたは手動更新できる必要がある場合。
- `effect` を使って一方の状態をもう一方に同期させることは**絶対にしない**。それはアンチパターンです。代わりに `computed` または `linkedSignal` を使ってください。

View File

@@ -0,0 +1,61 @@
# ルート読み込み戦略
Angular は、初期読み込み時間とナビゲーションの応答性のバランスを取るために、ルートとコンポーネントの読み込みに関する2つの主要な戦略をサポートしています。
## イーガーローディングEager Loading
コンポーネントは初期 JavaScript ペイロードにバンドルされ、即座に利用可能になります。
```ts
{ path: 'home', component: Home }
```
- **メリット**:シームレスなトランジション。
- **デメリット**:初期バンドルサイズが増加する。
## レイジーローディングLazy Loading
コンポーネントやルートは、ユーザーが画面に遷移したときにのみ読み込まれます。これにより JavaScript の独立した「チャンク」が生成されます。
### コンポーネントのレイジーローディング
`loadComponent` を使用して、コンポーネントをオンデマンドで取得します。
```ts
{
path: 'admin',
loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent)`,
}
```
### 子ルートのレイジーローディング
`loadChildren` を使用して、ルートのセットを取得します。
```ts
{
path: 'settings',
loadChildren: () => import('./settings/settings.routes'),
}
```
## インジェクションコンテキストとレイジーローディング
ローダー関数は現在のルートの**インジェクションコンテキスト**内で実行されます。これにより `inject()` を呼び出して、コンテキストを考慮した読み込み判断が可能になります。
```ts
{
path: 'dashboard',
loadComponent: () => {
const flags = inject(FeatureFlags);
return flags.isPremium
? import('./premium-dashboard')
: import('./basic-dashboard');
},
}
```
## 推奨事項
- メインのランディングページには**イーガーローディング**を使用する。
- 初期バンドルを小さく保つために、その他のすべての機能領域には**レイジーローディング**を使用する。

View File

@@ -0,0 +1,108 @@
# Angular CLI MCP サーバー
Angular CLI には Model Context ProtocolMCPサーバーが含まれており、AI アシスタントCursor、Gemini CLI、JetBrains AI など)が Angular CLI と直接やり取りできるようになります。コード生成、コードの近代化、サンプルの取得、ビルド/テストの実行を行うツールを提供します。
## 利用可能なツール(デフォルト)
MCP サーバーが有効化されると、AI エージェントは以下のツールにアクセスできます:
| 名前 | 説明 |
| :-------------------------- | :-------------------------------------------------------------------------------------------------------- |
| `ai_tutor` | インタラクティブな AI 搭載の Angular チューターを起動します。 |
| `find_examples` | モダンな Angular 機能に関する権威あるベストプラクティスのコードサンプルを検索します。 |
| `get_best_practices` | Angular ベストプラクティスガイドを取得します(スタンドアロンコンポーネント、型付きフォームなどに必須)。 |
| `list_projects` | `angular.json` を読み取り、ワークスペース内のすべてのアプリケーションとライブラリを一覧表示します。 |
| `onpush_zoneless_migration` | コードを分析し、`OnPush` 変更検知(ゾーンレスの前提条件)への移行計画を提供します。 |
| `search_documentation` | `https://angular.dev` の公式ドキュメントを検索します。 |
## 実験的ツール
一部のツールは `--experimental-tool`(または `-E`)フラグを使って明示的に有効化する必要があります。
| 名前 | 説明 |
| :------------------------- | :----------------------------------------------------------------------- |
| `build` | `ng build` を使って一度限りのビルドを実行します。 |
| `devserver.start` | 開発サーバー(`ng serve`)を非同期で起動します。即座に返ります。 |
| `devserver.stop` | 開発サーバーを停止します。 |
| `devserver.wait_for_build` | 実行中の開発サーバーの最新ビルドのログを返します。 |
| `e2e` | エンドツーエンドテストを実行します。 |
| `modernize` | 最新のベストプラクティスと構文に合わせてコードの移行を行います。 |
| `test` | プロジェクトのユニットテストを実行します。 |
## 設定
MCP サーバーを使用するには、ホスト環境IDE または CLI`npx @angular/cli mcp` を実行するよう設定します。
### Antigravity IDE
プロジェクトのルートに `.antigravity/mcp.json` というファイルを作成します:
```json
{
"mcpServers": {
"angular-cli": {
"command": "npx",
"args": ["-y", "@angular/cli", "mcp"]
}
}
}
```
### Gemini CLI
プロジェクトルートに `.gemini/settings.json` を作成します:
```json
{
"mcpServers": {
"angular-cli": {
"command": "npx",
"args": ["-y", "@angular/cli", "mcp"]
}
}
}
```
### Cursor
プロジェクトルートに `.cursor/mcp.json` を作成します(またはグローバルに `~/.cursor/mcp.json`
```json
{
"mcpServers": {
"angular-cli": {
"command": "npx",
"args": ["-y", "@angular/cli", "mcp"]
}
}
}
```
### VS Code
`.vscode/mcp.json` を作成します:
```json
{
"servers": {
"angular-cli": {
"command": "npx",
"args": ["-y", "@angular/cli", "mcp"]
}
}
}
```
## コマンドオプション
設定の `args` 配列に MCP サーバーへの引数を渡せます:
- `--read-only`:プロジェクトを変更しないツールのみを登録します。
- `--local-only`:インターネット接続を必要としないツールのみを登録します。
- `--experimental-tool``-E`):特定の実験的ツールを有効化します(例:`-E build``-E devserver`)。
実験的ツールを有効にした読み取り専用モードの例:
```json
"args": ["-y", "@angular/cli", "mcp", "--read-only", "-E", "build", "-E", "modernize"]
```

View File

@@ -0,0 +1,69 @@
# ルートへのナビゲーション
Angular はルート間を遷移するための宣言的な方法とプログラム的な方法の両方を提供します。
## 宣言的なナビゲーション(`RouterLink`
アンカー要素に `RouterLink` ディレクティブを使用します。
```ts
import {RouterLink, RouterLinkActive} from '@angular/router';
@Component({
imports: [RouterLink, RouterLinkActive],
template: `
<nav>
<a routerLink="/dashboard" routerLinkActive="active-link">Dashboard</a>
<a [routerLink]="['/user', userId]">Profile</a>
</nav>
`,
})
export class Nav {
userId = '123';
}
```
- **絶対パス**`/` で始まります(例:`/settings`)。
- **相対パス**:先頭に `/` がありません。一つ上のレベルに移動するには `../` を使います。
## プログラム的なナビゲーション(`Router`
`Router` サービスを注入して TypeScript コードからナビゲートします。
### `router.navigate()`
コマンドの配列を使用します。
```ts
private router = inject(Router);
private route = inject(ActivatedRoute);
// 標準的なナビゲーション
this.router.navigate(['/profile']);
// パラメーター付き
this.router.navigate(['/search'], {
queryParams: { q: 'angular' },
fragment: 'results'
});
// 相対ナビゲーション
this.router.navigate(['edit'], { relativeTo: this.route });
```
### `router.navigateByUrl()`
文字列パスを使用します。絶対ナビゲーションや完全な URL に最適です。
```ts
this.router.navigateByUrl('/products/123?view=details');
// 履歴の現在のエントリを置き換える
this.router.navigateByUrl('/login', {replaceUrl: true});
```
## URL パラメーター
- **ルートパラメーター**:パスの一部(例:`/user/123`)。
- **クエリパラメーター**`?` の後(例:`/search?q=query`)。
- **マトリックスパラメーター**:セグメントにスコープされる(例:`/products;category=books`)。

View File

@@ -0,0 +1,86 @@
# アウトレット(カスタムイベント)
アウトプットを使うと、子コンポーネントがカスタムイベントを発行でき、親コンポーネントがそれをリッスンできます。Angular はモダンなアプリケーションでは新しい `output()` 関数の使用を推奨しています。
## 関数ベースのアウトプット
`output()` 関数を使ってアウトプットを宣言します。これにより `OutputEmitterRef` が返されます。
```ts
import {Component, output} from '@angular/core';
@Component({
selector: 'custom-slider',
template: `<button (click)="changeValue(50)">Set to 50</button>`,
})
export class CustomSlider {
// イベントデータなしのアウトプット
panelClosed = output<void>();
// イベントデータありnumberのアウトプット
valueChanged = output<number>();
changeValue(newValue: number) {
this.valueChanged.emit(newValue);
}
}
```
### テンプレートでの使用
アウトプットイベントにバインドするには括弧 `()` を使います。イベントがデータを発行する場合、特別な `$event` 変数を使ってアクセスします。
```html
<custom-slider (panelClosed)="savePanelState()" (valueChanged)="logValue($event)" />
```
## 設定オプション
`output` 関数はエイリアスを指定するための設定オブジェクトを受け取ります。
```ts
@Component({...})
export class CustomSlider {
// テンプレートでは 'valueChanged' という名前のイベントだが、
// コンポーネントクラス内では 'changed' としてアクセスする。
changed = output<number>({ alias: 'valueChanged' });
}
```
## プログラム的なサブスクリプション
コンポーネントを動的に生成する場合、アウトプットにプログラムでサブスクライブできます:
```ts
const componentRef = viewContainerRef.createComponent(CustomSlider);
const subscription = componentRef.instance.valueChanged.subscribe((val) => {
console.log('Value changed:', val);
});
// 必要に応じて手動でクリーンアップAngular は破棄されたコンポーネントを自動でクリーンアップする)
subscription.unsubscribe();
```
## デコレーターベースのアウトプット(@Output
レガシー API は `@Output()` デコレーターと `EventEmitter` を使用します。引き続きサポートされますが、新しいコードでは推奨されません。
```ts
import { Component, Output, EventEmitter } from '@angular/core';
@Component({...})
export class LegacyExample {
@Output() valueChanged = new EventEmitter<number>();
// エイリアスあり
@Output('customEventName') changed = new EventEmitter<void>();
}
```
## ベストプラクティス
- **`output()` を優先する**`@Output()``EventEmitter` の代わりに関数ベースの `output()` を使用してください。
- **命名**:アウトプット名には `camelCase` を使用します。`on` を前置することは避けてください(例:`onValueChanged` ではなく `valueChanged` を使用する)。
- **DOM バブリングなし**Angular のカスタムイベントは、ネイティブイベントのように DOM ツリーをバブルアップしません。
- **衝突を避ける**:ネイティブ DOM イベント(`click``submit` など)と衝突する名前は選ばないでください。

View File

@@ -0,0 +1,122 @@
# リアクティブフォーム
リアクティブフォームは、フォーム入力を処理するためのモデル駆動のアプローチを提供します。オブザーバブルストリームを基盤として構築され、データモデルへの同期アクセスを提供するため、テンプレート駆動フォームよりもスケーラブルでテストしやすい特徴があります。
## コアクラス
リアクティブフォームは `@angular/forms` の以下の基本クラスで構成されます:
- `FormControl`:個別の入力の値と有効性を管理します。
- `FormGroup`:コントロールのグループ(オブジェクトのような構造)を管理します。
- `FormArray`:数値インデックスによるコントロールの配列を管理します。
- `FormBuilder`:コントロールインスタンス作成のためのファクトリーメソッドを提供するサービス。
## セットアップ
コンポーネントに `ReactiveFormsModule` をインポートします。
```ts
import {Component, inject} from '@angular/core';
import {ReactiveFormsModule, FormGroup, FormControl, Validators, FormBuilder} from '@angular/forms';
@Component({
selector: 'app-profile-editor',
imports: [ReactiveFormsModule],
templateUrl: './profile-editor.component.html',
})
export class ProfileEditor {
private fb = inject(FormBuilder);
// FormBuilder を使った簡潔な定義
profileForm = this.fb.group({
firstName: ['', Validators.required],
lastName: [''],
address: this.fb.group({
street: [''],
city: [''],
}),
aliases: this.fb.array([this.fb.control('')]),
});
onSubmit() {
console.warn(this.profileForm.value);
}
}
```
## テンプレートバインディング
モデルをビューにバインドするためのディレクティブを使用します:
- `[formGroup]``FormGroup``<form>` または `<div>` にバインドします。
- `formControlName`:グループ内の名前付きコントロールを入力にバインドします。
- `formGroupName`:ネストされた `FormGroup` をバインドします。
- `formArrayName`:ネストされた `FormArray` をバインドします。
- `[formControl]`:スタンドアロンの `FormControl` をバインドします。
```html
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
<input type="text" formControlName="firstName" />
<div formGroupName="address">
<input type="text" formControlName="street" />
</div>
<div formArrayName="aliases">
@for (alias of aliases.controls; track $index) {
<input type="text" [formControlName]="$index" />
}
</div>
<button type="submit" [disabled]="!profileForm.valid">Submit</button>
</form>
```
## コントロールへのアクセス
特に `FormArray` のコントロールに簡単にアクセスするためのゲッターを使用します。
```ts
get aliases() {
return this.profileForm.get('aliases') as FormArray;
}
addAlias() {
this.aliases.push(this.fb.control(''));
}
```
## 値の更新
- `patchValue()`:指定されたプロパティのみを更新します。構造の不一致があっても警告なく無視します。
- `setValue()`:モデル全体を置き換えます。フォームの構造を厳密に強制します。
```ts
updateProfile() {
this.profileForm.patchValue({
firstName: 'Nancy',
address: { street: '123 Drew Street' }
});
}
```
## 統合変更イベント
モダンな Angularv18+)では、すべてのコントロールに単一の `events` オブザーバブルが提供され、値、ステータス、pristine、touched、リセット、サブミットイベントを追跡できます。
```ts
import {ValueChangeEvent, StatusChangeEvent} from '@angular/forms';
this.profileForm.events.subscribe((event) => {
if (event instanceof ValueChangeEvent) {
console.log('New value:', event.value);
}
});
```
## 手動ステート管理
- `markAsTouched()` / `markAllAsTouched()`:サブミット時にバリデーションエラーを表示するのに便利です。
- `markAsDirty()` / `markAsPristine()`:値が変更されたかどうかを追跡します。
- `updateValueAndValidity()`:値とステータスの再計算を手動でトリガーします。
- ほとんどのメソッドに `{ emitEvent: false }` または `{ onlySelf: true }` オプションを渡して伝播を制御できます。

View File

@@ -0,0 +1,44 @@
# レンダリング戦略
Angular は SEO、パフォーマンス、インタラクティビティを最適化するために、複数のレンダリング戦略をサポートしています。
## 1. クライアントサイドレンダリングCSR
**デフォルト戦略。** コンテンツはブラウザで完全にレンダリングされます。
- **ユースケース**:インタラクティブなダッシュボード、社内ツール。
- **メリット**:設定が最もシンプルで、サーバーコストが低い。
- **デメリット**SEO が弱く、初期コンテンツの表示が遅いJS を待つ必要がある)。
## 2. 静的サイト生成SSG / プリレンダリング)
コンテンツは**ビルド時**に静的 HTML ファイルとして事前にレンダリングされます。
- **ユースケース**:マーケティングページ、ブログ、ドキュメント。
- **メリット**:最速の初期読み込み、優れた SEO、CDN フレンドリー。
- **デメリット**:コンテンツ更新のたびに再ビルドが必要で、ユーザー固有のデータには対応できない。
## 3. サーバーサイドレンダリングSSR
コンテンツは**初回リクエスト**に対してサーバー上でレンダリングされます。その後のナビゲーションはクライアントサイドで行われますSPA スタイル)。
- **ユースケース**EC サイトの商品ページ、ニュースサイト、パーソナライズされた動的コンテンツ。
- **メリット**:優れた SEO、初期コンテンツの表示が速い。
- **デメリット**サーバーNode.jsが必要で、サーバーコスト/レイテンシが高い。
## ハイドレーション
ハイドレーションは、サーバーレンダリングされた HTML をブラウザでインタラクティブにするプロセスです。
- **フルハイドレーション**:アプリ全体が一度にインタラクティブになります。
- **インクリメンタルハイドレーション**:(高度)`@defer` ブロックを使用して必要に応じてパーツがインタラクティブになります。
- **イベントリプレイ**:ハイドレーション完了前に発生したユーザーイベントをキャプチャして再生します。
## 判断マトリックス
| 要件 | 戦略 |
| :------------------------------ | :------------------- |
| **SEO + 静的コンテンツ** | SSG |
| **SEO + 動的コンテンツ** | SSR |
| **SEO不要 + 高インタラクティビティ** | CSR |
| **混在** | ハイブリッド(ルートベース) |

View File

@@ -0,0 +1,77 @@
# `resource` を使った非同期リアクティビティ
> [!IMPORTANT]
> `resource` API は現在 Angular で実験的な機能です。
`Resource` は非同期データフェッチを Angular のシグナルベースのリアクティビティに組み込みます。依存関係が変わるたびに非同期ローダー関数を実行し、状態と結果を同期的なシグナルとして公開します。
## 基本的な使い方
`resource` 関数は主に2つのプロパティを持つオプションオブジェクトを受け取ります
1. `params`:リアクティブな計算(`computed` のようなもの)。ここで読み取られるシグナルが変わると、リソースは再フェッチします。
2. `loader`:パラメーターに基づいてデータを取得する非同期関数。
```ts
import { Component, resource, signal, computed } from '@angular/core';
@Component({...})
export class UserProfile {
userId = signal('123');
userResource = resource({
// userId をリアクティブに追跡
params: () => ({ id: this.userId() }),
// params が変わるたびに実行される
loader: async ({ params, abortSignal }) => {
const response = await fetch(`/api/users/${params.id}`, { signal: abortSignal });
if (!response.ok) throw new Error('Network error');
return response.json();
}
});
// 計算済みシグナル内でリソースの値を使用する
userName = computed(() => {
if (this.userResource.hasValue()) {
return this.userResource.value()?.name;
} else {
return 'Loading...';
}
});
}
```
## リクエストの中断
前のローダーがまだ実行中に `params` シグナルが変わった場合、`Resource` は提供された `abortSignal` を使って実行中のリクエストを中断しようとします。**`fetch` の呼び出しには常に `abortSignal` を渡してください。**
## データの再読み込み
params を変えずにローダーを強制的に再実行させるには、`.reload()` を呼び出します。
```ts
this.userResource.reload();
```
## リソースのステータスシグナル
`Resource` オブジェクトは現在の状態を読み取るためのいくつかのシグナルを提供します:
- `value()`:解決済みデータ、または `undefined`
- `hasValue()`:型ガードとなるブール値。値が存在する場合は `true`
- `isLoading()`:ローダーが現在実行中かどうかを示すブール値。
- `error()`:ローダーがスローしたエラー、または `undefined`
- `status()`:正確な状態を表す文字列定数(`'idle'``'loading'``'resolved'``'error'``'reloading'``'local'`)。
## ローカルミューテーション
リソースの値を直接楽観的に更新できます。これにより状態が `'local'` に変わります。
```ts
this.userResource.value.set({name: 'Optimistic Update'});
```
## `httpResource` を使ったリアクティブなデータフェッチ
Angular の `HttpClient` を使用している場合は、`httpResource` の使用を優先してください。これはインターセプターを含む Angular の HTTP スタックを活用しながら、同じシグナルベースのリソース API を提供する専用ラッパーです。

View File

@@ -0,0 +1,56 @@
# ルートトランジションアニメーション
Angular ルーターは、ルート間のスムーズなビジュアルトランジションのためにブラウザの **View Transitions API** をサポートしています。
## ビュートランジションの有効化
ルーター設定に `withViewTransitions()` を追加します。
```ts
provideRouter(routes, withViewTransitions());
```
これは**プログレッシブエンハンスメント**です。API をサポートしていないブラウザでも、ルーターは引き続き動作しますが、トランジションアニメーションは行われません。
## 仕組み
1. ブラウザが古い状態のスクリーンショットを撮ります。
2. ルーターが DOM を更新します(新しいコンポーネントをアクティベートします)。
3. ブラウザが新しい状態のスクリーンショットを撮ります。
4. ブラウザが2つの状態の間をアニメーションします。
## CSS によるカスタマイズ
トランジションは**グローバル CSS ファイル**でカスタマイズします(コンポーネントスコープの CSS ではありません)。
`::view-transition-old()``::view-transition-new()` 疑似要素を使用します。
```css
/* 例:クロスフェード + スライド */
::view-transition-old(root) {
animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out;
}
::view-transition-new(root) {
animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in;
}
```
## 高度な制御
`onViewTransitionCreated` を使用して、ナビゲーションコンテキストに基づいてトランジションをスキップしたり、動作をカスタマイズしたりします。
```ts
withViewTransitions({
onViewTransitionCreated: ({transition, from, to}) => {
// 特定のルートに対してアニメーションをスキップ
if (to.url === '/no-animation') {
transition.skipTransition();
}
},
});
```
## ベストプラクティス
- **グローバルスタイル**:ビューカプセル化の問題を避けるため、トランジションアニメーションは常に `styles.css` で定義してください。
- **ビュートランジション名**:ルートをまたいでスムーズにトランジションさせたい要素(ヘッダー画像など)には、一意の `view-transition-name` を割り当ててください。

View File

@@ -0,0 +1,52 @@
# ルートガード
ルートガードは、ユーザーがルートへ遷移できるか、またはルートから離れられるかを制御します。
## ガードの種類
- **`CanActivate`**:ユーザーはこのルートにアクセスできるか?(例:認証チェック)
- **`CanActivateChild`**:ユーザーはこのルートの子にアクセスできるか?
- **`CanDeactivate`**:ユーザーはこのルートから離れられるか?(例:未保存の変更)
- **`CanMatch`**:このルートはマッチングの対象として考慮すべきか?(例:フィーチャーフラグ)`false` を返すと、ルーターは他のルートのチェックを継続します。
## ガードの作成
Angular 15 以降、ガードは通常、関数として定義します。
```ts
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isLoggedIn()) {
return true;
}
// ログインページへリダイレクト
return router.parseUrl('/login');
};
```
## ガードの適用
ルート設定に配列として追加します。順番通りに実行されます。
```ts
{
path: 'admin',
component: Admin,
canActivate: [authGuard],
canActivateChild: [adminChildGuard],
canDeactivate: [unsavedChangesGuard]
}
```
## 戻り値
- `boolean``true` で許可、`false` でブロック。
- `UrlTree` または `RedirectCommand`:別のルートへリダイレクト。
- `Observable` または `Promise`:上記の型に解決されます。
## セキュリティに関する注意
**クライアントサイドのガードはサーバーサイドのセキュリティの代わりにはなりません。** サーバー側で常に権限を検証してください。

View File

@@ -0,0 +1,45 @@
# ルーターのライフサイクルとイベント
Angular ルーターは `Router.events` オブザーバブルを通じてイベントを発行し、開始から終了までのナビゲーションのライフサイクルを追跡できます。
## 主要なルーターイベント(時系列順)
1. **`NavigationStart`**:ナビゲーションが開始されます。
2. **`RoutesRecognized`**:ルーターが URL をルートにマッチさせます。
3. **`GuardsCheckStart` / `End`**`canActivate``canMatch` などの評価が行われます。
4. **`ResolveStart` / `End`**:データ解決フェーズ(リゾルバーによるデータの取得)。
5. **`NavigationEnd`**:ナビゲーションが正常に完了しました。
6. **`NavigationCancel`**:ナビゲーションがキャンセルされました(例:ガードが `false` を返した)。
7. **`NavigationError`**:ナビゲーションが失敗しました(例:リゾルバーでのエラー)。
## イベントのサブスクライブ
`Router` を注入して `events` オブザーバブルをフィルタリングします。
```ts
import {Router, NavigationStart, NavigationEnd} from '@angular/router';
export class MyService {
private router = inject(Router);
constructor() {
this.router.events.pipe(filter((e) => e instanceof NavigationEnd)).subscribe((event) => {
console.log('Navigated to:', event.url);
});
}
}
```
## デバッグ
アプリケーション起動時にすべてのルーティングイベントの詳細なコンソールログを有効化します。
```ts
provideRouter(routes, withDebugTracing());
```
## よくあるユースケース
- **ローディングインジケーター**`NavigationStart` が発火したときにスピナーを表示し、`NavigationEnd`/`Cancel`/`Error` で非表示にします。
- **アナリティクス**`NavigationEnd` をリッスンしてページビューを追跡します。
- **スクロール管理**:カスタムスクロール動作のために `Scroll` イベントに応答します。

View File

@@ -0,0 +1,87 @@
# RouterTestingHarness を使ったテスト
ルーティングを伴うコンポーネントをテストする場合、**ルーターや関連サービスをモック化しない**ことが重要です。代わりに `RouterTestingHarness` を使用してください。これにより、実際のアプリケーションに近い環境でルーティングロジックをテストするための堅牢で信頼性の高い方法が提供されます。
ハーネスを使用することで、実際のルーター設定、ガード、リゾルバーをテストでき、より意味のあるテストにつながります。
## ルーターテストのセットアップ
`RouterTestingHarness` はルーティングシナリオをテストするための主要なツールです。また、`TestBed` 設定内で `provideRouter` 関数を使ってテスト用ルートを提供する必要があります。
### セットアップ例
```ts
import {TestBed} from '@angular/core/testing';
import {provideRouter} from '@angular/router';
import {RouterTestingHarness} from '@angular/router/testing';
import {Dashboard} from './dashboard.component';
import {HeroDetail} from './hero-detail.component';
describe('Dashboard Component Routing', () => {
let harness: RouterTestingHarness;
beforeEach(async () => {
// 1. テスト用ルートで TestBed を設定
await TestBed.configureTestingModule({
providers: [
// テスト固有のルートで provideRouter を使用
provideRouter([
{path: '', component: Dashboard},
{path: 'heroes/:id', component: HeroDetail},
]),
],
}).compileComponents();
// 2. RouterTestingHarness を作成
harness = await RouterTestingHarness.create();
});
});
```
### 主要なコンセプト
1. **`provideRouter([...])`**:テスト用のルーティング設定を提供します。テスト対象のコンポーネントが正しく機能するために必要なルートを含める必要があります。
2. **`RouterTestingHarness.create()`**:ハーネスを非同期で作成・初期化し、ルート URL`/`)への初回ナビゲーションを実行します。
## ルーターテストの作成
ハーネスを作成したら、ナビゲーションを駆動し、ルーターの状態やアクティベートされたコンポーネントの状態に対してアサーションを行えます。
### 例:ナビゲーションのテスト
```ts
it('should navigate to a hero detail when a hero is selected', async () => {
// 1. 初期コンポーネントにナビゲートし、そのインスタンスを取得
const dashboard = await harness.navigateByUrl('/', Dashboard);
// ダッシュボードにヒーローを選択するメソッドがあると仮定
const heroToSelect = {id: 42, name: 'Test Hero'};
dashboard.selectHero(heroToSelect);
// ナビゲーションをトリガーするアクション後の安定を待つ
await harness.fixture.whenStable();
// 2. URL に対してアサート
expect(harness.router.url).toEqual('/heroes/42');
// 3. ナビゲーション後にアクティベートされたコンポーネントを取得
const heroDetail = await harness.getHarness(HeroDetail);
// 4. 新しいコンポーネントの状態に対してアサート
expect(await heroDetail.componentInstance.hero.name).toBe('Test Hero');
});
it('should get the activated component directly', async () => {
// 一度にナビゲートしてコンポーネントインスタンスを取得
const dashboardInstance = await harness.navigateByUrl('/', Dashboard);
expect(dashboardInstance).toBeInstanceOf(Dashboard);
});
```
### ベストプラクティス
- **ハーネスでナビゲートする**:ナビゲーションをシミュレートするには常に `harness.navigateByUrl()` を使用してください。このメソッドはアクティベートされたコンポーネントのインスタンスで解決される Promise を返します。
- **ルーターの状態にアクセスする**`harness.router` を使用してライブのルーターインスタンスにアクセスし、状態に対してアサートします(例:`harness.router.url`)。
- **アクティベートされたコンポーネントの取得**:現在アクティベートされているルーティングコンポーネントのコンポーネントハーネスインスタンスを取得するには `harness.getHarness(ComponentType)` を、`DebugElement` を取得するには `harness.routeDebugElement` を使用してください。
- **安定を待つ**:ナビゲーションを引き起こすアクションを実行した後は、アサーションを行う前に必ず `await harness.fixture.whenStable()` でルーティングの完了を待ってください。

View File

@@ -0,0 +1,68 @@
# アウトレットを使ったルートの表示
`RouterOutlet` ディレクティブは、Angularが現在のURLに対応するコンポーネントをレンダリングするためのプレースホルダーです。
## 基本的な使い方
テンプレートに `<router-outlet />` を含めます。Angularはルーティングされたコンポーネントをアウトレットの直後の兄弟要素として挿入します。
```html
<app-header /> <router-outlet />
<!-- ルートのコンテンツがここに表示される -->
<app-footer />
```
## ネストされたアウトレット
子ルートには、親コンポーネントのテンプレート内に独自の `<router-outlet />` が必要です。
```ts
// 親コンポーネントのテンプレート
<h1>Settings</h1>
<router-outlet /> <!-- ProfileやSecurityなどの子コンポーネントがここにレンダリングされる -->
```
## 名前付きアウトレット(セカンダリルート)
ページには複数のアウトレットを設定できます。アウトレットに `name` を割り当てることで、特定のアウトレットをターゲットにできます。デフォルト名は `'primary'` です。
```html
<router-outlet />
<!-- プライマリ -->
<router-outlet name="sidebar" />
<!-- セカンダリ -->
```
ルート設定で `outlet` を定義します:
```ts
{
path: 'chat',
component: Chat,
outlet: 'sidebar'
}
```
## アウトレットのライフサイクルイベント
`RouterOutlet` はコンポーネントが変更されるときにイベントを発行します:
- `activate`: 新しいコンポーネントがインスタンス化された。
- `deactivate`: コンポーネントが破棄された。
- `attach` / `detach`: `RouteReuseStrategy` と共に使用される。
```html
<router-outlet (activate)="onActivate($event)" />
```
## `routerOutletData` によるデータの受け渡し
`routerOutletData` インプットを使用して、ルーティングされたコンポーネントにコンテキストデータを渡すことができます。コンポーネントはシグナルとして `ROUTER_OUTLET_DATA` インジェクショントークン経由でこのデータにアクセスします。
```ts
// 親コンポーネント内
<router-outlet [routerOutletData]="{ theme: 'dark' }" />
// ルーティングされたコンポーネント内
outletData = inject(ROUTER_OUTLET_DATA) as Signal<{ theme: string }>;
```

View File

@@ -0,0 +1,795 @@
# シグナルフォーム
シグナルフォームは、対象のAngularバージョンがサポートしている場合、新しいフォームに推奨されます。Angularシグナルを使用して、リアクティブかつ型安全なモデル駆動型のフォーム状態管理を提供します。
シグナルフォームを使用する場合、フィールドの値や型に `null` を使用しないでください。
## インポート
`@angular/forms/signals` から以下をインポートできます:
```ts
import {
form,
FormField,
submit,
// フィールド状態のルール
disabled,
hidden,
readonly,
debounce,
// スキーマヘルパー
applyWhen,
applyEach,
schema,
// カスタムバリデーション
validate,
validateHttp,
validateStandardSchema,
// メタデータ
metadata,
} from '@angular/forms/signals';
```
## フォームの作成
シグナルモデルとともに `form()` 関数を使用します。フォームの構造はモデルから直接導出されます。
```ts
import {Component, signal} from '@angular/core';
import {form, FormField} from '@angular/forms/signals';
@Component({
// ...
imports: [FormField],
})
export class Example {
// 1. 初期値でモデルを定義するundefinedは避ける
userModel = signal({
name: '', // 重要: 初期値にnullやundefinedは絶対に使用しない
email: '',
age: 0, // 数値には0を使用し、nullは使用しない
address: {
street: '',
city: '',
},
hobbies: [] as string[], // 配列には[]を使用し、nullは使用しない
});
// 誤り - このようにしてはいけない:
// badModel = signal({
// name: null, // エラー: '' を使用すること
// age: null, // エラー: 0 を使用すること
// items: null // エラー: [] を使用すること
// });
// 2. フォームを作成する
userForm = form(this.userModel);
}
```
## バリデーション
バリデーターを `@angular/forms/signals` からインポートします。
```ts
import {required, email, min, max, minLength, maxLength, pattern} from '@angular/forms/signals';
```
`form()` に渡すスキーマ関数内で使用します:
```ts
userForm = form(this.userModel, (schemaPath) => {
// 必須
required(schemaPath.name, {message: 'Name is required'});
// 条件付き必須
required(schemaPath.name, {
when({valueOf}) {
return valueOf(schemaPath.age) > 10;
},
});
// when は required にのみ使用可能
// このようにしてはいけない: pattern(p.name, /xxx/, {when /* ERROR */)
// メールアドレス
email(schemaPath.email, {message: 'Invalid email'});
// 数値の最小値/最大値
min(schemaPath.age, 18);
max(schemaPath.age, 100);
// 文字列/配列の最小長/最大長
minLength(schemaPath.password, 8);
maxLength(schemaPath.description, 500);
// パターン(正規表現)
pattern(schemaPath.zipCode, /^\d{5}$/);
});
```
## FieldState と FormField: 親要素の要件
**FormField**(構造)と **FieldState**(実際のデータ/シグナル)の違いを理解することが重要です。
**ルール**: フィールドを関数として**呼び出す**ことで、そのステートシグナルvalid、touched、dirty、hidden など)にアクセスできます。
```ts
// f は FormField構造的
const f = form(signal({cat: {name: 'pirojok-the-cat', age: 5}}));
f.cat.name; // FormField: ここからフラグは取得できない!
f.cat.name.touched(); // エラー: touched() は FormField に存在しない
f.cat.name(); // FieldState: 呼び出すことでシグナルへのアクセスが可能になる
f.cat.name().touched(); // 有効: シグナルへのアクセス
f.cat().name.touched(); // エラー: f.cat() はステートであり、子要素を持たない!
```
テンプレートでも同様です:
```html
<!-- 誤り: 'hidden' プロパティは 'FormField' 型に存在しない -->
@if (bookingForm.hotelDetails.hidden()) { ... }
<!-- 正しい: 最初に呼び出す -->
@if (bookingForm.hotelDetails().hidden()) { ... }
```
## Disabled / Readonly / Hidden
スキーマ内のルールを使用してフィールドのステータスを制御します。
```ts
import {disabled, readonly, hidden} from '@angular/forms/signals';
userForm = form(this.userModel, (schemaPath) => {
// 条件付き無効化
disabled(schemaPath.password, ({valueOf}) => !valueOf(schemaPath.createAccount));
// 条件付き非表示(モデルからは削除せず、非表示としてマークするのみ)
hidden(schemaPath.shippingAddress, ({valueOf}) => valueOf(schemaPath.sameAsBilling));
// 読み取り専用
readonly(schemaPath.username);
});
```
## バインディング
`FormField` をインポートし、`[formField]` ディレクティブを使用します。
```ts
import {FormField} from '@angular/forms/signals';
```
`disabled``hidden``readonly``name` などのステート上のすべてのプロパティは自動的にバインドされます。
`name` フィールドは手動でバインド_しないでください_。
**重要: 禁止属性**
`[formField]` を使用する場合、テンプレートで以下の属性を設定してはなりません(静的またはバインドされた形式のいずれも):
- `min``max`(代わりにスキーマ内のバリデーターを使用)
- `value``[value]``[attr.value]``[formField]` によって処理済み)
- `[attr.min]``[attr.max]`
- `[disabled]``[readonly]``[formField]` によって処理済み)
このようにしてはいけません: `<input min="1" [formField]>``<input [value]="val" [formField]>`
```html
<!-- 入力 -->
<input [formField]="userForm.name" />
<!-- チェックボックス -->
<input type="checkbox" [formField]="userForm.isAdmin" />
<!-- セレクト -->
<select [formField]="userForm.country">
<option value="us">US</option>
</select>
<!-- userForm.name は null にできない。input は null を受け付けないため -->
<input [formField]="userForm.name" />
```
## リアクティブフォーム
`@angular/forms` から `FormControl``FormGroup``FormArray``FormBuilder` を**インポートしないでください**。シグナルフォームはこれらのコンセプトを完全に置き換えます。
シグナルフォームにはビルダーがありません。
## ステートへのアクセス
フォーム内の各フィールドは、そのステートを返す関数です。
```ts
// フィールドを呼び出してアクセスする
const emailState = this.userForm.email();
// 値WritableSignal
const value = this.userForm().value();
// バリデーションステート(シグナル)
const isValid = this.userForm().valid();
const isInvalid = this.userForm().invalid();
const errors = this.userForm().errors(); // エラーの配列
const isPending = this.userForm().pending(); // 非同期バリデーション待ち
// インタラクションステート(シグナル)
const isTouched = this.userForm().touched();
const isDirty = this.userForm().dirty();
// 可用性ステート(シグナル)
const isDisabled = this.userForm().disabled();
const isHidden = this.userForm().hidden();
const isReadonly = this.userForm().readonly();
```
重要!: ステートを取得するには、必ずフィールドを呼び出してください。
```ts
form().invalid()
form.field().dirty()
form.field.subfield().touched()
form.a.b.c.d().value()
form.address.ssn().pending()
form().reset()
// 唯一の例外は length です:
form.children.length
form.length // 注意: 括弧なし!
form.client.addresses.length // "()" なし
@for (income of form.addresses; track $index) {/**/}
```
## 送信
`submit()` 関数を使用します。アクションを実行する前に、すべてのフィールドを自動的にタッチ済みとしてマークします。
**重要**: `submit()` へのコールバックは `async` でなければならず、Promise を返す必要があります。
```ts
import { submit } from '@angular/forms/signals';
// 正しい - async コールバック
onSubmit() {
submit(this.userForm, async () => {
// フォームが有効な場合のみ実行される
await this.apiService.save(this.userModel());
console.log('Saved!');
});
}
// 誤り - async キーワードが欠けている
onSubmit() {
submit(this.userForm, () => { // エラー: async でなければならない
console.log('Saved!');
});
}
```
## エラー処理
`field().errors()` は ValidationError の配列を返します:
```ts
interface ValidationError {
readonly kind: string;
readonly message?: string;
}
```
バリデーターから null を返さないでください。
エラーがない場合は undefined を返してください。
### コンテキスト
`validate()``disabled()``applyWhen` などのルールに渡される関数は、コンテキストオブジェクトを受け取ります。その構造を理解することが**重要**です:
```ts
validate(
schemaPath.username,
({
value, // Signal<T>: フィールドの現在値(書き込み可能)
fieldTree, // FieldTree<T>: サブフィールド(グループ/配列の場合)
state, // FieldState<T>: state.valid()、state.dirty() などのフラグへのアクセス
valueOf, // (path) => T: 他のフィールドの値を読む(依存関係を追跡)例: valueOf(schemaPath.password)
stateOf, // (path) => FieldState: 他のフィールドのステートにアクセス 例: stateOf(schemaPath.password).valid()
pathKeys, // Signal<string[]>: ルートからこのフィールドへのパス
}) => {
// 誤り: if (touched()) ... touched はコンテキスト内にない)
// 正しい: if (state.touched()) ...
if (value() === 'admin') {
return {kind: 'reserved', message: 'Username admin is reserved'};
}
},
);
```
### 重要: パスはシグナルではない
`form()` コールバック内で、`schemaPath` とその子要素(例: `schemaPath.user.name`)は**シグナルではなく**、**呼び出し可能でもありません**。
```ts
// 誤り - エラーが発生します:
applyWhen(p.ssn, () => p.ssn().touched(), (ssnField) => { ... });
// 正しい - stateOf() を使用してパスのステートを取得する:
applyWhen(p.ssn, ({ stateOf }) => stateOf(p.ssn).touched(), (ssnField) => { ... });
// 正しい - valueOf() を使用してパスの値を取得する:
applyWhen(p.ssn, ({ valueOf }) => valueOf(p.ssn) !== '', (ssnField) => { ... });
```
### 複数アイテム
- アイテムごとにルールを適用するには `applyEach` を使用します。
- **重要**: `applyEach` のコールバックは引数を**1つだけ**取りますアイテムパス。2つではありません:
```ts
// 正しい - 引数1つ
applyEach(s.items, (item) => {
required(item.name);
});
// 誤り - インデックスを渡さない
applyEach(s.items, (item, index) => {
// エラー: コールバックは引数を1つしか取らない
required(item.name);
});
```
- テンプレートではアイテムの反復に `@for` を使用します。
- 配列からアイテムを削除するには、データ内の該当アイテムをそのまま削除してください。
- **`select` バインディング**: `<select [formField]="form.country">` にバインドできます。オプションには `value` 属性があることを確認してください。
### ネストした @for ループ
**重要**: AngularにはAngularには `$parent` がありません。ネストされたループでは、外側のインデックスを変数に保存してください:
```html
<!-- 誤り - $parent は存在しない -->
@for (item of form.items; track $index) { @for (option of item.options; track $index) {
<button (click)="removeOption($parent.$index, $index)">Remove</button>
<!-- エラー -->
} }
<!-- 正しい - let を使用して外側のインデックスを保存する -->
@for (item of form.items; track $index; let outerIndex = $index) { @for (option of item.options;
track $index) {
<button (click)="removeOption(outerIndex, $index)">Remove</button>
} }
```
### フォームボタンの無効化
```html
<button [disabled]="form().invalid() || form().pending()" />
<!-- または -->
<button [disabled]="taxForm.invalid()" />
```
inputに `[disabled]` を使用しないでください。`[formField]` がこれを行います。
inputに `[readonly]` を使用しないでください。`[formField]` がこれを行います。
フィールドを無効化または読み取り専用にする必要がある場合は、スキーマ内の `disabled()` または `readonly()` ルールを使用してください。
### 非同期バリデーション
非同期には `validate()` を使用せず、代わりに `validateAsync()` を使用してください:
**重要**:
1. `params` オプションはバリデートする値を返す関数でなければなりません。
2. `onError` ハンドラーは**必須**です - 省略不可です!
```ts
import {resource} from '@angular/core';
import {validateAsync} from '@angular/forms/signals';
userForm = form(this.userModel, (s) => {
validateAsync(s.username, {
// 1. 関数でなければならない - params はコンテキストを受け取り、値を返す
params: ({value}) => value(),
// 2. リソースを作成する - ファクトリーはシグナルを受け取る
factory: (username) =>
resource({
params: username, // resource() では 'params' を使用
loader: async ({params: value}) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return value === 'taken';
},
}),
// 3. 成功をエラーにマッピングする
onSuccess: (isTaken) =>
isTaken ? {kind: 'taken', message: 'Username is already taken'} : undefined,
// 4. エラー処理 - これは必須です!
onError: () => ({kind: 'error', message: 'Validation failed'}),
});
});
```
**誤りの例:**
```ts
// 誤り - params は関数でなければならない
validateAsync(s.username, {
params: s.username, // エラー: ({ value }) => value() でなければならない
// ...
});
// 誤り - onError が欠けている(必須です!)
validateAsync(s.username, {
params: ({value}) => value(),
factory: (username) =>
resource({
/* ... */
}),
onSuccess: (result) => (result ? {kind: 'error'} : undefined),
// エラー: 'onError' が欠けているが必須!
});
```
### リソースの使用
**重要**: Angularの `resource()` では、入力シグナルに `params` を使用してください。
```ts
// 正しい
resource({
params: mySignal,
loader: async ({params: value}) => {
/* ... */
},
});
// 誤り
resource({
request: mySignal, // エラー: 'params' にすべき
loader: async ({request}) => {
/* ... */
},
});
```
UIとモデル間の同期を遅延させるには `debounce()` を使用します。
```ts
import {debounce} from '@angular/forms/signals';
userForm = form(this.userModel, (s) => {
// モデルの更新を300ms遅延させる
debounce(s.username, 300);
});
```
### 条件付きバリデーション
```ts
form(
data,
(path) => {
applyWhen(
name,
({value}) => value() !== 'admin',
(namePath) => {
validate(namePath.last /* ... */);
disable(namePath.last /* ... */);
},
);
},
{injector: TestBed.inject(Injector)},
);
```
`applyWhen` は最初の引数にマッピングされたパスを渡します。
親フィールドが必要な場合は、それを `applyWhen` に渡してください:
```ts
form(
data,
(path) => {
applyWhen(
cat,
({value}) => value().name !== 'admin',
(catPath) => {
require(cat.catPath /* ... */);
},
);
},
{injector: TestBed.inject(Injector)},
);
```
## よくある落とし穴(やってはいけないこと)
| エラーシナリオ | 誤り(よくあるミス) | 正しい方法 |
| :------------------------- | :------------------------------------------------------------ | :---------------------------------------------------------- |
| **フラグへのアクセス** | `form.field.valid()` | `form.field().valid()` |
| **値へのアクセス** | `form.field.value()` | `form.field().value()` |
| **値の設定** | `form.field.set(x)` | モデルシグナルを更新: `this.model.update(...)` |
| **フォームルートフラグ** | `form.invalid()` | `form().invalid()` |
| **二重呼び出し** | `form.field()()` | `form.field().value()` |
| **ルールコンテキスト** | `({ touched }) => touched()` | `({ state }) => state.touched()` |
| **パスの呼び出し** | `applyWhen(p.foo, () => p.foo() === 'x')` | `applyWhen(p.foo, ({ valueOf }) => valueOf(p.foo) === 'x')` |
| **applyWhen の引数** | `applyWhen(condition, () => {...})` | `applyWhen(path, condition, schemaFn)` - 引数3つ必要 |
| **配列の長さ** | `form.items().length` | `form.items.length`(構造的) |
| **複数選択配列** | `<select [formField]="form.tags">` (string[]) | 配列フィールドにはチェックボックスを使用 |
| **readonly 属性** | `<input readonly [formField]>` | スキーマで `readonly()` ルールを使用 |
| **min/max 属性** | `<input min="1" max="10">` | スキーマで `min()``max()` ルールを使用 |
| **value バインディング** | `<input [value]="val">` | `[formField]` と共に `[value]` を使用しない |
| **when オプション** | `pattern(p.x, /.../, {when: ...})` | `when``required()` でのみ動作する |
| **Submit コールバック** | `submit(form, () => { ... })` | `submit(form, async () => { ... })` |
| **Async params** | `params: s.field` | `params: ({ value }) => value()` |
| **Async onError** | `onError` を省略する | `validateAsync` では `onError` は必須 |
| **resource() API** | `request: signal` | `params: signal` |
| **applyEach の引数** | `applyEach(s.items, (item, index) => ...)` | `applyEach(s.items, (item) => ...)` |
| **ネストした @for** | `$parent.$index` | `let outerIndex = $index` を使用 |
| **FormState インポート** | `import { FormState }` | `FormState` は存在しない。`FieldState` を使用 |
| **モデル内の Null** | `signal({ name: null })` | `signal({ name: '' })` または `signal({ age: 0 })` |
| **Validate 構文** | `validate(s.field, { value } => ...)` | `validate(s.field, ({ value }) => ...)` |
| **チェックボックス配列** | `[formField]="form.tags"` (string[]) | チェックボックスは `boolean` にのみバインドする |
## 大規模フォームの例
### `src/app/app.ts`
```ts
import {Component, signal, ChangeDetectionStrategy} from '@angular/core';
import {
form,
FormField,
submit,
required,
email,
min,
hidden,
applyEach,
validate,
} from '@angular/forms/signals';
@Component({
selector: 'app-root',
standalone: true,
imports: [FormField],
templateUrl: './app.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
model = signal({
personalInfo: {
firstName: '',
lastName: '',
email: '',
age: 0,
},
tripDetails: {
destination: 'Mars',
launchDate: '',
},
package: {
tier: 'economy',
extras: [] as string[],
},
companions: [] as Array<{name: string; relation: string}>,
});
bookingForm = form(this.model, (s) => {
required(s.personalInfo.firstName, {message: 'First name is required'});
required(s.personalInfo.lastName, {message: 'Last name is required'});
required(s.personalInfo.email, {message: 'Email is required'});
email(s.personalInfo.email, {message: 'Invalid email address'});
required(s.personalInfo.age, {message: 'Age is required'});
min(s.personalInfo.age, 18, {message: 'Must be at least 18'});
required(s.tripDetails.destination);
required(s.tripDetails.launchDate);
validate(s.tripDetails.launchDate, ({value}) => {
const date = new Date(value());
if (isNaN(date.getTime())) return undefined;
const today = new Date();
if (date < today) {
return {kind: 'pastData', message: 'Launch date must be in the future'};
}
return undefined;
});
// valueOf は、ルール内で他のフィールドの値にアクセスするために使用される
hidden(s.package.extras, ({valueOf}) => valueOf(s.package.tier) === 'economy');
applyEach(s.companions, (companion) => {
required(companion.name, {message: 'Companion name required'});
required(companion.relation, {message: 'Relation required'});
});
});
addCompanion() {
this.model.update((m) => ({
...m,
companions: [...m.companions, {name: '', relation: ''}],
}));
}
removeCompanion(index: number) {
this.model.update((m) => ({
...m,
companions: m.companions.filter((_, i) => i !== index),
}));
}
onSubmit() {
// 重要: submit コールバックは async でなければならない
submit(this.bookingForm, async () => {
console.log('Booking Confirmed:', this.model());
// 非同期処理が必要な場合:
// await this.apiService.save(this.model());
});
}
}
```
### `src/app/app.html`
```html
<form (submit)="onSubmit(); $event.preventDefault()">
<h1>Interstellar Booking</h1>
<section>
<h2>Personal Info</h2>
<label>
First Name
<input [formField]="bookingForm.personalInfo.firstName" />
@if (bookingForm.personalInfo.firstName().touched() &&
bookingForm.personalInfo.firstName().errors().length) {
<span>{{ bookingForm.personalInfo.firstName().errors()[0].message }}</span>
}
</label>
<label>
Last Name
<input [formField]="bookingForm.personalInfo.lastName" />
@if (bookingForm.personalInfo.lastName().touched() &&
bookingForm.personalInfo.lastName().errors().length) {
<span>{{ bookingForm.personalInfo.lastName().errors()[0].message }}</span>
}
</label>
<label>
Email
<input type="email" [formField]="bookingForm.personalInfo.email" />
@if (bookingForm.personalInfo.email().touched() &&
bookingForm.personalInfo.email().errors().length) {
<span>{{ bookingForm.personalInfo.email().errors()[0].message }}</span>
}
</label>
<label>
Age
<input type="number" [formField]="bookingForm.personalInfo.age" />
@if (bookingForm.personalInfo.age().touched() &&
bookingForm.personalInfo.age().errors().length) {
<span>{{ bookingForm.personalInfo.age().errors()[0].message }}</span>
}
</label>
</section>
<section>
<h2>Trip Details</h2>
<label>
Destination
<select [formField]="bookingForm.tripDetails.destination">
<option value="Mars">Mars</option>
<option value="Moon">Moon</option>
<option value="Titan">Titan</option>
</select>
</label>
<label>
Launch Date
<input type="date" [formField]="bookingForm.tripDetails.launchDate" />
@if (bookingForm.tripDetails.launchDate().touched() &&
bookingForm.tripDetails.launchDate().errors().length) {
<span>{{ bookingForm.tripDetails.launchDate().errors()[0].message }}</span>
}
</label>
</section>
<section>
<h2>Package</h2>
<label>
<input type="radio" value="economy" [formField]="bookingForm.package.tier" />
Economy
</label>
<label>
<input type="radio" value="business" [formField]="bookingForm.package.tier" />
Business
</label>
<label>
<input type="radio" value="first" [formField]="bookingForm.package.tier" />
First Class
</label>
@if (!bookingForm.package.extras().hidden()) {
<div>
<h3>Extras</h3>
<!-- 配列の複数選択には select multiple を使用する -->
<select multiple [formField]="bookingForm.package.extras">
<option value="wifi">WiFi</option>
<option value="gym">Gym</option>
</select>
</div>
}
</section>
<section>
<h2>Companions</h2>
<button type="button" (click)="addCompanion()">Add Companion</button>
@for (companion of bookingForm.companions; track $index) {
<div>
<input [formField]="companion.name" placeholder="Name" />
@if (companion.name().touched() && companion.name().errors().length) {
<span>{{ companion.name().errors()[0].message }}</span>
}
<input [formField]="companion.relation" placeholder="Relation" />
@if (companion.relation().touched() && companion.relation().errors().length) {
<span>{{ companion.relation().errors()[0].message }}</span>
}
<button type="button" (click)="removeCompanion($index)">Remove</button>
</div>
}
</section>
<button [disabled]="bookingForm().invalid()">Submit</button>
</form>
```
## ビルドエラーからの回復
ビルドエラーが発生した場合、最も一般的な修正方法を示します:
### `Property 'value' does not exist on type 'FieldTree'`
**問題**: 最初に呼び出さずにフィールドの `.value()` に直接アクセスしています。
```ts
// 誤り
const val = this.form.field.value();
// 正しい
const val = this.form.field().value();
```
### `Property 'set' does not exist on type 'FieldTree'`
**問題**: フォームツリーに対して値を設定しようとしています。シグナルフォームはモデル駆動型です。
```ts
// 誤り
this.form.address.street.set('Main St');
// 正しい - 代わりにモデルシグナルを更新する
this.model.update((m) => ({...m, address: {...m.address, street: 'Main St'}}));
```
### `Type 'string[]' is not assignable to type 'string'`
**問題**: 配列フィールドに単一値の `<select>``[formField]` をバインドしています。
```html
<!-- 誤り - assignees は string[]、select は string を期待している -->
<select [formField]="form.assignees">
...
</select>
<!-- 正しい - 配列フィールドには select multiple を使用する -->
<select multiple [formField]="form.assignees">
<option value="us">US</option>
</select>
```

View File

@@ -0,0 +1,94 @@
# Angular シグナル概要
シグナルは、モダンなAngularアプリケーションにおけるリアクティビティの基盤です。**シグナル**とは、値が変更されたときに関心のあるコンシューマーに通知する、値のラッパーです。
## 書き込み可能なシグナル(`signal`
`signal()` を使用して、直接更新できる状態を作成します。
```ts
import {signal} from '@angular/core';
// 書き込み可能なシグナルを作成
const count = signal(0);
// 値を読み取る(常にゲッター関数を呼び出す必要がある)
console.log(count());
// 値を直接更新する
count.set(3);
// 前の値に基づいて更新する
count.update((value) => value + 1);
```
### 読み取り専用として公開する
サービスから状態を公開する場合、外部からの変更を防ぐために読み取り専用バージョンを公開するのがベストプラクティスです。
```ts
private readonly _count = signal(0);
// コンシューマーはこれを読み取れるが、.set() や .update() は呼び出せない
readonly count = this._count.asReadonly();
```
## 算出シグナル(`computed`
`computed()` を使用して、他のシグナルから値を導出する読み取り専用のシグナルを作成します。
- **遅延評価**: 導出関数は算出シグナルが読み取られるまで実行されません。
- **メモ化**: 結果はキャッシュされます。依存するシグナルのいずれかが変更されたときのみ再計算されます。
- **動的な依存関係**: 導出中に_実際に読み取られた_シグナルのみが追跡されます。
```ts
import {signal, computed} from '@angular/core';
const count = signal(0);
const doubleCount = computed(() => count() * 2);
// doubleCount は count が変更されると自動的に更新される。
```
## リアクティブコンテキスト
**リアクティブコンテキスト**とは、Angularが依存関係を確立するためにシグナルの読み取りを監視するランタイム状態です。
Angularは以下を評価する際に自動的にリアクティブコンテキストに入ります:
- `computed` シグナル
- `effect` コールバック
- `linkedSignal` の計算
- コンポーネントテンプレート
### 追跡なしの読み取り(`untracked`
リアクティブコンテキスト内でシグナルを読み取る際に、依存関係を作成_せずに_シグナルが変更されてもコンテキストが再実行されないように読み取る必要がある場合は、`untracked()` を使用します。
```ts
import {effect, untracked} from '@angular/core';
effect(() => {
// このエフェクトは currentUser が変更されたときのみ実行される。
// counter がここで読み取られても、counter が変更されたときには実行されない。
console.log(`User: ${currentUser()}, Count: ${untracked(counter)}`);
});
```
### リアクティブコンテキストでの非同期操作
リアクティブコンテキストは**同期**コードに対してのみ有効です。`await` の後のシグナル読み取りは追跡されません。**常に非同期境界の前にシグナルを読み取ってください。**
```ts
// 誤り: theme() は await の後に読み取られるため追跡されない
effect(async () => {
const data = await fetchUserData();
console.log(theme());
});
// 正しい: await の前にシグナルを読み取る
effect(async () => {
const currentTheme = theme();
const data = await fetchUserData();
console.log(currentTheme);
});
```

View File

@@ -0,0 +1,69 @@
# AngularでのTailwind CSSの使用
Tailwind CSSはユーティリティファーストのCSSフレームワークで、Angularとシームレスに統合できます。
**エージェントへの重要な注意: 常にTailwind CSS v4のプラクティスに従ってください。古いTailwind v3のパターン`tailwind.config.js` を作成して `@tailwind` ディレクティブを使用するなどに戻らないでください。これはアプリケーションのビルドを壊す原因となります。モダンなAngularプロジェクトはTailwind v4を使用します。**
## 自動セットアップ(推奨)
Angular CLIを使用してAngularプロジェクトにTailwind CSSを追加する最も簡単な方法:
```shell
ng add tailwindcss
```
これにより自動的に以下が実行されます:
1. `tailwindcss` と必要な依存関係のインストール。
2. プロジェクトをTailwind CSSを使用するように設定。
3. グローバルスタイルへの適切なインポートの追加。
## 手動セットアップTailwind v4
手動でセットアップする場合は、以下のTailwind v4パターンを使用してください:
### 1. 依存関係のインストール
Tailwind CSSとPostCSSをインストールします:
```shell
npm install tailwindcss @tailwindcss/postcss postcss
```
### 2. PostCSSの設定
プロジェクトルートに `.postcssrc.json` ファイルを作成します:
```json
{
"plugins": {
"@tailwindcss/postcss": {}
}
}
```
_`tailwind.config.js` ファイルは作成しないでくださいv4での設定はCSS変数で処理されます_
### 3. Tailwind CSSのインポート
グローバルスタイルファイル(例: `src/styles.css`に、標準的なv4インポートを追加します:
```css
@import 'tailwindcss';
```
_SCSSを使用している場合は `@use 'tailwindcss';` を使用してください)。_
### 4. ユーティリティクラスの使用
コンポーネントテンプレートでTailwindクラスを直接使用できます:
```html
<h1 class="text-3xl font-bold underline">Hello world!</h1>
```
## AIエージェントへのまとめ
- **`@tailwind base; @tailwind components; @tailwind utilities;` は使用しないでください**。`@import 'tailwindcss';` を使用してください。
- **`tailwind.config.js` は作成しないでください**。設定はテーマ変数を通じたCSS内、またはPostCSS設定を使用して直接管理されます。
- v4の構文とワークフローに厳密に従ってください。

View File

@@ -0,0 +1,114 @@
# テンプレート駆動フォーム
テンプレート駆動フォームは、双方向データバインディング(`[(ngModel)]`を使用して、テンプレートで変更が加えられるとコンポーネントのデータモデルを更新し、その逆も同様に行います。シンプルなフォームに最適で、HTMLテンプレート内のディレクティブを使用してフォームの状態とバリデーションを管理します。
## 主要なディレクティブ
テンプレート駆動フォームは `FormsModule` に依存しており、以下の主要なディレクティブを提供します:
- `NgModel`: フォーム要素の値の変更をデータモデルと調整します(`[(ngModel)]`)。
- `NgForm`: `<form>` タグにバインドされたトップレベルの `FormGroup` を自動的に作成します。
- `NgModelGroup`: DOM要素にバインドされたネストされた `FormGroup` を作成します。
## セットアップ
まず、コンポーネントまたはモジュールに `FormsModule` をインポートします。
```ts
import {Component} from '@angular/core';
import {FormsModule} from '@angular/forms';
@Component({
selector: 'app-user-form',
imports: [FormsModule],
templateUrl: './user-form.component.html',
})
export class UserForm {
user = {name: '', role: 'Guest'};
onSubmit() {
console.log('Form submitted!', this.user);
}
}
```
## フォームテンプレートの構築
### `[(ngModel)]` による双方向バインディング
入力要素に `[(ngModel)]` を使用します。**`[(ngModel)]` を使用するすべての要素には `name` 属性が必須です。** Angularは `name` 属性を使用して、親 `NgForm` にコントロールを登録します。
```html
<form #userForm="ngForm" (ngSubmit)="onSubmit()">
<!-- 基本的な入力 -->
<div>
<label for="name">Name:</label>
<input type="text" id="name" required [(ngModel)]="user.name" name="name" #nameCtrl="ngModel" />
</div>
<!-- セレクトボックス -->
<div>
<label for="role">Role:</label>
<select id="role" [(ngModel)]="user.role" name="role">
<option value="Admin">Admin</option>
<option value="Guest">Guest</option>
</select>
</div>
<!-- 送信ボタン(フォームが無効な場合は無効化) -->
<button type="submit" [disabled]="!userForm.form.valid">Submit</button>
</form>
```
## フォームとコントロールの状態
Angularは状態に基づいてコントロールとフォームにCSSクラスを自動的に適用します:
| 状態 | Trueの場合のクラス | Falseの場合のクラス |
| :------------- | :-------------------------------- | :------------- |
| 訪問済み | `ng-touched` | `ng-untouched` |
| 値が変更済み | `ng-dirty` | `ng-pristine` |
| 値が有効 | `ng-valid` | `ng-invalid` |
| フォーム送信済み | `ng-submitted``<form>` のみ) | - |
これらのクラスをCSSでビジュアルフィードバックとして使用できます:
```css
.ng-valid[required],
.ng-valid.required {
border-left: 5px solid #42a948; /* 緑 */
}
.ng-invalid:not(form) {
border-left: 5px solid #a94442; /* 赤 */
}
```
## バリデーションとエラーメッセージ
条件付きでエラーメッセージを表示するには、`ngModel` ディレクティブをテンプレート参照変数(例: `#nameCtrl="ngModel"`)にエクスポートします。
```html
<input type="text" id="name" required [(ngModel)]="user.name" name="name" #nameCtrl="ngModel" />
<!-- コントロールが無効かつ(タッチ済みまたはダーティ)の場合のみエラーを表示 -->
@if (nameCtrl.invalid && (nameCtrl.dirty || nameCtrl.touched)) {
<div class="alert alert-danger">
@if (nameCtrl.errors?.['required']) {
<div>Name is required.</div>
}
</div>
}
```
## フォームの送信
1. `<form>` 要素に `(ngSubmit)` イベントを使用します。
2. `NgForm` テンプレート参照変数(例: `[disabled]="!userForm.form.valid"`)を使用して、送信ボタンの無効状態をフォーム全体の有効性にバインドします。
## フォームのリセット
フォームをプリスティン状態(値とバリデーションフラグをクリア)にプログラムでリセットするには、`NgForm` インスタンスの `reset()` メソッドを使用します。
```html
<button type="button" (click)="userForm.reset()">Reset</button>
```

View File

@@ -0,0 +1,65 @@
# テストの基礎
このガイドでは、Angularのユニットテストおよびコンポーネントテストを記述するための基本的な原則と実践を説明します。プロジェクトにすでに設定されているテストランナーを使用してください。
## 核心哲学: 非同期ファースト
最新のAngularアプリケーションは、特にシグナルやゾーンレス変更検知を使用する場合、状態変更を非同期にスケジュールすることが多いです。テストはこれを考慮する必要があります。
「Act実行、Wait待機、Assert検証」パターンを推奨します:
1. **Act実行:** 状態を更新するかアクションを実行します(例: コンポーネントの入力を設定する、ボタンをクリックする)。
2. **Wait待機:** `await fixture.whenStable()` を使用して、フレームワークがスケジュールされた更新を処理しレンダリングするのを待ちます。
3. **Assert検証:** 結果を確認します。
### 基本的なテスト構造の例
```ts
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {MyComponent} from './my.component';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
let h1: HTMLElement;
beforeEach(async () => {
// 1. テストモジュールを設定する
await TestBed.configureTestingModule({
imports: [MyComponent],
}).compileComponents();
// 2. コンポーネントフィクスチャーを作成する
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
h1 = fixture.nativeElement.querySelector('h1');
});
it('should display the default title', async () => {
// ACT: (暗黙的)コンポーネントはデフォルト状態で作成される。
// 初期データバインディングを待機する。
await fixture.whenStable();
// 初期状態を検証する。
expect(h1.textContent).toContain('Default Title');
});
it('should display a different title after a change', async () => {
// ACT: コンポーネントのタイトルプロパティを変更する。
component.title.set('New Test Title');
// 非同期更新が完了するのを待機する。
await fixture.whenStable();
// DOMが更新されたことを検証する。
expect(h1.textContent).toContain('New Test Title');
});
});
```
## TestBed と ComponentFixture
- **`TestBed`**: テスト専用のAngularモジュールを作成するための主要なユーティリティです。テストに必要なコンポーネントの宣言、サービスの提供、インポートのセットアップのために `beforeEach` 内で `TestBed.configureTestingModule({...})` を使用します。
- **`ComponentFixture`**: 作成されたコンポーネントのインスタンスとその環境を扱うためのハンドルです。
- `fixture.componentInstance`: コンポーネントのクラスインスタンスにアクセスします。
- `fixture.nativeElement`: コンポーネントのルートDOM要素にアクセスします。
- `fixture.debugElement`: `nativeElement` をAngular固有のラッパーでラップしたもので、DOMをより安全かつプラットフォームに依存しない方法でクエリできます例: `debugElement.query(By.css('p'))`)。