From 106643de149722a817c9b97b49717a9b73df547a Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Tue, 6 Jan 2026 21:38:28 +0900 Subject: [PATCH 01/32] chore: update origin to 71cde39ff04 --- origin | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/origin b/origin index 8243bb306..71cde39ff 160000 --- a/origin +++ b/origin @@ -1 +1 @@ -Subproject commit 8243bb3064931c659b8fb807f9802ca75ff886f6 +Subproject commit 71cde39ff04dec1fee2ae92e953b7b145a996736 From 82d758e4d67bbc3afdea2deb34f2d859f01cd000 Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 10:12:49 +0900 Subject: [PATCH 02/32] fix: migrate new and untranslated files --- .../profiling-with-chrome-devtools.md | 6 +- .../ecosystem/rxjs-interop/output-interop.md | 12 +- .../ecosystem/rxjs-interop/signals-interop.md | 24 +- .../rxjs-interop/take-until-destroyed.md | 4 +- .../ecosystem/service-workers/app-shell.md | 4 +- .../ecosystem/service-workers/config.md | 13 +- .../custom-service-worker-scripts.md | 4 +- .../service-workers/getting-started.md | 19 +- adev-ja/src/content/ecosystem/web-workers.md | 33 ++- adev-ja/src/content/guide/animations/css.md | 4 +- .../src/content/guide/animations/migration.md | 4 +- .../src/content/guide/animations/overview.md | 10 +- .../guide/di/creating-injectable-service.md | 43 +-- .../guide/di/defining-dependency-providers.md | 203 +++++++-------- adev-ja/src/content/guide/drag-drop.md | 12 +- .../signals/designing-your-form-model.md | 124 +++++---- .../content/guide/forms/signals/migration.md | 246 ++++++++++++++++++ .../src/content/guide/http/http-resource.md | 18 +- adev-ja/src/content/guide/i18n/add-package.md | 2 +- .../guide/i18n/import-global-variants.md | 2 - adev-ja/src/content/guide/i18n/locale-id.md | 2 +- adev-ja/src/content/guide/i18n/prepare.md | 33 +-- .../content/guide/i18n/translation-files.md | 10 +- .../src/content/guide/ngmodules/overview.md | 54 ++-- adev-ja/src/content/guide/signals/effect.md | 36 ++- adev-ja/src/content/reference/cli.md | 2 +- .../content/reference/configs/npm-packages.md | 2 +- .../reference/configs/workspace-config.md | 8 +- .../src/content/reference/errors/NG02802.md | 8 +- .../src/content/reference/errors/NG0401.md | 6 +- .../src/content/reference/errors/NG0919.md | 99 +++++++ .../reference/extended-diagnostics/NG8021.md | 21 +- .../reference/extended-diagnostics/NG8101.md | 3 - .../reference/extended-diagnostics/NG8102.md | 1 - .../reference/extended-diagnostics/NG8103.md | 1 - .../reference/extended-diagnostics/NG8104.md | 2 - .../reference/extended-diagnostics/NG8105.md | 1 - .../reference/extended-diagnostics/NG8106.md | 5 +- .../reference/extended-diagnostics/NG8107.md | 1 - .../reference/extended-diagnostics/NG8108.md | 20 +- .../reference/extended-diagnostics/NG8109.md | 1 - .../reference/extended-diagnostics/NG8111.md | 1 - .../reference/extended-diagnostics/NG8113.md | 4 +- .../reference/extended-diagnostics/NG8115.md | 1 - .../extended-diagnostics/overview.md | 1 - .../reference/migrations/inject-function.md | 34 +-- .../reference/migrations/ngclass-to-class.md | 4 +- .../reference/migrations/ngstyle-to-style.md | 4 +- .../content/reference/migrations/outputs.md | 4 +- .../migrations/route-lazy-loading.md | 2 +- .../router-testing-module-migration.md | 42 ++- .../reference/migrations/standalone.md | 28 +- adev-ja/src/content/tools/cli/aot-compiler.md | 26 +- .../content/tools/cli/aot-metadata-errors.md | 18 +- .../tools/cli/build-system-migration.md | 16 +- adev-ja/src/content/tools/cli/cli-builder.md | 31 +-- adev-ja/src/content/tools/cli/environments.md | 20 +- .../content/tools/cli/schematics-authoring.md | 57 ++-- .../content/tools/cli/template-typecheck.md | 18 +- .../tools/libraries/angular-package-format.md | 29 +-- .../tools/libraries/creating-libraries.md | 6 +- .../tools/libraries/using-libraries.md | 30 +-- 62 files changed, 830 insertions(+), 649 deletions(-) create mode 100644 adev-ja/src/content/guide/forms/signals/migration.md create mode 100644 adev-ja/src/content/reference/errors/NG0919.md diff --git a/adev-ja/src/content/best-practices/runtime-performance/profiling-with-chrome-devtools.md b/adev-ja/src/content/best-practices/runtime-performance/profiling-with-chrome-devtools.md index c78d6c28e..67a98b65e 100644 --- a/adev-ja/src/content/best-practices/runtime-performance/profiling-with-chrome-devtools.md +++ b/adev-ja/src/content/best-practices/runtime-performance/profiling-with-chrome-devtools.md @@ -34,9 +34,9 @@ Angular profiling works exclusively in development mode. Here is an example of how you can enable the integration in the application bootstrap to capture all possible events: ```ts -import { enableProfiling } from '@angular/core'; -import { bootstrapApplication } from '@angular/platform-browser'; -import { MyApp } from './my-app'; +import {enableProfiling} from '@angular/core'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {MyApp} from './my-app'; // Turn on profiling *before* bootstrapping your application // in order to capture all of the code run on start-up. diff --git a/adev-ja/src/content/ecosystem/rxjs-interop/output-interop.md b/adev-ja/src/content/ecosystem/rxjs-interop/output-interop.md index bd9faff69..5990164ab 100644 --- a/adev-ja/src/content/ecosystem/rxjs-interop/output-interop.md +++ b/adev-ja/src/content/ecosystem/rxjs-interop/output-interop.md @@ -8,16 +8,18 @@ The `@angular/rxjs-interop` package offers two APIs related to component and dir The `outputFromObservable` lets you create a component or directive output that emits based on an RxJS observable: -```ts {highlight:[9]} +```ts {highlight:[11]} import {Directive} from '@angular/core'; import {outputFromObservable} from '@angular/core/rxjs-interop'; -@Directive({/*...*/}) +@Directive({ + /*...*/ +}) class Draggable { - pointerMoves$: Observable = listenToPointerMoves(); + pointerMoves$: Observable = listenToPointerMoves(); - // Whenever `pointerMoves$` emits, the `pointerMove` event fires. - pointerMove = outputFromObservable(this.pointerMoves$); + // Whenever `pointerMoves$` emits, the `pointerMove` event fires. + pointerMove = outputFromObservable(this.pointerMoves$); } ``` diff --git a/adev-ja/src/content/ecosystem/rxjs-interop/signals-interop.md b/adev-ja/src/content/ecosystem/rxjs-interop/signals-interop.md index 2ab060e0c..ee763e37a 100644 --- a/adev-ja/src/content/ecosystem/rxjs-interop/signals-interop.md +++ b/adev-ja/src/content/ecosystem/rxjs-interop/signals-interop.md @@ -62,20 +62,20 @@ Some observables may emit values that are **equals** even though they differ by When two emitted values are considered equal, the resulting signal **does not update**. This prevents redundant computations, DOM updates, or effects from re-running unnecessarily. ```ts -import { Component } from '@angular/core'; -import { toSignal } from '@angular/core/rxjs-interop'; -import { interval, map } from 'rxjs'; +import {Component} from '@angular/core'; +import {toSignal} from '@angular/core/rxjs-interop'; +import {interval, map} from 'rxjs'; @Component(/* ... */) export class EqualExample { temperature$ = interval(1000).pipe( - map(() => ({ temperature: Math.floor(Math.random() * 3) + 20 }) ) // 20, 21, or 22 randomly + map(() => ({temperature: Math.floor(Math.random() * 3) + 20})), // 20, 21, or 22 randomly ); // Only update if the temperature changes temperature = toSignal(this.temperature$, { - initialValue: { temperature : 20 }, - equal: (prev, curr) => prev.temperature === curr.temperature + initialValue: {temperature: 20}, + equal: (prev, curr) => prev.temperature === curr.temperature, }); } ``` @@ -91,17 +91,15 @@ If an Observable used in `toSignal` completes, the signal continues to return th Use the `toObservable` utility to create an `Observable` which tracks the value of a signal. The signal's value is monitored with an `effect` which emits the value to the Observable when it changes. ```ts -import { Component, signal } from '@angular/core'; -import { toObservable } from '@angular/core/rxjs-interop'; +import {Component, signal} from '@angular/core'; +import {toObservable} from '@angular/core/rxjs-interop'; @Component(/* ... */) export class SearchResults { query: Signal = inject(QueryService).query; query$ = toObservable(this.query); - results$ = this.query$.pipe( - switchMap(query => this.http.get('/search?q=' + query )) - ); + results$ = this.query$.pipe(switchMap((query) => this.http.get('/search?q=' + query))); } ``` @@ -119,7 +117,7 @@ Unlike Observables, signals never provide a synchronous notification of changes. ```ts const obs$ = toObservable(mySignal); -obs$.subscribe(value => console.log(value)); +obs$.subscribe((value) => console.log(value)); mySignal.set(1); mySignal.set(2); @@ -146,7 +144,7 @@ export class UserProfile { protected userId = input(); private userResource = rxResource({ - params: () => ({ userId: this.userId() }), + params: () => ({userId: this.userId()}), // The `stream` property expects a factory function that returns // a data stream as an RxJS Observable. diff --git a/adev-ja/src/content/ecosystem/rxjs-interop/take-until-destroyed.md b/adev-ja/src/content/ecosystem/rxjs-interop/take-until-destroyed.md index 19e8a4294..fc94b13d5 100644 --- a/adev-ja/src/content/ecosystem/rxjs-interop/take-until-destroyed.md +++ b/adev-ja/src/content/ecosystem/rxjs-interop/take-until-destroyed.md @@ -18,7 +18,7 @@ export class UserProfile { // This subscription the 'notifications' Observable is automatically // unsubscribed when the 'UserProfile' component is destroyed. const messages: Observable = this.dispatcher.notifications; - messages.pipe(takeUntilDestroyed()).subscribe(message => { + messages.pipe(takeUntilDestroyed()).subscribe((message) => { this.popup.show(message); }); } @@ -38,7 +38,7 @@ export class UserProfile { // Always pass a `DestroyRef` if you call `takeUntilDestroyed` outside // of an injection context. const messages: Observable = this.dispatcher.notifications; - messages.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(message => { + messages.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((message) => { this.popup.show(message); }); } diff --git a/adev-ja/src/content/ecosystem/service-workers/app-shell.md b/adev-ja/src/content/ecosystem/service-workers/app-shell.md index 31e86a0a8..7a7d9afc4 100644 --- a/adev-ja/src/content/ecosystem/service-workers/app-shell.md +++ b/adev-ja/src/content/ecosystem/service-workers/app-shell.md @@ -26,7 +26,7 @@ For more information about this command, see [App shell command](cli/generate/ap The command updates the application code and adds extra files to the project structure. - +```text src ├── app │ ├── app.config.server.ts # server application configuration @@ -36,7 +36,7 @@ src │ ├── app-shell.component.spec.ts │ └── app-shell.component.ts └── main.server.ts # main server application bootstrapping - +``` diff --git a/adev-ja/src/content/ecosystem/service-workers/config.md b/adev-ja/src/content/ecosystem/service-workers/config.md index 8f1333947..9a6500f88 100644 --- a/adev-ja/src/content/ecosystem/service-workers/config.md +++ b/adev-ja/src/content/ecosystem/service-workers/config.md @@ -354,14 +354,12 @@ The URL query is ignored when matching. If the field is omitted, it defaults to: ```ts - [ -'/**', // Include all URLs. -'!/**/*.*', // Exclude URLs to files (containing a file extension in the last segment). -'!/**/*__*', // Exclude URLs containing `__` in the last segment. -'!/**/*__*/**', // Exclude URLs containing `__` in any other segment. -] - + '/**', // Include all URLs. + '!/**/*.*', // Exclude URLs to files (containing a file extension in the last segment). + '!/**/*__*', // Exclude URLs containing `__` in the last segment. + '!/**/*__*/**', // Exclude URLs containing `__` in any other segment. +]; ``` ### `navigationRequestStrategy` @@ -369,7 +367,6 @@ If the field is omitted, it defaults to: This optional property enables you to configure how the service worker handles navigation requests: ```json - { "navigationRequestStrategy": "freshness" } diff --git a/adev-ja/src/content/ecosystem/service-workers/custom-service-worker-scripts.md b/adev-ja/src/content/ecosystem/service-workers/custom-service-worker-scripts.md index d5fdcaa6d..2a5dbaeac 100644 --- a/adev-ja/src/content/ecosystem/service-workers/custom-service-worker-scripts.md +++ b/adev-ja/src/content/ecosystem/service-workers/custom-service-worker-scripts.md @@ -73,8 +73,8 @@ importScripts('./ngsw-worker.js'); 3. Configure the service worker registration to use your custom script: ```ts -import { ApplicationConfig, isDevMode } from '@angular/core'; -import { provideServiceWorker } from '@angular/service-worker'; +import {ApplicationConfig, isDevMode} from '@angular/core'; +import {provideServiceWorker} from '@angular/service-worker'; export const appConfig: ApplicationConfig = { providers: [ diff --git a/adev-ja/src/content/ecosystem/service-workers/getting-started.md b/adev-ja/src/content/ecosystem/service-workers/getting-started.md index 268a3a187..a99bb75ca 100644 --- a/adev-ja/src/content/ecosystem/service-workers/getting-started.md +++ b/adev-ja/src/content/ecosystem/service-workers/getting-started.md @@ -166,9 +166,8 @@ Angular service workers support comprehensive configuration options through the The `enabled` option controls whether the service worker will be registered and related services will attempt to communicate with it. ```ts - -import { ApplicationConfig, isDevMode } from '@angular/core'; -import { provideServiceWorker } from '@angular/service-worker'; +import {ApplicationConfig, isDevMode} from '@angular/core'; +import {provideServiceWorker} from '@angular/service-worker'; export const appConfig: ApplicationConfig = { providers: [ @@ -177,7 +176,6 @@ export const appConfig: ApplicationConfig = { }), ], }; - ``` ### Cache control with updateViaCache @@ -185,7 +183,6 @@ export const appConfig: ApplicationConfig = { The `updateViaCache` option controls how the browser consults the HTTP cache during service worker updates. This provides fine-grained control over when the browser fetches updated service worker scripts and imported modules. ```ts - export const appConfig: ApplicationConfig = { providers: [ provideServiceWorker('ngsw-worker.js', { @@ -194,7 +191,6 @@ export const appConfig: ApplicationConfig = { }), ], }; - ``` The `updateViaCache` option accepts the following values: @@ -208,7 +204,6 @@ The `updateViaCache` option accepts the following values: The `type` option enables specifying the script type when registering service workers, providing support for ES module features in your service worker scripts. ```ts - export const appConfig: ApplicationConfig = { providers: [ provideServiceWorker('ngsw-worker.js', { @@ -217,7 +212,6 @@ export const appConfig: ApplicationConfig = { }), ], }; - ``` The `type` option accepts the following values: @@ -230,7 +224,6 @@ The `type` option accepts the following values: The `scope` option defines the service worker's registration scope, determining what range of URLs it can control. ```ts - export const appConfig: ApplicationConfig = { providers: [ provideServiceWorker('ngsw-worker.js', { @@ -239,7 +232,6 @@ export const appConfig: ApplicationConfig = { }), ], }; - ``` - Controls which URLs the service worker can intercept and manage @@ -251,7 +243,6 @@ export const appConfig: ApplicationConfig = { The `registrationStrategy` option defines when the service worker will be registered with the browser, providing control over the timing of registration. ```ts - export const appConfig: ApplicationConfig = { providers: [ provideServiceWorker('ngsw-worker.js', { @@ -260,7 +251,6 @@ export const appConfig: ApplicationConfig = { }), ], }; - ``` Available registration strategies: @@ -270,7 +260,6 @@ Available registration strategies: - **`'registerWithDelay:timeout'`** - Register with a delay of the specified timeout in milliseconds ```ts - // Register immediately export const immediateConfig: ApplicationConfig = { providers: [ @@ -290,13 +279,12 @@ export const delayedConfig: ApplicationConfig = { }), ], }; - ``` You can also provide an Observable factory function for custom registration timing: ```ts -import { timer } from 'rxjs'; +import {timer} from 'rxjs'; export const customConfig: ApplicationConfig = { providers: [ @@ -306,7 +294,6 @@ export const customConfig: ApplicationConfig = { }), ], }; - ``` ## More on Angular service workers diff --git a/adev-ja/src/content/ecosystem/web-workers.md b/adev-ja/src/content/ecosystem/web-workers.md index 9ebd9fcdc..74ba77363 100644 --- a/adev-ja/src/content/ecosystem/web-workers.md +++ b/adev-ja/src/content/ecosystem/web-workers.md @@ -26,29 +26,26 @@ The command performs the following actions. 1. Adds the following scaffold code to `src/app/app.worker.ts` to receive messages. ```ts {header:"src/app/app.worker.ts"} - - addEventListener('message', ({ data }) => { - const response = `worker response to ${data}`; - postMessage(response); - }); - + addEventListener('message', ({data}) => { + const response = `worker response to ${data}`; + postMessage(response); + }); ``` 1. Adds the following scaffold code to `src/app/app.component.ts` to use the worker. ```ts {header:"src/app/app.component.ts"} - - if (typeof Worker !== 'undefined') { - // Create a new - const worker = new Worker(new URL('./app.worker', import.meta.url)); - worker.onmessage = ({ data }) => { - console.log(`page got message: ${data}`); - }; - worker.postMessage('hello'); - } else { - // Web workers are not supported in this environment. - // You should add a fallback so that your program still executes correctly. - } + if (typeof Worker !== 'undefined') { + // Create a new + const worker = new Worker(new URL('./app.worker', import.meta.url)); + worker.onmessage = ({data}) => { + console.log(`page got message: ${data}`); + }; + worker.postMessage('hello'); + } else { + // Web workers are not supported in this environment. + // You should add a fallback so that your program still executes correctly. + } ``` After you create this initial scaffold, you must refactor your code to use the web worker by sending messages to and from the worker. diff --git a/adev-ja/src/content/guide/animations/css.md b/adev-ja/src/content/guide/animations/css.md index 5776ba519..a88a32ef7 100644 --- a/adev-ja/src/content/guide/animations/css.md +++ b/adev-ja/src/content/guide/animations/css.md @@ -159,7 +159,9 @@ You can apply multiple animations to an element at once using the `animation` sh ```css .target-element { - animation: rotate 3s, fade-in 2s; + animation: + rotate 3s, + fade-in 2s; } ``` diff --git a/adev-ja/src/content/guide/animations/migration.md b/adev-ja/src/content/guide/animations/migration.md index aa91dfa00..7872a9b50 100644 --- a/adev-ja/src/content/guide/animations/migration.md +++ b/adev-ja/src/content/guide/animations/migration.md @@ -245,7 +245,9 @@ The animations package has a `group()` function to play multiple animations at t ```css .target-element { - animation: rotate 3s, fade-in 2s; + animation: + rotate 3s, + fade-in 2s; } ``` diff --git a/adev-ja/src/content/guide/animations/overview.md b/adev-ja/src/content/guide/animations/overview.md index fcfee2966..04981eaa6 100644 --- a/adev-ja/src/content/guide/animations/overview.md +++ b/adev-ja/src/content/guide/animations/overview.md @@ -34,9 +34,7 @@ Import `provideAnimationsAsync` from `@angular/platform-browser/animations/async ```ts {header: "Enabling Animations", linenums} bootstrapApplication(AppComponent, { - providers: [ - provideAnimationsAsync(), - ] + providers: [provideAnimationsAsync()], }); ``` @@ -127,13 +125,13 @@ The `animate()` function \(second argument of the transition function\) accepts The `timings` parameter takes either a number or a string defined in three parts. ```ts -animate (duration) +animate(duration); ``` or ```ts -animate ('duration delay easing') +animate('duration delay easing'); ``` The first part, `duration`, is required. @@ -191,7 +189,7 @@ HELPFUL: Some additional notes on using styles within [`state`](api/animations/s - Include multiple state pairs within the same `transition()` argument: ```ts - transition( 'on => off, off => void' ) + transition('on => off, off => void'); ``` ### Triggering the animation diff --git a/adev-ja/src/content/guide/di/creating-injectable-service.md b/adev-ja/src/content/guide/di/creating-injectable-service.md index 0d1784ea2..1dad19288 100644 --- a/adev-ja/src/content/guide/di/creating-injectable-service.md +++ b/adev-ja/src/content/guide/di/creating-injectable-service.md @@ -23,9 +23,15 @@ Here's an example of a service class that logs to the browser console: ```ts {header: "logger.service.ts (class)"} export class Logger { - log(msg: unknown) { console.log(msg); } - error(msg: unknown) { console.error(msg); } - warn(msg: unknown) { console.warn(msg); } + log(msg: unknown) { + console.log(msg); + } + error(msg: unknown) { + console.error(msg); + } + warn(msg: unknown) { + console.warn(msg); + } } ``` @@ -33,25 +39,24 @@ Services can depend on other services. For example, here's a `HeroService` that depends on the `Logger` service, and also uses `BackendService` to get heroes. That service in turn might depend on the `HttpClient` service to fetch heroes asynchronously from a server: - -import { inject } from "@angular/core"; +```ts {header: "hero.service.ts", highlight="[7,8,12,13]"} +import {inject} from '@angular/core'; export class HeroService { -private heroes: Hero[] = []; + private heroes: Hero[] = []; -private backend = inject(BackendService); -private logger = inject(Logger); + private backend = inject(BackendService); + private logger = inject(Logger); -async getHeroes() { -// Fetch -this.heroes = await this.backend.getAll(Hero); -// Log -this.logger.log(`Fetched ${this.heroes.length} heroes.`); -return this.heroes; -} + async getHeroes() { + // Fetch + this.heroes = await this.backend.getAll(Hero); + // Log + this.logger.log(`Fetched ${this.heroes.length} heroes.`); + return this.heroes; + } } - +``` ## Creating an injectable service with the CLI @@ -61,9 +66,9 @@ To generate a new `HeroService` class in the `src/app/heroes` folder, follow the 1. Run this [Angular CLI](/tools/cli) command: - +```sh ng generate service heroes/hero - +``` This command creates the following default `HeroService`: diff --git a/adev-ja/src/content/guide/di/defining-dependency-providers.md b/adev-ja/src/content/guide/di/defining-dependency-providers.md index 3702715d5..f685b05ef 100644 --- a/adev-ja/src/content/guide/di/defining-dependency-providers.md +++ b/adev-ja/src/content/guide/di/defining-dependency-providers.md @@ -16,7 +16,7 @@ While the `@Injectable` decorator with `providedIn: 'root'` works great for serv An `InjectionToken` is an object that Angular's dependency injection system uses to uniquely identify values for injection. Think of it as a special key that lets you store and retrieve any type of value in Angular's DI system: ```ts -import { InjectionToken } from '@angular/core'; +import {InjectionToken} from '@angular/core'; // Create a token for a string value export const API_URL = new InjectionToken('api.url'); @@ -40,7 +40,7 @@ An `InjectionToken` that has a `factory` results in `providedIn: 'root'` by defa ```ts // 📁 /app/config.token.ts -import { InjectionToken } from '@angular/core'; +import {InjectionToken} from '@angular/core'; export interface AppConfig { apiUrl: string; @@ -56,15 +56,15 @@ export const APP_CONFIG = new InjectionToken('app.config', { version: '1.0.0', features: { darkMode: true, - analytics: false - } - }) + analytics: false, + }, + }), }); // No need to add to providers array - available everywhere! @Component({ selector: 'app-header', - template: `

Version: {{ config.version }}

` + template: `

Version: {{ config.version }}

`, }) export class HeaderComponent { config = inject(APP_CONFIG); // Automatically available @@ -77,8 +77,8 @@ InjectionToken with factory functions is ideal when you can't use a class but ne ```ts // 📁 /app/logger.token.ts -import { InjectionToken, inject } from '@angular/core'; -import { APP_CONFIG } from './config.token'; +import {InjectionToken, inject} from '@angular/core'; +import {APP_CONFIG} from './config.token'; // Logger function type export type LoggerFn = (level: string, message: string) => void; @@ -94,19 +94,19 @@ export const LOGGER_FN = new InjectionToken('logger.function', { console[level](`[${new Date().toISOString()}] ${message}`); } }; - } + }, }); // 📁 /app/storage.token.ts // Providing browser APIs as tokens export const LOCAL_STORAGE = new InjectionToken('localStorage', { // providedIn: 'root' is configured as the default - factory: () => window.localStorage + factory: () => window.localStorage, }); export const SESSION_STORAGE = new InjectionToken('sessionStorage', { providedIn: 'root', - factory: () => window.sessionStorage + factory: () => window.sessionStorage, }); // 📁 /app/feature-flags.token.ts @@ -125,7 +125,7 @@ export const FEATURE_FLAGS = new InjectionToken>('feature.f flags.set('newDashboard', false); return flags; - } + }, }); ``` @@ -148,7 +148,7 @@ When you need more control than `providedIn: 'root'` offers, you can manually co ### Example: Service without `providedIn` ```ts -import { Injectable, Component, inject } from '@angular/core'; +import {Injectable, Component, inject} from '@angular/core'; // Service without providedIn @Injectable() @@ -165,7 +165,7 @@ export class LocalDataStore { selector: 'app-example', // A provider is required here because the `LocalDataStore` service has no providedIn. providers: [LocalDataStore], - template: `...` + template: `...`, }) export class ExampleComponent { dataStore = inject(LocalDataStore); @@ -177,9 +177,9 @@ export class ExampleComponent { Services with `providedIn: 'root'` can be overridden at the component level. This ties the instance of the service to the life of a component. As a result, when the component gets destroyed, the provided service is also destroyed as well. ```ts -import { Injectable, Component, inject } from '@angular/core'; +import {Injectable, Component, inject} from '@angular/core'; -@Injectable({ providedIn: 'root' }) +@Injectable({providedIn: 'root'}) export class DataStore { private data: ListItem[] = []; } @@ -189,7 +189,7 @@ export class DataStore { selector: 'app-isolated', // Creates new instance of `DataStore` rather than using the root-provided instance. providers: [DataStore], - template: `...` + template: `...`, }) export class IsolatedComponent { dataStore = inject(DataStore); // Component-specific instance @@ -302,8 +302,8 @@ Angular provides a built-in [`InjectionToken`](api/core/InjectionToken) class th ```ts // 📁 /app/tokens.ts -import { InjectionToken } from '@angular/core'; -import { DataService } from './data-service.interface'; +import {InjectionToken} from '@angular/core'; +import {DataService} from './data-service.interface'; export const DATA_SERVICE_TOKEN = new InjectionToken('DataService'); ``` @@ -341,8 +341,8 @@ interface DataService { // Interfaces disappear after TypeScript compilation @Component({ providers: [ - { provide: DataService, useClass: LocalDataService } // Error! - ] + {provide: DataService, useClass: LocalDataService}, // Error! + ], }) export class ExampleComponent { private dataService = inject(DataService); // Error! @@ -352,9 +352,7 @@ export class ExampleComponent { export const DATA_SERVICE_TOKEN = new InjectionToken('DataService'); @Component({ - providers: [ - { provide: DATA_SERVICE_TOKEN, useClass: LocalDataService } - ] + providers: [{provide: DATA_SERVICE_TOKEN, useClass: LocalDataService}], }) export class ExampleComponent { private dataService = inject(DATA_SERVICE_TOKEN); // Works! @@ -371,25 +369,21 @@ The InjectionToken provides a runtime value that Angular's DI system can use, wh ```ts // Shorthand -providers: [DataService] +providers: [DataService]; // Full syntax -providers: [ - { provide: DataService, useClass: DataService } -] +providers: [{provide: DataService, useClass: DataService}]; // Different implementation -providers: [ - { provide: DataService, useClass: MockDataService } -] +providers: [{provide: DataService, useClass: MockDataService}]; // Conditional implementation providers: [ { provide: StorageService, - useClass: environment.production ? CloudStorageService : LocalStorageService - } -] + useClass: environment.production ? CloudStorageService : LocalStorageService, + }, +]; ``` #### Practical example: Logger substitution @@ -397,7 +391,7 @@ providers: [ You can substitute implementations to extend functionality: ```ts -import { Injectable, Component, inject } from '@angular/core'; +import {Injectable, Component, inject} from '@angular/core'; // Base logger @Injectable() @@ -431,8 +425,8 @@ export class EvenBetterLogger extends Logger { selector: 'app-example', providers: [ UserService, // EvenBetterLogger needs this - { provide: Logger, useClass: EvenBetterLogger } - ] + {provide: Logger, useClass: EvenBetterLogger}, + ], }) export class ExampleComponent { private logger = inject(Logger); // Gets EvenBetterLogger instance @@ -445,10 +439,10 @@ export class ExampleComponent { ```ts providers: [ - { provide: API_URL_TOKEN, useValue: 'https://api.example.com' }, - { provide: MAX_RETRIES_TOKEN, useValue: 3 }, - { provide: FEATURE_FLAGS_TOKEN, useValue: { darkMode: true, beta: false } } -] + {provide: API_URL_TOKEN, useValue: 'https://api.example.com'}, + {provide: MAX_RETRIES_TOKEN, useValue: 3}, + {provide: FEATURE_FLAGS_TOKEN, useValue: {darkMode: true, beta: false}}, +]; ``` IMPORTANT: TypeScript types and interfaces cannot serve as dependency values. They exist only at compile-time. @@ -477,21 +471,19 @@ const appConfig: AppConfig = { appTitle: 'My Application', features: { darkMode: true, - analytics: false - } + analytics: false, + }, }; // Provide in bootstrap bootstrapApplication(AppComponent, { - providers: [ - { provide: APP_CONFIG, useValue: appConfig } - ] + providers: [{provide: APP_CONFIG, useValue: appConfig}], }); // Use in component @Component({ selector: 'app-header', - template: `

{{ title }}

` + template: `

{{ title }}

`, }) export class HeaderComponent { private config = inject(APP_CONFIG); @@ -512,15 +504,15 @@ providers: [ { provide: LoggerService, useFactory: loggerFactory, - deps: [APP_CONFIG] // Dependencies for the factory function - } -] + deps: [APP_CONFIG], // Dependencies for the factory function + }, +]; ``` You can mark factory dependencies as optional: ```ts -import { Optional } from '@angular/core'; +import {Optional} from '@angular/core'; providers: [ { @@ -528,9 +520,9 @@ providers: [ useFactory: (required: RequiredService, optional?: OptionalService) => { return new MyService(required, optional || new DefaultService()); }, - deps: [RequiredService, [new Optional(), OptionalService]] - } -] + deps: [RequiredService, [new Optional(), OptionalService]], + }, +]; ``` #### Practical example: Configuration-based API client @@ -543,7 +535,7 @@ class ApiClient { constructor( private http: HttpClient, private baseUrl: string, - private rateLimitMs: number + private rateLimitMs: number, ) {} async fetchData(endpoint: string) { @@ -554,13 +546,13 @@ class ApiClient { private async applyRateLimit() { // Simplified example - real implementation would track request timing - return new Promise(resolve => setTimeout(resolve, this.rateLimitMs)); + return new Promise((resolve) => setTimeout(resolve, this.rateLimitMs)); } } // Factory function that configures based on user tier -import { inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import {inject} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; const apiClientFactory = () => { const http = inject(HttpClient); const userService = inject(UserService); @@ -575,13 +567,13 @@ const apiClientFactory = () => { // Provider configuration export const apiClientProvider = { provide: ApiClient, - useFactory: apiClientFactory + useFactory: apiClientFactory, }; // Usage in component @Component({ selector: 'app-dashboard', - providers: [apiClientProvider] + providers: [apiClientProvider], }) export class DashboardComponent { private apiClient = inject(ApiClient); @@ -594,9 +586,9 @@ export class DashboardComponent { ```ts providers: [ - NewLogger, // The actual service - { provide: OldLogger, useExisting: NewLogger } // The alias -] + NewLogger, // The actual service + {provide: OldLogger, useExisting: NewLogger}, // The alias +]; ``` IMPORTANT: Don't confuse `useExisting` with `useClass`. `useClass` creates separate instances, while `useExisting` ensures you get the same singleton instance. @@ -609,10 +601,10 @@ Use the `multi: true` flag when multiple providers contribute values to the same export const INTERCEPTOR_TOKEN = new InjectionToken('interceptors'); providers: [ - { provide: INTERCEPTOR_TOKEN, useClass: AuthInterceptor, multi: true }, - { provide: INTERCEPTOR_TOKEN, useClass: LoggingInterceptor, multi: true }, - { provide: INTERCEPTOR_TOKEN, useClass: RetryInterceptor, multi: true } -] + {provide: INTERCEPTOR_TOKEN, useClass: AuthInterceptor, multi: true}, + {provide: INTERCEPTOR_TOKEN, useClass: LoggingInterceptor, multi: true}, + {provide: INTERCEPTOR_TOKEN, useClass: RetryInterceptor, multi: true}, +]; ``` When you inject `INTERCEPTOR_TOKEN`, you'll receive an array containing instances of all three interceptors. @@ -638,11 +630,11 @@ Use application-level providers in `bootstrapApplication` when: // main.ts bootstrapApplication(AppComponent, { providers: [ - { provide: API_BASE_URL, useValue: 'https://api.example.com' }, - { provide: INTERCEPTOR_TOKEN, useClass: AuthInterceptor, multi: true }, - LoggingService, // Used throughout the app - { provide: ErrorHandler, useClass: GlobalErrorHandler } - ] + {provide: API_BASE_URL, useValue: 'https://api.example.com'}, + {provide: INTERCEPTOR_TOKEN, useClass: AuthInterceptor, multi: true}, + LoggingService, // Used throughout the app + {provide: ErrorHandler, useClass: GlobalErrorHandler}, + ], }); ``` @@ -725,19 +717,19 @@ export const routes: Routes = [ { path: 'admin', providers: [ - AdminService, // Only loaded with admin routes - { provide: FEATURE_FLAGS, useValue: { adminMode: true } } + AdminService, // Only loaded with admin routes + {provide: FEATURE_FLAGS, useValue: {adminMode: true}}, ], - loadChildren: () => import('./admin/admin.routes') + loadChildren: () => import('./admin/admin.routes'), }, { path: 'shop', providers: [ - ShoppingCartService, // Isolated shopping state - PaymentService + ShoppingCartService, // Isolated shopping state + PaymentService, ], - loadChildren: () => import('./shop/shop.routes') - } + loadChildren: () => import('./shop/shop.routes'), + }, ]; ``` @@ -751,7 +743,7 @@ Instead of requiring users to manually configure complex providers, library auth ```ts // 📁 /libs/analytics/src/providers.ts -import { InjectionToken, Provider, inject } from '@angular/core'; +import {InjectionToken, Provider, inject} from '@angular/core'; // Configuration interface export interface AnalyticsConfig { @@ -774,10 +766,7 @@ export class AnalyticsService { // Provider function for consumers export function provideAnalytics(config: AnalyticsConfig): Provider[] { - return [ - { provide: ANALYTICS_CONFIG, useValue: config }, - AnalyticsService - ]; + return [{provide: ANALYTICS_CONFIG, useValue: config}, AnalyticsService]; } // Usage in consumer app @@ -786,9 +775,9 @@ bootstrapApplication(AppComponent, { providers: [ provideAnalytics({ trackingId: 'GA-12345', - enableDebugMode: !environment.production - }) - ] + enableDebugMode: !environment.production, + }), + ], }); ``` @@ -798,13 +787,13 @@ For more complex scenarios, you can combine multiple configuration approaches: ```ts // 📁 /libs/http-client/src/provider.ts -import { Provider, InjectionToken, inject } from '@angular/core'; +import {Provider, InjectionToken, inject} from '@angular/core'; // Feature flags for optional functionality export enum HttpFeatures { Interceptors = 'interceptors', Caching = 'caching', - Retry = 'retry' + Retry = 'retry', } // Configuration interfaces @@ -826,7 +815,7 @@ const HTTP_FEATURES = new InjectionToken>('http.features'); // Core service class HttpClientService { - private config = inject(HTTP_CONFIG, { optional: true }); + private config = inject(HTTP_CONFIG, {optional: true}); private features = inject(HTTP_FEATURES); get(url: string) { @@ -845,18 +834,15 @@ class CacheInterceptor { } // Main provider function -export function provideHttpClient( - config?: HttpConfig, - ...features: HttpFeature[] -): Provider[] { +export function provideHttpClient(config?: HttpConfig, ...features: HttpFeature[]): Provider[] { const providers: Provider[] = [ - { provide: HTTP_CONFIG, useValue: config || {} }, - { provide: HTTP_FEATURES, useValue: new Set(features.map(f => f.kind)) }, - HttpClientService + {provide: HTTP_CONFIG, useValue: config || {}}, + {provide: HTTP_FEATURES, useValue: new Set(features.map((f) => f.kind))}, + HttpClientService, ]; // Add feature-specific providers - features.forEach(feature => { + features.forEach((feature) => { providers.push(...feature.providers); }); @@ -872,28 +858,25 @@ export interface HttpFeature { export function withInterceptors(...interceptors: any[]): HttpFeature { return { kind: HttpFeatures.Interceptors, - providers: interceptors.map(interceptor => ({ + providers: interceptors.map((interceptor) => ({ provide: INTERCEPTOR_TOKEN, useClass: interceptor, - multi: true - })) + multi: true, + })), }; } export function withCaching(): HttpFeature { return { kind: HttpFeatures.Caching, - providers: [CacheInterceptor] + providers: [CacheInterceptor], }; } export function withRetry(config: RetryConfig): HttpFeature { return { kind: HttpFeatures.Retry, - providers: [ - { provide: RETRY_CONFIG, useValue: config }, - RetryInterceptor - ] + providers: [{provide: RETRY_CONFIG, useValue: config}, RetryInterceptor], }; } @@ -901,12 +884,12 @@ export function withRetry(config: RetryConfig): HttpFeature { bootstrapApplication(AppComponent, { providers: [ provideHttpClient( - { baseUrl: 'https://api.example.com' }, + {baseUrl: 'https://api.example.com'}, withInterceptors(AuthInterceptor, LoggingInterceptor), withCaching(), - withRetry({ maxAttempts: 3, delayMs: 1000 }) - ) - ] + withRetry({maxAttempts: 3, delayMs: 1000}), + ), + ], }); ``` diff --git a/adev-ja/src/content/guide/drag-drop.md b/adev-ja/src/content/guide/drag-drop.md index 9962c92e9..c33451438 100644 --- a/adev-ja/src/content/guide/drag-drop.md +++ b/adev-ja/src/content/guide/drag-drop.md @@ -96,7 +96,7 @@ Use the `cdkDropListGroup` directive if you have an unknown number of connected
@for (list of lists; track list) { -
+
}
``` @@ -125,11 +125,11 @@ You can associate some arbitrary data with both `cdkDrag` and `cdkDropList` by s ```html @for (list of lists; track list) { -
- @for (item of list; track item) { -
- } -
+
+ @for (item of list; track item) { +
+ } +
} ``` diff --git a/adev-ja/src/content/guide/forms/signals/designing-your-form-model.md b/adev-ja/src/content/guide/forms/signals/designing-your-form-model.md index f78d33d78..007f092d3 100644 --- a/adev-ja/src/content/guide/forms/signals/designing-your-form-model.md +++ b/adev-ja/src/content/guide/forms/signals/designing-your-form-model.md @@ -10,14 +10,14 @@ The form model represents the raw user input as it appears in the UI. For instan ```ts interface AppointmentFormModel { - name: string; // Appointment owner's name - date: Date; // Appointment date (carries only date information, time component is unused) - time: string; // Selected time as a string + name: string; // Appointment owner's name + date: Date; // Appointment date (carries only date information, time component is unused) + time: string; // Selected time as a string } interface AppointmentDomainModel { - name: string; // Appointment owner's name - time: Date; // Appointment time (carries both date and time information) + name: string; // Appointment owner's name + time: Date; // Appointment time (carries both date and time information) } ``` @@ -38,15 +38,15 @@ const taskModel = signal({ title: '', description: '', priority: 'medium', - completed: false -}) + completed: false, +}); ``` ```ts {avoid, header: 'Partial initialization'} const taskModel = signal({ - title: '' + title: '', // Missing description, priority, completed -}) +}); ``` Missing initial values mean those fields won't exist in the field tree, making them inaccessible for form interactions. @@ -58,8 +58,8 @@ Each model should represent a single form or a cohesive set of related data: ```ts {prefer, header: 'Focused on a single purpose'} const loginModel = signal({ email: '', - password: '' -}) + password: '', +}); ``` ```ts {avoid, header: 'Mixing unrelated concerns'} @@ -71,8 +71,8 @@ const appModel = signal({ theme: 'light', language: 'en', // Shopping cart - cartItems: [] -}) + cartItems: [], +}); ``` Separate models for different concerns makes forms easier to understand and reuse. Create multiple forms if you're managing distinct sets of data. @@ -84,9 +84,9 @@ Design models with validation in mind. Group fields that validate together: ```ts {prefer, header: 'Related fields grouped for comparison'} // Password fields grouped for comparison interface PasswordChangeData { - currentPassword: string - newPassword: string - confirmPassword: string + currentPassword: string; + newPassword: string; + confirmPassword: string; } ``` @@ -102,8 +102,8 @@ Although the size options look numeric, ` (option values: "6", "12", "24") - quantity: number; // Bound to: + size: string; // Bound to: } ``` @@ -115,8 +115,8 @@ To represent a property with an empty value in your form model, use a value that ```ts {prefer, header: 'Appropriate empty values'} interface UserFormModel { - name: string; // Bound to - birthday: Date | null; // Bound to + name: string; // Bound to + birthday: Date | null; // Bound to } // Initialize our form with empty values. @@ -144,29 +144,29 @@ interface CreateAccountFormModel { When creating the form we encounter a dilemma, what should the initial value in the model be? It may be tempting to create a `form()` since we don't have any input from the user yet. ```ts {avoid, header: 'Using null as empty value for complex object'} -createAccountForm = form( - signal(/* what goes here, null? */) -); +createAccountForm = form(signal(/* what goes here, null? */)); ``` However, it is important to remember that Signal Forms is _model driven_. If our model is `null` and `null` doesn't have a `name` or `username` property, that means our form won't have those subfields either. Instead what we really want is an instance of `CreateAccountFormModel` with all of its leaf fields set to an empty value. ```ts {prefer, header: 'Same shape value with empty values for properties'} -createAccountForm = form(signal({ - name: { - first: '', - last: '' - }, - username: '' -})); +createAccountForm = form( + signal({ + name: { + first: '', + last: '', + }, + username: '', + }), +); ``` Using this representation, all of the subfields we need now exist, and we can bind them using the `[field]` directive in our template. ```html -First: -Last: -Username: +First: Last: + Username: + ``` #### Fields that are conditionally hidden or unavailable @@ -174,24 +174,18 @@ Username: Forms aren't always linear. You often need to create conditional paths based on previous user input. One example of this is a form where we give the user different payment options. Let's start by imagining what the UI for such a form might look like. ```html -Name: +Name:

Payment Info

- Credit Card - @if (/* credit card selected */) { -
- Card Number - Security Code - Expiration -
+ Credit Card @if (/* credit card selected */) { +
+ Card Number Security Code Expiration + +
} - Bank Account - @if (/* bank account selected */) { -
- Account Number - Routing Number -
+ Bank Account @if (/* bank account selected */) { +
Account Number Routing Number
}
``` @@ -202,16 +196,16 @@ The best way to handle this is to use a form model with a static structure that interface BillPayFormModel { name: string; method: { - type: string, + type: string; card: { - cardNumber: string, - securityCode: string, - expiration: string - }, + cardNumber: string; + securityCode: string; + expiration: string; + }; bank: { - accountNumber: string, - routingNumber: string - } + accountNumber: string; + routingNumber: string; + }; }; } @@ -232,15 +226,15 @@ interface BillPayFormModel { name: string; method: | { - type: 'card', - cardNumber: string, - securityCode: string, - expiration: string + type: 'card'; + cardNumber: string; + securityCode: string; + expiration: string; } | { - type: 'bank', - accountNumber: string, - routingNumber: string + type: 'bank'; + accountNumber: string; + routingNumber: string; }; } ``` @@ -266,7 +260,7 @@ Arrays are the most common exception. Forms often need to collect a variable num ```ts interface SendEmailFormModel { subject: string; - recipientEmails: string[] + recipientEmails: string[]; } ``` @@ -295,8 +289,8 @@ interface UserProfileFormModel { In the template, we bind the `location` field directly to our custom control: ```html -Username: -Location: +Username: Location: + ``` Here, `` consumes and produces the entire `Location` object (or `null`), and doesn't access `userForm.location.lat` or `userForm.location.lng`. Therefore, `location` can safely have a dynamic shape without violating the principles of model-driven forms. diff --git a/adev-ja/src/content/guide/forms/signals/migration.md b/adev-ja/src/content/guide/forms/signals/migration.md new file mode 100644 index 000000000..ab2d4d82c --- /dev/null +++ b/adev-ja/src/content/guide/forms/signals/migration.md @@ -0,0 +1,246 @@ +# Migrating existing forms to Signal Forms + +This guide provides strategies for migrating existing codebases to Signal Forms, focusing on interoperability with +legacy Reactive Forms. + +## Top-down migration using `compatForm` + +Sometimes you may want to use existing reactive `FormControl` instances within a Signal Form. This is useful for +controls that involve: + +- Complex asynchronous logic. +- Intricate RxJS operators that are not yet ported. +- Integration with legacy third-party libraries. + +### Integrating a `FormControl` into a signal form + +Consider an existing `passwordControl` that uses a specialized `enterprisePasswordValidator`. Instead of rewriting the +validator, you can bridge the control into your signal state. + +We can do it using `compatForm`: + +```typescript +import {signal} from '@angular/core'; +import {FormControl, Validators} from '@angular/forms'; +import {compatForm} from '@angular/forms/signals/compat'; + +// 1. Existing legacy control with a specialized validator +const passwordControl = new FormControl('', { + validators: [Validators.required, enterprisePasswordValidator()], + nonNullable: true, +}); + +// 2. Wrap it inside your form state signal +const user = signal({ + email: '', + password: passwordControl, // Nest the legacy control directly +}); + +// 3. Create the form +const f = compatForm(user); + +// Access values via the signal tree +console.log(f.email().value()); // Current email value +console.log(f.password().value()); // Current value of passwordControl + +// Reactive state is proxied automatically +const isPasswordValid = f.password().valid(); +const passwordErrors = f.password().errors(); // Returns CompatValidationError if the legacy validator fails +``` + +In the template, use standard reactive syntax by binding the underlying control: + +```angular-html +
+
+ +
+ +
+ + + @if (f.password().touched() && f.password().invalid()) { +
+ @for (error of f.password().errors(); track error) { +

{{ error.message || error.kind }}

+ } +
+ } +
+
+``` + + + + + + +### Integrating a `FormGroup` into a signal form + +You can also wrap an entire `FormGroup`. This is common when a reusable sub-section of a form—such as an **Address Block**—is still managed by legacy Reactive Forms. + +```typescript +import {signal} from '@angular/core'; +import {FormGroup, FormControl, Validators} from '@angular/forms'; +import {compatForm} from '@angular/forms/signals/compat'; + +// 1. A legacy address group with its own validation logic +const addressGroup = new FormGroup({ + street: new FormControl('123 Angular Way', Validators.required), + city: new FormControl('Mountain View', Validators.required), + zip: new FormControl('94043', Validators.required), +}); + +// 2. Include it in the state like it's a value +const checkoutModel = signal({ + customerName: 'Pirojok the Cat', + shippingAddress: addressGroup, +}); + +const f = compatForm(checkoutModel, (p) => { + required(p.customerName); +}); +``` + +The `shippingAddress` field acts as a branch in your Signal Form tree. You can bind these nested controls in your +template by accessing the underlying legacy controls via `.control()`: + +```angular-html +
+

Shipping Details

+ +
+ + + @if (f.customerName().touched() && f.customerName().invalid()) { +
+

Customer name is required.

+
+ } +
+ +
+ Address + + @let street = f.shippingAddress().control().controls.street; +
+ + @if (street.touched && street.invalid) { +
+

Street is required

+
+ } +
+ + @let city = f.shippingAddress().control().controls.city; +
+ + @if (city.touched && city.invalid) { +
+

City is required

+
+ } +
+ + @let zip = f.shippingAddress().control().controls.zip; +
+ + @if (zip.touched && zip.invalid) { +
+

Zip Code is required

+
+ } +
+
+
+``` + + + + + + +### Accessing values + +While `compatForm` proxies value access on the `FormControl` level, the full form value preserves the control: + +```typescript +const passwordControl = new FormControl('password' /** ... */); + +const user = signal({ + email: '', + password: passwordControl, // Nest the legacy control directly +}); + +const form = compatForm(user); +form.password().value(); // 'password' +form().value(); // { email: '', password: FormControl} +``` + +If you need the whole form value, you'd have to build it manually: + +```typescript +const formValue = computed(() => ({ + email: form.email().value(), + password: form.password().value(), +})); // {email: '', password: ''} +``` + +## Bottom-up migration + +This is coming soon. + +## Automatic status classes + +Reactive/Template Forms automatically adds [class attributes](/guide/forms/template-driven-forms#track-control-states) (such as `.ng-valid` or `.ng-dirty`) to facilitate styling control states. Signal Forms does not do that. + +If you want to preserve this behavior, you can provide the `NG_STATUS_CLASSES` preset: + +```typescript +import {NG_STATUS_CLASSES, provideSignalFormsConfig} from '@angular/forms/signals'; + +bootstrapApplication(App, { + providers: [ + provideSignalFormsConfig({ + classes: NG_STATUS_CLASSES, + }), + ], +}); +``` + +You can also provide your own custom configuration to apply whatever classes you wish based on you custom logic: + +```typescript +import {provideSignalFormsConfig} from '@angular/forms/signals'; + +bootstrapApplication(App, { + providers: [ + provideSignalFormsConfig({ + classes: { + 'ng-valid': ({state}) => state().valid(), + 'ng-invalid': ({state}) => state().invalid(), + 'ng-touched': ({state}) => state().touched(), + 'ng-dirty': ({state}) => state().dirty(), + }, + }), + ], +}); +``` diff --git a/adev-ja/src/content/guide/http/http-resource.md b/adev-ja/src/content/guide/http/http-resource.md index c7c9467f4..1c6623fc4 100644 --- a/adev-ja/src/content/guide/http/http-resource.md +++ b/adev-ja/src/content/guide/http/http-resource.md @@ -42,11 +42,11 @@ user = httpResource(() => ({ mode: 'cors', redirect: 'error', priority: 'high', - cache : 'force-cache', + cache: 'force-cache', credentials: 'include', referrer: 'no-referrer', integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GhEXAMPLEKEY=', - referrerPolicy: 'no-referrer' + referrerPolicy: 'no-referrer', })); ``` @@ -87,7 +87,7 @@ The following example uses Zod to parse and validate the response from the [Star ```ts const starWarsPersonSchema = z.object({ name: z.string(), - height: z.number({ coerce: true }), + height: z.number({coerce: true}), edited: z.string().datetime(), films: z.array(z.string()), }); @@ -95,10 +95,9 @@ const starWarsPersonSchema = z.object({ export class CharacterViewer { id = signal(1); - swPersonResource = httpResource( - () => `https://swapi.info/api/people/${this.id()}`, - { parse: starWarsPersonSchema.parse } - ); + swPersonResource = httpResource(() => `https://swapi.info/api/people/${this.id()}`, { + parse: starWarsPersonSchema.parse, + }); } ``` @@ -110,10 +109,7 @@ The following example shows a unit test for code using `httpResource`. ```ts TestBed.configureTestingModule({ - providers: [ - provideHttpClient(), - provideHttpClientTesting(), - ], + providers: [provideHttpClient(), provideHttpClientTesting()], }); const id = signal(0); diff --git a/adev-ja/src/content/guide/i18n/add-package.md b/adev-ja/src/content/guide/i18n/add-package.md index b96f2376f..92f4a6be8 100644 --- a/adev-ja/src/content/guide/i18n/add-package.md +++ b/adev-ja/src/content/guide/i18n/add-package.md @@ -4,7 +4,7 @@ To take advantage of the localization features of Angular, use the [Angular CLI] To add the `@angular/localize` package, use the following command to update the `package.json` and TypeScript configuration files in your project. - + It adds `types: ["@angular/localize"]` in the TypeScript configuration files. It also adds line `/// ` at the top of the `main.ts` file which is the reference to the type definition. diff --git a/adev-ja/src/content/guide/i18n/import-global-variants.md b/adev-ja/src/content/guide/i18n/import-global-variants.md index 0ef6b0842..dc1ff6075 100644 --- a/adev-ja/src/content/guide/i18n/import-global-variants.md +++ b/adev-ja/src/content/guide/i18n/import-global-variants.md @@ -2,8 +2,6 @@ The [Angular CLI][CliMain] automatically includes locale data if you run the [`ng build`][CliBuild] command with the `--localize` option. - - ```shell ng build --localize ``` diff --git a/adev-ja/src/content/guide/i18n/locale-id.md b/adev-ja/src/content/guide/i18n/locale-id.md index 34eea840a..36336bb56 100644 --- a/adev-ja/src/content/guide/i18n/locale-id.md +++ b/adev-ja/src/content/guide/i18n/locale-id.md @@ -54,7 +54,7 @@ To change the source locale of your project for the build, complete the followin "projects": { "your-project": { "i18n": { - "sourceLocale": "ca" // Use your desired locale code + "sourceLocale": "ca" // Use your desired locale code } } } diff --git a/adev-ja/src/content/guide/i18n/prepare.md b/adev-ja/src/content/guide/i18n/prepare.md index 6361204bb..071fdfbc5 100644 --- a/adev-ja/src/content/guide/i18n/prepare.md +++ b/adev-ja/src/content/guide/i18n/prepare.md @@ -74,11 +74,11 @@ The following example displays an image with a `title` attribute. To mark the title attribute for translation, complete the following action. -1. Add the `i18n-title` attribute +Add the `i18n-title` attribute - The following example displays how to mark the `title` attribute on the `img` tag by adding `i18n-title`. +The following example displays how to mark the `title` attribute on the `img` tag by adding `i18n-title`. - + ## Mark text in component code @@ -93,7 +93,7 @@ $localize`string_to_translate`; The i18n metadata is surrounded by colon \(`:`\) characters and prepends the translation source text. ```ts -$localize`:{i18n_metadata}:string_to_translate` +$localize`:{i18n_metadata}:string_to_translate`; ``` ### Include interpolated text @@ -195,7 +195,6 @@ ICU expressions help you mark alternate text in component templates to meet cond An ICU expression includes a component property, an ICU clause, and the case statements surrounded by open curly brace \(`{`\) and close curly brace \(`}`\) characters. ```html - { component_property, icu_clause, case_statements } ``` @@ -217,19 +216,13 @@ Different languages have different pluralization rules that increase the difficu Because other locales express cardinality differently, you may need to set pluralization categories that do not align with English. Use the `plural` clause to mark expressions that may not be meaningful if translated word-for-word. - - ```html - { component_property, plural, pluralization_categories } ``` After the pluralization category, enter the default text \(English\) surrounded by open curly brace \(`{`\) and close curly brace \(`}`\) characters. - - ```html - pluralization_category { } ``` @@ -246,10 +239,7 @@ The following pluralization categories are available for English and may change If none of the pluralization categories match, Angular uses `other` to match the standard fallback for a missing category. - - ```html - other { default_quantity } ``` @@ -276,7 +266,6 @@ If you want to display the following phrase in English, where `x` is a number. ```html - updated x minutes ago ``` @@ -285,14 +274,12 @@ And you also want to display the following phrases based on the cardinality of ` ```html - updated just now ``` ```html - updated one minute ago ``` @@ -317,10 +304,7 @@ Review the following details in the previous code example. The `select` clause marks choices for alternate text based on your defined string values. - - ```html - { component_property, select, selection_categories } ``` @@ -328,10 +312,7 @@ Translate all of the alternates to display alternate text based on the value of After the selection category, enter the text \(English\) surrounded by open curly brace \(`{`\) and close curly brace \(`}`\) characters. - - ```html - selection_category { text } ``` @@ -339,10 +320,7 @@ Different locales have different grammatical constructions that increase the dif Use HTML markup. If none of the selection categories match, Angular uses `other` to match the standard fallback for a missing category. - - ```html - other { default_value } ``` @@ -353,7 +331,6 @@ If you want to display the following phrase in English. ```html - The author is other ``` @@ -362,14 +339,12 @@ And you also want to display the following phrases based on the `gender` propert ```html - The author is female ``` ```html - The author is male ``` diff --git a/adev-ja/src/content/guide/i18n/translation-files.md b/adev-ja/src/content/guide/i18n/translation-files.md index d7f26a54c..45782f9b4 100644 --- a/adev-ja/src/content/guide/i18n/translation-files.md +++ b/adev-ja/src/content/guide/i18n/translation-files.md @@ -86,19 +86,19 @@ To create a translation file for a locale or language, complete the following ac 1. Make a copy of the source language file to create a _translation_ file for each language. 1. Rename the _translation_ file to add the locale. - + ```file {hideCopy} messages.xlf --> messages.{locale}.xlf - + ``` 1. Create a new directory at your project root named `locale`. - + ```file {hideCopy} src/locale - + ``` 1. Move the _translation_ file to the new directory. 1. Send the _translation_ file to your translator. @@ -145,7 +145,7 @@ The following actions describe the translation process for French. 1. Translate the other text nodes. The following example displays the way to translate. - + IMPORTANT: Don't change the IDs for translation units. Each `id` attribute is generated by Angular and depends on the content of the component text and the assigned meaning. diff --git a/adev-ja/src/content/guide/ngmodules/overview.md b/adev-ja/src/content/guide/ngmodules/overview.md index c0b4bbdfd..76739afce 100644 --- a/adev-ja/src/content/guide/ngmodules/overview.md +++ b/adev-ja/src/content/guide/ngmodules/overview.md @@ -10,7 +10,7 @@ import {NgModule} from '@angular/core'; @NgModule({ // Metadata goes here }) -export class CustomMenuModule { } +export class CustomMenuModule {} ``` An NgModule has two main responsibilities: @@ -28,7 +28,7 @@ The `declarations` property of the `@NgModule` metadata declares the components, // CustomMenu and CustomMenuItem are components. declarations: [CustomMenu, CustomMenuItem], }) -export class CustomMenuModule { } +export class CustomMenuModule {} ``` In the example above, the components `CustomMenu` and `CustomMenuItem` belong to `CustomMenuModule`. @@ -45,7 +45,7 @@ const WIDGETS = [MENU_COMPONENTS, CustomSlider]; // CustomSlider, and CustomCheckbox. declarations: [WIDGETS, CustomCheckbox], }) -export class CustomMenuModule { } +export class CustomMenuModule {} ``` If Angular discovers any components, directives, or pipes declared in more than one NgModule, it reports an error. @@ -58,7 +58,9 @@ Any components, directives, or pipes must be explicitly marked as `standalone: f standalone: false, /* ... */ }) -export class CustomMenu { /* ... */ } +export class CustomMenu { + /* ... */ +} ``` ### imports @@ -72,7 +74,7 @@ Components declared in an NgModule may depend on other components, directives, a imports: [PopupTrigger, SelectionIndicator], declarations: [CustomMenu, CustomMenuItem], }) -export class CustomMenuModule { } +export class CustomMenuModule {} ``` The `imports` array accepts other NgModules, as well as standalone components, directives, and pipes. @@ -83,27 +85,27 @@ An NgModule can _export_ its declared components, directives, and pipes such tha ```typescript @NgModule({ - imports: [PopupTrigger, SelectionIndicator], - declarations: [CustomMenu, CustomMenuItem], + imports: [PopupTrigger, SelectionIndicator], + declarations: [CustomMenu, CustomMenuItem], - // Make CustomMenu and CustomMenuItem available to - // components and NgModules that import CustomMenuModule. - exports: [CustomMenu, CustomMenuItem], + // Make CustomMenu and CustomMenuItem available to + // components and NgModules that import CustomMenuModule. + exports: [CustomMenu, CustomMenuItem], }) -export class CustomMenuModule { } +export class CustomMenuModule {} ``` The `exports` property is not limited to declarations, however. An NgModule can also export any other components, directives, pipes, and NgModules that it imports. ```typescript @NgModule({ - imports: [PopupTrigger, SelectionIndicator], - declarations: [CustomMenu, CustomMenuItem], + imports: [PopupTrigger, SelectionIndicator], + declarations: [CustomMenu, CustomMenuItem], - // Also make PopupTrigger available to any component or NgModule that imports CustomMenuModule. - exports: [CustomMenu, CustomMenuItem, PopupTrigger], + // Also make PopupTrigger available to any component or NgModule that imports CustomMenuModule. + exports: [CustomMenu, CustomMenuItem, PopupTrigger], }) -export class CustomMenuModule { } +export class CustomMenuModule {} ``` ## `NgModule` providers @@ -124,14 +126,14 @@ An `NgModule` can specify `providers` for injected dependencies. These providers providers: [OverlayManager], /* ... */ }) -export class CustomMenuModule { } +export class CustomMenuModule {} @NgModule({ imports: [CustomMenuModule], declarations: [UserProfile], providers: [UserDataClient], }) -export class UserProfileModule { } +export class UserProfileModule {} ``` In the example above: @@ -149,9 +151,7 @@ Any providers included in this way are eagerly loaded, increasing the JavaScript ```typescript bootstrapApplication(MyApplicationRoot, { - providers: [ - CustomMenuModule.forRoot(/* some config */), - ], + providers: [CustomMenuModule.forRoot(/* some config */)], }); ``` @@ -160,11 +160,11 @@ Similarly, some NgModules may define a static `forChild` that indicates the prov ```typescript @Component({ /* ... */ - providers: [ - CustomMenuModule.forChild(/* some config */), - ], + providers: [CustomMenuModule.forChild(/* some config */)], }) -export class UserProfile { /* ... */ } +export class UserProfile { + /* ... */ +} ``` ## Bootstrapping an application @@ -173,7 +173,7 @@ IMPORTANT: The Angular team recommends using [bootstrapApplication](api/platform The `@NgModule` decorator accepts an optional `bootstrap` array that may contain one or more components. -You can use the [`bootstrapModule`](https://angular.dev/api/core/PlatformRef#bootstrapModule) method from either [`platformBrowser`](api/platform-browser/platformBrowser) or [`platformServer`](api/platform-server/platformServer) to start an Angular application. When run, this function locates any elements on the page with a CSS selector that matches the listed componet(s) and renders those components on the page. +You can use the [`bootstrapModule`](/api/core/PlatformRef#bootstrapModule) method from either [`platformBrowser`](api/platform-browser/platformBrowser) or [`platformServer`](api/platform-server/platformServer) to start an Angular application. When run, this function locates any elements on the page with a CSS selector that matches the listed componet(s) and renders those components on the page. ```typescript import {platformBrowser} from '@angular/platform-browser'; @@ -181,7 +181,7 @@ import {platformBrowser} from '@angular/platform-browser'; @NgModule({ bootstrap: [MyApplication], }) -export class MyApplicationModule { } +export class MyApplicationModule {} platformBrowser().bootstrapModule(MyApplicationModule); ``` diff --git a/adev-ja/src/content/guide/signals/effect.md b/adev-ja/src/content/guide/signals/effect.md index 9b5fcd79a..211f7d763 100644 --- a/adev-ja/src/content/guide/signals/effect.md +++ b/adev-ja/src/content/guide/signals/effect.md @@ -34,7 +34,9 @@ Instead, use `computed` signals to model state that depends on other state. By default, you can only create an `effect()` within an [injection context](guide/di/dependency-injection-context) (where you have access to the `inject` function). The easiest way to satisfy this requirement is to call `effect` within a component, directive, or service `constructor`: ```ts -@Component({/* ... */}) +@Component({ + /* ... */ +}) export class EffectiveCounter { readonly count = signal(0); @@ -70,18 +72,18 @@ Angular implicitly defines two implicit behaviors for its effects depending on t A "View Effect" is an `effect` created in the context of a component instantiation. This includes effects created by services that are tied to component injectors.
A "Root Effect" is created in the context of a root provided service instantiation. -The execution of both kind of `effect` are tied to the change detection process. +The execution of both kinds of `effect` are tied to the change detection process. -- "View effects" are executed _before_ there corresponding component is checked the change detection process. +- "View effects" are executed _before_ their corresponding component is checked by the change detection process. - "Root effects" are executed prior to all components being checked by the change detection process. -In both cases, if at least one of the effect dependency changed during the effect execution, the effect will re-run before moving ahead on the change detection process, +In both cases, if at least one of the effect dependencies changed during the effect execution, the effect will re-run before moving ahead on the change detection process, ### Destroying effects When a component or directive is destroyed, Angular automatically cleans up any associated effects. -An `effect` can be created two different context that will affect when it's destroyed: +An `effect` can be created in two different contexts that will affect when it's destroyed: - A "View effect" is destroyed when the component is destroyed. - A "Root effect" is destroyed when the application is destroyed. @@ -114,7 +116,9 @@ The `effect` function is a general-purpose tool for running code in reaction to For these situations, you can use `afterRenderEffect`. It functions like `effect`, but runs after Angular has finished rendering and committed its changes to the DOM. ```ts -@Component({/* ... */}) +@Component({ + /* ... */ +}) export class MyFancyChart { chartData = input.required(); canvas = viewChild.required>('canvas'); @@ -124,8 +128,8 @@ export class MyFancyChart { // Run a single time to create the chart instance afterNextRender({ write: () => { - this.chart = initializeChart(this.nativeElement(), this.charData()); - } + this.chart = initializeChart(this.canvas().nativeElement(), this.charData()); + }, }); // Re-run after DOM has been updated whenever `chartData` changes @@ -161,10 +165,18 @@ You can specify the phase by passing an object with a `phase` property to `after ```ts afterRenderEffect({ - earlyRead: (cleanupFn) => { /* ... */ }, - write: (previousPhaseValue, cleanupFn) => { /* ... */ }, - mixedReadWrite: (previousPhaseValue, cleanupFn) => { /* ... */ }, - read: (previousPhaseValue, cleanupFn) => { /* ... */ }, + earlyRead: (cleanupFn) => { + /* ... */ + }, + write: (previousPhaseValue, cleanupFn) => { + /* ... */ + }, + mixedReadWrite: (previousPhaseValue, cleanupFn) => { + /* ... */ + }, + read: (previousPhaseValue, cleanupFn) => { + /* ... */ + }, }); ``` diff --git a/adev-ja/src/content/reference/cli.md b/adev-ja/src/content/reference/cli.md index 16b419bb1..9411e6a0a 100644 --- a/adev-ja/src/content/reference/cli.md +++ b/adev-ja/src/content/reference/cli.md @@ -17,5 +17,5 @@ | [`run`](cli/run) | | Runs an Architect target with an optional custom builder configuration defined in your project. | | [`serve`](cli/serve) | `s`, `dev` | Builds and serves your application, rebuilding on file changes. | | [`test`](cli/test) | `t` | Runs unit tests in a project. | -| [`update`](cli/update) | | Updates your workspace and its dependencies. See https://angular.dev/update-guide/. | +| [`update`](cli/update) | | Updates your workspace and its dependencies. See the [Update Guide](/update-guide). | | [`version`](cli/version) | `v` | Outputs Angular CLI version. | diff --git a/adev-ja/src/content/reference/configs/npm-packages.md b/adev-ja/src/content/reference/configs/npm-packages.md index a6abb9f01..486cdce94 100644 --- a/adev-ja/src/content/reference/configs/npm-packages.md +++ b/adev-ja/src/content/reference/configs/npm-packages.md @@ -39,5 +39,5 @@ For a complete list of Angular packages, see the [API reference](api). | [`@angular/cli`](https://github.com/angular/angular-cli) | Contains the Angular CLI binary for running `ng` commands. | | [`@angular-devkit/build-angular`](https://www.npmjs.com/package/@angular-devkit/build-angular) | Contains default CLI builders for bundling, testing, and serving Angular applications and libraries. | | [`rxjs`](https://www.npmjs.com/package/rxjs) | A library for reactive programming using `Observables`. | -| [`zone.js`](https://github.com/angular/zone.js) | Angular relies on `zone.js`` to run Angular's change detection processes when native JavaScript operations raise events. | +| [`zone.js`](https://github.com/angular/zone.js) | Angular relies on `zone.js` to run Angular's change detection processes when native JavaScript operations raise events. | | [`typescript`](https://www.npmjs.com/package/typescript) | The TypeScript compiler, language server, and built-in type definitions. | diff --git a/adev-ja/src/content/reference/configs/workspace-config.md b/adev-ja/src/content/reference/configs/workspace-config.md index a122c8535..1757ff391 100644 --- a/adev-ja/src/content/reference/configs/workspace-config.md +++ b/adev-ja/src/content/reference/configs/workspace-config.md @@ -362,9 +362,7 @@ To add paths, use the `stylePreprocessorOptions` option: "builder": "@angular/build:application", "options": { "stylePreprocessorOptions": { - "includePaths": [ - "src/style-paths" - ] + "includePaths": ["src/style-paths"] } } } @@ -432,9 +430,7 @@ You can supply a value such as the following to apply optimization to one or the "builder": "@angular/build:application", "options": { "stylePreprocessorOptions": { - "includePaths": [ - "src/style-paths" - ] + "includePaths": ["src/style-paths"] } } } diff --git a/adev-ja/src/content/reference/errors/NG02802.md b/adev-ja/src/content/reference/errors/NG02802.md index fefbab65d..56e84fd72 100644 --- a/adev-ja/src/content/reference/errors/NG02802.md +++ b/adev-ja/src/content/reference/errors/NG02802.md @@ -15,8 +15,8 @@ Add the `transferCache` parameter to your HTTP request: ```typescript this.http.get('/api/data', { transferCache: { - includeHeaders: ['cache-control', 'etag'] - } + includeHeaders: ['cache-control', 'etag'], + }, }); ``` @@ -27,8 +27,8 @@ Configure `includeHeaders` when providing the transfer cache: ```typescript provideClientHydration( withHttpTransferCache({ - includeHeaders: ['cache-control', 'etag'] - }) + includeHeaders: ['cache-control', 'etag'], + }), ); ``` diff --git a/adev-ja/src/content/reference/errors/NG0401.md b/adev-ja/src/content/reference/errors/NG0401.md index af7037a4f..1b2bea4d9 100644 --- a/adev-ja/src/content/reference/errors/NG0401.md +++ b/adev-ja/src/content/reference/errors/NG0401.md @@ -9,9 +9,9 @@ When using server-side rendering, `bootstrapApplication` requires a `BootstrapCo To resolve this error, ensure that your `main.server.ts` file correctly passes a `BootstrapContext` to the `bootstrapApplication` function. ```typescript -import { bootstrapApplication, BootstrapContext } from '@angular/platform-browser'; -import { App } from './app/app'; -import { config } from './app/app.config.server'; +import {bootstrapApplication, BootstrapContext} from '@angular/platform-browser'; +import {App} from './app/app'; +import {config} from './app/app.config.server'; const bootstrap = (context: BootstrapContext) => bootstrapApplication(App, config, context); export default bootstrap; diff --git a/adev-ja/src/content/reference/errors/NG0919.md b/adev-ja/src/content/reference/errors/NG0919.md new file mode 100644 index 000000000..ae1c76efe --- /dev/null +++ b/adev-ja/src/content/reference/errors/NG0919.md @@ -0,0 +1,99 @@ +# Circular Dependency Detected + +Angular detected a circular dependency between components, directives, or pipes. This error occurs when component A imports component B, and component B (directly or indirectly) imports component A, creating a cycle. + +This circular reference prevents Angular from properly initializing the components, resulting in an error like: + +```text +NG0919: Cannot read @Component metadata. This can indicate a runtime circular dependency in your app that needs to be resolved. +``` + +In older Angular versions, you might instead see an error like: + +```text +Cannot read properties of undefined (reading 'ɵcmp') +``` + +## Common Causes + +### Mutual Component Imports + +The most common cause is when two components import each other: + +```angular-ts {header:"parent.component.ts"} +import {Component} from '@angular/core'; +import {ChildComponent} from './child.component'; + +@Component({ + selector: 'app-parent', + imports: [ChildComponent], + template: '', +}) +export class ParentComponent {} +``` + +```angular-ts {header:"child.component.ts"} +import {Component} from '@angular/core'; +import {ParentComponent} from './parent.component'; + +@Component({ + selector: 'app-child', + imports: [ParentComponent], + template: '', +}) +export class ChildComponent {} +``` + +### Indirect Circular References + +Circular dependencies can also occur through intermediate files: + +```text +ComponentA -> ComponentB -> ComponentC -> ComponentA +``` + +## Resolving the error + +### Refactor shared logic + +Move shared functionality to a separate file that doesn't import either component: + +```angular-ts {header:"shared.service.ts"} +import {Injectable} from '@angular/core'; + +@Injectable({providedIn: 'root'}) +export class SharedService { + // Shared logic here +} +``` + +### Use type-only imports + +If you only need types for TypeScript, use `import type`: + +```ts +import type {ParentComponent} from './parent.component'; +``` + +Type-only imports are erased at compile time and don't contribute to runtime circular dependencies. + +### Restructure component hierarchy + +Consider whether the circular dependency indicates a design issue. Often, extracting shared functionality into a third component or service is the cleanest solution. + +### Debugging complex circular dependencies + +For complex applications with many modules, circular dependencies can be difficult to identify manually. Consider using tools like [madge](https://www.npmjs.com/package/madge) to visualize and detect circular imports: + +```bash +# Install madge +npm install -g madge + +# Check for circular dependencies +madge --circular --extensions ts ./src + +# Generate a visual graph +madge --circular --extensions ts --image graph.svg ./src +``` + +These tools can help identify circular dependency chains across your entire project and generate visual dependency graphs. diff --git a/adev-ja/src/content/reference/extended-diagnostics/NG8021.md b/adev-ja/src/content/reference/extended-diagnostics/NG8021.md index 6a41db2da..11ca06700 100644 --- a/adev-ja/src/content/reference/extended-diagnostics/NG8021.md +++ b/adev-ja/src/content/reference/extended-diagnostics/NG8021.md @@ -3,7 +3,6 @@ This diagnostic detects unreachable or redundant triggers in `@defer` blocks. ```typescript - import {Component} from '@angular/core'; @Component({ @@ -11,7 +10,7 @@ import {Component} from '@angular/core'; @defer (on immediate; on timer(500ms)) { } - ` + `, }) class MyComponent {} ``` @@ -38,7 +37,7 @@ This diagnostic flags the following problematic patterns: @defer (on immediate; prefetch on idle) { } - ` + `, }) class MyComponent {} ``` @@ -51,7 +50,7 @@ class MyComponent {} @defer (on immediate) { } - ` + `, }) class MyComponent {} ``` @@ -66,7 +65,7 @@ class MyComponent {} @defer (on timer(100ms); prefetch on timer(3000ms)) { } - ` + `, }) class MyComponent {} ``` @@ -79,7 +78,7 @@ class MyComponent {} @defer (on timer(500ms); prefetch on timer(500ms)) { } - ` + `, }) class MyComponent {} ``` @@ -92,7 +91,7 @@ class MyComponent {} @defer (on timer(1000ms); prefetch on timer(500ms)) { } - ` + `, }) class MyComponent {} ``` @@ -107,7 +106,7 @@ class MyComponent {} @defer (on viewport; prefetch on viewport) { } - ` + `, }) class MyComponent {} ``` @@ -121,7 +120,7 @@ class MyComponent {} @defer (on interaction(loadBtn); prefetch on interaction(loadBtn)) { } - ` + `, }) class MyComponent {} ``` @@ -135,7 +134,7 @@ class MyComponent {} @defer (on interaction(loadBtn)) { } - ` + `, }) class MyComponent {} ``` @@ -150,7 +149,7 @@ class MyComponent {} @defer (on interaction(clickBtn); prefetch on hover(hoverArea)) { } - ` + `, }) class MyComponent {} ``` diff --git a/adev-ja/src/content/reference/extended-diagnostics/NG8101.md b/adev-ja/src/content/reference/extended-diagnostics/NG8101.md index 43799f403..e8644b44a 100644 --- a/adev-ja/src/content/reference/extended-diagnostics/NG8101.md +++ b/adev-ja/src/content/reference/extended-diagnostics/NG8101.md @@ -3,7 +3,6 @@ This diagnostic detects a backwards "banana-in-box" syntax for [two-way bindings](guide/templates/two-way-binding). ```html - ``` @@ -19,7 +18,6 @@ As the name suggests, the banana `(` should go _inside_ the box `[]`. In this case: ```html - ``` @@ -33,7 +31,6 @@ In this case: This diagnostic can be disabled by editing the project's `tsconfig.json` file: ```json - { "angularCompilerOptions": { "extendedDiagnostics": { diff --git a/adev-ja/src/content/reference/extended-diagnostics/NG8102.md b/adev-ja/src/content/reference/extended-diagnostics/NG8102.md index 15133eb4a..8c4e56901 100644 --- a/adev-ja/src/content/reference/extended-diagnostics/NG8102.md +++ b/adev-ja/src/content/reference/extended-diagnostics/NG8102.md @@ -74,7 +74,6 @@ username: string = 'Angelino'; This diagnostic can be disabled by editing the project's `tsconfig.json` file: ```json - { "angularCompilerOptions": { "extendedDiagnostics": { diff --git a/adev-ja/src/content/reference/extended-diagnostics/NG8103.md b/adev-ja/src/content/reference/extended-diagnostics/NG8103.md index f03fdda3d..831190915 100644 --- a/adev-ja/src/content/reference/extended-diagnostics/NG8103.md +++ b/adev-ja/src/content/reference/extended-diagnostics/NG8103.md @@ -59,7 +59,6 @@ class MyComponent {} This diagnostic can be disabled by editing the project's `tsconfig.json` file: ```json - { "angularCompilerOptions": { "extendedDiagnostics": { diff --git a/adev-ja/src/content/reference/extended-diagnostics/NG8104.md b/adev-ja/src/content/reference/extended-diagnostics/NG8104.md index e5279497c..89b106683 100644 --- a/adev-ja/src/content/reference/extended-diagnostics/NG8104.md +++ b/adev-ja/src/content/reference/extended-diagnostics/NG8104.md @@ -4,7 +4,6 @@ This diagnostic ensures that attributes which have the "special" Angular binding `class.`) are interpreted as bindings. ```html -
``` @@ -34,7 +33,6 @@ When binding to `attr.`, `class.`, or `style.`, ensure you use the Angular templ This diagnostic can be disabled by editing the project's `tsconfig.json` file: ```json - { "angularCompilerOptions": { "extendedDiagnostics": { diff --git a/adev-ja/src/content/reference/extended-diagnostics/NG8105.md b/adev-ja/src/content/reference/extended-diagnostics/NG8105.md index 5c7d80c2d..fdce634d2 100644 --- a/adev-ja/src/content/reference/extended-diagnostics/NG8105.md +++ b/adev-ja/src/content/reference/extended-diagnostics/NG8105.md @@ -45,7 +45,6 @@ class MyComponent { This diagnostic can be disabled by editing the project's `tsconfig.json` file: ```json - { "angularCompilerOptions": { "extendedDiagnostics": { diff --git a/adev-ja/src/content/reference/extended-diagnostics/NG8106.md b/adev-ja/src/content/reference/extended-diagnostics/NG8106.md index b2deac26b..417dc5fae 100644 --- a/adev-ja/src/content/reference/extended-diagnostics/NG8106.md +++ b/adev-ja/src/content/reference/extended-diagnostics/NG8106.md @@ -4,7 +4,7 @@ This diagnostic detects when the `.px`, `.%`, and `.em` suffixes are used with a binding. ```html - + ``` ## What's wrong with that? @@ -17,7 +17,7 @@ Rather than using the `.px`, `.%`, or `.em` suffixes that are only supported in move this to the value assignment of the binding. ```html - + ``` ## Configuration requirements @@ -30,7 +30,6 @@ move this to the value assignment of the binding. This diagnostic can be disabled by editing the project's `tsconfig.json` file: ```json - { "angularCompilerOptions": { "extendedDiagnostics": { diff --git a/adev-ja/src/content/reference/extended-diagnostics/NG8107.md b/adev-ja/src/content/reference/extended-diagnostics/NG8107.md index 4910b7834..f0c1729d1 100644 --- a/adev-ja/src/content/reference/extended-diagnostics/NG8107.md +++ b/adev-ja/src/content/reference/extended-diagnostics/NG8107.md @@ -66,7 +66,6 @@ class MyComponent { This diagnostic can be disabled by editing the project's `tsconfig.json` file: ```json - { "angularCompilerOptions": { "extendedDiagnostics": { diff --git a/adev-ja/src/content/reference/extended-diagnostics/NG8108.md b/adev-ja/src/content/reference/extended-diagnostics/NG8108.md index 6a623d99c..96f1530df 100644 --- a/adev-ja/src/content/reference/extended-diagnostics/NG8108.md +++ b/adev-ja/src/content/reference/extended-diagnostics/NG8108.md @@ -37,24 +37,17 @@ class MyComponent {} If a conditional is necessary, you can wrap the component in an `*ngIf`. ```html - -import {Component} from '@angular/core'; - -@Component({ -template: ` +import {Component} from '@angular/core'; @Component({ template: `
- +
- - - - -`, -}) -class MyComponent {} + + + +`, }) class MyComponent {} ``` ## Configuration requirements @@ -67,7 +60,6 @@ class MyComponent {} This diagnostic can be disabled by editing the project's `tsconfig.json` file: ```json - { "angularCompilerOptions": { "extendedDiagnostics": { diff --git a/adev-ja/src/content/reference/extended-diagnostics/NG8109.md b/adev-ja/src/content/reference/extended-diagnostics/NG8109.md index c29349c95..e3cfe35fa 100644 --- a/adev-ja/src/content/reference/extended-diagnostics/NG8109.md +++ b/adev-ja/src/content/reference/extended-diagnostics/NG8109.md @@ -43,7 +43,6 @@ class MyComponent { This diagnostic can be disabled by editing the project's `tsconfig.json` file: ```json - { "angularCompilerOptions": { "extendedDiagnostics": { diff --git a/adev-ja/src/content/reference/extended-diagnostics/NG8111.md b/adev-ja/src/content/reference/extended-diagnostics/NG8111.md index 2e54c0ddd..7a9df6059 100644 --- a/adev-ja/src/content/reference/extended-diagnostics/NG8111.md +++ b/adev-ja/src/content/reference/extended-diagnostics/NG8111.md @@ -47,7 +47,6 @@ class MyComponent { This diagnostic can be disabled by editing the project's `tsconfig.json` file: ```json - { "angularCompilerOptions": { "extendedDiagnostics": { diff --git a/adev-ja/src/content/reference/extended-diagnostics/NG8113.md b/adev-ja/src/content/reference/extended-diagnostics/NG8113.md index 0084db545..e441a1816 100644 --- a/adev-ja/src/content/reference/extended-diagnostics/NG8113.md +++ b/adev-ja/src/content/reference/extended-diagnostics/NG8113.md @@ -8,7 +8,6 @@ aren't used within the template. imports: [UsedDirective, UnusedPipe], }) class AwesomeCheckbox {} - ``` ## What's wrong with that? @@ -21,7 +20,7 @@ Delete the unused import. ```typescript @Component({ - imports: [UsedDirective] + imports: [UsedDirective], }) class AwesomeCheckbox {} ``` @@ -31,7 +30,6 @@ class AwesomeCheckbox {} This diagnostic can be disabled by editing the project's `tsconfig.json` file: ```json - { "angularCompilerOptions": { "extendedDiagnostics": { diff --git a/adev-ja/src/content/reference/extended-diagnostics/NG8115.md b/adev-ja/src/content/reference/extended-diagnostics/NG8115.md index a4310fcb0..562ee975c 100644 --- a/adev-ja/src/content/reference/extended-diagnostics/NG8115.md +++ b/adev-ja/src/content/reference/extended-diagnostics/NG8115.md @@ -47,7 +47,6 @@ class Items { This diagnostic can be disabled by editing the project's `tsconfig.json` file: ```json - { "angularCompilerOptions": { "extendedDiagnostics": { diff --git a/adev-ja/src/content/reference/extended-diagnostics/overview.md b/adev-ja/src/content/reference/extended-diagnostics/overview.md index e966bdf01..28c562f4e 100644 --- a/adev-ja/src/content/reference/extended-diagnostics/overview.md +++ b/adev-ja/src/content/reference/extended-diagnostics/overview.md @@ -41,7 +41,6 @@ Each diagnostic can be configured as either: Check severity can be configured as an [Angular compiler option](reference/configs/angular-compiler-options): ```json - { "angularCompilerOptions": { "extendedDiagnostics": { diff --git a/adev-ja/src/content/reference/migrations/inject-function.md b/adev-ja/src/content/reference/migrations/inject-function.md index 58dd68715..d07aa3e35 100644 --- a/adev-ja/src/content/reference/migrations/inject-function.md +++ b/adev-ja/src/content/reference/migrations/inject-function.md @@ -13,15 +13,15 @@ ng generate @angular/core:inject #### Before ```typescript -import { Component, Inject, Optional } from '@angular/core'; -import { MyService } from './service'; -import { DI_TOKEN } from './token'; +import {Component, Inject, Optional} from '@angular/core'; +import {MyService} from './service'; +import {DI_TOKEN} from './token'; @Component() export class MyComp { constructor( private service: MyService, - @Inject(DI_TOKEN) @Optional() readonly token: string + @Inject(DI_TOKEN) @Optional() readonly token: string, ) {} } ``` @@ -29,14 +29,14 @@ export class MyComp { #### After ```typescript -import { Component, inject } from '@angular/core'; -import { MyService } from './service'; -import { DI_TOKEN } from './token'; +import {Component, inject} from '@angular/core'; +import {MyService} from './service'; +import {DI_TOKEN} from './token'; @Component() export class MyComp { private service = inject(MyService); - readonly token = inject(DI_TOKEN, { optional: true }); + readonly token = inject(DI_TOKEN, {optional: true}); } ``` @@ -67,8 +67,8 @@ additional constructor signature to keep it backwards compatible, at the expense #### Before ```typescript -import { Component } from '@angular/core'; -import { MyService } from './service'; +import {Component} from '@angular/core'; +import {MyService} from './service'; @Component() export class MyComp { @@ -108,14 +108,14 @@ because the code that depends on them likely already accounts for their nullabil #### Before ```typescript -import { Component, Inject, Optional } from '@angular/core'; -import { TOKEN_ONE, TOKEN_TWO } from './token'; +import {Component, Inject, Optional} from '@angular/core'; +import {TOKEN_ONE, TOKEN_TWO} from './token'; @Component() export class MyComp { constructor( @Inject(TOKEN_ONE) @Optional() private tokenOne: number, - @Inject(TOKEN_TWO) @Optional() private tokenTwo: string | null + @Inject(TOKEN_TWO) @Optional() private tokenTwo: string | null, ) {} } ``` @@ -123,15 +123,15 @@ export class MyComp { #### After ```typescript -import { Component, inject } from '@angular/core'; -import { TOKEN_ONE, TOKEN_TWO } from './token'; +import {Component, inject} from '@angular/core'; +import {TOKEN_ONE, TOKEN_TWO} from './token'; @Component() export class MyComp { // Note the `!` at the end. - private tokenOne = inject(TOKEN_ONE, { optional: true })!; + private tokenOne = inject(TOKEN_ONE, {optional: true})!; // Does not have `!` at the end, because the type was already nullable. - private tokenTwo = inject(TOKEN_TWO, { optional: true }); + private tokenTwo = inject(TOKEN_TWO, {optional: true}); } ``` diff --git a/adev-ja/src/content/reference/migrations/ngclass-to-class.md b/adev-ja/src/content/reference/migrations/ngclass-to-class.md index df282a811..244aec819 100644 --- a/adev-ja/src/content/reference/migrations/ngclass-to-class.md +++ b/adev-ja/src/content/reference/migrations/ngclass-to-class.md @@ -12,13 +12,13 @@ ng generate @angular/core:ngclass-to-class #### Before ```html -
+
``` #### After ```html -
+
``` ## Configuration options diff --git a/adev-ja/src/content/reference/migrations/ngstyle-to-style.md b/adev-ja/src/content/reference/migrations/ngstyle-to-style.md index 223055ac4..e54913c0c 100644 --- a/adev-ja/src/content/reference/migrations/ngstyle-to-style.md +++ b/adev-ja/src/content/reference/migrations/ngstyle-to-style.md @@ -12,13 +12,13 @@ ng generate @angular/core:ngstyle-to-style #### Before ```html -
+
``` #### After ```html -
+
``` ## Configuration options diff --git a/adev-ja/src/content/reference/migrations/outputs.md b/adev-ja/src/content/reference/migrations/outputs.md index 3bd6d9996..8657571a7 100644 --- a/adev-ja/src/content/reference/migrations/outputs.md +++ b/adev-ja/src/content/reference/migrations/outputs.md @@ -25,7 +25,7 @@ ng generate @angular/core:output-migration import {Component, Output, EventEmitter} from '@angular/core'; @Component({ - template: `` + template: ``, }) export class MyComponent { @Output() someChange = new EventEmitter(); @@ -42,7 +42,7 @@ export class MyComponent { import {Component, output} from '@angular/core'; @Component({ - template: `` + template: ``, }) export class MyComponent { readonly someChange = output(); diff --git a/adev-ja/src/content/reference/migrations/route-lazy-loading.md b/adev-ja/src/content/reference/migrations/route-lazy-loading.md index 66db02021..1ae553df5 100644 --- a/adev-ja/src/content/reference/migrations/route-lazy-loading.md +++ b/adev-ja/src/content/reference/migrations/route-lazy-loading.md @@ -60,7 +60,7 @@ export class AppModule {} { path: 'home', // ↓ HomeComponent is now lazy loaded - loadComponent: () => import('./home/home.component').then(m => m.HomeComponent), + loadComponent: () => import('./home/home.component').then((m) => m.HomeComponent), }, ]), ], diff --git a/adev-ja/src/content/reference/migrations/router-testing-module-migration.md b/adev-ja/src/content/reference/migrations/router-testing-module-migration.md index 4cf969e75..2186c6086 100644 --- a/adev-ja/src/content/reference/migrations/router-testing-module-migration.md +++ b/adev-ja/src/content/reference/migrations/router-testing-module-migration.md @@ -23,34 +23,30 @@ ng generate @angular/core:router-testing-module-migration Before: ```ts -import { RouterTestingModule } from '@angular/router/testing'; -import { SpyLocation } from '@angular/common/testing'; +import {RouterTestingModule} from '@angular/router/testing'; +import {SpyLocation} from '@angular/common/testing'; describe('test', () => { - beforeEach(() => { TestBed.configureTestingModule({ - imports: [RouterTestingModule.withRoutes(routes, { initialNavigation: 'enabledBlocking' })] + imports: [RouterTestingModule.withRoutes(routes, {initialNavigation: 'enabledBlocking'})], }); }); - }); ``` After: ```ts -import { RouterModule } from '@angular/router'; -import { SpyLocation } from '@angular/common/testing'; +import {RouterModule} from '@angular/router'; +import {SpyLocation} from '@angular/common/testing'; describe('test', () => { - beforeEach(() => { TestBed.configureTestingModule({ - imports: [RouterModule.forRoot(routes, { initialNavigation: 'enabledBlocking' })] + imports: [RouterModule.forRoot(routes, {initialNavigation: 'enabledBlocking'})], }); }); - }); ``` @@ -59,45 +55,45 @@ describe('test', () => { Before: ```ts -import { RouterTestingModule } from '@angular/router/testing'; -import { SpyLocation } from '@angular/common/testing'; +import {RouterTestingModule} from '@angular/router/testing'; +import {SpyLocation} from '@angular/common/testing'; describe('test', () => { - let spy : SpyLocation; + let spy: SpyLocation; beforeEach(() => { TestBed.configureTestingModule({ - imports: [RouterTestingModule] + imports: [RouterTestingModule], }); spy = TestBed.inject(SpyLocation); }); it('Awesome test', () => { - expect(spy.urlChanges).toBeDefined() - }) + expect(spy.urlChanges).toBeDefined(); + }); }); ``` After: ```ts -import { RouterModule } from '@angular/router'; -import { provideLocationMocks } from '@angular/common/testing'; -import { SpyLocation } from '@angular/common/testing'; +import {RouterModule} from '@angular/router'; +import {provideLocationMocks} from '@angular/common/testing'; +import {SpyLocation} from '@angular/common/testing'; describe('test', () => { - let spy : SpyLocation; + let spy: SpyLocation; beforeEach(() => { TestBed.configureTestingModule({ imports: [RouterModule], - providers: [provideLocationMocks()] + providers: [provideLocationMocks()], }); spy = TestBed.inject(SpyLocation); }); it('Awesome test', () => { - expect(spy.urlChanges).toBeDefined() - }) + expect(spy.urlChanges).toBeDefined(); + }); }); ``` diff --git a/adev-ja/src/content/reference/migrations/standalone.md b/adev-ja/src/content/reference/migrations/standalone.md index 8b17b697b..472f74df2 100644 --- a/adev-ja/src/content/reference/migrations/standalone.md +++ b/adev-ja/src/content/reference/migrations/standalone.md @@ -72,7 +72,7 @@ HELPFUL: The schematic ignores NgModules which bootstrap a component during this @NgModule({ imports: [CommonModule], declarations: [GreeterComponent], - exports: [GreeterComponent] + exports: [GreeterComponent], }) export class SharedModule {} ``` @@ -95,7 +95,7 @@ export class GreeterComponent { // shared.module.ts @NgModule({ imports: [CommonModule, GreeterComponent], - exports: [GreeterComponent] + exports: [GreeterComponent], }) export class SharedModule {} ``` @@ -134,7 +134,7 @@ The migration considers a module safe to remove if that module: // importer.module.ts @NgModule({ imports: [FooComponent, BarPipe], - exports: [FooComponent, BarPipe] + exports: [FooComponent, BarPipe], }) export class ImporterModule {} ``` @@ -154,12 +154,12 @@ This step converts any usages of `bootstrapModule` to the new, standalone-based ```typescript // ./app/app.module.ts -import { NgModule } from '@angular/core'; -import { AppComponent } from './app.component'; +import {NgModule} from '@angular/core'; +import {AppComponent} from './app.component'; @NgModule({ declarations: [AppComponent], - bootstrap: [AppComponent] + bootstrap: [AppComponent], }) export class AppModule {} ``` @@ -176,10 +176,12 @@ export class AppComponent {} ```typescript // ./main.ts -import { platformBrowser } from '@angular/platform-browser'; -import { AppModule } from './app/app.module'; +import {platformBrowser} from '@angular/platform-browser'; +import {AppModule} from './app/app.module'; -platformBrowser().bootstrapModule(AppModule).catch(e => console.error(e)); +platformBrowser() + .bootstrapModule(AppModule) + .catch((e) => console.error(e)); ``` **After:** @@ -193,17 +195,17 @@ platformBrowser().bootstrapModule(AppModule).catch(e => console.error(e)); // ./app/app.component.ts @Component({ selector: 'app', - template: 'hello' + template: 'hello', }) export class AppComponent {} ``` ```typescript // ./main.ts -import { bootstrapApplication } from '@angular/platform-browser'; -import { AppComponent } from './app/app.component'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {AppComponent} from './app/app.component'; -bootstrapApplication(AppComponent).catch(e => console.error(e)); +bootstrapApplication(AppComponent).catch((e) => console.error(e)); ``` ## Common problems diff --git a/adev-ja/src/content/tools/cli/aot-compiler.md b/adev-ja/src/content/tools/cli/aot-compiler.md index cc529003f..afa68a38d 100644 --- a/adev-ja/src/content/tools/cli/aot-compiler.md +++ b/adev-ja/src/content/tools/cli/aot-compiler.md @@ -308,11 +308,9 @@ The compiler, however, only supports macros in the form of functions or static m For example, consider the following function: ```ts - export function wrapInArray(value: T): T[] { return [value]; } - ``` You can call the `wrapInArray` in a metadata definition because it returns the value of an expression that conforms to the compiler's restrictive JavaScript subset. @@ -320,23 +318,19 @@ You can call the `wrapInArray` in a metadata definition because it returns the v You might use `wrapInArray()` like this: ```ts - @NgModule({ - declarations: wrapInArray(TypicalComponent) + declarations: wrapInArray(TypicalComponent), }) export class TypicalModule {} - ``` The compiler treats this usage as if you had written: ```ts - @NgModule({ - declarations: [TypicalComponent] + declarations: [TypicalComponent], }) export class TypicalModule {} - ``` The Angular [`RouterModule`](api/router/RouterModule) exports two macro static methods, `forRoot` and `forChild`, to help declare root and child routes. @@ -352,34 +346,26 @@ the compiler doesn't need to know the expression's value — it just needs to be You might write something like: ```ts - -class TypicalServer { - -} +class TypicalServer {} @NgModule({ - providers: [{provide: SERVER, useFactory: () => TypicalServer}] + providers: [{provide: SERVER, useFactory: () => TypicalServer}], }) export class TypicalModule {} - ``` Without rewriting, this would be invalid because lambdas are not supported and `TypicalServer` is not exported. To allow this, the compiler automatically rewrites this to something like: ```ts - -class TypicalServer { - -} +class TypicalServer {} export const θ0 = () => new TypicalServer(); @NgModule({ - providers: [{provide: SERVER, useFactory: θ0}] + providers: [{provide: SERVER, useFactory: θ0}], }) export class TypicalModule {} - ``` This allows the compiler to generate a reference to `θ0` in the factory without having to know what the value of `θ0` contains. diff --git a/adev-ja/src/content/tools/cli/aot-metadata-errors.md b/adev-ja/src/content/tools/cli/aot-metadata-errors.md index 945640b44..0f30278ec 100644 --- a/adev-ja/src/content/tools/cli/aot-metadata-errors.md +++ b/adev-ja/src/content/tools/cli/aot-metadata-errors.md @@ -62,9 +62,7 @@ let foo = 42; // initialized The compiler will [fold](tools/cli/aot-compiler#code-folding) the expression into the provider as if you had written this. ```ts -providers: [ - { provide: Foo, useValue: 42 } -] +providers: [{provide: Foo, useValue: 42}]; ``` Alternatively, you can fix it by exporting `foo` with the expectation that `foo` will be assigned at runtime when you actually know its value. @@ -95,7 +93,7 @@ export let someTemplate: string; // exported but not initialized @Component({ selector: 'my-component', - template: someTemplate + template: someTemplate, }) export class MyComponent {} ``` @@ -119,7 +117,7 @@ export let someTemplate: string; @Component({ selector: 'my-component', - template: someTemplate + template: someTemplate, }) export class MyComponent {} ``` @@ -128,11 +126,11 @@ You'd also get this error if you imported `someTemplate` from some other module ```ts // ERROR - not initialized there either -import { someTemplate } from './config'; +import {someTemplate} from './config'; @Component({ selector: 'my-component', - template: someTemplate + template: someTemplate, }) export class MyComponent {} ``` @@ -148,7 +146,7 @@ export let someTemplate = '

Greetings from Angular

'; @Component({ selector: 'my-component', - template: someTemplate + template: someTemplate, }) export class MyComponent {} ``` @@ -378,14 +376,14 @@ This can happen if you use a number as a property name as in the following examp ```ts // ERROR -provider: [{ provide: Foo, useValue: { 0: 'test' } }] +provider: [{provide: Foo, useValue: {0: 'test'}}]; ``` Change the name of the property to something non-numeric. ```ts // CORRECTED -provider: [{ provide: Foo, useValue: { '0': 'test' } }] +provider: [{provide: Foo, useValue: {'0': 'test'}}]; ``` ## Unsupported enum member name diff --git a/adev-ja/src/content/tools/cli/build-system-migration.md b/adev-ja/src/content/tools/cli/build-system-migration.md index 51eec62be..fcd33b7ab 100644 --- a/adev-ja/src/content/tools/cli/build-system-migration.md +++ b/adev-ja/src/content/tools/cli/build-system-migration.md @@ -370,7 +370,7 @@ console.log(contents); // ... Additionally, TypeScript needs to be aware of the module type for the import to prevent type-checking errors during the build. This can be accomplished with an additional type definition file within the application source code (`src/types.d.ts`, for example) with the following or similar content: ```ts -declare module "*.svg" { +declare module '*.svg' { const content: string; export default content; } @@ -402,7 +402,7 @@ As an example, an SVG file can be imported as text via: ```ts // @ts-expect-error TypeScript cannot provide types based on attributes yet -import contents from './some-file.svg' with { loader: 'text' }; +import contents from './some-file.svg' with {loader: 'text'}; ``` The same can be accomplished with an import expression inside an async function. @@ -410,7 +410,7 @@ The same can be accomplished with an import expression inside an async function. ```ts async function loadSvg(): Promise { // @ts-expect-error TypeScript cannot provide types based on attributes yet - return import('./some-file.svg', { with: { loader: 'text' } }).then((m) => m.default); + return import('./some-file.svg', {with: {loader: 'text'}}).then((m) => m.default); } ``` @@ -421,7 +421,7 @@ The `file` loader is useful when a file will be loaded at runtime through either ```ts // @ts-expect-error TypeScript cannot provide types based on attributes yet -import imagePath from './image.webp' with { loader: 'file' }; +import imagePath from './image.webp' with {loader: 'file'}; console.log(imagePath); // media/image-ULK2SIIB.webp ``` @@ -430,18 +430,18 @@ The `base64` loader is useful when a file needs to be embedded directly into the ```ts // @ts-expect-error TypeScript cannot provide types based on attributes yet -import logo from './logo.png' with { loader: 'base64' }; +import logo from './logo.png' with {loader: 'base64'}; -console.log(logo) // "iVBORw0KGgoAAAANSUhEUgAA..." +console.log(logo); // "iVBORw0KGgoAAAANSUhEUgAA..." ``` The `dataurl` loader to inline assets as complete Data URLs. ```ts // @ts-expect-error TypeScript cannot provide types based on attributes yet -import icon from './icon.svg' with { loader: 'dataurl' }; +import icon from './icon.svg' with {loader: 'dataurl'}; -console.log(icon);// "data:image/svg+xml;..." +console.log(icon); // "data:image/svg+xml;..." ``` For production builds as shown in the code comment above, hashing will be automatically added to the path for long-term caching. diff --git a/adev-ja/src/content/tools/cli/cli-builder.md b/adev-ja/src/content/tools/cli/cli-builder.md index 07fcc1527..782728231 100644 --- a/adev-ja/src/content/tools/cli/cli-builder.md +++ b/adev-ja/src/content/tools/cli/cli-builder.md @@ -105,7 +105,6 @@ a `source` and a `destination`, each of which are a string. You can provide the following schema for type validation of these values. ```json {header: "schema.json"} - { "$schema": "http://json-schema.org/schema", "type": "object", @@ -128,7 +127,6 @@ To link our builder implementation with its schema and name, you need to create Create a file named `builders.json` that looks like this: ```json {header: "builders.json"} - { "builders": { "copy": { @@ -302,27 +300,24 @@ This target tells the builder to copy the `package.json` file. - `source` - The existing file you are copying. - `destination` - The path you want to copy to. -< header="angular.json" language="json"> - +```json {header: "angular.json"} { -"projects": { -"builder-test": { -"architect": { -"copy-package": { -"builder": "@example/copy-file:copy", -"options": { -"source": "package.json", -"destination": "package-copy.json" -} -}, - + "projects": { + "builder-test": { + "architect": { + "copy-package": { + "builder": "@example/copy-file:copy", + "options": { + "source": "package.json", + "destination": "package-copy.json" + } + } // Existing targets... } } - -} + } } - +``` ### Running the builder diff --git a/adev-ja/src/content/tools/cli/environments.md b/adev-ja/src/content/tools/cli/environments.md index 7fe7b278c..902ef1069 100644 --- a/adev-ja/src/content/tools/cli/environments.md +++ b/adev-ja/src/content/tools/cli/environments.md @@ -83,11 +83,9 @@ The base file `environment.ts`, contains the default environment settings. For example: ```ts - export const environment = { - production: true + production: true, }; - ``` The `build` command uses this as the build target when no environment is specified. @@ -95,24 +93,20 @@ You can add further variables, either as additional properties on the environmen For example, the following adds a default for a variable to the default environment: ```ts - export const environment = { production: true, - apiUrl: 'http://my-prod-url' + apiUrl: 'http://my-prod-url', }; - ``` You can add target-specific configuration files, such as `environment.development.ts`. The following content sets default values for the development build target: ```ts - export const environment = { production: false, - apiUrl: 'http://my-dev-url' + apiUrl: 'http://my-dev-url', }; - ``` ## Using environment-specific variables in your app @@ -120,9 +114,7 @@ export const environment = { To use the environment configurations you have defined, your components must import the original environments file: ```ts - -import { environment } from './environments/environment'; - +import {environment} from './environments/environment'; ``` This ensures that the build and serve commands can find the configurations for specific build targets. @@ -130,12 +122,10 @@ This ensures that the build and serve commands can find the configurations for s The following code in the component file (`app.component.ts`) uses an environment variable defined in the configuration files. ```ts - -import { environment } from './../environments/environment'; +import {environment} from './../environments/environment'; // Fetches from `http://my-prod-url` in production, `http://my-dev-url` in development. fetch(environment.apiUrl); - ``` The main CLI configuration file, `angular.json`, contains a `fileReplacements` section in the configuration for each build target, which lets you replace any file in the TypeScript program with a target-specific version of that file. diff --git a/adev-ja/src/content/tools/cli/schematics-authoring.md b/adev-ja/src/content/tools/cli/schematics-authoring.md index 006edcd4b..074e3968a 100644 --- a/adev-ja/src/content/tools/cli/schematics-authoring.md +++ b/adev-ja/src/content/tools/cli/schematics-authoring.md @@ -55,15 +55,14 @@ Rules can make use of utilities provided with the `@schematics/angular` package. Look for helper functions for working with modules, dependencies, TypeScript, AST, JSON, Angular CLI workspaces and projects, and more. ```ts {header: "index.ts"} - import { -JsonAstObject, -JsonObject, -JsonValue, -Path, -normalize, -parseJsonAst, -strings, + JsonAstObject, + JsonObject, + JsonValue, + Path, + normalize, + parseJsonAst, + strings, } from '@angular-devkit/core'; ``` @@ -154,19 +153,12 @@ In the short form, the type is inferred from the property's type and constraints In the following example, the property takes an enumerated value, so the schematic automatically chooses the list type, and creates a menu from the possible values. ```json {header: "schema.json"} - { "style": { "description": "The file extension or preprocessor to use for style files.", "type": "string", "default": "css", - "enum": [ - "css", - "scss", - "sass", - "less", - "styl" - ], + "enum": ["css", "scss", "sass", "less", "styl"], "x-prompt": "Which stylesheet format would you like to use?" } } @@ -192,26 +184,23 @@ It defines the prompt that lets users choose which style preprocessor they want By using the long form, the schematic can provide more explicit formatting of the menu choices. ```json {header: "schema.json"} - { "style": { "description": "The file extension or preprocessor to use for style files.", "type": "string", "default": "css", - "enum": [ - "css", - "scss", - "sass", - "less" - ], + "enum": ["css", "scss", "sass", "less"], "x-prompt": { "message": "Which stylesheet format would you like to use?", "type": "list", "items": [ - { "value": "css", "label": "CSS" }, - { "value": "scss", "label": "SCSS [ https://sass-lang.com/documentation/syntax#scss ]" }, - { "value": "sass", "label": "Sass [ https://sass-lang.com/documentation/syntax#the-indented-syntax ]" }, - { "value": "less", "label": "Less [ https://lesscss.org/ ]" } + {"value": "css", "label": "CSS"}, + {"value": "scss", "label": "SCSS [ https://sass-lang.com/documentation/syntax#scss ]"}, + { + "value": "sass", + "label": "Sass [ https://sass-lang.com/documentation/syntax#the-indented-syntax ]" + }, + {"value": "less", "label": "Less [ https://lesscss.org/ ]"} ] } } @@ -225,7 +214,6 @@ No additional logic or changes are required to the code of a schematic to suppor The following JSON schema is a complete description of the long-form syntax for the `x-prompt` field. ```json {header: "x-prompt schema"} - { "oneOf": [ { @@ -255,17 +243,13 @@ The following JSON schema is a complete description of the long-form syntax for }, "value": {} }, - "required": [ - "value" - ] + "required": ["value"] } ] } } }, - "required": [ - "message" - ] + "required": ["message"] } ] } @@ -360,10 +344,8 @@ The `src/` folder contains subfolders for named schematics in the collection, an Each schematic is created with a name, description, and factory function. ```json - { - "$schema": - "../node_modules/@angular-devkit/schematics/collection-schema.json", + "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json", "schematics": { "hello-world": { "description": "A blank schematic.", @@ -371,7 +353,6 @@ Each schematic is created with a name, description, and factory function. } } } - ``` - The `$schema` property specifies the schema that the CLI uses for validation. diff --git a/adev-ja/src/content/tools/cli/template-typecheck.md b/adev-ja/src/content/tools/cli/template-typecheck.md index b6340c808..298eb81a9 100644 --- a/adev-ja/src/content/tools/cli/template-typecheck.md +++ b/adev-ja/src/content/tools/cli/template-typecheck.md @@ -63,7 +63,6 @@ The three modes of type-checking treat embedded views differently. Consider the following example. ```ts {header:"User interface"} - interface User { name: string; address: { @@ -71,16 +70,13 @@ interface User { state: string; }; } - ``` ```html -

{{config.title}}

City: {{user.address.city}}
- ``` The `

` and the `` are in the `*ngFor` embedded view. @@ -197,18 +193,14 @@ There are two potential workarounds to the preceding issues: - In the template, include the non-null assertion operator `!` at the end of a nullable expression, such as ```html - - ``` In this example, the compiler disregards type incompatibilities in nullability, just as in TypeScript code. In the case of the `async` pipe, notice that the expression needs to be wrapped in parentheses, as in ```html - - ``` - Disable strict null checks in Angular templates completely. @@ -252,17 +244,13 @@ All of this works as expected, as long as a `boolean` value is bound to the inpu But, suppose a consumer uses this input in the template as an attribute: ```html - - ``` This has the same effect as the binding: ```html - - ``` At runtime, the input will be set to the empty string, which is not a `boolean` value. @@ -285,7 +273,6 @@ As a workaround for this problem, Angular supports checking a wider, more permis Enable this by adding a static property with the `ngAcceptInputType_` prefix to the component class: ```ts - class SubmitButton { private _disabled: boolean; @@ -295,12 +282,11 @@ class SubmitButton { } set disabled(value: boolean) { - this._disabled = (value === '') || value; + this._disabled = value === '' || value; } - static ngAcceptInputType_disabled: boolean|''; + static ngAcceptInputType_disabled: boolean | ''; } - ``` Since TypeScript 4.3, the setter could have been declared to accept `boolean|''` as type, making the input setter coercion field obsolete. diff --git a/adev-ja/src/content/tools/libraries/angular-package-format.md b/adev-ja/src/content/tools/libraries/angular-package-format.md index 4176aa328..9f29cfb29 100644 --- a/adev-ja/src/content/tools/libraries/angular-package-format.md +++ b/adev-ja/src/content/tools/libraries/angular-package-format.md @@ -29,13 +29,13 @@ node_modules/@angular/core ├── README.md ├── package.json ├── fesm2022 -│ ├── core.mjs -│ ├── core.mjs.map -│ ├── testing.mjs -│ └── testing.mjs.map +│ ├── core.mjs +│ ├── core.mjs.map +│ ├── testing.mjs +│ └── testing.mjs.map └── types -│ ├── core.d.ts -│ ├── testing.d.ts +│ ├── core.d.ts +│ ├── testing.d.ts ``` This table describes the file layout under `node_modules/@angular/core` annotated to describe the purpose of files and directories: @@ -169,14 +169,9 @@ The README file in the Markdown format that is used to display description of a Example README content of @angular/core package: ```html - -Angular -======= - -The sources for this package are in the main [Angular](https://github.com/angular/angular) repo.Please file issues and pull requests against that repo. - -License: MIT - +Angular ======= The sources for this package are in +the main [Angular](https://github.com/angular/angular) repo.Please file issues and pull requests +against that repo. License: MIT ``` ## Partial compilation @@ -273,8 +268,8 @@ This is because the tslib version is tied to the TypeScript version used to comp ## Examples - - + + ## Definition of terms @@ -368,4 +363,4 @@ The Ahead of Time Compiler for Angular. ### Flattened type definitions -The bundled TypeScript definitions generated from [API Extractor](https://api-extractor.com). +The bundled TypeScript definitions generated using tools like [API Extractor](https://api-extractor.com) or [rollup-plugin-dts](https://github.com/Swatinem/rollup-plugin-dts). diff --git a/adev-ja/src/content/tools/libraries/creating-libraries.md b/adev-ja/src/content/tools/libraries/creating-libraries.md index c551d7e57..bc33665ef 100644 --- a/adev-ja/src/content/tools/libraries/creating-libraries.md +++ b/adev-ja/src/content/tools/libraries/creating-libraries.md @@ -196,7 +196,7 @@ To use your own library in an application: - In your applications, import from the library by name: ```ts - import { myExport } from 'my-lib'; +import {myExport} from 'my-lib'; ``` ### Building and rebuilding your library @@ -277,9 +277,7 @@ To use linked libraries, you need to configure your application's `angular.json` "builder": "@angular-devkit/build-angular:dev-server", "options": { "prebundle": { - "exclude": [ - "my-lib" - ] + "exclude": ["my-lib"] } } } diff --git a/adev-ja/src/content/tools/libraries/using-libraries.md b/adev-ja/src/content/tools/libraries/using-libraries.md index 15b42b10a..67f6db62a 100644 --- a/adev-ja/src/content/tools/libraries/using-libraries.md +++ b/adev-ja/src/content/tools/libraries/using-libraries.md @@ -42,22 +42,22 @@ To do this: 1. Add the following code in `src/typings.d.ts`: ```ts - declare module 'host' { - export interface Host { - protocol?: string; - hostname?: string; - pathname?: string; - } - export function parse(url: string, queryString?: string): Host; + declare module 'host' { + export interface Host { + protocol?: string; + hostname?: string; + pathname?: string; } + export function parse(url: string, queryString?: string): Host; + } ``` 1. In the component or file that uses the library, add the following code: ```ts - import * as host from 'host'; - const parsedUrl = host.parse('https://angular.dev'); - console.log(parsedUrl.hostname); + import * as host from 'host'; + const parsedUrl = host.parse('https://angular.dev'); + console.log(parsedUrl.hostname); ``` Define more typings as needed. @@ -116,9 +116,7 @@ After you import a library using the "scripts" array, do **not** import it using The following code snippet is an example import statement. ```ts - import * as $ from 'jquery'; - ``` If you import it using import statements, you have two different copies of the library: one imported as a global library, and one imported as a module. @@ -134,28 +132,22 @@ If the global library you need to use does not have global typings, you can decl For example: ```ts - declare var libraryName: any; - ``` Some scripts extend other libraries; for instance with JQuery plugins: ```ts - $('.test').myPlugin(); - ``` In this case, the installed `@types/jquery` does not include `myPlugin`, so you need to add an interface in `src/typings.d.ts`. For example: ```ts - interface JQuery { myPlugin(options?: any): any; } - ``` If you do not add the interface for the script-defined extension, your IDE shows an error: @@ -171,7 +163,7 @@ If you do not add the interface for the script-defined extension, your IDE shows [GuideWorkspaceConfig]: reference/configs/workspace-config 'Angular workspace configuration | Angular' [Resources]: resources 'Explore Angular Resources | Angular' [AngularMaterialMain]: https://material.angular.dev 'Angular Material | Angular' -[AngularUpdateMain]: https://angular.dev/update-guide 'Angular Update Guide | Angular' +[AngularUpdateMain]: /update-guide 'Angular Update Guide | Angular' [GetbootstrapDocs40GettingStartedIntroduction]: https://getbootstrap.com/docs/4.0/getting-started/introduction 'Introduction | Bootstrap' [NpmjsMain]: https://www.npmjs.com 'npm' [YarnpkgMain]: https://yarnpkg.com ' Yarn' From 8503d829e36bdc3cbe4c3131c8bf33b6b49bdc2a Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 10:39:45 +0900 Subject: [PATCH 03/32] docs(guide/forms): migrate .en.md changes to Japanese translations - Apply structural changes from English sources to Japanese files - Update code formatting to match upstream style (spaces around operators) - Add new sections: "Managing form control state" and "Controlling event emission" to reactive-forms.md - Add "Integration with schema validation libraries" section to validation.md - Update navigation links in overview.md to include new guides - Maintain line-by-line correspondence between .en.md and .md files for easier diffing --- .../src/content/guide/forms/overview.en.md | 2 +- adev-ja/src/content/guide/forms/overview.md | 2 +- .../content/guide/forms/reactive-forms.en.md | 148 ++++++++++++++++-- .../src/content/guide/forms/reactive-forms.md | 148 ++++++++++++++++-- .../guide/forms/signals/comparison.en.md | 14 +- .../content/guide/forms/signals/comparison.md | 14 +- .../guide/forms/signals/custom-controls.en.md | 12 +- .../guide/forms/signals/custom-controls.md | 12 +- .../signals/field-state-management.en.md | 94 +++++------ .../forms/signals/field-state-management.md | 94 +++++------ .../content/guide/forms/signals/models.en.md | 85 +++++----- .../src/content/guide/forms/signals/models.md | 97 ++++++------ .../guide/forms/signals/overview.en.md | 9 +- .../content/guide/forms/signals/overview.md | 9 +- .../guide/forms/signals/validation.en.md | 96 +++++++----- .../content/guide/forms/signals/validation.md | 98 +++++++----- .../guide/forms/template-driven-forms.en.md | 3 +- .../guide/forms/template-driven-forms.md | 3 +- .../src/content/guide/forms/typed-forms.en.md | 8 +- .../src/content/guide/forms/typed-forms.md | 8 +- 20 files changed, 615 insertions(+), 341 deletions(-) diff --git a/adev-ja/src/content/guide/forms/overview.en.md b/adev-ja/src/content/guide/forms/overview.en.md index 361754a20..ef499562a 100644 --- a/adev-ja/src/content/guide/forms/overview.en.md +++ b/adev-ja/src/content/guide/forms/overview.en.md @@ -304,7 +304,7 @@ Here are the steps performed in the model to view test. 1. Use the component instance to set the value of the `favoriteColor` property. 1. Run change detection through the test fixture. -1. Use the `tick()` method to simulate the passage of time within the `fakeAsync()` task. +1. Use `await fixture.whenStable()` to wait for the next rendering. 1. Query the view for the form input element. 1. Assert that the input value matches the value of the `favoriteColor` property in the component instance. diff --git a/adev-ja/src/content/guide/forms/overview.md b/adev-ja/src/content/guide/forms/overview.md index 3bf5fa9c1..ec8772abb 100644 --- a/adev-ja/src/content/guide/forms/overview.md +++ b/adev-ja/src/content/guide/forms/overview.md @@ -304,7 +304,7 @@ NOTE: `NgModel` は、値の変更が入力バインディングから発生す 1. コンポーネントインスタンスを使用して、`favoriteColor` プロパティの値を設定します。 1. テストフィクスチャを通じて変更検知を実行します。 -1. `fakeAsync()` タスク内で、`tick()` メソッドを使用して時間の経過をシミュレートします。 +1. `await fixture.whenStable()` を使用して次のレンダリングを待ちます。 1. ビューからフォーム入力要素をクエリします。 1. 入力の値が、コンポーネントインスタンスの `favoriteColor` プロパティの値と一致することをアサートします。 diff --git a/adev-ja/src/content/guide/forms/reactive-forms.en.md b/adev-ja/src/content/guide/forms/reactive-forms.en.md index 1a9a6746f..5dc6e6c54 100644 --- a/adev-ja/src/content/guide/forms/reactive-forms.en.md +++ b/adev-ja/src/content/guide/forms/reactive-forms.en.md @@ -437,7 +437,7 @@ Each item emitted by `events` is an instance of a specific event class: All event classes extend `ControlEvent` and include a `source` reference to the `AbstractControl` that originated the change, which is useful in large forms. ```ts -import { Component } from '@angular/core'; +import {Component} from '@angular/core'; import { FormControl, ValueChangeEvent, @@ -450,7 +450,9 @@ import { FormGroup, } from '@angular/forms'; -@Component({/* ... */ }) +@Component({ + /* ... */ +}) export class UnifiedEventsBasicComponent { form = new FormGroup({ username: new FormControl(''), @@ -491,8 +493,8 @@ export class UnifiedEventsBasicComponent { Prefer RxJS operators when you only need a subset of event types. ```ts -import { filter } from 'rxjs/operators'; -import { StatusChangeEvent } from '@angular/forms'; +import {filter} from 'rxjs/operators'; +import {StatusChangeEvent} from '@angular/forms'; control.events .pipe(filter((e) => e instanceof StatusChangeEvent)) @@ -504,10 +506,11 @@ control.events **Before** ```ts -import { combineLatest } from 'rxjs/operators'; +import {combineLatest} from 'rxjs/operators'; -combineLatest([control.valueChanges, control.statusChanges]) - .subscribe(([value, status]) => { /* ... */ }); +combineLatest([control.valueChanges, control.statusChanges]).subscribe(([value, status]) => { + /* ... */ +}); ``` **After** @@ -520,6 +523,123 @@ control.events.subscribe((e) => { NOTE: On value change, the emit happens right after a value of this control is updated. The value of a parent control (for example if this FormControl is a part of a FormGroup) is updated later, so accessing a value of a parent control (using the `value` property) from the callback of this event might result in getting a value that has not been updated yet. Subscribe to the `events` of the parent control instead. +## Managing form control state + +Reactive forms track control state through **touched/untouched** and **pristine/dirty**. Angular updates these automatically during DOM interactions, but you can also manage them programmatically. + +**[`markAsTouched`](api/forms/FormControl#markAsTouched)** — Marks a control or form as touched by focus and blur events that do not change the value. Propagates to parent controls by default. + +```ts +// Show validation errors after user leaves a field +onEmailBlur() { + const email = this.form.get('email'); + email.markAsTouched(); +} +``` + +**[`markAsUntouched`](api/forms/FormControl#markAsUntouched)** — Marks a control or form as untouched. Cascades to all child controls and recalculates the touched status of all parent controls. + +```ts +// Reset form state after successful submission +onSubmitSuccess() { + this.form.markAsUntouched(); + this.form.markAsPristine(); +} +``` + +**[`markAsDirty`](api/forms/FormControl#markAsDirty)** — Marks a control or form as dirty, meaning the value has been changed. Propagates to parent controls by default. + +```ts +// Mark programmatically changed values as modified +autofillAddress() { + const previousAddress = getAddress(); + this.form.patchValue(previousAddress, { emitEvent: false }); + this.form.markAsDirty(); +} +``` + +**[`markAsPristine`](api/forms/FormControl#markAsPristine)** — Marks a control or form as pristine. Marks all child controls as pristine and recalculates the pristine status of all parent controls. + +```ts +// Reset pristine state after saving to track new changes +saveForm() { + this.api.save(this.form.value).subscribe(() => { + this.form.markAsPristine(); + }); +} +``` + +**[`markAllAsDirty`](api/forms/FormControl#markAllAsDirty)** — Marks the control or form and all its descendant controls as dirty. + +```ts +// Mark imported data as dirty +loadData(data: FormData) { + this.form.patchValue(data); + this.form.markAllAsDirty(); +} +``` + +**[`markAllAsTouched`](api/forms/FormControl#markAllAsTouched)** — Marks the control or form and all its descendant controls as touched. Useful for showing validation errors across the entire form. + +```ts +// Show all validation errors before submission +onSubmit() { + if (this.form.invalid) { + this.form.markAllAsTouched(); + return; + } + this.saveForm(); +} +``` + +## Controlling event emission and propagation + +When updating form controls programmatically, you have precise control over how changes propagate through the form hierarchy and whether events are emitted. + +### Understanding event emission + +By default `emitEvent: true`, any change to a control emits events through the `valueChanges` and `statusChanges` observables. Setting `emitEvent: false` suppresses these emissions, which is useful when setting values programmatically without triggering reactive behavior like auto-save, avoiding circular updates between controls, or performing bulk updates where events should emit only once at the end. + +```ts +@Component({/* ... */}) +export class BlogPostEditor { + postForm = new FormGroup({ + title: new FormControl(''), + content: new FormControl(''), + }); + + constructor() { + // Auto-save draft every time user types + this.postForm.valueChanges.subscribe(formValue => { + this.autosaveDraft(formValue); + }); + } + + loadExistingDraft(savedDraft: {title: string; content: string}) { + // Restore draft without triggering auto-save + this.postForm.setValue(savedDraft, { emitEvent: false }); + } + +} +``` + +### Understanding propagation control + +By default `onlySelf: false` , updates cascade to parent controls, recalculating their values and validation status. Setting `onlySelf: true` isolates the update to the current control, preventing parent notification. This is useful for batch operations where you want to manually trigger the parent update once. + +```ts +updatePostalCodeValidator(country: string) { + const postal = this.addressForm.get('postalCode'); + + const validators = country === 'US' + ? [Validators.maxLength(5)] + : [Validators.maxLength(7)]; + + postal.setValidators(validators); + postal.updateValueAndValidity({ onlySelf: true, emitEvent: false }); +} +``` + ## Utility functions for narrowing form control types Angular provides four utility functions that help determine the concrete type of an `AbstractControl`. These functions act as **type guards** and narrow the control type when they return `true`, which lets you safely access subtype-specific properties inside the same block. @@ -534,16 +654,16 @@ Angular provides four utility functions that help determine the concrete type of These helpers are particularly useful in **custom validators**, where the function signature receives an `AbstractControl`, but the logic is intended for a specific control kind. ```ts -import { AbstractControl, isFormArray } from '@angular/forms'; +import {AbstractControl, isFormArray} from '@angular/forms'; export function positiveValues(control: AbstractControl) { - if (!isFormArray(control)) { - return null; // Not a FormArray: validator is not applicable. - } + if (!isFormArray(control)) { + return null; // Not a FormArray: validator is not applicable. + } - // Safe to access FormArray-specific API after narrowing. - const hasNegative = control.controls.some(c => c.value < 0); - return hasNegative ? { positiveValues: true } : null; + // Safe to access FormArray-specific API after narrowing. + const hasNegative = control.controls.some((c) => c.value < 0); + return hasNegative ? {positiveValues: true} : null; } ``` diff --git a/adev-ja/src/content/guide/forms/reactive-forms.md b/adev-ja/src/content/guide/forms/reactive-forms.md index 2488ed8ee..e7f2c693f 100644 --- a/adev-ja/src/content/guide/forms/reactive-forms.md +++ b/adev-ja/src/content/guide/forms/reactive-forms.md @@ -437,7 +437,7 @@ export class FormArrayExampleComponent { すべてのイベントクラスは `ControlEvent` を拡張し、変更を発生させた `AbstractControl` への `source` 参照が含まれます。これは大規模なフォームで便利です。 ```ts -import { Component } from '@angular/core'; +import {Component} from '@angular/core'; import { FormControl, ValueChangeEvent, @@ -450,7 +450,9 @@ import { FormGroup, } from '@angular/forms'; -@Component({/* ... */ }) +@Component({ + /* ... */ +}) export class UnifiedEventsBasicComponent { form = new FormGroup({ username: new FormControl(''), @@ -491,8 +493,8 @@ export class UnifiedEventsBasicComponent { イベントタイプのサブセットのみが必要な場合は、RxJS演算子を使用します。 ```ts -import { filter } from 'rxjs/operators'; -import { StatusChangeEvent } from '@angular/forms'; +import {filter} from 'rxjs/operators'; +import {StatusChangeEvent} from '@angular/forms'; control.events .pipe(filter((e) => e instanceof StatusChangeEvent)) @@ -504,10 +506,11 @@ control.events **Before** ```ts -import { combineLatest } from 'rxjs/operators'; +import {combineLatest} from 'rxjs/operators'; -combineLatest([control.valueChanges, control.statusChanges]) - .subscribe(([value, status]) => { /* ... */ }); +combineLatest([control.valueChanges, control.statusChanges]).subscribe(([value, status]) => { + /* ... */ +}); ``` **After** @@ -520,6 +523,123 @@ control.events.subscribe((e) => { NOTE: 値の変更時、このコントロールの値が更新された直後に発行が発生します。親コントロールの値(たとえば、このFormControlがFormGroupの一部である場合)は後で更新されるため、このイベントのコールバックから親コントロールの値(`value` プロパティを使用)にアクセスすると、まだ更新されていない値を取得する可能性があります。代わりに親コントロールの `events` をサブスクライブしてください。 +## フォームコントロール状態の管理 + +リアクティブフォームは、**touched/untouched** と **pristine/dirty** を通じてコントロールの状態を追跡します。Angularは、DOM操作中にこれらを自動的に更新しますが、プログラムで管理することもできます。 + +**[`markAsTouched`](api/forms/FormControl#markAsTouched)** — フォーカスとブラーイベントによってコントロールまたはフォームをtouchedとしてマークし、値は変更されません。デフォルトで親コントロールに伝播します。 + +```ts +// ユーザーがフィールドから離れた後に検証エラーを表示 +onEmailBlur() { + const email = this.form.get('email'); + email.markAsTouched(); +} +``` + +**[`markAsUntouched`](api/forms/FormControl#markAsUntouched)** — コントロールまたはフォームをuntouchedとしてマークします。すべての子コントロールにカスケードし、すべての親コントロールのtouchedステータスを再計算します。 + +```ts +// 正常な送信後にフォーム状態をリセット +onSubmitSuccess() { + this.form.markAsUntouched(); + this.form.markAsPristine(); +} +``` + +**[`markAsDirty`](api/forms/FormControl#markAsDirty)** — コントロールまたはフォームをdirty(値が変更された)としてマークします。デフォルトで親コントロールに伝播します。 + +```ts +// プログラムで変更された値を変更済みとしてマーク +autofillAddress() { + const previousAddress = getAddress(); + this.form.patchValue(previousAddress, { emitEvent: false }); + this.form.markAsDirty(); +} +``` + +**[`markAsPristine`](api/forms/FormControl#markAsPristine)** — コントロールまたはフォームをpristineとしてマークします。すべての子コントロールをpristineとしてマークし、すべての親コントロールのpristineステータスを再計算します。 + +```ts +// 保存後にpristine状態をリセットして新しい変更を追跡 +saveForm() { + this.api.save(this.form.value).subscribe(() => { + this.form.markAsPristine(); + }); +} +``` + +**[`markAllAsDirty`](api/forms/FormControl#markAllAsDirty)** — コントロールまたはフォームと、そのすべての子孫コントロールをdirtyとしてマークします。 + +```ts +// インポートされたデータをdirtyとしてマーク +loadData(data: FormData) { + this.form.patchValue(data); + this.form.markAllAsDirty(); +} +``` + +**[`markAllAsTouched`](api/forms/FormControl#markAllAsTouched)** — コントロールまたはフォームと、そのすべての子孫コントロールをtouchedとしてマークします。フォーム全体に検証エラーを表示するのに便利です。 + +```ts +// 送信前にすべての検証エラーを表示 +onSubmit() { + if (this.form.invalid) { + this.form.markAllAsTouched(); + return; + } + this.saveForm(); +} +``` + +## イベント発行と伝播の制御 + +フォームコントロールをプログラムで更新する場合、フォーム階層を通じて変更を伝播する方法と、イベントを発行するかどうかを正確に制御できます。 + +### イベント発行の理解 + +デフォルトで `emitEvent: true` の場合、コントロールへの変更は `valueChanges` と `statusChanges` Observableを通じてイベントを発行します。`emitEvent: false` を設定すると、これらの発行が抑制されます。これは、自動保存のようなリアクティブな動作をトリガーせずにプログラムで値を設定する場合、コントロール間の循環更新を回避する場合、またはイベントが最後に一度だけ発行されるべき一括更新を実行する場合に便利です。 + +```ts +@Component({/* ... */}) +export class BlogPostEditor { + postForm = new FormGroup({ + title: new FormControl(''), + content: new FormControl(''), + }); + + constructor() { + // ユーザーが入力するたびに下書きを自動保存 + this.postForm.valueChanges.subscribe(formValue => { + this.autosaveDraft(formValue); + }); + } + + loadExistingDraft(savedDraft: {title: string; content: string}) { + // 自動保存をトリガーせずに下書きを復元 + this.postForm.setValue(savedDraft, { emitEvent: false }); + } + +} +``` + +### 伝播制御の理解 + +デフォルトで `onlySelf: false` の場合、更新は親コントロールにカスケードし、値と検証ステータスを再計算します。`onlySelf: true` を設定すると、更新が現在のコントロールに分離され、親への通知が防止されます。これは、親の更新を一度だけ手動でトリガーしたいバッチ操作に便利です。 + +```ts +updatePostalCodeValidator(country: string) { + const postal = this.addressForm.get('postalCode'); + + const validators = country === 'US' + ? [Validators.maxLength(5)] + : [Validators.maxLength(7)]; + + postal.setValidators(validators); + postal.updateValueAndValidity({ onlySelf: true, emitEvent: false }); +} +``` + ## フォームコントロールタイプを絞り込むユーティリティ関数 {#utility-functions-for-narrowing-form-control-types} Angularは、`AbstractControl` の具体的な型を判定するのに役立つ4つのユーティリティ関数を提供します。これらの関数は **型ガード** として機能し、`true` を返すときにコントロールタイプを絞り込むため、同じブロック内でサブタイプ固有のプロパティに安全にアクセスできます。 @@ -534,16 +654,16 @@ Angularは、`AbstractControl` の具体的な型を判定するのに役立つ4 これらのヘルパーは、関数シグネチャが `AbstractControl` を受け取りますが、ロジックが特定のコントロールの種類を対象としている **カスタムバリデーター** で特に便利です。 ```ts -import { AbstractControl, isFormArray } from '@angular/forms'; +import {AbstractControl, isFormArray} from '@angular/forms'; export function positiveValues(control: AbstractControl) { - if (!isFormArray(control)) { - return null; // Not a FormArray: validator is not applicable. - } + if (!isFormArray(control)) { + return null; // Not a FormArray: validator is not applicable. + } - // Safe to access FormArray-specific API after narrowing. - const hasNegative = control.controls.some(c => c.value < 0); - return hasNegative ? { positiveValues: true } : null; + // Safe to access FormArray-specific API after narrowing. + const hasNegative = control.controls.some((c) => c.value < 0); + return hasNegative ? {positiveValues: true} : null; } ``` diff --git a/adev-ja/src/content/guide/forms/signals/comparison.en.md b/adev-ja/src/content/guide/forms/signals/comparison.en.md index d2690a929..ff2f23107 100644 --- a/adev-ja/src/content/guide/forms/signals/comparison.en.md +++ b/adev-ja/src/content/guide/forms/signals/comparison.en.md @@ -22,7 +22,7 @@ NOTE: Signal Forms are [experimental](reference/releases#experimental) as of Ang The best way to understand the differences is to see the same form implemented in all three approaches. - + @@ -54,7 +54,7 @@ This separates form state management from your component's data model. The form Template-driven Forms stores data in component properties. You access values directly: ```ts -const credentials = { email: this.email, password: this.password }; +const credentials = {email: this.email, password: this.password}; ``` This is the most direct approach but requires manually assembling values when you need them. Angular manages form state through directives in the template. @@ -67,8 +67,8 @@ Signal Forms uses a schema function where you bind validators to field paths: ```ts loginForm = form(this.loginModel, (fieldPath) => { - required(fieldPath.email, { message: 'Email is required' }); - email(fieldPath.email, { message: 'Enter a valid email address' }); + required(fieldPath.email, {message: 'Email is required'}); + email(fieldPath.email, {message: 'Enter a valid email address'}); }); ``` @@ -78,7 +78,7 @@ Reactive Forms attaches validators when creating controls: ```ts loginForm = new FormGroup({ - email: new FormControl('', [Validators.required, Validators.email]) + email: new FormControl('', [Validators.required, Validators.email]), }); ``` @@ -99,7 +99,7 @@ TypeScript integration differs significantly between approaches, affecting how m Signal Forms infers types from your model structure: ```ts -const loginModel = signal({ email: '', password: '' }); +const loginModel = signal({email: '', password: ''}); const loginForm = form(loginModel); // TypeScript knows: loginForm.email exists and returns FieldState ``` @@ -111,7 +111,7 @@ Reactive Forms requires explicit type annotations with typed forms: ```ts const loginForm = new FormGroup({ email: new FormControl(''), - password: new FormControl('') + password: new FormControl(''), }); // TypeScript knows: loginForm.controls.email is FormControl ``` diff --git a/adev-ja/src/content/guide/forms/signals/comparison.md b/adev-ja/src/content/guide/forms/signals/comparison.md index 1837546ed..082cc8484 100644 --- a/adev-ja/src/content/guide/forms/signals/comparison.md +++ b/adev-ja/src/content/guide/forms/signals/comparison.md @@ -22,7 +22,7 @@ NOTE: シグナルフォームはAngular v21の時点では[experimental](refere 違いを理解する最善の方法は、3つのアプローチすべてで実装された同じフォームを見ることです。 - + @@ -54,7 +54,7 @@ const credentials = this.loginForm.value; // { email: '...', password: '...' } テンプレート駆動フォームは、コンポーネントのプロパティにデータを保存します。値に直接アクセスします。 ```ts -const credentials = { email: this.email, password: this.password }; +const credentials = {email: this.email, password: this.password}; ``` これは最も直接的なアプローチですが、値が必要なときに手動で組み立てる必要があります。Angularは、テンプレート内のディレクティブを通じてフォームの状態を管理します。 @@ -67,8 +67,8 @@ const credentials = { email: this.email, password: this.password }; ```ts loginForm = form(this.loginModel, (fieldPath) => { - required(fieldPath.email, { message: 'Email is required' }); - email(fieldPath.email, { message: 'Enter a valid email address' }); + required(fieldPath.email, {message: 'Email is required'}); + email(fieldPath.email, {message: 'Enter a valid email address'}); }); ``` @@ -78,7 +78,7 @@ Reactive Formsは、コントロールを作成するときにバリデーター ```ts loginForm = new FormGroup({ - email: new FormControl('', [Validators.required, Validators.email]) + email: new FormControl('', [Validators.required, Validators.email]), }); ``` @@ -99,7 +99,7 @@ TypeScriptの統合はアプローチによって大きく異なり、コンパ シグナルフォームは、モデル構造から型を推論します。 ```ts -const loginModel = signal({ email: '', password: '' }); +const loginModel = signal({email: '', password: ''}); const loginForm = form(loginModel); // TypeScript knows: loginForm.email exists and returns FieldState ``` @@ -111,7 +111,7 @@ Reactive Formsは、型付きフォームで明示的な型アノテーション ```ts const loginForm = new FormGroup({ email: new FormControl(''), - password: new FormControl('') + password: new FormControl(''), }); // TypeScript knows: loginForm.controls.email is FormControl ``` diff --git a/adev-ja/src/content/guide/forms/signals/custom-controls.en.md b/adev-ja/src/content/guide/forms/signals/custom-controls.en.md index 6492b3d55..163aee9bc 100644 --- a/adev-ja/src/content/guide/forms/signals/custom-controls.en.md +++ b/adev-ja/src/content/guide/forms/signals/custom-controls.en.md @@ -419,14 +419,16 @@ IMPORTANT: Don't implement validation logic in your control. Define validation r ```typescript // Avoid: Validation in control export class BadControl implements FormValueControl { - value = model('') - isValid() { return this.value().length >= 8 } // Don't do this! + value = model(''); + isValid() { + return this.value().length >= 8; + } // Don't do this! } // Good: Validation in schema, control displays results -accountForm = form(this.accountModel, schemaPath => { - minLength(schemaPath.password, 8, { message: 'Password must be at least 8 characters' }) -}) +accountForm = form(this.accountModel, (schemaPath) => { + minLength(schemaPath.password, 8, {message: 'Password must be at least 8 characters'}); +}); ``` ## Next steps diff --git a/adev-ja/src/content/guide/forms/signals/custom-controls.md b/adev-ja/src/content/guide/forms/signals/custom-controls.md index 419ef1420..49aebe6ac 100644 --- a/adev-ja/src/content/guide/forms/signals/custom-controls.md +++ b/adev-ja/src/content/guide/forms/signals/custom-controls.md @@ -419,14 +419,16 @@ IMPORTANT: コントロールにバリデーションロジックを実装しな ```typescript // 悪い例:コントロール内でのバリデーション export class BadControl implements FormValueControl { - value = model('') - isValid() { return this.value().length >= 8 } // これは行わないでください! + value = model(''); + isValid() { + return this.value().length >= 8; + } // これは行わないでください! } // 良い例:スキーマでバリデーションし、コントロールは結果を表示 -accountForm = form(this.accountModel, schemaPath => { - minLength(schemaPath.password, 8, { message: 'Password must be at least 8 characters' }) -}) +accountForm = form(this.accountModel, (schemaPath) => { + minLength(schemaPath.password, 8, {message: 'Password must be at least 8 characters'}); +}); ``` ## 次のステップ {#next-steps} diff --git a/adev-ja/src/content/guide/forms/signals/field-state-management.en.md b/adev-ja/src/content/guide/forms/signals/field-state-management.en.md index ad82aa290..093c60390 100644 --- a/adev-ja/src/content/guide/forms/signals/field-state-management.en.md +++ b/adev-ja/src/content/guide/forms/signals/field-state-management.en.md @@ -50,8 +50,8 @@ In this example, the template checks `registrationForm.email().invalid()` to det The most commonly used signal is `value()`, a [writable signal](guide/forms/signals/models#updating-models) that provides access to the field's current value: ```ts -const emailValue = registrationForm.email().value() -console.log(emailValue) // Current email string +const emailValue = registrationForm.email().value(); +console.log(emailValue); // Current email string ``` Beyond `value()`, field state includes signals for validation, interaction tracking, and availability control: @@ -410,18 +410,18 @@ When a child field becomes invalid, its parent field group becomes invalid, and const userModel = signal({ profile: { firstName: '', - lastName: '' + lastName: '', }, address: { street: '', - city: '' - } -}) + city: '', + }, +}); -const userForm = form(userModel) +const userForm = form(userModel); // If firstName is invalid, profile is invalid -userForm.profile.firstName().invalid() === true +userForm.profile.firstName().invalid() === true; // → userForm.profile().invalid() === true // → userForm().invalid() === true ``` @@ -434,12 +434,12 @@ Hidden, disabled, and readonly fields are non-interactive and don't affect paren const orderModel = signal({ customerName: '', requiresShipping: false, - shippingAddress: '' -}) + shippingAddress: '', +}); -const orderForm = form(orderModel, schemaPath => { - hidden(schemaPath.shippingAddress, ({valueOf}) => !valueOf(schemaPath.requiresShipping)) -}) +const orderForm = form(orderModel, (schemaPath) => { + hidden(schemaPath.shippingAddress, ({valueOf}) => !valueOf(schemaPath.requiresShipping)); +}); ``` In this example, when `shippingAddress` is hidden, it doesn't affect form validity. As a result, even if `shippingAddress` is empty and required, the form can be valid. @@ -535,26 +535,26 @@ export class Registration { registrationModel = signal({ username: '', email: '', - password: '' - }) + password: '', + }); - registrationForm = form(this.registrationModel) + registrationForm = form(this.registrationModel); async onSubmit() { // Wait for any pending async validation if (this.registrationForm().pending()) { - console.log('Waiting for validation...') - return + console.log('Waiting for validation...'); + return; } // Guard against invalid submissions if (this.registrationForm().invalid()) { - console.error('Form is invalid') - return + console.error('Form is invalid'); + return; } - const data = this.registrationModel() - await this.api.register(data) + const data = this.registrationModel(); + await this.api.register(data); } } ``` @@ -567,24 +567,24 @@ Create computed signals based on field state to automatically update when the un ```ts export class Password { - passwordModel = signal({ password: '', confirmPassword: '' }) - passwordForm = form(this.passwordModel) + passwordModel = signal({password: '', confirmPassword: ''}); + passwordForm = form(this.passwordModel); // Compute password strength indicator passwordStrength = computed(() => { - const password = this.passwordForm.password().value() - if (password.length < 8) return 'weak' - if (password.length < 12) return 'medium' - return 'strong' - }) + const password = this.passwordForm.password().value(); + if (password.length < 8) return 'weak'; + if (password.length < 12) return 'medium'; + return 'strong'; + }); // Check if all required fields are filled allFieldsFilled = computed(() => { return ( this.passwordForm.password().value().length > 0 && this.passwordForm.confirmPassword().value().length > 0 - ) - }) + ); + }); } ``` @@ -597,22 +597,22 @@ While field state typically updates through user interactions (typing, focusing, When a user submits a form, use the `submit()` function to handle validation and reveal errors: ```ts -import { Component, signal } from '@angular/core' -import { form, submit, required, email } from '@angular/forms/signals' +import {Component, signal} from '@angular/core'; +import {form, submit, required, email} from '@angular/forms/signals'; export class Registration { - registrationModel = signal({ username: '', email: '', password: '' }) + registrationModel = signal({username: '', email: '', password: ''}); - registrationForm = form(this.registrationModel, schemaPath => { - required(schemaPath.username) - email(schemaPath.email) - required(schemaPath.password) - }) + registrationForm = form(this.registrationModel, (schemaPath) => { + required(schemaPath.username); + email(schemaPath.email); + required(schemaPath.password); + }); onSubmit() { submit(this.registrationForm, async () => { - this.submitToServer() - }) + this.submitToServer(); + }); } submitToServer() { @@ -629,19 +629,19 @@ After successfully submitting a form, you may want to return it to its initial s ```ts export class Contact { - contactModel = signal({ name: '', email: '', message: '' }) - contactForm = form(this.contactModel) + contactModel = signal({name: '', email: '', message: ''}); + contactForm = form(this.contactModel); async onSubmit() { - if (!this.contactForm().valid()) return + if (!this.contactForm().valid()) return; - await this.api.sendMessage(this.contactModel()) + await this.api.sendMessage(this.contactModel()); // Clear interaction state (touched, dirty) - this.contactForm().reset() + this.contactForm().reset(); // Clear values - this.contactModel.set({ name: '', email: '', message: '' }) + this.contactModel.set({name: '', email: '', message: ''}); } } ``` diff --git a/adev-ja/src/content/guide/forms/signals/field-state-management.md b/adev-ja/src/content/guide/forms/signals/field-state-management.md index 647babbb7..908c28f8f 100644 --- a/adev-ja/src/content/guide/forms/signals/field-state-management.md +++ b/adev-ja/src/content/guide/forms/signals/field-state-management.md @@ -50,8 +50,8 @@ export class Registration { 最も一般的に使用されるシグナルは`value()`です。これはフィールドの現在の値へのアクセスを提供する[書き込み可能なシグナル](guide/forms/signals/models#updating-models)です: ```ts -const emailValue = registrationForm.email().value() -console.log(emailValue) // Current email string +const emailValue = registrationForm.email().value(); +console.log(emailValue); // Current email string ``` `value()`に加えて、フィールドの状態には、バリデーション、インタラクションの追跡、および可用性の制御のためのシグナルが含まれています: @@ -410,18 +410,18 @@ export class Login { const userModel = signal({ profile: { firstName: '', - lastName: '' + lastName: '', }, address: { street: '', - city: '' - } -}) + city: '', + }, +}); -const userForm = form(userModel) +const userForm = form(userModel); // firstNameが無効な場合、profileも無効になります -userForm.profile.firstName().invalid() === true +userForm.profile.firstName().invalid() === true; // → userForm.profile().invalid() === true // → userForm().invalid() === true ``` @@ -434,12 +434,12 @@ userForm.profile.firstName().invalid() === true const orderModel = signal({ customerName: '', requiresShipping: false, - shippingAddress: '' -}) + shippingAddress: '', +}); -const orderForm = form(orderModel, schemaPath => { - hidden(schemaPath.shippingAddress, ({valueOf}) => !valueOf(schemaPath.requiresShipping)) -}) +const orderForm = form(orderModel, (schemaPath) => { + hidden(schemaPath.shippingAddress, ({valueOf}) => !valueOf(schemaPath.requiresShipping)); +}); ``` この例では、`shippingAddress`が非表示の場合、フォームの有効性には影響しません。その結果、`shippingAddress`が空で必須であっても、フォームは有効になり得ます。 @@ -535,26 +535,26 @@ export class Registration { registrationModel = signal({ username: '', email: '', - password: '' - }) + password: '', + }); - registrationForm = form(this.registrationModel) + registrationForm = form(this.registrationModel); async onSubmit() { // Wait for any pending async validation if (this.registrationForm().pending()) { - console.log('Waiting for validation...') - return + console.log('Waiting for validation...'); + return; } // Guard against invalid submissions if (this.registrationForm().invalid()) { - console.error('Form is invalid') - return + console.error('Form is invalid'); + return; } - const data = this.registrationModel() - await this.api.register(data) + const data = this.registrationModel(); + await this.api.register(data); } } ``` @@ -567,24 +567,24 @@ export class Registration { ```ts export class Password { - passwordModel = signal({ password: '', confirmPassword: '' }) - passwordForm = form(this.passwordModel) + passwordModel = signal({password: '', confirmPassword: ''}); + passwordForm = form(this.passwordModel); // Compute password strength indicator passwordStrength = computed(() => { - const password = this.passwordForm.password().value() - if (password.length < 8) return 'weak' - if (password.length < 12) return 'medium' - return 'strong' - }) + const password = this.passwordForm.password().value(); + if (password.length < 8) return 'weak'; + if (password.length < 12) return 'medium'; + return 'strong'; + }); // Check if all required fields are filled allFieldsFilled = computed(() => { return ( this.passwordForm.password().value().length > 0 && this.passwordForm.confirmPassword().value().length > 0 - ) - }) + ); + }); } ``` @@ -597,22 +597,22 @@ export class Password { ユーザーがフォームを送信するときは、`submit()`関数を使用してバリデーションを処理し、エラーを表示します: ```ts -import { Component, signal } from '@angular/core' -import { form, submit, required, email } from '@angular/forms/signals' +import {Component, signal} from '@angular/core'; +import {form, submit, required, email} from '@angular/forms/signals'; export class Registration { - registrationModel = signal({ username: '', email: '', password: '' }) + registrationModel = signal({username: '', email: '', password: ''}); - registrationForm = form(this.registrationModel, schemaPath => { - required(schemaPath.username) - email(schemaPath.email) - required(schemaPath.password) - }) + registrationForm = form(this.registrationModel, (schemaPath) => { + required(schemaPath.username); + email(schemaPath.email); + required(schemaPath.password); + }); onSubmit() { submit(this.registrationForm, async () => { - this.submitToServer() - }) + this.submitToServer(); + }); } submitToServer() { @@ -629,19 +629,19 @@ export class Registration { ```ts export class Contact { - contactModel = signal({ name: '', email: '', message: '' }) - contactForm = form(this.contactModel) + contactModel = signal({name: '', email: '', message: ''}); + contactForm = form(this.contactModel); async onSubmit() { - if (!this.contactForm().valid()) return + if (!this.contactForm().valid()) return; - await this.api.sendMessage(this.contactModel()) + await this.api.sendMessage(this.contactModel()); // Clear interaction state (touched, dirty) - this.contactForm().reset() + this.contactForm().reset(); // Clear values - this.contactModel.set({ name: '', email: '', message: '' }) + this.contactModel.set({name: '', email: '', message: ''}); } } ``` diff --git a/adev-ja/src/content/guide/forms/signals/models.en.md b/adev-ja/src/content/guide/forms/signals/models.en.md index 75222860c..45580feec 100644 --- a/adev-ja/src/content/guide/forms/signals/models.en.md +++ b/adev-ja/src/content/guide/forms/signals/models.en.md @@ -46,17 +46,17 @@ While TypeScript infers types from object literals, defining explicit types impr ```ts interface LoginData { - email: string - password: string + email: string; + password: string; } export class LoginComponent { loginModel = signal({ email: '', - password: '' - }) + password: '', + }); - loginForm = form(this.loginModel) + loginForm = form(this.loginModel); } ``` @@ -64,10 +64,10 @@ With explicit types, the field tree provides full type safety. Accessing `loginF ```ts // TypeScript knows this is FieldTree -const emailField = loginForm.email +const emailField = loginForm.email; // TypeScript error: Property 'username' does not exist -const usernameField = loginForm.username +const usernameField = loginForm.username; ``` ### Initializing all fields @@ -79,31 +79,31 @@ Form models should provide initial values for all fields you want to include in const userModel = signal({ name: '', email: '', - age: 0 -}) + age: 0, +}); // Avoid: Missing initial value const userModel = signal({ name: '', - email: '' + email: '', // age field is not defined - cannot access userForm.age -}) +}); ``` For optional fields, explicitly set them to `null` or an empty value: ```ts interface UserData { - name: string - email: string - phoneNumber: string | null + name: string; + email: string; + phoneNumber: string | null; } const userModel = signal({ name: '', email: '', - phoneNumber: null -}) + phoneNumber: null, +}); ``` Fields set to `undefined` are excluded from the field tree. A model with `{value: undefined}` behaves identically to `{}` - accessing the field returns `undefined` rather than a `FieldTree`. @@ -184,7 +184,7 @@ resetForm() { This approach works well when loading data from an API or resetting the entire form. -### Update a single field directly with `set()` +### Update a single field directly with `set()` or `update()` Use `set()` on individual field values to directly update the field state: @@ -194,8 +194,7 @@ clearEmail() { } incrementAge() { - const currentAge = this.userForm.age().value(); - this.userForm.age().value.set(currentAge + 1); + this.userForm.age().value.update(currentAge => currentAge + 1); } ``` @@ -210,19 +209,19 @@ export class UserProfileComponent { userModel = signal({ name: '', email: '', - bio: '' - }) + bio: '', + }); - userForm = form(this.userModel) - private userService = inject(UserService) + userForm = form(this.userModel); + private userService = inject(UserService); ngOnInit() { - this.loadUserProfile() + this.loadUserProfile(); } async loadUserProfile() { - const userData = await this.userService.getUserProfile() - this.userModel.set(userData) + const userData = await this.userService.getUserProfile(); + this.userModel.set(userData); } } ``` @@ -292,8 +291,8 @@ const userModel = signal({ street: '', city: '', state: '', - zip: '' -}) + zip: '', +}); ``` Nested models group related fields: @@ -307,9 +306,9 @@ const userModel = signal({ street: '', city: '', state: '', - zip: '' - } -}) + zip: '', + }, +}); ``` **Use flat structures when:** @@ -332,19 +331,19 @@ You can access nested fields by following the object path: const userModel = signal({ profile: { firstName: '', - lastName: '' + lastName: '', }, settings: { theme: 'light', - notifications: true - } -}) + notifications: true, + }, +}); -const userForm = form(userModel) +const userForm = form(userModel); // Access nested fields -userForm.profile.firstName // FieldTree -userForm.settings.theme // FieldTree +userForm.profile.firstName; // FieldTree +userForm.settings.theme; // FieldTree ``` In templates, you bind nested fields the same way as top-level fields: @@ -370,14 +369,14 @@ Models can include arrays for collections of items: ```ts const orderModel = signal({ customerName: '', - items: [{ product: '', quantity: 0, price: 0 }] -}) + items: [{product: '', quantity: 0, price: 0}], +}); -const orderForm = form(orderModel) +const orderForm = form(orderModel); // Access array items by index -orderForm.items[0].product // FieldTree -orderForm.items[0].quantity // FieldTree +orderForm.items[0].product; // FieldTree +orderForm.items[0].quantity; // FieldTree ``` Array items containing objects automatically receive tracking identities, which helps maintain field state even when items change position in the array. This ensures validation state and user interactions persist correctly when arrays are reordered. diff --git a/adev-ja/src/content/guide/forms/signals/models.md b/adev-ja/src/content/guide/forms/signals/models.md index 78154a4d6..129fbcce8 100644 --- a/adev-ja/src/content/guide/forms/signals/models.md +++ b/adev-ja/src/content/guide/forms/signals/models.md @@ -46,17 +46,17 @@ TypeScriptはオブジェクトリテラルから型を推論しますが、明 ```ts interface LoginData { - email: string - password: string + email: string; + password: string; } export class LoginComponent { loginModel = signal({ email: '', - password: '' - }) + password: '', + }); - loginForm = form(this.loginModel) + loginForm = form(this.loginModel); } ``` @@ -64,10 +64,10 @@ export class LoginComponent { ```ts // TypeScript knows this is FieldTree -const emailField = loginForm.email +const emailField = loginForm.email; // TypeScript error: Property 'username' does not exist -const usernameField = loginForm.username +const usernameField = loginForm.username; ``` ### すべてのフィールドを初期化する {#initializing-all-fields} @@ -79,31 +79,31 @@ const usernameField = loginForm.username const userModel = signal({ name: '', email: '', - age: 0 -}) + age: 0, +}); // Avoid: Missing initial value const userModel = signal({ name: '', - email: '' + email: '', // age field is not defined - cannot access userForm.age -}) +}); ``` オプショナルなフィールドについては、明示的に`null`または空の値を設定してください: ```ts interface UserData { - name: string - email: string - phoneNumber: string | null + name: string; + email: string; + phoneNumber: string | null; } const userModel = signal({ name: '', email: '', - phoneNumber: null -}) + phoneNumber: null, +}); ``` `undefined`に設定されたフィールドは、フィールドツリーから除外されます。`{value: undefined}`を持つモデルは`{}`と全く同じように動作し、そのフィールドにアクセスすると`FieldTree`ではなく`undefined`が返されます。 @@ -142,12 +142,12 @@ onSubmit() { ` }) export class LoginComponent { - loginModel = signal({ email: '', password: '' }) - loginForm = form(this.loginModel) + loginModel = signal({email: '', password: ''}); + loginForm = form(this.loginModel); passwordLength = computed(() => { - return this.loginForm.password().value().length - }) + return this.loginForm.password().value().length; + }); } ``` @@ -184,7 +184,7 @@ resetForm() { このアプローチは、APIからデータを読み込む場合や、フォーム全体をリセットする場合に適しています。 -### `set()`で単一のフィールドを直接更新する {#update-a-single-field-directly-with-set} +### `set()`または`update()`で単一のフィールドを直接更新する {#update-a-single-field-directly-with-set} 個々のフィールドの値に`set()`を使用して、フィールドの状態を直接更新します: @@ -194,8 +194,7 @@ clearEmail() { } incrementAge() { - const currentAge = this.userForm.age().value(); - this.userForm.age().value.set(currentAge + 1); + this.userForm.age().value.update(currentAge => currentAge + 1); } ``` @@ -210,19 +209,19 @@ export class UserProfileComponent { userModel = signal({ name: '', email: '', - bio: '' - }) + bio: '', + }); - userForm = form(this.userModel) - private userService = inject(UserService) + userForm = form(this.userModel); + private userService = inject(UserService); ngOnInit() { - this.loadUserProfile() + this.loadUserProfile(); } async loadUserProfile() { - const userData = await this.userService.getUserProfile() - this.userModel.set(userData) + const userData = await this.userService.getUserProfile(); + this.userModel.set(userData); } } ``` @@ -264,8 +263,8 @@ export class UserProfileComponent { ` }) export class UserComponent { - userModel = signal({ name: '' }) - userForm = form(this.userModel) + userModel = signal({name: ''}); + userForm = form(this.userModel); setName(name: string) { this.userForm.name().value.set(name); @@ -292,8 +291,8 @@ const userModel = signal({ street: '', city: '', state: '', - zip: '' -}) + zip: '', +}); ``` ネストされたモデルは、関連するフィールドをグループ化します: @@ -307,9 +306,9 @@ const userModel = signal({ street: '', city: '', state: '', - zip: '' - } -}) + zip: '', + }, +}); ``` **次のような場合は、フラットな構造を使用します:** @@ -332,19 +331,19 @@ const userModel = signal({ const userModel = signal({ profile: { firstName: '', - lastName: '' + lastName: '', }, settings: { theme: 'light', - notifications: true - } -}) + notifications: true, + }, +}); -const userForm = form(userModel) +const userForm = form(userModel); // Access nested fields -userForm.profile.firstName // FieldTree -userForm.settings.theme // FieldTree +userForm.profile.firstName; // FieldTree +userForm.settings.theme; // FieldTree ``` テンプレートでは、トップレベルのフィールドと同じ方法でネストされたフィールドをバインドします: @@ -370,14 +369,14 @@ userForm.settings.theme // FieldTree ```ts const orderModel = signal({ customerName: '', - items: [{ product: '', quantity: 0, price: 0 }] -}) + items: [{product: '', quantity: 0, price: 0}], +}); -const orderForm = form(orderModel) +const orderForm = form(orderModel); // Access array items by index -orderForm.items[0].product // FieldTree -orderForm.items[0].quantity // FieldTree +orderForm.items[0].product; // FieldTree +orderForm.items[0].quantity; // FieldTree ``` オブジェクトを含む配列のアイテムは自動的に追跡IDを受け取ります。これにより、配列内でアイテムの位置が変わってもフィールドの状態を維持できます。これにより、配列が並べ替えられた場合でも、バリデーションの状態とユーザーインタラクションが正しく維持されることが保証されます。 diff --git a/adev-ja/src/content/guide/forms/signals/overview.en.md b/adev-ja/src/content/guide/forms/signals/overview.en.md index 924c6e651..c9fd61f1b 100644 --- a/adev-ja/src/content/guide/forms/signals/overview.en.md +++ b/adev-ja/src/content/guide/forms/signals/overview.en.md @@ -19,8 +19,7 @@ Signal Forms address these challenges by: Signal Forms work best in new applications built with signals. If you're working with an existing application that uses reactive forms, or if you need production stability guarantees, reactive forms remain a solid choice. - - +NOTE: If you're coming from template or reactive forms, you may be interested in the [comparison guide](guide/forms/signals/comparison). ## Prerequisites @@ -33,7 +32,7 @@ Signal Forms require: Signal Forms are already included in the `@angular/forms` package. Import the necessary functions and directives from `@angular/forms/signals`: ```ts -import { form, Field, required, email } from '@angular/forms/signals' +import {form, Field, required, email} from '@angular/forms/signals'; ``` The `Field` directive must be imported into any component that binds form fields to HTML inputs: @@ -49,12 +48,12 @@ The `Field` directive must be imported into any component that binds form fields To learn more about how Signal Forms work, check out the following guides: - + - + diff --git a/adev-ja/src/content/guide/forms/signals/overview.md b/adev-ja/src/content/guide/forms/signals/overview.md index 005d9a6af..83a6ed63e 100644 --- a/adev-ja/src/content/guide/forms/signals/overview.md +++ b/adev-ja/src/content/guide/forms/signals/overview.md @@ -19,8 +19,7 @@ Webアプリケーションにおけるフォームの構築には、フィー シグナルフォームは、シグナルを使用して構築された新しいアプリケーションで最適に機能します。リアクティブフォームを使用している既存のアプリケーションで作業している場合や、本番環境での安定性の保証が必要な場合は、リアクティブフォームが依然として確実な選択肢です。 - - +NOTE: テンプレート駆動フォームやリアクティブフォームから移行する場合は、[比較ガイド](guide/forms/signals/comparison)をご覧ください。 ## 前提条件 {#prerequisites} @@ -33,7 +32,7 @@ Webアプリケーションにおけるフォームの構築には、フィー シグナルフォームはすでに`@angular/forms`パッケージに含まれています。`@angular/forms/signals`から必要な関数とディレクティブをインポートします: ```ts -import { form, Field, required, email } from '@angular/forms/signals' +import {form, Field, required, email} from '@angular/forms/signals'; ``` `Field`ディレクティブは、フォームフィールドをHTML入力にバインドするすべてのコンポーネントにインポートする必要があります: @@ -49,12 +48,12 @@ import { form, Field, required, email } from '@angular/forms/signals' シグナルフォームの仕組みについて詳しく知るには、以下のガイドを確認してください。 - + - + diff --git a/adev-ja/src/content/guide/forms/signals/validation.en.md b/adev-ja/src/content/guide/forms/signals/validation.en.md index 38d12c76b..b1e0aa26c 100644 --- a/adev-ja/src/content/guide/forms/signals/validation.en.md +++ b/adev-ja/src/content/guide/forms/signals/validation.en.md @@ -110,9 +110,9 @@ For conditional requirements, use the `when` option: registrationForm = form(this.registrationModel, (schemaPath) => { required(schemaPath.promoCode, { message: 'Promo code is required for discounts', - when: ({valueOf}) => valueOf(schemaPath.applyDiscount) - }) -}) + when: ({valueOf}) => valueOf(schemaPath.applyDiscount), + }); +}); ``` The validation rule only runs when the `when` function returns `true`. @@ -194,9 +194,9 @@ You can use computed values for dynamic constraints: ```ts ageForm = form(this.ageModel, (schemaPath) => { min(schemaPath.participants, () => this.minimumRequired(), { - message: 'Not enough participants' - }) -}) + message: 'Not enough participants', + }); +}); ``` ### minLength() and maxLength() @@ -298,10 +298,10 @@ Common patterns: Forms can include arrays of nested objects (for example, a list of order items). To apply validation rules to each item in an array, use `applyEach()` inside your schema function. `applyEach()` iterates the array path and supplies a path for each item where you can apply validators just like top-level fields. ```ts -import { Component, signal } from '@angular/core' -import { applyEach, Field, form, min, required, SchemaPathTree } from '@angular/forms/signals'; +import {Component, signal} from '@angular/core'; +import {applyEach, Field, form, min, required, SchemaPathTree} from '@angular/forms/signals'; -type Item = { name: string; quantity: number } +type Item = {name: string; quantity: number}; interface Order { title: string; @@ -310,8 +310,8 @@ interface Order { } function ItemSchema(item: SchemaPathTree) { - required(item.name, { message: 'Item name is required' }) - min(item.quantity, 1, { message: 'Quantity must be at least 1' }) + required(item.name, {message: 'Item name is required'}); + min(item.quantity, 1, {message: 'Quantity must be at least 1'}); } @Component(/* ... */) @@ -319,17 +319,15 @@ export class OrderComponent { orderModel = signal({ title: '', description: '', - items: [ - { name: '', quantity: 0 }, - ] - }) + items: [{name: '', quantity: 0}], + }); orderForm = form(this.orderModel, (schemaPath) => { - required(schemaPath.title) - required(schemaPath.description) + required(schemaPath.title); + required(schemaPath.description); - applyEach(schemaPath.items, ItemSchema) - }) + applyEach(schemaPath.items, ItemSchema); + }); } ``` @@ -406,10 +404,10 @@ When a field has multiple validation rules, each validation rule runs independen ```ts signupForm = form(this.signupModel, (schemaPath) => { - required(schemaPath.email, { message: 'Email is required' }) - email(schemaPath.email, { message: 'Enter a valid email address' }) - minLength(schemaPath.email, 5, { message: 'Email is too short' }) -}) + required(schemaPath.email, {message: 'Email is required'}); + email(schemaPath.email, {message: 'Enter a valid email address'}); + minLength(schemaPath.email, 5, {message: 'Email is too short'}); +}); ``` If the email field is empty, only the `required()` error appears. If the user types "a@b", both `email()` and `minLength()` errors appear. All validation rules run - validation doesn't stop after the first failure. @@ -484,33 +482,33 @@ Return an error object with `kind` and `message` when validation fails. Return ` Create reusable validation rule functions by wrapping `validate()`: ```ts -function url(field: any, options?: { message?: string }) { +function url(field: any, options?: {message?: string}) { validate(field, ({value}) => { try { - new URL(value()) - return null + new URL(value()); + return null; } catch { return { kind: 'url', - message: options?.message || 'Enter a valid URL' - } + message: options?.message || 'Enter a valid URL', + }; } - }) + }); } -function phoneNumber(field: any, options?: { message?: string }) { +function phoneNumber(field: any, options?: {message?: string}) { validate(field, ({value}) => { - const phoneRegex = /^\d{3}-\d{3}-\d{4}$/ + const phoneRegex = /^\d{3}-\d{3}-\d{4}$/; if (!phoneRegex.test(value())) { return { kind: 'phoneNumber', - message: options?.message || 'Phone must be in format: 555-123-4567' - } + message: options?.message || 'Phone must be in format: 555-123-4567', + }; } - return null - }) + return null; + }); } ``` @@ -518,9 +516,9 @@ You can use custom validation rules just like built-in validation rules: ```ts urlForm = form(this.urlModel, (schemaPath) => { - url(schemaPath.website, { message: 'Please enter a valid website URL' }) - phoneNumber(schemaPath.phone) -}) + url(schemaPath.website, {message: 'Please enter a valid website URL'}); + phoneNumber(schemaPath.phone); +}); ``` ## Cross-field validation @@ -652,7 +650,7 @@ The `validateHttp()` validation rule: While async validation runs, the field's `pending()` signal returns `true`. Use this to show loading indicators: -```ts +```angular-html @if (form.username().pending()) { Checking... } @@ -660,6 +658,26 @@ While async validation runs, the field's `pending()` signal returns `true`. Use The `valid()` signal returns `false` while validation is pending, even if there are no errors yet. The `invalid()` signal only returns `true` if errors exist. +## Integration with schema validation libraries + +Signal Forms have built-in support for libraries that conform to [Standard Schema](https://standardschema.dev/) like [Zod](https://zod.dev/) or [Valibot](https://valibot.dev/). The integration is provided via the `validateStandardSchema` function. This allows you to use existing schemas while maintaining Signal Forms' reactive validation benefits. + +```ts +import {form, validateStandardSchema} from '@angular/forms/signals'; +import * as z from 'zod'; + +// Define your schema +const userSchema = z.object({ + email: z.email(), + password: z.string().min(8), +}); + +// Use with Signal Forms +const userForm = form(signal({email: '', password: ''}), (schemaPath) => { + validateStandardSchema(schemaPath, userSchema); +}); +``` + ## Next steps This guide covered creating and applying validation rules. Related guides explore other aspects of Signal Forms: diff --git a/adev-ja/src/content/guide/forms/signals/validation.md b/adev-ja/src/content/guide/forms/signals/validation.md index 3e4f977de..1f48d91da 100644 --- a/adev-ja/src/content/guide/forms/signals/validation.md +++ b/adev-ja/src/content/guide/forms/signals/validation.md @@ -110,12 +110,12 @@ export class RegistrationComponent { registrationForm = form(this.registrationModel, (schemaPath) => { required(schemaPath.promoCode, { message: 'Promo code is required for discounts', - when: ({valueOf}) => valueOf(schemaPath.applyDiscount) - }) -}) + when: ({valueOf}) => valueOf(schemaPath.applyDiscount), + }); +}); ``` -このバリデーションルールは、`when`関数が`true`を返す場合にのみ実行されます。 +バリデーションルールは、`when`関数が`true`を返す場合にのみ実行されます。 ### email() {#email} @@ -194,9 +194,9 @@ export class AgeFormComponent { ```ts ageForm = form(this.ageModel, (schemaPath) => { min(schemaPath.participants, () => this.minimumRequired(), { - message: 'Not enough participants' - }) -}) + message: 'Not enough participants', + }); +}); ``` ### minLength()とmaxLength() {#minlength-and-maxlength} @@ -298,10 +298,10 @@ export class PhoneFormComponent { フォームには、ネストされたオブジェクトの配列を含めることができます(例: 注文アイテムのリスト)。配列内の各アイテムにバリデーションルールを適用するには、スキーマ関数内で`applyEach()`を使用します。`applyEach()`は配列パスを反復処理し、各アイテムのパスを提供します。このパスでは、トップレベルのフィールドと同様にバリデーターを適用できます。 ```ts -import { Component, signal } from '@angular/core' -import { applyEach, Field, form, min, required, SchemaPathTree } from '@angular/forms/signals'; +import {Component, signal} from '@angular/core'; +import {applyEach, Field, form, min, required, SchemaPathTree} from '@angular/forms/signals'; -type Item = { name: string; quantity: number } +type Item = {name: string; quantity: number}; interface Order { title: string; @@ -310,8 +310,8 @@ interface Order { } function ItemSchema(item: SchemaPathTree) { - required(item.name, { message: 'Item name is required' }) - min(item.quantity, 1, { message: 'Quantity must be at least 1' }) + required(item.name, {message: 'Item name is required'}); + min(item.quantity, 1, {message: 'Quantity must be at least 1'}); } @Component(/* ... */) @@ -319,17 +319,15 @@ export class OrderComponent { orderModel = signal({ title: '', description: '', - items: [ - { name: '', quantity: 0 }, - ] - }) + items: [{name: '', quantity: 0}], + }); orderForm = form(this.orderModel, (schemaPath) => { - required(schemaPath.title) - required(schemaPath.description) + required(schemaPath.title); + required(schemaPath.description); - applyEach(schemaPath.items, ItemSchema) - }) + applyEach(schemaPath.items, ItemSchema); + }); } ``` @@ -406,10 +404,10 @@ export class SignupComponent { ```ts signupForm = form(this.signupModel, (schemaPath) => { - required(schemaPath.email, { message: 'Email is required' }) - email(schemaPath.email, { message: 'Enter a valid email address' }) - minLength(schemaPath.email, 5, { message: 'Email is too short' }) -}) + required(schemaPath.email, {message: 'Email is required'}); + email(schemaPath.email, {message: 'Enter a valid email address'}); + minLength(schemaPath.email, 5, {message: 'Email is too short'}); +}); ``` emailフィールドが空の場合、`required()`エラーのみが表示されます。ユーザーが"a@b"と入力すると、`email()`と`minLength()`の両方のエラーが表示されます。すべてのバリデーションルールが実行され、最初の失敗でバリデーションが停止することはありません。 @@ -484,33 +482,33 @@ NOTE: 子フィールドには`key`シグナルもあり、配列アイテムの `validate()`をラップして、再利用可能なバリデーションルール関数を作成します: ```ts -function url(field: any, options?: { message?: string }) { +function url(field: any, options?: {message?: string}) { validate(field, ({value}) => { try { - new URL(value()) - return null + new URL(value()); + return null; } catch { return { kind: 'url', - message: options?.message || 'Enter a valid URL' - } + message: options?.message || 'Enter a valid URL', + }; } - }) + }); } -function phoneNumber(field: any, options?: { message?: string }) { +function phoneNumber(field: any, options?: {message?: string}) { validate(field, ({value}) => { - const phoneRegex = /^\d{3}-\d{3}-\d{4}$/ + const phoneRegex = /^\d{3}-\d{3}-\d{4}$/; if (!phoneRegex.test(value())) { return { kind: 'phoneNumber', - message: options?.message || 'Phone must be in format: 555-123-4567' - } + message: options?.message || 'Phone must be in format: 555-123-4567', + }; } - return null - }) + return null; + }); } ``` @@ -518,9 +516,9 @@ function phoneNumber(field: any, options?: { message?: string }) { ```ts urlForm = form(this.urlModel, (schemaPath) => { - url(schemaPath.website, { message: 'Please enter a valid website URL' }) - phoneNumber(schemaPath.phone) -}) + url(schemaPath.website, {message: 'Please enter a valid website URL'}); + phoneNumber(schemaPath.phone); +}); ``` ## クロスフィールドバリデーション {#cross-field-validation} @@ -652,7 +650,7 @@ export class UsernameFormComponent { 非同期バリデーションの実行中、フィールドの`pending()`シグナルは`true`を返します。これを使用してローディングインジケーターを表示します: -```ts +```angular-html @if (form.username().pending()) { Checking... } @@ -660,6 +658,26 @@ export class UsernameFormComponent { `valid()`シグナルは、まだエラーがない場合でも、バリデーションがペンディング中の間は`false`を返します。`invalid()`シグナルは、エラーが存在する場合にのみ`true`を返します。 +## スキーマバリデーションライブラリとの統合 + +シグナルフォームは、[Zod](https://zod.dev/)や[Valibot](https://valibot.dev/)のような[Standard Schema](https://standardschema.dev/)に準拠したライブラリに対する組み込みサポートを提供しています。統合は`validateStandardSchema`関数によって提供されます。これにより、シグナルフォームのリアクティブなバリデーションの利点を維持しながら、既存のスキーマを使用できます。 + +```ts +import {form, validateStandardSchema} from '@angular/forms/signals'; +import * as z from 'zod'; + +// スキーマを定義 +const userSchema = z.object({ + email: z.email(), + password: z.string().min(8), +}); + +// シグナルフォームで使用 +const userForm = form(signal({email: '', password: ''}), (schemaPath) => { + validateStandardSchema(schemaPath, userSchema); +}); +``` + ## 次のステップ {#next-steps} このガイドでは、バリデーションルールの作成と適用について説明しました。関連ガイドでは、シグナルフォームの他の側面について解説します: diff --git a/adev-ja/src/content/guide/forms/template-driven-forms.en.md b/adev-ja/src/content/guide/forms/template-driven-forms.en.md index 0392ac395..b957ad0c8 100644 --- a/adev-ja/src/content/guide/forms/template-driven-forms.en.md +++ b/adev-ja/src/content/guide/forms/template-driven-forms.en.md @@ -197,8 +197,7 @@ To see how the classes are added and removed by the framework, open the browser' 1. When you first bring it up, the classes indicate that it has a valid value, that the value has not been changed since initialization or reset, and that the control has not been visited since initialization or reset. ```html - - ; + ; ``` 1. Take the following actions on the **Name** `` box, and observe which classes appear. diff --git a/adev-ja/src/content/guide/forms/template-driven-forms.md b/adev-ja/src/content/guide/forms/template-driven-forms.md index 20e0217e3..d293c46d1 100644 --- a/adev-ja/src/content/guide/forms/template-driven-forms.md +++ b/adev-ja/src/content/guide/forms/template-driven-forms.md @@ -197,8 +197,7 @@ Angularは、`form`要素に`ng-submitted`クラスを適用しますが、 1. 最初に表示すると、クラスは、値が有効であり、初期化またはリセット以降値が変更されていないこと、初期化またはリセット以降コントロールが訪問されていないことを示しています。 ```html - - ; + ; ``` 1. **Name** ``ボックスで次のように操作し、表示されるクラスを観察します。 diff --git a/adev-ja/src/content/guide/forms/typed-forms.en.md b/adev-ja/src/content/guide/forms/typed-forms.en.md index 2ad3b69dc..896757d1d 100644 --- a/adev-ja/src/content/guide/forms/typed-forms.en.md +++ b/adev-ja/src/content/guide/forms/typed-forms.en.md @@ -86,7 +86,7 @@ email.setValue('angularrox@gmail.com'); // Error! To prevent this, we explicitly specify the type as `string|null`: ```ts -const email = new FormControl(null); +const email = new FormControl(null); email.setValue('angularrox@gmail.com'); ``` @@ -128,8 +128,8 @@ Consider again a login form: ```ts const login = new FormGroup({ - email: new FormControl('', {nonNullable: true}), - password: new FormControl('', {nonNullable: true}), + email: new FormControl('', {nonNullable: true}), + password: new FormControl('', {nonNullable: true}), }); ``` @@ -166,7 +166,7 @@ In this form, we explicitly specify the type, which allows us to make the `passw Some `FormGroup` usages do not fit the above pattern because the keys are not known ahead of time. The `FormRecord` class is designed for that case: ```ts -const addresses = new FormRecord>({}); +const addresses = new FormRecord>({}); addresses.addControl('Andrew', new FormControl('2340 Folsom St')); ``` diff --git a/adev-ja/src/content/guide/forms/typed-forms.md b/adev-ja/src/content/guide/forms/typed-forms.md index d007fc837..c455aefb6 100644 --- a/adev-ja/src/content/guide/forms/typed-forms.md +++ b/adev-ja/src/content/guide/forms/typed-forms.md @@ -86,7 +86,7 @@ email.setValue('angularrox@gmail.com'); // エラー! これを防ぐために、型を `string|null` として明示的に指定します。 ```ts -const email = new FormControl(null); +const email = new FormControl(null); email.setValue('angularrox@gmail.com'); ``` @@ -128,8 +128,8 @@ Angularは、列挙されたキーセットを持つフォームに `FormGroup` ```ts const login = new FormGroup({ - email: new FormControl('', {nonNullable: true}), - password: new FormControl('', {nonNullable: true}), + email: new FormControl('', {nonNullable: true}), + password: new FormControl('', {nonNullable: true}), }); ``` @@ -166,7 +166,7 @@ login.removeControl('password'); 一部の `FormGroup` の使用方法は、上記の例に当てはまりません。なぜなら、キーが事前にわからないからです。`FormRecord` クラスは、そのケース用に設計されています。 ```ts -const addresses = new FormRecord>({}); +const addresses = new FormRecord>({}); addresses.addControl('Andrew', new FormControl('2340 Folsom St')); ``` From 3051050c6224160e9628a217d4a53c786fe43718 Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 11:12:54 +0900 Subject: [PATCH 04/32] docs(ai): migrate .en.md changes --- adev-ja/src/content/ai/design-patterns.en.md | 70 ++++++----- adev-ja/src/content/ai/design-patterns.md | 70 ++++++----- adev-ja/src/content/ai/mcp-server-setup.en.md | 18 +-- adev-ja/src/content/ai/mcp-server-setup.md | 18 +-- .../routing/route-transition-animations.md | 25 ++-- .../content/guide/routing/router-reference.md | 4 +- .../content/guide/routing/router-tutorial.md | 6 +- .../guide/routing/show-routes-with-outlets.md | 18 +-- adev-ja/src/content/guide/routing/testing.md | 115 ++++++++---------- adev-ja/src/content/guide/security.md | 24 ++-- 10 files changed, 180 insertions(+), 188 deletions(-) diff --git a/adev-ja/src/content/ai/design-patterns.en.md b/adev-ja/src/content/ai/design-patterns.en.md index cdcd7530d..d40582e24 100644 --- a/adev-ja/src/content/ai/design-patterns.en.md +++ b/adev-ja/src/content/ai/design-patterns.en.md @@ -27,11 +27,14 @@ storyResource = resource({ loader: ({params}): Promise => { // The params value is the current value of the storyInput signal const url = this.endpoint(); - return runFlow({ url, input: { - userInput: params, - sessionId: this.storyService.sessionId() // Read from another signal - }}); - } + return runFlow({ + url, + input: { + userInput: params, + sessionId: this.storyService.sessionId(), // Read from another signal + }, + }); + }, }); ``` @@ -56,7 +59,7 @@ storyParts = linkedSignal({ const existingStoryParts = previous?.value || []; // Return a new array with the old and new parts return [...existingStoryParts, ...newStoryParts]; - } + }, }); ``` @@ -101,23 +104,23 @@ characters = resource({ // exposed by the Genkit client SDK const response = streamFlow({ url: '/streamCharacters', - input: 10 + input: 10, }); (async () => { for await (const chunk of response.stream) { data.update((prev) => { if ('value' in prev) { - return { value: `${prev.value} ${chunk}` }; + return {value: `${prev.value} ${chunk}`}; } else { - return { error: chunk as unknown as Error }; + return {error: chunk as unknown as Error}; } }); } })(); return data; - } + }, }); ``` @@ -136,38 +139,39 @@ The `characters` member is updated asynchronously and can be displayed in the te On the server side, in `server.ts` for example, the defined endpoint sends the data to be streamed to the client. The following code uses Gemini with the Genkit framework but this technique is applicable to other APIs that support streaming responses from LLMs: ```ts -import { startFlowServer } from '@genkit-ai/express'; -import { genkit } from "genkit/beta"; -import { googleAI, gemini20Flash } from "@genkit-ai/googleai"; +import {startFlowServer} from '@genkit-ai/express'; +import {genkit} from 'genkit/beta'; +import {googleAI, gemini20Flash} from '@genkit-ai/googleai'; -const ai = genkit({ plugins: [googleAI()] }); +const ai = genkit({plugins: [googleAI()]}); -export const streamCharacters = ai.defineFlow({ +export const streamCharacters = ai.defineFlow( + { name: 'streamCharacters', inputSchema: z.number(), outputSchema: z.string(), streamSchema: z.string(), }, - async (count, { sendChunk }) => { - const { response, stream } = ai.generateStream({ - model: gemini20Flash, - config: { - temperature: 1, - }, - prompt: `Generate ${count} different RPG game characters.`, - }); - - (async () => { - for await (const chunk of stream) { - sendChunk(chunk.content[0].text!); - } - })(); - - return (await response).text; -}); + async (count, {sendChunk}) => { + const {response, stream} = ai.generateStream({ + model: gemini20Flash, + config: { + temperature: 1, + }, + prompt: `Generate ${count} different RPG game characters.`, + }); + + (async () => { + for await (const chunk of stream) { + sendChunk(chunk.content[0].text!); + } + })(); + + return (await response).text; + }, +); startFlowServer({ flows: [streamCharacters], }); - ``` diff --git a/adev-ja/src/content/ai/design-patterns.md b/adev-ja/src/content/ai/design-patterns.md index 0f2e3b349..bc1186048 100644 --- a/adev-ja/src/content/ai/design-patterns.md +++ b/adev-ja/src/content/ai/design-patterns.md @@ -27,11 +27,14 @@ storyResource = resource({ loader: ({params}): Promise => { // The params value is the current value of the storyInput signal const url = this.endpoint(); - return runFlow({ url, input: { - userInput: params, - sessionId: this.storyService.sessionId() // Read from another signal - }}); - } + return runFlow({ + url, + input: { + userInput: params, + sessionId: this.storyService.sessionId(), // Read from another signal + }, + }); + }, }); ``` @@ -56,7 +59,7 @@ storyParts = linkedSignal({ const existingStoryParts = previous?.value || []; // Return a new array with the old and new parts return [...existingStoryParts, ...newStoryParts]; - } + }, }); ``` @@ -101,23 +104,23 @@ characters = resource({ // exposed by the Genkit client SDK const response = streamFlow({ url: '/streamCharacters', - input: 10 + input: 10, }); (async () => { for await (const chunk of response.stream) { data.update((prev) => { if ('value' in prev) { - return { value: `${prev.value} ${chunk}` }; + return {value: `${prev.value} ${chunk}`}; } else { - return { error: chunk as unknown as Error }; + return {error: chunk as unknown as Error}; } }); } })(); return data; - } + }, }); ``` @@ -136,38 +139,39 @@ characters = resource({ サーバー側では、例えば`server.ts`で、定義されたエンドポイントがストリーミングされるデータをクライアントに送信します。以下のコードはGenkitフレームワークでGeminiを使用していますが、この手法はLLMからのストリーミング応答をサポートする他のAPIにも適用できます。 ```ts -import { startFlowServer } from '@genkit-ai/express'; -import { genkit } from "genkit/beta"; -import { googleAI, gemini20Flash } from "@genkit-ai/googleai"; +import {startFlowServer} from '@genkit-ai/express'; +import {genkit} from 'genkit/beta'; +import {googleAI, gemini20Flash} from '@genkit-ai/googleai'; -const ai = genkit({ plugins: [googleAI()] }); +const ai = genkit({plugins: [googleAI()]}); -export const streamCharacters = ai.defineFlow({ +export const streamCharacters = ai.defineFlow( + { name: 'streamCharacters', inputSchema: z.number(), outputSchema: z.string(), streamSchema: z.string(), }, - async (count, { sendChunk }) => { - const { response, stream } = ai.generateStream({ - model: gemini20Flash, - config: { - temperature: 1, - }, - prompt: `Generate ${count} different RPG game characters.`, - }); - - (async () => { - for await (const chunk of stream) { - sendChunk(chunk.content[0].text!); - } - })(); - - return (await response).text; -}); + async (count, {sendChunk}) => { + const {response, stream} = ai.generateStream({ + model: gemini20Flash, + config: { + temperature: 1, + }, + prompt: `Generate ${count} different RPG game characters.`, + }); + + (async () => { + for await (const chunk of stream) { + sendChunk(chunk.content[0].text!); + } + })(); + + return (await response).text; + }, +); startFlowServer({ flows: [streamCharacters], }); - ``` diff --git a/adev-ja/src/content/ai/mcp-server-setup.en.md b/adev-ja/src/content/ai/mcp-server-setup.en.md index 182ea835e..c8e929e38 100644 --- a/adev-ja/src/content/ai/mcp-server-setup.en.md +++ b/adev-ja/src/content/ai/mcp-server-setup.en.md @@ -22,10 +22,10 @@ Some tools are provided in experimental / preview status since they are new or n | Name | Description | `local-only` | `read-only` | | :------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :----------: | :---------: | | `build` | Perform a one-off, non-watched build using `ng build`. | ✅ | ❌ | -| `modernize` | Performs code migrations and provides further instructions on how to modernize Angular code to align with the latest best practices and syntax. [Learn more](https://angular.dev/reference/migrations) | ✅ | ❌ | -| `start_devserver` | Asynchronously starts a development server that watches the workspace for changes, similar to running `ng serve`. Since this is asynchronous it returns immediately. To manage the resulting server, use the `stop_devserver` and `wait_for_devserver_build` tools. | ✅ | ✅ | -| `stop_devserver` | Stops a development server started by `start_devserver`. | ✅ | ✅ | -| `wait_for_devserver_build` | Returns the output logs of the most recent build in a running development server started by `start_devserver`. If a build is currently ongoing, it will first wait for that build to complete and then return the logs. | ✅ | ✅ | +| `modernize` | Performs code migrations and provides further instructions on how to modernize Angular code to align with the latest best practices and syntax. [Learn more](/reference/migrations) | ✅ | ❌ | +| `devserver.start` | Asynchronously starts a development server that watches the workspace for changes, similar to running `ng serve`. Since this is asynchronous it returns immediately. To manage the resulting server, use the `devserver.stop` and `devserver.wait_for_build` tools. | ✅ | ✅ | +| `devserver.stop` | Stops a development server started by `devserver.start`. | ✅ | ✅ | +| `devserver.wait_for_build` | Returns the output logs of the most recent build in a running development server started by `devserver.start`. If a build is currently ongoing, it will first wait for that build to complete and then return the logs. | ✅ | ✅ | ## Get Started @@ -133,11 +133,11 @@ For other IDEs, check your IDE's documentation for the proper location of the MC The `mcp` command can be configured with the following options passed as arguments in your IDE's MCP configuration: -| Option | Type | Description | Default | -| :---------------------------- | :-------- | :-------------------------------------------------------------------------------------------------------------------------------- | :------ | -| `--read-only` | `boolean` | Only register tools that do not make changes to the project. Your editor or coding agent may still perform edits. | `false` | -| `--local-only` | `boolean` | Only register tools that do not require an internet connection. Your editor or coding agent may still send data over the network. | `false` | -| `--experimental-tool`
`-E` | `string` | Enable an [experimental tool](#experimental-tools). Separate multiple options by spaces, e.g. `-E tool_a tool_b`. | | +| Option | Type | Description | Default | +| :---------------------------- | :-------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------ | +| `--read-only` | `boolean` | Only register tools that do not make changes to the project. Your editor or coding agent may still perform edits. | `false` | +| `--local-only` | `boolean` | Only register tools that do not require an internet connection. Your editor or coding agent may still send data over the network. | `false` | +| `--experimental-tool`
`-E` | `string` | Enable an [experimental tool](#experimental-tools). Separate multiple options by spaces, e.g. `-E tool_a tool_b`. You can also enable tool groups like `devserver` to enable all related tools at once, e.g. `-E devserver`. | | For example, to run the server in read-only mode in VS Code, you would update your `mcp.json` like this: diff --git a/adev-ja/src/content/ai/mcp-server-setup.md b/adev-ja/src/content/ai/mcp-server-setup.md index 0730e6c36..ec20c1d2e 100644 --- a/adev-ja/src/content/ai/mcp-server-setup.md +++ b/adev-ja/src/content/ai/mcp-server-setup.md @@ -22,10 +22,10 @@ Angular CLI MCPサーバーは、開発ワークフローを支援するいく | 名前 | 説明 | `local-only` | `read-only` | | :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------: | :---------: | | `build` | `ng build`を使用して、1回限りの非監視ビルドを実行します。 | ✅ | ❌ | -| `modernize` | コード移行を実行し、最新のベストプラクティスと構文に合わせてAngularコードをモダナイズする方法についてさらなる指示を提供します。[詳細を見る](https://angular.dev/reference/migrations) | ✅ | ❌ | -| `start_devserver` | ワークスペースの変更を監視する開発サーバーを非同期に起動します。`ng serve`の実行に似ています。これは非同期であるため、即座に戻ります。結果として得られるサーバーを管理するには、`stop_devserver`および`wait_for_devserver_build`ツールを使用します。 | ✅ | ✅ | -| `stop_devserver` | `start_devserver`によって起動された開発サーバーを停止します。 | ✅ | ✅ | -| `wait_for_devserver_build` | `start_devserver`によって起動された実行中の開発サーバーにおける最新のビルドの出力ログを返します。ビルドが現在進行中の場合、まずそのビルドが完了するのを待ってからログを返します。 | ✅ | ✅ | +| `modernize` | コード移行を実行し、最新のベストプラクティスと構文に合わせてAngularコードをモダナイズする方法についてさらなる指示を提供します。[詳細を見る](/reference/migrations) | ✅ | ❌ | +| `devserver.start` | ワークスペースの変更を監視する開発サーバーを非同期に起動します。`ng serve`の実行に似ています。これは非同期であるため、即座に戻ります。結果として得られるサーバーを管理するには、`devserver.stop`および`devserver.wait_for_build`ツールを使用します。 | ✅ | ✅ | +| `devserver.stop` | `devserver.start`によって起動された開発サーバーを停止します。 | ✅ | ✅ | +| `devserver.wait_for_build` | `devserver.start`によって起動された実行中の開発サーバーにおける最新のビルドの出力ログを返します。ビルドが現在進行中の場合、まずそのビルドが完了するのを待ってからログを返します。 | ✅ | ✅ | ## 開始方法 {#get-started} @@ -133,11 +133,11 @@ MCPサーバーの設定に関する最新の手順については、JetBrains `mcp`コマンドは、IDEのMCP設定で引数として渡される以下のオプションで設定できます: -| オプション | 型 | 説明 | デフォルト | -| :---------------------------- | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------- | :--------- | -| `--read-only` | `boolean` | プロジェクトに変更を加えないツールのみを登録します。エディタまたはコーディングエージェントは引き続き編集を実行する場合があります。 | `false` | -| `--local-only` | `boolean` | インターネット接続を必要としないツールのみを登録します。エディタまたはコーディングエージェントは引き続きネットワーク経由でデータを送信する場合があります。 | `false` | -| `--experimental-tool`
`-E` | `string` | [実験的ツール](#experimental-tools)を有効にします。複数のオプションをスペースで区切ります。例: `-E tool_a tool_b`。 | | +| オプション | 型 | 説明 | デフォルト | +| :---------------------------- | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------- | +| `--read-only` | `boolean` | プロジェクトに変更を加えないツールのみを登録します。エディタまたはコーディングエージェントは引き続き編集を実行する場合があります。 | `false` | +| `--local-only` | `boolean` | インターネット接続を必要としないツールのみを登録します。エディタまたはコーディングエージェントは引き続きネットワーク経由でデータを送信する場合があります。 | `false` | +| `--experimental-tool`
`-E` | `string` | [実験的ツール](#experimental-tools)を有効にします。複数のオプションをスペースで区切ります。例: `-E tool_a tool_b`。`devserver`のようなツールグループを有効にして、関連するすべてのツールを一度に有効にすることもできます。例: `-E devserver`。 | | たとえば、VS Codeにおいて読み取り専用モードでサーバーを実行する場合は、`mcp.json`を次のように更新します: diff --git a/adev-ja/src/content/guide/routing/route-transition-animations.md b/adev-ja/src/content/guide/routing/route-transition-animations.md index 910a72b8b..4eace7481 100644 --- a/adev-ja/src/content/guide/routing/route-transition-animations.md +++ b/adev-ja/src/content/guide/routing/route-transition-animations.md @@ -41,25 +41,23 @@ Routerのビュー遷移統合は[プログレッシブエンハンスメント] ### スタンドアロンブートストラップ {#standalone-bootstrap} ```ts -import { bootstrapApplication } from '@angular/platform-browser'; -import { provideRouter, withViewTransitions } from '@angular/router'; -import { routes } from './app.routes'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {provideRouter, withViewTransitions} from '@angular/router'; +import {routes} from './app.routes'; bootstrapApplication(MyApp, { - providers: [ - provideRouter(routes, withViewTransitions()), - ] + providers: [provideRouter(routes, withViewTransitions())], }); ``` ### NgModuleブートストラップ {#ngmodule-bootstrap} ```ts -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; +import {NgModule} from '@angular/core'; +import {RouterModule} from '@angular/router'; @NgModule({ - imports: [RouterModule.forRoot(routes, {enableViewTransitions: true})] + imports: [RouterModule.forRoot(routes, {enableViewTransitions: true})], }) export class AppRouting {} ``` @@ -123,13 +121,13 @@ IMPORTANT: ビューのトランジションアニメーションは、コンポ このコールバックを使用して、ナビゲーションコンテキストに基づいてトランジション動作をカスタマイズできます。たとえば、特定のナビゲーションタイプではトランジションをスキップできます。 ```ts -import { inject } from '@angular/core'; -import { Router, withViewTransitions } from '@angular/router'; +import {inject} from '@angular/core'; +import {Router, withViewTransitions} from '@angular/router'; withViewTransitions({ onViewTransitionCreated: ({transition}) => { const router = inject(Router); - const targetUrl = router.getCurrentNavigation()!.finalUrl!; + const targetUrl = router.currentNavigation()!.finalUrl!; // Skip transition if only fragment or query params change const config = { @@ -143,7 +141,7 @@ withViewTransitions({ transition.skipTransition(); } }, -}) +}); ``` この例では、ナビゲーションが[URLフラグメントまたはクエリパラメータ](/guide/routing/read-route-state#query-parameters)のみを変更する場合(同じページ内のアンカーリンクなど)にビュー遷移をスキップします。`skipTransition()`メソッドは、ナビゲーションの完了を許可しながらアニメーションを防止します。 @@ -201,4 +199,3 @@ NOTE: Angular Routerはビュー遷移を遅延させる方法を提供してい - [Chrome Explainer](https://developer.chrome.com/docs/web-platform/view-transitions/same-document#animating-with-javascript) - [Angular Example on StackBlitz](https://stackblitz.com/edit/stackblitz-starters-cklnkm) - diff --git a/adev-ja/src/content/guide/routing/router-reference.md b/adev-ja/src/content/guide/routing/router-reference.md index eb9f570e6..7178fd397 100644 --- a/adev-ja/src/content/guide/routing/router-reference.md +++ b/adev-ja/src/content/guide/routing/router-reference.md @@ -110,9 +110,7 @@ scheme authority path query fragment `AppModule`の`RouterModule.forRoot()`の2番目の引数として、オブジェクト内で`useHash: true`を提供することで`HashLocationStrategy`を使用します。 ```ts -providers: [ - provideRouter(appRoutes, withHashLocation()) -] +providers: [provideRouter(appRoutes, withHashLocation())]; ``` `RouterModule.forRoot`を使用する場合、これは2番目の引数`RouterModule.forRoot(routes, {useHash: true})`で`useHash: true`を使用して設定されます。 diff --git a/adev-ja/src/content/guide/routing/router-tutorial.md b/adev-ja/src/content/guide/routing/router-tutorial.md index 9c6add526..134424507 100644 --- a/adev-ja/src/content/guide/routing/router-tutorial.md +++ b/adev-ja/src/content/guide/routing/router-tutorial.md @@ -113,14 +113,14 @@ ng serve 1. 次のインポートステートメントを追加します。 ```ts - import { provideRouter } from '@angular/router'; - import { routes } from './app.routes'; + import {provideRouter} from '@angular/router'; + import {routes} from './app.routes'; ``` 1. `appConfig`のプロバイダーを更新します。 ```ts - providers: [provideRouter(routes)] + providers: [provideRouter(routes)]; ``` `NgModule`ベースのアプリケーションの場合、`provideRouter`は`AppModule`の`providers`リスト、またはアプリケーションで`bootstrapModule`に渡されるモジュールに配置します。 diff --git a/adev-ja/src/content/guide/routing/show-routes-with-outlets.md b/adev-ja/src/content/guide/routing/show-routes-with-outlets.md index 15a74f920..cde561f6a 100644 --- a/adev-ja/src/content/guide/routing/show-routes-with-outlets.md +++ b/adev-ja/src/content/guide/routing/show-routes-with-outlets.md @@ -9,14 +9,14 @@ ``` ```ts -import { Component } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; +import {Component} from '@angular/core'; +import {RouterOutlet} from '@angular/router'; @Component({ selector: 'app-root', imports: [RouterOutlet], templateUrl: './app.component.html', - styleUrl: './app.component.css' + styleUrl: './app.component.css', }) export class AppComponent {} ``` @@ -24,21 +24,21 @@ export class AppComponent {} この例では、アプリケーションに次のルートが定義されている場合: ```ts -import { Routes } from '@angular/router'; -import { HomeComponent } from './home/home.component'; -import { ProductsComponent } from './products/products.component'; +import {Routes} from '@angular/router'; +import {HomeComponent} from './home/home.component'; +import {ProductsComponent} from './products/products.component'; const routes: Routes = [ { path: '', component: HomeComponent, - title: 'Home Page' + title: 'Home Page', }, { path: 'products', component: ProductsComponent, - title: 'Our Products' - } + title: 'Our Products', + }, ]; ``` diff --git a/adev-ja/src/content/guide/routing/testing.md b/adev-ja/src/content/guide/routing/testing.md index 324c30c22..b3977de74 100644 --- a/adev-ja/src/content/guide/routing/testing.md +++ b/adev-ja/src/content/guide/routing/testing.md @@ -6,8 +6,7 @@ このガイドは、以下のツールとライブラリに精通していることを前提としています。 -- **[Jasmine](https://jasmine.github.io/)** - テスト構文(`describe`、`it`、`expect`)を提供するJavaScriptテストフレームワーク -- **[Karma](https://karma-runner.github.io/)** - ブラウザでテストを実行するテストランナー +- **[Vitest](https://vitest.dev/)** - テスト構文(`describe`、`it`、`expect`)を提供するJavaScriptテストフレームワーク - **[Angular Testing Utilities](guide/testing)** - Angularに組み込まれているテストツール([`TestBed`](api/core/testing/TestBed)、[`ComponentFixture`](api/core/testing/ComponentFixture)) - **[`RouterTestingHarness`](api/router/testing/RouterTestingHarness)** - 組み込みのナビゲーションとコンポーネントテスト機能を備えた、ルーティングされたコンポーネントをテストするためのテストハーネス @@ -21,21 +20,17 @@ ```ts // user-profile.component.spec.ts -import { TestBed } from '@angular/core/testing'; -import { Router } from '@angular/router'; -import { RouterTestingHarness } from '@angular/router/testing'; -import { provideRouter } from '@angular/router'; -import { UserProfile } from './user-profile'; +import {TestBed} from '@angular/core/testing'; +import {Router} from '@angular/router'; +import {RouterTestingHarness} from '@angular/router/testing'; +import {provideRouter} from '@angular/router'; +import {UserProfile} from './user-profile'; describe('UserProfile', () => { it('should display user ID from route parameters', async () => { TestBed.configureTestingModule({ imports: [UserProfile], - providers: [ - provideRouter([ - { path: 'user/:id', component: UserProfile } - ]) - ] + providers: [provideRouter([{path: 'user/:id', component: UserProfile}])], }); const harness = await RouterTestingHarness.create(); @@ -68,17 +63,17 @@ export class UserProfile { ```ts // auth.guard.spec.ts -import { RouterTestingHarness } from '@angular/router/testing'; -import { provideRouter, Router } from '@angular/router'; -import { authGuard } from './auth.guard'; -import { AuthStore } from './auth-store'; -import { Component } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; - -@Component({ template: '

Protected Page

' }) +import {RouterTestingHarness} from '@angular/router/testing'; +import {provideRouter, Router} from '@angular/router'; +import {authGuard} from './auth.guard'; +import {AuthStore} from './auth-store'; +import {Component} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; + +@Component({template: '

Protected Page

'}) class ProtectedComponent {} -@Component({ template: '

Login Page

' }) +@Component({template: '

Login Page

'}) class LoginComponent {} describe('authGuard', () => { @@ -91,10 +86,10 @@ describe('authGuard', () => { TestBed.configureTestingModule({ providers: [ - { provide: AuthStore, useValue: authStore }, + {provide: AuthStore, useValue: authStore}, provideRouter([ - { path: 'protected', component: ProtectedComponent, canActivate: [authGuard] }, - { path: 'login', component: LoginComponent }, + {path: 'protected', component: ProtectedComponent, canActivate: [authGuard]}, + {path: 'login', component: LoginComponent}, ]), ], }); @@ -120,9 +115,9 @@ describe('authGuard', () => { ```ts // auth.guard.ts -import { inject } from '@angular/core'; -import { CanActivateFn, Router } from '@angular/router'; -import { AuthStore } from './auth-store'; +import {inject} from '@angular/core'; +import {CanActivateFn, Router} from '@angular/router'; +import {AuthStore} from './auth-store'; export const authGuard: CanActivateFn = () => { const authStore = inject(AuthStore); @@ -139,19 +134,19 @@ export const authGuard: CanActivateFn = () => { ```ts // app.component.spec.ts -import { TestBed } from '@angular/core/testing'; -import { RouterTestingHarness } from '@angular/router/testing'; -import { provideRouter } from '@angular/router'; -import { Component } from '@angular/core'; -import { App } from './app'; +import {TestBed} from '@angular/core/testing'; +import {RouterTestingHarness} from '@angular/router/testing'; +import {provideRouter} from '@angular/router'; +import {Component} from '@angular/core'; +import {App} from './app'; @Component({ - template: '

Home Page

' + template: '

Home Page

', }) class MockHome {} @Component({ - template: '

About Page

' + template: '

About Page

', }) class MockAbout {} @@ -163,10 +158,10 @@ describe('App Router Outlet', () => { imports: [App], providers: [ provideRouter([ - { path: '', component: MockHome }, - { path: 'about', component: MockAbout } - ]) - ] + {path: '', component: MockHome}, + {path: 'about', component: MockAbout}, + ]), + ], }); harness = await RouterTestingHarness.create(); @@ -218,10 +213,10 @@ export class App {} ```ts // nested-routes.spec.ts -import { TestBed } from '@angular/core/testing'; -import { RouterTestingHarness } from '@angular/router/testing'; -import { provideRouter } from '@angular/router'; -import { Parent, Child } from './nested-components'; +import {TestBed} from '@angular/core/testing'; +import {RouterTestingHarness} from '@angular/router/testing'; +import {provideRouter} from '@angular/router'; +import {Parent, Child} from './nested-components'; describe('Nested Routes', () => { let harness: RouterTestingHarness; @@ -234,12 +229,10 @@ describe('Nested Routes', () => { { path: 'parent', component: Parent, - children: [ - { path: 'child', component: Child } - ] - } - ]) - ] + children: [{path: 'child', component: Child}], + }, + ]), + ], }); harness = await RouterTestingHarness.create(); @@ -284,10 +277,10 @@ export class Child {} ```ts // search.component.spec.ts -import { TestBed } from '@angular/core/testing'; -import { Router, provideRouter } from '@angular/router'; -import { RouterTestingHarness } from '@angular/router/testing'; -import { Search } from './search'; +import {TestBed} from '@angular/core/testing'; +import {Router, provideRouter} from '@angular/router'; +import {RouterTestingHarness} from '@angular/router/testing'; +import {Search} from './search'; describe('Search', () => { let component: Search; @@ -296,11 +289,7 @@ describe('Search', () => { beforeEach(async () => { TestBed.configureTestingModule({ imports: [Search], - providers: [ - provideRouter([ - { path: 'search', component: Search } - ]) - ] + providers: [provideRouter([{path: 'search', component: Search}])], }); harness = await RouterTestingHarness.create(); @@ -333,9 +322,9 @@ export class Search { ## ルーターテストのベストプラクティス {#best-practices-for-router-testing} -1. **RouterTestingHarnessを使用する** - ルーティングされたコンポーネントをテストするには、よりクリーンなAPIを提供し、テストホストコンポーネントの必要性を排除する[`RouterTestingHarness`](api/router/testing/RouterTestingHarness)を使用してください。これは、直接的なコンポーネントアクセス、組み込みのナビゲーション、およびより優れた型安全性を提供します。ただし、名前付きアウトレットのテストなど、カスタムホストコンポーネントを作成する必要がある一部のシナリオには適していません。 -2. **外部の依存関係を慎重に処理する** - より現実的なテストのために、可能な場合は実際の実装を優先してください。実際の実装が実現不可能な場合(例:外部API)は、実際の動作を近似するフェイクを使用してください。モックやスタブは、テストを脆弱にし、信頼性を低下させる可能性があるため、最後の手段としてのみ使用してください。 -3. **ナビゲーション状態をテストする** - URLの変更やコンポーネントのレンダリングを含む、ナビゲーションアクションと結果として生じるアプリケーション状態の両方を検証してください。 -4. **非同期操作を処理する** - ルーターのナビゲーションは非同期です。テストでのタイミングを適切に処理するために、`async/await` または [`fakeAsync`](api/core/testing/fakeAsync) を使用してください。 -5. **エラーシナリオをテストする** - アプリケーションがエッジケースを適切に処理することを確認するために、無効なルート、失敗したナビゲーション、およびガードによる拒否のテストを含めてください。 -6. **Angularルーターをモックしない** - 代わりに、実際のルート設定を提供し、ハーネスを使用してナビゲートしてください。これにより、テストがより堅牢になり、Angularの内部更新で壊れる可能性が低くなります。また、モックは破壊的変更を隠す可能性があるため、ルーターが更新されたときに実際の問題を確実に捕捉できます。 +1. **RouterTestingHarnessを使用する** - ルーティングされたコンポーネントをテストするには、よりクリーンなAPIを提供し、テストホストコンポーネントの必要性を排除する[`RouterTestingHarness`](api/router/testing/RouterTestingHarness)を使用してください。これは、直接的なコンポーネントアクセス、組み込みのナビゲーション、およびより優れた型安全性を提供します。ただし、名前付きアウトレットのテストなど、カスタムホストコンポーネントを作成する必要がある一部のシナリオには適していません。 +2. **外部の依存関係を慎重に処理する** - より現実的なテストのために、可能な場合は実際の実装を優先してください。実際の実装が実現不可能な場合(例:外部API)は、実際の動作を近似するフェイクを使用してください。モックやスタブは、テストを脆弱にし、信頼性を低下させる可能性があるため、最後の手段としてのみ使用してください。 +3. **ナビゲーション状態をテストする** - URLの変更やコンポーネントのレンダリングを含む、ナビゲーションアクションと結果として生じるアプリケーション状態の両方を検証してください。 +4. **非同期操作を処理する** - ルーターのナビゲーションは非同期です。テストでのタイミングを適切に処理するために、`async/await`を使用してください。 +5. **エラーシナリオをテストする** - アプリケーションがエッジケースを適切に処理することを確認するために、無効なルート、失敗したナビゲーション、およびガードによる拒否のテストを含めてください。 +6. **Angularルーターをモックしない** - 代わりに、実際のルート設定を提供し、ハーネスを使用してナビゲートしてください。これにより、テストがより堅牢になり、Angularの内部更新で壊れる可能性が低くなります。また、モックは破壊的変更を隠す可能性があるため、ルーターが更新されたときに実際の問題を確実に捕捉できます。 diff --git a/adev-ja/src/content/guide/security.md b/adev-ja/src/content/guide/security.md index a70b1fc65..7882d9a20 100644 --- a/adev-ja/src/content/guide/security.md +++ b/adev-ja/src/content/guide/security.md @@ -159,10 +159,12 @@ import {bootstrapApplication, CSP_NONCE} from '@angular/core'; import {AppComponent} from './app/app.component'; bootstrapApplication(AppComponent, { - providers: [{ - provide: CSP_NONCE, - useValue: globalThis.myRandomNonceValue - }] + providers: [ + { + provide: CSP_NONCE, + useValue: globalThis.myRandomNonceValue, + }, + ], }); ``` @@ -225,13 +227,15 @@ Content-Security-Policy: trusted-types angular; require-trusted-types-for 'scrip Trusted TypesとAngularアプリケーション用に構成されたヘッダーの例で、Angularの[DomSanitizer](api/platform-browser/DomSanitizer)でセキュリティをバイパスするメソッドをいずれか使用しています。 ```html -Content-Security-Policy: trusted-types angular angular#unsafe-bypass; require-trusted-types-for 'script'; +Content-Security-Policy: trusted-types angular angular#unsafe-bypass; require-trusted-types-for ++'script'; ``` Trusted TypesとJITを使用するAngularアプリケーション用に構成されたヘッダーの例を以下に示します。 ```html -Content-Security-Policy: trusted-types angular angular#unsafe-jit; require-trusted-types-for 'script'; +Content-Security-Policy: trusted-types angular angular#unsafe-jit; require-trusted-types-for ++'script'; ``` 遅延読み込みモジュールを使用するAngularアプリケーション用に構成されたヘッダーの例を以下に示します。 @@ -328,7 +332,7 @@ export const appConfig: ApplicationConfig = { headerName: 'X-Custom-Xsrf-Header', }), ), - ] + ], }; ``` @@ -338,11 +342,7 @@ export const appConfig: ApplicationConfig = { ```ts export const appConfig: ApplicationConfig = { - providers: [ - provideHttpClient( - withNoXsrfProtection(), - ), - ] + providers: [provideHttpClient(withNoXsrfProtection())], }; ``` From 9f1aadd364a0c14146fd1de4b995d4cf921322ca Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 11:13:37 +0900 Subject: [PATCH 05/32] docs(best-practices): migrate .en.md changes --- .../best-practices/error-handling.en.md | 3 +- .../content/best-practices/error-handling.md | 3 +- .../skipping-subtrees.en.md | 2 +- .../runtime-performance/skipping-subtrees.md | 2 +- .../content/best-practices/style-guide.en.md | 38 ++++++++++++------- .../src/content/best-practices/style-guide.md | 38 ++++++++++++------- 6 files changed, 54 insertions(+), 32 deletions(-) diff --git a/adev-ja/src/content/best-practices/error-handling.en.md b/adev-ja/src/content/best-practices/error-handling.en.md index c4cd17a7f..d9cb07d36 100644 --- a/adev-ja/src/content/best-practices/error-handling.en.md +++ b/adev-ja/src/content/best-practices/error-handling.en.md @@ -37,10 +37,9 @@ export class GlobalErrorHandler implements ErrorHandler { description: `Screen: ${url} | ${errorMessage}`, }); - console.error(GlobalErrorHandler.name, { error }); + console.error(GlobalErrorHandler.name, {error}); } } - ``` ### `TestBed` rethrows errors by default diff --git a/adev-ja/src/content/best-practices/error-handling.md b/adev-ja/src/content/best-practices/error-handling.md index a8588f63f..94e84906f 100644 --- a/adev-ja/src/content/best-practices/error-handling.md +++ b/adev-ja/src/content/best-practices/error-handling.md @@ -37,10 +37,9 @@ export class GlobalErrorHandler implements ErrorHandler { description: `Screen: ${url} | ${errorMessage}`, }); - console.error(GlobalErrorHandler.name, { error }); + console.error(GlobalErrorHandler.name, {error}); } } - ``` ### `TestBed`はデフォルトでエラーを再スローします {#testbed-rethrows-errors-by-default} diff --git a/adev-ja/src/content/best-practices/runtime-performance/skipping-subtrees.en.md b/adev-ja/src/content/best-practices/runtime-performance/skipping-subtrees.en.md index 05160e1ce..02304d483 100644 --- a/adev-ja/src/content/best-practices/runtime-performance/skipping-subtrees.en.md +++ b/adev-ja/src/content/best-practices/runtime-performance/skipping-subtrees.en.md @@ -16,7 +16,7 @@ OnPush change detection instructs Angular to run change detection for a componen You can set the change detection strategy of a component to `OnPush` in the `@Component` decorator: ```ts -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/adev-ja/src/content/best-practices/runtime-performance/skipping-subtrees.md b/adev-ja/src/content/best-practices/runtime-performance/skipping-subtrees.md index 2be97dc83..77167acbe 100644 --- a/adev-ja/src/content/best-practices/runtime-performance/skipping-subtrees.md +++ b/adev-ja/src/content/best-practices/runtime-performance/skipping-subtrees.md @@ -16,7 +16,7 @@ OnPush変更検知は、Angularにコンポーネントのサブツリーの変 コンポーネントの変更検知戦略を`@Component`デコレーターで`OnPush`に設定できます。 ```ts -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/adev-ja/src/content/best-practices/style-guide.en.md b/adev-ja/src/content/best-practices/style-guide.en.md index 9277e92d4..6b32e7646 100644 --- a/adev-ja/src/content/best-practices/style-guide.en.md +++ b/adev-ja/src/content/best-practices/style-guide.en.md @@ -196,7 +196,9 @@ properties initialized by `input`, `model`, `output`, and queries. The readonly ensures that the value set by Angular is not overwritten. ```ts -@Component({/* ... */}) +@Component({ + /* ... */ +}) export class UserProfile { readonly userId = input(); readonly userSaved = output(); @@ -208,7 +210,9 @@ For components and directives that use the decorator-based `@Input`, `@Output`, advice applies to output properties and queries, but not input properties. ```ts -@Component({/* ... */}) +@Component({ + /* ... */ +}) export class UserProfile { @Output() readonly userSaved = new EventEmitter(); @ViewChildren(PaymentMethod) readonly paymentMethods?: QueryList; @@ -221,15 +225,19 @@ Prefer `class` and `style` bindings over using the [`NgClass`](/api/common/NgCla ```html {prefer}
-
- -
-
+
+ +
+
+
+
+
``` ```html {avoid}
-
+
+
``` Both `class` and `style` bindings use a more straightforward syntax that aligns closely with @@ -267,8 +275,9 @@ single well-named handler. In these cases, it's fine to fall back to a name like then delegate to more specific behaviors based on the event details: ```ts - -@Component({/* ... */}) +@Component({ + /* ... */ +}) class RichText { handleKeydown(event: KeyboardEvent) { if (event.ctrlKey) { @@ -277,7 +286,7 @@ class RichText { } else if (event.key === 'I') { this.activateItalic(); } -// ... + // ... } } } @@ -313,10 +322,13 @@ your class, import and `implement` these interfaces to ensure that the methods a ```ts import {Component, OnInit} from '@angular/core'; -@Component({/* ... */}) +@Component({ + /* ... */ +}) export class UserProfile implements OnInit { - // The `OnInit` interface ensures this method is named correctly. - ngOnInit() { /* ... */ } + ngOnInit() { + /* ... */ + } } ``` diff --git a/adev-ja/src/content/best-practices/style-guide.md b/adev-ja/src/content/best-practices/style-guide.md index 1f4ddb18c..fc541f67c 100644 --- a/adev-ja/src/content/best-practices/style-guide.md +++ b/adev-ja/src/content/best-practices/style-guide.md @@ -196,7 +196,9 @@ Angularによって初期化されるコンポーネントとディレクティ readonlyアクセス修飾子は、Angularによって設定された値が上書きされないことを保証します。 ```ts -@Component({/* ... */}) +@Component({ + /* ... */ +}) export class UserProfile { readonly userId = input(); readonly userSaved = output(); @@ -208,7 +210,9 @@ export class UserProfile { このアドバイスは出力プロパティとクエリに適用されますが、入力プロパティには適用されません。 ```ts -@Component({/* ... */}) +@Component({ + /* ... */ +}) export class UserProfile { @Output() readonly userSaved = new EventEmitter(); @ViewChildren(PaymentMethod) readonly paymentMethods?: QueryList; @@ -221,15 +225,19 @@ export class UserProfile { ```html {prefer}
-
- -
-
+
+ +
+
+
+
+
``` ```html {avoid}
-
+
+
``` `class`および`style`バインディングはどちらも、 @@ -267,8 +275,9 @@ export class UserProfile { イベントの詳細に基づいてより具体的な動作に委譲しても問題ありません。 ```ts - -@Component({/* ... */}) +@Component({ + /* ... */ +}) class RichText { handleKeydown(event: KeyboardEvent) { if (event.ctrlKey) { @@ -277,7 +286,7 @@ class RichText { } else if (event.key === 'I') { this.activateItalic(); } -// ... + // ... } } } @@ -313,10 +322,13 @@ Angularは、各ライフサイクルメソッドに対応するTypeScriptイン ```ts import {Component, OnInit} from '@angular/core'; -@Component({/* ... */}) +@Component({ + /* ... */ +}) export class UserProfile implements OnInit { - // The `OnInit` interface ensures this method is named correctly. - ngOnInit() { /* ... */ } + ngOnInit() { + /* ... */ + } } ``` From 9e11b144c0fb2d469ed66403182f3d9e14ae834c Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 11:14:18 +0900 Subject: [PATCH 06/32] docs(guide/animations): migrate .en.md changes --- adev-ja/src/content/guide/animations/enter-and-leave.en.md | 2 +- adev-ja/src/content/guide/animations/enter-and-leave.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/adev-ja/src/content/guide/animations/enter-and-leave.en.md b/adev-ja/src/content/guide/animations/enter-and-leave.en.md index 8ab48087d..2a994183a 100644 --- a/adev-ja/src/content/guide/animations/enter-and-leave.en.md +++ b/adev-ja/src/content/guide/animations/enter-and-leave.en.md @@ -86,7 +86,7 @@ TestBed provides built-in support for enabling or disabling animations in your t If you want to test that the animations are animating in a browser test, for example an end-to-end test, you can configure TestBed to enable animations by specifying `animationsEnabled: true` in your test configuration. ```typescript - TestBed.configureTestingModule({animationsEnabled: true}); +TestBed.configureTestingModule({animationsEnabled: true}); ``` This will configure animations in your test environment to behave normally. diff --git a/adev-ja/src/content/guide/animations/enter-and-leave.md b/adev-ja/src/content/guide/animations/enter-and-leave.md index fd08e4f89..243f06d66 100644 --- a/adev-ja/src/content/guide/animations/enter-and-leave.md +++ b/adev-ja/src/content/guide/animations/enter-and-leave.md @@ -86,7 +86,7 @@ TestBedは、テスト環境でアニメーションを有効または無効に ブラウザテスト、例えばエンドツーエンドテストでアニメーションが動作していることをテストしたい場合は、テスト設定で`animationsEnabled: true`を指定することで、TestBedをアニメーションを有効にするように設定できます。 ```typescript - TestBed.configureTestingModule({animationsEnabled: true}); +TestBed.configureTestingModule({animationsEnabled: true}); ``` これにより、テスト環境でのアニメーションが通常通りに動作するように設定されます。 From 75cb7b03f30a567b5a4b9d1996f0d3a812540c39 Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 11:15:17 +0900 Subject: [PATCH 07/32] docs(guide/aria): migrate .en.md changes --- adev-ja/src/content/guide/aria/toolbar.en.md | 12 ++---------- adev-ja/src/content/guide/aria/toolbar.md | 12 ++---------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/adev-ja/src/content/guide/aria/toolbar.en.md b/adev-ja/src/content/guide/aria/toolbar.en.md index 3f204d5cc..926e8c441 100644 --- a/adev-ja/src/content/guide/aria/toolbar.en.md +++ b/adev-ja/src/content/guide/aria/toolbar.en.md @@ -135,22 +135,14 @@ The `multi` input controls whether multiple widgets within a group can be select ```html {highlight: [15]} -
+
-
+
diff --git a/adev-ja/src/content/guide/aria/toolbar.md b/adev-ja/src/content/guide/aria/toolbar.md index 280ca3a99..b78aaf37b 100644 --- a/adev-ja/src/content/guide/aria/toolbar.md +++ b/adev-ja/src/content/guide/aria/toolbar.md @@ -135,22 +135,14 @@ Angularのツールバーは、以下の機能を備えた完全にアクセシ ```html {highlight: [15]} -
+
-
+
From 71ff94358c2f553b2b2cf3955e51e6c3e1aa416d Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 11:17:04 +0900 Subject: [PATCH 08/32] docs(guide/components): migrate .en.md changes --- .../components/anatomy-of-components.en.md | 4 +- .../guide/components/anatomy-of-components.md | 4 +- .../src/content/guide/components/inputs.en.md | 48 ++++++++++++++----- .../src/content/guide/components/inputs.md | 48 ++++++++++++++----- .../content/guide/components/outputs.en.md | 24 ++++++---- .../src/content/guide/components/outputs.md | 24 ++++++---- .../components/programmatic-rendering.en.md | 26 ++++++---- .../components/programmatic-rendering.md | 26 ++++++---- .../content/guide/components/queries.en.md | 17 ++++--- .../src/content/guide/components/queries.md | 17 ++++--- 10 files changed, 160 insertions(+), 78 deletions(-) diff --git a/adev-ja/src/content/guide/components/anatomy-of-components.en.md b/adev-ja/src/content/guide/components/anatomy-of-components.en.md index 924843ad3..c9732799a 100644 --- a/adev-ja/src/content/guide/components/anatomy-of-components.en.md +++ b/adev-ja/src/content/guide/components/anatomy-of-components.en.md @@ -44,7 +44,7 @@ You can alternatively choose to write your template and styles in separate files templateUrl: 'profile-photo.html', styleUrl: 'profile-photo.css', }) -export class ProfilePhoto { } +export class ProfilePhoto {} ``` This can help separate the concerns of _presentation_ from _behavior_ in your project. You can choose one approach for your entire project, or you decide which to use for each component. @@ -67,7 +67,7 @@ import {ProfilePhoto} from './profile-photo'; imports: [ProfilePhoto], /* ... */ }) -export class UserProfile { } +export class UserProfile {} ``` By default, Angular components are _standalone_, meaning that you can directly add them to the `imports` array of other components. Components created with an earlier version of Angular may instead specify `standalone: false` in their `@Component` decorator. For these components, you instead import the `NgModule` in which the component is defined. See the full [`NgModule` guide](guide/ngmodules) for details. diff --git a/adev-ja/src/content/guide/components/anatomy-of-components.md b/adev-ja/src/content/guide/components/anatomy-of-components.md index 8e28dda77..53464d688 100644 --- a/adev-ja/src/content/guide/components/anatomy-of-components.md +++ b/adev-ja/src/content/guide/components/anatomy-of-components.md @@ -44,7 +44,7 @@ export class ProfilePhoto { } templateUrl: 'profile-photo.html', styleUrl: 'profile-photo.css', }) -export class ProfilePhoto { } +export class ProfilePhoto {} ``` これにより、プロジェクト内の_表示_と_動作_の懸念事項を分離できます。プロジェクト全体に対して1つのアプローチを選択するか、コンポーネントごとにどちらを使用するかを決定できます。 @@ -67,7 +67,7 @@ import {ProfilePhoto} from './profile-photo'; imports: [ProfilePhoto], /* ... */ }) -export class UserProfile { } +export class UserProfile {} ``` デフォルトでは、Angularコンポーネントは*スタンドアロン*です。つまり、他のコンポーネントの`imports`配列に直接追加できます。以前のバージョンのAngularで作成されたコンポーネントは、代わりに`@Component`デコレーターで`standalone: false`を指定している場合があります。これらのコンポーネントの場合、代わりにコンポーネントが定義されている`NgModule`をインポートします。詳細は、完全な[`NgModule`ガイド](guide/ngmodules)を参照してください。 diff --git a/adev-ja/src/content/guide/components/inputs.en.md b/adev-ja/src/content/guide/components/inputs.en.md index 73651d757..0b2d8236f 100644 --- a/adev-ja/src/content/guide/components/inputs.en.md +++ b/adev-ja/src/content/guide/components/inputs.en.md @@ -10,7 +10,9 @@ When you use a component, you commonly want to pass some data to it. A component ```ts {highlight:[5]} import {Component, input} from '@angular/core'; -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomSlider { // Declare an input named 'value' with a default value of zero. value = input(0); @@ -26,7 +28,9 @@ This lets you bind to the property in a template: If an input has a default value, TypeScript infers the type from the default value: ```ts -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomSlider { // TypeScript infers that this input is a number, returning InputSignal. value = input(0); @@ -38,7 +42,9 @@ You can explicitly declare a type for the input by specifying a generic paramete If an input without a default value is not set, its value is `undefined`: ```ts -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomSlider { // Produces an InputSignal because `value` may not be set. value = input(); @@ -60,7 +66,9 @@ The `input` function returns an `InputSignal`. You can read the value by calling ```ts {highlight:[5]} import {Component, input, computed} from '@angular/core'; -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomSlider { // Declare an input named 'value' with a default value of zero. value = input(0); @@ -77,7 +85,9 @@ Signals created by the `input` function are read-only. You can declare that an input is `required` by calling `input.required` instead of `input`: ```ts {highlight:[3]} -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomSlider { // Declare a required input named value. Returns an `InputSignal`. value = input.required(); @@ -127,7 +137,9 @@ The most common use-case for input transforms is to accept a wider range of valu When you specify an input transform, the type of the transform function's parameter determines the types of values that can be set to the input in a template. ```ts -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomSlider { widthPx = input('', {transform: appendPx}); } @@ -146,7 +158,9 @@ Angular includes two built-in transform functions for the two most common scenar ```ts import {Component, input, booleanAttribute, numberAttribute} from '@angular/core'; -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomSlider { disabled = input(false, {transform: booleanAttribute}); value = input(0, {transform: numberAttribute}); @@ -163,7 +177,9 @@ _presence_ of the attribute indicates a "true" value. However, Angular's `boolea You can specify the `alias` option to change the name of an input in templates. ```ts {highlight:[3]} -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomSlider { value = input(0, {alias: 'sliderValue'}); } @@ -186,14 +202,16 @@ When creating a component, you can define a model input similarly to how you cre Both types of input allow someone to bind a value into the property. However, **model inputs allow the component author to write values into the property**. If the property is bound with a two-way binding, the new value propagates to that binding. ```ts -@Component({ /* ... */}) +@Component({ + /* ... */ +}) export class CustomSlider { // Define a model input named "value". value = model(0); increment() { // Update the model input with a new value, propagating the value to any bindings. - this.value.update(oldValue => oldValue + 10); + this.value.update((oldValue) => oldValue + 10); } } @@ -212,7 +230,7 @@ export class MediaControls { In the above example, the `CustomSlider` can write values into its `value` model input, which then propagates those values back to the `volume` signal in `MediaControls`. This binding keeps the values of `value` and `volume` in sync. Notice that the binding passes the `volume` signal instance, not the _value_ of the signal. -In other respects, model inputs work similarly to standard inputs. You can read the value by calling the signal function, including in reactive contexts like `computed` and `effect`. +In other respects, model inputs work similarly to standard inputs. You can read the value by calling the signal function, including in [reactive contexts](guide/signals#reactive-contexts) like `computed` and `effect`. See [Two-way binding](guide/templates/two-way-binding) for more details on two-way binding in templates. @@ -239,7 +257,9 @@ In the example above, the `CustomSlider` can write values into its `value` model When you declare a model input in a component or directive, Angular automatically creates a corresponding [output](guide/components/outputs) for that model. The output's name is the model input's name suffixed with "Change". ```ts -@Directive({ /* ... */ }) +@Directive({ + /* ... */ +}) export class CustomCheckbox { // This automatically creates an output named "checkedChange". // Can be subscribed to using `(checkedChange)="handler()"` in the template. @@ -351,7 +371,9 @@ export class CustomSlider { return this.internalValue; } - set value(newValue: number) { this.internalValue = newValue; } + set value(newValue: number) { + this.internalValue = newValue; + } private internalValue = 0; } diff --git a/adev-ja/src/content/guide/components/inputs.md b/adev-ja/src/content/guide/components/inputs.md index 5a995abc4..52574fc20 100644 --- a/adev-ja/src/content/guide/components/inputs.md +++ b/adev-ja/src/content/guide/components/inputs.md @@ -10,7 +10,9 @@ TIP: 他のウェブフレームワークに精通している場合は、入力 ```ts {highlight:[5]} import {Component, input} from '@angular/core'; -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomSlider { // Declare an input named 'value' with a default value of zero. value = input(0); @@ -26,7 +28,9 @@ export class CustomSlider { 入力にデフォルト値がある場合、TypeScriptはデフォルト値から型を推論します。 ```ts -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomSlider { // TypeScriptは、この入力が数値であると推論し、InputSignalを返します。 value = input(0); @@ -38,7 +42,9 @@ export class CustomSlider { デフォルト値のない入力が設定されていない場合、その値は`undefined`になります。 ```ts -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomSlider { // `value`は設定されない可能性があるため、InputSignalを生成します。 value = input(); @@ -60,7 +66,9 @@ export class CustomSlider { ```ts {highlight:[5]} import {Component, input, computed} from '@angular/core'; -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomSlider { // Declare an input named 'value' with a default value of zero. value = input(0); @@ -77,7 +85,9 @@ export class CustomSlider { `input`の代わりに`input.required`を呼び出すことで、入力が`required`であることを宣言できます。 ```ts {highlight:[3]} -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomSlider { // Declare a required input named value. Returns an `InputSignal`. value = input.required(); @@ -127,7 +137,9 @@ function trimString(value: string | undefined): string { 入力変換を指定すると、変換関数の引数の型によって、テンプレートで入力に設定できる値の型が決まります。 -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomSlider { widthPx = input('', {transform: appendPx}); } @@ -146,7 +158,9 @@ Angularには、最も一般的な2つのシナリオのための2つの組み ```ts import {Component, input, booleanAttribute, numberAttribute} from '@angular/core'; -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomSlider { disabled = input(false, {transform: booleanAttribute}); value = input(0, {transform: numberAttribute}); @@ -163,7 +177,9 @@ export class CustomSlider { `alias`オプションを指定して、テンプレートでの入力の名前を変更できます。 ```ts {highlight:[3]} -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomSlider { value = input(0, {alias: 'sliderValue'}); } @@ -186,14 +202,16 @@ export class CustomSlider { どちらの種類の入力も、値をプロパティにバインドすることを可能にします。しかし、**モデル入力は、コンポーネントの作者がプロパティに値を書き込むことを可能にします。**プロパティが双方向バインディングでバインドされている場合、新しい値はそのバインディングに伝播します。 ```ts -@Component({ /* ... */}) +@Component({ + /* ... */ +}) export class CustomSlider { // "value"という名前のモデル入力を定義します。 value = model(0); increment() { // 新しい値でモデル入力を更新し、値をすべてのバインディングに伝播します。 - this.value.update(oldValue => oldValue + 10); + this.value.update((oldValue) => oldValue + 10); } } @@ -212,7 +230,7 @@ export class MediaControls { 上記の例では、`CustomSlider`は`value`モデル入力に値を書き込むことができ、それによって`MediaControls`の`volume`シグナルに値が伝播します。このバインディングにより、`value`と`volume`の値が同期します。バインディングはシグナルの_値_ではなく、`volume`シグナルのインスタンスを渡していることに注意してください。 -他の点では、モデル入力は標準入力と同様に機能します。`computed`や`effect`のようなリアクティブなコンテキスト内を含め、シグナル関数を呼び出すことで値を読み取ることができます。 +他の点では、モデル入力は標準入力と同様に機能します。`computed`や`effect`のような[リアクティブなコンテキスト](guide/signals#reactive-contexts)内を含め、シグナル関数を呼び出すことで値を読み取ることができます。 テンプレートでの双方向バインディングの詳細については、[双方向バインディング](guide/templates/two-way-binding)を参照してください。 @@ -239,7 +257,9 @@ export class MediaControls { コンポーネントまたはディレクティブでモデル入力を宣言すると、Angularはそのモデルに対応する[出力](guide/components/outputs)を自動的に作成します。出力の名前は、モデル入力の名前に「Change」が付いたものです。 ```ts -@Directive({ /* ... */ }) +@Directive({ + /* ... */ +}) export class CustomCheckbox { // これにより、「checkedChange」という名前の出力が自動的に作成されます。 // テンプレートで `(checkedChange)="handler()"` を使用して購読できます。 @@ -351,7 +371,9 @@ export class CustomSlider { return this.internalValue; } - set value(newValue: number) { this.internalValue = newValue; } + set value(newValue: number) { + this.internalValue = newValue; + } private internalValue = 0; } diff --git a/adev-ja/src/content/guide/components/outputs.en.md b/adev-ja/src/content/guide/components/outputs.en.md index 57a302cc3..b21550ef9 100644 --- a/adev-ja/src/content/guide/components/outputs.en.md +++ b/adev-ja/src/content/guide/components/outputs.en.md @@ -5,7 +5,9 @@ TIP: This guide assumes you've already read the [Essentials Guide](essentials). Angular components can define custom events by assigning a property to the `output` function: ```ts {highlight:[3]} -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class ExpandablePanel { panelClosed = output(); } @@ -18,7 +20,7 @@ export class ExpandablePanel { The `output` function returns an `OutputEmitterRef`. You can emit an event by calling the `emit` method on the `OutputEmitterRef`: ```ts - this.panelClosed.emit(); +this.panelClosed.emit(); ``` Angular refers to properties initialized with the `output` function as **outputs**. You can use outputs to raise custom events, similar to native browser events like `click`. @@ -43,7 +45,7 @@ this.valueChanged.emit(7); this.thumbDropped.emit({ pointerX: 123, pointerY: 456, -}) +}); ``` When defining an event listener in a template, you can access the event data from the `$event` variable: @@ -71,7 +73,9 @@ export class App { The `output` function accepts a parameter that lets you specify a different name for the event in a template: ```ts -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomSlider { changed = output({alias: 'valueChanged'}); } @@ -93,7 +97,7 @@ from the component instance. The `OutputRef` type includes a `subscribe` method: ```ts const someComponentRef: ComponentRef = viewContainerRef.createComponent(/*...*/); -someComponentRef.instance.someEventProperty.subscribe(eventData => { +someComponentRef.instance.someEventProperty.subscribe((eventData) => { console.log(eventData); }); ``` @@ -101,7 +105,7 @@ someComponentRef.instance.someEventProperty.subscribe(eventData => { Angular automatically cleans up event subscriptions when it destroys components with subscribers. Alternatively, you can manually unsubscribe from an event. The `subscribe` function returns an `OutputRefSubscription` with an `unsubscribe` method: ```ts -const eventSubscription = someComponent.someEventProperty.subscribe(eventData => { +const eventSubscription = someComponent.someEventProperty.subscribe((eventData) => { console.log(eventData); }); @@ -130,7 +134,9 @@ original decorator-based `@Output` API remains fully supported. You can alternatively define custom events by assigning a property to a new `EventEmitter` and adding the `@Output` decorator: ```ts -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class ExpandablePanel { @Output() panelClosed = new EventEmitter(); } @@ -143,7 +149,9 @@ You can emit an event by calling the `emit` method on the `EventEmitter`. The `@Output` decorator accepts a parameter that lets you specify a different name for the event in a template: ```ts -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomSlider { @Output('valueChanged') changed = new EventEmitter(); } diff --git a/adev-ja/src/content/guide/components/outputs.md b/adev-ja/src/content/guide/components/outputs.md index e1941e648..0c27cb940 100644 --- a/adev-ja/src/content/guide/components/outputs.md +++ b/adev-ja/src/content/guide/components/outputs.md @@ -5,7 +5,9 @@ TIP: このガイドは、[基本概念のガイド](essentials) を既読して Angularコンポーネントは、`output`関数にプロパティを割り当てることでカスタムイベントを定義できます。 ```ts {highlight:[3]} -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class ExpandablePanel { panelClosed = output(); } @@ -18,7 +20,7 @@ export class ExpandablePanel { `output`関数は`OutputEmitterRef`を返します。`OutputEmitterRef`の`emit`メソッドを呼び出すことで、イベントを発生させることができます。 ```ts - this.panelClosed.emit(); +this.panelClosed.emit(); ``` Angularでは、`output`関数で初期化されたプロパティを**出力**と呼びます。出力を使用すると、`click`などのネイティブブラウザイベントと同様に、カスタムイベントを発生させることができます。 @@ -43,7 +45,7 @@ this.valueChanged.emit(7); this.thumbDropped.emit({ pointerX: 123, pointerY: 456, -}) +}); ``` テンプレートでイベントリスナーを定義する場合、`$event`変数からイベントデータにアクセスできます。 @@ -71,7 +73,9 @@ export class App { `output`関数は、テンプレートでイベントに異なる名前を指定できるパラメーターを受け入れます。 ```ts -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomSlider { changed = output({alias: 'valueChanged'}); } @@ -93,7 +97,7 @@ export class CustomSlider { ```ts const someComponentRef: ComponentRef = viewContainerRef.createComponent(/*...*/); -someComponentRef.instance.someEventProperty.subscribe(eventData => { +someComponentRef.instance.someEventProperty.subscribe((eventData) => { console.log(eventData); }); ``` @@ -101,7 +105,7 @@ someComponentRef.instance.someEventProperty.subscribe(eventData => { Angularは、サブスクライバーを持つコンポーネントを破棄するときに、イベントサブスクリプションを自動的にクリーンアップします。または、イベントから手動で購読解除できます。`subscribe`関数は、`unsubscribe`メソッドを持つ`OutputRefSubscription`を返します。 ```ts -const eventSubscription = someComponent.someEventProperty.subscribe(eventData => { +const eventSubscription = someComponent.someEventProperty.subscribe((eventData) => { console.log(eventData); }); @@ -130,7 +134,9 @@ TIP: Angularチームは新規プロジェクトでは`output`関数の使用を 代替として、新しい`EventEmitter`にプロパティを割り当て、`@Output`デコレーターを追加することで、カスタムイベントを定義できます。 ```ts -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class ExpandablePanel { @Output() panelClosed = new EventEmitter(); } @@ -143,7 +149,9 @@ export class ExpandablePanel { `@Output`デコレーターは、テンプレートでイベントに異なる名前を指定できるパラメーターを受け入れます。 ```ts -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomSlider { @Output('valueChanged') changed = new EventEmitter(); } diff --git a/adev-ja/src/content/guide/components/programmatic-rendering.en.md b/adev-ja/src/content/guide/components/programmatic-rendering.en.md index 51887ae35..f1d2a2f05 100644 --- a/adev-ja/src/content/guide/components/programmatic-rendering.en.md +++ b/adev-ja/src/content/guide/components/programmatic-rendering.en.md @@ -174,12 +174,20 @@ export class AppWarningComponent { ``` ```ts -import { Component, ViewContainerRef, signal, inputBinding, outputBinding, twoWayBinding, inject } from '@angular/core'; -import { FocusTrap } from "@angular/cdk/a11y"; -import { ThemeDirective } from '../theme.directive'; +import { + Component, + ViewContainerRef, + signal, + inputBinding, + outputBinding, + twoWayBinding, + inject, +} from '@angular/core'; +import {FocusTrap} from '@angular/cdk/a11y'; +import {ThemeDirective} from '../theme.directive'; @Component({ - template: `` + template: ``, }) export class HostComponent { private vcr = inject(ViewContainerRef); @@ -193,12 +201,12 @@ export class HostComponent { twoWayBinding('isExpanded', this.isExpanded), outputBinding('close', (confirmed) => { console.log('Closed with result:', confirmed); - }) + }), ], directives: [ FocusTrap, - { type: ThemeDirective, bindings: [inputBinding('theme', () => 'warning')] } - ] + {type: ThemeDirective, bindings: [inputBinding('theme', () => 'warning')]}, + ], }); } } @@ -220,9 +228,9 @@ import { inputBinding, outputBinding, } from '@angular/core'; -import { PopupComponent } from './popup.component'; +import {PopupComponent} from './popup.component'; -@Injectable({ providedIn: 'root' }) +@Injectable({providedIn: 'root'}) export class PopupService { private readonly injector = inject(EnvironmentInjector); private readonly appRef = inject(ApplicationRef); diff --git a/adev-ja/src/content/guide/components/programmatic-rendering.md b/adev-ja/src/content/guide/components/programmatic-rendering.md index 114794618..1d7631d64 100644 --- a/adev-ja/src/content/guide/components/programmatic-rendering.md +++ b/adev-ja/src/content/guide/components/programmatic-rendering.md @@ -174,12 +174,20 @@ export class AppWarningComponent { ``` ```ts -import { Component, ViewContainerRef, signal, inputBinding, outputBinding, twoWayBinding, inject } from '@angular/core'; -import { FocusTrap } from "@angular/cdk/a11y"; -import { ThemeDirective } from '../theme.directive'; +import { + Component, + ViewContainerRef, + signal, + inputBinding, + outputBinding, + twoWayBinding, + inject, +} from '@angular/core'; +import {FocusTrap} from '@angular/cdk/a11y'; +import {ThemeDirective} from '../theme.directive'; @Component({ - template: `` + template: ``, }) export class HostComponent { private vcr = inject(ViewContainerRef); @@ -193,12 +201,12 @@ export class HostComponent { twoWayBinding('isExpanded', this.isExpanded), outputBinding('close', (confirmed) => { console.log('Closed with result:', confirmed); - }) + }), ], directives: [ FocusTrap, - { type: ThemeDirective, bindings: [inputBinding('theme', () => 'warning')] } - ] + {type: ThemeDirective, bindings: [inputBinding('theme', () => 'warning')]}, + ], }); } } @@ -220,9 +228,9 @@ import { inputBinding, outputBinding, } from '@angular/core'; -import { PopupComponent } from './popup.component'; +import {PopupComponent} from './popup.component'; -@Injectable({ providedIn: 'root' }) +@Injectable({providedIn: 'root'}) export class PopupService { private readonly injector = inject(EnvironmentInjector); private readonly appRef = inject(ApplicationRef); diff --git a/adev-ja/src/content/guide/components/queries.en.md b/adev-ja/src/content/guide/components/queries.en.md index de451090d..512067b0f 100644 --- a/adev-ja/src/content/guide/components/queries.en.md +++ b/adev-ja/src/content/guide/components/queries.en.md @@ -7,7 +7,7 @@ A component can define **queries** that find child elements and read values from Developers most commonly use queries to retrieve references to child components, directives, DOM elements, and more. All query functions return signals that reflect the most up-to-date results. You can read the -result by calling the signal function, including in reactive contexts like `computed` and `effect`. +result by calling the signal function, including in [reactive contexts](guide/signals#reactive-contexts) like `computed` and `effect`. There are two categories of query: **view queries** and **content queries.** @@ -149,7 +149,9 @@ If a child query (`viewChild` or `contentChild`) does not find a result, its val In some cases, especially with `viewChild`, you know with certainty that a specific child is always available. In other cases, you may want to strictly enforce that a specific child is present. For these cases, you can use a _required query_. ```ts -@Component({/* ... */}) +@Component({ + /* ... */ +}) export class CustomCard { header = viewChild.required(CustomCardHeader); body = contentChild.required(CustomCardBody); @@ -216,8 +218,9 @@ All query functions accept an options object as a second parameter. These option By default, the query locator indicates both the element you're searching for and the value retrieved. You can alternatively specify the `read` option to retrieve a different value from the element matched by the locator. ```ts - -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomExpando { toggle = contentChild(ExpandoContent, {read: TemplateRef}); } @@ -267,7 +270,7 @@ original decorator-based query APIs remain fully supported. You can alternatively declare queries by adding the corresponding decorator to a property. Decorator-based queries behave the same way as signal-based queries except as described below. -### View queries +### View queries {#decorator-view-queries} You can query for a single result with the `@ViewChild` decorator. @@ -330,11 +333,11 @@ export class CustomCard { `@ViewChildren` creates a `QueryList` object that contains the query results. You can subscribe to changes to the query results over time via the `changes` property. -### Content queries +### Content queries {#decorator-content-queries} You can query for a single result with the `@ContentChild` decorator. -```angular-ts {highlight: [14, 16, 17, 18, 25]} +```angular-ts {highlight: [15, 16, 17, 18, 19, 26]} @Component({ selector: 'custom-toggle', /*...*/ diff --git a/adev-ja/src/content/guide/components/queries.md b/adev-ja/src/content/guide/components/queries.md index 283d7d2a8..4be5c09f5 100644 --- a/adev-ja/src/content/guide/components/queries.md +++ b/adev-ja/src/content/guide/components/queries.md @@ -7,7 +7,7 @@ TIP: このガイドでは、[基本概念のガイド](essentials)を読んで 開発者は、クエリを使用して、子コンポーネント、ディレクティブ、DOM要素などの参照を取得することがほとんどです。 すべてのクエリ関数は、最新の結果を反映するシグナルを返します。 -`computed`や`effect`などのリアクティブなコンテキストでも、シグナル関数を呼び出すことで結果を読み取れます。 +`computed`や`effect`などの[リアクティブなコンテキスト](guide/signals#reactive-contexts)でも、シグナル関数を呼び出すことで結果を読み取れます。 クエリには、**ビュークエリ**と**コンテンツクエリ**の2つのカテゴリーがあります。 @@ -149,7 +149,9 @@ export class UserProfile { } 場合によっては、特に`viewChild`を使用する場合、特定の子が常に利用可能であることが確実な場合があります。他の場合では、特定の子が存在することを厳格に適用したい場合があります。これらの場合、_必須クエリ_を使用できます。 ```ts -@Component({/* ... */}) +@Component({ + /* ... */ +}) export class CustomCard { header = viewChild.required(CustomCardHeader); body = contentChild.required(CustomCardBody); @@ -216,8 +218,9 @@ export class CustomList { デフォルトでは、クエリロケーターは、検索対象の要素と取得される値の両方を示します。代わりに、`read`オプションを指定して、ロケーターによって一致した要素から別の値を取得できます。 ```ts - -@Component({/*...*/}) +@Component({ + /*...*/ +}) export class CustomExpando { toggle = contentChild(ExpandoContent, {read: TemplateRef}); } @@ -267,7 +270,7 @@ TIP: Angularチームは新規プロジェクトにはシグナルベースの 代わりに、対応するデコレーターをプロパティに追加することでもクエリを宣言できます。デコレーターベースのクエリは、下記で説明する点を除いて、シグナルベースのクエリと同じように動作します。 -### ビュークエリ +### ビュークエリ {#decorator-view-queries} `@ViewChild`デコレーターを使用して、単一の結果をクエリできます。 @@ -330,11 +333,11 @@ export class CustomCard { `@ViewChildren`は、クエリ結果を含む`QueryList`オブジェクトを作成します。`changes`プロパティを使用して、時間の経過とともにクエリ結果の変更を購読できます。 -### コンテンツクエリ +### コンテンツクエリ {#decorator-content-queries} `@ContentChild`デコレーターを使用して、単一の結果をクエリできます。 -```angular-ts {highlight: [14, 16, 17, 18, 25]} +```angular-ts {highlight: [15, 16, 17, 18, 19, 26]} @Component({ selector: 'custom-toggle', /* ... */ From 52f623a7cc0821d4c06f69bd85f72edeb99cf62a Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 11:17:39 +0900 Subject: [PATCH 09/32] docs(guide/di): migrate .en.md changes --- .../di/creating-and-using-services.en.md | 14 ++-- .../guide/di/creating-and-using-services.md | 14 ++-- .../di/dependency-injection-context.en.md | 26 ++++--- .../guide/di/dependency-injection-context.md | 26 ++++--- .../src/content/guide/di/di-in-action.en.md | 1 - adev-ja/src/content/guide/di/di-in-action.md | 1 - .../hierarchical-dependency-injection.en.md | 39 +++++----- .../di/hierarchical-dependency-injection.md | 39 +++++----- .../di/lightweight-injection-tokens.en.md | 74 +++++++++---------- .../guide/di/lightweight-injection-tokens.md | 22 +++--- adev-ja/src/content/guide/di/overview.en.md | 16 ++-- adev-ja/src/content/guide/di/overview.md | 16 ++-- 12 files changed, 144 insertions(+), 144 deletions(-) diff --git a/adev-ja/src/content/guide/di/creating-and-using-services.en.md b/adev-ja/src/content/guide/di/creating-and-using-services.en.md index f8df5139a..d7b1486f6 100644 --- a/adev-ja/src/content/guide/di/creating-and-using-services.en.md +++ b/adev-ja/src/content/guide/di/creating-and-using-services.en.md @@ -18,18 +18,18 @@ Here is an example of a service that allows users to add and request data: ```ts // 📄 src/app/basic-data-store.ts -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; -@Injectable({ providedIn: 'root' }) +@Injectable({providedIn: 'root'}) export class BasicDataStore { - private data: string[] = [] + private data: string[] = []; addData(item: string): void { - this.data.push(item) + this.data.push(item); } getData(): string[] { - return [...this.data] + return [...this.data]; } } ``` @@ -73,8 +73,8 @@ export class ExampleComponent { ### Injecting into another service ```ts -import { inject, Injectable } from '@angular/core'; -import { AdvancedDataStore } from './advanced-data-store'; +import {inject, Injectable} from '@angular/core'; +import {AdvancedDataStore} from './advanced-data-store'; @Injectable({ providedIn: 'root', diff --git a/adev-ja/src/content/guide/di/creating-and-using-services.md b/adev-ja/src/content/guide/di/creating-and-using-services.md index 2d80bd494..02236a8ff 100644 --- a/adev-ja/src/content/guide/di/creating-and-using-services.md +++ b/adev-ja/src/content/guide/di/creating-and-using-services.md @@ -18,18 +18,18 @@ TypeScriptクラスに`@Injectable()`デコレーターを追加して、手動 ```ts // 📄 src/app/basic-data-store.ts -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; -@Injectable({ providedIn: 'root' }) +@Injectable({providedIn: 'root'}) export class BasicDataStore { - private data: string[] = [] + private data: string[] = []; addData(item: string): void { - this.data.push(item) + this.data.push(item); } getData(): string[] { - return [...this.data] + return [...this.data]; } } ``` @@ -73,8 +73,8 @@ export class ExampleComponent { ### 別のサービスへの注入 {#injecting-into-another-service} ```ts -import { inject, Injectable } from '@angular/core'; -import { AdvancedDataStore } from './advanced-data-store'; +import {inject, Injectable} from '@angular/core'; +import {AdvancedDataStore} from './advanced-data-store'; @Injectable({ providedIn: 'root', diff --git a/adev-ja/src/content/guide/di/dependency-injection-context.en.md b/adev-ja/src/content/guide/di/dependency-injection-context.en.md index b04d6a7e7..e0b6f7aec 100644 --- a/adev-ja/src/content/guide/di/dependency-injection-context.en.md +++ b/adev-ja/src/content/guide/di/dependency-injection-context.en.md @@ -23,10 +23,12 @@ Some APIs are designed to be run in an injection context. This is the case, for Here is an example for `CanActivateFn` ```ts {highlight: [3]} -const canActivateTeam: CanActivateFn = - (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { - return inject(PermissionsService).canActivate(inject(UserToken), route.params.id); - }; +const canActivateTeam: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +) => { + return inject(PermissionsService).canActivate(inject(UserToken), route.params.id); +}; ``` ## Run within an injection context @@ -36,7 +38,7 @@ This requires access to a given injector, like the `EnvironmentInjector`, for ex ```ts {highlight: [9], header"hero.service.ts"} @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class HeroService { private environmentInjector = inject(EnvironmentInjector); @@ -56,21 +58,23 @@ Note that [`inject`](/api/core/inject) will return an instance only if the injec Angular provides the `assertInInjectionContext` helper function to assert that the current context is an injection context and throws a clear error if not. Pass a reference to the calling function so the error message points to the correct API entry point. This produces a clearer, more actionable message than the default generic injection error. ```ts -import { ElementRef, assertInInjectionContext, inject } from '@angular/core'; +import {ElementRef, assertInInjectionContext, inject} from '@angular/core'; export function injectNativeElement(): T { - assertInInjectionContext(injectNativeElement); - return inject(ElementRef).nativeElement; + assertInInjectionContext(injectNativeElement); + return inject(ElementRef).nativeElement; } ``` You can then call this helper **from an injection context** (constructor, field initializer, provider factory, or code executed via `runInInjectionContext`): ```ts -import { Component, inject } from '@angular/core'; -import { injectNativeElement } from './dom-helpers'; +import {Component, inject} from '@angular/core'; +import {injectNativeElement} from './dom-helpers'; -@Component({ /* … */ }) +@Component({ + /* … */ +}) export class PreviewCard { readonly hostEl = injectNativeElement(); // Field initializer runs in an injection context. diff --git a/adev-ja/src/content/guide/di/dependency-injection-context.md b/adev-ja/src/content/guide/di/dependency-injection-context.md index 4044fe77c..d986c1313 100644 --- a/adev-ja/src/content/guide/di/dependency-injection-context.md +++ b/adev-ja/src/content/guide/di/dependency-injection-context.md @@ -23,10 +23,12 @@ NOTE: クラスコンストラクターとフィールドイニシャライザ `CanActivateFn` の例を次に示します。 ```ts {highlight: [3]} -const canActivateTeam: CanActivateFn = - (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { - return inject(PermissionsService).canActivate(inject(UserToken), route.params.id); - }; +const canActivateTeam: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +) => { + return inject(PermissionsService).canActivate(inject(UserToken), route.params.id); +}; ``` ## 注入コンテキスト内で実行する @@ -36,7 +38,7 @@ const canActivateTeam: CanActivateFn = ```ts {highlight: [9], header"hero.service.ts"} @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class HeroService { private environmentInjector = inject(EnvironmentInjector); @@ -56,21 +58,23 @@ export class HeroService { Angularは、現在のコンテキストが注入コンテキストであることをアサートし、そうでない場合は明確なエラーをスローするための `assertInInjectionContext` ヘルパー関数を提供します。呼び出し元の関数への参照を渡すと、エラーメッセージが正しいAPIエントリーポイントを指し示すようになります。これにより、デフォルトの汎用的な注入エラーよりも明確で実用的なメッセージが生成されます。 ```ts -import { ElementRef, assertInInjectionContext, inject } from '@angular/core'; +import {ElementRef, assertInInjectionContext, inject} from '@angular/core'; export function injectNativeElement(): T { - assertInInjectionContext(injectNativeElement); - return inject(ElementRef).nativeElement; + assertInInjectionContext(injectNativeElement); + return inject(ElementRef).nativeElement; } ``` このヘルパーは、**注入コンテキスト内から**(コンストラクター、フィールドイニシャライザー、プロバイダーファクトリー、または `runInInjectionContext` 経由で実行されるコード)呼び出すことができます。 ```ts -import { Component, inject } from '@angular/core'; -import { injectNativeElement } from './dom-helpers'; +import {Component, inject} from '@angular/core'; +import {injectNativeElement} from './dom-helpers'; -@Component({ /* … */ }) +@Component({ + /* … */ +}) export class PreviewCard { readonly hostEl = injectNativeElement(); // フィールドイニシャライザーは注入コンテキスト内で実行されます。 diff --git a/adev-ja/src/content/guide/di/di-in-action.en.md b/adev-ja/src/content/guide/di/di-in-action.en.md index 423d7fff3..5d866b3cf 100644 --- a/adev-ja/src/content/guide/di/di-in-action.en.md +++ b/adev-ja/src/content/guide/di/di-in-action.en.md @@ -24,7 +24,6 @@ export class HighlightDirective { this.element.nativeElement.style.color = 'red'; } } - ``` ## Inject the host element's tag name diff --git a/adev-ja/src/content/guide/di/di-in-action.md b/adev-ja/src/content/guide/di/di-in-action.md index 149e4a78b..f1b419e12 100644 --- a/adev-ja/src/content/guide/di/di-in-action.md +++ b/adev-ja/src/content/guide/di/di-in-action.md @@ -24,7 +24,6 @@ export class HighlightDirective { this.element.nativeElement.style.color = 'red'; } } - ``` ## ホスト要素のタグ名を注入する {#inject-the-host-elements-tag-name} diff --git a/adev-ja/src/content/guide/di/hierarchical-dependency-injection.en.md b/adev-ja/src/content/guide/di/hierarchical-dependency-injection.en.md index 1b54c0a12..f9a09ce17 100644 --- a/adev-ja/src/content/guide/di/hierarchical-dependency-injection.en.md +++ b/adev-ja/src/content/guide/di/hierarchical-dependency-injection.en.md @@ -37,10 +37,10 @@ Tree-shaking is especially useful for a library because the application which us Provide services using `providedIn` of `@Injectable()` as follows: ```ts {highlight:[4]} -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; @Injectable({ - providedIn: 'root' // <--provides this service in the root EnvironmentInjector + providedIn: 'root', // <--provides this service in the root EnvironmentInjector }) export class ItemService { name = 'telephone'; @@ -109,9 +109,7 @@ You can do this to configure a non-default provider of a service that is shared Here is an example of the case where the component router configuration includes a non-default [location strategy](guide/routing#location-strategy) by listing its provider in the `providers` list of the `ApplicationConfig`. ```ts -providers: [ - { provide: LocationStrategy, useClass: HashLocationStrategy } -] +providers: [{provide: LocationStrategy, useClass: HashLocationStrategy}]; ``` For `NgModule` based applications, configure app-wide providers in the `AppModule` `providers`. @@ -214,7 +212,7 @@ For example, in the following `SelfNoDataComponent`, notice the injected `LeafSe @Component({ selector: 'app-self-no-data', templateUrl: './self-no-data.component.html', - styleUrls: ['./self-no-data.component.css'] + styleUrls: ['./self-no-data.component.css'], }) export class SelfNoDataComponent { public leaf = inject(LeafService, {optional: true, self: true}); @@ -261,7 +259,7 @@ This is when you'd use `skipSelf`: templateUrl: './skipself.component.html', styleUrls: ['./skipself.component.css'], // Angular would ignore this LeafService instance - providers: [{ provide: LeafService, useValue: { emoji: '🍁' } }] + providers: [{provide: LeafService, useValue: {emoji: '🍁'}}], }) export class SkipselfComponent { // Use skipSelf as inject option @@ -280,7 +278,7 @@ In the following example, the `Person` service is injected during property initi ```ts class Person { - parent = inject(Person, {optional: true, skipSelf: true}) + parent = inject(Person, {optional: true, skipSelf: true}); } ``` @@ -317,7 +315,7 @@ Import each of them from `@angular/core` and use each in the component class con ```ts {header:"self-no-data.component.ts" highlight:[2]} export class SelfNoDataComponent { - constructor(@Self() @Optional() public leaf?: LeafService) { } + constructor(@Self() @Optional() public leaf?: LeafService) {} } ``` @@ -330,9 +328,7 @@ Understanding the underlying logical structure of the Angular template will give Components are used in your templates, as in the following example: ```html - - ; - + ; ``` HELPFUL: Usually, you declare the components and their templates in separate files. @@ -387,7 +383,7 @@ The example application has a `FlowerService` provided in `root` with an `emoji` ```ts {header:"lower.service.ts"} @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class FlowerService { emoji = '🌺'; @@ -398,9 +394,9 @@ Consider an application with only an `AppComponent` and a `ChildComponent`. The most basic rendered view would look like nested HTML elements such as the following: ```html - - - + + + ``` @@ -425,7 +421,7 @@ Knowledge of this structure can inform how you provide and inject your services, Now, consider that `` injects the `FlowerService`: ```typescript -export class AppComponent { +export class AppComponent { flower = inject(FlowerService); } ``` @@ -599,7 +595,8 @@ In the `ChildComponent` template, add the following binding: Additionally, add the same to the `AppComponent` template: ```html -

Emoji from AnimalService: {{animal.emoji}}

s +

Emoji from AnimalService: {{animal.emoji}}

+s ``` Now you should see both values in the browser: @@ -757,7 +754,7 @@ To alter where the injector starts looking for `FlowerService`, add `skipSelf` t This invocation is a property initializer the `` as shown in `child.component.ts`: ```typescript - flower = inject(FlowerService, { skipSelf: true }) +flower = inject(FlowerService, {skipSelf: true}); ``` With `skipSelf`, the `` injector doesn't look to itself for the `FlowerService`. @@ -900,7 +897,7 @@ Here are `host` and `skipSelf` in the `animal` property initialization: ```typescript export class ChildComponent { - animal = inject(AnimalService, { host: true, skipSelf: true }); + animal = inject(AnimalService, {host: true, skipSelf: true}); } ``` @@ -1085,7 +1082,7 @@ Every component would share the same service instance, and each component would To prevent this, configure the component-level injector of `HeroTaxReturnComponent` to provide the service, using the `providers` property in the component metadata. ```typescript - providers: [HeroTaxReturnService] +providers: [HeroTaxReturnService]; ``` The `HeroTaxReturnComponent` has its own provider of the `HeroTaxReturnService`. diff --git a/adev-ja/src/content/guide/di/hierarchical-dependency-injection.md b/adev-ja/src/content/guide/di/hierarchical-dependency-injection.md index 672b5ce87..78499ddaa 100644 --- a/adev-ja/src/content/guide/di/hierarchical-dependency-injection.md +++ b/adev-ja/src/content/guide/di/hierarchical-dependency-injection.md @@ -37,10 +37,10 @@ Angularには、次の2つのインジェクター階層があります。 `providedIn` を使用して、次のように `@Injectable()` を使用してサービスを提供します。 ```ts {highlight:[4]} -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; @Injectable({ - providedIn: 'root' // <--ルート EnvironmentInjector でこのサービスを提供します + providedIn: 'root', // <--ルート EnvironmentInjector でこのサービスを提供します }) export class ItemService { name = 'telephone'; @@ -109,9 +109,7 @@ stateDiagram-v2 コンポーネントルーターの構成にデフォルト以外の [ロケーション戦略](guide/routing#location-strategy) が含まれている場合、 `ApplicationConfig` の `providers` リストにそのプロバイダーをリストすることによって、その例を示します。 ```ts -providers: [ - { provide: LocationStrategy, useClass: HashLocationStrategy } -] +providers: [{provide: LocationStrategy, useClass: HashLocationStrategy}]; ``` `NgModule` ベースのアプリケーションの場合、アプリケーション全体のプロバイダーを `AppModule` の `providers` で構成します。 @@ -214,7 +212,7 @@ export class OptionalComponent { @Component({ selector: 'app-self-no-data', templateUrl: './self-no-data.component.html', - styleUrls: ['./self-no-data.component.css'] + styleUrls: ['./self-no-data.component.css'], }) export class SelfNoDataComponent { public leaf = inject(LeafService, {optional: true, self: true}); @@ -261,7 +259,7 @@ export class LeafService { templateUrl: './skipself.component.html', styleUrls: ['./skipself.component.css'], // Angular はこの LeafService インスタンスを無視します - providers: [{ provide: LeafService, useValue: { emoji: '🍁' } }] + providers: [{provide: LeafService, useValue: {emoji: '🍁'}}], }) export class SkipselfComponent { // Use skipSelf as inject option @@ -280,7 +278,7 @@ export class SkipselfComponent { ```ts class Person { - parent = inject(Person, {optional: true, skipSelf: true}) + parent = inject(Person, {optional: true, skipSelf: true}); } ``` @@ -317,7 +315,7 @@ export class HostComponent { ```ts {header:"self-no-data.component.ts" highlight:[2]} export class SelfNoDataComponent { - constructor(@Self() @Optional() public leaf?: LeafService) { } + constructor(@Self() @Optional() public leaf?: LeafService) {} } ``` @@ -330,9 +328,7 @@ Angularテンプレートの基になる論理構造を理解すると、サー コンポーネントは、次の例のようにテンプレートで使用されます。 ```html - - ; - + ; ``` HELPFUL: 通常、コンポーネントとそのテンプレートは別々のファイルに宣言します。 @@ -387,7 +383,7 @@ HELPFUL: 通常、コンポーネントとそのテンプレートは別々の ```ts {header:"lower.service.ts"} @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class FlowerService { emoji = '🌺'; @@ -398,9 +394,9 @@ export class FlowerService { 最も基本的なレンダリングされたビューは、次のようなネストされたHTML要素のように見えます。 ```html - - - + + + ``` @@ -425,7 +421,7 @@ export class FlowerService { 次に、 `` が `FlowerService` を注入しているとします。 ```typescript -export class AppComponent { +export class AppComponent { flower = inject(FlowerService); } ``` @@ -599,7 +595,8 @@ export class ChildComponent { さらに、 `AppComponent` テンプレートにも同じものを追加します。 ```html -

Emoji from AnimalService: {{animal.emoji}}

s +

Emoji from AnimalService: {{animal.emoji}}

+s ``` これで、ブラウザに両方の値が表示されます。 @@ -757,7 +754,7 @@ Emoji from AnimalService: 🐶 この呼び出しは、`child.component.ts` に示すように、`` のプロパティ初期化子です。 ```typescript - flower = inject(FlowerService, { skipSelf: true }) +flower = inject(FlowerService, {skipSelf: true}); ``` `skipSelf` を使用すると、`` インジェクターは `FlowerService` を自分自身で探しません。 @@ -900,7 +897,7 @@ export class ChildComponent { ```typescript export class ChildComponent { - animal = inject(AnimalService, { host: true, skipSelf: true }); + animal = inject(AnimalService, {host: true, skipSelf: true}); } ``` @@ -1085,7 +1082,7 @@ _編集対象の税務申告_ は、 `input` プロパティを介して到着 これを防ぐために、コンポーネントレベルのインジェクター `HeroTaxReturnComponent` を構成して、コンポーネントメタデータの `providers` プロパティを使用してサービスを提供します。 ```typescript - providers: [HeroTaxReturnService] +providers: [HeroTaxReturnService]; ``` `HeroTaxReturnComponent` には、 `HeroTaxReturnService` の独自の提供者がいます。 diff --git a/adev-ja/src/content/guide/di/lightweight-injection-tokens.en.md b/adev-ja/src/content/guide/di/lightweight-injection-tokens.en.md index f526fb0a3..a4f13143d 100644 --- a/adev-ja/src/content/guide/di/lightweight-injection-tokens.en.md +++ b/adev-ja/src/content/guide/di/lightweight-injection-tokens.en.md @@ -29,45 +29,45 @@ This component contains a body and can contain an optional header: ; ``` -In a likely implementation, the `` component uses `@ContentChild()` or `@ContentChildren()` to get `` and ``, as in the following: +In a likely implementation, the `` component uses `contentChild` or `contentChildren` to get `` and ``, as in the following: ```ts {highlight: [14]} -import {Component, ContentChild} from '@angular/core'; +import {Component, contentChild} from '@angular/core'; @Component({ selector: 'lib-header', …, }) -class LibHeaderComponent {} +class LibHeader {} @Component({ selector: 'lib-card', …, }) -class LibCardComponent { - @ContentChild(LibHeaderComponent) header: LibHeaderComponent | null = null; +class LibCard { + readonly header = contentChild(LibHeader); } ``` Because `` is optional, the element can appear in the template in its minimal form, ``. In this case, `` is not used and you would expect it to be tree-shaken, but that is not what happens. -This is because `LibCardComponent` actually contains two references to the `LibHeaderComponent`: +This is because `LibCard` actually contains two references to the `LibHeader`: ```ts -@ContentChild(LibHeaderComponent) header: LibHeaderComponent; +readonly header = contentChild(LibHeader); ``` -- One of these reference is in the _type position_-- that is, it specifies `LibHeaderComponent` as a type: `header: LibHeaderComponent;`. -- The other reference is in the _value position_-- that is, LibHeaderComponent is the value of the `@ContentChild()` parameter decorator: `@ContentChild(LibHeaderComponent)`. +- One of these reference is in the _type position_-- that is, it specifies `LibHeader` as a type: `readonly header: Signal`. +- The other reference is in the _value position_-- that is, `LibHeader` is the value passed into the `contentChild` function: `contentChild(LibHeader)`. The compiler handles token references in these positions differently: - The compiler erases _type position_ references after conversion from TypeScript, so they have no impact on tree-shaking. - The compiler must keep _value position_ references at runtime, which **prevents** the component from being tree-shaken. -In the example, the compiler retains the `LibHeaderComponent` token that occurs in the value position. +In the example, the compiler retains the `LibHeader` token that occurs in the value position. This prevents the referenced component from being tree-shaken, even if the application does not actually use `` anywhere. -If `LibHeaderComponent` 's code, template, and styles combine to become too large, including it unnecessarily can significantly increase the size of the client application. +If `LibHeader` 's code, template, and styles combine to become too large, including it unnecessarily can significantly increase the size of the client application. ## When to use the lightweight injection token pattern @@ -75,20 +75,20 @@ The tree-shaking problem arises when a component is used as an injection token. There are two cases when that can happen: - The token is used in the value position of a [content query](guide/components/queries#content-queries). -- The token is used as a type specifier for constructor injection. +- The token is used with the `inject` function. -In the following example, both uses of the `OtherComponent` token cause retention of `OtherComponent`, preventing it from being tree-shaken when it is not used: +In the following example, both uses of the `CustomOther` token cause retention of `CustomOther`, preventing it from being tree-shaken when it is not used: ```ts {highlight: [[2],[4]]} -class MyComponent { - constructor(@Optional() other: OtherComponent) {} +class App { + private readonly other = inject(CustomOther, {optional: true}); - @ContentChild(OtherComponent) other: OtherComponent | null; + readonly header = contentChild(CustomOther); } ``` Although tokens used only as type specifiers are removed when converted to JavaScript, all tokens used for dependency injection are needed at runtime. -These effectively change `constructor(@Optional() other: OtherComponent)` to `constructor(@Optional() @Inject(OtherComponent) other)`. +When using `inject(CustomOther)`, `CustomOther` is passed as a value argument. The token is now in a value position, which causes the tree-shaker to keep the reference. HELPFUL: Libraries should use [tree-shakable providers](guide/di/dependency-injection#providing-dependency) for all services, providing dependencies at the root level rather than in components or modules. @@ -98,40 +98,40 @@ HELPFUL: Libraries should use [tree-shakable providers](guide/di/dependency-inje The lightweight injection token design pattern consists of using a small abstract class as an injection token, and providing the actual implementation at a later stage. The abstract class is retained, not tree-shaken, but it is small and has no material impact on the application size. -The following example shows how this works for the `LibHeaderComponent`: +The following example shows how this works for the `LibHeader`: ```ts {highlight: [[1],[5], [15]]} abstract class LibHeaderToken {} @Component({ selector: 'lib-header', - providers: [{provide: LibHeaderToken, useExisting: LibHeaderComponent}], + providers: [{provide: LibHeaderToken, useExisting: LibHeader}], …, }) -class LibHeaderComponent extends LibHeaderToken {} +class LibHeader extends LibHeaderToken {} @Component({ selector: 'lib-card', …, }) -class LibCardComponent { - @ContentChild(LibHeaderToken) header: LibHeaderToken | null = null; +class LibCard { + readonly header = contentChild(LibHeaderToken); } ``` -In this example, the `LibCardComponent` implementation no longer refers to `LibHeaderComponent` in either the type position or the value position. -This lets full tree-shaking of `LibHeaderComponent` take place. +In this example, the `LibCard` implementation no longer refers to `LibHeader` in either the type position or the value position. +This lets full tree-shaking of `LibHeader` take place. The `LibHeaderToken` is retained, but it is only a class declaration, with no concrete implementation. It is small and does not materially impact the application size when retained after compilation. -Instead, `LibHeaderComponent` itself implements the abstract `LibHeaderToken` class. +Instead, `LibHeader` itself implements the abstract `LibHeaderToken` class. You can safely use that token as the provider in the component definition, allowing Angular to correctly inject the concrete type. To summarize, the lightweight injection token pattern consists of the following: 1. A lightweight injection token that is represented as an abstract class. 2. A component definition that implements the abstract class. -3. Injection of the lightweight pattern, using `@ContentChild()` or `@ContentChildren()`. +3. Injection of the lightweight pattern, using `contentChild` or `contentChildren`. 4. A provider in the implementation of the lightweight injection token which associates the lightweight injection token with the implementation. ### Use the lightweight injection token for API definition @@ -141,8 +141,8 @@ The token is now an abstract class. Since the injectable component implements th The implementation of the method, with all its code overhead, resides in the injectable component that can be tree-shaken. This lets the parent communicate with the child, if it is present, in a type-safe manner. -For example, the `LibCardComponent` now queries `LibHeaderToken` rather than `LibHeaderComponent`. -The following example shows how the pattern lets `LibCardComponent` communicate with the `LibHeaderComponent` without actually referring to `LibHeaderComponent`: +For example, the `LibCard` now queries `LibHeaderToken` rather than `LibHeader`. +The following example shows how the pattern lets `LibCard` communicate with the `LibHeader` without actually referring to `LibHeader`: ```ts {highlight: [[2],[9],[11],[19]]} abstract class LibHeaderToken { @@ -151,9 +151,9 @@ abstract class LibHeaderToken { @Component({ selector: 'lib-header', - providers: [{provide: LibHeaderToken, useExisting: LibHeaderComponent}], + providers: [{provide: LibHeaderToken, useExisting: LibHeader}], }) -class LibHeaderComponent extends LibHeaderToken { +class LibHeader extends LibHeaderToken { doSomething(): void { // Concrete implementation of `doSomething` } @@ -162,12 +162,12 @@ class LibHeaderComponent extends LibHeaderToken { @Component({ selector: 'lib-card', }) -class LibCardComponent implements AfterContentInit { - @ContentChild(LibHeaderToken) header: LibHeaderToken | null = null; +class LibCard implements AfterContentInit { + readonly header = contentChild(LibHeaderToken); ngAfterContentInit(): void { - if (this.header !== null) { - this.header?.doSomething(); + if (this.header() !== undefined) { + this.header()!.doSomething(); } } } @@ -180,8 +180,8 @@ If the child component has been tree-shaken, there is no runtime reference to it ### Naming your lightweight injection token Lightweight injection tokens are only useful with components. -The Angular style guide suggests that you name components using the "Component" suffix. -The example "LibHeaderComponent" follows this convention. +The [Angular Style Guide](style-guide) suggests that you name components without the suffix `Component`. +The example `LibHeader` follows this convention. You should maintain the relationship between the component and its token while still distinguishing between them. -The recommended style is to use the component base name with the suffix "`Token`" to name your lightweight injection tokens: "`LibHeaderToken`." +The recommended style is to use the component base name with the suffix `Token` to name your lightweight injection tokens: `LibHeaderToken`. diff --git a/adev-ja/src/content/guide/di/lightweight-injection-tokens.md b/adev-ja/src/content/guide/di/lightweight-injection-tokens.md index 51669ad4c..71ca7f279 100644 --- a/adev-ja/src/content/guide/di/lightweight-injection-tokens.md +++ b/adev-ja/src/content/guide/di/lightweight-injection-tokens.md @@ -29,45 +29,45 @@ Angularがインジェクショントークンを格納する方法により、 ; ``` -一般的な実装では、 `` コンポーネントは、次の例のように `@ContentChild()` または `@ContentChildren()` を使用して `` と `` を取得します。 +一般的な実装では、 `` コンポーネントは、次の例のように `contentChild` または `contentChildren` を使用して `` と `` を取得します。 ```ts {highlight: [14]} -import {Component, ContentChild} from '@angular/core'; +import {Component, contentChild} from '@angular/core'; @Component({ selector: 'lib-header', …, }) -class LibHeaderComponent {} +class LibHeader {} @Component({ selector: 'lib-card', …, }) -class LibCardComponent { - @ContentChild(LibHeaderComponent) header: LibHeaderComponent | null = null; +class LibCard { + readonly header = contentChild(LibHeader); } ``` `` はオプションなので、要素はテンプレートに最小限の形式 `` で表示できます。 この場合、 `` は使用されず、ツリーシェイクされることを期待しますが、実際にはそうなりません。 -これは、 `LibCardComponent` には `LibHeaderComponent` への参照が2つあるためです。 +これは、 `LibCard` には `LibHeader` への参照が2つあるためです。 ```ts -@ContentChild(LibHeaderComponent) header: LibHeaderComponent; +readonly header = contentChild(LibHeader); ``` -- これらの参照の1つは _型の位置_ にあります。つまり、 `LibHeaderComponent` を型として指定します: `header: LibHeaderComponent;`。 -- もう1つの参照は _値の位置_ にあります。つまり、LibHeaderComponentは `@ContentChild()` パラメータデコレーターの値です: `@ContentChild(LibHeaderComponent)`。 +- これらの参照の1つは _型の位置_ にあります。つまり、 `LibHeader` を型として指定します: `readonly header: Signal`。 +- もう1つの参照は _値の位置_ にあります。つまり、`LibHeader` は `contentChild` 関数に渡される値です: `contentChild(LibHeader)`。 コンパイラはこれらの位置にあるトークン参照を異なる方法で処理します。 - コンパイラは、TypeScriptから変換した後の _型の位置_ の参照を消去するため、ツリーシェイクには影響しません。 - コンパイラは、_値の位置_ の参照をランタイムに保持する必要があり、これが**妨げます** コンポーネントがツリーシェイクされること。 -この例では、コンパイラは値位置にある `LibHeaderComponent` トークンを保持します。 +この例では、コンパイラは値位置にある `LibHeader` トークンを保持します。 これにより、アプリケーションで実際に `` をどこでも使用していない場合でも、参照されるコンポーネントがツリーシェイクされることがなくなります。 -`LibHeaderComponent` のコード、テンプレート、スタイルを組み合わせると大きくなりすぎるため、不要に含めるとクライアントアプリケーションのサイズが大幅に増加する可能性があります。 +`LibHeader` のコード、テンプレート、スタイルを組み合わせると大きくなりすぎるため、不要に含めるとクライアントアプリケーションのサイズが大幅に増加する可能性があります。 ## 軽量インジェクショントークンパターンを使用する場合 diff --git a/adev-ja/src/content/guide/di/overview.en.md b/adev-ja/src/content/guide/di/overview.en.md index 5a37b7ecf..cb0829d21 100644 --- a/adev-ja/src/content/guide/di/overview.en.md +++ b/adev-ja/src/content/guide/di/overview.en.md @@ -46,16 +46,16 @@ Common types of services include: The following example declares a service named `AnalyticsLogger`: ```ts -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; -@Injectable({ providedIn: 'root' }) +@Injectable({providedIn: 'root'}) export class AnalyticsLogger { trackEvent(category: string, value: string) { console.log('Analytics event logged:', { category, value, - timestamp: new Date().toISOString() - }) + timestamp: new Date().toISOString(), + }); } } ``` @@ -119,10 +119,10 @@ export class MyDirective { ``` ```ts -import { Injectable, inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import {Injectable, inject} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; -@Injectable({ providedIn: 'root' }) +@Injectable({providedIn: 'root'}) export class MyService { // ✅ In a service private http = inject(HttpClient); @@ -134,7 +134,7 @@ export const authGuard = () => { // ✅ In a route guard const auth = inject(AuthService); return auth.isAuthenticated(); -} +}; ``` Angular uses the term "injection context" to describe any place in your code where you can call [`inject`](/api/core/inject). While component, directive, and service construction is the most common, see [injection contexts](/guide/di/dependency-injection-context) for more details. diff --git a/adev-ja/src/content/guide/di/overview.md b/adev-ja/src/content/guide/di/overview.md index d8d4de9bb..e2964a7e4 100644 --- a/adev-ja/src/content/guide/di/overview.md +++ b/adev-ja/src/content/guide/di/overview.md @@ -46,16 +46,16 @@ Angular _サービス_ は `@Injectable` で装飾されたTypeScriptクラス 次の例は、`AnalyticsLogger` という名前のサービスを宣言しています。 ```ts -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; -@Injectable({ providedIn: 'root' }) +@Injectable({providedIn: 'root'}) export class AnalyticsLogger { trackEvent(category: string, value: string) { console.log('Analytics event logged:', { category, value, - timestamp: new Date().toISOString() - }) + timestamp: new Date().toISOString(), + }); } } ``` @@ -119,10 +119,10 @@ export class MyDirective { ``` ```ts -import { Injectable, inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import {Injectable, inject} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; -@Injectable({ providedIn: 'root' }) +@Injectable({providedIn: 'root'}) export class MyService { // ✅ サービス内 private http = inject(HttpClient); @@ -134,7 +134,7 @@ export const authGuard = () => { // ✅ ルートガード内 const auth = inject(AuthService); return auth.isAuthenticated(); -} +}; ``` Angularは「注入コンテキスト」という用語を使用して、コード内で [`inject`](/api/core/inject) を呼び出せる場所を説明します。コンポーネント、ディレクティブ、サービスの構築が最も一般的ですが、詳細については[注入コンテキスト](/guide/di/dependency-injection-context)を参照してください。 From a82edad9c1d638b25acb50f3cf6c2c946fef91bc Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 11:17:59 +0900 Subject: [PATCH 10/32] docs(guide/directives): migrate .en.md changes --- .../directives/attribute-directives.en.md | 2 +- .../guide/directives/attribute-directives.md | 4 +-- .../directive-composition-api.en.md | 32 +++++++++++-------- .../directives/directive-composition-api.md | 32 +++++++++++-------- .../directives/structural-directives.en.md | 7 +--- .../guide/directives/structural-directives.md | 7 +--- 6 files changed, 41 insertions(+), 43 deletions(-) diff --git a/adev-ja/src/content/guide/directives/attribute-directives.en.md b/adev-ja/src/content/guide/directives/attribute-directives.en.md index 843c1a400..e07205484 100644 --- a/adev-ja/src/content/guide/directives/attribute-directives.en.md +++ b/adev-ja/src/content/guide/directives/attribute-directives.en.md @@ -67,7 +67,7 @@ The background color appears when the pointer hovers over the paragraph element This section walks you through setting the highlight color while applying the `HighlightDirective`. -1. In `highlight.directive.ts`, import `Input` from `@angular/core`. +1. In `highlight.directive.ts`, import `input` from `@angular/core`. diff --git a/adev-ja/src/content/guide/directives/attribute-directives.md b/adev-ja/src/content/guide/directives/attribute-directives.md index 85e885938..f4ec819d2 100644 --- a/adev-ja/src/content/guide/directives/attribute-directives.md +++ b/adev-ja/src/content/guide/directives/attribute-directives.md @@ -67,7 +67,7 @@ HELPFUL: ハンドラーは、ホストDOM要素`el`に色を設定するヘル このセクションでは、`HighlightDirective`を適用するときにハイライトカラーを設定する方法について説明します。 -1. `highlight.directive.ts`で、`@angular/core`から`Input`をインポートします。 +1. `highlight.directive.ts`で、`@angular/core`から`input`をインポートします。 @@ -148,4 +148,4 @@ HELPFUL: ハンドラーは、ホストDOM要素`el`に色を設定するヘル -`ngNonBindable`を親要素に適用すると、Angularは要素の子要素に対して、プロパティバインディングやイベントバインディングなどあらゆる種類の補間とバインディングを無効にします。 +`ngNonBindable`を親要素に適用すると、Angularは要素の子要素に対して、プロパティバインディングやイベントバインディングなどあらゆる種類の補間とバインディングを無効にします。 \ No newline at end of file diff --git a/adev-ja/src/content/guide/directives/directive-composition-api.en.md b/adev-ja/src/content/guide/directives/directive-composition-api.en.md index 53694c00b..4dce2bfe6 100644 --- a/adev-ja/src/content/guide/directives/directive-composition-api.en.md +++ b/adev-ja/src/content/guide/directives/directive-composition-api.en.md @@ -20,7 +20,7 @@ works similarly to applying the `MenuBehavior` to the `` element in template: 'admin-menu.html', hostDirectives: [MenuBehavior], }) -export class AdminMenu { } +export class AdminMenu {} ``` When the framework renders a component, Angular also creates an instance of each host directive. The @@ -45,13 +45,15 @@ in your component's API by expanding the entry in `hostDirectives`: @Component({ selector: 'admin-menu', template: 'admin-menu.html', - hostDirectives: [{ - directive: MenuBehavior, - inputs: ['menuId'], - outputs: ['menuClosed'], - }], + hostDirectives: [ + { + directive: MenuBehavior, + inputs: ['menuId'], + outputs: ['menuClosed'], + }, + ], }) -export class AdminMenu { } +export class AdminMenu {} ``` By explicitly specifying the inputs and outputs, consumers of the component with `hostDirective` can @@ -69,13 +71,15 @@ component: @Component({ selector: 'admin-menu', template: 'admin-menu.html', - hostDirectives: [{ - directive: MenuBehavior, - inputs: ['menuId: id'], - outputs: ['menuClosed: closed'], - }], + hostDirectives: [ + { + directive: MenuBehavior, + inputs: ['menuId: id'], + outputs: ['menuClosed: closed'], + }, + ], }) -export class AdminMenu { } +export class AdminMenu {} ``` ```angular-html @@ -131,7 +135,7 @@ The following example shows minimal use of a host directive: template: 'admin-menu.html', hostDirectives: [MenuBehavior], }) -export class AdminMenu { } +export class AdminMenu {} ``` The order of execution here is: diff --git a/adev-ja/src/content/guide/directives/directive-composition-api.md b/adev-ja/src/content/guide/directives/directive-composition-api.md index 872254564..faf9391d6 100644 --- a/adev-ja/src/content/guide/directives/directive-composition-api.md +++ b/adev-ja/src/content/guide/directives/directive-composition-api.md @@ -20,7 +20,7 @@ Angularディレクティブは、再利用可能な動作をカプセル化す template: 'admin-menu.html', hostDirectives: [MenuBehavior], }) -export class AdminMenu { } +export class AdminMenu {} ``` フレームワークがコンポーネントをレンダリングすると、Angularは各ホストディレクティブのインスタンスも作成します。 @@ -45,13 +45,15 @@ export class AdminMenu { } @Component({ selector: 'admin-menu', template: 'admin-menu.html', - hostDirectives: [{ - directive: MenuBehavior, - inputs: ['menuId'], - outputs: ['menuClosed'], - }], + hostDirectives: [ + { + directive: MenuBehavior, + inputs: ['menuId'], + outputs: ['menuClosed'], + }, + ], }) -export class AdminMenu { } +export class AdminMenu {} ``` 入力と出力を明示的に指定することで、`hostDirective` を持つコンポーネントのコンシューマーは @@ -69,13 +71,15 @@ export class AdminMenu { } @Component({ selector: 'admin-menu', template: 'admin-menu.html', - hostDirectives: [{ - directive: MenuBehavior, - inputs: ['menuId: id'], - outputs: ['menuClosed: closed'], - }], + hostDirectives: [ + { + directive: MenuBehavior, + inputs: ['menuId: id'], + outputs: ['menuClosed: closed'], + }, + ], }) -export class AdminMenu { } +export class AdminMenu {} ``` ```angular-html @@ -131,7 +135,7 @@ export class SpecializedMenuWithTooltip { } template: 'admin-menu.html', hostDirectives: [MenuBehavior], }) -export class AdminMenu { } +export class AdminMenu {} ``` ここでの実行順序は次のとおりです。 diff --git a/adev-ja/src/content/guide/directives/structural-directives.en.md b/adev-ja/src/content/guide/directives/structural-directives.en.md index 4c41ed1dd..0e696ce22 100644 --- a/adev-ja/src/content/guide/directives/structural-directives.en.md +++ b/adev-ja/src/content/guide/directives/structural-directives.en.md @@ -85,7 +85,6 @@ export class SelectDirective { private templateRef = inject(TemplateRef); private viewContainerRef = inject(ViewContainerRef); } - ``` @@ -95,7 +94,6 @@ Add a `selectFrom` `input()` property. ```ts export class SelectDirective { // ... - selectFrom = input.required(); } ``` @@ -107,7 +105,6 @@ With `SelectDirective` now scaffolded as a structural directive with its input, ```ts export class SelectDirective { // ... - async ngOnInit() { const data = await this.selectFrom.load(); this.viewContainerRef.createEmbeddedView(this.templateRef, { @@ -129,9 +126,7 @@ That's it - `SelectDirective` is up and running. A follow-up step might be to [a When you write your own structural directives, use the following syntax: ```ts {hideCopy} - -_:prefix="( :let | :expression ) (';' | ',')? ( :let | :as | :keyExp )_" - +_: prefix = "( :let | :expression ) (';' | ',')? ( :let | :as | :keyExp )_"; ``` The following patterns describe each portion of the structural directive grammar: diff --git a/adev-ja/src/content/guide/directives/structural-directives.md b/adev-ja/src/content/guide/directives/structural-directives.md index 9741c73c3..32abd0368 100644 --- a/adev-ja/src/content/guide/directives/structural-directives.md +++ b/adev-ja/src/content/guide/directives/structural-directives.md @@ -85,7 +85,6 @@ export class SelectDirective { private templateRef = inject(TemplateRef); private viewContainerRef = inject(ViewContainerRef); } - ``` @@ -95,7 +94,6 @@ export class SelectDirective { ```ts export class SelectDirective { // ... - selectFrom = input.required(); } ``` @@ -107,7 +105,6 @@ export class SelectDirective { ```ts export class SelectDirective { // ... - async ngOnInit() { const data = await this.selectFrom.load(); this.viewContainerRef.createEmbeddedView(this.templateRef, { @@ -129,9 +126,7 @@ export class SelectDirective { 独自の構造ディレクティブを作成する際は、次の構文を使用します。 ```ts {hideCopy} - -_:prefix="( :let | :expression ) (';' | ',')? ( :let | :as | :keyExp )_" - +_: prefix = "( :let | :expression ) (';' | ',')? ( :let | :as | :keyExp )_"; ``` 次のパターンは、構造ディレクティブ文法の各部分を説明しています。 From a6f103a86af32460333ad5bf8247019bf586ec2f Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 11:22:23 +0900 Subject: [PATCH 11/32] docs(guide): migrate .en.md changes for root files --- adev-ja/src/content/guide/elements.en.md | 20 ++-- adev-ja/src/content/guide/elements.md | 20 ++-- adev-ja/src/content/guide/hydration.en.md | 22 ++++- adev-ja/src/content/guide/hydration.md | 22 ++++- .../content/guide/image-optimization.en.md | 78 ++++----------- .../src/content/guide/image-optimization.md | 78 ++++----------- adev-ja/src/content/guide/security.en.md | 24 ++--- adev-ja/src/content/guide/ssr.en.md | 96 ++++++++++--------- adev-ja/src/content/guide/ssr.md | 96 ++++++++++--------- adev-ja/src/content/guide/tailwind.en.md | 17 ++-- adev-ja/src/content/guide/tailwind.md | 17 ++-- adev-ja/src/content/guide/zoneless.en.md | 8 +- adev-ja/src/content/guide/zoneless.md | 8 +- 13 files changed, 224 insertions(+), 282 deletions(-) diff --git a/adev-ja/src/content/guide/elements.en.md b/adev-ja/src/content/guide/elements.en.md index 9b63ff093..90bb9d2e3 100644 --- a/adev-ja/src/content/guide/elements.en.md +++ b/adev-ja/src/content/guide/elements.en.md @@ -39,9 +39,7 @@ The `createCustomElement()` function converts a component into a class that can After you register your configured class with the browser's custom-element registry, use the new element just like a built-in HTML element in content that you add directly into the DOM: ```html - - ``` When your custom element is placed on a page, the browser creates an instance of the registered class and adds it to the DOM. @@ -121,21 +119,19 @@ Assume you create a `my-dialog` custom element based on the following component: ```ts @Component(/* ... */) class MyDialog { - content = input(string); + content = input(string); } - ``` The most straightforward way to get accurate typings is to cast the return value of the relevant DOM methods to the correct type. For that, use the `NgElement` and `WithProperties` types \(both exported from `@angular/elements`\): ```ts - -const aDialog = document.createElement('my-dialog') as NgElement & WithProperties<{content: string}>; +const aDialog = document.createElement('my-dialog') as NgElement & + WithProperties<{content: string}>; aDialog.content = 'Hello, world!'; aDialog.content = 123; // <-- ERROR: TypeScript knows this should be a string. aDialog.body = 'News'; // <-- ERROR: TypeScript knows there is no `body` property on `aDialog`. - ``` This is a good way to quickly get TypeScript features, such as type checking and autocomplete support, for your custom element. @@ -158,12 +154,10 @@ declare global { Now, TypeScript can infer the correct type the same way it does for built-in elements: ```ts - -document.createElement('div') //--> HTMLDivElement (built-in element) -document.querySelector('foo') //--> Element (unknown element) -document.createElement('my-dialog') //--> NgElement & WithProperties<{content: string}> (custom element) -document.querySelector('my-other-element') //--> NgElement & WithProperties<{foo: 'bar'}> (custom element) - +document.createElement('div'); //--> HTMLDivElement (built-in element) +document.querySelector('foo'); //--> Element (unknown element) +document.createElement('my-dialog'); //--> NgElement & WithProperties<{content: string}> (custom element) +document.querySelector('my-other-element'); //--> NgElement & WithProperties<{foo: 'bar'}> (custom element) ``` ## Limitations diff --git a/adev-ja/src/content/guide/elements.md b/adev-ja/src/content/guide/elements.md index 1b0ab4faa..e09c66fbe 100644 --- a/adev-ja/src/content/guide/elements.md +++ b/adev-ja/src/content/guide/elements.md @@ -39,9 +39,7 @@ _Angular elements_ はAngularコンポーネントを _カスタム要素_(Web 設定したクラスをブラウザのカスタム要素レジストリに登録した後、新しい要素を、DOMに直接追加するコンテンツ内で組み込みのHTML要素と同じように使用します。 ```html - - ``` カスタム要素がページに配置されると、ブラウザは登録されたクラスのインスタンスを作成し、それをDOMに追加します。 @@ -121,21 +119,19 @@ Angularで作成されたカスタム要素は`NgElement`(これはさらに`H ```ts @Component(/* ... */) class MyDialog { - content = input(string); + content = input(string); } - ``` 正確な型定義を取得する最も簡単な方法は、関連するDOMメソッドの戻り値を正しい型にキャストすることです。 そのためには、`NgElement`および`WithProperties`型(両方とも`@angular/elements`からエクスポートされています)を使用します。 ```ts - -const aDialog = document.createElement('my-dialog') as NgElement & WithProperties<{content: string}>; +const aDialog = document.createElement('my-dialog') as NgElement & + WithProperties<{content: string}>; aDialog.content = 'Hello, world!'; aDialog.content = 123; // <-- ERROR: TypeScript knows this should be a string. aDialog.body = 'News'; // <-- ERROR: TypeScript knows there is no `body` property on `aDialog`. - ``` これは、カスタム要素に対して型チェックやオートコンプリートサポートなどのTypeScript機能を素早く利用するための良い方法です。 @@ -158,12 +154,10 @@ declare global { これで、TypeScriptは組み込み要素と同じように正しい型を推論できます。 ```ts - -document.createElement('div') //--> HTMLDivElement (built-in element) -document.querySelector('foo') //--> Element (unknown element) -document.createElement('my-dialog') //--> NgElement & WithProperties<{content: string}> (custom element) -document.querySelector('my-other-element') //--> NgElement & WithProperties<{foo: 'bar'}> (custom element) - +document.createElement('div'); //--> HTMLDivElement (built-in element) +document.querySelector('foo'); //--> Element (unknown element) +document.createElement('my-dialog'); //--> NgElement & WithProperties<{content: string}> (custom element) +document.querySelector('my-other-element'); //--> NgElement & WithProperties<{foo: 'bar'}> (custom element) ``` ## 制限事項 {#limitations} diff --git a/adev-ja/src/content/guide/hydration.en.md b/adev-ja/src/content/guide/hydration.en.md index 6a5d2bffd..e9e71a0a6 100644 --- a/adev-ja/src/content/guide/hydration.en.md +++ b/adev-ja/src/content/guide/hydration.en.md @@ -69,9 +69,7 @@ When an application is rendered on the server, it is visible in a browser as soo import {provideClientHydration, withEventReplay} from '@angular/platform-browser'; bootstrapApplication(App, { - providers: [ - provideClientHydration(withEventReplay()) - ] + providers: [provideClientHydration(withEventReplay())], }); ``` @@ -176,6 +174,24 @@ Keep in mind that adding the `ngSkipHydration` attribute to your root applicatio Application stability is an important part of the hydration process. Hydration and any post-hydration processes only occur once the application has reported stability. There are a number of ways that stability can be delayed. Examples include setting timeouts and intervals, unresolved promises, and pending microtasks. In those cases, you may encounter the [Application remains unstable](errors/NG0506) error, which indicates that your app has not yet reached the stable state after 10 seconds. If you're finding that your application is not hydrating right away, take a look at what is impacting application stability and refactor to avoid causing these delays. +### Debugging Application Stability + +The `provideStabilityDebugging` utility helps identify why your application fails to stabilize. This utility is provided by default in dev mode when using `provideClientHydration`. You can also add it manually to the application providers for use in production bundles or when using SSR without hydration, for example. The feature logs information to the console if the application takes longer than expected to stabilize. + +```typescript +import {provideStabilityDebugging} from '@angular/core'; +import {bootstrapApplication} from '@angular/platform-browser'; +import 'zone.js/plugins/task-tracking'; // Use if you have Zone.js with `provideZoneChangeDetection` + +bootstrapApplication(AppComponent, { + providers: [provideStabilityDebugging()], +}); +``` + +When enabled, the utility logs pending tasks (`PendingTasks`) to the console. If your application uses Zone.js, you can also import `zone.js/plugins/task-tracking` to see which macrotasks are keeping the Angular Zone from stabilizing. This plugin provides the stack trace of the macrotask creation, effectively helping you identify the source of the delay. + +IMPORTANT: Angular does not remove the zone.js task tracking plugin or this utility from production bundles. Use them only for temporary debugging of stability issues during development, including for optimized production builds. + ## I18N HELPFUL: By default, Angular will skip hydration for components that use i18n blocks, effectively re-rendering those components from scratch. diff --git a/adev-ja/src/content/guide/hydration.md b/adev-ja/src/content/guide/hydration.md index e28cfa7f2..44834ec8a 100644 --- a/adev-ja/src/content/guide/hydration.md +++ b/adev-ja/src/content/guide/hydration.md @@ -69,9 +69,7 @@ HELPFUL: Angularの構成要素に切り替えるか、`ngSkipHydration` を使 import {provideClientHydration, withEventReplay} from '@angular/platform-browser'; bootstrapApplication(App, { - providers: [ - provideClientHydration(withEventReplay()) - ] + providers: [provideClientHydration(withEventReplay())], }); ``` @@ -176,6 +174,24 @@ HELPFUL: これによりレンダリングの問題は修正されますが、 アプリケーションの安定性は、ハイドレーションプロセスにおいて重要な部分です。ハイドレーションおよびハイドレーション後のプロセスは、アプリケーションが安定性を報告した後にのみ発生します。安定性が遅延する可能性のある方法はいくつかあります。例としては、タイムアウトとインターバルの設定、未解決のPromise、保留中のマイクロタスクなどがあります。これらの場合、アプリケーションが10秒後に安定状態に達していないことを示す [アプリケーションが不安定なままです](errors/NG0506) エラーに遭遇する可能性があります。アプリケーションがすぐにハイドレーションされない場合は、アプリケーションの安定性に影響を与えているものを見直し、これらの遅延を引き起こさないようにリファクタリングしてください。 +### アプリケーションの安定性のデバッグ {#debugging-application-stability} + +`provideStabilityDebugging` ユーティリティは、アプリケーションが安定化に失敗する理由を特定するのに役立ちます。このユーティリティは、`provideClientHydration` を使用する場合、devモードではデフォルトで提供されます。また、本番バンドルで使用したり、たとえばハイドレーションなしでSSRを使用する場合に、アプリケーションプロバイダーに手動で追加することもできます。この機能は、予想よりもアプリケーションの安定化に時間がかかる場合、コンソールに情報をログ出力します。 + +```typescript +import {provideStabilityDebugging} from '@angular/core'; +import {bootstrapApplication} from '@angular/platform-browser'; +import 'zone.js/plugins/task-tracking'; // `provideZoneChangeDetection` でZone.jsを使用している場合に使用 + +bootstrapApplication(AppComponent, { + providers: [provideStabilityDebugging()], +}); +``` + +有効にすると、ユーティリティは保留中のタスク(`PendingTasks`)をコンソールにログ出力します。アプリケーションでZone.jsを使用している場合は、`zone.js/plugins/task-tracking` をインポートすることで、AngularゾーンがUnstableになる原因となっているマクロタスクを確認することもできます。このプラグインは、マクロタスク作成のスタックトレースを提供し、遅延の原因を効果的に特定するのに役立ちます。 + +IMPORTANT: Angularは、zone.jsタスクトラッキングプラグインやこのユーティリティを本番バンドルから削除しません。これらは、最適化された本番ビルドを含む、開発中の安定性の問題を一時的にデバッグするためにのみ使用してください。 + ## I18N {#i18n} HELPFUL: デフォルトでは、Angularはi18nブロックを使用するコンポーネントのハイドレーションをスキップし、それらのコンポーネントを最初から再レンダリングします。 diff --git a/adev-ja/src/content/guide/image-optimization.en.md b/adev-ja/src/content/guide/image-optimization.en.md index 74696e59b..014f83ec3 100644 --- a/adev-ja/src/content/guide/image-optimization.en.md +++ b/adev-ja/src/content/guide/image-optimization.en.md @@ -28,9 +28,7 @@ If you're using a background image in CSS, [start here](#how-to-migrate-your-bac Import `NgOptimizedImage` directive from `@angular/common`: ```ts - -import { NgOptimizedImage } from '@angular/common' - +import {NgOptimizedImage} from '@angular/common'; ``` and include it into the `imports` array of a standalone component or an NgModule: @@ -42,7 +40,6 @@ imports: [ ], ``` - An image loader is not **required** in order to use NgOptimizedImage, but using one with an image CDN enables powerful performance features, including automatic `srcset`s for your images. @@ -53,9 +50,7 @@ A brief guide for setting up a loader can be found in the [Configuring an Image To activate the `NgOptimizedImage` directive, replace your image's `src` attribute with `ngSrc`. ```html - - - + ``` If you're using a [built-in third-party loader](#built-in-loaders), make sure to omit the base URL path from `src`, as that will be prepended automatically by the loader. @@ -64,9 +59,7 @@ If you're using a [built-in third-party loader](#built-in-loaders), make sure to Always mark the [LCP image](https://web.dev/lcp/#what-elements-are-considered) on your page as `priority` to prioritize its loading. ```html - - - + ``` Marking an image as `priority` applies the following optimizations: @@ -81,9 +74,7 @@ Angular displays a warning during development if the LCP element is an image tha In order to prevent [image-related layout shifts](https://web.dev/css-web-vitals/#images-and-layout-shifts), NgOptimizedImage requires that you specify a height and width for your image, as follows: ```html - - - + ``` For **responsive images** (images which you've styled to grow and shrink relative to the viewport), the `width` and `height` attributes should be the intrinsic size of the image file. For responsive images it's also important to [set a value for `sizes`.](#responsive-images) @@ -101,9 +92,7 @@ In cases where you want to have an image fill a containing element, you can use When you add the `fill` attribute to your image, you do not need and should not include a `width` and `height`, as in this example: ```html - - - + ``` You can use the [object-fit](https://developer.mozilla.org/docs/Web/CSS/object-fit) CSS property to change how the image will fill its container. If you style your image with `object-fit: "contain"`, the image will maintain its aspect ratio and be "letterboxed" to fit the element. If you set `object-fit: "cover"`, the element will retain its aspect ratio, fully fill the element, and some content may be "cropped" off. @@ -133,9 +122,7 @@ You can adjust how the background image fills the container as described in the NgOptimizedImage can display an automatic low-resolution placeholder for your image if you're using a CDN or image host that provides automatic image resizing. Take advantage of this feature by adding the `placeholder` attribute to your image: ```html - - - + ``` Adding this attribute automatically requests a second, smaller version of the image using your specified image loader. This small image will be applied as a `background-image` style with a CSS blur while your image loads. If no image loader is provided, no placeholder image can be generated and an error will be thrown. @@ -160,14 +147,7 @@ If you want sharp edges around your blurred placeholder, you can wrap your image You can also specify a placeholder using a base64 [data URL](https://developer.mozilla.org/docs/Web/HTTP/Basics_of_HTTP/Data_URLs) without an image loader. The data url format is `data:image/[imagetype];[data]`, where `[imagetype]` is the image format, just as `png`, and `[data]` is a base64 encoding of the image. That encoding can be done using the command line or in JavaScript. For specific commands, see [the MDN documentation](https://developer.mozilla.org/docs/Web/HTTP/Basics_of_HTTP/Data_URLs#encoding_data_into_base64_format). An example of a data URL placeholder with truncated data is shown below: ```html - - - + ``` However, large data URLs increase the size of your Angular bundles and slow down page load. If you cannot use an image loader, the Angular team recommends keeping base64 placeholder images smaller than 4KB and using them exclusively on critical images. In addition to decreasing placeholder dimensions, consider changing image formats or parameters used when saving images. At very low resolutions, these parameters can have a large effect on file size. @@ -177,13 +157,7 @@ However, large data URLs increase the size of your Angular bundles and slow down By default, NgOptimizedImage applies a CSS blur effect to image placeholders. To render a placeholder without blur, provide a `placeholderConfig` argument with an object that includes the `blur` property, set to false. For example: ```html - + ``` ## Adjusting image styling @@ -205,9 +179,7 @@ A [`preconnect` resource hint](https://web.dev/preconnect-and-dns-prefetch) for Preconnect links are automatically generated for domains provided as an argument to a [loader](#optional-set-up-a-loader). If an image origin cannot be automatically identified, and no preconnect link is detected for the LCP image, `NgOptimizedImage` will warn during development. In that case, you should manually add a resource hint to `index.html`. Within the `` of the document, add a `link` tag with `rel="preconnect"`, as shown below: ```html - - ``` To disable preconnect warnings, inject the `PRECONNECT_CHECK_BLOCKLIST` token: @@ -233,7 +205,7 @@ If your image should be "fixed" in size (i.e. the same size across devices, exce Example srcset generated: ```html - + ``` #### Responsive images @@ -266,17 +238,13 @@ providers: [ If you would like to manually define a `srcset` attribute, you can provide your own using the `ngSrcset` attribute: ```html - - - + ``` If the `ngSrcset` attribute is present, `NgOptimizedImage` generates and sets the `srcset` based on the sizes included. Do not include image file names in `ngSrcset` - the directive infers this information from `ngSrc`. The directive supports both width descriptors (e.g. `100w`) and density descriptors (e.g. `1x`). ```html - - - + ``` ### Disabling automatic srcset generation @@ -284,9 +252,7 @@ If the `ngSrcset` attribute is present, `NgOptimizedImage` generates and sets th To disable srcset generation for a single image, you can add the `disableOptimizedSrcset` attribute on the image: ```html - - - + ``` ### Disabling image lazy loading @@ -294,9 +260,7 @@ To disable srcset generation for a single image, you can add the `disableOptimiz By default, `NgOptimizedImage` sets `loading=lazy` for all images that are not marked `priority`. You can disable this behavior for non-priority images by setting the `loading` attribute. This attribute accepts values: `eager`, `auto`, and `lazy`. [See the documentation for the standard image `loading` attribute for details](https://developer.mozilla.org/docs/Web/API/HTMLImageElement/loading#value). ```html - - - + ``` ### Controlling image decoding @@ -308,16 +272,16 @@ You can still override this behavior by explicitly setting the `decoding` attrib ```html - + - + - + - + ``` **Allowed values** @@ -331,9 +295,7 @@ You can still override this behavior by explicitly setting the `decoding` attrib You may want to have images displayed at varying widths on differently-sized screens. A common example of this pattern is a grid- or column-based layout that renders a single column on mobile devices, and two columns on larger devices. You can capture this behavior in the `sizes` attribute, using a "media query" syntax, such as the following: ```html - - - + ``` The `sizes` attribute in the above example says "I expect this image to be 100 percent of the screen width on devices under 768px wide. Otherwise, I expect it to be 50 percent of the screen width. @@ -428,9 +390,7 @@ const myCustomLoader = (config: ImageLoaderConfig) => { Note that in the above example, we've invented the 'roundedCorners' property name to control a feature of our custom loader. We could then use this feature when creating an image, as follows: ```html - - - + ``` ## Frequently Asked Questions diff --git a/adev-ja/src/content/guide/image-optimization.md b/adev-ja/src/content/guide/image-optimization.md index 7459cc38e..8945183c4 100644 --- a/adev-ja/src/content/guide/image-optimization.md +++ b/adev-ja/src/content/guide/image-optimization.md @@ -28,9 +28,7 @@ CSSで背景画像を使用している場合は、[こちらから開始して `NgOptimizedImage`ディレクティブを`@angular/common`からインポートします。 ```ts - -import { NgOptimizedImage } from '@angular/common' - +import {NgOptimizedImage} from '@angular/common'; ``` そして、スタンドアロンコンポーネントまたはNgModuleの`imports`配列に含めます。 @@ -42,7 +40,6 @@ imports: [ ], ``` - NgOptimizedImageを使用するために画像ローダーは**必須ではありません**が、画像CDNと組み合わせて使用すると、画像の自動`srcset`を含む強力なパフォーマンス機能が有効になります。 @@ -53,9 +50,7 @@ NgOptimizedImageを使用するために画像ローダーは**必須ではあ `NgOptimizedImage`ディレクティブを有効にするには、画像の`src`属性を`ngSrc`に置き換えます。 ```html - - - + ``` [組み込みのサードパーティローダー](#built-in-loaders)を使用している場合は、`src`からベースURLパスを省略してください。これはローダーによって自動的に前置されます。 @@ -64,9 +59,7 @@ NgOptimizedImageを使用するために画像ローダーは**必須ではあ ページの[LCP画像](https://web.dev/lcp/#what-elements-are-considered)は、その読み込みを優先するために常に`priority`としてマークしてください。 ```html - - - + ``` 画像を`priority`としてマークすると、以下の最適化が適用されます。 @@ -81,9 +74,7 @@ LCP要素が`priority`属性を持たない画像である場合、Angularは開 [画像関連のレイアウトシフト](https://web.dev/css-web-vitals/#images-and-layout-shifts)を防ぐため、NgOptimizedImageでは次のように画像の高さと幅を指定する必要があります。 ```html - - - + ``` **レスポンシブ画像**(ビューポートに対して拡大縮小するようにスタイル設定された画像)の場合、`width`および`height`属性は画像ファイルの固有のサイズである必要があります。レスポンシブ画像の場合、[`sizes`の値を設定する](#responsive-images)ことも重要です。 @@ -101,9 +92,7 @@ NOTE: 画像のサイズが不明な場合は、以下で説明するように 画像に`fill`属性を追加する場合、以下の例のように`width`と`height`を含める必要はなく、含めるべきではありません。 ```html - - - + ``` [object-fit](https://developer.mozilla.org/docs/Web/CSS/object-fit)CSSプロパティを使用して、画像がコンテナをどのように埋めるかを変更できます。画像を`object-fit: "contain"`でスタイル設定すると、画像はアスペクト比を維持し、要素にフィットするように「レターボックス」されます。`object-fit: "cover"`を設定すると、要素はアスペクト比を維持し、要素を完全に埋めますが、一部のコンテンツは「切り取られる」場合があります。 @@ -133,9 +122,7 @@ IMPORTANT:「fill」画像が適切にレンダリングされるには、その NgOptimizedImageは、CDNまたは画像ホストが自動画像リサイズを提供している場合、画像の低解像度の自動プレースホルダーを表示できます。この機能を利用するには、画像に`placeholder`属性を追加します。 ```html - - - + ``` この属性を追加すると、指定された画像ローダーを使用して、画像の2番目の小さいバージョンが自動的にリクエストされます。この小さい画像は、画像が読み込まれる間、CSSのぼかしを伴う`background-image`スタイルとして適用されます。画像ローダーが提供されていない場合、プレースホルダー画像は生成されず、エラーがスローされます。 @@ -160,14 +147,7 @@ providers: [ 画像ローダーなしで、base64 [データURL](https://developer.mozilla.org/docs/Web/HTTP/Basics_of_HTTP/Data_URLs)を使用してプレースホルダーを指定できます。データURLの形式は`data:image/[imagetype];[data]`で、`[imagetype]`は`png`のような画像形式、`[data]`は画像のbase64エンコーディングです。このエンコーディングはコマンドラインまたはJavaScriptで行うことができます。具体的なコマンドについては、[MDNドキュメント](https://developer.mozilla.org/docs/Web/HTTP/Basics_of_HTTP/Data_URLs#encoding_data_into_base64_format)を参照してください。データが切り詰められたデータURLプレースホルダーの例を以下に示します。 ```html - - - + ``` しかし、大きなデータURLはAngularバンドルのサイズを増やし、ページロードを遅くします。画像ローダーを使用できない場合、Angularチームはbase64プレースホルダー画像を4KB未満に保ち、重要な画像にのみ使用することを推奨しています。プレースホルダーの寸法を小さくすることに加えて、画像形式や画像を保存する際に使用するパラメーターの変更も検討してください。非常に低い解像度では、これらのパラメーターがファイルサイズに大きな影響を与える可能性があります。 @@ -177,13 +157,7 @@ providers: [ デフォルトでは、NgOptimizedImageは画像プレースホルダーにCSSのぼかし効果を適用します。ぼかしなしでプレースホルダーをレンダリングするには、`blur`プロパティをfalseに設定したオブジェクトを含む`placeholderConfig`引数を指定します。例: ```html - + ``` ## 画像のスタイル調整 {#adjusting-image-styling} @@ -205,9 +179,7 @@ NgOptimizedImageには、アプリケーションの読み込みパフォーマ プリコネクトリンクは、[ローダー](#configuring-an-image-loader-for-ngoptimizedimage)への引数として提供されたドメインに対して自動的に生成されます。画像オリジンを自動的に識別できない場合、またはLCP画像に対してプリコネクトリンクが検出されない場合、`NgOptimizedImage`は開発中に警告を表示します。その場合、`index.html`にリソースヒントを手動で追加する必要があります。ドキュメントの``内に、以下に示すように`rel="preconnect"`を持つ`link`タグを追加します。 ```html - - ``` プリコネクト警告を無効にするには、`PRECONNECT_CHECK_BLOCKLIST`トークンをインジェクトします。 @@ -233,7 +205,7 @@ providers: [ 生成されるsrcsetの例: ```html - + ``` #### レスポンシブ画像 {#responsive-images} @@ -266,17 +238,13 @@ providers: [ `srcset`属性を手動で定義したい場合、`ngSrcset`属性を使用して独自のものを指定できます: ```html - - - + ``` `ngSrcset`属性が存在する場合、`NgOptimizedImage`は含まれるサイズに基づいて`srcset`を生成および設定します。`ngSrcset`に画像ファイル名を含めないでください。ディレクティブはこの情報を`ngSrc`から推測します。このディレクティブは、幅記述子(例: `100w`)と密度記述子(例: `1x`)の両方をサポートしています。 ```html - - - + ``` ### 自動srcset生成の無効化 {#disabling-automatic-srcset-generation} @@ -284,9 +252,7 @@ providers: [ 単一の画像に対してsrcset生成を無効にするには、画像に`disableOptimizedSrcset`属性を追加できます。 ```html - - - + ``` ### 画像の遅延読み込みの無効化 {#disabling-image-lazy-loading} @@ -294,9 +260,7 @@ providers: [ デフォルトでは、`NgOptimizedImage`は`priority`とマークされていないすべての画像に対して`loading=lazy`を設定します。優先度の低い画像に対してこの動作を無効にするには、`loading`属性を設定します。この属性は`eager`、`auto`、`lazy`の値をサポートします。詳細については、[標準の画像`loading`属性に関するドキュメントを参照してください](https://developer.mozilla.org/docs/Web/API/HTMLImageElement/loading#value)。 ```html - - - + ``` ### 画像のデコード制御 {#controlling-image-decoding} @@ -308,16 +272,16 @@ providers: [ ```html - + - + - + - + ``` **許可される値** @@ -331,9 +295,7 @@ providers: [ 異なるサイズの画面で画像を異なる幅で表示したい場合があります。このパターンの一般的な例は、モバイルデバイスでは単一の列を、より大きなデバイスでは2つの列をレンダリングするグリッドベースまたはカラムベースのレイアウトです。この動作は、以下のような「メディアクエリ」構文を使用して`sizes`属性で表現できます。 ```html - - - + ``` 上記の例の`sizes`属性は、「この画像は幅768px未満のデバイスでは画面幅の100%になることを想定しています。それ以外の場合は、画面幅の50%になることを想定しています。」という意味です。 @@ -428,9 +390,7 @@ const myCustomLoader = (config: ImageLoaderConfig) => { 上記の例では、カスタムローダーの機能を制御するために「roundedCorners」というプロパティ名を考案したことに注意してください。この機能は、画像を生成する際に次のように使用できます。 ```html - - - + ``` ## よくある質問 {#frequently-asked-questions} diff --git a/adev-ja/src/content/guide/security.en.md b/adev-ja/src/content/guide/security.en.md index c0ca220f5..ecee243ee 100644 --- a/adev-ja/src/content/guide/security.en.md +++ b/adev-ja/src/content/guide/security.en.md @@ -159,10 +159,12 @@ import {bootstrapApplication, CSP_NONCE} from '@angular/core'; import {AppComponent} from './app/app.component'; bootstrapApplication(AppComponent, { - providers: [{ - provide: CSP_NONCE, - useValue: globalThis.myRandomNonceValue - }] + providers: [ + { + provide: CSP_NONCE, + useValue: globalThis.myRandomNonceValue, + }, + ], }); ``` @@ -225,13 +227,15 @@ Content-Security-Policy: trusted-types angular; require-trusted-types-for 'scrip An example of a header specifically configured for Trusted Types and Angular applications that use any of Angular's methods in [DomSanitizer](api/platform-browser/DomSanitizer) that bypasses security: ```html -Content-Security-Policy: trusted-types angular angular#unsafe-bypass; require-trusted-types-for 'script'; +Content-Security-Policy: trusted-types angular angular#unsafe-bypass; require-trusted-types-for +'script'; ``` The following is an example of a header specifically configured for Trusted Types and Angular applications using JIT: ```html -Content-Security-Policy: trusted-types angular angular#unsafe-jit; require-trusted-types-for 'script'; +Content-Security-Policy: trusted-types angular angular#unsafe-jit; require-trusted-types-for +'script'; ``` The following is an example of a header specifically configured for Trusted Types and Angular applications that use lazy loading of modules: @@ -328,7 +332,7 @@ export const appConfig: ApplicationConfig = { headerName: 'X-Custom-Xsrf-Header', }), ), - ] + ], }; ``` @@ -338,11 +342,7 @@ If the built-in XSRF protection mechanism doesn't work for your application, you ```ts export const appConfig: ApplicationConfig = { - providers: [ - provideHttpClient( - withNoXsrfProtection(), - ), - ] + providers: [provideHttpClient(withNoXsrfProtection())], }; ``` diff --git a/adev-ja/src/content/guide/ssr.en.md b/adev-ja/src/content/guide/ssr.en.md index 3a1b60185..cb8265a8c 100644 --- a/adev-ja/src/content/guide/ssr.en.md +++ b/adev-ja/src/content/guide/ssr.en.md @@ -30,7 +30,7 @@ You can create a server route config by declaring an array of [`ServerRoute`](ap ```typescript // app.routes.server.ts -import { RenderMode, ServerRoute } from '@angular/ssr'; +import {RenderMode, ServerRoute} from '@angular/ssr'; export const serverRoutes: ServerRoute[] = [ { @@ -55,32 +55,29 @@ export const serverRoutes: ServerRoute[] = [ You can add this config to your application with [`provideServerRendering`](api/ssr/provideServerRendering 'API reference') using the [`withRoutes`](api/ssr/withRoutes 'API reference') function: ```typescript -import { provideServerRendering, withRoutes } from '@angular/ssr'; -import { serverRoutes } from './app.routes.server'; +import {provideServerRendering, withRoutes} from '@angular/ssr'; +import {serverRoutes} from './app.routes.server'; // app.config.server.ts const serverConfig: ApplicationConfig = { providers: [ provideServerRendering(withRoutes(serverRoutes)), // ... other providers ... - ] + ], }; ``` When using the [App shell pattern](ecosystem/service-workers/app-shell), you must specify the component to be used as the app shell for client-side rendered routes. To do this, use the [`withAppShell`](api/ssr/withAppShell 'API reference') feature: ```typescript -import { provideServerRendering, withRoutes, withAppShell } from '@angular/ssr'; -import { AppShellComponent } from './app-shell/app-shell.component'; +import {provideServerRendering, withRoutes, withAppShell} from '@angular/ssr'; +import {AppShellComponent} from './app-shell/app-shell.component'; const serverConfig: ApplicationConfig = { providers: [ - provideServerRendering( - withRoutes(serverRoutes), - withAppShell(AppShellComponent), - ), + provideServerRendering(withRoutes(serverRoutes), withAppShell(AppShellComponent)), // ... other providers ... - ] + ], }; ``` @@ -142,7 +139,7 @@ You can set custom headers and status codes for individual server routes using t ```typescript // app.routes.server.ts -import { RenderMode, ServerRoute } from '@angular/ssr'; +import {RenderMode, ServerRoute} from '@angular/ssr'; export const serverRoutes: ServerRoute[] = [ { @@ -183,7 +180,7 @@ You can also use this function with catch-all routes (e.g., `/**`), where the pa ```ts // app.routes.server.ts -import { RenderMode, ServerRoute } from '@angular/ssr'; +import {RenderMode, ServerRoute} from '@angular/ssr'; export const serverRoutes: ServerRoute[] = [ { @@ -193,7 +190,7 @@ export const serverRoutes: ServerRoute[] = [ const dataService = inject(PostService); const ids = await dataService.getIds(); // Assuming this returns ['1', '2', '3'] - return ids.map(id => ({ id })); // Generates paths like: /post/1, /post/2, /post/3 + return ids.map((id) => ({id})); // Generates paths like: /post/1, /post/2, /post/3 }, }, { @@ -201,8 +198,8 @@ export const serverRoutes: ServerRoute[] = [ renderMode: RenderMode.Prerender, async getPrerenderParams() { return [ - { id: '1', '**': 'foo/3' }, - { id: '2', '**': 'bar/4' }, + {id: '1', '**': 'foo/3'}, + {id: '2', '**': 'bar/4'}, ]; // Generates paths like: /post/1/foo/3, /post/2/bar/4 }, }, @@ -225,7 +222,7 @@ The available fallback strategies are: ```ts // app.routes.server.ts -import { RenderMode, PrerenderFallback, ServerRoute } from '@angular/ssr'; +import {RenderMode, PrerenderFallback, ServerRoute} from '@angular/ssr'; export const serverRoutes: ServerRoute[] = [ { @@ -236,7 +233,7 @@ export const serverRoutes: ServerRoute[] = [ // This function returns an array of objects representing prerendered posts at the paths: // `/post/1`, `/post/2`, and `/post/3`. // The path `/post/4` will utilize the fallback behavior if it's requested. - return [{ id: 1 }, { id: 2 }, { id: 3 }]; + return [{id: 1}, {id: 2}, {id: 3}]; }, }, ]; @@ -315,9 +312,7 @@ Register the browser implementation in your main application configuration: ```ts // app.config.ts export const appConfig: ApplicationConfig = { - providers: [ - { provide: AnalyticsService, useClass: BrowserAnalyticsService }, - ] + providers: [{provide: AnalyticsService, useClass: BrowserAnalyticsService}], }; ``` @@ -326,16 +321,16 @@ Override with the server implementation in your server configuration: ```ts // app.config.server.ts const serverConfig: ApplicationConfig = { - providers: [ - { provide: AnalyticsService, useClass: ServerAnalyticsService }, - ] + providers: [{provide: AnalyticsService, useClass: ServerAnalyticsService}], }; ``` Inject and use the service in your components: ```ts -@Component({/* ... */}) +@Component({ + /* ... */ +}) export class Checkout { private analytics = inject(AnalyticsService); @@ -350,9 +345,9 @@ export class Checkout { When working with server-side rendering, you should avoid directly referencing browser-specific globals like `document`. Instead, use the [`DOCUMENT`](api/core/DOCUMENT) token to access the document object in a platform-agnostic way. ```ts -import { Injectable, inject, DOCUMENT } from '@angular/core'; +import {Injectable, inject, DOCUMENT} from '@angular/core'; -@Injectable({ providedIn: 'root' }) +@Injectable({providedIn: 'root'}) export class CanonicalLinkService { private readonly document = inject(DOCUMENT); @@ -365,7 +360,6 @@ export class CanonicalLinkService { this.document.head.appendChild(link); } } - ``` HELPFUL: For managing meta tags, Angular provides the `Meta` service. @@ -436,8 +430,8 @@ This configuration is provided globally using `withHttpTransferCacheOptions` ins By default, `HttpClient` caches all `HEAD` and `GET` requests which don't contain `Authorization` or `Proxy-Authorization` headers. You can override those settings by using `withHttpTransferCacheOptions` to the hydration configuration. ```ts -import { bootstrapApplication } from '@angular/platform-browser'; -import { provideClientHydration, withHttpTransferCacheOptions } from '@angular/platform-browser'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {provideClientHydration, withHttpTransferCacheOptions} from '@angular/platform-browser'; bootstrapApplication(AppComponent, { providers: [ @@ -504,7 +498,7 @@ You can override caching behavior for a specific request using the `transferCach ```ts // Include specific headers for this request -http.get('/api/profile', { transferCache: { includeHeaders: ['CustomHeader'] } }); +http.get('/api/profile', {transferCache: {includeHeaders: ['CustomHeader']}}); ``` ### Disabling caching @@ -516,12 +510,14 @@ You can disable HTTP caching of requests sent from the server either globally or To disable caching for all requests in your application, use the `withNoHttpTransferCache` feature: ```ts -import { bootstrapApplication, provideClientHydration, withNoHttpTransferCache } from '@angular/platform-browser'; +import { + bootstrapApplication, + provideClientHydration, + withNoHttpTransferCache, +} from '@angular/platform-browser'; bootstrapApplication(AppComponent, { - providers: [ - provideClientHydration(withNoHttpTransferCache()) - ] + providers: [provideClientHydration(withNoHttpTransferCache())], }); ``` @@ -530,14 +526,20 @@ bootstrapApplication(AppComponent, { You can also selectively disable caching for certain requests using the [`filter`](api/common/http/HttpTransferCacheOptions) option in `withHttpTransferCacheOptions`. For example, you can disable caching for a specific API endpoint: ```ts -import { bootstrapApplication, provideClientHydration, withHttpTransferCacheOptions } from '@angular/platform-browser'; +import { + bootstrapApplication, + provideClientHydration, + withHttpTransferCacheOptions, +} from '@angular/platform-browser'; bootstrapApplication(AppComponent, { providers: [ - provideClientHydration(withHttpTransferCacheOptions({ - filter: (req) => !req.url.includes('/api/sensitive-data') - })) - ] + provideClientHydration( + withHttpTransferCacheOptions({ + filter: (req) => !req.url.includes('/api/sensitive-data'), + }), + ), + ], }); ``` @@ -548,7 +550,7 @@ Use this option to exclude endpoints with user‑specific or dynamic data (for e To disable caching for an individual request, you can specify the [`transferCache`](api/common/http/HttpRequest#transferCache) option in an `HttpRequest`. ```ts -httpClient.get('/api/sensitive-data', { transferCache: false }); +httpClient.get('/api/sensitive-data', {transferCache: false}); ``` NOTE: If your application uses different HTTP origins to make API calls on the server and on the client, the `HTTP_TRANSFER_CACHE_ORIGIN_MAP` token allows you to establish a mapping between those origins, so that `HttpTransferCache` feature can recognize those requests as the same ones and reuse the data cached on the server during hydration on the client. @@ -561,7 +563,11 @@ The `@angular/ssr/node` extends `@angular/ssr` specifically for Node.js environm ```ts // server.ts -import { AngularNodeAppEngine, createNodeRequestHandler, writeResponseToNodeResponse } from '@angular/ssr/node'; +import { + AngularNodeAppEngine, + createNodeRequestHandler, + writeResponseToNodeResponse, +} from '@angular/ssr/node'; import express from 'express'; const app = express(); @@ -570,7 +576,7 @@ const angularApp = new AngularNodeAppEngine(); app.use('*', (req, res, next) => { angularApp .handle(req) - .then(response => { + .then((response) => { if (response) { writeResponseToNodeResponse(response, res); } else { @@ -592,7 +598,7 @@ The `@angular/ssr` provides essential APIs for server-side rendering your Angula ```ts // server.ts -import { AngularAppEngine, createRequestHandler } from '@angular/ssr'; +import {AngularAppEngine, createRequestHandler} from '@angular/ssr'; const angularApp = new AngularAppEngine(); @@ -600,7 +606,7 @@ const angularApp = new AngularAppEngine(); * This is a request handler used by the Angular CLI (dev-server and during build). */ export const reqHandler = createRequestHandler(async (req: Request) => { - const res: Response|null = await angularApp.render(req); + const res: Response | null = await angularApp.render(req); // ... }); diff --git a/adev-ja/src/content/guide/ssr.md b/adev-ja/src/content/guide/ssr.md index c2fb3c83d..ec5c41c6a 100644 --- a/adev-ja/src/content/guide/ssr.md +++ b/adev-ja/src/content/guide/ssr.md @@ -30,7 +30,7 @@ NOTE: デフォルトでは、Angularはアプリケーション全体をプリ ```typescript // app.routes.server.ts -import { RenderMode, ServerRoute } from '@angular/ssr'; +import {RenderMode, ServerRoute} from '@angular/ssr'; export const serverRoutes: ServerRoute[] = [ { @@ -55,32 +55,29 @@ export const serverRoutes: ServerRoute[] = [ この設定は、[`withRoutes`](api/ssr/withRoutes 'API reference') 関数を使用して [`provideServerRendering`](api/ssr/provideServerRendering 'API reference') でアプリケーションに追加できます。 ```typescript -import { provideServerRendering, withRoutes } from '@angular/ssr'; -import { serverRoutes } from './app.routes.server'; +import {provideServerRendering, withRoutes} from '@angular/ssr'; +import {serverRoutes} from './app.routes.server'; // app.config.server.ts const serverConfig: ApplicationConfig = { providers: [ provideServerRendering(withRoutes(serverRoutes)), // ... その他のプロバイダー ... - ] + ], }; ``` [App shellパターン](ecosystem/service-workers/app-shell)を使用する場合、クライアントサイドレンダリングされたルートのApp shellとして使用するコンポーネントを指定する必要があります。これを行うには、[`withAppShell`](api/ssr/withAppShell 'API reference') 機能を使用します。 ```typescript -import { provideServerRendering, withRoutes, withAppShell } from '@angular/ssr'; -import { AppShellComponent } from './app-shell/app-shell.component'; +import {provideServerRendering, withRoutes, withAppShell} from '@angular/ssr'; +import {AppShellComponent} from './app-shell/app-shell.component'; const serverConfig: ApplicationConfig = { providers: [ - provideServerRendering( - withRoutes(serverRoutes), - withAppShell(AppShellComponent), - ), + provideServerRendering(withRoutes(serverRoutes), withAppShell(AppShellComponent)), // ... その他のプロバイダー ... - ] + ], }; ``` @@ -142,7 +139,7 @@ NOTE: Angular Service Workerを使用する場合、最初のリクエストは ```typescript // app.routes.server.ts -import { RenderMode, ServerRoute } from '@angular/ssr'; +import {RenderMode, ServerRoute} from '@angular/ssr'; export const serverRoutes: ServerRoute[] = [ { @@ -183,7 +180,7 @@ Angularは、ルート設定の [`redirectTo`](api/router/Route#redirectTo 'API ```ts // app.routes.server.ts -import { RenderMode, ServerRoute } from '@angular/ssr'; +import {RenderMode, ServerRoute} from '@angular/ssr'; export const serverRoutes: ServerRoute[] = [ { @@ -193,7 +190,7 @@ export const serverRoutes: ServerRoute[] = [ const dataService = inject(PostService); const ids = await dataService.getIds(); // これは ['1', '2', '3'] を返すと仮定します - return ids.map(id => ({ id })); // /post/1, /post/2, /post/3 のようなパスを生成します + return ids.map((id) => ({id})); // /post/1, /post/2, /post/3 のようなパスを生成します }, }, { @@ -201,8 +198,8 @@ export const serverRoutes: ServerRoute[] = [ renderMode: RenderMode.Prerender, async getPrerenderParams() { return [ - { id: '1', '**': 'foo/3' }, - { id: '2', '**': 'bar/4' }, + {id: '1', '**': 'foo/3'}, + {id: '2', '**': 'bar/4'}, ]; // /post/1/foo/3, /post/2/bar/4 のようなパスを生成します }, }, @@ -225,7 +222,7 @@ IMPORTANT: `getPrerenderParams` 内で [`inject`](api/core/inject 'API reference ```ts // app.routes.server.ts -import { RenderMode, PrerenderFallback, ServerRoute } from '@angular/ssr'; +import {RenderMode, PrerenderFallback, ServerRoute} from '@angular/ssr'; export const serverRoutes: ServerRoute[] = [ { @@ -236,7 +233,7 @@ export const serverRoutes: ServerRoute[] = [ // この関数は、/post/1、/post/2、/post/3 のパスで // プリレンダリングされた投稿を表すオブジェクトの配列を返します。 // /post/4 のパスがリクエストされた場合、フォールバック動作が利用されます。 - return [{ id: 1 }, { id: 2 }, { id: 3 }]; + return [{id: 1}, {id: 2}, {id: 3}]; }, }, ]; @@ -315,9 +312,7 @@ export class ServerAnalyticsService implements AnalyticsService { ```ts // app.config.ts export const appConfig: ApplicationConfig = { - providers: [ - { provide: AnalyticsService, useClass: BrowserAnalyticsService }, - ] + providers: [{provide: AnalyticsService, useClass: BrowserAnalyticsService}], }; ``` @@ -326,16 +321,16 @@ export const appConfig: ApplicationConfig = { ```ts // app.config.server.ts const serverConfig: ApplicationConfig = { - providers: [ - { provide: AnalyticsService, useClass: ServerAnalyticsService }, - ] + providers: [{provide: AnalyticsService, useClass: ServerAnalyticsService}], }; ``` コンポーネントでサービスを注入して使用します。 ```ts -@Component({/* ... */}) +@Component({ + /* ... */ +}) export class Checkout { private analytics = inject(AnalyticsService); @@ -350,9 +345,9 @@ export class Checkout { サーバーサイドレンダリングを使用する場合、`document` のようなブラウザ固有のグローバルを直接参照することは避けるべきです。代わりに、[`DOCUMENT`](api/core/DOCUMENT) トークンを使用して、プラットフォームに依存しない方法でdocumentオブジェクトにアクセスします。 ```ts -import { Injectable, inject, DOCUMENT } from '@angular/core'; +import {Injectable, inject, DOCUMENT} from '@angular/core'; -@Injectable({ providedIn: 'root' }) +@Injectable({providedIn: 'root'}) export class CanonicalLinkService { private readonly document = inject(DOCUMENT); @@ -365,7 +360,6 @@ export class CanonicalLinkService { this.document.head.appendChild(link); } } - ``` HELPFUL: メタタグの管理には、Angularは `Meta` サービスを提供しています。 @@ -436,8 +430,8 @@ IMPORTANT: 上記のトークンは、次のシナリオでは `null` になり デフォルトでは、`HttpClient` は `Authorization` または `Proxy-Authorization` ヘッダーを含まないすべての `HEAD` および `GET` リクエストをキャッシュします。ハイドレーション設定に `withHttpTransferCacheOptions` を使用することで、これらの設定をオーバーライドできます。 ```ts -import { bootstrapApplication } from '@angular/platform-browser'; -import { provideClientHydration, withHttpTransferCacheOptions } from '@angular/platform-browser'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {provideClientHydration, withHttpTransferCacheOptions} from '@angular/platform-browser'; bootstrapApplication(AppComponent, { providers: [ @@ -504,7 +498,7 @@ withHttpTransferCacheOptions({ ```ts // このリクエストに特定のヘッダーを含める -http.get('/api/profile', { transferCache: { includeHeaders: ['CustomHeader'] } }); +http.get('/api/profile', {transferCache: {includeHeaders: ['CustomHeader']}}); ``` ### キャッシュの無効化 {#disabling-caching} @@ -516,12 +510,14 @@ http.get('/api/profile', { transferCache: { includeHeaders: ['CustomHeader'] } } アプリケーション内のすべてのリクエストのキャッシュを無効にするには、`withNoHttpTransferCache` 機能を使用します。 ```ts -import { bootstrapApplication, provideClientHydration, withNoHttpTransferCache } from '@angular/platform-browser'; +import { + bootstrapApplication, + provideClientHydration, + withNoHttpTransferCache, +} from '@angular/platform-browser'; bootstrapApplication(AppComponent, { - providers: [ - provideClientHydration(withNoHttpTransferCache()) - ] + providers: [provideClientHydration(withNoHttpTransferCache())], }); ``` @@ -530,14 +526,20 @@ bootstrapApplication(AppComponent, { `withHttpTransferCacheOptions` の [`filter`](api/common/http/HttpTransferCacheOptions) オプションを使用して、特定のリクエストのキャッシュを選択的に無効にできます。たとえば、特定のAPIエンドポイントのキャッシュを無効にできます。 ```ts -import { bootstrapApplication, provideClientHydration, withHttpTransferCacheOptions } from '@angular/platform-browser'; +import { + bootstrapApplication, + provideClientHydration, + withHttpTransferCacheOptions, +} from '@angular/platform-browser'; bootstrapApplication(AppComponent, { providers: [ - provideClientHydration(withHttpTransferCacheOptions({ - filter: (req) => !req.url.includes('/api/sensitive-data') - })) - ] + provideClientHydration( + withHttpTransferCacheOptions({ + filter: (req) => !req.url.includes('/api/sensitive-data'), + }), + ), + ], }); ``` @@ -548,7 +550,7 @@ bootstrapApplication(AppComponent, { 個々のリクエストのキャッシュを無効にするには、`HttpRequest` で [`transferCache`](api/common/http/HttpRequest#transferCache) オプションを指定できます。 ```ts -httpClient.get('/api/sensitive-data', { transferCache: false }); +httpClient.get('/api/sensitive-data', {transferCache: false}); ``` NOTE: アプリケーションがサーバーとクライアントで異なるHTTPオリジンを使用してAPIコールを行う場合、`HTTP_TRANSFER_CACHE_ORIGIN_MAP`トークンを使用してそれらのオリジン間のマッピングを確立できます。これにより、`HttpTransferCache`機能がそれらのリクエストを同じものとして認識し、クライアントでのハイドレーション中にサーバーでキャッシュされたデータを再利用できます。 @@ -561,7 +563,11 @@ NOTE: アプリケーションがサーバーとクライアントで異なるHT ```ts // server.ts -import { AngularNodeAppEngine, createNodeRequestHandler, writeResponseToNodeResponse } from '@angular/ssr/node'; +import { + AngularNodeAppEngine, + createNodeRequestHandler, + writeResponseToNodeResponse, +} from '@angular/ssr/node'; import express from 'express'; const app = express(); @@ -570,7 +576,7 @@ const angularApp = new AngularNodeAppEngine(); app.use('*', (req, res, next) => { angularApp .handle(req) - .then(response => { + .then((response) => { if (response) { writeResponseToNodeResponse(response, res); } else { @@ -592,7 +598,7 @@ export const reqHandler = createNodeRequestHandler(app); ```ts // server.ts -import { AngularAppEngine, createRequestHandler } from '@angular/ssr'; +import {AngularAppEngine, createRequestHandler} from '@angular/ssr'; const angularApp = new AngularAppEngine(); @@ -600,7 +606,7 @@ const angularApp = new AngularAppEngine(); * これは、Angular CLI (開発サーバーおよびビルド時) で使用されるリクエストハンドラーです。 */ export const reqHandler = createRequestHandler(async (req: Request) => { - const res: Response|null = await angularApp.render(req); + const res: Response | null = await angularApp.render(req); // ... }); diff --git a/adev-ja/src/content/guide/tailwind.en.md b/adev-ja/src/content/guide/tailwind.en.md index b0b6e85ba..36cd034d9 100644 --- a/adev-ja/src/content/guide/tailwind.en.md +++ b/adev-ja/src/content/guide/tailwind.en.md @@ -58,7 +58,6 @@ Next, add a `.postcssrc.json` file in the file root of the project. Add the `@tailwindcss/postcss` plugin into your PostCSS configuration. ```json {header: '.postcssrc.json'} - { "plugins": { "@tailwindcss/postcss": {} @@ -70,15 +69,15 @@ Add the `@tailwindcss/postcss` plugin into your PostCSS configuration. Add an `@import` to `./src/styles.css` that imports Tailwind CSS. - -@import "tailwindcss"; - +```css {header: "src/styles.css"} +@import 'tailwindcss'; +``` If you're using SCSS, add `@use` to `./src/styles.scss`. - -@use "tailwindcss"; - +```scss {header: "src/styles.scss"} +@use 'tailwindcss'; +``` ### 5. Start using Tailwind in your project @@ -87,9 +86,7 @@ You can now start using Tailwind's utility classes in your component templates t For example, you can add the following to your `app.html` file: ```html -

- Hello world! -

+

Hello world!

``` ## Additional Resources diff --git a/adev-ja/src/content/guide/tailwind.md b/adev-ja/src/content/guide/tailwind.md index 879fddc69..ea92cb7a0 100644 --- a/adev-ja/src/content/guide/tailwind.md +++ b/adev-ja/src/content/guide/tailwind.md @@ -58,7 +58,6 @@ cd my-project PostCSSの設定に`@tailwindcss/postcss`プラグインを追加します。 ```json {header: '.postcssrc.json'} - { "plugins": { "@tailwindcss/postcss": {} @@ -70,15 +69,15 @@ PostCSSの設定に`@tailwindcss/postcss`プラグインを追加します。 Tailwind CSSをインポートする`@import`を`./src/styles.css`に追加します。 - -@import "tailwindcss"; - +```css {header: "src/styles.css"} +@import 'tailwindcss'; +``` SCSSを使用している場合は、`@use`を`./src/styles.scss`に追加します。 - -@use "tailwindcss"; - +```scss {header: "src/styles.scss"} +@use 'tailwindcss'; +``` ### 5. プロジェクトでTailwindを使い始める {#start-using-tailwind-in-your-project} @@ -87,9 +86,7 @@ SCSSを使用している場合は、`@use`を`./src/styles.scss`に追加しま たとえば、次の内容を`app.html`ファイルに追加できます: ```html -

- Hello world! -

+

Hello world!

``` ## その他のリソース {#additional-resources} diff --git a/adev-ja/src/content/guide/zoneless.en.md b/adev-ja/src/content/guide/zoneless.en.md index 4eafe6505..cf3516c16 100644 --- a/adev-ja/src/content/guide/zoneless.en.md +++ b/adev-ja/src/content/guide/zoneless.en.md @@ -13,14 +13,12 @@ The main advantages to removing ZoneJS as a dependency are: ```typescript // standalone bootstrap -bootstrapApplication(MyApp, {providers: [ - provideZonelessChangeDetection(), -]}); +bootstrapApplication(MyApp, {providers: [provideZonelessChangeDetection()]}); // NgModule bootstrap platformBrowser().bootstrapModule(AppModule); @NgModule({ - providers: [provideZonelessChangeDetection()] + providers: [provideZonelessChangeDetection()], }) export class AppModule {} ``` @@ -130,7 +128,7 @@ Angular application. ```typescript TestBed.configureTestingModule({ - providers: [provideZonelessChangeDetection()] + providers: [provideZonelessChangeDetection()], }); const fixture = TestBed.createComponent(MyComponent); diff --git a/adev-ja/src/content/guide/zoneless.md b/adev-ja/src/content/guide/zoneless.md index d50a2eef9..3adff1750 100644 --- a/adev-ja/src/content/guide/zoneless.md +++ b/adev-ja/src/content/guide/zoneless.md @@ -13,14 +13,12 @@ ZoneJSを依存関係として削除する主な利点は次のとおりです ```typescript // スタンドアロン ブートストラップ -bootstrapApplication(MyApp, {providers: [ - provideZonelessChangeDetection(), -]}); +bootstrapApplication(MyApp, {providers: [provideZonelessChangeDetection()]}); // NgModule ブートストラップ platformBrowser().bootstrapModule(AppModule); @NgModule({ - providers: [provideZonelessChangeDetection()] + providers: [provideZonelessChangeDetection()], }) export class AppModule {} ``` @@ -130,7 +128,7 @@ Zonelessプロバイダー関数は、`TestBed`でも使用して、 ```typescript TestBed.configureTestingModule({ - providers: [provideZonelessChangeDetection()] + providers: [provideZonelessChangeDetection()], }); const fixture = TestBed.createComponent(MyComponent); From 00f561c3bea251aad7b3b2ec8a0cd7fd328f6d37 Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 11:24:56 +0900 Subject: [PATCH 12/32] docs(guide/http): migrate .en.md changes --- .../src/content/guide/http/interceptors.en.md | 133 +++--- .../src/content/guide/http/interceptors.md | 133 +++--- .../content/guide/http/making-requests.en.md | 402 ++++++++++-------- .../src/content/guide/http/making-requests.md | 402 ++++++++++-------- adev-ja/src/content/guide/http/security.en.md | 8 +- adev-ja/src/content/guide/http/security.md | 8 +- adev-ja/src/content/guide/http/setup.en.md | 14 +- adev-ja/src/content/guide/http/setup.md | 14 +- adev-ja/src/content/guide/http/testing.en.md | 28 +- adev-ja/src/content/guide/http/testing.md | 28 +- 10 files changed, 656 insertions(+), 514 deletions(-) diff --git a/adev-ja/src/content/guide/http/interceptors.en.md b/adev-ja/src/content/guide/http/interceptors.en.md index 7b43b3e88..cb8ae2d7c 100644 --- a/adev-ja/src/content/guide/http/interceptors.en.md +++ b/adev-ja/src/content/guide/http/interceptors.en.md @@ -29,7 +29,10 @@ The basic form of an interceptor is a function which receives the outgoing `Http For example, this `loggingInterceptor` will log the outgoing request URL to `console.log` before forwarding the request: ```ts -export function loggingInterceptor(req: HttpRequest, next: HttpHandlerFn): Observable> { +export function loggingInterceptor( + req: HttpRequest, + next: HttpHandlerFn, +): Observable> { console.log(req.url); return next(req); } @@ -42,11 +45,9 @@ In order for this interceptor to actually intercept requests, you must configure You declare the set of interceptors to use when configuring `HttpClient` through dependency injection, by using the `withInterceptors` feature: ```ts -bootstrapApplication(AppComponent, {providers: [ - provideHttpClient( - withInterceptors([loggingInterceptor, cachingInterceptor]), - ) -]}); +bootstrapApplication(AppComponent, { + providers: [provideHttpClient(withInterceptors([loggingInterceptor, cachingInterceptor]))], +}); ``` The interceptors you configure are chained together in the order that you've listed them in the providers. In the above example, the `loggingInterceptor` would process the request and then forward it to the `cachingInterceptor`. @@ -56,12 +57,17 @@ The interceptors you configure are chained together in the order that you've lis An interceptor may transform the `Observable` stream of `HttpEvent`s returned by `next` in order to access or manipulate the response. Because this stream includes all response events, inspecting the `.type` of each event may be necessary in order to identify the final response object. ```ts -export function loggingInterceptor(req: HttpRequest, next: HttpHandlerFn): Observable> { - return next(req).pipe(tap(event => { - if (event.type === HttpEventType.Response) { - console.log(req.url, 'returned a response with status', event.status); - } - })); +export function loggingInterceptor( + req: HttpRequest, + next: HttpHandlerFn, +): Observable> { + return next(req).pipe( + tap((event) => { + if (event.type === HttpEventType.Response) { + console.log(req.url, 'returned a response with status', event.status); + } + }), + ); } ``` @@ -171,29 +177,39 @@ When using `HttpClient` with the `withFetch` provider, responses include a `redi An interceptor can access and act upon the redirect information: ```ts -export function redirectTrackingInterceptor(req: HttpRequest, next: HttpHandlerFn): Observable> { - return next(req).pipe(tap(event => { - if (event.type === HttpEventType.Response && event.redirected) { - console.log('Request to', req.url, 'was redirected to', event.url); - // Handle redirect logic - maybe update analytics, security checks, etc. - } - })); +export function redirectTrackingInterceptor( + req: HttpRequest, + next: HttpHandlerFn, +): Observable> { + return next(req).pipe( + tap((event) => { + if (event.type === HttpEventType.Response && event.redirected) { + console.log('Request to', req.url, 'was redirected to', event.url); + // Handle redirect logic - maybe update analytics, security checks, etc. + } + }), + ); } ``` You can also use the redirect information to implement conditional logic in your interceptors: ```ts -export function authRedirectInterceptor(req: HttpRequest, next: HttpHandlerFn): Observable> { - return next(req).pipe(tap(event => { - if (event.type === HttpEventType.Response && event.redirected) { - // Check if we were redirected to a login page - if (event.url?.includes('/login')) { - // Handle authentication redirect - handleAuthRedirect(); +export function authRedirectInterceptor( + req: HttpRequest, + next: HttpHandlerFn, +): Observable> { + return next(req).pipe( + tap((event) => { + if (event.type === HttpEventType.Response && event.redirected) { + // Check if we were redirected to a login page + if (event.url?.includes('/login')) { + // Handle authentication redirect + handleAuthRedirect(); + } } - } - })); + }), + ); } ``` @@ -212,26 +228,31 @@ The response `type` property can have the following values: An interceptor can use response type information for CORS debugging and error handling: ```ts -export function responseTypeInterceptor(req: HttpRequest, next: HttpHandlerFn): Observable> { - return next(req).pipe(map(event => { - if (event.type === HttpEventType.Response) { - // Handle different response types appropriately - switch (event.responseType) { - case 'opaque': - // Limited access to response data - console.warn('Limited response data due to CORS policy'); - break; - case 'cors': - case 'basic': - // Full access to response data - break; - case 'error': - // Handle network errors - console.error('Network error in response'); - break; +export function responseTypeInterceptor( + req: HttpRequest, + next: HttpHandlerFn, +): Observable> { + return next(req).pipe( + map((event) => { + if (event.type === HttpEventType.Response) { + // Handle different response types appropriately + switch (event.responseType) { + case 'opaque': + // Limited access to response data + console.warn('Limited response data due to CORS policy'); + break; + case 'cors': + case 'basic': + // Full access to response data + break; + case 'error': + // Handle network errors + console.error('Network error in response'); + break; + } } - } - })); + }), + ); } ``` @@ -254,14 +275,16 @@ export class LoggingInterceptor implements HttpInterceptor { DI-based interceptors are configured through a dependency injection multi-provider: ```ts -bootstrapApplication(AppComponent, {providers: [ - provideHttpClient( - // DI-based interceptors must be explicitly enabled. - withInterceptorsFromDi(), - ), - - {provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true}, -]}); +bootstrapApplication(AppComponent, { + providers: [ + provideHttpClient( + // DI-based interceptors must be explicitly enabled. + withInterceptorsFromDi(), + ), + + {provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true}, + ], +}); ``` DI-based interceptors run in the order that their providers are registered. In an app with an extensive and hierarchical DI configuration, this order can be very hard to predict. diff --git a/adev-ja/src/content/guide/http/interceptors.md b/adev-ja/src/content/guide/http/interceptors.md index 2fab80bf5..c85bf21ca 100644 --- a/adev-ja/src/content/guide/http/interceptors.md +++ b/adev-ja/src/content/guide/http/interceptors.md @@ -29,7 +29,10 @@ TLDR: インターセプターは、再試行、キャッシュ、ロギング たとえば、この `loggingInterceptor` は、リクエストを転送する前に、送信されるリクエストのURLを `console.log` にログに記録します。 ```ts -export function loggingInterceptor(req: HttpRequest, next: HttpHandlerFn): Observable> { +export function loggingInterceptor( + req: HttpRequest, + next: HttpHandlerFn, +): Observable> { console.log(req.url); return next(req); } @@ -42,11 +45,9 @@ export function loggingInterceptor(req: HttpRequest, next: HttpHandlerF `HttpClient` を構成するときに使用するインターセプターのセットは、`withInterceptors` 機能を使用して、依存性の注入によって宣言します。 ```ts -bootstrapApplication(AppComponent, {providers: [ - provideHttpClient( - withInterceptors([loggingInterceptor, cachingInterceptor]), - ) -]}); +bootstrapApplication(AppComponent, { + providers: [provideHttpClient(withInterceptors([loggingInterceptor, cachingInterceptor]))], +}); ``` 構成したインターセプターは、プロバイダーにリストした順序でチェーンされます。上記の例では、`loggingInterceptor` がリクエストを処理し、`cachingInterceptor` に転送します。 @@ -56,12 +57,17 @@ bootstrapApplication(AppComponent, {providers: [ インターセプターは、`next` から返される `HttpEvent` の `Observable` ストリームを変換して、レスポンスにアクセスしたり、レスポンスを操作したりできます。このストリームにはすべてのレスポンスイベントが含まれているため、最終的なレスポンスオブジェクトを識別するためには、各イベントの `.type` を調べる必要がある場合があります。 ```ts -export function loggingInterceptor(req: HttpRequest, next: HttpHandlerFn): Observable> { - return next(req).pipe(tap(event => { - if (event.type === HttpEventType.Response) { - console.log(req.url, 'returned a response with status', event.status); - } - })); +export function loggingInterceptor( + req: HttpRequest, + next: HttpHandlerFn, +): Observable> { + return next(req).pipe( + tap((event) => { + if (event.type === HttpEventType.Response) { + console.log(req.url, 'returned a response with status', event.status); + } + }), + ); } ``` @@ -171,29 +177,39 @@ const resp = new HttpResponse({ インターセプターは、リダイレクト情報にアクセスして作用できます。 ```ts -export function redirectTrackingInterceptor(req: HttpRequest, next: HttpHandlerFn): Observable> { - return next(req).pipe(tap(event => { - if (event.type === HttpEventType.Response && event.redirected) { - console.log('Request to', req.url, 'was redirected to', event.url); - // リダイレクトロジックの処理 - 分析の更新、セキュリティチェックなど - } - })); +export function redirectTrackingInterceptor( + req: HttpRequest, + next: HttpHandlerFn, +): Observable> { + return next(req).pipe( + tap((event) => { + if (event.type === HttpEventType.Response && event.redirected) { + console.log('Request to', req.url, 'was redirected to', event.url); + // リダイレクトロジックの処理 - 分析の更新、セキュリティチェックなど + } + }), + ); } ``` また、リダイレクト情報を使用してインターセプターで条件付きロジックを実装できます。 ```ts -export function authRedirectInterceptor(req: HttpRequest, next: HttpHandlerFn): Observable> { - return next(req).pipe(tap(event => { - if (event.type === HttpEventType.Response && event.redirected) { - // ログインページにリダイレクトされたかどうかを確認する - if (event.url?.includes('/login')) { - // 認証リダイレクトを処理する - handleAuthRedirect(); +export function authRedirectInterceptor( + req: HttpRequest, + next: HttpHandlerFn, +): Observable> { + return next(req).pipe( + tap((event) => { + if (event.type === HttpEventType.Response && event.redirected) { + // ログインページにリダイレクトされたかどうかを確認する + if (event.url?.includes('/login')) { + // 認証リダイレクトを処理する + handleAuthRedirect(); + } } - } - })); + }), + ); } ``` @@ -212,26 +228,31 @@ export function authRedirectInterceptor(req: HttpRequest, next: HttpHan インターセプターは、CORSのデバッグとエラー処理のためにレスポンスタイプ情報を使用できます。 ```ts -export function responseTypeInterceptor(req: HttpRequest, next: HttpHandlerFn): Observable> { - return next(req).pipe(map(event => { - if (event.type === HttpEventType.Response) { - // さまざまなレスポンスタイプを適切に処理する - switch (event.responseType) { - case 'opaque': - // レスポンスデータへのアクセスが制限されている - console.warn('Limited response data due to CORS policy'); - break; - case 'cors': - case 'basic': - // レスポンスデータへのフルアクセス - break; - case 'error': - // ネットワークエラーを処理する - console.error('Network error in response'); - break; +export function responseTypeInterceptor( + req: HttpRequest, + next: HttpHandlerFn, +): Observable> { + return next(req).pipe( + map((event) => { + if (event.type === HttpEventType.Response) { + // さまざまなレスポンスタイプを適切に処理する + switch (event.responseType) { + case 'opaque': + // レスポンスデータへのアクセスが制限されている + console.warn('Limited response data due to CORS policy'); + break; + case 'cors': + case 'basic': + // レスポンスデータへのフルアクセス + break; + case 'error': + // ネットワークエラーを処理する + console.error('Network error in response'); + break; + } } - } - })); + }), + ); } ``` @@ -254,14 +275,16 @@ export class LoggingInterceptor implements HttpInterceptor { DIベースのインターセプターは、依存性の注入のマルチプロバイダーによって構成されます。 ```ts -bootstrapApplication(AppComponent, {providers: [ - provideHttpClient( - // DI ベースのインターセプターは明示的に有効にする必要があります。 - withInterceptorsFromDi(), - ), - - {provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true}, -]}); +bootstrapApplication(AppComponent, { + providers: [ + provideHttpClient( + // DI ベースのインターセプターは明示的に有効にする必要があります。 + withInterceptorsFromDi(), + ), + + {provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true}, + ], +}); ``` DIベースのインターセプターは、プロバイダーが登録された順序で実行されます。DI構成が複雑で階層的なアプリケーションでは、この順序を予測することは非常に難しい場合があります。 diff --git a/adev-ja/src/content/guide/http/making-requests.en.md b/adev-ja/src/content/guide/http/making-requests.en.md index 131a1a08e..da3792294 100644 --- a/adev-ja/src/content/guide/http/making-requests.en.md +++ b/adev-ja/src/content/guide/http/making-requests.en.md @@ -13,7 +13,7 @@ Fetching data from a backend often requires making a GET request using the [`Htt For example, to fetch configuration data from a hypothetical API using the `HttpClient.get()` method: ```ts -http.get('/api/config').subscribe(config => { +http.get('/api/config').subscribe((config) => { // process the configuration. }); ``` @@ -38,7 +38,7 @@ By default, `HttpClient` assumes that servers will return JSON data. When intera For example, you can ask `HttpClient` to download the raw bytes of a `.jpeg` image into an `ArrayBuffer`: ```ts -http.get('/images/dog.jpg', {responseType: 'arraybuffer'}).subscribe(buffer => { +http.get('/images/dog.jpg', {responseType: 'arraybuffer'}).subscribe((buffer) => { console.log('The image is ' + buffer.byteLength + ' bytes large'); }); ``` @@ -56,7 +56,7 @@ Server APIs which perform mutations often require making POST requests with a re The [`HttpClient.post()`](api/common/http/HttpClient#post) method behaves similarly to `get()`, and accepts an additional `body` argument before its options: ```ts -http.post('/api/config', newConfig).subscribe(config => { +http.post('/api/config', newConfig).subscribe((config) => { console.log('Updated config:', config); }); ``` @@ -81,11 +81,13 @@ Specify request parameters that should be included in the request URL using the Passing an object literal is the simplest way of configuring URL parameters: ```ts -http.get('/api/config', { - params: {filter: 'all'}, -}).subscribe(config => { - // ... -}); +http + .get('/api/config', { + params: {filter: 'all'}, + }) + .subscribe((config) => { + // ... + }); ``` Alternatively, pass an instance of `HttpParams` if you need more control over the construction or serialization of the parameters. @@ -95,11 +97,13 @@ IMPORTANT: Instances of `HttpParams` are _immutable_ and cannot be directly chan ```ts const baseParams = new HttpParams().set('filter', 'all'); -http.get('/api/config', { - params: baseParams.set('details', 'enabled'), -}).subscribe(config => { - // ... -}); +http + .get('/api/config', { + params: baseParams.set('details', 'enabled'), + }) + .subscribe((config) => { + // ... + }); ``` You can instantiate `HttpParams` with a custom `HttpParameterCodec` that determines how `HttpClient` will encode the parameters into the URL. @@ -111,11 +115,11 @@ By default, `HttpParams` uses the built-in [`HttpUrlEncodingCodec`](api/common/h You can provide your own implementation of [`HttpParameterCodec`](api/common/http/HttpParameterCodec) to customize how encoding and decoding are applied. ```ts -import { HttpClient, HttpParams, HttpParameterCodec } from '@angular/common/http'; -import { inject } from '@angular/core'; +import {HttpClient, HttpParams, HttpParameterCodec} from '@angular/common/http'; +import {inject} from '@angular/core'; -export class CustomHttpParamEncoder implements HttpParameterCodec { - encodeKey(key: string): string { +export class CustomHttpParamEncoder implements HttpParameterCodec { + encodeKey(key: string): string { return encodeURIComponent(key); } @@ -139,10 +143,10 @@ export class ApiService { const params = new HttpParams({ encoder: new CustomHttpParamEncoder(), }) - .set('email', 'dev+alerts@example.com') - .set('q', 'a & b? c/d = e'); + .set('email', 'dev+alerts@example.com') + .set('q', 'a & b? c/d = e'); - return this.http.get('/api/items', { params }); + return this.http.get('/api/items', {params}); } } ``` @@ -154,13 +158,15 @@ Specify request headers that should be included in the request using the `header Passing an object literal is the simplest way of configuring request headers: ```ts -http.get('/api/config', { - headers: { - 'X-Debug-Level': 'verbose', - } -}).subscribe(config => { - // ... -}); +http + .get('/api/config', { + headers: { + 'X-Debug-Level': 'verbose', + }, + }) + .subscribe((config) => { + // ... + }); ``` Alternatively, pass an instance of `HttpHeaders` if you need more control over the construction of headers @@ -170,11 +176,13 @@ IMPORTANT: Instances of `HttpHeaders` are _immutable_ and cannot be directly cha ```ts const baseHeaders = new HttpHeaders().set('X-Debug-Level', 'minimal'); -http.get('/api/config', { - headers: baseHeaders.set('X-Debug-Level', 'verbose'), -}).subscribe(config => { - // ... -}); +http + .get('/api/config', { + headers: baseHeaders.set('X-Debug-Level', 'verbose'), + }) + .subscribe((config) => { + // ... + }); ``` ## Interacting with the server response events @@ -184,7 +192,7 @@ For convenience, `HttpClient` by default returns an `Observable` of the data ret To access the entire response, set the `observe` option to `'response'`: ```ts -http.get('/api/config', {observe: 'response'}).subscribe(res => { +http.get('/api/config', {observe: 'response'}).subscribe((res) => { console.log('Response status:', res.status); console.log('Body:', res.body); }); @@ -207,19 +215,21 @@ NOTE: The optional `fetch` implementation of `HttpClient` does not report _uploa To observe the event stream, set the `observe` option to `'events'`: ```ts -http.post('/api/upload', myData, { - reportProgress: true, - observe: 'events', -}).subscribe(event => { - switch (event.type) { - case HttpEventType.UploadProgress: - console.log('Uploaded ' + event.loaded + ' out of ' + event.total + ' bytes'); - break; - case HttpEventType.Response: - console.log('Finished uploading!'); - break; - } -}); +http + .post('/api/upload', myData, { + reportProgress: true, + observe: 'events', + }) + .subscribe((event) => { + switch (event.type) { + case HttpEventType.UploadProgress: + console.log('Uploaded ' + event.loaded + ' out of ' + event.total + ' bytes'); + break; + case HttpEventType.Response: + console.log('Finished uploading!'); + break; + } + }); ``` @@ -262,16 +272,18 @@ To set a timeout for a request, you can set the `timeout` option to a number of NOTE: The timeout will only apply to the backend HTTP request itself. It is not a timeout for the entire request handling chain. Therefore, this option is not affected by any delay introduced by interceptors. ```ts -http.get('/api/config', { - timeout: 3000, -}).subscribe({ - next: config => { - console.log('Config fetched successfully:', config); - }, - error: err => { - // If the request times out, an error will have been emitted. - } -}); +http + .get('/api/config', { + timeout: 3000, + }) + .subscribe({ + next: (config) => { + console.log('Config fetched successfully:', config); + }, + error: (err) => { + // If the request times out, an error will have been emitted. + }, + }); ``` ## Advanced fetch options @@ -287,9 +299,11 @@ The following options provide fine-grained control over request behavior when us The `keepalive` option allows a request to outlive the page that initiated it. This is particularly useful for analytics or logging requests that need to complete even if the user navigates away from the page. ```ts -http.post('/api/analytics', analyticsData, { - keepalive: true -}).subscribe(); +http + .post('/api/analytics', analyticsData, { + keepalive: true, + }) + .subscribe(); ``` #### HTTP caching control @@ -298,25 +312,31 @@ The `cache` option controls how the request interacts with the browser's HTTP ca ```ts // Use cached response regardless of freshness -http.get('/api/config', { - cache: 'force-cache' -}).subscribe(config => { - // ... -}); +http + .get('/api/config', { + cache: 'force-cache', + }) + .subscribe((config) => { + // ... + }); // Always fetch from network, bypass cache -http.get('/api/live-data', { - cache: 'no-cache' -}).subscribe(data => { - // ... -}); +http + .get('/api/live-data', { + cache: 'no-cache', + }) + .subscribe((data) => { + // ... + }); // Use cached response only, fail if not in cache -http.get('/api/static-data', { - cache: 'only-if-cached' -}).subscribe(data => { - // ... -}); +http + .get('/api/static-data', { + cache: 'only-if-cached', + }) + .subscribe((data) => { + // ... + }); ``` #### Request priority for Core Web Vitals @@ -325,25 +345,31 @@ The `priority` option allows you to indicate the relative importance of a reques ```ts // High priority for critical resources -http.get('/api/user-profile', { - priority: 'high' -}).subscribe(profile => { - // ... -}); +http + .get('/api/user-profile', { + priority: 'high', + }) + .subscribe((profile) => { + // ... + }); // Low priority for non-critical resources -http.get('/api/recommendations', { - priority: 'low' -}).subscribe(recommendations => { - // ... -}); +http + .get('/api/recommendations', { + priority: 'low', + }) + .subscribe((recommendations) => { + // ... + }); // Auto priority (default) lets the browser decide -http.get('/api/settings', { - priority: 'auto' -}).subscribe(settings => { - // ... -}); +http + .get('/api/settings', { + priority: 'auto', + }) + .subscribe((settings) => { + // ... + }); ``` Available `priority` values: @@ -360,25 +386,31 @@ The `mode` option controls how the request handles cross-origin requests and det ```ts // Same-origin requests only -http.get('/api/local-data', { - mode: 'same-origin' -}).subscribe(data => { - // ... -}); +http + .get('/api/local-data', { + mode: 'same-origin', + }) + .subscribe((data) => { + // ... + }); // CORS-enabled cross-origin requests -http.get('https://api.external.com/data', { - mode: 'cors' -}).subscribe(data => { - // ... -}); +http + .get('https://api.external.com/data', { + mode: 'cors', + }) + .subscribe((data) => { + // ... + }); // No-CORS mode for simple cross-origin requests -http.get('https://external-api.com/public-data', { - mode: 'no-cors' -}).subscribe(data => { - // ... -}); +http + .get('https://external-api.com/public-data', { + mode: 'no-cors', + }) + .subscribe((data) => { + // ... + }); ``` Available `mode` values: @@ -395,30 +427,36 @@ The `redirect` option specifies how to handle redirect responses from the server ```ts // Follow redirects automatically (default behavior) -http.get('/api/resource', { - redirect: 'follow' -}).subscribe(data => { - // ... -}); +http + .get('/api/resource', { + redirect: 'follow', + }) + .subscribe((data) => { + // ... + }); // Prevent automatic redirects -http.get('/api/resource', { - redirect: 'manual' -}).subscribe(response => { - // Handle redirect manually -}); +http + .get('/api/resource', { + redirect: 'manual', + }) + .subscribe((response) => { + // Handle redirect manually + }); // Treat redirects as errors -http.get('/api/resource', { - redirect: 'error' -}).subscribe({ - next: data => { - // Success response - }, - error: err => { - // Redirect responses will trigger this error handler - } -}); +http + .get('/api/resource', { + redirect: 'error', + }) + .subscribe({ + next: (data) => { + // Success response + }, + error: (err) => { + // Redirect responses will trigger this error handler + }, + }); ``` Available `redirect` values: @@ -435,40 +473,50 @@ The `credentials` option controls whether cookies, authorization headers, and ot ```ts // Include credentials for cross-origin requests -http.get('https://api.example.com/protected-data', { - credentials: 'include' -}).subscribe(data => { - // ... -}); +http + .get('https://api.example.com/protected-data', { + credentials: 'include', + }) + .subscribe((data) => { + // ... + }); // Never send credentials (default for cross-origin) -http.get('https://api.example.com/public-data', { - credentials: 'omit' -}).subscribe(data => { - // ... -}); +http + .get('https://api.example.com/public-data', { + credentials: 'omit', + }) + .subscribe((data) => { + // ... + }); // Send credentials only for same-origin requests -http.get('/api/user-data', { - credentials: 'same-origin' -}).subscribe(data => { - // ... -}); +http + .get('/api/user-data', { + credentials: 'same-origin', + }) + .subscribe((data) => { + // ... + }); // withCredentials overrides credentials setting -http.get('https://api.example.com/data', { - credentials: 'omit', // This will be ignored - withCredentials: true // This forces credentials: 'include' -}).subscribe(data => { - // Request will include credentials despite credentials: 'omit' -}); +http + .get('https://api.example.com/data', { + credentials: 'omit', // This will be ignored + withCredentials: true, // This forces credentials: 'include' + }) + .subscribe((data) => { + // Request will include credentials despite credentials: 'omit' + }); // Legacy approach (still supported) -http.get('https://api.example.com/data', { - withCredentials: true -}).subscribe(data => { - // Equivalent to credentials: 'include' -}); +http + .get('https://api.example.com/data', { + withCredentials: true, + }) + .subscribe((data) => { + // Equivalent to credentials: 'include' + }); ``` IMPORTANT: The `withCredentials` option takes precedence over the `credentials` option. If both are specified, `withCredentials: true` will always result in `credentials: 'include'`, regardless of the explicit `credentials` value. @@ -487,18 +535,22 @@ The `referrer` option allows you to control what referrer information is sent wi ```ts // Send a specific referrer URL -http.get('/api/data', { - referrer: 'https://example.com/page' -}).subscribe(data => { - // ... -}); +http + .get('/api/data', { + referrer: 'https://example.com/page', + }) + .subscribe((data) => { + // ... + }); // Use the current page as referrer (default behavior) -http.get('/api/analytics', { - referrer: 'about:client' -}).subscribe(data => { - // ... -}); +http + .get('/api/analytics', { + referrer: 'about:client', + }) + .subscribe((data) => { + // ... + }); ``` The `referrer` option accepts: @@ -515,14 +567,18 @@ The `referrerPolicy` option controls how much referrer information , the URL of ```ts // Send no referrer information regardless of the current page -http.get('/api/data', { - referrerPolicy: 'no-referrer' -}).subscribe(); +http + .get('/api/data', { + referrerPolicy: 'no-referrer', + }) + .subscribe(); // Send origin only (e.g. https://example.com) -http.get('/api/analytics', { - referrerPolicy: 'origin' -}).subscribe(); +http + .get('/api/analytics', { + referrerPolicy: 'origin', + }) + .subscribe(); ``` The `referrerPolicy` option accepts: @@ -544,12 +600,14 @@ The `integrity` option allows you to verify that the response hasn't been tamper ```ts // Verify response integrity with SHA-256 hash -http.get('/api/script.js', { - integrity: 'sha256-ABC123...', - responseType: 'text' -}).subscribe(script => { - // Script content is verified against the hash -}); +http + .get('/api/script.js', { + integrity: 'sha256-ABC123...', + responseType: 'text', + }) + .subscribe((script) => { + // Script content is verified against the hash + }); ``` IMPORTANT: The `integrity` option requires an exact match between the response content and the provided hash. If the content doesn't match, the request will fail with a network error. diff --git a/adev-ja/src/content/guide/http/making-requests.md b/adev-ja/src/content/guide/http/making-requests.md index 6d9d496b3..a1a548fab 100644 --- a/adev-ja/src/content/guide/http/making-requests.md +++ b/adev-ja/src/content/guide/http/making-requests.md @@ -13,7 +13,7 @@ NOTE: `HttpClient` によって作成された `Observable` は、何度でも たとえば、`HttpClient.get()` メソッドを使用して、仮説上のAPIから構成データを取得するには、次のようになります。 ```ts -http.get('/api/config').subscribe(config => { +http.get('/api/config').subscribe((config) => { // 構成を処理します。 }); ``` @@ -38,7 +38,7 @@ CRITICAL: リクエストメソッドのジェネリック型は、サーバー たとえば、`HttpClient` に `.jpeg` イメージの生のバイトを `ArrayBuffer` にダウンロードするように依頼できます。 ```ts -http.get('/images/dog.jpg', {responseType: 'arraybuffer'}).subscribe(buffer => { +http.get('/images/dog.jpg', {responseType: 'arraybuffer'}).subscribe((buffer) => { console.log('画像は ' + buffer.byteLength + ' バイトです'); }); ``` @@ -56,7 +56,7 @@ http.get('/images/dog.jpg', {responseType: 'arraybuffer'}).subscribe(buffer => { [`HttpClient.post()`](api/common/http/HttpClient#post) メソッドは `get()` と同様に動作し、オプションの前に `body` 引数を追加で受け取ります。 ```ts -http.post('/api/config', newConfig).subscribe(config => { +http.post('/api/config', newConfig).subscribe((config) => { console.log('更新された構成:', config); }); ``` @@ -81,11 +81,13 @@ IMPORTANT: 変更リクエスト `Observable` を `.subscribe()` することを オブジェクトリテラルを渡すことは、URLパラメータを構成するための最も簡単な方法です。 ```ts -http.get('/api/config', { - params: {filter: 'all'}, -}).subscribe(config => { - // ... -}); +http + .get('/api/config', { + params: {filter: 'all'}, + }) + .subscribe((config) => { + // ... + }); ``` あるいは、パラメータの構築やシリアル化をより細かく制御する必要がある場合は、`HttpParams` のインスタンスを渡します。 @@ -95,11 +97,13 @@ IMPORTANT: `HttpParams` のインスタンスは*変更不可能*であり、直 ```ts const baseParams = new HttpParams().set('filter', 'all'); -http.get('/api/config', { - params: baseParams.set('details', 'enabled'), -}).subscribe(config => { - // ... -}); +http + .get('/api/config', { + params: baseParams.set('details', 'enabled'), + }) + .subscribe((config) => { + // ... + }); ``` `HttpParams` を、`HttpClient` がパラメータをURLにエンコードする方法を決定するカスタム `HttpParameterCodec` でインスタンス化できます。 @@ -111,11 +115,11 @@ http.get('/api/config', { [`HttpParameterCodec`](api/common/http/HttpParameterCodec) の独自の実装を提供することで、エンコードとデコードの適用方法をカスタマイズできます。 ```ts -import { HttpClient, HttpParams, HttpParameterCodec } from '@angular/common/http'; -import { inject } from '@angular/core'; +import {HttpClient, HttpParams, HttpParameterCodec} from '@angular/common/http'; +import {inject} from '@angular/core'; -export class CustomHttpParamEncoder implements HttpParameterCodec { - encodeKey(key: string): string { +export class CustomHttpParamEncoder implements HttpParameterCodec { + encodeKey(key: string): string { return encodeURIComponent(key); } @@ -139,10 +143,10 @@ export class ApiService { const params = new HttpParams({ encoder: new CustomHttpParamEncoder(), }) - .set('email', 'dev+alerts@example.com') - .set('q', 'a & b? c/d = e'); + .set('email', 'dev+alerts@example.com') + .set('q', 'a & b? c/d = e'); - return this.http.get('/api/items', { params }); + return this.http.get('/api/items', {params}); } } ``` @@ -154,13 +158,15 @@ export class ApiService { オブジェクトリテラルを渡すことは、リクエストヘッダーを構成するための最も簡単な方法です。 ```ts -http.get('/api/config', { - headers: { - 'X-Debug-Level': 'verbose', - } -}).subscribe(config => { - // ... -}); +http + .get('/api/config', { + headers: { + 'X-Debug-Level': 'verbose', + }, + }) + .subscribe((config) => { + // ... + }); ``` あるいは、ヘッダーの構築をより細かく制御する必要がある場合は、`HttpHeaders` のインスタンスを渡します。 @@ -170,11 +176,13 @@ IMPORTANT: `HttpHeaders` のインスタンスは*変更不可能*であり、 ```ts const baseHeaders = new HttpHeaders().set('X-Debug-Level', 'minimal'); -http.get('/api/config', { - headers: baseHeaders.set('X-Debug-Level', 'verbose'), -}).subscribe(config => { - // ... -}); +http + .get('/api/config', { + headers: baseHeaders.set('X-Debug-Level', 'verbose'), + }) + .subscribe((config) => { + // ... + }); ``` ## サーバーレスポンスイベントとの対話 {#interacting-with-the-server-response-events} @@ -184,7 +192,7 @@ http.get('/api/config', { レスポンス全体にアクセスするには、`observe` オプションを `'response'` に設定します。 ```ts -http.get('/api/config', {observe: 'response'}).subscribe(res => { +http.get('/api/config', {observe: 'response'}).subscribe((res) => { console.log('レスポンスステータス:', res.status); console.log('ボディ:', res.body); }); @@ -207,19 +215,21 @@ NOTE: `HttpClient` のオプションの `fetch` 実装は、*アップロード イベントストリームを観察するには、`observe` オプションを `'events'` に設定します。 ```ts -http.post('/api/upload', myData, { - reportProgress: true, - observe: 'events', -}).subscribe(event => { - switch (event.type) { - case HttpEventType.UploadProgress: - console.log('Uploaded ' + event.loaded + ' out of ' + event.total + ' bytes'); - break; - case HttpEventType.Response: - console.log('Finished uploading!'); - break; - } -}); +http + .post('/api/upload', myData, { + reportProgress: true, + observe: 'events', + }) + .subscribe((event) => { + switch (event.type) { + case HttpEventType.UploadProgress: + console.log('Uploaded ' + event.loaded + ' out of ' + event.total + ' bytes'); + break; + case HttpEventType.Response: + console.log('Finished uploading!'); + break; + } + }); ``` @@ -262,16 +272,18 @@ HTTPリクエストは、次の3つの方法で失敗する可能性がありま NOTE: タイムアウトは、バックエンドHTTPリクエスト自体にのみ適用されます。リクエスト処理チェーン全体のタイムアウトではありません。したがって、このオプションは、インターセプターによって導入される遅延の影響を受けません。 ```ts -http.get('/api/config', { - timeout: 3000, -}).subscribe({ - next: config => { - console.log('設定の取得に成功:', config); - }, - error: err => { - // リクエストがタイムアウトした場合、エラーが発行されます。 - } -}); +http + .get('/api/config', { + timeout: 3000, + }) + .subscribe({ + next: (config) => { + console.log('設定の取得に成功:', config); + }, + error: (err) => { + // リクエストがタイムアウトした場合、エラーが発行されます。 + }, + }); ``` ## 高度な fetch オプション {#advanced-fetch-options} @@ -287,9 +299,11 @@ http.get('/api/config', { `keepalive` オプションにより、リクエストはそれを開始したページよりも長く存続できます。これは、ユーザーがページから移動した場合でも完了する必要がある分析やログリクエストに特に有用です。 ```ts -http.post('/api/analytics', analyticsData, { - keepalive: true -}).subscribe(); +http + .post('/api/analytics', analyticsData, { + keepalive: true, + }) + .subscribe(); ``` #### HTTP キャッシュ制御 {#http-caching-control} @@ -298,25 +312,31 @@ http.post('/api/analytics', analyticsData, { ```ts // 新鮮度に関係なくキャッシュされたレスポンスを使用 -http.get('/api/config', { - cache: 'force-cache' -}).subscribe(config => { - // ... -}); +http + .get('/api/config', { + cache: 'force-cache', + }) + .subscribe((config) => { + // ... + }); // 常にネットワークから取得、キャッシュをバイパス -http.get('/api/live-data', { - cache: 'no-cache' -}).subscribe(data => { - // ... -}); +http + .get('/api/live-data', { + cache: 'no-cache', + }) + .subscribe((data) => { + // ... + }); // キャッシュされたレスポンスのみを使用、キャッシュにない場合は失敗 -http.get('/api/static-data', { - cache: 'only-if-cached' -}).subscribe(data => { - // ... -}); +http + .get('/api/static-data', { + cache: 'only-if-cached', + }) + .subscribe((data) => { + // ... + }); ``` #### Core Web Vitals のためのリクエスト優先度 {#request-priority-for-core-web-vitals} @@ -325,25 +345,31 @@ http.get('/api/static-data', { ```ts // 重要なリソースの高優先度 -http.get('/api/user-profile', { - priority: 'high' -}).subscribe(profile => { - // ... -}); +http + .get('/api/user-profile', { + priority: 'high', + }) + .subscribe((profile) => { + // ... + }); // 重要でないリソースの低優先度 -http.get('/api/recommendations', { - priority: 'low' -}).subscribe(recommendations => { - // ... -}); +http + .get('/api/recommendations', { + priority: 'low', + }) + .subscribe((recommendations) => { + // ... + }); // 自動優先度 (デフォルト) はブラウザーが決定 -http.get('/api/settings', { - priority: 'auto' -}).subscribe(settings => { - // ... -}); +http + .get('/api/settings', { + priority: 'auto', + }) + .subscribe((settings) => { + // ... + }); ``` 利用可能な `priority` 値: @@ -360,25 +386,31 @@ TIP: Largest Contentful Paint (LCP) に影響するリクエストには `priori ```ts // 同一オリジンリクエストのみ -http.get('/api/local-data', { - mode: 'same-origin' -}).subscribe(data => { - // ... -}); +http + .get('/api/local-data', { + mode: 'same-origin', + }) + .subscribe((data) => { + // ... + }); // CORS が有効なクロスオリジンリクエスト -http.get('https://api.external.com/data', { - mode: 'cors' -}).subscribe(data => { - // ... -}); +http + .get('https://api.external.com/data', { + mode: 'cors', + }) + .subscribe((data) => { + // ... + }); // シンプルなクロスオリジンリクエストの No-CORS モード -http.get('https://external-api.com/public-data', { - mode: 'no-cors' -}).subscribe(data => { - // ... -}); +http + .get('https://external-api.com/public-data', { + mode: 'no-cors', + }) + .subscribe((data) => { + // ... + }); ``` 利用可能な `mode` 値: @@ -395,30 +427,36 @@ TIP: クロスオリジンに行くべきでない機密リクエストには `m ```ts // リダイレクトを自動的に追跡(デフォルトの動作) -http.get('/api/resource', { - redirect: 'follow' -}).subscribe(data => { - // ... -}); +http + .get('/api/resource', { + redirect: 'follow', + }) + .subscribe((data) => { + // ... + }); // 自動リダイレクトを防ぐ -http.get('/api/resource', { - redirect: 'manual' -}).subscribe(response => { - // リダイレクトを手動で処理 -}); +http + .get('/api/resource', { + redirect: 'manual', + }) + .subscribe((response) => { + // リダイレクトを手動で処理 + }); // リダイレクトをエラーとして扱う -http.get('/api/resource', { - redirect: 'error' -}).subscribe({ - next: data => { - // 成功レスポンス - }, - error: err => { - // リダイレクトレスポンスはこのエラーハンドラーをトリガーします - } -}); +http + .get('/api/resource', { + redirect: 'error', + }) + .subscribe({ + next: (data) => { + // 成功レスポンス + }, + error: (err) => { + // リダイレクトレスポンスはこのエラーハンドラーをトリガーします + }, + }); ``` 利用可能な `redirect` 値: @@ -435,40 +473,50 @@ TIP: カスタムロジックでリダイレクトを処理する必要がある ```ts // クロスオリジンリクエストに認証情報を含める -http.get('https://api.example.com/protected-data', { - credentials: 'include' -}).subscribe(data => { - // ... -}); +http + .get('https://api.example.com/protected-data', { + credentials: 'include', + }) + .subscribe((data) => { + // ... + }); // 認証情報を送信しない(クロスオリジンのデフォルト) -http.get('https://api.example.com/public-data', { - credentials: 'omit' -}).subscribe(data => { - // ... -}); +http + .get('https://api.example.com/public-data', { + credentials: 'omit', + }) + .subscribe((data) => { + // ... + }); // 同一オリジンリクエストのみに認証情報を送信 -http.get('/api/user-data', { - credentials: 'same-origin' -}).subscribe(data => { - // ... -}); +http + .get('/api/user-data', { + credentials: 'same-origin', + }) + .subscribe((data) => { + // ... + }); // withCredentials は credentials 設定を上書きします -http.get('https://api.example.com/data', { - credentials: 'omit', // これは無視されます - withCredentials: true // これにより credentials: 'include' が強制されます -}).subscribe(data => { - // credentials: 'omit' にもかかわらず、リクエストは認証情報を含みます -}); +http + .get('https://api.example.com/data', { + credentials: 'omit', // これは無視されます + withCredentials: true, // これにより credentials: 'include' が強制されます + }) + .subscribe((data) => { + // credentials: 'omit' にもかかわらず、リクエストは認証情報を含みます + }); // レガシーアプローチ(まだサポートされています) -http.get('https://api.example.com/data', { - withCredentials: true -}).subscribe(data => { - // credentials: 'include' と同等 -}); +http + .get('https://api.example.com/data', { + withCredentials: true, + }) + .subscribe((data) => { + // credentials: 'include' と同等 + }); ``` IMPORTANT: `withCredentials` オプションは `credentials` オプションより優先されます。両方が指定されている場合、明示的な `credentials` 値に関係なく、`withCredentials: true` は常に `credentials: 'include'` になります。 @@ -487,18 +535,22 @@ TIP: CORSをサポートする異なるドメインに認証Cookieやヘッダ ```ts // 特定のリファラーURLを送信 -http.get('/api/data', { - referrer: 'https://example.com/page' -}).subscribe(data => { - // ... -}); +http + .get('/api/data', { + referrer: 'https://example.com/page', + }) + .subscribe((data) => { + // ... + }); // 現在のページをリファラーとして使用(デフォルトの動作) -http.get('/api/analytics', { - referrer: 'about:client' -}).subscribe(data => { - // ... -}); +http + .get('/api/analytics', { + referrer: 'about:client', + }) + .subscribe((data) => { + // ... + }); ``` `referrer` オプションは以下を受け入れます。 @@ -515,14 +567,18 @@ TIP: 参照元ページのURLを漏らしたくない機密リクエストには ```ts // 現在のページに関係なくリファラー情報を送信しない -http.get('/api/data', { - referrerPolicy: 'no-referrer' -}).subscribe(); +http + .get('/api/data', { + referrerPolicy: 'no-referrer', + }) + .subscribe(); // オリジンのみを送信(例: https://example.com) -http.get('/api/analytics', { - referrerPolicy: 'origin' -}).subscribe(); +http + .get('/api/analytics', { + referrerPolicy: 'origin', + }) + .subscribe(); ``` `referrerPolicy` オプションは以下を受け入れます。 @@ -544,12 +600,14 @@ TIP: プライバシーに配慮したリクエストには、`'no-referrer'`、 ```ts // SHA-256ハッシュでレスポンスの整合性を検証 -http.get('/api/script.js', { - integrity: 'sha256-ABC123...', - responseType: 'text' -}).subscribe(script => { - // スクリプトのコンテンツがハッシュに対して検証されます -}); +http + .get('/api/script.js', { + integrity: 'sha256-ABC123...', + responseType: 'text', + }) + .subscribe((script) => { + // スクリプトのコンテンツがハッシュに対して検証されます + }); ``` IMPORTANT: `integrity` オプションには、レスポンスコンテンツと提供されたハッシュとの厳密な一致が必要です。コンテンツが一致しない場合、リクエストはネットワークエラーで失敗します。 diff --git a/adev-ja/src/content/guide/http/security.en.md b/adev-ja/src/content/guide/http/security.en.md index fbb283d11..8aba9b2f9 100644 --- a/adev-ja/src/content/guide/http/security.en.md +++ b/adev-ja/src/content/guide/http/security.en.md @@ -47,7 +47,7 @@ export const appConfig: ApplicationConfig = { headerName: 'X-Custom-Xsrf-Header', }), ), - ] + ], }; ``` @@ -57,10 +57,6 @@ If the built-in XSRF protection mechanism doesn't work for your application, you ```ts export const appConfig: ApplicationConfig = { - providers: [ - provideHttpClient( - withNoXsrfProtection(), - ), - ] + providers: [provideHttpClient(withNoXsrfProtection())], }; ``` diff --git a/adev-ja/src/content/guide/http/security.md b/adev-ja/src/content/guide/http/security.md index 1e2188686..b3b6df0b4 100644 --- a/adev-ja/src/content/guide/http/security.md +++ b/adev-ja/src/content/guide/http/security.md @@ -47,7 +47,7 @@ export const appConfig: ApplicationConfig = { headerName: 'X-Custom-Xsrf-Header', }), ), - ] + ], }; ``` @@ -57,10 +57,6 @@ export const appConfig: ApplicationConfig = { ```ts export const appConfig: ApplicationConfig = { - providers: [ - provideHttpClient( - withNoXsrfProtection(), - ), - ] + providers: [provideHttpClient(withNoXsrfProtection())], }; ``` diff --git a/adev-ja/src/content/guide/http/setup.en.md b/adev-ja/src/content/guide/http/setup.en.md index b9961ae2b..242a3f61b 100644 --- a/adev-ja/src/content/guide/http/setup.en.md +++ b/adev-ja/src/content/guide/http/setup.en.md @@ -8,9 +8,7 @@ Before you can use `HttpClient` in your app, you must configure it using [depend ```ts export const appConfig: ApplicationConfig = { - providers: [ - provideHttpClient(), - ] + providers: [provideHttpClient()], }; ``` @@ -18,9 +16,7 @@ If your app is using NgModule-based bootstrap instead, you can include `provideH ```ts @NgModule({ - providers: [ - provideHttpClient(), - ], + providers: [provideHttpClient()], // ... other application configuration }) export class AppModule {} @@ -44,11 +40,7 @@ export class ConfigService { ```ts export const appConfig: ApplicationConfig = { - providers: [ - provideHttpClient( - withFetch(), - ), - ] + providers: [provideHttpClient(withFetch())], }; ``` diff --git a/adev-ja/src/content/guide/http/setup.md b/adev-ja/src/content/guide/http/setup.md index a8bf6f2e2..5aade9e7b 100644 --- a/adev-ja/src/content/guide/http/setup.md +++ b/adev-ja/src/content/guide/http/setup.md @@ -8,9 +8,7 @@ ```ts export const appConfig: ApplicationConfig = { - providers: [ - provideHttpClient(), - ] + providers: [provideHttpClient()], }; ``` @@ -18,9 +16,7 @@ export const appConfig: ApplicationConfig = { ```ts @NgModule({ - providers: [ - provideHttpClient(), - ], + providers: [provideHttpClient()], // ...その他のアプリケーション設定 }) export class AppModule {} @@ -44,11 +40,7 @@ export class ConfigService { ```ts export const appConfig: ApplicationConfig = { - providers: [ - provideHttpClient( - withFetch(), - ), - ] + providers: [provideHttpClient(withFetch())], }; ``` diff --git a/adev-ja/src/content/guide/http/testing.en.md b/adev-ja/src/content/guide/http/testing.en.md index 951ff1181..558efc8d5 100644 --- a/adev-ja/src/content/guide/http/testing.en.md +++ b/adev-ja/src/content/guide/http/testing.en.md @@ -32,11 +32,7 @@ For example, you can write a test that expects a GET request to occur and provid ```ts TestBed.configureTestingModule({ - providers: [ - ConfigService, - provideHttpClient(), - provideHttpClientTesting(), - ], + providers: [ConfigService, provideHttpClient(), provideHttpClientTesting()], }); const httpTesting = TestBed.inject(HttpTestingController); @@ -71,10 +67,13 @@ NOTE: `expectOne` will fail if the test has made more than one request which mat As an alternative to asserting on `req.method`, you could instead use an expanded form of `expectOne` to also match the request method: ```ts -const req = httpTesting.expectOne({ - method: 'GET', - url: '/api/config', -}, 'Request to load the configuration'); +const req = httpTesting.expectOne( + { + method: 'GET', + url: '/api/config', + }, + 'Request to load the configuration', +); ``` HELPFUL: The expectation APIs match against the full URL of requests, including any query parameters. @@ -105,14 +104,14 @@ All matching functions accept a predicate function for custom matching logic: ```ts // Look for one request that has a request body. -const requestsWithBody = httpTesting.expectOne(req => req.body !== null); +const requestsWithBody = httpTesting.expectOne((req) => req.body !== null); ``` The `expectNone` function asserts that no requests match the given criteria. ```ts // Assert that no mutation requests have been issued. -httpTesting.expectNone(req => req.method !== 'GET'); +httpTesting.expectNone((req) => req.method !== 'GET'); ``` ## Testing error handling @@ -149,7 +148,10 @@ For example, an application may be required to add an authentication token gener This behavior can be enforced with the use of an interceptor: ```ts -export function authInterceptor(request: HttpRequest, next: HttpHandlerFn): Observable> { +export function authInterceptor( + request: HttpRequest, + next: HttpHandlerFn, +): Observable> { const authService = inject(AuthService); const clonedRequest = request.clone({ @@ -206,7 +208,7 @@ TestBed.configureTestingModule({ provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting(), // We rely on the HTTP_INTERCEPTORS token to register the AuthInterceptor as an HttpInterceptor - { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, + {provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true}, ], }); ``` diff --git a/adev-ja/src/content/guide/http/testing.md b/adev-ja/src/content/guide/http/testing.md index 8b6576333..b4e41788d 100644 --- a/adev-ja/src/content/guide/http/testing.md +++ b/adev-ja/src/content/guide/http/testing.md @@ -32,11 +32,7 @@ const httpTesting = TestBed.inject(HttpTestingController); ```ts TestBed.configureTestingModule({ - providers: [ - ConfigService, - provideHttpClient(), - provideHttpClientTesting(), - ], + providers: [ConfigService, provideHttpClient(), provideHttpClientTesting()], }); const httpTesting = TestBed.inject(HttpTestingController); @@ -71,10 +67,13 @@ NOTE: `expectOne` は、テストが指定された基準に一致するリク `req.method` についてアサートする代わりに、`expectOne` の拡張形式を使用してリクエストメソッドも一致させることができます。 ```ts -const req = httpTesting.expectOne({ - method: 'GET', - url: '/api/config', -}, '構成をロードするリクエスト'); +const req = httpTesting.expectOne( + { + method: 'GET', + url: '/api/config', + }, + '構成をロードするリクエスト', +); ``` HELPFUL: 期待APIは、クエリパラメータを含むリクエストの完全なURLと一致します。 @@ -105,14 +104,14 @@ for (const req of allGetRequests) { ```ts // リクエストボディを持つリクエストを 1 つ探します。 -const requestsWithBody = httpTesting.expectOne(req => req.body !== null); +const requestsWithBody = httpTesting.expectOne((req) => req.body !== null); ``` `expectNone` 関数は、指定された基準に一致するリクエストがないことをアサートします。 ```ts // 突然変異リクエストが発行されていないことをアサートします。 -httpTesting.expectNone(req => req.method !== 'GET'); +httpTesting.expectNone((req) => req.method !== 'GET'); ``` ## エラー処理のテスト @@ -149,7 +148,10 @@ req.error(new ProgressEvent('network error!')); この動作は、インターセプターの使用によって強制できます。 ```ts -export function authInterceptor(request: HttpRequest, next: HttpHandlerFn): Observable> { +export function authInterceptor( + request: HttpRequest, + next: HttpHandlerFn, +): Observable> { const authService = inject(AuthService); const clonedRequest = request.clone({ @@ -206,7 +208,7 @@ TestBed.configureTestingModule({ provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting(), // HTTP_INTERCEPTORS トークンに依存して、AuthInterceptor を HttpInterceptor として登録します - { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, + {provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true}, ], }); ``` From 62a62abf9adf7d8397a353079ddd7baf30edc482 Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 11:25:38 +0900 Subject: [PATCH 13/32] docs(guide/routing): migrate common-router-tasks .en.md changes --- .../guide/routing/common-router-tasks.en.md | 14 ++++++-------- .../content/guide/routing/common-router-tasks.md | 14 ++++++-------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/adev-ja/src/content/guide/routing/common-router-tasks.en.md b/adev-ja/src/content/guide/routing/common-router-tasks.en.md index 9d4ce0574..61c29c5ea 100644 --- a/adev-ja/src/content/guide/routing/common-router-tasks.en.md +++ b/adev-ja/src/content/guide/routing/common-router-tasks.en.md @@ -22,9 +22,7 @@ To get information from a route: Add the `withComponentInputBinding` feature to the `provideRouter` method. ```ts -providers: [ - provideRouter(appRoutes, withComponentInputBinding()), -] +providers: [provideRouter(appRoutes, withComponentInputBinding())]; ```
@@ -34,7 +32,7 @@ providers: [ Update the component to have an `input()` property matching the name of the parameter. ```ts -id = input.required() +id = input.required(); hero = computed(() => this.service.getHero(id())); ``` @@ -51,7 +49,7 @@ id = input.required({ transform: (maybeUndefined: string | undefined) => maybeUndefined ?? '0', }); // or -id = input(); +id = input(); internalId = linkedSignal(() => this.id() ?? getDefaultId()); ``` @@ -68,9 +66,9 @@ To display a 404 page, set up a [wildcard route](guide/routing/common-router-tas ```ts const routes: Routes = [ - { path: 'first-component', component: FirstComponent }, - { path: 'second-component', component: SecondComponent }, - { path: '**', component: PageNotFoundComponent }, // Wildcard route for a 404 page + {path: 'first-component', component: FirstComponent}, + {path: 'second-component', component: SecondComponent}, + {path: '**', component: PageNotFoundComponent}, // Wildcard route for a 404 page ]; ``` diff --git a/adev-ja/src/content/guide/routing/common-router-tasks.md b/adev-ja/src/content/guide/routing/common-router-tasks.md index 583a71220..e58eba4c5 100644 --- a/adev-ja/src/content/guide/routing/common-router-tasks.md +++ b/adev-ja/src/content/guide/routing/common-router-tasks.md @@ -22,9 +22,7 @@ `provideRouter` メソッドに `withComponentInputBinding` 機能を追加します。 ```ts -providers: [ - provideRouter(appRoutes, withComponentInputBinding()), -] +providers: [provideRouter(appRoutes, withComponentInputBinding())]; ``` @@ -34,7 +32,7 @@ providers: [ パラメータ名と一致する `input()` プロパティを持つようにコンポーネントを更新します。 ```ts -id = input.required() +id = input.required(); hero = computed(() => this.service.getHero(id())); ``` @@ -51,7 +49,7 @@ id = input.required({ transform: (maybeUndefined: string | undefined) => maybeUndefined ?? '0', }); // or -id = input(); +id = input(); internalId = linkedSignal(() => this.id() ?? getDefaultId()); ``` @@ -68,9 +66,9 @@ NOTE: 静的なルートデータ、解決されたルートデータ、パス ```ts const routes: Routes = [ - { path: 'first-component', component: FirstComponent }, - { path: 'second-component', component: SecondComponent }, - { path: '**', component: PageNotFoundComponent }, // Wildcard route for a 404 page + {path: 'first-component', component: FirstComponent}, + {path: 'second-component', component: SecondComponent}, + {path: '**', component: PageNotFoundComponent}, // Wildcard route for a 404 page ]; ``` From 301e42e5b9a57bac9fdc070fb63912fca8cd7c8f Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 11:33:29 +0900 Subject: [PATCH 14/32] docs(guide/routing): migrate customizing-route-behavior .en.md changes --- .../routing/customizing-route-behavior.en.md | 241 ++++++++++++------ .../routing/customizing-route-behavior.md | 241 ++++++++++++------ 2 files changed, 320 insertions(+), 162 deletions(-) diff --git a/adev-ja/src/content/guide/routing/customizing-route-behavior.en.md b/adev-ja/src/content/guide/routing/customizing-route-behavior.en.md index a489a8e15..ae063dd42 100644 --- a/adev-ja/src/content/guide/routing/customizing-route-behavior.en.md +++ b/adev-ja/src/content/guide/routing/customizing-route-behavior.en.md @@ -24,7 +24,7 @@ Switching to `'computed'` keeps the in-flight history index in sync with the Ang This setting is most helpful when your app uses `urlUpdateStrategy: 'eager'` or when guards frequently cancel popstate navigations initiated by the browser. ```ts -provideRouter(routes, withRouterConfig({ canceledNavigationResolution: 'computed' })); +provideRouter(routes, withRouterConfig({canceledNavigationResolution: 'computed'})); ``` ### React to same-URL navigations @@ -34,13 +34,13 @@ provideRouter(routes, withRouterConfig({ canceledNavigationResolution: 'computed This is useful when you want repeated clicks on a list filter, left-nav item, or refresh button to trigger new data retrieval even though the URL does not change. ```ts -provideRouter(routes, withRouterConfig({ onSameUrlNavigation: 'reload' })); +provideRouter(routes, withRouterConfig({onSameUrlNavigation: 'reload'})); ``` You can also control this behavior on individual navigations rather than globally. This allows you to keep the keep the default of `'ignore'` while selectively enabling reload behavior for specific use cases: ```ts -router.navigate(['/some-path'], { onSameUrlNavigation: 'reload' }); +router.navigate(['/some-path'], {onSameUrlNavigation: 'reload'}); ``` ### Control parameter inheritance @@ -50,7 +50,7 @@ router.navigate(['/some-path'], { onSameUrlNavigation: 'reload' }); With the default `'emptyOnly'`, child routes inherit params only when their path is empty or the parent does not declare a component. ```ts -provideRouter(routes, withRouterConfig({ paramsInheritanceStrategy: 'always' })); +provideRouter(routes, withRouterConfig({paramsInheritanceStrategy: 'always'})); ``` ```ts @@ -65,17 +65,19 @@ export const routes: Routes = [ children: [ { path: 'customers/:customerId', - component: Customer - } - ] - } - ] - } + component: Customer, + }, + ], + }, + ], + }, ]; ``` ```ts -@Component({ /* ... */}) +@Component({ + /* ... */ +}) export class CustomerComponent { private route = inject(ActivatedRoute); @@ -85,10 +87,16 @@ export class CustomerComponent { } ``` -Using `'always'` ensures matrix parameters, route data, and resolved values are available further down the route tree—handy when you share contextual identifiers across feature areas such as `/org/:orgId/projects/:projectId/customers/:customerId`. +Using `'always'` ensures matrix parameters, route data, and resolved values are available further down the route tree—handy when you share contextual identifiers across feature areas such as: + +```text {hideCopy} +/org/:orgId/projects/:projectId/customers/:customerId +``` ```ts -@Component({ /* ... */}) +@Component({ + /* ... */ +}) export class CustomerComponent { private route = inject(ActivatedRoute); @@ -106,7 +114,7 @@ export class CustomerComponent { Consider this when your analytics pipeline needs to see the attempted route even if guards block it. ```ts -provideRouter(routes, withRouterConfig({ urlUpdateStrategy: 'eager' })); +provideRouter(routes, withRouterConfig({urlUpdateStrategy: 'eager'})); ``` ### Choose default query parameter handling @@ -114,7 +122,7 @@ provideRouter(routes, withRouterConfig({ urlUpdateStrategy: 'eager' })); `defaultQueryParamsHandling` sets the fallback behavior for `Router.createUrlTree` when the call does not specify `queryParamsHandling`. `'replace'` is the default and swaps out the existing query string. `'merge'` combines the provided values with the current ones, and `'preserve'` keeps the existing query parameters unless you explicitly supply new ones. ```ts -provideRouter(routes, withRouterConfig({ defaultQueryParamsHandling: 'merge' })); +provideRouter(routes, withRouterConfig({defaultQueryParamsHandling: 'merge'})); ``` This is especially helpful for search and filter pages to automatically retain existing filters when additional parameters are provided. @@ -147,21 +155,27 @@ Angular's `RouteReuseStrategy` class allows you to customize navigation behavior "Detached route handles" are Angular's way of storing component instances and their entire view hierarchy. When a route is detached, Angular preserves the component instance, its child components, and all associated state in memory. This preserved state can later be reattached when navigating back to the route. -The `RouteReuseStrategy` class provides five methods that control the lifecycle of route components: +The `RouteReuseStrategy` class provides the following methods that control the lifecycle of route components: -| Method | Description | -| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -| [`shouldDetach`](api/router/RouteReuseStrategy#shouldDetach) | Determines if a route should be stored for later reuse when navigating away | -| [`store`](api/router/RouteReuseStrategy#store) | Stores the detached route handle when `shouldDetach` returns true | -| [`shouldAttach`](api/router/RouteReuseStrategy#shouldAttach) | Determines if a stored route should be reattached when navigating to it | -| [`retrieve`](api/router/RouteReuseStrategy#retrieve) | Returns the previously stored route handle for reattachment | -| [`shouldReuseRoute`](api/router/RouteReuseStrategy#shouldReuseRoute) | Determines if the router should reuse the current route instance instead of destroying it during navigation | +| Method | Description | +| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | +| [`shouldDetach`](api/router/RouteReuseStrategy#shouldDetach) | Determines if a route should be stored for later reuse when navigating away | +| [`store`](api/router/RouteReuseStrategy#store) | Stores the detached route handle when `shouldDetach` returns true | +| [`shouldAttach`](api/router/RouteReuseStrategy#shouldAttach) | Determines if a stored route should be reattached when navigating to it | +| [`retrieve`](api/router/RouteReuseStrategy#retrieve) | Returns the previously stored route handle for reattachment | +| [`shouldReuseRoute`](api/router/RouteReuseStrategy#shouldReuseRoute) | Determines if the router should reuse the current route instance instead of destroying it during navigation | +| [`shouldDestroyInjector`](api/router/RouteReuseStrategy#shouldDestroyInjector) | (Experimental) Determines if the router should destroy the injector of a detached route when it is no longer stored | The following example demonstrates a custom route reuse strategy that selectively preserves component state based on route metadata: ```ts -import { RouteReuseStrategy, Route, ActivatedRouteSnapshot, DetachedRouteHandle } from '@angular/router'; -import { Injectable } from '@angular/core'; +import { + RouteReuseStrategy, + Route, + ActivatedRouteSnapshot, + DetachedRouteHandle, +} from '@angular/router'; +import {Injectable} from '@angular/core'; @Injectable() export class CustomRouteReuseStrategy implements RouteReuseStrategy { @@ -189,7 +203,7 @@ export class CustomRouteReuseStrategy implements RouteReuseStrategy { retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null { // Returns the stored route handle for reattachment const key = this.getRouteKey(route); - return route.data['reuse'] === true ? this.handlers.get(key) ?? null : null; + return route.data['reuse'] === true ? (this.handlers.get(key) ?? null) : null; } shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { @@ -203,8 +217,79 @@ export class CustomRouteReuseStrategy implements RouteReuseStrategy { } ``` +### Manually destroying detached route handles + +When implementing a custom `RouteReuseStrategy`, you may need to manually destroy a `DetachedRouteHandle` if you decide to discard it without reattaching it. For example, if your strategy has a cache size limit or expires handles after a certain time, you must ensure the component and its state are properly destroyed to avoid memory leaks. + +Since `DetachedRouteHandle` is an opaque type, you cannot call a destroy method directly on it. Instead, use the `destroyDetachedRouteHandle` function provided by the Router. + +```ts +import { destroyDetachedRouteHandle } from '@angular/router'; + +// ... inside your strategy +if (this.handles.size > MAX_CACHE_SIZE) { + const handle = this.handles.get(oldestKey); + if (handle) { + destroyDetachedRouteHandle(handle); + this.handles.delete(oldestKey); + } +} +``` + NOTE: Avoid using the route path as the key when `canMatch` guards are involved, as it may lead to duplicate entries. +### (Experimental) Automatic cleanup of unused route injectors + +By default, Angular does not destroy the injectors of detached routes, even if they are no longer stored by the `RouteReuseStrategy`. This is primarily because this level of memory management is not commonly needed for most applications. + +To enable automatic cleanup of unused route injectors, you can use the `withExperimentalAutoCleanupInjectors` feature in your router configuration. This feature checks which routes are currently stored by the strategy after navigations and destroys the injectors of any detached routes that are not currently stored by the reuse strategy. + +```ts +import { provideRouter, withExperimentalAutoCleanupInjectors } from '@angular/router'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideRouter(routes, withExperimentalAutoCleanupInjectors()) + ] +}; +``` + +If you do not provide a custom `RouteReuseStrategy` or your custom strategy extends `BaseRouteReuseStrategy`, injectors will now be destroyed when the route is inactive. + +#### Cleanup with a custom `RouteReuseStrategy` + +If your application uses a custom `RouteReuseStrategy` _and_ the strategy does not extend `BaseRouteReuseStrategy`, you must implement `shouldDestroyInjector` to tell the router which routes should have their injectors destroyed: + +```ts +@Injectable() +export class CustomRouteReuseStrategy implements RouteReuseStrategy { + // ... other methods + + shouldDestroyInjector(route: Route): boolean { + return !route.data['retainInjector']; + } +} +``` + +If your strategy ever stores a `DetachedRouteHandle`, you will also need to tell the Router about these so it does not destroy any injectors needed by that detached handle: + +```ts +@Injectable() +export class CustomRouteReuseStrategy implements RouteReuseStrategy { + private readonly handles = new Map(); + + store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null) { + this.handles.set(route.routeConfig!, handle); + } + + retrieveStoredRouteHandles(): DetachedRouteHandle { + return Array.from(this.handles.values()); + } + + // ... other methods +} +``` + ### Configuring a route to use a custom route reuse strategy Routes can opt into reuse behavior through route configuration metadata. This approach keeps the reuse logic separate from component code, making it easy to adjust behavior without modifying components: @@ -214,7 +299,7 @@ export const routes: Routes = [ { path: 'products', component: ProductListComponent, - data: { reuse: true } // Component state persists across navigations + data: {reuse: true}, // Component state persists across navigations }, { path: 'products/:id', @@ -224,8 +309,8 @@ export const routes: Routes = [ { path: 'search', component: SearchComponent, - data: { reuse: true } // Preserves search results and filter state - } + data: {reuse: true}, // Preserves search results and filter state + }, ]; ``` @@ -235,8 +320,8 @@ You can also configure a custom route reuse strategy at the application level th export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes), - { provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy } - ] + {provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy}, + ], }; ``` @@ -256,17 +341,12 @@ Angular provides two preloading strategies out of the box: The `PreloadAllModules` strategy can be configured as follows: ```ts -import { ApplicationConfig } from '@angular/core'; -import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router'; -import { routes } from './app.routes'; +import {ApplicationConfig} from '@angular/core'; +import {provideRouter, withPreloading, PreloadAllModules} from '@angular/router'; +import {routes} from './app.routes'; export const appConfig: ApplicationConfig = { - providers: [ - provideRouter( - routes, - withPreloading(PreloadAllModules) - ) - ] + providers: [provideRouter(routes, withPreloading(PreloadAllModules))], }; ``` @@ -277,10 +357,10 @@ The `PreloadAllModules` strategy works well for small to medium applications whe Custom preloading strategies implement the `PreloadingStrategy` interface, which requires a single `preload` method. This method receives the route configuration and a function that triggers the actual module load. The strategy returns an Observable that emits when preloading completes or an empty Observable to skip preloading: ```ts -import { Injectable } from '@angular/core'; -import { PreloadingStrategy, Route } from '@angular/router'; -import { Observable, of, timer } from 'rxjs'; -import { mergeMap } from 'rxjs/operators'; +import {Injectable} from '@angular/core'; +import {PreloadingStrategy, Route} from '@angular/router'; +import {Observable, of, timer} from 'rxjs'; +import {mergeMap} from 'rxjs/operators'; @Injectable() export class SelectivePreloadingStrategy implements PreloadingStrategy { @@ -297,24 +377,24 @@ export class SelectivePreloadingStrategy implements PreloadingStrategy { This selective strategy checks route metadata to determine preloading behavior. Routes can opt into preloading through their configuration: ```ts -import { Routes } from '@angular/router'; +import {Routes} from '@angular/router'; export const routes: Routes = [ { path: 'dashboard', loadChildren: () => import('./dashboard/dashboard.routes'), - data: { preload: true } // Preload immediately after initial navigation + data: {preload: true}, // Preload immediately after initial navigation }, { path: 'reports', loadChildren: () => import('./reports/reports.routes'), - data: { preload: false } // Only load when user navigates to reports + data: {preload: false}, // Only load when user navigates to reports }, { path: 'admin', - loadChildren: () => import('./admin/admin.routes') + loadChildren: () => import('./admin/admin.routes'), // No preload flag - won't be preloaded - } + }, ]; ``` @@ -337,15 +417,14 @@ The `UrlHandlingStrategy` class gives you control over this boundary between Ang Custom URL handling strategies extend the `UrlHandlingStrategy` class and implement three methods. The `shouldProcessUrl` method determines whether Angular should handle a given URL, `extract` returns the portion of the URL that Angular should process, and `merge` combines the URL fragment with the rest of the URL: ```ts -import { Injectable } from '@angular/core'; -import { UrlHandlingStrategy, UrlTree } from '@angular/router'; +import {Injectable} from '@angular/core'; +import {UrlHandlingStrategy, UrlTree} from '@angular/router'; @Injectable() export class CustomUrlHandlingStrategy implements UrlHandlingStrategy { shouldProcessUrl(url: UrlTree): boolean { // Only handle URLs that start with /app or /admin - return url.toString().startsWith('/app') || - url.toString().startsWith('/admin'); + return url.toString().startsWith('/app') || url.toString().startsWith('/admin'); } extract(url: UrlTree): UrlTree { @@ -367,15 +446,15 @@ This strategy creates clear boundaries in the URL space. Angular handles `/app` You can register a custom strategy through Angular's dependency injection system: ```ts -import { ApplicationConfig } from '@angular/core'; -import { provideRouter } from '@angular/router'; -import { UrlHandlingStrategy } from '@angular/router'; +import {ApplicationConfig} from '@angular/core'; +import {provideRouter} from '@angular/router'; +import {UrlHandlingStrategy} from '@angular/router'; export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes), - { provide: UrlHandlingStrategy, useClass: CustomUrlHandlingStrategy } - ] + {provide: UrlHandlingStrategy, useClass: CustomUrlHandlingStrategy}, + ], }; ``` @@ -392,20 +471,20 @@ The router evaluates custom matchers during the route matching phase, before pat A custom matcher is a function that receives URL segments and returns either a match result with consumed segments and parameters, or null to indicate no match. The matcher function runs before Angular evaluates the route's path property: ```ts -import { Route, UrlSegment, UrlSegmentGroup, UrlMatchResult } from '@angular/router'; +import {Route, UrlSegment, UrlSegmentGroup, UrlMatchResult} from '@angular/router'; export function customMatcher( segments: UrlSegment[], group: UrlSegmentGroup, - route: Route + route: Route, ): UrlMatchResult | null { // Matching logic here if (matchSuccessful) { return { consumed: segments, posParams: { - paramName: new UrlSegment('paramValue', {}) - } + paramName: new UrlSegment('paramValue', {}), + }, }; } return null; @@ -417,17 +496,17 @@ export function customMatcher( Consider an API documentation site that needs to route based on version numbers in the URL. Different versions might have different component structures or feature sets: ```ts -import { Routes, UrlSegment, UrlMatchResult } from '@angular/router'; +import {Routes, UrlSegment, UrlMatchResult} from '@angular/router'; export function versionMatcher(segments: UrlSegment[]): UrlMatchResult | null { // Match patterns like /v1/docs, /v2.1/docs, /v3.0.1/docs if (segments.length >= 2 && segments[0].path.match(/^v\d+(\.\d+)*$/)) { return { - consumed: segments.slice(0, 2), // Consume version and 'docs' + consumed: segments.slice(0, 2), // Consume version and 'docs' posParams: { - version: segments[0], // Make version available as a parameter - section: segments[1] // Make section available too - } + version: segments[0], // Make version available as a parameter + section: segments[1], // Make section available too + }, }; } return null; @@ -437,12 +516,12 @@ export function versionMatcher(segments: UrlSegment[]): UrlMatchResult | null { export const routes: Routes = [ { matcher: versionMatcher, - component: DocumentationComponent + component: DocumentationComponent, }, { path: 'latest/docs', - redirectTo: 'v3/docs' - } + redirectTo: 'v3/docs', + }, ]; ``` @@ -505,16 +584,16 @@ export function localeMatcher(segments: UrlSegment[]): UrlMatchResult | null { return { consumed: [segments[0]], posParams: { - locale: segments[0] - } + locale: segments[0], + }, }; } else { // No locale prefix, use default locale return { - consumed: [], // Don't consume any segments + consumed: [], // Don't consume any segments posParams: { - locale: new UrlSegment('en', {}) - } + locale: new UrlSegment('en', {}), + }, }; } } @@ -539,8 +618,8 @@ export function productMatcher(segments: UrlSegment[]): UrlMatchResult | null { consumed: [segments[0]], posParams: { productType: new UrlSegment('book', {}), - identifier: new UrlSegment(firstSegment.substring(5), {}) - } + identifier: new UrlSegment(firstSegment.substring(5), {}), + }, }; } @@ -550,8 +629,8 @@ export function productMatcher(segments: UrlSegment[]): UrlMatchResult | null { consumed: segments.slice(0, 2), posParams: { productType: new UrlSegment('electronics', {}), - identifier: segments[1] - } + identifier: segments[1], + }, }; } @@ -562,8 +641,8 @@ export function productMatcher(segments: UrlSegment[]): UrlMatchResult | null { posParams: { productType: new UrlSegment('clothing', {}), brand: segments[1], - identifier: segments[2] - } + identifier: segments[2], + }, }; } diff --git a/adev-ja/src/content/guide/routing/customizing-route-behavior.md b/adev-ja/src/content/guide/routing/customizing-route-behavior.md index bf8deb176..0d159070e 100644 --- a/adev-ja/src/content/guide/routing/customizing-route-behavior.md +++ b/adev-ja/src/content/guide/routing/customizing-route-behavior.md @@ -24,7 +24,7 @@ NOTE: カスタムストラテジーを実装する前に、デフォルトの この設定は、アプリケーションが`urlUpdateStrategy: 'eager'`を使用している場合や、ガードがブラウザによって開始された`popstate`ナビゲーションを頻繁にキャンセルする場合に最も役立ちます。 ```ts -provideRouter(routes, withRouterConfig({ canceledNavigationResolution: 'computed' })); +provideRouter(routes, withRouterConfig({canceledNavigationResolution: 'computed'})); ``` ### 同じURLへのナビゲーションへの対応 {#react-to-same-url-navigations} @@ -34,13 +34,13 @@ provideRouter(routes, withRouterConfig({ canceledNavigationResolution: 'computed これは、URLが変更されない場合でも、リストフィルター、左側のナビゲーション項目、または更新ボタンを繰り返しクリックして新しいデータ取得をトリガーしたい場合に便利です。 ```ts -provideRouter(routes, withRouterConfig({ onSameUrlNavigation: 'reload' })); +provideRouter(routes, withRouterConfig({onSameUrlNavigation: 'reload'})); ``` グローバルにではなく、個々のナビゲーションでこの動作を制御できます。これにより、特定のユースケースに対してリロード動作を選択的に有効にしながら、デフォルトの`'ignore'`を維持できます。 ```ts -router.navigate(['/some-path'], { onSameUrlNavigation: 'reload' }); +router.navigate(['/some-path'], {onSameUrlNavigation: 'reload'}); ``` ### パラメータ継承の制御 {#control-parameter-inheritance} @@ -50,7 +50,7 @@ router.navigate(['/some-path'], { onSameUrlNavigation: 'reload' }); デフォルトの`'emptyOnly'`では、子ルートはパスが空の場合、または親がコンポーネントを宣言していない場合にのみパラメータを継承します。 ```ts -provideRouter(routes, withRouterConfig({ paramsInheritanceStrategy: 'always' })); +provideRouter(routes, withRouterConfig({paramsInheritanceStrategy: 'always'})); ``` ```ts @@ -65,17 +65,19 @@ export const routes: Routes = [ children: [ { path: 'customers/:customerId', - component: Customer - } - ] - } - ] - } + component: Customer, + }, + ], + }, + ], + }, ]; ``` ```ts -@Component({ /* ... */}) +@Component({ + /* ... */ +}) export class CustomerComponent { private route = inject(ActivatedRoute); @@ -85,10 +87,16 @@ export class CustomerComponent { } ``` -`'always'`を使用すると、マトリックスパラメータ、ルートデータ、および解決された値がルートツリーのさらに下で利用可能になります。これは、`/org/:orgId/projects/:projectId/customers/:customerId`のような機能領域間でコンテキスト識別子を共有する場合に便利です。 +`'always'`を使用すると、マトリックスパラメータ、ルートデータ、および解決された値がルートツリーのさらに下で利用可能になります。これは、次のような機能領域間でコンテキスト識別子を共有する場合に便利です: + +```text {hideCopy} +/org/:orgId/projects/:projectId/customers/:customerId +``` ```ts -@Component({ /* ... */}) +@Component({ + /* ... */ +}) export class CustomerComponent { private route = inject(ActivatedRoute); @@ -106,7 +114,7 @@ export class CustomerComponent { 分析パイプラインが、ガードによってブロックされた場合でも試行されたルートを確認する必要がある場合にこれを検討してください。 ```ts -provideRouter(routes, withRouterConfig({ urlUpdateStrategy: 'eager' })); +provideRouter(routes, withRouterConfig({urlUpdateStrategy: 'eager'})); ``` ### デフォルトのクエリパラメータ処理の選択 {#choose-default-query-parameter-handling} @@ -114,7 +122,7 @@ provideRouter(routes, withRouterConfig({ urlUpdateStrategy: 'eager' })); `defaultQueryParamsHandling`は、呼び出しが`queryParamsHandling`を指定しない場合の`Router.createUrlTree`のフォールバック動作を設定します。`'replace'`はデフォルトで、既存のクエリ文字列を置き換えます。`'merge'`は提供された値を現在の値と結合し、`'preserve'`は明示的に新しいクエリパラメータを指定しない限り、既存のクエリパラメータを保持します。 ```ts -provideRouter(routes, withRouterConfig({ defaultQueryParamsHandling: 'merge' })); +provideRouter(routes, withRouterConfig({defaultQueryParamsHandling: 'merge'})); ``` これは、追加のパラメータが提供されたときに既存のフィルターを自動的に保持するために、検索およびフィルターページで特に役立ちます。 @@ -147,21 +155,27 @@ Angularの`RouteReuseStrategy`クラスを使用すると、「デタッチさ 「デタッチされたルートハンドル」は、Angularがコンポーネントインスタンスとそのビュー階層全体を保存する方法です。ルートがデタッチされると、Angularはコンポーネントインスタンス、その子コンポーネント、および関連するすべての状態をメモリに保持します。この保持された状態は、後でそのルートに戻ったときに再アタッチできます。 -`RouteReuseStrategy`クラスは、ルートコンポーネントのライフサイクルを制御する5つのメソッドを提供します: +`RouteReuseStrategy`クラスは、ルートコンポーネントのライフサイクルを制御する次のメソッドを提供します: -| メソッド | 説明 | -| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -| [`shouldDetach`](api/router/RouteReuseStrategy#shouldDetach) | ルートから離れるときに、後で再利用するためにルートを保存すべきかどうかを決定します | -| [`store`](api/router/RouteReuseStrategy#store) | `shouldDetach`がtrueを返した場合に、デタッチされたルートハンドルを保存します | -| [`shouldAttach`](api/router/RouteReuseStrategy#shouldAttach) | 保存されたルートにナビゲートするときに、それを再アタッチすべきかどうかを決定します | -| [`retrieve`](api/router/RouteReuseStrategy#retrieve) | 以前に保存されたルートハンドルを再アタッチのために返します | -| [`shouldReuseRoute`](api/router/RouteReuseStrategy#shouldReuseRoute) | ナビゲーション中に現在のルートインスタンスを破棄する代わりに、ルーターが再利用すべきかどうかを決定します | +| メソッド | 説明 | +| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | +| [`shouldDetach`](api/router/RouteReuseStrategy#shouldDetach) | ルートから離れるときに、後で再利用するためにルートを保存すべきかどうかを決定します | +| [`store`](api/router/RouteReuseStrategy#store) | `shouldDetach`がtrueを返した場合に、デタッチされたルートハンドルを保存します | +| [`shouldAttach`](api/router/RouteReuseStrategy#shouldAttach) | 保存されたルートにナビゲートするときに、それを再アタッチすべきかどうかを決定します | +| [`retrieve`](api/router/RouteReuseStrategy#retrieve) | 以前に保存されたルートハンドルを再アタッチのために返します | +| [`shouldReuseRoute`](api/router/RouteReuseStrategy#shouldReuseRoute) | ナビゲーション中に現在のルートインスタンスを破棄する代わりに、ルーターが再利用すべきかどうかを決定します | +| [`shouldDestroyInjector`](api/router/RouteReuseStrategy#shouldDestroyInjector) | (実験的) ルーターが、保存されなくなったデタッチされたルートのインジェクターを破棄すべきかどうかを決定します | 次の例は、ルートメタデータに基づいてコンポーネントの状態を選択的に保持するカスタムルート再利用戦略を示しています: ```ts -import { RouteReuseStrategy, Route, ActivatedRouteSnapshot, DetachedRouteHandle } from '@angular/router'; -import { Injectable } from '@angular/core'; +import { + RouteReuseStrategy, + Route, + ActivatedRouteSnapshot, + DetachedRouteHandle, +} from '@angular/router'; +import {Injectable} from '@angular/core'; @Injectable() export class CustomRouteReuseStrategy implements RouteReuseStrategy { @@ -189,7 +203,7 @@ export class CustomRouteReuseStrategy implements RouteReuseStrategy { retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null { // Returns the stored route handle for reattachment const key = this.getRouteKey(route); - return route.data['reuse'] === true ? this.handlers.get(key) ?? null : null; + return route.data['reuse'] === true ? (this.handlers.get(key) ?? null) : null; } shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { @@ -203,8 +217,79 @@ export class CustomRouteReuseStrategy implements RouteReuseStrategy { } ``` +### デタッチされたルートハンドルを手動で破棄する {#manually-destroying-detached-route-handles} + +カスタム`RouteReuseStrategy`を実装する際、再アタッチせずにそれを破棄することにした場合、`DetachedRouteHandle`を手動で破棄する必要がある場合があります。たとえば、ストラテジーにキャッシュサイズの制限がある場合や、一定時間後にハンドルの有効期限が切れる場合、メモリリークを防ぐために、コンポーネントとその状態が適切に破棄されていることを確認する必要があります。 + +`DetachedRouteHandle`は不透明な型であるため、それに対して直接破棄メソッドを呼び出すことはできません。代わりに、ルーターが提供する`destroyDetachedRouteHandle`関数を使用します。 + +```ts +import {destroyDetachedRouteHandle} from '@angular/router'; + +// ... inside your strategy +if (this.handles.size > MAX_CACHE_SIZE) { + const handle = this.handles.get(oldestKey); + if (handle) { + destroyDetachedRouteHandle(handle); + this.handles.delete(oldestKey); + } +} +``` + NOTE: `canMatch`ガードが関与している場合、重複したエントリにつながる可能性があるため、キーとしてルートパスを使用することは避けてください。 +### (実験的) 未使用のルートインジェクターの自動クリーンアップ {#experimental-automatic-cleanup-of-unused-route-injectors} + +デフォルトでは、Angularは、`RouteReuseStrategy`によって保存されなくなった場合でも、デタッチされたルートのインジェクターを破棄しません。これは主に、このレベルのメモリ管理がほとんどのアプリケーションで一般的に必要とされていないためです。 + +未使用のルートインジェクターの自動クリーンアップを有効にするには、ルーター設定で`withExperimentalAutoCleanupInjectors`機能を使用できます。この機能は、ナビゲーション後に現在ストラテジーによって保存されているルートを確認し、現在再利用戦略によって保存されていないデタッチされたルートのインジェクターを破棄します。 + +```ts +import {provideRouter, withExperimentalAutoCleanupInjectors} from '@angular/router'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideRouter(routes, withExperimentalAutoCleanupInjectors()) + ] +}; +``` + +カスタム`RouteReuseStrategy`を提供しない場合、またはカスタム戦略が`BaseRouteReuseStrategy`を拡張している場合、ルートが非アクティブになったときにインジェクターが破棄されるようになります。 + +#### カスタム`RouteReuseStrategy`を使用したクリーンアップ {#cleanup-with-a-custom-routereusestrategy} + +アプリケーションがカスタム`RouteReuseStrategy`を使用しており、_かつ_そのストラテジーが`BaseRouteReuseStrategy`を拡張していない場合、どのルートがインジェクターを破棄すべきかをルーターに伝えるために`shouldDestroyInjector`を実装する必要があります: + +```ts +@Injectable() +export class CustomRouteReuseStrategy implements RouteReuseStrategy { + // ... other methods + + shouldDestroyInjector(route: Route): boolean { + return !route.data['retainInjector']; + } +} +``` + +ストラテジーが`DetachedRouteHandle`を保存する場合、そのデタッチされたハンドルが必要とするインジェクターをルーターが破棄しないように、これらについてもルーターに伝える必要があります: + +```ts +@Injectable() +export class CustomRouteReuseStrategy implements RouteReuseStrategy { + private readonly handles = new Map(); + + store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null) { + this.handles.set(route.routeConfig!, handle); + } + + retrieveStoredRouteHandles(): DetachedRouteHandle { + return Array.from(this.handles.values()); + } + + // ... other methods +} +``` + ### カスタムルート再利用戦略を使用するようにルートを設定する {#configuring-a-route-to-use-a-custom-route-reuse-strategy} ルートは、ルート設定メタデータを通じて再利用の動作をオプトインできます。このアプローチでは、再利用ロジックをコンポーネントのコードから分離し、コンポーネントを変更することなく動作を簡単に調整できるようにします: @@ -214,7 +299,7 @@ export const routes: Routes = [ { path: 'products', component: ProductListComponent, - data: { reuse: true } // Component state persists across navigations + data: {reuse: true}, // Component state persists across navigations }, { path: 'products/:id', @@ -224,8 +309,8 @@ export const routes: Routes = [ { path: 'search', component: SearchComponent, - data: { reuse: true } // Preserves search results and filter state - } + data: {reuse: true}, // Preserves search results and filter state + }, ]; ``` @@ -235,8 +320,8 @@ export const routes: Routes = [ export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes), - { provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy } - ] + {provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy}, + ], }; ``` @@ -256,17 +341,12 @@ Angularは、標準で2つのプリロード戦略を提供しています: `PreloadAllModules`戦略は次のように設定できます: ```ts -import { ApplicationConfig } from '@angular/core'; -import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router'; -import { routes } from './app.routes'; +import {ApplicationConfig} from '@angular/core'; +import {provideRouter, withPreloading, PreloadAllModules} from '@angular/router'; +import {routes} from './app.routes'; export const appConfig: ApplicationConfig = { - providers: [ - provideRouter( - routes, - withPreloading(PreloadAllModules) - ) - ] + providers: [provideRouter(routes, withPreloading(PreloadAllModules))], }; ``` @@ -277,10 +357,10 @@ export const appConfig: ApplicationConfig = { カスタムプリロード戦略は`PreloadingStrategy`インターフェースを実装します。これには単一の`preload`メソッドが必要です。このメソッドは、ルート設定と、実際のモジュール読み込みをトリガーする関数を受け取ります。この戦略は、プリロードが完了したときにemitするObservableか、プリロードをスキップするための空のObservableを返します: ```ts -import { Injectable } from '@angular/core'; -import { PreloadingStrategy, Route } from '@angular/router'; -import { Observable, of, timer } from 'rxjs'; -import { mergeMap } from 'rxjs/operators'; +import {Injectable} from '@angular/core'; +import {PreloadingStrategy, Route} from '@angular/router'; +import {Observable, of, timer} from 'rxjs'; +import {mergeMap} from 'rxjs/operators'; @Injectable() export class SelectivePreloadingStrategy implements PreloadingStrategy { @@ -297,24 +377,24 @@ export class SelectivePreloadingStrategy implements PreloadingStrategy { この選択的戦略は、ルートメタデータをチェックしてプリロードの動作を決定します。ルートは、その設定を通じてプリロードをオプトインできます: ```ts -import { Routes } from '@angular/router'; +import {Routes} from '@angular/router'; export const routes: Routes = [ { path: 'dashboard', loadChildren: () => import('./dashboard/dashboard.routes'), - data: { preload: true } // Preload immediately after initial navigation + data: {preload: true}, // Preload immediately after initial navigation }, { path: 'reports', loadChildren: () => import('./reports/reports.routes'), - data: { preload: false } // Only load when user navigates to reports + data: {preload: false}, // Only load when user navigates to reports }, { path: 'admin', - loadChildren: () => import('./admin/admin.routes') + loadChildren: () => import('./admin/admin.routes'), // No preload flag - won't be preloaded - } + }, ]; ``` @@ -337,15 +417,14 @@ URLハンドリングストラテジーは、AngularルーターがどのURLを カスタムURLハンドリングストラテジーは`UrlHandlingStrategy`クラスを拡張し、3つのメソッドを実装します。`shouldProcessUrl`メソッドはAngularが特定のURLを処理すべきかどうかを判断し、`extract`はAngularが処理すべきURLの部分を返し、`merge`はURLフラグメントをURLの残りの部分と結合します: ```ts -import { Injectable } from '@angular/core'; -import { UrlHandlingStrategy, UrlTree } from '@angular/router'; +import {Injectable} from '@angular/core'; +import {UrlHandlingStrategy, UrlTree} from '@angular/router'; @Injectable() export class CustomUrlHandlingStrategy implements UrlHandlingStrategy { shouldProcessUrl(url: UrlTree): boolean { // Only handle URLs that start with /app or /admin - return url.toString().startsWith('/app') || - url.toString().startsWith('/admin'); + return url.toString().startsWith('/app') || url.toString().startsWith('/admin'); } extract(url: UrlTree): UrlTree { @@ -367,15 +446,15 @@ export class CustomUrlHandlingStrategy implements UrlHandlingStrategy { Angularの依存性の注入システムを通じて、カスタムストラテジーを登録できます: ```ts -import { ApplicationConfig } from '@angular/core'; -import { provideRouter } from '@angular/router'; -import { UrlHandlingStrategy } from '@angular/router'; +import {ApplicationConfig} from '@angular/core'; +import {provideRouter} from '@angular/router'; +import {UrlHandlingStrategy} from '@angular/router'; export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes), - { provide: UrlHandlingStrategy, useClass: CustomUrlHandlingStrategy } - ] + {provide: UrlHandlingStrategy, useClass: CustomUrlHandlingStrategy}, + ], }; ``` @@ -392,20 +471,20 @@ export const appConfig: ApplicationConfig = { カスタムマッチャーは、URLセグメントを受け取り、消費されたセグメントとパラメーターを含むマッチ結果、またはマッチしなかったことを示すnullを返す関数です。このマッチャー関数は、Angularがルートのpathプロパティを評価する前に実行されます: ```ts -import { Route, UrlSegment, UrlSegmentGroup, UrlMatchResult } from '@angular/router'; +import {Route, UrlSegment, UrlSegmentGroup, UrlMatchResult} from '@angular/router'; export function customMatcher( segments: UrlSegment[], group: UrlSegmentGroup, - route: Route + route: Route, ): UrlMatchResult | null { // Matching logic here if (matchSuccessful) { return { consumed: segments, posParams: { - paramName: new UrlSegment('paramValue', {}) - } + paramName: new UrlSegment('paramValue', {}), + }, }; } return null; @@ -417,17 +496,17 @@ export function customMatcher( URL内のバージョン番号に基づいてルーティングする必要があるAPIドキュメントサイトを考えてみましょう。バージョンが異なれば、コンポーネントの構造や機能セットも異なる場合があります: ```ts -import { Routes, UrlSegment, UrlMatchResult } from '@angular/router'; +import {Routes, UrlSegment, UrlMatchResult} from '@angular/router'; export function versionMatcher(segments: UrlSegment[]): UrlMatchResult | null { // Match patterns like /v1/docs, /v2.1/docs, /v3.0.1/docs if (segments.length >= 2 && segments[0].path.match(/^v\d+(\.\d+)*$/)) { return { - consumed: segments.slice(0, 2), // Consume version and 'docs' + consumed: segments.slice(0, 2), // Consume version and 'docs' posParams: { - version: segments[0], // Make version available as a parameter - section: segments[1] // Make section available too - } + version: segments[0], // Make version available as a parameter + section: segments[1], // Make section available too + }, }; } return null; @@ -437,12 +516,12 @@ export function versionMatcher(segments: UrlSegment[]): UrlMatchResult | null { export const routes: Routes = [ { matcher: versionMatcher, - component: DocumentationComponent + component: DocumentationComponent, }, { path: 'latest/docs', - redirectTo: 'v3/docs' - } + redirectTo: 'v3/docs', + }, ]; ``` @@ -505,16 +584,16 @@ export function localeMatcher(segments: UrlSegment[]): UrlMatchResult | null { return { consumed: [segments[0]], posParams: { - locale: segments[0] - } + locale: segments[0], + }, }; } else { // No locale prefix, use default locale return { - consumed: [], // Don't consume any segments + consumed: [], // Don't consume any segments posParams: { - locale: new UrlSegment('en', {}) - } + locale: new UrlSegment('en', {}), + }, }; } } @@ -539,8 +618,8 @@ export function productMatcher(segments: UrlSegment[]): UrlMatchResult | null { consumed: [segments[0]], posParams: { productType: new UrlSegment('book', {}), - identifier: new UrlSegment(firstSegment.substring(5), {}) - } + identifier: new UrlSegment(firstSegment.substring(5), {}), + }, }; } @@ -550,8 +629,8 @@ export function productMatcher(segments: UrlSegment[]): UrlMatchResult | null { consumed: segments.slice(0, 2), posParams: { productType: new UrlSegment('electronics', {}), - identifier: segments[1] - } + identifier: segments[1], + }, }; } @@ -562,8 +641,8 @@ export function productMatcher(segments: UrlSegment[]): UrlMatchResult | null { posParams: { productType: new UrlSegment('clothing', {}), brand: segments[1], - identifier: segments[2] - } + identifier: segments[2], + }, }; } From 3af859097b47d9ac8880fe2b56904622a77059e3 Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 11:35:48 +0900 Subject: [PATCH 15/32] docs(guide/routing): migrate data-resolvers .en.md changes --- .../guide/routing/data-resolvers.en.md | 89 ++++++++++--------- .../content/guide/routing/data-resolvers.md | 85 +++++++++--------- 2 files changed, 92 insertions(+), 82 deletions(-) diff --git a/adev-ja/src/content/guide/routing/data-resolvers.en.md b/adev-ja/src/content/guide/routing/data-resolvers.en.md index da83b8b1b..11eb4d8a9 100644 --- a/adev-ja/src/content/guide/routing/data-resolvers.en.md +++ b/adev-ja/src/content/guide/routing/data-resolvers.en.md @@ -24,18 +24,24 @@ It receives the `ActivatedRouteSnapshot` and `RouterStateSnapshot` as parameters Here is a resolver that gets the user information before rendering a route using the [`inject`](api/core/inject) function: ```ts -import { inject } from '@angular/core'; -import { UserStore, SettingsStore } from './user-store'; -import type { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot } from '@angular/router'; -import type { User, Settings } from './types'; - -export const userResolver: ResolveFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { +import {inject} from '@angular/core'; +import {UserStore, SettingsStore} from './user-store'; +import type {ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot} from '@angular/router'; +import type {User, Settings} from './types'; + +export const userResolver: ResolveFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +) => { const userStore = inject(UserStore); const userId = route.paramMap.get('id')!; return userStore.getUser(userId); }; -export const settingsResolver: ResolveFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { +export const settingsResolver: ResolveFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +) => { const settingsStore = inject(SettingsStore); const userId = route.paramMap.get('id')!; return settingsStore.getUserSettings(userId); @@ -47,7 +53,7 @@ export const settingsResolver: ResolveFn = (route: ActivatedRouteSnaps When you want to add one or more data resolvers to a route, you can add it under the `resolve` key in the route configuration. The `Routes` type defines the structure for route configurations: ```ts -import { Routes } from '@angular/router'; +import {Routes} from '@angular/router'; export const routes: Routes = [ { @@ -55,9 +61,9 @@ export const routes: Routes = [ component: UserDetail, resolve: { user: userResolver, - settings: settingsResolver - } - } + settings: settingsResolver, + }, + }, ]; ``` @@ -95,14 +101,12 @@ export class UserDetail { A different approach to accessing the resolved data is to use `withComponentInputBinding()` when configuring your router with `provideRouter`. This allows resolved data to be passed directly as component inputs: ```ts -import { bootstrapApplication } from '@angular/platform-browser'; -import { provideRouter, withComponentInputBinding } from '@angular/router'; -import { routes } from './app.routes'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {provideRouter, withComponentInputBinding} from '@angular/router'; +import {routes} from './app.routes'; bootstrapApplication(App, { - providers: [ - provideRouter(routes, withComponentInputBinding()) - ] + providers: [provideRouter(routes, withComponentInputBinding())], }); ``` @@ -142,24 +146,27 @@ There are three primary ways to handle errors with data resolvers: The [`withNavigationErrorHandler`](api/router/withNavigationErrorHandler) feature provides a centralized way to handle all navigation errors, including those from failed data resolvers. This approach keeps error handling logic in one place and prevents duplicate error handling code across resolvers. ```ts -import { bootstrapApplication } from '@angular/platform-browser'; -import { provideRouter, withNavigationErrorHandler } from '@angular/router'; -import { inject } from '@angular/core'; -import { Router } from '@angular/router'; -import { routes } from './app.routes'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {provideRouter, withNavigationErrorHandler} from '@angular/router'; +import {inject} from '@angular/core'; +import {Router} from '@angular/router'; +import {routes} from './app.routes'; bootstrapApplication(App, { providers: [ - provideRouter(routes, withNavigationErrorHandler((error) => { - const router = inject(Router); + provideRouter( + routes, + withNavigationErrorHandler((error) => { + const router = inject(Router); - if (error?.message) { - console.error('Navigation error occurred:', error.message) - } + if (error?.message) { + console.error('Navigation error occurred:', error.message); + } - router.navigate(['/error']); - })) - ] + router.navigate(['/error']); + }), + ), + ], }); ``` @@ -205,11 +212,11 @@ export class App { map(event => { if (event instanceof NavigationError) { this.lastFailedUrl.set(event.url); - + if (event.error) { console.error('Navigation error', event.error) } - + return 'Navigation failed. Please try again.'; } return ''; @@ -239,11 +246,11 @@ This approach is particularly useful when you need to: Here's an updated example of the `userResolver` that logs the error and navigates back to the generic `/users` page using the `Router` service: ```ts -import { inject } from '@angular/core'; -import { ResolveFn, RedirectCommand, Router } from '@angular/router'; -import { catchError, of, EMPTY } from 'rxjs'; -import { UserStore } from './user-store'; -import type { User } from './types'; +import {inject} from '@angular/core'; +import {ResolveFn, RedirectCommand, Router} from '@angular/router'; +import {catchError, of, EMPTY} from 'rxjs'; +import {UserStore} from './user-store'; +import type {User} from './types'; export const userResolver: ResolveFn = (route) => { const userStore = inject(UserStore); @@ -251,10 +258,10 @@ export const userResolver: ResolveFn = (route) => { const userId = route.paramMap.get('id')!; return userStore.getUser(userId).pipe( - catchError(error => { + catchError((error) => { console.error('Failed to load user:', error); return of(new RedirectCommand(router.parseUrl('/users'))); - }) + }), ); }; ``` @@ -270,8 +277,6 @@ To improve user experience during resolver execution, you can listen to router e ```angular-ts import { Component, inject } from '@angular/core'; import { Router } from '@angular/router'; -import { toSignal } from '@angular/core/rxjs-interop'; -import { map } from 'rxjs'; @Component({ selector: 'app-root', @@ -323,7 +328,7 @@ provideRouter([ resolve: { posts: (route: ActivatedRouteSnapshot) => { const postService = inject(PostService); - const user = route.data['user'] as User; // parent data + const user = route.parent?.data['user'] as User; // parent data const userId = user.id; return postService.getPostByUser(userId); }, diff --git a/adev-ja/src/content/guide/routing/data-resolvers.md b/adev-ja/src/content/guide/routing/data-resolvers.md index c8bbddcad..2ef979143 100644 --- a/adev-ja/src/content/guide/routing/data-resolvers.md +++ b/adev-ja/src/content/guide/routing/data-resolvers.md @@ -24,18 +24,24 @@ 以下は、[`inject`](api/core/inject)関数を使用してルートをレンダリングする前にユーザー情報を取得するリゾルバです。 ```ts -import { inject } from '@angular/core'; -import { UserStore, SettingsStore } from './user-store'; -import type { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot } from '@angular/router'; -import type { User, Settings } from './types'; - -export const userResolver: ResolveFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { +import {inject} from '@angular/core'; +import {UserStore, SettingsStore} from './user-store'; +import type {ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot} from '@angular/router'; +import type {User, Settings} from './types'; + +export const userResolver: ResolveFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +) => { const userStore = inject(UserStore); const userId = route.paramMap.get('id')!; return userStore.getUser(userId); }; -export const settingsResolver: ResolveFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { +export const settingsResolver: ResolveFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +) => { const settingsStore = inject(SettingsStore); const userId = route.paramMap.get('id')!; return settingsStore.getUserSettings(userId); @@ -47,7 +53,7 @@ export const settingsResolver: ResolveFn = (route: ActivatedRouteSnaps ルートに1つ以上のデータリゾルバーを追加したい場合は、ルート設定の`resolve`キーの下に追加できます。`Routes`型は、ルート設定の構造を定義します。 ```ts -import { Routes } from '@angular/router'; +import {Routes} from '@angular/router'; export const routes: Routes = [ { @@ -55,9 +61,9 @@ export const routes: Routes = [ component: UserDetail, resolve: { user: userResolver, - settings: settingsResolver - } - } + settings: settingsResolver, + }, + }, ]; ``` @@ -95,14 +101,12 @@ export class UserDetail { 解決済みデータにアクセスする別のアプローチは、`provideRouter`でルーターを設定する際に`withComponentInputBinding()`を使用することです。これにより、解決済みデータをコンポーネントの入力として直接渡すことができます。 ```ts -import { bootstrapApplication } from '@angular/platform-browser'; -import { provideRouter, withComponentInputBinding } from '@angular/router'; -import { routes } from './app.routes'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {provideRouter, withComponentInputBinding} from '@angular/router'; +import {routes} from './app.routes'; bootstrapApplication(App, { - providers: [ - provideRouter(routes, withComponentInputBinding()) - ] + providers: [provideRouter(routes, withComponentInputBinding())], }); ``` @@ -142,24 +146,27 @@ export class UserDetail { `withNavigationErrorHandler`機能は、失敗したデータリゾルバーからのエラーを含む、すべてのナビゲーションエラーを処理する一元的な方法を提供します。このアプローチにより、エラー処理ロジックが一箇所に保持され、リゾルバー間での重複したエラー処理コードが防止されます。 ```ts -import { bootstrapApplication } from '@angular/platform-browser'; -import { provideRouter, withNavigationErrorHandler } from '@angular/router'; -import { inject } from '@angular/core'; -import { Router } from '@angular/router'; -import { routes } from './app.routes'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {provideRouter, withNavigationErrorHandler} from '@angular/router'; +import {inject} from '@angular/core'; +import {Router} from '@angular/router'; +import {routes} from './app.routes'; bootstrapApplication(App, { providers: [ - provideRouter(routes, withNavigationErrorHandler((error) => { - const router = inject(Router); + provideRouter( + routes, + withNavigationErrorHandler((error) => { + const router = inject(Router); - if (error?.message) { - console.error('Navigation error occurred:', error.message) - } + if (error?.message) { + console.error('Navigation error occurred:', error.message); + } - router.navigate(['/error']); - })) - ] + router.navigate(['/error']); + }), + ), + ], }); ``` @@ -239,11 +246,11 @@ export class App { 以下は、エラーをログに記録し、`Router`サービスを使用して汎用の`/users`ページにナビゲートし直す`userResolver`の更新された例です。 ```ts -import { inject } from '@angular/core'; -import { ResolveFn, RedirectCommand, Router } from '@angular/router'; -import { catchError, of, EMPTY } from 'rxjs'; -import { UserStore } from './user-store'; -import type { User } from './types'; +import {inject} from '@angular/core'; +import {ResolveFn, RedirectCommand, Router} from '@angular/router'; +import {catchError, of, EMPTY} from 'rxjs'; +import {UserStore} from './user-store'; +import type {User} from './types'; export const userResolver: ResolveFn = (route) => { const userStore = inject(UserStore); @@ -251,10 +258,10 @@ export const userResolver: ResolveFn = (route) => { const userId = route.paramMap.get('id')!; return userStore.getUser(userId).pipe( - catchError(error => { + catchError((error) => { console.error('Failed to load user:', error); return of(new RedirectCommand(router.parseUrl('/users'))); - }) + }), ); }; ``` @@ -270,8 +277,6 @@ export const userResolver: ResolveFn = (route) => { ```angular-ts import { Component, inject } from '@angular/core'; import { Router } from '@angular/router'; -import { toSignal } from '@angular/core/rxjs-interop'; -import { map } from 'rxjs'; @Component({ selector: 'app-root', @@ -323,7 +328,7 @@ provideRouter([ resolve: { posts: (route: ActivatedRouteSnapshot) => { const postService = inject(PostService); - const user = route.data['user'] as User; // parent data + const user = route.parent?.data['user'] as User; // parent data const userId = user.id; return postService.getPostByUser(userId); }, From aad2f30f450ff8e5b4e5455e0b504421a003bd15 Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 11:39:11 +0900 Subject: [PATCH 16/32] docs(guide/routing): migrate define-routes .en.md changes --- .../content/guide/routing/define-routes.en.md | 187 +++++++++--------- .../content/guide/routing/define-routes.md | 185 +++++++++-------- 2 files changed, 177 insertions(+), 195 deletions(-) diff --git a/adev-ja/src/content/guide/routing/define-routes.en.md b/adev-ja/src/content/guide/routing/define-routes.en.md index 1570aeb1f..420e73976 100644 --- a/adev-ja/src/content/guide/routing/define-routes.en.md +++ b/adev-ja/src/content/guide/routing/define-routes.en.md @@ -9,12 +9,12 @@ In Angular, a **route** is an object that defines which component should render Here is a basic example of a route: ```ts -import { AdminPage } from './app-admin/app-admin.component'; +import {AdminPage} from './app-admin/app-admin.component'; const adminPage = { path: 'admin', - component: AdminPage -} + component: AdminPage, +}; ``` For this route, when a user visits the `/admin` path, the app will display the `AdminPage` component. @@ -26,9 +26,9 @@ Most projects define routes in a separate file that contains `routes` in the fil A collection of routes looks like this: ```ts -import { Routes } from '@angular/router'; -import { HomePage } from './home-page/home-page.component'; -import { AdminPage } from './about-page/admin-page.component'; +import {Routes} from '@angular/router'; +import {HomePage} from './home-page/home-page.component'; +import {AdminPage} from './about-page/admin-page.component'; export const routes: Routes = [ { @@ -51,16 +51,16 @@ When bootstrapping an Angular application without the Angular CLI, you can pass Inside of the `providers` array, you can add the Angular router to your application by adding a `provideRouter` function call with your routes. ```ts -import { ApplicationConfig } from '@angular/core'; -import { provideRouter } from '@angular/router'; +import {ApplicationConfig} from '@angular/core'; +import {provideRouter} from '@angular/router'; -import { routes } from './app.routes'; +import {routes} from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes), // ... - ] + ], }; ``` @@ -88,12 +88,10 @@ Learn more about [query parameters in Angular in this guide](/guide/routing/read The following example displays a user profile component based on the user id passed in through the URL. ```ts -import { Routes } from '@angular/router'; -import { UserProfile } from './user-profile/user-profile'; +import {Routes} from '@angular/router'; +import {UserProfile} from './user-profile/user-profile'; -const routes: Routes = [ - { path: 'user/:id', component: UserProfile } -]; +const routes: Routes = [{path: 'user/:id', component: UserProfile}]; ``` In this example, URLs such as `/user/leeroy` and `/user/jenkins` render the `UserProfile` component. This component can then read the `id` parameter and use it to perform additional work, such as fetching data. See [reading route state guide](/guide/routing/read-route-state) for details on reading route parameters. @@ -108,13 +106,13 @@ Valid route parameter names must start with a letter (a-z, A-Z) and can only con You can also define paths with multiple parameters: ```ts -import { Routes } from '@angular/router'; -import { UserProfile } from './user-profile/user-profile.component'; -import { SocialMediaFeed } from './user-profile/social–media-feed.component'; +import {Routes} from '@angular/router'; +import {UserProfile} from './user-profile/user-profile.component'; +import {SocialMediaFeed} from './user-profile/social–media-feed.component'; const routes: Routes = [ - { path: 'user/:id/:social-media', component: SocialMediaFeed }, - { path: 'user/:id/', component: UserProfile }, + {path: 'user/:id/:social-media', component: SocialMediaFeed}, + {path: 'user/:id/', component: UserProfile}, ]; ``` @@ -129,14 +127,14 @@ When you need to catch all routes for a specific path, the solution is a wildcar A common example is defining a Page Not Found component. ```ts -import { Home } from './home/home.component'; -import { UserProfile } from './user-profile/user-profile.component'; -import { NotFound } from './not-found/not-found.component'; +import {Home} from './home/home.component'; +import {UserProfile} from './user-profile/user-profile.component'; +import {NotFound} from './not-found/not-found.component'; const routes: Routes = [ - { path: 'home', component: Home }, - { path: 'user/:id', component: UserProfile }, - { path: '**', component: NotFound } + {path: 'home', component: Home}, + {path: 'user/:id', component: UserProfile}, + {path: '**', component: NotFound}, ]; ``` @@ -152,11 +150,11 @@ The following example shows routes defined from most-specific to least specific: ```ts const routes: Routes = [ - { path: '', component: HomeComponent }, // Empty path - { path: 'users/new', component: NewUserComponent }, // Static, most specific - { path: 'users/:id', component: UserDetailComponent }, // Dynamic - { path: 'users', component: UsersComponent }, // Static, less specific - { path: '**', component: NotFoundComponent } // Wildcard - always last + {path: '', component: HomeComponent}, // Empty path + {path: 'users/new', component: NewUserComponent}, // Static, most specific + {path: 'users/:id', component: UserDetailComponent}, // Dynamic + {path: 'users', component: UsersComponent}, // Static, less specific + {path: '**', component: NotFoundComponent}, // Wildcard - always last ]; ``` @@ -168,12 +166,12 @@ If a user visits `/users/new`, Angular router would go through the following ste 1. Never reaches `users` 1. Never reaches `**` -## Loading Route Component Strategies +## Route Loading Strategies -Understanding how and when components load in Angular routing is crucial for building responsive web applications. Angular offers two primary strategies to control component loading behavior: +Understanding how and when routes and components load in Angular routing is crucial for building responsive web applications. Angular offers two primary strategies to control loading behavior: -1. **Eagerly loaded**: Components that are loaded immediately -2. **Lazily loaded**: Components loaded only when needed +1. **Eagerly loaded**: Routes and components that are loaded immediately +2. **Lazily loaded**: Routes and components loaded only when needed Each approach offers distinct advantages for different scenarios. @@ -182,50 +180,51 @@ Each approach offers distinct advantages for different scenarios. When you define a route with the `component` property, the referenced component is eagerly loaded as part of the same JavaScript bundle as the route configuration. ```ts -import { Routes } from "@angular/router"; -import { HomePage } from "./components/home/home-page" -import { LoginPage } from "./components/auth/login-page" +import {Routes} from '@angular/router'; +import {HomePage} from './components/home/home-page'; +import {LoginPage} from './components/auth/login-page'; export const routes: Routes = [ // HomePage and LoginPage are both directly referenced in this config, // so their code is eagerly included in the same JavaScript bundle as this file. { - path: "", - component: HomePage + path: '', + component: HomePage, }, { - path: "login", - component: LoginPage - } -] + path: 'login', + component: LoginPage, + }, +]; ``` Eagerly loading route components like this means that the browser has to download and parse all of the JavaScript for these components as part of your initial page load, but the components are available to Angular immediately. While including more JavaScript in your initial page load leads to slower initial load times, this can lead to more seamless transitions as the user navigates through an application. -### Lazily loaded components +### Lazily loaded components and routes -You can use the `loadComponent` property to lazily load the JavaScript for a route only at the point at which that route would become active. +You can use the `loadComponent` property to lazily load the JavaScript for a component at the point at which that route would become active. The `loadChildren` property lazily loads child routes during route matching. ```ts -import { Routes } from "@angular/router"; +import {Routes} from '@angular/router'; export const routes: Routes = [ - // The HomePage and LoginPage components are loaded lazily at the point at which - // their corresponding routes become active. { path: 'login', - loadComponent: () => import('./components/auth/login-page').then(m => m.LoginPage) + loadComponent: () => import('./components/auth/login-page'), }, { - path: '', - loadComponent: () => import('./components/home/home-page').then(m => m.HomePage) - } -] + path: 'admin', + loadComponent: () => import('./admin/admin.component'), + loadChildren: () => import('./admin/admin.routes'), + }, +]; ``` -The `loadComponent` property accepts a loader function that returns a Promise that resolves to an Angular component. In most cases, this function uses the standard [JavaScript dynamic import API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import). You can, however, use any arbitrary async loader function. +The `loadComponent` and `loadChildren` properties accept a loader function that returns a Promise that resolves to an Angular component or a set of routes respectively. In most cases, this function uses the standard [JavaScript dynamic import API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import). You can, however, use any arbitrary async loader function. + +If the lazily loaded file uses a `default` export, you can return the `import()` promise directly without an additional `.then` call to select the exported class. Lazily loading routes can significantly improve the load speed of your Angular application by removing large portions of JavaScript from the initial bundle. These portions of your code compile into separate JavaScript "chunks" that the router requests only when the user visits the corresponding route. @@ -234,9 +233,9 @@ Lazily loading routes can significantly improve the load speed of your Angular a The Router executes `loadComponent` and `loadChildren` within the **injection context of the current route**, allowing you to call [`inject`](/api/core/inject)inside these loader functions to access providers declared on that route, inherited from parent routes through hierarchical dependency injection, or available globally. This enables context-aware lazy loading. ```ts -import { Routes } from '@angular/router'; -import { inject } from '@angular/core'; -import { FeatureFlags } from './feature-flags'; +import {Routes} from '@angular/router'; +import {inject} from '@angular/core'; +import {FeatureFlags} from './feature-flags'; export const routes: Routes = [ { @@ -245,8 +244,8 @@ export const routes: Routes = [ loadComponent: () => { const flags = inject(FeatureFlags); return flags.isPremium - ? import('./dashboard/premium-dashboard').then(m => m.PremiumDashboard) - : import('./dashboard/basic-dashboard').then(m => m.BasicDashboard); + ? import('./dashboard/premium-dashboard') + : import('./dashboard/basic-dashboard'); }, }, ]; @@ -265,7 +264,7 @@ NOTE: While lazy routes have the upfront performance benefit of reducing the amo You can define a route that redirects to another route instead of rendering a component: ```ts -import { BlogComponent } from './home/blog.component'; +import {BlogComponent} from './home/blog.component'; const routes: Routes = [ { @@ -274,7 +273,7 @@ const routes: Routes = [ }, { path: 'blog', - component: BlogComponent + component: BlogComponent, }, ]; ``` @@ -286,21 +285,21 @@ If you modify or remove a route, some users may still click on out-of-date links You can associate a **title** with each route. Angular automatically updates the [page title](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title) when a route activates. Always define appropriate page titles for your application, as these titles are necessary to create an accessible experience. ```ts -import { Routes } from '@angular/router'; -import { HomeComponent } from './home/home.component'; -import { AboutComponent } from './about/about.component'; -import { ProductsComponent } from './products/products.component'; +import {Routes} from '@angular/router'; +import {HomeComponent} from './home/home.component'; +import {AboutComponent} from './about/about.component'; +import {ProductsComponent} from './products/products.component'; const routes: Routes = [ { path: '', component: HomeComponent, - title: 'Home Page' + title: 'Home Page', }, { path: 'about', component: AboutComponent, - title: 'About Us' + title: 'About Us', }, ]; ``` @@ -310,14 +309,12 @@ The page `title` property can be set dynamincally to a resolver function using [ ```ts const titleResolver: ResolveFn = (route) => route.queryParams['id']; const routes: Routes = [ - ... - { + ...{ path: 'products', component: ProductsComponent, title: titleResolver, - } + }, ]; - ``` Route titles can also be set via a service extending the [`TitleStrategy`](/api/router/TitleStrategy) abstract class. By default, Angular uses the [`DefaultTitleStrategy`](/api/router/DefaultTitleStrategy). @@ -329,9 +326,9 @@ For advanced scenarios where you need centralized control over how the document `TitleStrategy` is a token you can provide to override the default title strategy used by Angular. You can supply a custom `TitleStrategy` to implement conventions such as adding an application suffix, formatting titles from breadcrumbs, or generating titles dynamically from route data. ```ts -import { Injectable } from '@angular/core'; -import { Title } from '@angular/platform-browser'; -import { TitleStrategy, RouterStateSnapshot } from '@angular/router'; +import {inject, Injectable} from '@angular/core'; +import {Title} from '@angular/platform-browser'; +import {TitleStrategy, RouterStateSnapshot} from '@angular/router'; @Injectable() export class AppTitleStrategy extends TitleStrategy { @@ -349,14 +346,11 @@ export class AppTitleStrategy extends TitleStrategy { To use the custom strategy, provide it with the `TitleStrategy` token at the application level: ```ts -import { provideRouter, TitleStrategy } from '@angular/router'; -import { AppTitleStrategy } from './app-title.strategy'; +import {provideRouter, TitleStrategy} from '@angular/router'; +import {AppTitleStrategy} from './app-title.strategy'; export const appConfig = { - providers: [ - provideRouter(routes), - { provide: TitleStrategy, useClass: AppTitleStrategy }, - ], + providers: [provideRouter(routes), {provide: TitleStrategy, useClass: AppTitleStrategy}], }; ``` @@ -370,10 +364,7 @@ Common scenarios where this can be helpful include applications that have differ export const ROUTES: Route[] = [ { path: 'admin', - providers: [ - AdminService, - {provide: ADMIN_API_KEY, useValue: '12345'}, - ], + providers: [AdminService, {provide: ADMIN_API_KEY, useValue: '12345'}], children: [ {path: 'users', component: AdminUsersComponent}, {path: 'teams', component: AdminTeamsComponent}, @@ -399,22 +390,22 @@ There are two ways to work with route data: static data that remains constant, a You can associate arbitrary static data with a route via the `data` property in order to centralize things like route-specific metadata (e.g., analytics tracking, permissions, etc.): ```ts -import { Routes } from '@angular/router'; -import { HomeComponent } from './home/home.component'; -import { AboutComponent } from './about/about.component'; -import { ProductsComponent } from './products/products.component'; +import {Routes} from '@angular/router'; +import {HomeComponent} from './home/home.component'; +import {AboutComponent} from './about/about.component'; +import {ProductsComponent} from './products/products.component'; const routes: Routes = [ { path: 'about', component: AboutComponent, - data: { analyticsId: '456' } + data: {analyticsId: '456'}, }, { path: '', component: HomeComponent, - data: { analyticsId: '123' } - } + data: {analyticsId: '123'}, + }, ]; ``` @@ -442,15 +433,15 @@ const routes: Routes = [ children: [ { path: 'info', - component: ProductInfoComponent + component: ProductInfoComponent, }, { path: 'reviews', - component: ProductReviewsComponent - } - ] - } -] + component: ProductReviewsComponent, + }, + ], + }, +]; ``` The above example defines a route for a product page that allows a user to change whether the product info or reviews are displayed based on the url. diff --git a/adev-ja/src/content/guide/routing/define-routes.md b/adev-ja/src/content/guide/routing/define-routes.md index 205820e64..03b8f1201 100644 --- a/adev-ja/src/content/guide/routing/define-routes.md +++ b/adev-ja/src/content/guide/routing/define-routes.md @@ -9,12 +9,12 @@ Angularでは、**ルート**は特定のURLパスまたはパターンに対し 次にルートの基本的な例を示します。 ```ts -import { AdminPage } from './app-admin/app-admin.component'; +import {AdminPage} from './app-admin/app-admin.component'; const adminPage = { path: 'admin', - component: AdminPage -} + component: AdminPage, +}; ``` このルートの場合、ユーザーが`/admin`パスにアクセスすると、アプリケーションは`AdminPage`コンポーネントを表示します。 @@ -26,9 +26,9 @@ const adminPage = { ルートのコレクションは次のようになります。 ```ts -import { Routes } from '@angular/router'; -import { HomePage } from './home-page/home-page.component'; -import { AdminPage } from './about-page/admin-page.component'; +import {Routes} from '@angular/router'; +import {HomePage} from './home-page/home-page.component'; +import {AdminPage} from './about-page/admin-page.component'; export const routes: Routes = [ { @@ -51,16 +51,16 @@ Angular CLIなしでAngularアプリケーションをブートストラップ `providers`配列内で、`provideRouter`関数呼び出しとルートを追加することで、Angularルーターをアプリケーションに追加できます。 ```ts -import { ApplicationConfig } from '@angular/core'; -import { provideRouter } from '@angular/router'; +import {ApplicationConfig} from '@angular/core'; +import {provideRouter} from '@angular/router'; -import { routes } from './app.routes'; +import {routes} from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes), // ... - ] + ], }; ``` @@ -88,12 +88,10 @@ IMPORTANT: パラメーターは、URLの[クエリ文字列](https://en.wikiped 次の例は、URLを介して渡されたユーザーIDに基づいてユーザープロファイルコンポーネントを表示します。 ```ts -import { Routes } from '@angular/router'; -import { UserProfile } from './user-profile/user-profile'; +import {Routes} from '@angular/router'; +import {UserProfile} from './user-profile/user-profile'; -const routes: Routes = [ - { path: 'user/:id', component: UserProfile } -]; +const routes: Routes = [{path: 'user/:id', component: UserProfile}]; ``` この例では、`/user/leeroy`や`/user/jenkins`などのURLは`UserProfile`コンポーネントをレンダリングします。このコンポーネントは、`id`パラメーターを読み取り、それを使用してデータの取得などの追加作業を実行できます。[ルートパラメーターの読み取りの詳細については、ルート状態の読み取りガイドを参照してください](/guide/routing/read-route-state)。 @@ -108,13 +106,13 @@ const routes: Routes = [ 複数のパラメーターを持つパスを定義できます。 ```ts -import { Routes } from '@angular/router'; -import { UserProfile } from './user-profile/user-profile.component'; -import { SocialMediaFeed } from './user-profile/social–media-feed.component'; +import {Routes} from '@angular/router'; +import {UserProfile} from './user-profile/user-profile.component'; +import {SocialMediaFeed} from './user-profile/social–media-feed.component'; const routes: Routes = [ - { path: 'user/:id/:social-media', component: SocialMediaFeed }, - { path: 'user/:id/', component: UserProfile }, + {path: 'user/:id/:social-media', component: SocialMediaFeed}, + {path: 'user/:id/', component: UserProfile}, ]; ``` @@ -129,14 +127,14 @@ const routes: Routes = [ 一般的な例は、ページが見つかりませんコンポーネントの定義です。 ```ts -import { Home } from './home/home.component'; -import { UserProfile } from './user-profile/user-profile.component'; -import { NotFound } from './not-found/not-found.component'; +import {Home} from './home/home.component'; +import {UserProfile} from './user-profile/user-profile.component'; +import {NotFound} from './not-found/not-found.component'; const routes: Routes = [ - { path: 'home', component: Home }, - { path: 'user/:id', component: UserProfile }, - { path: '**', component: NotFound } + {path: 'home', component: Home}, + {path: 'user/:id', component: UserProfile}, + {path: '**', component: NotFound}, ]; ``` @@ -152,11 +150,11 @@ Tip: ワイルドカードルートは通常、ルート配列の最後に配置 ```ts const routes: Routes = [ - { path: '', component: HomeComponent }, // 空のパス - { path: 'users/new', component: NewUserComponent }, // 静的、最も具体的 - { path: 'users/:id', component: UserDetailComponent }, // 動的 - { path: 'users', component: UsersComponent }, // 静的、具体的でない - { path: '**', component: NotFoundComponent } // ワイルドカード - 常に最後 + {path: '', component: HomeComponent}, // 空のパス + {path: 'users/new', component: NewUserComponent}, // 静的、最も具体的 + {path: 'users/:id', component: UserDetailComponent}, // 動的 + {path: 'users', component: UsersComponent}, // 静的、具体的でない + {path: '**', component: NotFoundComponent}, // ワイルドカード - 常に最後 ]; ``` @@ -168,12 +166,12 @@ const routes: Routes = [ 1. `users`には到達しません 1. `**`には到達しません -## ルートコンポーネントの読み込み戦略 {#loading-route-component-strategies} +## ルートの読み込み戦略 {#route-loading-strategies} -Angularルーティングでコンポーネントがどのように、いつ読み込まれるかを理解することは、応答性の高いWebアプリケーションを構築するために不可欠です。Angularは、コンポーネントの読み込み動作を制御するための2つの主要な戦略を提供します。 +Angularルーティングでルートとコンポーネントがどのように、いつ読み込まれるかを理解することは、応答性の高いWebアプリケーションを構築するために不可欠です。Angularは、読み込み動作を制御するための2つの主要な戦略を提供します。 -1. **即時読み込み (Eagerly loaded)**: すぐに読み込まれるコンポーネント -2. **遅延読み込み (Lazily loaded)**: 必要になったときにのみ読み込まれるコンポーネント +1. **即時読み込み (Eagerly loaded)**: すぐに読み込まれるルートとコンポーネント +2. **遅延読み込み (Lazily loaded)**: 必要になったときにのみ読み込まれるルートとコンポーネント それぞれのアプローチは、異なるシナリオに対して明確な利点を提供します。 @@ -182,50 +180,51 @@ Angularルーティングでコンポーネントがどのように、いつ読 `component`プロパティでルートを定義すると、参照されるコンポーネントは、ルート構成と同じJavaScriptバンドルの一部として即時読み込みされます。 ```ts -import { Routes } from "@angular/router"; -import { HomePage } from "./components/home/home-page" -import { LoginPage } from "./components/auth/login-page" +import {Routes} from '@angular/router'; +import {HomePage} from './components/home/home-page'; +import {LoginPage} from './components/auth/login-page'; export const routes: Routes = [ // HomePageとLoginPageはどちらもこの設定で直接参照されているため、 // そのコードはこのファイルと同じJavaScriptバンドルに即時含まれます。 { - path: "", - component: HomePage + path: '', + component: HomePage, }, { - path: "login", - component: LoginPage - } -] + path: 'login', + component: LoginPage, + }, +]; ``` このようにルートコンポーネントを即時読み込みすることは、ブラウザが初期ページ読み込みの一部としてこれらのコンポーネントのすべてのJavaScriptをダウンロードして解析する必要があることを意味しますが、コンポーネントはAngularですぐに利用できます。 初期ページ読み込みに多くのJavaScriptを含めると初期読み込み時間は遅くなりますが、ユーザーがアプリケーション内を移動する際のよりシームレスな遷移につながる可能性があります。 -### 遅延読み込みされるコンポーネント {#lazily-loaded-components} +### 遅延読み込みされるコンポーネントとルート {#lazily-loaded-components-and-routes} -`loadComponent`プロパティを使用すると、そのルートがアクティブになる時点でのみ、ルートのJavaScriptを遅延読み込みできます。 +`loadComponent`プロパティを使用すると、そのルートがアクティブになる時点でのみ、コンポーネントのJavaScriptを遅延読み込みできます。`loadChildren`プロパティは、ルートマッチング時に子ルートを遅延読み込みします。 ```ts -import { Routes } from "@angular/router"; +import {Routes} from '@angular/router'; export const routes: Routes = [ - // HomePageとLoginPageコンポーネントは、 - // 対応するルートがアクティブになった時点で遅延読み込みされます。 { path: 'login', - loadComponent: () => import('./components/auth/login-page').then(m => m.LoginPage) + loadComponent: () => import('./components/auth/login-page'), }, { - path: '', - loadComponent: () => import('./components/home/home-page').then(m => m.HomePage) - } -] + path: 'admin', + loadComponent: () => import('./admin/admin.component'), + loadChildren: () => import('./admin/admin.routes'), + }, +]; ``` -`loadComponent`プロパティは、Angularコンポーネントに解決されるPromiseを返すローダー関数を受け入れます。ほとんどの場合、この関数は標準の[JavaScript動的インポートAPI](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import)を使用します。ただし、任意の非同期ローダー関数も使用できます。 +`loadComponent`と`loadChildren`プロパティは、それぞれAngularコンポーネントまたはルートのセットに解決されるPromiseを返すローダー関数を受け入れます。ほとんどの場合、この関数は標準の[JavaScript動的インポートAPI](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import)を使用します。ただし、任意の非同期ローダー関数も使用できます。 + +遅延読み込みされるファイルが`default`エクスポートを使用している場合、エクスポートされたクラスを選択するための追加の`.then`呼び出しなしで、`import()`プロミスを直接返すことができます。 遅延読み込みルートは、初期バンドルからJavaScriptの大部分を削除することで、Angularアプリケーションの読み込み速度を大幅に向上させることができます。これらのコード部分は、ユーザーが対応するルートにアクセスしたときにのみルーターが要求する個別のJavaScript「チャンク」にコンパイルされます。 @@ -234,9 +233,9 @@ export const routes: Routes = [ ルーターは`loadComponent`と`loadChildren`を**現在のルートの注入コンテキスト**内で実行します。これにより、これらのローダー関数内で[`inject`](/api/core/inject)を呼び出して、そのルートで宣言されたプロバイダー、階層的な依存性の注入を介して親ルートから継承されたプロバイダー、またはグローバルに利用可能なプロバイダーにアクセスできます。これにより、コンテキストを認識した遅延読み込みが可能になります。 ```ts -import { Routes } from '@angular/router'; -import { inject } from '@angular/core'; -import { FeatureFlags } from './feature-flags'; +import {Routes} from '@angular/router'; +import {inject} from '@angular/core'; +import {FeatureFlags} from './feature-flags'; export const routes: Routes = [ { @@ -245,8 +244,8 @@ export const routes: Routes = [ loadComponent: () => { const flags = inject(FeatureFlags); return flags.isPremium - ? import('./dashboard/premium-dashboard').then(m => m.PremiumDashboard) - : import('./dashboard/basic-dashboard').then(m => m.BasicDashboard); + ? import('./dashboard/premium-dashboard') + : import('./dashboard/basic-dashboard'); }, }, ]; @@ -265,7 +264,7 @@ NOTE: 遅延ルートは、ユーザーが要求する初期データの量を コンポーネントをレンダリングする代わりに、別のルートにリダイレクトするルートを定義できます。 ```ts -import { BlogComponent } from './home/blog.component'; +import {BlogComponent} from './home/blog.component'; const routes: Routes = [ { @@ -274,7 +273,7 @@ const routes: Routes = [ }, { path: 'blog', - component: BlogComponent + component: BlogComponent, }, ]; ``` @@ -286,20 +285,20 @@ const routes: Routes = [ 各ルートには**タイトル**を関連付けることができます。ルートがアクティブになると、Angularは[ページタイトル](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title)を自動的に更新します。アクセシブルな体験を作成するためにこれらのタイトルが必要となるため、アプリケーションに適切なページタイトルを常に定義してください。 ```ts -import { Routes } from '@angular/router'; -import { HomeComponent } from './home/home.component'; -import { AboutComponent } from './about/about.component'; +import {Routes} from '@angular/router'; +import {HomeComponent} from './home/home.component'; +import {AboutComponent} from './about/about.component'; const routes: Routes = [ { path: '', component: HomeComponent, - title: 'Home Page' + title: 'Home Page', }, { path: 'about', component: AboutComponent, - title: 'About Us' + title: 'About Us', }, ]; ``` @@ -309,14 +308,12 @@ const routes: Routes = [ ```ts const titleResolver: ResolveFn = (route) => route.queryParams['id']; const routes: Routes = [ - ... - { + ...{ path: 'products', component: ProductsComponent, title: titleResolver, - } + }, ]; - ``` ルートタイトルは、[`TitleStrategy`](/api/router/TitleStrategy) 抽象クラスを継承するサービスを介しても設定できます。デフォルトでは、Angularは[`DefaultTitleStrategy`](/api/router/DefaultTitleStrategy)を使用します。 @@ -328,9 +325,9 @@ const routes: Routes = [ `TitleStrategy`は、Angularが使用するデフォルトのタイトル戦略をオーバーライドするために提供できるトークンです。カスタムの`TitleStrategy`を提供して、アプリケーションのサフィックスの追加、パンくずリストからのタイトルのフォーマット、ルートデータからのタイトルの動的生成などの規約を実装できます。 ```ts -import { Injectable } from '@angular/core'; -import { Title } from '@angular/platform-browser'; -import { TitleStrategy, RouterStateSnapshot } from '@angular/router'; +import {inject, Injectable} from '@angular/core'; +import {Title} from '@angular/platform-browser'; +import {TitleStrategy, RouterStateSnapshot} from '@angular/router'; @Injectable() export class AppTitleStrategy extends TitleStrategy { @@ -348,14 +345,11 @@ export class AppTitleStrategy extends TitleStrategy { カスタム戦略を使用するには、アプリケーションレベルで`TitleStrategy`トークンを使用してそれを提供します。 ```ts -import { provideRouter, TitleStrategy } from '@angular/router'; -import { AppTitleStrategy } from './app-title.strategy'; +import {provideRouter, TitleStrategy} from '@angular/router'; +import {AppTitleStrategy} from './app-title.strategy'; export const appConfig = { - providers: [ - provideRouter(routes), - { provide: TitleStrategy, useClass: AppTitleStrategy }, - ], + providers: [provideRouter(routes), {provide: TitleStrategy, useClass: AppTitleStrategy}], }; ``` @@ -369,10 +363,7 @@ export const appConfig = { export const ROUTES: Route[] = [ { path: 'admin', - providers: [ - AdminService, - {provide: ADMIN_API_KEY, useValue: '12345'}, - ], + providers: [AdminService, {provide: ADMIN_API_KEY, useValue: '12345'}], children: [ {path: 'users', component: AdminUsersComponent}, {path: 'teams', component: AdminTeamsComponent}, @@ -398,22 +389,22 @@ Angularのプロバイダーと注入に関する詳細については、[依存 `data`プロパティを介して任意の静的データをルートに関連付けることで、ルート固有のメタデータ(例: 分析トラッキング、権限など)を一元化できます。 ```ts -import { Routes } from '@angular/router'; -import { HomeComponent } from './home/home.component'; -import { AboutComponent } from './about/about.component'; -import { ProductsComponent } from './products/products.component'; +import {Routes} from '@angular/router'; +import {HomeComponent} from './home/home.component'; +import {AboutComponent} from './about/about.component'; +import {ProductsComponent} from './products/products.component'; const routes: Routes = [ { path: 'about', component: AboutComponent, - data: { analyticsId: '456' } + data: {analyticsId: '456'}, }, { path: '', component: HomeComponent, - data: { analyticsId: '123' } - } + data: {analyticsId: '123'}, + }, ]; ``` @@ -441,15 +432,15 @@ const routes: Routes = [ children: [ { path: 'info', - component: ProductInfoComponent + component: ProductInfoComponent, }, { path: 'reviews', - component: ProductReviewsComponent - } - ] - } -] + component: ProductReviewsComponent, + }, + ], + }, +]; ``` 上記の例では、ユーザーがURLに基づいて製品情報またはレビューのどちらを表示するかを変更できる製品ページのルートを定義しています。 From 403504928d27855ded58cbc405390ee71d14983e Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 11:43:17 +0900 Subject: [PATCH 17/32] docs(guide/routing): migrate lifecycle-and-events .en.md changes --- .../guide/routing/lifecycle-and-events.en.md | 56 ++++++++----------- .../guide/routing/lifecycle-and-events.md | 56 ++++++++----------- 2 files changed, 44 insertions(+), 68 deletions(-) diff --git a/adev-ja/src/content/guide/routing/lifecycle-and-events.en.md b/adev-ja/src/content/guide/routing/lifecycle-and-events.en.md index 1d998ac28..c4f713230 100644 --- a/adev-ja/src/content/guide/routing/lifecycle-and-events.en.md +++ b/adev-ja/src/content/guide/routing/lifecycle-and-events.en.md @@ -64,16 +64,12 @@ Debugging router navigation issues can be challenging without visibility into th When you need to inspect a Router event sequence, you can enable logging for internal navigation events for debugging. You can configure this by passing a configuration option (`withDebugTracing()`) that enables detailed console logging of all routing events. ```ts -import { provideRouter, withDebugTracing } from '@angular/router'; +import {provideRouter, withDebugTracing} from '@angular/router'; const appRoutes: Routes = []; -bootstrapApplication(AppComponent, - { - providers: [ - provideRouter(appRoutes, withDebugTracing()) - ] - } -); +bootstrapApplication(AppComponent, { + providers: [provideRouter(appRoutes, withDebugTracing())], +}); ``` For more information, check out the official docs on [`withDebugTracing`](api/router/withDebugTracing). @@ -89,26 +85,19 @@ Show loading indicators during navigation: ```angular-ts import { Component, inject } from '@angular/core'; import { Router } from '@angular/router'; -import { toSignal } from '@angular/core/rxjs-interop'; -import { map } from 'rxjs/operators'; @Component({ - selector: 'app-loading', + selector: 'app-root', template: ` - @if (loading()) { -
Loading...
+ @if (isNavigating()) { +
Loading...
} + ` }) -export class AppComponent { +export class App { private router = inject(Router); - - readonly loading = toSignal( - this.router.events.pipe( - map(() => !!this.router.getCurrentNavigation()) - ), - { initialValue: false } - ); + isNavigating = computed(() => !!this.router.currentNavigation()); } ``` @@ -117,30 +106,29 @@ export class AppComponent { Track page views for analytics: ```ts -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { inject, Injectable, DestroyRef } from '@angular/core'; -import { Router, NavigationEnd } from '@angular/router'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {inject, Injectable, DestroyRef} from '@angular/core'; +import {Router, NavigationEnd} from '@angular/router'; -@Injectable({ providedIn: 'root' }) +@Injectable({providedIn: 'root'}) export class AnalyticsService { private router = inject(Router); private destroyRef = inject(DestroyRef); startTracking() { - this.router.events.pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(event => { - // Track page views when URL changes - if (event instanceof NavigationEnd) { - // Send page view to analytics - this.analytics.trackPageView(event.url); - } - }); + this.router.events.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => { + // Track page views when URL changes + if (event instanceof NavigationEnd) { + // Send page view to analytics + this.analytics.trackPageView(event.url); + } + }); } private analytics = { trackPageView: (url: string) => { console.log('Page view tracked:', url); - } + }, }; } ``` diff --git a/adev-ja/src/content/guide/routing/lifecycle-and-events.md b/adev-ja/src/content/guide/routing/lifecycle-and-events.md index ad96d693e..3cbd8a52f 100644 --- a/adev-ja/src/content/guide/routing/lifecycle-and-events.md +++ b/adev-ja/src/content/guide/routing/lifecycle-and-events.md @@ -64,16 +64,12 @@ NOTE: `@angular/router`の[`Event`](api/router/Event)型は、通常のグロー ルーターイベントシーケンスを検査する必要がある場合、デバッグのために内部ナビゲーションイベントのログ記録を有効にできます。これは、すべてのルーティングイベントの詳細なコンソールログ記録を有効にする設定オプション(`withDebugTracing()`)を渡すことで設定できます。 ```ts -import { provideRouter, withDebugTracing } from '@angular/router'; +import {provideRouter, withDebugTracing} from '@angular/router'; const appRoutes: Routes = []; -bootstrapApplication(AppComponent, - { - providers: [ - provideRouter(appRoutes, withDebugTracing()) - ] - } -); +bootstrapApplication(AppComponent, { + providers: [provideRouter(appRoutes, withDebugTracing())], +}); ``` 詳細については、[`withDebugTracing`](api/router/withDebugTracing)に関する公式ドキュメントを参照してください。 @@ -89,26 +85,19 @@ bootstrapApplication(AppComponent, ```angular-ts import { Component, inject } from '@angular/core'; import { Router } from '@angular/router'; -import { toSignal } from '@angular/core/rxjs-interop'; -import { map } from 'rxjs/operators'; @Component({ - selector: 'app-loading', + selector: 'app-root', template: ` - @if (loading()) { -
Loading...
+ @if (isNavigating()) { +
Loading...
} + ` }) -export class AppComponent { +export class App { private router = inject(Router); - - readonly loading = toSignal( - this.router.events.pipe( - map(() => !!this.router.getCurrentNavigation()) - ), - { initialValue: false } - ); + isNavigating = computed(() => !!this.router.currentNavigation()); } ``` @@ -117,30 +106,29 @@ export class AppComponent { アナリティクス用にページビューを追跡します。 ```ts -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { inject, Injectable, DestroyRef } from '@angular/core'; -import { Router, NavigationEnd } from '@angular/router'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {inject, Injectable, DestroyRef} from '@angular/core'; +import {Router, NavigationEnd} from '@angular/router'; -@Injectable({ providedIn: 'root' }) +@Injectable({providedIn: 'root'}) export class AnalyticsService { private router = inject(Router); private destroyRef = inject(DestroyRef); startTracking() { - this.router.events.pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(event => { - // Track page views when URL changes - if (event instanceof NavigationEnd) { - // Send page view to analytics - this.analytics.trackPageView(event.url); - } - }); + this.router.events.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => { + // Track page views when URL changes + if (event instanceof NavigationEnd) { + // Send page view to analytics + this.analytics.trackPageView(event.url); + } + }); } private analytics = { trackPageView: (url: string) => { console.log('Page view tracked:', url); - } + }, }; } ``` From 220a24c8961f73e480a79ef458fb88a0b5bad1c0 Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 11:58:25 +0900 Subject: [PATCH 18/32] docs(guide/routing): migrate navigate, read-route-state, redirecting .en.md changes --- .../guide/routing/navigate-to-routes.en.md | 4 +-- .../guide/routing/navigate-to-routes.md | 4 +-- .../guide/routing/read-route-state.en.md | 8 ++--- .../content/guide/routing/read-route-state.md | 8 ++--- .../guide/routing/redirecting-routes.en.md | 30 ++++++++--------- .../guide/routing/redirecting-routes.md | 32 ++++++++----------- 6 files changed, 39 insertions(+), 47 deletions(-) diff --git a/adev-ja/src/content/guide/routing/navigate-to-routes.en.md b/adev-ja/src/content/guide/routing/navigate-to-routes.en.md index 7a270b90c..f49889f8f 100644 --- a/adev-ja/src/content/guide/routing/navigate-to-routes.en.md +++ b/adev-ja/src/content/guide/routing/navigate-to-routes.en.md @@ -162,7 +162,7 @@ router.navigateByUrl('/products/123?view=details#reviews'); router.navigateByUrl('/search?category=books&sortBy=price'); // With matrix parameters -router.navigateByUrl('/sales-awesome;isOffer=true;showModal=false') +router.navigateByUrl('/sales-awesome;isOffer=true;showModal=false'); ``` In the event you need to replace the current URL in history, `navigateByUrl` also accepts a configuration object that has a `replaceUrl` option. @@ -170,7 +170,7 @@ In the event you need to replace the current URL in history, `navigateByUrl` als ```ts // Replace current URL in history router.navigateByUrl('/checkout', { - replaceUrl: true + replaceUrl: true, }); ``` diff --git a/adev-ja/src/content/guide/routing/navigate-to-routes.md b/adev-ja/src/content/guide/routing/navigate-to-routes.md index 29aab8cfb..9aab81eed 100644 --- a/adev-ja/src/content/guide/routing/navigate-to-routes.md +++ b/adev-ja/src/content/guide/routing/navigate-to-routes.md @@ -162,7 +162,7 @@ router.navigateByUrl('/products/123?view=details#reviews'); router.navigateByUrl('/search?category=books&sortBy=price'); // With matrix parameters -router.navigateByUrl('/sales-awesome;isOffer=true;showModal=false') +router.navigateByUrl('/sales-awesome;isOffer=true;showModal=false'); ``` 履歴内の現在のURLを置き換える必要がある場合、`navigateByUrl`は`replaceUrl`オプションを持つ設定オブジェクトも受け入れます。 @@ -170,7 +170,7 @@ router.navigateByUrl('/sales-awesome;isOffer=true;showModal=false') ```ts // Replace current URL in history router.navigateByUrl('/checkout', { - replaceUrl: true + replaceUrl: true, }); ``` diff --git a/adev-ja/src/content/guide/routing/read-route-state.en.md b/adev-ja/src/content/guide/routing/read-route-state.en.md index 85d74aeba..6377858ee 100644 --- a/adev-ja/src/content/guide/routing/read-route-state.en.md +++ b/adev-ja/src/content/guide/routing/read-route-state.en.md @@ -192,18 +192,18 @@ Matrix parameters are useful when you need to pass auxiliary data to a specific // Multiple parameters: /path;key1=value1;key2=value2 // Navigate with matrix parameters -this.router.navigate(['/awesome-products', { view: 'grid', filter: 'new' }]); +this.router.navigate(['/awesome-products', {view: 'grid', filter: 'new'}]); // Results in URL: /awesome-products;view=grid;filter=new ``` **Using ActivatedRoute** ```ts -import { Component, inject } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import {Component, inject} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; @Component(/* ... */) -export class AwesomeProducts { +export class AwesomeProducts { private route = inject(ActivatedRoute); constructor() { diff --git a/adev-ja/src/content/guide/routing/read-route-state.md b/adev-ja/src/content/guide/routing/read-route-state.md index 083b9649f..e436570ad 100644 --- a/adev-ja/src/content/guide/routing/read-route-state.md +++ b/adev-ja/src/content/guide/routing/read-route-state.md @@ -192,18 +192,18 @@ export class ProductListComponent implements OnInit { // 複数のパラメーター: /path;key1=value1;key2=value2 // マトリックスパラメーターでナビゲート -this.router.navigate(['/awesome-products', { view: 'grid', filter: 'new' }]); +this.router.navigate(['/awesome-products', {view: 'grid', filter: 'new'}]); // 結果のURL: /awesome-products;view=grid;filter=new ``` **ActivatedRouteの使用** ```ts -import { Component, inject } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import {Component, inject} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; @Component(/* ... */) -export class AwesomeProducts { +export class AwesomeProducts { private route = inject(ActivatedRoute); constructor() { diff --git a/adev-ja/src/content/guide/routing/redirecting-routes.en.md b/adev-ja/src/content/guide/routing/redirecting-routes.en.md index d8b6148a3..b58f60ab4 100644 --- a/adev-ja/src/content/guide/routing/redirecting-routes.en.md +++ b/adev-ja/src/content/guide/routing/redirecting-routes.en.md @@ -7,18 +7,18 @@ Route redirects allow you to automatically navigate users from one route to anot You can define redirects in your route configuration with the `redirectTo` property. This property accepts a string. ```ts -import { Routes } from '@angular/router'; +import {Routes} from '@angular/router'; const routes: Routes = [ // Simple redirect - { path: 'marketing', redirectTo: 'newsletter' }, + {path: 'marketing', redirectTo: 'newsletter'}, // Redirect with path parameters - { path: 'legacy-user/:id', redirectTo: 'users/:id' }, + {path: 'legacy-user/:id', redirectTo: 'users/:id'}, // Redirect any other URLs that don’t match // (also known as a "wildcard" redirect) - { path: '**', redirectTo: '/login' } + {path: '**', redirectTo: '/login'}, ]; ``` @@ -66,9 +66,7 @@ In this example, all routes that are prefixed with `news` are redirected to thei On the other hand, `pathMatch: 'full'` is useful when you want Angular Router to only redirect a specific path. ```ts -export const routes: Routes = [ - { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, -]; +export const routes: Routes = [{path: '', redirectTo: '/dashboard', pathMatch: 'full'}]; ``` In this example, any time the user visits the root URL (i.e., `''`), the router redirects that user to the `'/dashboard'` page. @@ -80,9 +78,7 @@ TIP: Be careful when configuring a redirect on the root page (i.e., `"/"` or `"" To further illustrate this, if the `news` example from the previous section used `pathMatch: 'full'` instead: ```ts -export const routes: Routes = [ - { path: 'news', redirectTo: '/blog', pathMatch: 'full' }, -]; +export const routes: Routes = [{path: 'news', redirectTo: '/blog', pathMatch: 'full'}]; ``` This means that: @@ -101,8 +97,8 @@ It typically returns a string or [`URLTree`](api/router/UrlTree), but it can als Here is an example where the user is redirected to different menu based on the time of the day: ```ts -import { Routes } from '@angular/router'; -import { MenuComponent } from './menu/menu.component'; +import {Routes} from '@angular/router'; +import {MenuComponent} from './menu/menu.component'; export const routes: Routes = [ { @@ -124,16 +120,16 @@ export const routes: Routes = [ } else { return `/restaurant/${location}/menu/dinner`; } - } + }, }, // Destination routes - { path: 'restaurant/:location/menu/breakfast', component: MenuComponent }, - { path: 'restaurant/:location/menu/lunch', component: MenuComponent }, - { path: 'restaurant/:location/menu/dinner', component: MenuComponent }, + {path: 'restaurant/:location/menu/breakfast', component: MenuComponent}, + {path: 'restaurant/:location/menu/lunch', component: MenuComponent}, + {path: 'restaurant/:location/menu/dinner', component: MenuComponent}, // Default redirect - { path: '', redirectTo: '/restaurant/downtown/menu', pathMatch: 'full' } + {path: '', redirectTo: '/restaurant/downtown/menu', pathMatch: 'full'}, ]; ``` diff --git a/adev-ja/src/content/guide/routing/redirecting-routes.md b/adev-ja/src/content/guide/routing/redirecting-routes.md index 374a85af7..9ef0a769b 100644 --- a/adev-ja/src/content/guide/routing/redirecting-routes.md +++ b/adev-ja/src/content/guide/routing/redirecting-routes.md @@ -7,18 +7,18 @@ ルート設定で`redirectTo`プロパティを使用してリダイレクトを定義できます。このプロパティは文字列を受け入れます。 ```ts -import { Routes } from '@angular/router'; +import {Routes} from '@angular/router'; const routes: Routes = [ // Simple redirect - { path: 'marketing', redirectTo: 'newsletter' }, + {path: 'marketing', redirectTo: 'newsletter'}, // Redirect with path parameters - { path: 'legacy-user/:id', redirectTo: 'users/:id' }, + {path: 'legacy-user/:id', redirectTo: 'users/:id'}, - // Redirect any other URLs that don’t match + // Redirect any other URLs that don't match // (also known as a "wildcard" redirect) - { path: '**', redirectTo: '/login' } + {path: '**', redirectTo: '/login'}, ]; ``` @@ -66,9 +66,7 @@ export const routes: Routes = [ 一方、`pathMatch: 'full'`は、Angular Routerに特定のパスのみをリダイレクトさせたい場合に役立ちます。 ```ts -export const routes: Routes = [ - { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, -]; +export const routes: Routes = [{path: '', redirectTo: '/dashboard', pathMatch: 'full'}]; ``` この例では、ユーザーがルートURL(つまり`''`)にアクセスするたびに、ルーターはそのユーザーを`'/dashboard'`ページにリダイレクトします。 @@ -80,9 +78,7 @@ TIP: ルートページ(つまり`"/"`または`""`)でリダイレクトを これをさらに説明するために、前のセクションの`news`の例で`pathMatch: 'full'`を使用した場合: ```ts -export const routes: Routes = [ - { path: 'news', redirectTo: '/blog', pathMatch: 'full' }, -]; +export const routes: Routes = [{path: 'news', redirectTo: '/blog', pathMatch: 'full'}]; ``` これは次のことを意味します。 @@ -101,8 +97,8 @@ export const routes: Routes = [ 以下は、時間帯に基づいてユーザーが異なるメニューにリダイレクトされる例です。 ```ts -import { Routes } from '@angular/router'; -import { MenuComponent } from './menu/menu.component'; +import {Routes} from '@angular/router'; +import {MenuComponent} from './menu/menu.component'; export const routes: Routes = [ { @@ -124,16 +120,16 @@ export const routes: Routes = [ } else { return `/restaurant/${location}/menu/dinner`; } - } + }, }, // Destination routes - { path: 'restaurant/:location/menu/breakfast', component: MenuComponent }, - { path: 'restaurant/:location/menu/lunch', component: MenuComponent }, - { path: 'restaurant/:location/menu/dinner', component: MenuComponent }, + {path: 'restaurant/:location/menu/breakfast', component: MenuComponent}, + {path: 'restaurant/:location/menu/lunch', component: MenuComponent}, + {path: 'restaurant/:location/menu/dinner', component: MenuComponent}, // Default redirect - { path: '', redirectTo: '/restaurant/downtown/menu', pathMatch: 'full' } + {path: '', redirectTo: '/restaurant/downtown/menu', pathMatch: 'full'}, ]; ``` From 6af0fc4305268125909930ba4971f06d49866365 Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 12:01:51 +0900 Subject: [PATCH 19/32] docs(guide/routing): migrate route-guards, animations, reference .en.md changes --- .../content/guide/routing/route-guards.en.md | 53 +++++++++++-------- .../src/content/guide/routing/route-guards.md | 53 +++++++++++-------- .../routing/route-transition-animations.en.md | 24 ++++----- .../guide/routing/router-reference.en.md | 4 +- 4 files changed, 76 insertions(+), 58 deletions(-) diff --git a/adev-ja/src/content/guide/routing/route-guards.en.md b/adev-ja/src/content/guide/routing/route-guards.en.md index 01d58af07..a08c5e6be 100644 --- a/adev-ja/src/content/guide/routing/route-guards.en.md +++ b/adev-ja/src/content/guide/routing/route-guards.en.md @@ -51,7 +51,10 @@ It has access to the following default arguments: It can return the [standard return guard types](#route-guard-return-types). ```ts -export const authGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { +export const authGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +) => { const authService = inject(AuthService); return authService.isAuthenticated(); }; @@ -73,7 +76,10 @@ It has access to the following default arguments: It can return the [standard return guard types](#route-guard-return-types). ```ts -export const adminChildGuard: CanActivateChildFn = (childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { +export const adminChildGuard: CanActivateChildFn = ( + childRoute: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +) => { const authService = inject(AuthService); return authService.hasRole('admin'); }; @@ -95,7 +101,12 @@ It has access to the following default arguments: It can return the [standard return guard types](#route-guard-return-types). ```ts -export const unsavedChangesGuard: CanDeactivateFn = (component: FormComponent, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState: RouterStateSnapshot) => { +export const unsavedChangesGuard: CanDeactivateFn = ( + component: FormComponent, + currentRoute: ActivatedRouteSnapshot, + currentState: RouterStateSnapshot, + nextState: RouterStateSnapshot, +) => { return component.hasUnsavedChanges() ? confirm('You have unsaved changes. Are you sure you want to leave?') : true; @@ -130,14 +141,14 @@ const routes: Routes = [ { path: 'dashboard', component: AdminDashboard, - canMatch: [adminGuard] + canMatch: [adminGuard], }, { path: 'dashboard', component: UserDashboard, - canMatch: [userGuard] - } -] + canMatch: [userGuard], + }, +]; ``` In this example, when the user visits `/dashboard`, the first one that matches the correct guard will be used. @@ -151,25 +162,25 @@ Once you've created your route guards, you need to configure them in your route Guards are specified as arrays in the route configuration in order to allow you to apply multiple guards to a single route. They are executed in the order they appear in the array. ```ts -import { Routes } from '@angular/router'; -import { authGuard } from './guards/auth.guard'; -import { adminGuard } from './guards/admin.guard'; -import { canDeactivateGuard } from './guards/can-deactivate.guard'; -import { featureToggleGuard } from './guards/feature-toggle.guard'; +import {Routes} from '@angular/router'; +import {authGuard} from './guards/auth.guard'; +import {adminGuard} from './guards/admin.guard'; +import {canDeactivateGuard} from './guards/can-deactivate.guard'; +import {featureToggleGuard} from './guards/feature-toggle.guard'; const routes: Routes = [ // Basic CanActivate - requires authentication { path: 'dashboard', component: DashboardComponent, - canActivate: [authGuard] + canActivate: [authGuard], }, // Multiple CanActivate guards - requires authentication AND admin role { path: 'admin', component: AdminComponent, - canActivate: [authGuard, adminGuard] + canActivate: [authGuard, adminGuard], }, // CanActivate + CanDeactivate - protected route with unsaved changes check @@ -177,7 +188,7 @@ const routes: Routes = [ path: 'profile', component: ProfileComponent, canActivate: [authGuard], - canDeactivate: [canDeactivateGuard] + canDeactivate: [canDeactivateGuard], }, // CanActivateChild - protects all child routes @@ -186,23 +197,23 @@ const routes: Routes = [ canActivateChild: [authGuard], children: [ // /users/list - PROTECTED - { path: 'list', component: UserListComponent }, + {path: 'list', component: UserListComponent}, // /users/detail/:id - PROTECTED - { path: 'detail/:id', component: UserDetailComponent } - ] + {path: 'detail/:id', component: UserDetailComponent}, + ], }, // CanMatch - conditionally matches route based on feature flag { path: 'beta-feature', component: BetaFeatureComponent, - canMatch: [featureToggleGuard] + canMatch: [featureToggleGuard], }, // Fallback route if beta feature is disabled { path: 'beta-feature', - component: ComingSoonComponent - } + component: ComingSoonComponent, + }, ]; ``` diff --git a/adev-ja/src/content/guide/routing/route-guards.md b/adev-ja/src/content/guide/routing/route-guards.md index c4d690202..b42818a65 100644 --- a/adev-ja/src/content/guide/routing/route-guards.md +++ b/adev-ja/src/content/guide/routing/route-guards.md @@ -51,7 +51,10 @@ Angularは、それぞれ異なる目的を持つ4種類のルートガードを [標準のガード戻り値の型](#route-guard-return-types)を返すことができます。 ```ts -export const authGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { +export const authGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +) => { const authService = inject(AuthService); return authService.isAuthenticated(); }; @@ -73,7 +76,10 @@ Tip: ユーザーをリダイレクトする必要がある場合は、[`URLTree [標準のガード戻り値の型](#route-guard-return-types)を返すことができます。 ```ts -export const adminChildGuard: CanActivateChildFn = (childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { +export const adminChildGuard: CanActivateChildFn = ( + childRoute: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +) => { const authService = inject(AuthService); return authService.hasRole('admin'); }; @@ -95,7 +101,12 @@ export const adminChildGuard: CanActivateChildFn = (childRoute: ActivatedRouteSn [標準のガード戻り値の型](#route-guard-return-types)を返すことができます。 ```ts -export const unsavedChangesGuard: CanDeactivateFn = (component: FormComponent, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState: RouterStateSnapshot) => { +export const unsavedChangesGuard: CanDeactivateFn = ( + component: FormComponent, + currentRoute: ActivatedRouteSnapshot, + currentState: RouterStateSnapshot, + nextState: RouterStateSnapshot, +) => { return component.hasUnsavedChanges() ? confirm('You have unsaved changes. Are you sure you want to leave?') : true; @@ -130,14 +141,14 @@ const routes: Routes = [ { path: 'dashboard', component: AdminDashboard, - canMatch: [adminGuard] + canMatch: [adminGuard], }, { path: 'dashboard', component: UserDashboard, - canMatch: [userGuard] - } -] + canMatch: [userGuard], + }, +]; ``` この例では、ユーザーが`/dashboard`にアクセスすると、正しいガードにマッチする最初のものが使用されます。 @@ -151,25 +162,25 @@ const routes: Routes = [ ガードは、単一のルートに複数のガードを適用できるように、ルート設定で配列として指定されます。それらは配列に現れる順序で実行されます。 ```ts -import { Routes } from '@angular/router'; -import { authGuard } from './guards/auth.guard'; -import { adminGuard } from './guards/admin.guard'; -import { canDeactivateGuard } from './guards/can-deactivate.guard'; -import { featureToggleGuard } from './guards/feature-toggle.guard'; +import {Routes} from '@angular/router'; +import {authGuard} from './guards/auth.guard'; +import {adminGuard} from './guards/admin.guard'; +import {canDeactivateGuard} from './guards/can-deactivate.guard'; +import {featureToggleGuard} from './guards/feature-toggle.guard'; const routes: Routes = [ // Basic CanActivate - 認証が必要 { path: 'dashboard', component: DashboardComponent, - canActivate: [authGuard] + canActivate: [authGuard], }, // 複数のCanActivateガード - 認証と管理者ロールが必要 { path: 'admin', component: AdminComponent, - canActivate: [authGuard, adminGuard] + canActivate: [authGuard, adminGuard], }, // CanActivate + CanDeactivate - 未保存の変更チェック付き保護ルート @@ -177,7 +188,7 @@ const routes: Routes = [ path: 'profile', component: ProfileComponent, canActivate: [authGuard], - canDeactivate: [canDeactivateGuard] + canDeactivate: [canDeactivateGuard], }, // CanActivateChild - すべての子ルートを保護 @@ -186,23 +197,23 @@ const routes: Routes = [ canActivateChild: [authGuard], children: [ // /users/list - 保護されている - { path: 'list', component: UserListComponent }, + {path: 'list', component: UserListComponent}, // /users/detail/:id - 保護されている - { path: 'detail/:id', component: UserDetailComponent } - ] + {path: 'detail/:id', component: UserDetailComponent}, + ], }, // CanMatch - 機能フラグに基づいてルートを条件付きでマッチング { path: 'beta-feature', component: BetaFeatureComponent, - canMatch: [featureToggleGuard] + canMatch: [featureToggleGuard], }, // ベータ機能が無効な場合のフォールバックルート { path: 'beta-feature', - component: ComingSoonComponent - } + component: ComingSoonComponent, + }, ]; ``` diff --git a/adev-ja/src/content/guide/routing/route-transition-animations.en.md b/adev-ja/src/content/guide/routing/route-transition-animations.en.md index a9aa290a1..bacb0b8e1 100644 --- a/adev-ja/src/content/guide/routing/route-transition-animations.en.md +++ b/adev-ja/src/content/guide/routing/route-transition-animations.en.md @@ -41,25 +41,23 @@ Enable view transitions by adding the `withViewTransitions` feature to your [rou ### Standalone bootstrap ```ts -import { bootstrapApplication } from '@angular/platform-browser'; -import { provideRouter, withViewTransitions } from '@angular/router'; -import { routes } from './app.routes'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {provideRouter, withViewTransitions} from '@angular/router'; +import {routes} from './app.routes'; bootstrapApplication(MyApp, { - providers: [ - provideRouter(routes, withViewTransitions()), - ] + providers: [provideRouter(routes, withViewTransitions())], }); ``` ### NgModule bootstrap ```ts -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; +import {NgModule} from '@angular/core'; +import {RouterModule} from '@angular/router'; @NgModule({ - imports: [RouterModule.forRoot(routes, {enableViewTransitions: true})] + imports: [RouterModule.forRoot(routes, {enableViewTransitions: true})], }) export class AppRouting {} ``` @@ -123,13 +121,13 @@ The `withViewTransitions` feature accepts an options object with an `onViewTrans Use this callback to customize transition behavior based on navigation context. For example, you can skip transitions for specific navigation types: ```ts -import { inject } from '@angular/core'; -import { Router, withViewTransitions } from '@angular/router'; +import {inject} from '@angular/core'; +import {Router, withViewTransitions} from '@angular/router'; withViewTransitions({ onViewTransitionCreated: ({transition}) => { const router = inject(Router); - const targetUrl = router.getCurrentNavigation()!.finalUrl!; + const targetUrl = router.currentNavigation()!.finalUrl!; // Skip transition if only fragment or query params change const config = { @@ -143,7 +141,7 @@ withViewTransitions({ transition.skipTransition(); } }, -}) +}); ``` This example skips the view transition when navigation only changes the [URL fragment or query parameters](/guide/routing/read-route-state#query-parameters) (such as anchor links within the same page). The `skipTransition()` method prevents the animation while still allowing the navigation to complete. diff --git a/adev-ja/src/content/guide/routing/router-reference.en.md b/adev-ja/src/content/guide/routing/router-reference.en.md index 7454068a9..2038c2c4c 100644 --- a/adev-ja/src/content/guide/routing/router-reference.en.md +++ b/adev-ja/src/content/guide/routing/router-reference.en.md @@ -110,9 +110,7 @@ For more complete information on how `` is used to construct target U Use `HashLocationStrategy` by providing the `useHash: true` in an object as the second argument of the `RouterModule.forRoot()` in the `AppModule`. ```ts -providers: [ - provideRouter(appRoutes, withHashLocation()) -] +providers: [provideRouter(appRoutes, withHashLocation())]; ``` When using `RouterModule.forRoot`, this is configured with the `useHash: true` in the second argument: `RouterModule.forRoot(routes, {useHash: true})`. From 965a7c0e1b4e6469aa4cd7780aef3b02191d3a15 Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 12:16:05 +0900 Subject: [PATCH 20/32] docs(guide/routing): migrate remaining routing .en.md changes --- .../guide/routing/router-tutorial.en.md | 6 +- .../routing/show-routes-with-outlets.en.md | 18 +-- .../guide/routing/show-routes-with-outlets.md | 8 +- .../src/content/guide/routing/testing.en.md | 105 ++++++++---------- 4 files changed, 63 insertions(+), 74 deletions(-) diff --git a/adev-ja/src/content/guide/routing/router-tutorial.en.md b/adev-ja/src/content/guide/routing/router-tutorial.en.md index d5ee8f522..d816c1e74 100644 --- a/adev-ja/src/content/guide/routing/router-tutorial.en.md +++ b/adev-ja/src/content/guide/routing/router-tutorial.en.md @@ -113,14 +113,14 @@ You import this provider function from `@angular/router`. 1. Add the following import statements: ```ts - import { provideRouter } from '@angular/router'; - import { routes } from './app.routes'; + import {provideRouter} from '@angular/router'; + import {routes} from './app.routes'; ``` 1. Update the providers in the `appConfig`: ```ts - providers: [provideRouter(routes)] + providers: [provideRouter(routes)]; ``` For `NgModule` based applications, put the `provideRouter` in the `providers` list of the `AppModule`, or whichever module is passed to `bootstrapModule` in the application. diff --git a/adev-ja/src/content/guide/routing/show-routes-with-outlets.en.md b/adev-ja/src/content/guide/routing/show-routes-with-outlets.en.md index 8588e5996..b46b4ecb1 100644 --- a/adev-ja/src/content/guide/routing/show-routes-with-outlets.en.md +++ b/adev-ja/src/content/guide/routing/show-routes-with-outlets.en.md @@ -9,14 +9,14 @@ The `RouterOutlet` directive is a placeholder that marks the location where the ``` ```ts -import { Component } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; +import {Component} from '@angular/core'; +import {RouterOutlet} from '@angular/router'; @Component({ selector: 'app-root', imports: [RouterOutlet], templateUrl: './app.component.html', - styleUrl: './app.component.css' + styleUrl: './app.component.css', }) export class AppComponent {} ``` @@ -24,21 +24,21 @@ export class AppComponent {} In this example, if an application has the following routes defined: ```ts -import { Routes } from '@angular/router'; -import { HomeComponent } from './home/home.component'; -import { ProductsComponent } from './products/products.component'; +import {Routes} from '@angular/router'; +import {HomeComponent} from './home/home.component'; +import {ProductsComponent} from './products/products.component'; const routes: Routes = [ { path: '', component: HomeComponent, - title: 'Home Page' + title: 'Home Page', }, { path: 'products', component: ProductsComponent, - title: 'Our Products' - } + title: 'Our Products', + }, ]; ``` diff --git a/adev-ja/src/content/guide/routing/show-routes-with-outlets.md b/adev-ja/src/content/guide/routing/show-routes-with-outlets.md index cde561f6a..0ccbab5f4 100644 --- a/adev-ja/src/content/guide/routing/show-routes-with-outlets.md +++ b/adev-ja/src/content/guide/routing/show-routes-with-outlets.md @@ -170,8 +170,8 @@ Angularは、各ルートで定義された`outlet`プロパティにアウト ルーティングされたコンポーネントにコンテキストデータを渡すには、多くの場合、グローバルステートや複雑なルート設定が必要です。これを簡単にするために、各`RouterOutlet`は`routerOutletData`入力をサポートしています。ルーティングされたコンポーネントとその子コンポーネントは、`ROUTER_OUTLET_DATA`インジェクショントークンを使用してこのデータをシグナルとして読み取ることができ、ルート定義を変更せずにアウトレット固有の設定が可能になります。 ```angular-ts -import { Component } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; +import {Component} from '@angular/core'; +import {RouterOutlet} from '@angular/router'; @Component({ selector: 'app-dashboard', @@ -187,8 +187,8 @@ export class DashboardComponent {} ルーティングされたコンポーネントは、`ROUTER_OUTLET_DATA`を使用して提供されたアウトレットデータをインジェクトできます。 ```angular-ts -import { Component, inject } from '@angular/core'; -import { ROUTER_OUTLET_DATA } from '@angular/router'; +import {Component, inject} from '@angular/core'; +import {ROUTER_OUTLET_DATA} from '@angular/router'; @Component({ selector: 'app-stats', diff --git a/adev-ja/src/content/guide/routing/testing.en.md b/adev-ja/src/content/guide/routing/testing.en.md index c231e552d..8684a7dd1 100644 --- a/adev-ja/src/content/guide/routing/testing.en.md +++ b/adev-ja/src/content/guide/routing/testing.en.md @@ -6,8 +6,7 @@ Testing routing and navigation is essential to ensure your application behaves c This guide assumes you are familiar with the following tools and libraries: -- **[Jasmine](https://jasmine.github.io/)** - JavaScript testing framework that provides the testing syntax (`describe`, `it`, `expect`) -- **[Karma](https://karma-runner.github.io/)** - Test runner that executes tests in browsers +- **[Vitest](https://vitest.dev/)** - JavaScript testing framework that provides the testing syntax (`describe`, `it`, `expect`) - **[Angular Testing Utilities](guide/testing)** - Angular's built-in testing tools ([`TestBed`](api/core/testing/TestBed), [`ComponentFixture`](api/core/testing/ComponentFixture)) - **[`RouterTestingHarness`](api/router/testing/RouterTestingHarness)** - Test harness for testing routed components with built-in navigation and component testing capabilities @@ -21,21 +20,17 @@ The following example shows how to test a `UserProfile` component that displays ```ts // user-profile.component.spec.ts -import { TestBed } from '@angular/core/testing'; -import { Router } from '@angular/router'; -import { RouterTestingHarness } from '@angular/router/testing'; -import { provideRouter } from '@angular/router'; -import { UserProfile } from './user-profile'; +import {TestBed} from '@angular/core/testing'; +import {Router} from '@angular/router'; +import {RouterTestingHarness} from '@angular/router/testing'; +import {provideRouter} from '@angular/router'; +import {UserProfile} from './user-profile'; describe('UserProfile', () => { it('should display user ID from route parameters', async () => { TestBed.configureTestingModule({ imports: [UserProfile], - providers: [ - provideRouter([ - { path: 'user/:id', component: UserProfile } - ]) - ] + providers: [provideRouter([{path: 'user/:id', component: UserProfile}])], }); const harness = await RouterTestingHarness.create(); @@ -68,17 +63,17 @@ The following example tests an `authGuard` that allows navigation for authentica ```ts // auth.guard.spec.ts -import { RouterTestingHarness } from '@angular/router/testing'; -import { provideRouter, Router } from '@angular/router'; -import { authGuard } from './auth.guard'; -import { AuthStore } from './auth-store'; -import { Component } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; - -@Component({ template: '

Protected Page

' }) +import {RouterTestingHarness} from '@angular/router/testing'; +import {provideRouter, Router} from '@angular/router'; +import {authGuard} from './auth.guard'; +import {AuthStore} from './auth-store'; +import {Component} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; + +@Component({template: '

Protected Page

'}) class ProtectedComponent {} -@Component({ template: '

Login Page

' }) +@Component({template: '

Login Page

'}) class LoginComponent {} describe('authGuard', () => { @@ -91,10 +86,10 @@ describe('authGuard', () => { TestBed.configureTestingModule({ providers: [ - { provide: AuthStore, useValue: authStore }, + {provide: AuthStore, useValue: authStore}, provideRouter([ - { path: 'protected', component: ProtectedComponent, canActivate: [authGuard] }, - { path: 'login', component: LoginComponent }, + {path: 'protected', component: ProtectedComponent, canActivate: [authGuard]}, + {path: 'login', component: LoginComponent}, ]), ], }); @@ -120,9 +115,9 @@ describe('authGuard', () => { ```ts // auth.guard.ts -import { inject } from '@angular/core'; -import { CanActivateFn, Router } from '@angular/router'; -import { AuthStore } from './auth-store'; +import {inject} from '@angular/core'; +import {CanActivateFn, Router} from '@angular/router'; +import {AuthStore} from './auth-store'; export const authGuard: CanActivateFn = () => { const authStore = inject(AuthStore); @@ -139,19 +134,19 @@ Here's an example of how to set up a test that verifies different components are ```ts // app.component.spec.ts -import { TestBed } from '@angular/core/testing'; -import { RouterTestingHarness } from '@angular/router/testing'; -import { provideRouter } from '@angular/router'; -import { Component } from '@angular/core'; -import { App } from './app'; +import {TestBed} from '@angular/core/testing'; +import {RouterTestingHarness} from '@angular/router/testing'; +import {provideRouter} from '@angular/router'; +import {Component} from '@angular/core'; +import {App} from './app'; @Component({ - template: '

Home Page

' + template: '

Home Page

', }) class MockHome {} @Component({ - template: '

About Page

' + template: '

About Page

', }) class MockAbout {} @@ -163,10 +158,10 @@ describe('App Router Outlet', () => { imports: [App], providers: [ provideRouter([ - { path: '', component: MockHome }, - { path: 'about', component: MockAbout } - ]) - ] + {path: '', component: MockHome}, + {path: 'about', component: MockAbout}, + ]), + ], }); harness = await RouterTestingHarness.create(); @@ -218,10 +213,10 @@ Here's an example of testing a parent-child route structure: ```ts // nested-routes.spec.ts -import { TestBed } from '@angular/core/testing'; -import { RouterTestingHarness } from '@angular/router/testing'; -import { provideRouter } from '@angular/router'; -import { Parent, Child } from './nested-components'; +import {TestBed} from '@angular/core/testing'; +import {RouterTestingHarness} from '@angular/router/testing'; +import {provideRouter} from '@angular/router'; +import {Parent, Child} from './nested-components'; describe('Nested Routes', () => { let harness: RouterTestingHarness; @@ -234,12 +229,10 @@ describe('Nested Routes', () => { { path: 'parent', component: Parent, - children: [ - { path: 'child', component: Child } - ] - } - ]) - ] + children: [{path: 'child', component: Child}], + }, + ]), + ], }); harness = await RouterTestingHarness.create(); @@ -284,10 +277,10 @@ Here's an example of how to test query parameters and fragments: ```ts // search.component.spec.ts -import { TestBed } from '@angular/core/testing'; -import { Router, provideRouter } from '@angular/router'; -import { RouterTestingHarness } from '@angular/router/testing'; -import { Search } from './search'; +import {TestBed} from '@angular/core/testing'; +import {Router, provideRouter} from '@angular/router'; +import {RouterTestingHarness} from '@angular/router/testing'; +import {Search} from './search'; describe('Search', () => { let component: Search; @@ -296,11 +289,7 @@ describe('Search', () => { beforeEach(async () => { TestBed.configureTestingModule({ imports: [Search], - providers: [ - provideRouter([ - { path: 'search', component: Search } - ]) - ] + providers: [provideRouter([{path: 'search', component: Search}])], }); harness = await RouterTestingHarness.create(); @@ -336,6 +325,6 @@ export class Search { 1. **Use RouterTestingHarness** - For testing routed components, use [`RouterTestingHarness`](api/router/testing/RouterTestingHarness) which provides a cleaner API and eliminates the need for test host components. It offers direct component access, built-in navigation, and better type safety. However, it isn't as suitable for some scenarios, such as testing named outlets, where you may need to create custom host components. 2. **Handle external dependencies thoughtfully** - Prefer real implementations when possible for more realistic tests. If real implementations aren't feasible (e.g., external APIs), use fakes that approximate the real behavior. Use mocks or stubs only as a last resort, as they can make tests brittle and less reliable. 3. **Test navigation state** - Verify both the navigation action and the resulting application state, including URL changes and component rendering. -4. **Handle asynchronous operations** - Router navigation is asynchronous. Use `async/await` or [`fakeAsync`](api/core/testing/fakeAsync) to properly handle timing in your tests. +4. **Handle asynchronous operations** - Router navigation is asynchronous. Use `async/await` to properly handle timing in your tests. 5. **Test error scenarios** - Include tests for invalid routes, failed navigation, and guard rejections to ensure your application handles edge cases gracefully. 6. **Do not mock Angular Router** - Instead, provide real route configurations and use the harness to navigate. This makes your tests more robust and less likely to break on internal Angular updates, while also ensuring you catch real issues when the router updates since mocks can hide breaking changes. From 72643c47bdf03c164de699a54288b82b6686f833 Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 12:31:16 +0900 Subject: [PATCH 21/32] docs(guide/signals): migrate .en.md changes --- .../content/guide/signals/linked-signal.en.md | 30 ++-- .../content/guide/signals/linked-signal.md | 30 ++-- .../src/content/guide/signals/overview.en.md | 136 +++++++++++++----- adev-ja/src/content/guide/signals/overview.md | 136 +++++++++++++----- 4 files changed, 232 insertions(+), 100 deletions(-) diff --git a/adev-ja/src/content/guide/signals/linked-signal.en.md b/adev-ja/src/content/guide/signals/linked-signal.en.md index c2c84a55f..4743057a5 100644 --- a/adev-ja/src/content/guide/signals/linked-signal.en.md +++ b/adev-ja/src/content/guide/signals/linked-signal.en.md @@ -3,7 +3,9 @@ You can use the `signal` function to hold some state in your Angular code. Sometimes, this state depends on some _other_ state. For example, imagine a component that lets the user select a shipping method for an order: ```typescript -@Component({/* ... */}) +@Component({ + /* ... */ +}) export class ShippingMethodPicker { shippingOptions: Signal = getShippingOptions(); @@ -21,7 +23,9 @@ In this example, the `selectedOption` defaults to the first option, but changes **The `linkedSignal` function lets you create a signal to hold some state that is intrinsically _linked_ to some other state.** Revisiting the example above, `linkedSignal` can replace `signal`: ```typescript -@Component({/* ... */}) +@Component({ + /* ... */ +}) export class ShippingMethodPicker { shippingOptions: Signal = getShippingOptions(); @@ -62,7 +66,9 @@ interface ShippingMethod { name: string; } -@Component({/* ... */}) +@Component({ + /* ... */ +}) export class ShippingMethodPicker { constructor() { this.changeShipping(2); @@ -71,9 +77,9 @@ export class ShippingMethodPicker { } shippingOptions = signal([ - { id: 0, name: 'Ground' }, - { id: 1, name: 'Air' }, - { id: 2, name: 'Sea' }, + {id: 0, name: 'Ground'}, + {id: 1, name: 'Air'}, + {id: 2, name: 'Sea'}, ]); selectedOption = linkedSignal({ @@ -82,9 +88,7 @@ export class ShippingMethodPicker { computation: (newOptions, previous) => { // If the newOptions contain the previously selected option, preserve that selection. // Otherwise, default to the first option. - return ( - newOptions.find((opt) => opt.id === previous?.value.id) ?? newOptions[0] - ); + return newOptions.find((opt) => opt.id === previous?.value.id) ?? newOptions[0]; }, }); @@ -94,9 +98,9 @@ export class ShippingMethodPicker { changeShippingOptions() { this.shippingOptions.set([ - { id: 0, name: 'Email' }, - { id: 1, name: 'Sea' }, - { id: 2, name: 'Postal Service' }, + {id: 0, name: 'Email'}, + {id: 1, name: 'Sea'}, + {id: 2, name: 'Postal Service'}, ]); } } @@ -125,7 +129,7 @@ const activeUserEditCopy = linkedSignal(() => activeUser(), { // Or, if separating `source` and `computation` const activeUserEditCopy = linkedSignal({ source: activeUser, - computation: user => user, + computation: (user) => user, equal: (a, b) => a.id === b.id, }); ``` diff --git a/adev-ja/src/content/guide/signals/linked-signal.md b/adev-ja/src/content/guide/signals/linked-signal.md index 3bfcba367..8840ce411 100644 --- a/adev-ja/src/content/guide/signals/linked-signal.md +++ b/adev-ja/src/content/guide/signals/linked-signal.md @@ -3,7 +3,9 @@ `signal`関数は、Angularコードで状態を保持するために使用できます。この状態は、他の状態に依存することがあります。例えば、ユーザーが注文の配送方法を選択できるコンポーネントを考えてみましょう。 ```typescript -@Component({/* ... */}) +@Component({ + /* ... */ +}) export class ShippingMethodPicker { shippingOptions: Signal = getShippingOptions(); @@ -21,7 +23,9 @@ export class ShippingMethodPicker { **`linkedSignal`関数は、本質的に他の状態に_リンク_された状態を保持するシグナルを作成できます。**上記の例を再考すると、`linkedSignal`は`signal`を置き換えることができます。 ```typescript -@Component({/* ... */}) +@Component({ + /* ... */ +}) export class ShippingMethodPicker { shippingOptions: Signal = getShippingOptions(); @@ -62,7 +66,9 @@ interface ShippingMethod { name: string; } -@Component({/* ... */}) +@Component({ + /* ... */ +}) export class ShippingMethodPicker { constructor() { this.changeShipping(2); @@ -71,9 +77,9 @@ export class ShippingMethodPicker { } shippingOptions = signal([ - { id: 0, name: 'Ground' }, - { id: 1, name: 'Air' }, - { id: 2, name: 'Sea' }, + {id: 0, name: 'Ground'}, + {id: 1, name: 'Air'}, + {id: 2, name: 'Sea'}, ]); selectedOption = linkedSignal({ @@ -82,9 +88,7 @@ export class ShippingMethodPicker { computation: (newOptions, previous) => { // If the newOptions contain the previously selected option, preserve that selection. // Otherwise, default to the first option. - return ( - newOptions.find((opt) => opt.id === previous?.value.id) ?? newOptions[0] - ); + return newOptions.find((opt) => opt.id === previous?.value.id) ?? newOptions[0]; }, }); @@ -94,9 +98,9 @@ export class ShippingMethodPicker { changeShippingOptions() { this.shippingOptions.set([ - { id: 0, name: 'Email' }, - { id: 1, name: 'Sea' }, - { id: 2, name: 'Postal Service' }, + {id: 0, name: 'Email'}, + {id: 1, name: 'Sea'}, + {id: 2, name: 'Postal Service'}, ]); } } @@ -125,7 +129,7 @@ const activeUserEditCopy = linkedSignal(() => activeUser(), { // または、`source`と`computation`を分ける場合 const activeUserEditCopy = linkedSignal({ source: activeUser, - computation: user => user, + computation: (user) => user, equal: (a, b) => a.id === b.id, }); ``` diff --git a/adev-ja/src/content/guide/signals/overview.en.md b/adev-ja/src/content/guide/signals/overview.en.md index 8bbd01158..cffc09248 100644 --- a/adev-ja/src/content/guide/signals/overview.en.md +++ b/adev-ja/src/content/guide/signals/overview.en.md @@ -33,11 +33,46 @@ or use the `.update()` operation to compute a new value from the previous one: ```ts // Increment the count by 1. -count.update(value => value + 1); +count.update((value) => value + 1); ``` Writable signals have the type `WritableSignal`. +#### Converting writable signals to readonly + +`WritableSignal` provide a `asReadonly()` method that returns a readonly version of the signal. This is useful when you want to expose a signal's value to consumers without allowing them to modify it directly: + +```ts +@Injectable({providedIn: 'root'}) +export class CounterState { + // Private writable state + private readonly _count = signal(0); + + readonly count = this._count.asReadonly(); // public readonly + + increment() { + this._count.update((v) => v + 1); + } +} + +@Component({ + /* ... */ +}) +export class AwesomeCounter { + state = inject(CounterState); + + count = this.state.count; // can read but not modify + + increment() { + this.state.increment(); + } +} +``` + +The readonly signal reflects any changes made to the original writable signal, but cannot be modified using `set()` or `update()` methods. + +IMPORTANT: The readonly signals do **not** have any built-in mechanism that would prevent deep-mutation of their value. + ### Computed signals **Computed signal** are read-only signals that derive their value from other signals. You define computed signals using the `computed` function and specifying a derivation: @@ -89,6 +124,68 @@ If you set `showCount` to `true` and then read `conditionalCount` again, the der Note that dependencies can be removed during a derivation as well as added. If you later set `showCount` back to `false`, then `count` will no longer be considered a dependency of `conditionalCount`. +## Reactive contexts + +A **reactive context** is a runtime state where Angular monitors signal reads to establish a dependency. The code reading the signal is the _consumer_, and the signal being read is the _producer_. + +Angular automatically enters a reactive context when: + +- Executing an `effect`, `afterRenderEffect` callback. +- Evaluating a `computed` signal. +- Evaluating a `linkedSignal`. +- Evaluating a `resource`'s params or loader function. +- Rendering a component template (including bindings in the [host property](guide/components/host-elements#binding-to-the-host-element)). + +During these operations, Angular creates a _live_ connection. If a tracked signal changes, Angular will _eventually_ re-run the consumer. + +### Asserts the reactive context + +Angular provides the `assertNotInReactiveContext` helper function to assert that code is not executing within a reactive context. Pass a reference to the calling function so the error message points to the correct API entry point if the assertion fails. This produces a clearer, more actionable error message than a generic reactive context error. + +```ts +import {assertNotInReactiveContext} from '@angular/core'; + +function subscribeToEvents() { + assertNotInReactiveContext(subscribeToEvents); + // Safe to proceed - subscription logic here +} +``` + +### Reading without tracking dependencies + +Rarely, you may want to execute code which may read signals within a reactive function such as `computed` or `effect` _without_ creating a dependency. + +For example, suppose that when `currentUser` changes, the value of a `counter` should be logged. You could create an `effect` which reads both signals: + +```ts +effect(() => { + console.log(`User set to ${currentUser()} and the counter is ${counter()}`); +}); +``` + +This example will log a message when _either_ `currentUser` or `counter` changes. However, if the effect should only run when `currentUser` changes, then the read of `counter` is only incidental and changes to `counter` shouldn't log a new message. + +You can prevent a signal read from being tracked by calling its getter with `untracked`: + +```ts +effect(() => { + console.log(`User set to ${currentUser()} and the counter is ${untracked(counter)}`); +}); +``` + +`untracked` is also useful when an effect needs to invoke some external code which shouldn't be treated as a dependency: + +```ts +effect(() => { + const user = currentUser(); + untracked(() => { + // If the `loggingService` reads signals, they won't be counted as + // dependencies of this effect. + this.loggingService.log(`User set to ${user}`); + }); +}); +``` + ## Advanced derivations While `computed` handles simple readonly derivations, you might find youself needing a writable state that is dependant on other signals. @@ -98,7 +195,7 @@ All signal APIs are synchronous— `signal`, `computed`, `input`, etc. However, ## Executing side effects on non-reactive APIs -Synchronous or asynchronous derivations are recommended when we want to react to state changes. However this doesn't cover all the usecase and you'll sometime find yourself in a situation where you need to react on signal changes on non-reactive apis. Use `effect` or `afterRenderEffect` for those specific usecases. For more information see [Side effects for non-reactives APIs](/guide/effect) guide. +Synchronous or asynchronous derivations are recommended when we want to react to state changes. However, this doesn't cover all the possible use cases, and you'll sometimes find yourself in a situation where you need to react to signal changes on non-reactive apis. Use `effect` or `afterRenderEffect` for those specific usecases. For more information see [Side effects for non-reactive APIs](/guide/signals/effect) guide. ## Reading signals in `OnPush` components @@ -148,41 +245,6 @@ isWritableSignal(count); // true isWritableSignal(doubled); // false ``` -### Reading without tracking dependencies - -Rarely, you may want to execute code which may read signals within a reactive function such as `computed` or `effect` _without_ creating a dependency. - -For example, suppose that when `currentUser` changes, the value of a `counter` should be logged. You could create an `effect` which reads both signals: - -```ts -effect(() => { - console.log(`User set to ${currentUser()} and the counter is ${counter()}`); -}); -``` - -This example will log a message when _either_ `currentUser` or `counter` changes. However, if the effect should only run when `currentUser` changes, then the read of `counter` is only incidental and changes to `counter` shouldn't log a new message. - -You can prevent a signal read from being tracked by calling its getter with `untracked`: - -```ts -effect(() => { - console.log(`User set to ${currentUser()} and the counter is ${untracked(counter)}`); -}); -``` - -`untracked` is also useful when an effect needs to invoke some external code which shouldn't be treated as a dependency: - -```ts -effect(() => { - const user = currentUser(); - untracked(() => { - // If the `loggingService` reads signals, they won't be counted as - // dependencies of this effect. - this.loggingService.log(`User set to ${user}`); - }); -}); -``` - ## Using signals with RxJS See [RxJS interop with Angular signals](ecosystem/rxjs-interop) for details on interoperability between signals and RxJS. diff --git a/adev-ja/src/content/guide/signals/overview.md b/adev-ja/src/content/guide/signals/overview.md index 1430db45b..91a356088 100644 --- a/adev-ja/src/content/guide/signals/overview.md +++ b/adev-ja/src/content/guide/signals/overview.md @@ -33,11 +33,46 @@ count.set(3); ```ts // カウントを1増やす。 -count.update(value => value + 1); +count.update((value) => value + 1); ``` 書き込み可能なシグナルは、`WritableSignal`という型になります。 +#### 書き込み可能なシグナルを読み取り専用に変換する + +`WritableSignal`は、シグナルの読み取り専用バージョンを返す`asReadonly()`メソッドを提供します。これは、シグナルの値をコンシューマーに公開したいが、直接変更できないようにしたい場合に便利です。 + +```ts +@Injectable({providedIn: 'root'}) +export class CounterState { + // プライベートな書き込み可能な状態 + private readonly _count = signal(0); + + readonly count = this._count.asReadonly(); // パブリックな読み取り専用 + + increment() { + this._count.update((v) => v + 1); + } +} + +@Component({ + /* ... */ +}) +export class AwesomeCounter { + state = inject(CounterState); + + count = this.state.count; // 読み取りはできるが変更はできない + + increment() { + this.state.increment(); + } +} +``` + +読み取り専用のシグナルは、元の書き込み可能なシグナルへの変更を反映しますが、`set()`または`update()`メソッドを使用して変更することはできません。 + +IMPORTANT: 読み取り専用のシグナルには、その値の深い変更を防ぐ組み込みのメカニズムは**ありません**。 + ### 算出シグナル **算出シグナル**は、他のシグナルから値を派生させる読み取り専用のシグナルです。算出シグナルは、`computed`関数を使用して、派生を指定することで定義します。 @@ -89,6 +124,68 @@ const conditionalCount = computed(() => { 依存関係は、派生中に追加されるだけでなく、削除されることもできます。後で`showCount`を再び偽に設定すると、`count`は`conditionalCount`の依存関係として扱われなくなります。 +## リアクティブコンテキスト + +**リアクティブコンテキスト**は、Angularがシグナルの読み取りを監視して依存関係を確立する実行時の状態です。シグナルを読み取るコードは_コンシューマー_であり、読み取られるシグナルは_プロデューサー_です。 + +Angularは次の場合に自動的にリアクティブコンテキストに入ります: + +- `effect`、`afterRenderEffect`のコールバックを実行する +- `computed`シグナルを評価する +- `linkedSignal`を評価する +- `resource`のparamsまたはloader関数を評価する +- コンポーネントテンプレートをレンダリングする([ホストプロパティ](guide/components/host-elements#binding-to-the-host-element)のバインディングを含む) + +これらの操作中、Angularは_ライブ_接続を作成します。追跡されたシグナルが変更されると、Angularは_最終的に_コンシューマーを再実行します。 + +### リアクティブコンテキストをアサートする + +Angularは、コードがリアクティブコンテキスト内で実行されていないことをアサートするための`assertNotInReactiveContext`ヘルパー関数を提供します。呼び出し元の関数への参照を渡すことで、アサーションが失敗した場合、エラーメッセージが正しいAPIエントリポイントを指すようにします。これにより、一般的なリアクティブコンテキストエラーよりも明確でアクションにつながるエラーメッセージが生成されます。 + +```ts +import {assertNotInReactiveContext} from '@angular/core'; + +function subscribeToEvents() { + assertNotInReactiveContext(subscribeToEvents); + // 安全に続行 - サブスクリプションロジックをここに +} +``` + +### 依存関係を追跡せずに読み取る + +まれに、`computed`や`effect`などのリアクティブ関数内でシグナルを読み取るコードを実行する必要があり、依存関係を作成しない場合があります。 + +たとえば、`currentUser`が変更されたときに、`counter`の値をログに記録する必要があるとします。両方のシグナルを読み取る`effect`を作成できます。 + +```ts +effect(() => { + console.log(`User set to ${currentUser()} and the counter is ${counter()}`); +}); +``` + +この例では、`currentUser`または`counter`のいずれかが変更されると、メッセージがログに記録されます。しかし、`currentUser`のみが変更されたときにエフェクトを実行する必要がある場合、`counter`の読み取りは単なる付随的なものであり、`counter`が変更されても新しいメッセージはログに記録されるべきではありません。 + +シグナルのゲッターを`untracked`で呼び出すことで、シグナルの読み取りが追跡されないようにできます。 + +```ts +effect(() => { + console.log(`User set to ${currentUser()} and the counter is ${untracked(counter)}`); +}); +``` + +`untracked`は、エフェクトが、依存関係として扱われない外部のコードを呼び出す必要がある場合にも役立ちます。 + +```ts +effect(() => { + const user = currentUser(); + untracked(() => { + // `loggingService`がSignalを読み取っても、 + // このEffectの依存関係として扱われません。 + this.loggingService.log(`User set to ${user}`); + }); +}); +``` + ## 高度な派生 {#advanced-derivations} `computed`はシンプルな読み取り専用の派生を処理しますが、他のシグナルに依存する書き込み可能な状態が必要な場合があります。 @@ -98,7 +195,7 @@ const conditionalCount = computed(() => { ## 非リアクティブAPIでの副作用の実行 {#executing-side-effects-on-non-reactive-apis} -状態の変更に反応したい場合、同期または非同期の派生が推奨されます。しかし、これがすべてのユースケースをカバーするわけではなく、非リアクティブAPIでシグナルの変更に反応する必要がある状況に遭遇することがあります。これらの特定のユースケースには、`effect`または`afterRenderEffect`を使用してください。詳細については、[非リアクティブAPIの副作用](/guide/effect)ガイドを参照してください。 +状態の変更に反応したい場合、同期または非同期の派生が推奨されます。しかし、これがすべての可能なユースケースをカバーするわけではなく、非リアクティブAPIでシグナルの変更に反応する必要がある状況に遭遇することがあります。これらの特定のユースケースには、`effect`または`afterRenderEffect`を使用してください。詳細については、[非リアクティブAPIの副作用](/guide/signals/effect)ガイドを参照してください。 ## `OnPush`コンポーネントでのシグナルの読み取り {#reading-signals-in-onpush-components} @@ -148,41 +245,6 @@ isWritableSignal(count); // true isWritableSignal(doubled); // false ``` -### 依存関係を追跡せずに読み取る {#reading-without-tracking-dependencies} - -まれに、`computed`や`effect`などのリアクティブ関数内でシグナルを読み取るコードを実行する必要があり、依存関係を作成しない場合があります。 - -たとえば、`currentUser`が変更されたときに、`counter`の値をログに記録する必要があるとします。両方のシグナルを読み取る`effect`を作成できます。 - -```ts -effect(() => { - console.log(`User set to ${currentUser()} and the counter is ${counter()}`); -}); -``` - -この例では、`currentUser`または`counter`のいずれかが変更されると、メッセージがログに記録されます。しかし、`currentUser`のみが変更されたときにエフェクトを実行する必要がある場合、`counter`の読み取りは単なる付随的なものであり、`counter`が変更されても新しいメッセージはログに記録されるべきではありません。 - -シグナルのゲッターを`untracked`で呼び出すことで、シグナルの読み取りが追跡されないようにできます。 - -```ts -effect(() => { - console.log(`User set to ${currentUser()} and the counter is ${untracked(counter)}`); -}); -``` - -`untracked`は、エフェクトが、依存関係として扱われない外部のコードを呼び出す必要がある場合にも役立ちます。 - -```ts -effect(() => { - const user = currentUser(); - untracked(() => { - // `loggingService`がSignalを読み取っても、 - // このEffectの依存関係として扱われません。 - this.loggingService.log(`User set to ${user}`); - }); -}); -``` - ## RxJSとシグナルを併用する シグナルとRxJSの相互運用性の詳細については、[RxJSとAngularシグナルの相互運用](ecosystem/rxjs-interop) を参照してください。 From 7729a253a53572e068aef5e23b62b19c91055acf Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 12:36:10 +0900 Subject: [PATCH 22/32] docs(guide/templates): migrate .en.md changes --- .../guide/templates/event-listeners.en.md | 28 ++++++++----------- .../guide/templates/event-listeners.md | 28 ++++++++----------- .../content/guide/templates/ng-template.en.md | 2 +- .../content/guide/templates/ng-template.md | 2 +- 4 files changed, 26 insertions(+), 34 deletions(-) diff --git a/adev-ja/src/content/guide/templates/event-listeners.en.md b/adev-ja/src/content/guide/templates/event-listeners.en.md index de0d54caf..344d29b94 100644 --- a/adev-ja/src/content/guide/templates/event-listeners.en.md +++ b/adev-ja/src/content/guide/templates/event-listeners.en.md @@ -128,8 +128,8 @@ Angular’s event system is extensible via custom event plugins registered with To create a custom event plugin, extend the `EventManagerPlugin` class and implement the required methods. ```ts -import { Injectable } from '@angular/core'; -import { EventManagerPlugin } from '@angular/platform-browser'; +import {Injectable} from '@angular/core'; +import {EventManagerPlugin} from '@angular/platform-browser'; @Injectable() export class DebounceEventPlugin extends EventManagerPlugin { @@ -143,21 +143,17 @@ export class DebounceEventPlugin extends EventManagerPlugin { } // Handle the event registration - override addEventListener( - element: HTMLElement, - eventName: string, - handler: Function - ) { + override addEventListener(element: HTMLElement, eventName: string, handler: Function) { // Parse the event: e.g., "click.debounce.500" // event: "click", delay: 500 - const [event, method , delay = 300 ] = eventName.split('.'); + const [event, method, delay = 300] = eventName.split('.'); let timeoutId: number; const listener = (event: Event) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => { - handler(event); + handler(event); }, delay); }; @@ -175,19 +171,19 @@ export class DebounceEventPlugin extends EventManagerPlugin { Register your custom plugin using the `EVENT_MANAGER_PLUGINS` token in your application's providers: ```ts -import { bootstrapApplication } from '@angular/platform-browser'; -import { EVENT_MANAGER_PLUGINS } from '@angular/platform-browser'; -import { AppComponent } from './app/app.component'; -import { DebounceEventPlugin } from './debounce-event-plugin'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {EVENT_MANAGER_PLUGINS} from '@angular/platform-browser'; +import {AppComponent} from './app/app.component'; +import {DebounceEventPlugin} from './debounce-event-plugin'; bootstrapApplication(AppComponent, { providers: [ { provide: EVENT_MANAGER_PLUGINS, useClass: DebounceEventPlugin, - multi: true - } - ] + multi: true, + }, + ], }); ``` diff --git a/adev-ja/src/content/guide/templates/event-listeners.md b/adev-ja/src/content/guide/templates/event-listeners.md index ee99e07c5..6ee264ab3 100644 --- a/adev-ja/src/content/guide/templates/event-listeners.md +++ b/adev-ja/src/content/guide/templates/event-listeners.md @@ -128,8 +128,8 @@ Angularのイベントシステムは、`EVENT_MANAGER_PLUGINS` インジェク カスタムイベントプラグインを作成するには、`EventManagerPlugin` クラスを拡張し、必要なメソッドを実装します。 ```ts -import { Injectable } from '@angular/core'; -import { EventManagerPlugin } from '@angular/platform-browser'; +import {Injectable} from '@angular/core'; +import {EventManagerPlugin} from '@angular/platform-browser'; @Injectable() export class DebounceEventPlugin extends EventManagerPlugin { @@ -143,21 +143,17 @@ export class DebounceEventPlugin extends EventManagerPlugin { } // Handle the event registration - override addEventListener( - element: HTMLElement, - eventName: string, - handler: Function - ) { + override addEventListener(element: HTMLElement, eventName: string, handler: Function) { // Parse the event: e.g., "click.debounce.500" // event: "click", delay: 500 - const [event, method , delay = 300 ] = eventName.split('.'); + const [event, method, delay = 300] = eventName.split('.'); let timeoutId: number; const listener = (event: Event) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => { - handler(event); + handler(event); }, delay); }; @@ -175,19 +171,19 @@ export class DebounceEventPlugin extends EventManagerPlugin { アプリケーションのプロバイダーで `EVENT_MANAGER_PLUGINS` トークンを使用してカスタムプラグインを登録します。 ```ts -import { bootstrapApplication } from '@angular/platform-browser'; -import { EVENT_MANAGER_PLUGINS } from '@angular/platform-browser'; -import { AppComponent } from './app/app.component'; -import { DebounceEventPlugin } from './debounce-event-plugin'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {EVENT_MANAGER_PLUGINS} from '@angular/platform-browser'; +import {AppComponent} from './app/app.component'; +import {DebounceEventPlugin} from './debounce-event-plugin'; bootstrapApplication(AppComponent, { providers: [ { provide: EVENT_MANAGER_PLUGINS, useClass: DebounceEventPlugin, - multi: true - } - ] + multi: true, + }, + ], }); ``` diff --git a/adev-ja/src/content/guide/templates/ng-template.en.md b/adev-ja/src/content/guide/templates/ng-template.en.md index b3118b505..a2e10d15e 100644 --- a/adev-ja/src/content/guide/templates/ng-template.en.md +++ b/adev-ja/src/content/guide/templates/ng-template.en.md @@ -137,7 +137,7 @@ The `NgTemplateOutlet` directive from `@angular/common` accepts a `TemplateRef` First, import `NgTemplateOutlet`: ```typescript -import { NgTemplateOutlet } from '@angular/common'; +import {NgTemplateOutlet} from '@angular/common'; ``` The following example declares a template fragment and renders that fragment to a `` element with `NgTemplateOutlet`: diff --git a/adev-ja/src/content/guide/templates/ng-template.md b/adev-ja/src/content/guide/templates/ng-template.md index 2fd16ce5f..34586b0ea 100644 --- a/adev-ja/src/content/guide/templates/ng-template.md +++ b/adev-ja/src/content/guide/templates/ng-template.md @@ -137,7 +137,7 @@ export class MyDirective { まず、`NgTemplateOutlet` をインポートします: ```typescript -import { NgTemplateOutlet } from '@angular/common'; +import {NgTemplateOutlet} from '@angular/common'; ``` 次の例では、テンプレートフラグメントを宣言し、そのフラグメントを `NgTemplateOutlet` を使用して `` 要素にレンダリングします。 From d3dd4236e35d5c468216715138d5e0bb7aa6121e Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 12:50:57 +0900 Subject: [PATCH 23/32] docs(guide/testing): migrate paired .en.md changes --- .../guide/testing/attribute-directives.en.md | 134 +++++++++++++-- .../guide/testing/attribute-directives.md | 134 +++++++++++++-- .../guide/testing/components-basics.en.md | 156 ++++++++++++++---- .../guide/testing/components-basics.md | 156 ++++++++++++++---- .../creating-component-harnesses.en.md | 31 ++-- .../testing/creating-component-harnesses.md | 31 ++-- .../src/content/guide/testing/debugging.en.md | 14 +- .../src/content/guide/testing/debugging.md | 14 +- adev-ja/src/content/guide/testing/karma.en.md | 10 +- adev-ja/src/content/guide/testing/karma.md | 10 +- .../guide/testing/migrating-to-vitest.en.md | 9 +- .../guide/testing/migrating-to-vitest.md | 9 +- .../src/content/guide/testing/overview.en.md | 13 +- adev-ja/src/content/guide/testing/overview.md | 13 +- adev-ja/src/content/guide/testing/pipes.en.md | 51 +++++- adev-ja/src/content/guide/testing/pipes.md | 51 +++++- .../src/content/guide/testing/services.en.md | 122 ++++++++------ adev-ja/src/content/guide/testing/services.md | 122 ++++++++------ 18 files changed, 792 insertions(+), 288 deletions(-) diff --git a/adev-ja/src/content/guide/testing/attribute-directives.en.md b/adev-ja/src/content/guide/testing/attribute-directives.en.md index d3b4e9173..161392901 100644 --- a/adev-ja/src/content/guide/testing/attribute-directives.en.md +++ b/adev-ja/src/content/guide/testing/attribute-directives.en.md @@ -3,20 +3,65 @@ An _attribute directive_ modifies the behavior of an element, component or another directive. Its name reflects the way the directive is applied: as an attribute on a host element. -## Testing the `HighlightDirective` +## Testing the `Highlight` directive -The sample application's `HighlightDirective` sets the background color of an element based on either a data bound color or a default color \(lightgray\). +The sample application's `Highlight` directive sets the background color of an element based on either a data bound color or a default color \(lightgray\). It also sets a custom property of the element \(`customProperty`\) to `true` for no reason other than to show that it can. - - -It's used throughout the application, perhaps most simply in the `AboutComponent`: - - - -Testing the specific use of the `HighlightDirective` within the `AboutComponent` requires only the techniques explored in the ["Nested component tests"](guide/testing/components-scenarios#nested-component-tests) section of [Component testing scenarios](guide/testing/components-scenarios). - - +```ts +import {Directive, inject, input} from '@angular/core'; + +/** + * Set backgroundColor for the attached element to highlight color + * and set the element's customProperty attribute to true + */ +@Directive({ + selector: '[highlight]', + host: { + '[style.backgroundColor]': 'bgColor() || defaultColor', + }, +}) +export class Highlight { + readonly defaultColor = 'rgb(211, 211, 211)'; // lightgray + + readonly bgColor = input('', {alias: 'highlight'}); +} +``` + +It's used throughout the application, perhaps most simply in the `About` component: + +```ts +@Component({ + imports: [Twain, Highlight], + template: ` +

About

+

Quote of the day:

+ + `, +}) +export class About {} +``` + +Testing the specific use of the `Highlight` directive within the `About` component requires only the techniques explored in the ["Nested component tests"](guide/testing/components-scenarios#nested-component-tests) section of [Component testing scenarios](guide/testing/components-scenarios). + +```ts +let fixture: ComponentFixture; + +beforeEach(async () => { + TestBed.configureTestingModule({ + providers: [TwainService, UserService], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }); + fixture = TestBed.createComponent(About); + await fixture.whenStable(); +}); + +it('should have skyblue

', () => { + const h2: HTMLElement = fixture.nativeElement.querySelector('h2'); + const bgColor = h2.style.backgroundColor; + expect(bgColor).toBe('skyblue'); +}); +``` However, testing a single use case is unlikely to explore the full range of a directive's capabilities. Finding and testing all components that use the directive is tedious, brittle, and almost as unlikely to afford full coverage. @@ -26,16 +71,75 @@ Isolated unit tests don't touch the DOM and, therefore, do not inspire confidenc A better solution is to create an artificial test component that demonstrates all ways to apply the directive. - +```angular-ts +@Component({ + imports: [Highlight], + template: ` +

Something Yellow

+

The Default (Gray)

+

No Highlight

+ + `, +}) +class Test {} +``` HighlightDirective spec in action -HELPFUL: The `` case binds the `HighlightDirective` to the name of a color value in the input box. +HELPFUL: The `` case binds the `Highlight` to the name of a color value in the input box. The initial value is the word "cyan" which should be the background color of the input box. Here are some tests of this component: - +```ts +let fixture: ComponentFixture; +let des: DebugElement[]; // the three elements w/ the directive + +beforeEach(async () => { + fixture = TestBed.createComponent(Test); + await fixture.whenStable(); + + // all elements with an attached Highlight + des = fixture.debugElement.queryAll(By.directive(Highlight)); +}); + +// color tests +it('should have three highlighted elements', () => { + expect(des.length).toBe(3); +}); + +it('should color 1st

background "yellow"', () => { + const bgColor = des[0].nativeElement.style.backgroundColor; + expect(bgColor).toBe('yellow'); +}); + +it('should color 2nd

background w/ default color', () => { + const dir = des[1].injector.get(Highlight); + const bgColor = des[1].nativeElement.style.backgroundColor; + expect(bgColor).toBe(dir.defaultColor); +}); + +it('should bind background to value color', async () => { + // easier to work with nativeElement + const input = des[2].nativeElement as HTMLInputElement; + expect(input.style.backgroundColor, 'initial backgroundColor').toBe('cyan'); + + input.value = 'green'; + + // Dispatch a DOM event so that Angular responds to the input value change. + input.dispatchEvent(new Event('input')); + await fixture.whenStable(); + + expect(input.style.backgroundColor, 'changed backgroundColor').toBe('green'); +}); + +it('bare

should not have a backgroundColor', () => { + // the h2 without the Highlight directive + const bareH2 = fixture.debugElement.query(By.css('h2:not([highlight])')); + + expect(bareH2.styles.backgroundColor).toBeUndefined(); +}); +``` A few techniques are noteworthy: @@ -47,6 +151,6 @@ A few techniques are noteworthy: But feel free to exploit the `nativeElement` when that seems easier or more clear than the abstraction. - Angular adds a directive to the injector of the element to which it is applied. - The test for the default color uses the injector of the second `

` to get its `HighlightDirective` instance and its `defaultColor`. + The test for the default color uses the injector of the second `

` to get its `Highlight` instance and its `defaultColor`. - `DebugElement.properties` affords access to the artificial custom property that is set by the directive diff --git a/adev-ja/src/content/guide/testing/attribute-directives.md b/adev-ja/src/content/guide/testing/attribute-directives.md index bc98b74b4..5b2ac20cc 100644 --- a/adev-ja/src/content/guide/testing/attribute-directives.md +++ b/adev-ja/src/content/guide/testing/attribute-directives.md @@ -3,20 +3,64 @@ _属性ディレクティブ_は、要素、コンポーネント、または他のディレクティブの動作を変更します。 その名前は、ディレクティブが適用される方法、つまりホスト要素の属性として適用される方法を反映しています。 -## `HighlightDirective`のテスト +## `Highlight`ディレクティブのテスト -サンプルアプリケーションの`HighlightDirective`は、データバインドされた色またはデフォルトの色(ライトグレー)に基づいて、要素の背景色を設定します。 +サンプルアプリケーションの`Highlight`ディレクティブは、データバインドされた色またはデフォルトの色(ライトグレー)に基づいて、要素の背景色を設定します。 また、要素のカスタムプロパティ(`customProperty`)を`true`に設定しますが、これは単に設定できることを示すためです。 - - -これはアプリケーション全体で使用されています。おそらく最も簡単な例は`AboutComponent`です。 - - - -`AboutComponent`内での`HighlightDirective`の特定の使用をテストするには、[Component testing scenarios](guide/testing/components-scenarios)セクションの["Nested component tests"](guide/testing/components-scenarios#nested-component-tests)で説明されているテクニックのみが必要です。 - - +```ts +import {Directive, inject, input} from '@angular/core'; + +/** + * Set backgroundColor for the attached element to highlight color + * and set the element's customProperty attribute to true + */ +@Directive({ + selector: '[highlight]', + host: { + '[style.backgroundColor]': 'bgColor() || defaultColor', + }, +}) +export class Highlight { + readonly defaultColor = 'rgb(211, 211, 211)'; // lightgray + + readonly bgColor = input('', {alias: 'highlight'}); +} +``` + +これはアプリケーション全体で使用されています。おそらく最も簡単な例は`About`コンポーネントです。 + +```ts +@Component({ + imports: [Twain, Highlight], + template: ` +

About

+

Quote of the day:

+ + `, +}) +export class About {} +``` + +`About`コンポーネント内での`Highlight`ディレクティブの特定の使用をテストするには、[Component testing scenarios](guide/testing/components-scenarios)セクションの["Nested component tests"](guide/testing/components-scenarios#nested-component-tests)で説明されているテクニックのみが必要です。 + +```ts +let fixture: ComponentFixture; + +beforeEach(async () => { + TestBed.configureTestingModule({ + providers: [TwainService, UserService], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }); + fixture = TestBed.createComponent(About); + await fixture.whenStable(); +}); + +it('should have skyblue

', () => { + const h2: HTMLElement = fixture.nativeElement.querySelector('h2'); + const bgColor = h2.style.backgroundColor; + expect(bgColor).toBe('skyblue'); +}); しかし、単一のユースケースをテストしても、ディレクティブの機能のすべてを調べられるとは限りません。 ディレクティブを使用するすべてのコンポーネントを見つけてテストすることは、面倒で壊れやすく、完全なカバレッジを実現する可能性もほとんどありません。 @@ -26,16 +70,75 @@ _クラスのみのテスト_は役立つ場合がありますが、このディ より良い解決策は、ディレクティブのすべての適用方法を示す人工的なテストコンポーネントを作成することです。 - +```angular-ts +@Component({ + imports: [Highlight], + template: ` +

Something Yellow

+

The Default (Gray)

+

No Highlight

+ + `, +}) +class Test {} +``` HighlightDirective spec in action -HELPFUL: ``のケースでは、`HighlightDirective`を、入力ボックス内の色の値の名前とバインドしています。 +HELPFUL: ``のケースでは、`Highlight`を、入力ボックス内の色の値の名前とバインドしています。 初期値は単語"cyan"であり、これは入力ボックスの背景色になります。 このコンポーネントのテストをいくつか紹介します。 - +```ts +let fixture: ComponentFixture; +let des: DebugElement[]; // the three elements w/ the directive + +beforeEach(async () => { + fixture = TestBed.createComponent(Test); + await fixture.whenStable(); + + // all elements with an attached Highlight + des = fixture.debugElement.queryAll(By.directive(Highlight)); +}); + +// color tests +it('should have three highlighted elements', () => { + expect(des.length).toBe(3); +}); + +it('should color 1st

background "yellow"', () => { + const bgColor = des[0].nativeElement.style.backgroundColor; + expect(bgColor).toBe('yellow'); +}); + +it('should color 2nd

background w/ default color', () => { + const dir = des[1].injector.get(Highlight); + const bgColor = des[1].nativeElement.style.backgroundColor; + expect(bgColor).toBe(dir.defaultColor); +}); + +it('should bind background to value color', async () => { + // easier to work with nativeElement + const input = des[2].nativeElement as HTMLInputElement; + expect(input.style.backgroundColor, 'initial backgroundColor').toBe('cyan'); + + input.value = 'green'; + + // Dispatch a DOM event so that Angular responds to the input value change. + input.dispatchEvent(new Event('input')); + await fixture.whenStable(); + + expect(input.style.backgroundColor, 'changed backgroundColor').toBe('green'); +}); + +it('bare

should not have a backgroundColor', () => { + // the h2 without the Highlight directive + const bareH2 = fixture.debugElement.query(By.css('h2:not([highlight])')); + + expect(bareH2.styles.backgroundColor).toBeUndefined(); +}); +``` いくつかのテクニックが注目に値します。 @@ -47,6 +150,7 @@ HELPFUL: ``のケースでは、`HighlightDirective`を、入力ボック しかし、抽象化よりも簡単で明確な場合は、`nativeElement`を利用してください。 - Angularは、適用された要素のインジェクターにディレクティブを追加します。 - デフォルトの色に対するテストでは、2番目の`

`のインジェクターを使用して、その`HighlightDirective`インスタンスとその`defaultColor`を取得します。 + デフォルトの色に対するテストでは、2番目の`

`のインジェクターを使用して、その`Highlight`インスタンスとその`defaultColor`を取得します。 - `DebugElement.properties`は、ディレクティブによって設定された人工的なカスタムプロパティにアクセスできます。 + diff --git a/adev-ja/src/content/guide/testing/components-basics.en.md b/adev-ja/src/content/guide/testing/components-basics.en.md index 4341bdd00..82dce45e0 100644 --- a/adev-ja/src/content/guide/testing/components-basics.en.md +++ b/adev-ja/src/content/guide/testing/components-basics.en.md @@ -17,9 +17,9 @@ Classes alone cannot tell you if the component is going to render properly, resp - Is `Lightswitch.clicked()` bound to anything such that the user can invoke it? - Is the `Lightswitch.message` displayed? -- Can the user actually select the hero displayed by `DashboardHeroComponent`? +- Can the user actually select the hero displayed by the `DashboardHero` component? - Is the hero name displayed as expected \(such as uppercase\)? -- Is the welcome message displayed by the template of `WelcomeComponent`? +- Is the welcome message displayed by the template of the `Welcome` component? These might not be troubling questions for the preceding simple components illustrated. But many components have complex interactions with the DOM elements described in their templates, causing HTML to appear and disappear as the component state changes. @@ -32,19 +32,37 @@ To write these kinds of test, you'll use additional features of the `TestBed` as The CLI creates an initial test file for you by default when you ask it to generate a new component. -For example, the following CLI command generates a `BannerComponent` in the `app/banner` folder \(with inline template and styles\): +For example, the following CLI command generates a `Banner` component in the `app/banner` folder \(with inline template and styles\): ```shell -ng generate component banner --inline-template --inline-style --module app +ng generate component banner --inline-template --inline-style ``` -It also generates an initial test file for the component, `banner-external.component.spec.ts`, that looks like this: +It also generates an initial test file for the component, `banner.spec.ts`, that looks like this: - +```ts +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Banner} from './banner'; -HELPFUL: Because `compileComponents` is asynchronous, it uses the [`waitForAsync`](api/core/testing/waitForAsync) utility function imported from `@angular/core/testing`. +describe('Banner', () => { + let component: Banner; + let fixture: ComponentFixture; -Refer to the [waitForAsync](guide/testing/components-scenarios#waitForAsync) section for more details. + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Banner], + }).compileComponents(); + + fixture = TestBed.createComponent(Banner); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); +``` ### Reduce the setup @@ -55,25 +73,30 @@ The rest of the file is boilerplate setup code anticipating more advanced tests You'll learn about these advanced test features in the following sections. For now, you can radically reduce this test file to a more manageable size: - - -In this example, the metadata object passed to `TestBed.configureTestingModule` simply declares `BannerComponent`, the component to test. - - - -HELPFUL: There's no need to declare or import anything else. -The default test module is pre-configured with something like the `BrowserModule` from `@angular/platform-browser`. +```ts +describe('Banner (minimal)', () => { + it('should create', () => { + const fixture = TestBed.createComponent(Banner); + const component = fixture.componentInstance; + expect(component).toBeDefined(); + }); +}); +``` Later you'll call `TestBed.configureTestingModule()` with imports, providers, and more declarations to suit your testing needs. Optional `override` methods can further fine-tune aspects of the configuration. +NOTE: `TestBed.compileComponents` is only required when `@defer` blocks are used in the tested components. + ### `createComponent()` After configuring `TestBed`, you call its `createComponent()` method. - +```ts +const fixture = TestBed.createComponent(Banner); +``` -`TestBed.createComponent()` creates an instance of the `BannerComponent`, adds a corresponding element to the test-runner DOM, and returns a [`ComponentFixture`](#componentfixture). +`TestBed.createComponent()` creates an instance of the `Banner` component, adds a corresponding element to the test-runner DOM, and returns a [`ComponentFixture`](#componentfixture). IMPORTANT: Do not re-configure `TestBed` after calling `createComponent`. @@ -84,22 +107,62 @@ If you try, `TestBed` throws an error. ### `ComponentFixture` -The [ComponentFixture](api/core/testing/ComponentFixture) is a test harness for interacting with the created component and its corresponding element. +The [`ComponentFixture`](api/core/testing/ComponentFixture) is a test harness for interacting with the created component and its corresponding element. -Access the component instance through the fixture and confirm it exists with a Jasmine expectation: +Access the component instance through the fixture and confirm it exists with an expectation: - +```ts +const component = fixture.componentInstance; +expect(component).toBeDefined(); +``` ### `beforeEach()` You will add more tests as this component evolves. -Rather than duplicate the `TestBed` configuration for each test, you refactor to pull the setup into a Jasmine `beforeEach()` and some supporting variables: +Rather than duplicate the `TestBed` configuration for each test, you refactor to pull the setup into a `beforeEach()` and some supporting variables: + +```ts +describe('Banner (with beforeEach)', () => { + let component: Banner; + let fixture: ComponentFixture; + + beforeEach(async () => { + fixture = TestBed.createComponent(Banner); + component = fixture.componentInstance; + + await fixture.whenStable(); // necessary to wait for the initial rendering + }); + + it('should create', () => { + expect(component).toBeDefined(); + }); +}); +``` - +HELPFUL: By awaiting the initial rendering in the `beforeEach` with `await fixture.whenStable` the single tests synchronous. Now add a test that gets the component's element from `fixture.nativeElement` and looks for the expected text. - +```ts +it('should contain "banner works!"', () => { + const bannerElement: HTMLElement = fixture.nativeElement; + expect(bannerElement.textContent).toContain('banner works!'); +}); +``` + +### create a `setup` function + +As an alternative to `beforeEach`, you can also create a setup function that you will call in every test. +A setup function has the advantage of being customizable via parameters. + +Here is an example of what a setup function could look like: + +```ts +function setup(providers?: StaticProviders[]): ComponentFixture { + TestBed.configureTestingModule({providers}); + return TestBed.createComponent(Banner); +} +``` ### `nativeElement` @@ -107,7 +170,7 @@ The value of `ComponentFixture.nativeElement` has the `any` type. Later you'll encounter the `DebugElement.nativeElement` and it too has the `any` type. Angular can't know at compile time what kind of HTML element the `nativeElement` is or if it even is an HTML element. -The application might be running on a _non-browser platform_, such as the server or a [Web Worker](https://developer.mozilla.org/docs/Web/API/Web_Workers_API), where the element might have a diminished API or not exist at all. +The application might be running on a _non-browser platform_, such as the server or a node environment, where the element might have a diminished API or not exist at all. The tests in this guide are designed to run in a browser so a `nativeElement` value will always be an `HTMLElement` or one of its derived classes. @@ -115,17 +178,28 @@ Knowing that it is an `HTMLElement` of some sort, use the standard HTML `querySe Here's another test that calls `HTMLElement.querySelector` to get the paragraph element and look for the banner text: - +```ts +it('should have

with "banner works!"', () => { + const bannerElement: HTMLElement = fixture.nativeElement; + const p = bannerElement.querySelector('p')!; + expect(p.textContent).toEqual('banner works!'); +}); +``` ### `DebugElement` The Angular _fixture_ provides the component's element directly through the `fixture.nativeElement`. - +```ts +const bannerElement: HTMLElement = fixture.nativeElement; +``` This is actually a convenience method, implemented as `fixture.debugElement.nativeElement`. - +```ts +const bannerDe: DebugElement = fixture.debugElement; +const bannerEl: HTMLElement = bannerDe.nativeElement; +``` There's a good reason for this circuitous path to the element. @@ -140,13 +214,22 @@ Because the sample tests for this guide are designed to run only in a browser, a Here's the previous test, re-implemented with `fixture.debugElement.nativeElement`: - +```ts +it('should find the

with fixture.debugElement.nativeElement', () => { + const bannerDe: DebugElement = fixture.debugElement; + const bannerEl: HTMLElement = bannerDe.nativeElement; + const p = bannerEl.querySelector('p')!; + expect(p.textContent).toEqual('banner works!'); +}); +``` The `DebugElement` has other methods and properties that are useful in tests, as you'll see elsewhere in this guide. You import the `DebugElement` symbol from the Angular core library. - +```ts +import {DebugElement} from '@angular/core'; +``` ### `By.css()` @@ -162,11 +245,20 @@ These query methods take a _predicate_ function that returns `true` when a node You create a _predicate_ with the help of a `By` class imported from a library for the runtime platform. Here's the `By` import for the browser platform: - +```ts +import {By} from '@angular/platform-browser'; +``` The following example re-implements the previous test with `DebugElement.query()` and the browser's `By.css` method. - +```ts +it('should find the

with fixture.debugElement.query(By.css)', () => { + const bannerDe: DebugElement = fixture.debugElement; + const paragraphDe = bannerDe.query(By.css('p')); + const p: HTMLElement = paragraphDe.nativeElement; + expect(p.textContent).toEqual('banner works!'); +}); +``` Some noteworthy observations: diff --git a/adev-ja/src/content/guide/testing/components-basics.md b/adev-ja/src/content/guide/testing/components-basics.md index 833d620f0..c04d93059 100644 --- a/adev-ja/src/content/guide/testing/components-basics.md +++ b/adev-ja/src/content/guide/testing/components-basics.md @@ -17,9 +17,9 @@ Angularの `TestBed` は、次のセクションで説明するように、こ - `Lightswitch.clicked()` はユーザーが呼び出せるように何かとバインドされていますか? - `Lightswitch.message` は表示されますか? -- ユーザーは `DashboardHeroComponent` によって表示されるヒーローを実際に選択できますか? +- ユーザーは `DashboardHero` コンポーネントによって表示されるヒーローを実際に選択できますか? - ヒーローの名前は期待通り(たとえば、大文字)に表示されますか? -- `WelcomeComponent` のテンプレートによってウェルカムメッセージは表示されますか? +- `Welcome` コンポーネントのテンプレートによってウェルカムメッセージは表示されますか? これらの質問は、説明した前の簡単なコンポーネントにとっては問題ないかもしれません。 しかし、多くのコンポーネントは、そのテンプレートで記述されているDOM要素との複雑な対話を持ち、コンポーネントの状態が変わるとHTMLが表示および非表示になります。 @@ -32,19 +32,37 @@ Angularの `TestBed` は、次のセクションで説明するように、こ CLIは、新しいコンポーネントの生成を要求するときに、デフォルトで初期テストファイルを自動的に生成します。 -たとえば、次のCLIコマンドは `app/banner` フォルダーに `BannerComponent` を生成します(インラインテンプレートとスタイル付き)。 +たとえば、次のCLIコマンドは `app/banner` フォルダーに `Banner` コンポーネントを生成します(インラインテンプレートとスタイル付き)。 ```shell -ng generate component banner --inline-template --inline-style --module app +ng generate component banner --inline-template --inline-style ``` -また、コンポーネントの初期テストファイル `banner-external.component.spec.ts` も生成し、次のようになります。 +また、コンポーネントの初期テストファイル `banner.spec.ts` も生成し、次のようになります。 - +```ts +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Banner} from './banner'; -HELPFUL: `compileComponents` は非同期であるため、`@angular/core/testing` からインポートされた [`waitForAsync`](api/core/testing/waitForAsync) ユーティリティ関数を使用します。 +describe('Banner', () => { + let component: Banner; + let fixture: ComponentFixture; -詳細については、[waitForAsync](guide/testing/components-scenarios#waitForAsync) セクションを参照してください。 + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Banner], + }).compileComponents(); + + fixture = TestBed.createComponent(Banner); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); +``` ### セットアップの削減 @@ -55,25 +73,30 @@ HELPFUL: `compileComponents` は非同期であるため、`@angular/core/testin これらの高度なテスト機能については、次のセクションで説明します。 今のところ、このテストファイルをより管理しやすいサイズに大幅に縮小できます。 - - -この例では、`TestBed.configureTestingModule` に渡されるメタデータオブジェクトは、単にテスト対象のコンポーネントである `BannerComponent` を宣言します。 - - - -HELPFUL: 他のものを宣言したりインポートする必要はありません。 -デフォルトのテストモジュールは、`@angular/platform-browser` の `BrowserModule` などのモジュールで事前に構成されています。 +```ts +describe('Banner (minimal)', () => { + it('should create', () => { + const fixture = TestBed.createComponent(Banner); + const component = fixture.componentInstance; + expect(component).toBeDefined(); + }); +}); +``` 後で `TestBed.configureTestingModule()` を呼び出して、インポート、プロバイダー、その他の宣言を追加して、テストのニーズに合わせて構成します。 オプションの `override` メソッドは、構成の側面をさらに微調整できます。 +NOTE: `TestBed.compileComponents` は、テストされるコンポーネントで `@defer` ブロックが使用されている場合にのみ必要です。 + ### `createComponent()` `TestBed` を構成したら、その `createComponent()` メソッドを呼び出します。 - +```ts +const fixture = TestBed.createComponent(Banner); +``` -`TestBed.createComponent()` は `BannerComponent` のインスタンスを作成し、対応する要素をテストランナーのDOMに追加し、[`ComponentFixture`](#componentfixture) を返します。 +`TestBed.createComponent()` は `Banner` コンポーネントのインスタンスを作成し、対応する要素をテストランナーのDOMに追加し、[`ComponentFixture`](#componentfixture) を返します。 IMPORTANT: `createComponent` を呼び出した後に `TestBed` を再構成しないでください。 @@ -84,22 +107,62 @@ IMPORTANT: `createComponent` を呼び出した後に `TestBed` を再構成し ### `ComponentFixture` -[ComponentFixture](api/core/testing/ComponentFixture) は、作成されたコンポーネントとその対応する要素を操作するためのテストハーネスです。 +[`ComponentFixture`](api/core/testing/ComponentFixture) は、作成されたコンポーネントとその対応する要素を操作するためのテストハーネスです。 -fixtureを介してコンポーネントインスタンスにアクセスし、Jasmineの期待を使用して存在を確認します。 +fixtureを介してコンポーネントインスタンスにアクセスし、期待を使用して存在を確認します。 - +```ts +const component = fixture.componentInstance; +expect(component).toBeDefined(); +``` ### `beforeEach()` このコンポーネントが進化するにつれて、さらに多くのテストを追加するでしょう。 -各テストのために `TestBed` 構成を複製するのではなく、セットアップをJasmineの `beforeEach()` といくつかのサポート変数にリファクタリングします。 +各テストのために `TestBed` 構成を複製するのではなく、セットアップを `beforeEach()` といくつかのサポート変数にリファクタリングします。 + +```ts +describe('Banner (with beforeEach)', () => { + let component: Banner; + let fixture: ComponentFixture; + + beforeEach(async () => { + fixture = TestBed.createComponent(Banner); + component = fixture.componentInstance; + + await fixture.whenStable(); // necessary to wait for the initial rendering + }); + + it('should create', () => { + expect(component).toBeDefined(); + }); +}); +``` - +HELPFUL: `beforeEach` で `await fixture.whenStable` を使用して初期レンダリングを待機することで、個別のテストは同期的になります。 次に、fixture.nativeElementからコンポーネントの要素を取得し、期待されるテキストを探すテストを追加します。 - +```ts +it('should contain "banner works!"', () => { + const bannerElement: HTMLElement = fixture.nativeElement; + expect(bannerElement.textContent).toContain('banner works!'); +}); +``` + +### `setup` 関数の作成 + +`beforeEach` の代わりに、各テストで呼び出すsetup関数を作成することもできます。 +setup関数は、パラメーターを介してカスタマイズできるという利点があります。 + +setup関数の例を次に示します。 + +```ts +function setup(providers?: StaticProviders[]): ComponentFixture { + TestBed.configureTestingModule({providers}); + return TestBed.createComponent(Banner); +} +``` ### `nativeElement` @@ -107,7 +170,7 @@ fixtureを介してコンポーネントインスタンスにアクセスし、J 後で `DebugElement.nativeElement` に遭遇しますが、これも `any` 型です。 Angularは、コンパイル時に `nativeElement` がどのようなHTML要素であるか、あるいはHTML要素であるかどうかすらを知ることができません。 -アプリケーションは、サーバーや [Web Worker](https://developer.mozilla.org/docs/Web/API/Web_Workers_API) などの _非ブラウザープラットフォーム_ で実行されている可能性があり、その場合、要素のAPIが制限されているか、存在しない可能性があります。 +アプリケーションは、サーバーやNode環境などの _非ブラウザープラットフォーム_ で実行されている可能性があり、その場合、要素のAPIが制限されているか、存在しない可能性があります。 このガイドのテストはブラウザで実行されるように設計されているため、`nativeElement` の値は常に `HTMLElement` またはその派生クラスのいずれかになります。 @@ -115,17 +178,28 @@ Angularは、コンパイル時に `nativeElement` がどのようなHTML要素 次に、`HTMLElement.querySelector` を呼び出して段落要素を取得し、バナーテキストを探すテストを示します。 - +```ts +it('should have

with "banner works!"', () => { + const bannerElement: HTMLElement = fixture.nativeElement; + const p = bannerElement.querySelector('p')!; + expect(p.textContent).toEqual('banner works!'); +}); +``` ### `DebugElement` Angularの _fixture_ は、`fixture.nativeElement` を介してコンポーネントの要素を直接提供します。 - +```ts +const bannerElement: HTMLElement = fixture.nativeElement; +``` これは実際には、`fixture.debugElement.nativeElement` として実装された便利なメソッドです。 - +```ts +const bannerDe: DebugElement = fixture.debugElement; +const bannerEl: HTMLElement = bannerDe.nativeElement; +``` この回りくどい要素へのパスには、正当な理由があります。 @@ -140,13 +214,22 @@ AngularはHTML要素ツリーを作成するのではなく、実行時プラッ 次に、`fixture.debugElement.nativeElement` を使用して再実装された前のテストを示します。 - +```ts +it('should find the

with fixture.debugElement.nativeElement', () => { + const bannerDe: DebugElement = fixture.debugElement; + const bannerEl: HTMLElement = bannerDe.nativeElement; + const p = bannerEl.querySelector('p')!; + expect(p.textContent).toEqual('banner works!'); +}); +``` `DebugElement` には、このガイドの他の場所で説明されているように、テストで役立つ他のメソッドとプロパティがあります。 Angularコアライブラリから `DebugElement` シンボルをインポートします。 - +```ts +import {DebugElement} from '@angular/core'; +``` ### `By.css()` @@ -162,11 +245,20 @@ Angularコアライブラリから `DebugElement` シンボルをインポート 実行時プラットフォームのライブラリからインポートされた `By` クラスの助けを借りて _述語_ を作成します。 次に、ブラウザプラットフォームの `By` のインポートを示します。 - +```ts +import {By} from '@angular/platform-browser'; +``` 次の例は、`DebugElement.query()` とブラウザの `By.css` メソッドを使用して、前のテストを再実装したものです。 - +```ts +it('should find the

with fixture.debugElement.query(By.css)', () => { + const bannerDe: DebugElement = fixture.debugElement; + const paragraphDe = bannerDe.query(By.css('p')); + const p: HTMLElement = paragraphDe.nativeElement; + expect(p.textContent).toEqual('banner works!'); +}); +``` 注目すべき観察結果をいくつか紹介します。 diff --git a/adev-ja/src/content/guide/testing/creating-component-harnesses.en.md b/adev-ja/src/content/guide/testing/creating-component-harnesses.en.md index 84c89294c..76c6bc4b8 100644 --- a/adev-ja/src/content/guide/testing/creating-component-harnesses.en.md +++ b/adev-ja/src/content/guide/testing/creating-component-harnesses.en.md @@ -28,11 +28,11 @@ The `hostSelector` property identifies elements in the DOM that match this harne @Component({ selector: 'my-popup', template: ` - + @if (isOpen()) {

} - ` + `, }) class MyPopup { triggerText = input(''); @@ -118,16 +118,17 @@ For example, consider a menu build using the popup from above: ```ts @Directive({ - selector: 'my-menu-item' + selector: 'my-menu-item', }) class MyMenuItem {} @Component({ -selector: 'my-menu', -template: ` + selector: 'my-menu', + template: ` + - ` + `, }) class MyMenu { triggerText = input(''); @@ -185,9 +186,11 @@ class MyMenuHarness extends ComponentHarness { /** Creates a `HarnessPredicate` used to locate a particular `MyMenuHarness`. */ static with(options: MyMenuHarnessFilters): HarnessPredicate { - return new HarnessPredicate(MyMenuHarness, options) - .addOption('trigger text', options.triggerText, - (harness, text) => HarnessPredicate.stringMatches(harness.getTriggerText(), text)); + return new HarnessPredicate(MyMenuHarness, options).addOption( + 'trigger text', + options.triggerText, + (harness, text) => HarnessPredicate.stringMatches(harness.getTriggerText(), text), + ); } protected getPopupHarness = this.locatorFor(MyPopupHarness); @@ -204,9 +207,11 @@ class MyMenuItemHarness extends ComponentHarness { /** Creates a `HarnessPredicate` used to locate a particular `MyMenuItemHarness`. */ static with(options: MyMenuItemHarnessFilters): HarnessPredicate { - return new HarnessPredicate(MyMenuItemHarness, options) - .addOption('text', options.text, - (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text)); + return new HarnessPredicate(MyMenuItemHarness, options).addOption( + 'text', + options.text, + (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text), + ); } /** Gets the text of the menu item. */ @@ -262,8 +267,8 @@ class MyPopupHarness extends ComponentHarness { async getHarnessLoaderForContent(): Promise { const rootLocator = this.documentRootLocatorFactory(); return rootLocator.harnessLoaderFor('my-popup-content'); - } } +} ``` ## Waiting for asynchronous tasks diff --git a/adev-ja/src/content/guide/testing/creating-component-harnesses.md b/adev-ja/src/content/guide/testing/creating-component-harnesses.md index e9e902313..78923fa15 100644 --- a/adev-ja/src/content/guide/testing/creating-component-harnesses.md +++ b/adev-ja/src/content/guide/testing/creating-component-harnesses.md @@ -28,11 +28,11 @@ ng add @angular/cdk @Component({ selector: 'my-popup', template: ` - + @if (isOpen()) {
} - ` + `, }) class MyPopup { triggerText = input(''); @@ -118,16 +118,17 @@ class MyPopupHarness extends ComponentHarness { ```ts @Directive({ - selector: 'my-menu-item' + selector: 'my-menu-item', }) class MyMenuItem {} @Component({ -selector: 'my-menu', -template: ` + selector: 'my-menu', + template: ` + - ` + `, }) class MyMenu { triggerText = input(''); @@ -185,9 +186,11 @@ class MyMenuHarness extends ComponentHarness { /** Creates a `HarnessPredicate` used to locate a particular `MyMenuHarness`. */ static with(options: MyMenuHarnessFilters): HarnessPredicate { - return new HarnessPredicate(MyMenuHarness, options) - .addOption('trigger text', options.triggerText, - (harness, text) => HarnessPredicate.stringMatches(harness.getTriggerText(), text)); + return new HarnessPredicate(MyMenuHarness, options).addOption( + 'trigger text', + options.triggerText, + (harness, text) => HarnessPredicate.stringMatches(harness.getTriggerText(), text), + ); } protected getPopupHarness = this.locatorFor(MyPopupHarness); @@ -204,9 +207,11 @@ class MyMenuItemHarness extends ComponentHarness { /** Creates a `HarnessPredicate` used to locate a particular `MyMenuItemHarness`. */ static with(options: MyMenuItemHarnessFilters): HarnessPredicate { - return new HarnessPredicate(MyMenuItemHarness, options) - .addOption('text', options.text, - (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text)); + return new HarnessPredicate(MyMenuItemHarness, options).addOption( + 'text', + options.text, + (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text), + ); } /** Gets the text of the menu item. */ @@ -262,8 +267,8 @@ class MyPopupHarness extends ComponentHarness { async getHarnessLoaderForContent(): Promise { const rootLocator = this.documentRootLocatorFactory(); return rootLocator.harnessLoaderFor('my-popup-content'); - } } +} ``` ## 非同期タスクの待機 {#waiting-for-asynchronous-tasks} diff --git a/adev-ja/src/content/guide/testing/debugging.en.md b/adev-ja/src/content/guide/testing/debugging.en.md index a1de79fb7..49012648e 100644 --- a/adev-ja/src/content/guide/testing/debugging.en.md +++ b/adev-ja/src/content/guide/testing/debugging.en.md @@ -15,16 +15,6 @@ Debugging in the default Node.js environment is often the quickest way to diagno ## Debugging in a browser -Debugging in a browser is recommended for tests that rely on the DOM or other browser-specific APIs. This approach allows you to use the browser's own developer tools. +Debugging with Vitest and [browser mode](/guide/testing/migrating-to-vitest#5-configure-browser-mode-optional) is not supported today. -1. Ensure you have a browser provider installed. See [Running tests in a browser](guide/testing/overview#running-tests-in-a-browser) for setup instructions. -2. Run the `ng test` command with both the `--browsers` and `--debug` flags: - ```shell - ng test --browsers=chromium --debug - ``` -3. This command runs the tests in a headed browser and keeps it open after the tests finish, allowing you to inspect the output. -4. Open the browser's **Developer Tools**. On Windows, press `Ctrl-Shift-I`. On macOS, press `Command-Option-I`. -5. Go to the **Sources** tab. -6. Use `Control/Command-P` to search for and open your test file. -7. Set a breakpoint in your test. -8. Reload the test runner UI in the browser. The execution will now stop at your breakpoint. + - ## Keep learning -Remember: Signal Forms is experimental, so check the [official documentation](guide/forms/signal-forms) for updates to the API. +Remember: Signal Forms is experimental, so check the [official documentation](guide/forms/signals/overview) for updates to the API. Happy coding! diff --git a/adev-ja/src/content/tutorials/signal-forms/steps/6-next-steps/README.md b/adev-ja/src/content/tutorials/signal-forms/steps/6-next-steps/README.md index 4d27410ac..d700d107d 100644 --- a/adev-ja/src/content/tutorials/signal-forms/steps/6-next-steps/README.md +++ b/adev-ja/src/content/tutorials/signal-forms/steps/6-next-steps/README.md @@ -20,10 +20,9 @@ - **[シグナルフォーム概要](guide/forms/signals/overview)** - シグナルフォームの紹介といつ使用するか - **[フォームモデルガイド](guide/forms/signals/models)** - フォームモデルとデータ管理の詳細 - - +- **[バリデーションガイド](guide/forms/signals/validation)** - 包括的なバリデーションリファレンス +- **[フィールド状態管理ガイド](guide/forms/signals/field-state-management)** - 高度な状態パターン +- **[カスタムコントロールガイド](guide/forms/signals/custom-controls)** - 再利用可能なフォームコンポーネントの構築 ## 学び続ける {#keep-learning} diff --git a/adev-ja/src/content/tutorials/signals/steps/1-creating-your-first-signal/README.en.md b/adev-ja/src/content/tutorials/signals/steps/1-creating-your-first-signal/README.en.md index cd2cf9372..00b9a1523 100644 --- a/adev-ja/src/content/tutorials/signals/steps/1-creating-your-first-signal/README.en.md +++ b/adev-ja/src/content/tutorials/signals/steps/1-creating-your-first-signal/README.en.md @@ -42,7 +42,7 @@ Update the status indicator to display the current user status by: 1. Binding the signal to the class attribute with `[class]="userStatus()"` 2. Displaying the status text by replacing `???` with `{{ userStatus() }}` -```html +```angular-html
@@ -83,12 +83,8 @@ The buttons are already in the template. Now connect them to your methods by add ```html - - + + ``` @@ -110,9 +106,7 @@ The `update()` method takes a function that receives the current value and retur The toggle button is already in the template. Connect it to your `toggleStatus()` method: ```html - + ``` diff --git a/adev-ja/src/content/tutorials/signals/steps/1-creating-your-first-signal/README.md b/adev-ja/src/content/tutorials/signals/steps/1-creating-your-first-signal/README.md index e9c938a3e..f09e61aa0 100644 --- a/adev-ja/src/content/tutorials/signals/steps/1-creating-your-first-signal/README.md +++ b/adev-ja/src/content/tutorials/signals/steps/1-creating-your-first-signal/README.md @@ -42,7 +42,7 @@ export class App { 1. `[class]="userStatus()"`を使用してシグナルをclass属性にバインドする 2. `???`を`{{ userStatus() }}`に置き換えてステータステキストを表示する -```html +```angular-html
@@ -83,12 +83,8 @@ goOffline() { ```html - - + + ``` @@ -110,9 +106,7 @@ toggleStatus() { トグルボタンはすでにテンプレートにあります。それを`toggleStatus()`メソッドに接続します。 ```html - + ``` diff --git a/adev-ja/src/content/tutorials/signals/steps/2-deriving-state-with-computed-signals/README.en.md b/adev-ja/src/content/tutorials/signals/steps/2-deriving-state-with-computed-signals/README.en.md index 8305008d1..82b786717 100644 --- a/adev-ja/src/content/tutorials/signals/steps/2-deriving-state-with-computed-signals/README.en.md +++ b/adev-ja/src/content/tutorials/signals/steps/2-deriving-state-with-computed-signals/README.en.md @@ -37,10 +37,14 @@ Add a computed signal that creates a descriptive message based on the user statu statusMessage = computed(() => { const status = this.userStatus(); switch (status) { - case 'online': return 'Available for meetings and messages'; - case 'away': return 'Temporarily away, will respond soon'; - case 'offline': return 'Not available, check back later'; - default: return 'Status unknown'; + case 'online': + return 'Available for meetings and messages'; + case 'away': + return 'Temporarily away, will respond soon'; + case 'offline': + return 'Not available, check back later'; + default: + return 'Status unknown'; } }); ``` @@ -66,31 +70,31 @@ This demonstrates how computed signals can perform calculations and combine mult The template already has placeholders showing "Loading...". Replace them with your computed signals: -1. For notifications, replace `Loading...` with an @if block: +1. For notifications, replace `Loading...` with an `@if` block: -```angular-html -@if (notificationsEnabled()) { - Enabled -} @else { - Disabled -} -``` + ```angular-html + @if (notificationsEnabled()) { + Enabled + } @else { + Disabled + } + ``` -2. For the message, replace `Loading...` with: +1. For the message, replace `Loading...` with: -```angular-html -{{ statusMessage() }} -``` + ```angular-html + {{ statusMessage() }} + ``` -3. For working hours, replace `Loading...` with an @if block: +1. For working hours, replace `Loading...` with an `@if` block: -```angular-html -@if (isWithinWorkingHours()) { - Yes -} @else { - No -} -``` + ```angular-html + @if (isWithinWorkingHours()) { + Yes + } @else { + No + } + ``` Notice how computed signals are called just like regular signals - with parentheses! diff --git a/adev-ja/src/content/tutorials/signals/steps/2-deriving-state-with-computed-signals/README.md b/adev-ja/src/content/tutorials/signals/steps/2-deriving-state-with-computed-signals/README.md index c012b0878..ca53b2616 100644 --- a/adev-ja/src/content/tutorials/signals/steps/2-deriving-state-with-computed-signals/README.md +++ b/adev-ja/src/content/tutorials/signals/steps/2-deriving-state-with-computed-signals/README.md @@ -37,10 +37,14 @@ notificationsEnabled = computed(() => this.userStatus() === 'online'); statusMessage = computed(() => { const status = this.userStatus(); switch (status) { - case 'online': return 'Available for meetings and messages'; - case 'away': return 'Temporarily away, will respond soon'; - case 'offline': return 'Not available, check back later'; - default: return 'Status unknown'; + case 'online': + return 'Available for meetings and messages'; + case 'away': + return 'Temporarily away, will respond soon'; + case 'offline': + return 'Not available, check back later'; + default: + return 'Status unknown'; } }); ``` @@ -66,31 +70,31 @@ isWithinWorkingHours = computed(() => { テンプレートにはすでに「Loading...」と表示されるプレースホルダーがあります。これらを算出シグナルに置き換えてください。 -1. 通知については、`Loading...`を@ifブロックに置き換えます。 +1. 通知については、`Loading...`を`@if`ブロックに置き換えます。 -```angular-html -@if (notificationsEnabled()) { - Enabled -} @else { - Disabled -} -``` + ```angular-html + @if (notificationsEnabled()) { + Enabled + } @else { + Disabled + } + ``` -2. メッセージについては、`Loading...`を以下に置き換えます。 +1. メッセージについては、`Loading...`を以下に置き換えます。 -```angular-html -{{ statusMessage() }} -``` + ```angular-html + {{ statusMessage() }} + ``` -3. 勤務時間については、`Loading...`を@ifブロックに置き換えます。 +1. 勤務時間については、`Loading...`を`@if`ブロックに置き換えます。 -```angular-html -@if (isWithinWorkingHours()) { - Yes -} @else { - No -} -``` + ```angular-html + @if (isWithinWorkingHours()) { + Yes + } @else { + No + } + ``` 算出シグナルは、通常のシグナルと同様に括弧を付けて呼び出されることに注目してください! diff --git a/adev-ja/src/content/tutorials/signals/steps/4-managing-async-data-with-signals/README.en.md b/adev-ja/src/content/tutorials/signals/steps/4-managing-async-data-with-signals/README.en.md index da3a520b6..9c78d2f94 100644 --- a/adev-ja/src/content/tutorials/signals/steps/4-managing-async-data-with-signals/README.en.md +++ b/adev-ja/src/content/tutorials/signals/steps/4-managing-async-data-with-signals/README.en.md @@ -27,8 +27,8 @@ Add a property in the component class that creates a resource to load user data userId = signal(1); userResource = resource({ - params: () => ({ id: this.userId() }), - loader: (params) => loadUser(params.params.id) + params: () => ({id: this.userId()}), + loader: (params) => loadUser(params.params.id), }); ``` diff --git a/adev-ja/src/content/tutorials/signals/steps/4-managing-async-data-with-signals/README.md b/adev-ja/src/content/tutorials/signals/steps/4-managing-async-data-with-signals/README.md index 463f6a51f..72c0222b2 100644 --- a/adev-ja/src/content/tutorials/signals/steps/4-managing-async-data-with-signals/README.md +++ b/adev-ja/src/content/tutorials/signals/steps/4-managing-async-data-with-signals/README.md @@ -27,8 +27,8 @@ import {loadUser} from './user-api'; userId = signal(1); userResource = resource({ - params: () => ({ id: this.userId() }), - loader: (params) => loadUser(params.params.id) + params: () => ({id: this.userId()}), + loader: (params) => loadUser(params.params.id), }); ``` diff --git a/adev-ja/src/content/tutorials/signals/steps/5-component-communication-with-signals/README.en.md b/adev-ja/src/content/tutorials/signals/steps/5-component-communication-with-signals/README.en.md index 636cac4f2..e532f9e78 100644 --- a/adev-ja/src/content/tutorials/signals/steps/5-component-communication-with-signals/README.en.md +++ b/adev-ja/src/content/tutorials/signals/steps/5-component-communication-with-signals/README.en.md @@ -49,18 +49,10 @@ Update the `product-card` usage in `app.ts` to pass dynamic signal values instea ```html - + - + ``` The square brackets `[]` create property bindings that pass the current signal values to the child. diff --git a/adev-ja/src/content/tutorials/signals/steps/5-component-communication-with-signals/README.md b/adev-ja/src/content/tutorials/signals/steps/5-component-communication-with-signals/README.md index 6e9b08381..86432467b 100644 --- a/adev-ja/src/content/tutorials/signals/steps/5-component-communication-with-signals/README.md +++ b/adev-ja/src/content/tutorials/signals/steps/5-component-communication-with-signals/README.md @@ -49,18 +49,10 @@ available = input(true); ```html - + - + ``` 角括弧`[]`は、現在のシグナル値を子に渡すプロパティバインディングを作成します。 diff --git a/adev-ja/src/content/tutorials/signals/steps/6-two-way-binding-with-model-signals/README.en.md b/adev-ja/src/content/tutorials/signals/steps/6-two-way-binding-with-model-signals/README.en.md index f451bc0ae..6050acb66 100644 --- a/adev-ja/src/content/tutorials/signals/steps/6-two-way-binding-with-model-signals/README.en.md +++ b/adev-ja/src/content/tutorials/signals/steps/6-two-way-binding-with-model-signals/README.en.md @@ -30,10 +30,7 @@ Build the checkbox template that responds to clicks and updates its own model. ```html diff --git a/adev-ja/src/content/tutorials/signals/steps/6-two-way-binding-with-model-signals/README.md b/adev-ja/src/content/tutorials/signals/steps/6-two-way-binding-with-model-signals/README.md index 0d79e6569..d332d7bd6 100644 --- a/adev-ja/src/content/tutorials/signals/steps/6-two-way-binding-with-model-signals/README.md +++ b/adev-ja/src/content/tutorials/signals/steps/6-two-way-binding-with-model-signals/README.md @@ -30,10 +30,7 @@ label = input(''); ```html diff --git a/adev-ja/src/content/tutorials/signals/steps/8-using-signals-with-directives/README.en.md b/adev-ja/src/content/tutorials/signals/steps/8-using-signals-with-directives/README.en.md index 5f6dba3ab..f5e03e542 100644 --- a/adev-ja/src/content/tutorials/signals/steps/8-using-signals-with-directives/README.en.md +++ b/adev-ja/src/content/tutorials/signals/steps/8-using-signals-with-directives/README.en.md @@ -76,7 +76,7 @@ The host bindings automatically re-evaluate when the signals change - just like Update the app template to demonstrate the reactive directive: -```angular-ts +```angular-html template: `

Directive with Signals

diff --git a/adev-ja/src/content/tutorials/signals/steps/8-using-signals-with-directives/README.md b/adev-ja/src/content/tutorials/signals/steps/8-using-signals-with-directives/README.md index b37348483..b1634f096 100644 --- a/adev-ja/src/content/tutorials/signals/steps/8-using-signals-with-directives/README.md +++ b/adev-ja/src/content/tutorials/signals/steps/8-using-signals-with-directives/README.md @@ -76,7 +76,7 @@ export class HighlightDirective { リアクティブなディレクティブをデモンストレーションするために、アプリのテンプレートを更新します。 -```angular-ts +```angular-html template: `

Directive with Signals

From 86dfe6eda937abb78f3ba89d8db06da20b1dcf9a Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 15:35:25 +0900 Subject: [PATCH 29/32] chore: migrate source code file changes - Sync update.component.html formatting (spacing in interpolation, conditions) - Update sub-navigation-data.ts with new migration entry and reorder zone-js-testing - Sync index.html formatting changes --- .../features/update/update.component.en.html | 24 ++++++++++--------- .../app/features/update/update.component.html | 24 ++++++++++--------- .../src/app/routing/sub-navigation-data.en.ts | 15 ++++++++---- .../src/app/routing/sub-navigation-data.ts | 15 ++++++++---- adev-ja/src/index.en.html | 6 ++--- adev-ja/src/index.html | 6 ++--- 6 files changed, 50 insertions(+), 40 deletions(-) diff --git a/adev-ja/src/app/features/update/update.component.en.html b/adev-ja/src/app/features/update/update.component.en.html index 13d9667e5..5ebb3dcbb 100644 --- a/adev-ja/src/app/features/update/update.component.en.html +++ b/adev-ja/src/app/features/update/update.component.en.html @@ -19,7 +19,7 @@

Angular versions

    @for (version of versions; track $index) {
  • -
  • @@ -40,7 +40,7 @@

    Angular versions

      @for (version of versions; track $index) {
    • -
    • @@ -70,7 +70,7 @@

      Angular versions

} - @if ((to.number - from.number > 150) && from.number > 240) { + @if (to.number - from.number > 150 && from.number > 240) {

Warning: @@ -86,7 +86,7 @@

Application complexity

Basic Medium @@ -106,7 +106,7 @@

Other dependencies

I use {{option.name}} {{option.description}}I use {{ option.name }} {{ option.description }}
} @@ -137,19 +137,21 @@

Package Manager

@if ( - beforeRecommendations.length > 0 || duringRecommendations.length > 0 || afterRecommendations.length > 0 + beforeRecommendations.length > 0 || + duringRecommendations.length > 0 || + afterRecommendations.length > 0 ) {
-

{{title()}}

+

{{ title() }}

Before you update

- @for (r of beforeRecommendations; track $index) { + @for (r of beforeRecommendations; track $index) {
- {{getComplexityLevelName(r.level)}} + {{ getComplexityLevelName(r.level) }}
@@ -173,7 +175,7 @@

Update to the new version

- {{getComplexityLevelName(r.level)}} + {{ getComplexityLevelName(r.level) }}
@@ -191,7 +193,7 @@

After you update

- {{getComplexityLevelName(r.level)}} + {{ getComplexityLevelName(r.level) }}
diff --git a/adev-ja/src/app/features/update/update.component.html b/adev-ja/src/app/features/update/update.component.html index 1d7b1257a..1aa79160a 100644 --- a/adev-ja/src/app/features/update/update.component.html +++ b/adev-ja/src/app/features/update/update.component.html @@ -19,7 +19,7 @@

Angularバージョン

    @for (version of versions; track $index) {
  • -
  • @@ -40,7 +40,7 @@

    Angularバージョン

      @for (version of versions; track $index) {
    • -
    • @@ -70,7 +70,7 @@

      Angularバージョン

} - @if ((to.number - from.number > 150) && from.number > 240) { + @if (to.number - from.number > 150 && from.number > 240) {

警告: @@ -86,7 +86,7 @@

アプリケーションの複雑さ

基本 中級 @@ -106,7 +106,7 @@

その他の依存関係

{{option.name}} {{option.description}}を使用しています{{ option.name }} {{ option.description }}を使用しています
} @@ -137,19 +137,21 @@

パッケージマネージャー

@if ( - beforeRecommendations.length > 0 || duringRecommendations.length > 0 || afterRecommendations.length > 0 + beforeRecommendations.length > 0 || + duringRecommendations.length > 0 || + afterRecommendations.length > 0 ) {
-

{{title()}}

+

{{ title() }}

アップデート前

- @for (r of beforeRecommendations; track $index) { + @for (r of beforeRecommendations; track $index) {
- {{getComplexityLevelName(r.level)}} + {{ getComplexityLevelName(r.level) }}
@@ -173,7 +175,7 @@

新しいバージョンにアップデートする

- {{getComplexityLevelName(r.level)}} + {{ getComplexityLevelName(r.level) }}
@@ -191,7 +193,7 @@

アップデート後

- {{getComplexityLevelName(r.level)}} + {{ getComplexityLevelName(r.level) }}
diff --git a/adev-ja/src/app/routing/sub-navigation-data.en.ts b/adev-ja/src/app/routing/sub-navigation-data.en.ts index a5f0cdd05..2ebf4da2c 100644 --- a/adev-ja/src/app/routing/sub-navigation-data.en.ts +++ b/adev-ja/src/app/routing/sub-navigation-data.en.ts @@ -475,6 +475,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'guide/forms/signals/comparison', contentPath: 'guide/forms/signals/comparison', }, + { + label: 'Migrating from Legacy Forms', + path: 'guide/forms/signals/migration', + contentPath: 'guide/forms/signals/migration', + }, ], }, { @@ -618,11 +623,6 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'guide/testing/utility-apis', contentPath: 'guide/testing/utility-apis', }, - { - label: 'Zone.js Testing Utilities', - path: 'guide/testing/zone-js-testing-utilities', - contentPath: 'guide/testing/zone-js-testing-utilities', - }, { label: 'Component harnesses overview', path: 'guide/testing/component-harnesses-overview', @@ -653,6 +653,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'guide/testing/karma', contentPath: 'guide/testing/karma', }, + { + label: 'Zone.js Testing Utilities', + path: 'guide/testing/zone-js-testing-utilities', + contentPath: 'guide/testing/zone-js-testing-utilities', + }, ], }, { diff --git a/adev-ja/src/app/routing/sub-navigation-data.ts b/adev-ja/src/app/routing/sub-navigation-data.ts index a08ba4d01..f45ec5202 100644 --- a/adev-ja/src/app/routing/sub-navigation-data.ts +++ b/adev-ja/src/app/routing/sub-navigation-data.ts @@ -475,6 +475,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'guide/forms/signals/comparison', contentPath: 'guide/forms/signals/comparison', }, + { + label: 'Migrating from Legacy Forms', + path: 'guide/forms/signals/migration', + contentPath: 'guide/forms/signals/migration', + }, ], }, { @@ -618,11 +623,6 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'guide/testing/utility-apis', contentPath: 'guide/testing/utility-apis', }, - { - label: 'Zone.jsテストユーティリティ', - path: 'guide/testing/zone-js-testing-utilities', - contentPath: 'guide/testing/zone-js-testing-utilities', - }, { label: 'コンポーネントハーネスの概要', path: 'guide/testing/component-harnesses-overview', @@ -653,6 +653,11 @@ const DOCS_SUB_NAVIGATION_DATA: NavigationItem[] = [ path: 'guide/testing/karma', contentPath: 'guide/testing/karma', }, + { + label: 'Zone.jsテストユーティリティ', + path: 'guide/testing/zone-js-testing-utilities', + contentPath: 'guide/testing/zone-js-testing-utilities', + }, ], }, { diff --git a/adev-ja/src/index.en.html b/adev-ja/src/index.en.html index c000bd4d2..ef4f4f69d 100644 --- a/adev-ja/src/index.en.html +++ b/adev-ja/src/index.en.html @@ -11,10 +11,8 @@ const LIGHT_MODE_CLASS_NAME = 'docs-light-mode'; const PREFERS_COLOR_SCHEME_DARK = '(prefers-color-scheme: dark)'; - const theme = localStorage.getItem(THEME_PREFERENCE_LOCAL_STORAGE_KEY) ?? 'auto'; - const prefersDark = - window.matchMedia && window.matchMedia(PREFERS_COLOR_SCHEME_DARK).matches; + const prefersDark = window.matchMedia && window.matchMedia(PREFERS_COLOR_SCHEME_DARK).matches; const documentClassList = this.document.documentElement.classList; // clearing classes before setting them. @@ -25,7 +23,7 @@ documentClassList.add(LIGHT_MODE_CLASS_NAME); } - if(location.search.includes('uwu')) { + if (location.search.includes('uwu')) { documentClassList.add('uwu'); } diff --git a/adev-ja/src/index.html b/adev-ja/src/index.html index d15782103..adb45ec14 100644 --- a/adev-ja/src/index.html +++ b/adev-ja/src/index.html @@ -11,10 +11,8 @@ const LIGHT_MODE_CLASS_NAME = 'docs-light-mode'; const PREFERS_COLOR_SCHEME_DARK = '(prefers-color-scheme: dark)'; - const theme = localStorage.getItem(THEME_PREFERENCE_LOCAL_STORAGE_KEY) ?? 'auto'; - const prefersDark = - window.matchMedia && window.matchMedia(PREFERS_COLOR_SCHEME_DARK).matches; + const prefersDark = window.matchMedia && window.matchMedia(PREFERS_COLOR_SCHEME_DARK).matches; const documentClassList = this.document.documentElement.classList; // clearing classes before setting them. @@ -25,7 +23,7 @@ documentClassList.add(LIGHT_MODE_CLASS_NAME); } - if(location.search.includes('uwu')) { + if (location.search.includes('uwu')) { documentClassList.add('uwu'); } From 6f3ff54d5746034d5033158c696d98bf2ad8a2e6 Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 17:25:27 +0900 Subject: [PATCH 30/32] docs(testing): translate components-scenarios.md to Japanese Translate the component testing scenarios guide from English to Japanese. This file was newly added in the upstream update. --- .../guide/testing/components-scenarios.en.md | 1118 +++++++++---- .../guide/testing/components-scenarios.md | 1426 +++++++++++------ 2 files changed, 1744 insertions(+), 800 deletions(-) diff --git a/adev-ja/src/content/guide/testing/components-scenarios.en.md b/adev-ja/src/content/guide/testing/components-scenarios.en.md index 8cc4023f0..a2699bd4b 100644 --- a/adev-ja/src/content/guide/testing/components-scenarios.en.md +++ b/adev-ja/src/content/guide/testing/components-scenarios.en.md @@ -4,11 +4,22 @@ This guide explores common component testing use cases. ## Component binding -In the example application, the `BannerComponent` presents static title text in the HTML template. +In the example application, the `Banner` component presents static title text in the HTML template. -After a few changes, the `BannerComponent` presents a dynamic title by binding to the component's `title` property like this. +After a few changes, the `Banner` component presents a dynamic title by binding to the component's `title` property like this. - +```angular-ts {header="banner.ts"} +import {Component, signal} from '@angular/core'; + +@Component({ + selector: 'app-banner', + template: '

{{ title() }}

', + styles: ['h1 { color: green; font-size: 350%}'], +}) +export class Banner { + title = signal('Test Tour of Heroes'); +} +``` As minimal as this is, you decide to add a test to confirm that component actually displays the right content where you think it should. @@ -18,75 +29,86 @@ You'll write a sequence of tests that inspect the value of the `

` element th You update the `beforeEach` to find that element with a standard HTML `querySelector` and assign it to the `h1` variable. - +```ts {header: "banner.component.spec.ts"} +let component: Banner; +let fixture: ComponentFixture; +let h1: HTMLElement; + +beforeEach(() => { + fixture = TestBed.createComponent(Banner); + component = fixture.componentInstance; // Banner test instance + h1 = fixture.nativeElement.querySelector('h1'); +}); +``` ### `createComponent()` does not bind data For your first test you'd like to see that the screen displays the default `title`. Your instinct is to write a test that immediately inspects the `

` like this: - +```ts +it('should display original title', () => { + expect(h1.textContent).toContain(component.title()); +}); +``` _That test fails_ with the message: - - +```shell {hideCopy} expected '' to contain 'Test Tour of Heroes'. - - +``` Binding happens when Angular performs **change detection**. In production, change detection kicks in automatically when Angular creates a component or the user enters a keystroke, for example. -The `TestBed.createComponent` does not trigger change detection by default; a fact confirmed in the revised test: +The `TestBed.createComponent` does not trigger change detection synchronously; a fact confirmed in the revised test: - +```ts +it('no title in the DOM after createComponent()', () => { + expect(h1.textContent).toEqual(''); +}); +``` -### `detectChanges()` +### `whenStable()` -You can tell the `TestBed` to perform data binding by calling `fixture.detectChanges()`. +You can tell the `TestBed` to wait for change detection to run with `await fixture.whenStable()`. Only then does the `

` have the expected title. - +```ts +it('should display original title', async () => { + await fixture.whenStable(); + expect(h1.textContent).toContain(component.title()); +}); +``` Delayed change detection is intentional and useful. It gives the tester an opportunity to inspect and change the state of the component _before Angular initiates data binding and calls [lifecycle hooks](guide/components/lifecycle)_. -Here's another test that changes the component's `title` property _before_ calling `fixture.detectChanges()`. +Here's another test that changes the component's `title` property _before_ calling `fixture.whenStable()`. - - -### Automatic change detection - -The `BannerComponent` tests frequently call `detectChanges`. -Many testers prefer that the Angular test environment run change detection automatically like it does in production. - -That's possible by configuring the `TestBed` with the `ComponentFixtureAutoDetect` provider. -First import it from the testing utility library: - - - -Then add it to the `providers` array of the testing module configuration: - - - -HELPFUL: You can also use the `fixture.autoDetectChanges()` function instead if you only want to enable automatic change detection -after making updates to the state of the fixture's component. In addition, automatic change detection is on by default -when using `provideZonelessChangeDetection` and turning it off is not recommended. - -Here are three tests that illustrate how automatic change detection works. - - +```ts +it('should display a different test title', async () => { + component.title.set('Test Title'); + await fixture.whenStable(); + expect(h1.textContent).toContain('Test Title'); +}); +``` -The first test shows the benefit of automatic change detection. +### Binding signals to inputs -The second and third test reveal an important limitation. -The Angular testing environment does not run change detection synchronously when updates happen inside the test case that changed the component's `title`. -The test must call `await fixture.whenStable` to wait for another round of change detection. +To reflect changes to inputs and listen to outputs you can dynamically bind signals to inputs and functions to outputs. -HELPFUL: Angular does not know about direct updates to values that are not signals. The easiest way to ensure that -change detection will be scheduled is to use signals for values read in the template. +```ts +import {inputBinding, outputBinding} from '@angular/core'; + +const fixture = TestBed.createComponent(ValueDisplay, { + bindings: [ + inputBinding('value', value), + outputBinding('valueChange', () => (/* ... */) ), + ], +}); +``` ### Change an input value with `dispatchEvent()` @@ -97,59 +119,63 @@ But there is an essential, intermediate step. Angular doesn't know that you set the input element's `value` property. It won't read that property until you raise the element's `input` event by calling `dispatchEvent()`. -The following example demonstrates the proper sequence. +The following example of a component using the `TitleCasePipe` demonstrates the proper sequence. - - -## Component with external files - -The preceding `BannerComponent` is defined with an _inline template_ and _inline css_, specified in the `@Component.template` and `@Component.styles` properties respectively. - -Many components specify _external templates_ and _external css_ with the `@Component.templateUrl` and `@Component.styleUrls` properties respectively, as the following variant of `BannerComponent` does. - - - -This syntax tells the Angular compiler to read the external files during component compilation. +```ts +it('should convert hero name to Title Case', async () => { + const hostElement = fixture.nativeElement; + const nameInput: HTMLInputElement = hostElement.querySelector('input')!; + const nameDisplay: HTMLElement = hostElement.querySelector('span')!; -That's not a problem when you run the CLI `ng test` command because it _compiles the application before running the tests_. + // simulate user entering a new name into the input box + nameInput.value = 'quick BROWN fOx'; -However, if you run the tests in a **non-CLI environment**, tests of this component might fail. -For example, if you run the `BannerComponent` tests in a web coding environment such as [plunker](https://plnkr.co), you'll see a message like this one: + // Dispatch a DOM event so that Angular learns of input value change. + nameInput.dispatchEvent(new Event('input')); -```shell {hideCopy} - -Error: This test module uses the component BannerComponent -which is using a "templateUrl" or "styleUrls", but they were never compiled. -Please call "TestBed.compileComponents" before your test. + // Wait for Angular to update the display binding through the title pipe + await fixture.whenStable(); + expect(nameDisplay.textContent).toBe('Quick Brown Fox'); +}); ``` -You get this test failure message when the runtime environment compiles the source code _during the tests themselves_. - -To correct the problem, call `compileComponents()`. - ## Component with a dependency Components often have service dependencies. -The `WelcomeComponent` displays a welcome message to the logged-in user. -It knows who the user is based on a property of the injected `UserService`: - - +The `Welcome` component displays a welcome message to the logged-in user. +It knows who the user is based on a property of the injected `UserAuthentication`: + +```angular-ts +import {Component, inject, OnInit, signal} from '@angular/core'; +import {UserAuthentication} from '../model/user.authentication'; + +@Component({ + selector: 'app-welcome', + template: '

{{ welcome() }}

', +}) +export class Welcome { + private userAuth = inject(UserAuthentication); + welcome = signal( + this.userAuth.isLoggedIn() ? `Welcome, ${this.userAuth.user().name}` : 'Please log in.', + ); +} +``` -The `WelcomeComponent` has decision logic that interacts with the service, logic that makes this component worth testing. +The `Welcome` component has decision logic that interacts with the service, logic that makes this component worth testing. ### Provide service test doubles A _component-under-test_ doesn't have to be provided with real services. -Injecting the real `UserService` could be difficult. +Injecting the real `UserAuthentication` could be difficult. The real service might ask the user for login credentials and attempt to reach an authentication server. These behaviors can be hard to intercept. Be aware that using test doubles makes the test behave differently from production so use them sparingly. ### Get injected services -The tests need access to the `UserService` injected into the `WelcomeComponent`. +The tests need access to the `UserAuthentication` injected into the `Welcome` component. Angular has a hierarchical injection system. There can be injectors at multiple levels, from the root injector created by the `TestBed` down through the component tree. @@ -158,7 +184,10 @@ The safest way to get the injected service, the way that **_always works_**, is to **get it from the injector of the _component-under-test_**. The component injector is a property of the fixture's `DebugElement`. - +```ts +// UserAuthentication actually injected into the component +userAuth = fixture.debugElement.injector.get(UserAuthentication); +``` HELPFUL: This is _usually_ not necessary. Services are often provided in the root or the TestBed overrides and can be retrieved more easily with `TestBed.inject()` (see below). @@ -166,9 +195,11 @@ HELPFUL: This is _usually_ not necessary. Services are often provided in the roo This is easier to remember and less verbose than retrieving a service using the fixture's `DebugElement`. -In this test suite, the _only_ provider of `UserService` is the root testing module, so it is safe to call `TestBed.inject()` as follows: +In this test suite, the _only_ provider of `UserAuthentication` is the root testing module, so it is safe to call `TestBed.inject()` as follows: - +```ts +userAuth = TestBed.inject(UserAuthentication); +``` HELPFUL: For a use case in which `TestBed.inject()` does not work, see the [_Override component providers_](#override-component-providers) section that explains when and why you must get the service from the component's injector instead. @@ -176,16 +207,56 @@ HELPFUL: For a use case in which `TestBed.inject()` does not work, see the [_Ove Here's the complete `beforeEach()`, using `TestBed.inject()`: - +```ts +let fixture: ComponentFixture; +let comp: Welcome; +let userAuth: UserAuthentication; // the TestBed injected service +let el: HTMLElement; // the DOM element with the welcome message + +beforeEach(() => { + fixture = TestBed.createComponent(Welcome); + comp = fixture.componentInstance; + + // UserAuthentication from the root injector + userAuth = TestBed.inject(UserAuthentication); + + // get the "welcome" element by CSS selector (e.g., by class name) + el = fixture.nativeElement.querySelector('.welcome'); +}); +``` And here are some tests: - +```ts +it('should welcome the user', async () => { + await fixture.whenStable(); + const content = el.textContent; + + expect(content, '"Welcome ..."').toContain('Welcome'); + expect(content, 'expected name').toContain('Test User'); +}); + +it('should welcome "Bubba"', async () => { + userAuth.user.set({name: 'Bubba'}); // welcome message hasn't been shown yet + await fixture.whenStable(); + + expect(el.textContent).toContain('Bubba'); +}); + +it('should request login if not logged in', async () => { + userAuth.isLoggedIn.set(false); // welcome message hasn't been shown yet + await fixture.whenStable(); + const content = el.textContent; -The first is a sanity test; it confirms that the `UserService` is called and working. + expect(content, 'not welcomed').not.toContain('Welcome'); + expect(content, '"log in"').toMatch(/log in/i); +}); +``` + +The first is a sanity test; it confirms that the `UserAuthentication` is called and working. -HELPFUL: The withContext function \(for example, `'expected name'`\) is an optional failure label. -If the expectation fails, Jasmine appends this label to the expectation failure message. +HELPFUL: The 2nd argument of `expect` \(for example, `'expected name'`\) is an optional failure label. +If the expectation fails, Vitest appends this label to the expectation failure message. In a spec with multiple expectations, it can help clarify what went wrong and which expectation failed. The remaining tests confirm the logic of the component when the service returns different values. @@ -194,216 +265,188 @@ The third test checks that the component displays the proper message when there ## Component with async service -In this sample, the `AboutComponent` template hosts a `TwainComponent`. -The `TwainComponent` displays Mark Twain quotes. - - +In this sample, the `About` component template hosts a `Twain` component. +The `Twain` component displays Mark Twain quotes. + +```angular-html +

+ {{ quote | async }} +

+ +@if (errorMessage()) { +

{{ errorMessage() }}

+} +``` HELPFUL: The value of the component's `quote` property passes through an `AsyncPipe`. That means the property returns either a `Promise` or an `Observable`. -In this example, the `TwainComponent.getQuote()` method tells you that the `quote` property returns an `Observable`. +In this example, the `TwainQuotes.getQuote()` method tells you that the `quote` property returns an `Observable`. - +```ts +getQuote() { + this.errorMessage.set(''); + this.quote = this.twainQuotes.getQuote().pipe( + startWith('...'), + catchError((err: any) => { + this.errorMessage.set(err.message || err.toString()); + return of('...'); // reset message to placeholder + }), + ); +} +``` -The `TwainComponent` gets quotes from an injected `TwainService`. +The `Twain` component gets quotes from an injected `TwainQuotes`. The component starts the returned `Observable` with a placeholder value \(`'...'`\), before the service can return its first quote. The `catchError` intercepts service errors, prepares an error message, and returns the placeholder value on the success channel. These are all features you'll want to test. -### Testing with a spy +### Testing by mocking http requests with the `HttpTestingController`. When testing a component, only the service's public API should matter. In general, tests themselves should not make calls to remote servers. They should emulate such calls. -The setup in this `app/twain/twain.component.spec.ts` shows one way to do that: - - - -Focus on the spy. - - - -The spy is designed such that any call to `getQuote` receives an observable with a test quote. -Unlike the real `getQuote()` method, this spy bypasses the server and returns a synchronous observable whose value is available immediately. - -You can write many useful tests with this spy, even though its `Observable` is synchronous. - -HELPFUL: It is best to limit the usage of spies to only what is necessary for the test. Creating mocks or spies for more than what's necessary can be brittle. As the component and injectable evolves, the unrelated tests can fail because they no longer mock enough behaviors that would otherwise not affect the test. - -### Async test with `fakeAsync()` - -To use `fakeAsync()` functionality, you must import `zone.js/testing` in your test setup file. -If you created your project with the Angular CLI, `zone-testing` is already imported in `src/test.ts`. - -The following test confirms the expected behavior when the service returns an `ErrorObservable`. - - - -HELPFUL: The `it()` function receives an argument of the following form. - - - -fakeAsync(() => { /_test body_/ }) - - - -The `fakeAsync()` function enables a linear coding style by running the test body in a special `fakeAsync test zone`. -The test body appears to be synchronous. -There is no nested syntax \(like a `Promise.then()`\) to disrupt the flow of control. - -HELPFUL: Limitation: The `fakeAsync()` function won't work if the test body makes an `XMLHttpRequest` \(XHR\) call. -XHR calls within a test are rare, but if you need to call XHR, use `waitForAsync()`. - -IMPORTANT: Be aware that asynchronous tasks that happen inside the `fakeAsync` zone need to be manually executed with `flush` or `tick`. If you attempt to -wait for them to complete (i.e. using `fixture.whenStable`) without using the -`fakeAsync` test helpers to advance time, your test will likely fail. See below for more information. - -### The `tick()` function - -You do have to call [tick()](api/core/testing/tick) to advance the virtual clock. - -Calling [tick()](api/core/testing/tick) simulates the passage of time until all pending asynchronous activities finish. -In this case, it waits for the observable's `setTimeout()`. -The [tick()](api/core/testing/tick) function accepts `millis` and `tickOptions` as parameters. The `millis` parameter specifies how much the virtual clock advances and defaults to `0` if not provided. -For example, if you have a `setTimeout(fn, 100)` in a `fakeAsync()` test, you need to use `tick(100)` to trigger the fn callback. -The optional `tickOptions` parameter has a property named `processNewMacroTasksSynchronously`. The `processNewMacroTasksSynchronously` property represents whether to invoke new generated macro tasks when ticking and defaults to `true`. +In the case your async service relies on the `HttpClient` to load remote data, it is recommended to return mock responses at the HTTP level with the `HttpTestingController`. - +For more details on mocking the `HttpBackend`, refer to the [dedicated guide](guide/http/testing). -The [tick()](api/core/testing/tick) function is one of the Angular testing utilities that you import with `TestBed`. -It's a companion to `fakeAsync()` and you can only call it within a `fakeAsync()` body. +### Testing by providing a stubbed implementation of a service. -### tickOptions +When mocking async request at the http level isn't possible, an alternative is to leverage spies. -In this example, you have a new macro task, the nested `setTimeout` function. By default, when the `tick` is setTimeout, `outside` and `nested` will both be triggered. +The setup in this `app/twain/twain-quotes.spec.ts` shows one way to do that: - +```ts {header: "twain.spec.ts"} +class TwainQuotesStub implements TwainQuotes { + private testQuote = 'Test Quote'; -In some case, you don't want to trigger the new macro task when ticking. You can use `tick(millis, {processNewMacroTasksSynchronously: false})` to not invoke a new macro task. + getQuote() { + return of(this.testQuote); + } - + // ... Implement everything to conform to the API +} -### Comparing dates inside fakeAsync() +beforeEach(async () => { + TestBed.configureTestingModule({ + providers: [{provide: TwainQuotes, useClass: TwainQuotesStub}], + }); -`fakeAsync()` simulates passage of time, which lets you calculate the difference between dates inside `fakeAsync()`. - - - -### jasmine.clock with fakeAsync() - -Jasmine also provides a `clock` feature to mock dates. -Angular automatically runs tests that are run after `jasmine.clock().install()` is called inside a `fakeAsync()` method until `jasmine.clock().uninstall()` is called. -`fakeAsync()` is not needed and throws an error if nested. - -By default, this feature is disabled. -To enable it, set a global flag before importing `zone-testing`. + fixture = TestBed.createComponent(Twain); + component = fixture.componentInstance; + await fixture.whenStable(); + quoteEl = fixture.nativeElement.querySelector('.twain'); +}); +``` -If you use the Angular CLI, configure this flag in `src/test.ts`. +Focus on the how the stub implementation replaces the original one. ```ts -[window as any]('__zone_symbol__fakeAsyncPatchLock') = true; -import 'zone.js/testing'; +TestBed.configureTestingModule({ + providers: [{provide: TwainQuotes, useClass: TwainQuotesStub}], +}); ``` - - -### Using the RxJS scheduler inside fakeAsync() - -You can also use RxJS scheduler in `fakeAsync()` just like using `setTimeout()` or `setInterval()`, but you need to import `zone.js/plugins/zone-patch-rxjs-fake-async` to patch RxJS scheduler. - - - -### Support more macroTasks - -By default, `fakeAsync()` supports the following macro tasks. - -- `setTimeout` -- `setInterval` -- `requestAnimationFrame` -- `webkitRequestAnimationFrame` -- `mozRequestAnimationFrame` - -If you run other macro tasks such as `HTMLCanvasElement.toBlob()`, an _"Unknown macroTask scheduled in fake async test"_ error is thrown. - - - - - +The stub is designed in such a way that any component or service that injects it will receive the stubbed implementation. +It means that any call to `getQuote` receives an observable with a test quote. -If you want to support such a case, you need to define the macro task you want to support in `beforeEach()`. -For example: - - - -HELPFUL: In order to make the `` element Zone.js-aware in your app, you need to import the `zone-patch-canvas` patch \(either in `polyfills.ts` or in the specific file that uses ``\): - - - -### Async observables +Unlike the real `getQuote()` method, this spy bypasses the server and returns a synchronous observable whose value is available immediately. -You might be satisfied with the test coverage of these tests. +### Async test with a Vitest fake timers -However, you might be troubled by the fact that the real service doesn't quite behave this way. -The real service sends requests to a remote server. -A server takes time to respond and the response certainly won't be available immediately as in the previous two tests. +To mock async functions like `setTimeout` or `Promise`s, you can leverage Vitest fake timers to controle whenever the fire. -Your tests will reflect the real world more faithfully if you return an _asynchronous_ observable from the `getQuote()` spy like this. +```ts +it('should display error when TwainQuotes service fails', async () => { + class TwainQuotesStub implements TwainQuotes { + getQuote() { + return defer(() => { + return new Promise((_, reject) => { + setTimeout(() => reject('TwainService test failure')); + }); + }); + } + + // ... Implement everything to conform to the API + } + + TestBed.configureTestingModule({ + providers: [{provide: TwainQuotes, useClass: TwainQuotesStub}], + }); + + vi.useFakeTimers(); // setting up the fake timers + const fixture = TestBed.createComponent(TwainComponent); + + // rendering isn't async, we need to flush + await vi.runAllTimersAsync(); + + await expect(fixture.nativeElement.querySelector('.error')!.textContent).toMatch(/test failure/); + expect(fixture.nativeElement.querySelector('.twain')!.textContent).toBe('...'); + + vi.useRealTimers(); // resets to regular async execution +}); +``` - +### More async tests -### Async observable helpers +With the stubbe service returning async observables, most of your tests will have to be async as well. -The async observable was produced by an `asyncData` helper. -The `asyncData` helper is a utility function that you'll have to write yourself, or copy this one from the sample code. +Here's a test that demonstrates the data flow you'd expect in the real world. - +```ts +it('should show quote after getQuote', async () => { + class MockTwainQuotes implements TwainQuotes { + private subject = new Subject(); -This helper's observable emits the `data` value in the next turn of the JavaScript engine. + getQuote() { + return this.subject.asObservable(); + } -The [RxJS `defer()` operator](http://reactivex.io/documentation/operators/defer.html) returns an observable. -It takes a factory function that returns either a promise or an observable. -When something subscribes to _defer_'s observable, it adds the subscriber to a new observable created with that factory. + emit(val: string) { + this.subject.next(val); + } + } -The `defer()` operator transforms the `Promise.resolve()` into a new observable that, like `HttpClient`, emits once and completes. -Subscribers are unsubscribed after they receive the data value. + it('should show quote after getQuote (success)', async () => { + vi.useFakeTimers(); -There's a similar helper for producing an async error. + TestBed.configureTestingModule({ + providers: [{provide: TwainQuotes, useClass: MockTwainQuotes}], + }); - + const fixture = TestBed.createComponent(TwainComponent); + const twainQuotes = TestBed.inject(TwainQuotes) as MockTwainQuotes; + await vi.runAllTimersAsync(); // render before the quote is recieved -### More async tests + const quoteEl = fixture.nativeElement.querySelector('.twain'); + expect(quoteEl.textContent).toBe('...'); -Now that the `getQuote()` spy is returning async observables, most of your tests will have to be async as well. + twainQuotes.emit('Twain Quote'); // emits the quote + await vi.runAllTimersAsync(); // render with the quote received -Here's a `fakeAsync()` test that demonstrates the data flow you'd expect in the real world. + expect(quoteEl.textContent).toBe('Twain Quote'); + expect(fixture.nativeElement.querySelector('.error')).toBeNull(); - + vi.useRealTimers(); + }); +}); +``` -Notice that the quote element displays the placeholder value \(`'...'`\) after `ngOnInit()`. +Notice that the quote element displays the placeholder value \(`'...'`\) on first rendering. The first quote hasn't arrived yet. -To flush the first quote from the observable, you call [tick()](api/core/testing/tick). -Then call `detectChanges()` to tell Angular to update the screen. - Then you can assert that the quote element displays the expected text. -### Async test without `fakeAsync()` - -Here's the previous `fakeAsync()` test, re-written with the `async`. +### Async tests with `zone.js` and `fakeAsync` - +The `fakeAsync` helper function is another mock clock that relies on patching asynchronous APIs with `zone.js`. It was commonly used in `zone.js` based applications for testing. The use of `fakeAsync` is no longer recommended. -### `whenStable` +TIP: Prefer using native async testing strategies or other fake timers (also called mock clocks) like those from Vitest or Jasmine. -The test must wait for the `getQuote()` observable to emit the next quote. -Instead of calling [tick()](api/core/testing/tick), it calls `fixture.whenStable()`. - -The `fixture.whenStable()` returns a promise that resolves when the JavaScript engine's task queue becomes empty. -In this example, the task queue becomes empty when the observable emits the first quote. +IMPORTANT: `fakeAsync` cannot be used with the Vitest test runner as no `zone.js` patch is applied for this runner. ## Component with inputs and outputs @@ -413,48 +456,107 @@ The host uses a property binding to set the input property and an event binding The testing goal is to verify that such bindings work as expected. The tests should set input values and listen for output events. -The `DashboardHeroComponent` is a tiny example of a component in this role. -It displays an individual hero provided by the `DashboardComponent`. -Clicking that hero tells the `DashboardComponent` that the user has selected the hero. +The `DashboardHero` component is a tiny example of a component in this role. +It displays an individual hero provided by the `Dashboard` component. +Clicking that hero tells the `Dashboard` component that the user has selected the hero. -The `DashboardHeroComponent` is embedded in the `DashboardComponent` template like this: +The `DashboardHero` component is embedded in the `Dashboard` component template like this: - +```angular-html +@for (hero of heroes; track hero) { + +} +``` -The `DashboardHeroComponent` appears in an `@for` block, which sets each component's `hero` input property to the looping value and listens for the component's `selected` event. +The `DashboardHero` component appears in an `@for` block, which sets each component's `hero` input property to the looping value and listens for the component's `selected` event. Here's the component's full definition: - +```angular-ts +@Component({ + selector: 'dashboard-hero', + imports: [UpperCasePipe], + template: ` + + `, +}) +export class DashboardHero { + readonly hero = input.required(); + readonly selected = output(); + + click() { + this.selected.emit(this.hero()); + } +} +``` While testing a component this simple has little intrinsic value, it's worth knowing how. Use one of these approaches: -- Test it as used by `DashboardComponent` +- Test it as used by the `Dashboard` component - Test it as a standalone component -- Test it as used by a substitute for `DashboardComponent` +- Test it as used by a substitute for the `Dashboard` component -The immediate goal is to test the `DashboardHeroComponent`, not the `DashboardComponent`, so, try the second and third options. +The immediate goal is to test the `DashboardHero` component, not the `Dashboard` component, so, try the second and third options. -### Test `DashboardHeroComponent` standalone +### Test the `DashboardHero` component standalone Here's the meat of the spec file setup. - +```ts +let fixture: ComponentFixture; +let comp: DashboardHero; +let heroDe: DebugElement; +let heroEl: HTMLElement; +let expectedHero: Hero; + +beforeEach(async () => { + fixture = TestBed.createComponent(DashboardHero); + comp = fixture.componentInstance; + + // find the hero's DebugElement and element + heroDe = fixture.debugElement.query(By.css('.hero')); + heroEl = heroDe.nativeElement; + + // mock the hero supplied by the parent component + expectedHero = {id: 42, name: 'Test Name'}; -Notice how the setup code assigns a test hero \(`expectedHero`\) to the component's `hero` property, emulating the way the `DashboardComponent` would set it using the property binding in its repeater. + // simulate the parent setting the input property with that hero + fixture.componentRef.setInput('hero', expectedHero); + + // wait for initial data binding + await fixture.whenStable(); +}); +``` + +Notice how the setup code assigns a test hero \(`expectedHero`\) to the component's `hero` property, emulating the way the `Dashboard` would set it using the property binding in its repeater. The following test verifies that the hero name is propagated to the template using a binding. - +```ts +it('should display hero name in uppercase', () => { + const expectedPipedName = expectedHero.name.toUpperCase(); + expect(heroEl.textContent).toContain(expectedPipedName); +}); +``` Because the template passes the hero name through the Angular `UpperCasePipe`, the test must match the element value with the upper-cased name. ### Clicking -Clicking the hero should raise a `selected` event that the host component \(`DashboardComponent` presumably\) can hear: +Clicking the hero should raise a `selected` event that the host component \(`Dashboard` presumably\) can hear: - +```ts +it('should raise selected event when clicked (triggerEventHandler)', () => { + let selectedHero: Hero | undefined; + comp.selected.subscribe((hero: Hero) => (selectedHero = hero)); + + heroDe.triggerEventHandler('click'); + expect(selectedHero).toBe(expectedHero); +}); +``` The component's `selected` property returns an `EventEmitter`, which looks like an RxJS synchronous `Observable` to consumers. The test subscribes to it _explicitly_ just as the host component does _implicitly_. @@ -469,14 +571,16 @@ The `heroDe` in the previous test is a `DebugElement` that represents the hero ` It has Angular properties and methods that abstract interaction with the native element. This test calls the `DebugElement.triggerEventHandler` with the "click" event name. -The "click" event binding responds by calling `DashboardHeroComponent.click()`. +The "click" event binding responds by calling `DashboardHero.click()`. The Angular `DebugElement.triggerEventHandler` can raise _any data-bound event_ by its _event name_. The second parameter is the event object passed to the handler. The test triggered a "click" event. - +```ts +heroDe.triggerEventHandler('click'); +``` In this case, the test correctly assumes that the runtime event handler, the component's `click()` method, doesn't care about the event object. @@ -488,7 +592,15 @@ The `RouterLink` directive throws an error if the event object is missing. The following test alternative calls the native element's own `click()` method, which is perfectly fine for _this component_. - +```ts +it('should raise selected event when clicked (element.click)', () => { + let selectedHero: Hero | undefined; + comp.selected.subscribe((hero: Hero) => (selectedHero = hero)); + + heroEl.click(); + expect(selectedHero).toBe(expectedHero); +}); +``` ### `click()` helper @@ -496,7 +608,25 @@ Clicking a button, an anchor, or an arbitrary HTML element is a common test task Make that consistent and straightforward by encapsulating the _click-triggering_ process in a helper such as the following `click()` function: - +```ts +/** Button events to pass to `DebugElement.triggerEventHandler` for RouterLink event handler */ +export const ButtonClickEvents = { + left: {button: 0}, + right: {button: 2}, +}; + +/** Simulate element click. Defaults to mouse left-button click event. */ +export function click( + el: DebugElement | HTMLElement, + eventObj: any = ButtonClickEvents.left, +): void { + if (el instanceof HTMLElement) { + el.click(); + } else { + el.triggerEventHandler('click', eventObj); + } +} +``` The first parameter is the _element-to-click_. If you want, pass a custom event object as the second parameter. @@ -509,69 +639,140 @@ If you like it, add it to your own collection of helpers. Here's the previous test, rewritten using the click helper. - +```ts +it('should raise selected event when clicked (click helper with DebugElement)', () => { + let selectedHero: Hero | undefined; + comp.selected.subscribe((hero: Hero) => (selectedHero = hero)); -## Component inside a test host + click(heroDe); // click helper with DebugElement -The previous tests played the role of the host `DashboardComponent` themselves. -But does the `DashboardHeroComponent` work correctly when properly data-bound to a host component? + expect(selectedHero).toBe(expectedHero); +}); +``` - +## Component inside a test host + +The previous tests played the role of the host `Dashboard` component themselves. +But does the `DashboardHero` component work correctly when properly data-bound to a host component? + +```angular-ts +@Component({ + imports: [DashboardHero], + template: ` `, +}) +class TestHost { + hero: Hero = {id: 42, name: 'Test Name'}; + selectedHero: Hero | undefined; + + onSelected(hero: Hero) { + this.selectedHero = hero; + } +} +``` The test host sets the component's `hero` input property with its test hero. It binds the component's `selected` event with its `onSelected` handler, which records the emitted hero in its `selectedHero` property. -Later, the tests will be able to check `selectedHero` to verify that the `DashboardHeroComponent.selected` event emitted the expected hero. +Later, the tests will be able to check `selectedHero` to verify that the `DashboardHero.selected` event emitted the expected hero. The setup for the `test-host` tests is similar to the setup for the stand-alone tests: - +```ts +beforeEach(async () => { + // create TestHost instead of DashboardHero + fixture = TestBed.createComponent(TestHost); + testHost = fixture.componentInstance; + heroEl = fixture.nativeElement.querySelector('.hero'); + + await fixture.whenStable(); +}); +``` This testing module configuration shows two important differences: -- It _creates_ the `TestHostComponent` instead of the `DashboardHeroComponent` -- The `TestHostComponent` sets the `DashboardHeroComponent.hero` with a binding +- It _creates_ the `TestHost` component instead of the `DashboardHero` +- The `TestHost` component sets the `DashboardHero.hero` with a binding -The `createComponent` returns a `fixture` that holds an instance of `TestHostComponent` instead of an instance of `DashboardHeroComponent`. +The `createComponent` returns a `fixture` that holds an instance of `TestHost` instead of an instance of `DashboardHero`. -Creating the `TestHostComponent` has the side effect of creating a `DashboardHeroComponent` because the latter appears within the template of the former. +Creating the `TestHost` has the side effect of creating a `DashboardHero` because the latter appears within the template of the former. The query for the hero element \(`heroEl`\) still finds it in the test DOM, albeit at greater depth in the element tree than before. The tests themselves are almost identical to the stand-alone version: - +```ts +it('should display hero name', () => { + const expectedPipedName = testHost.hero.name.toUpperCase(); + expect(heroEl.textContent).toContain(expectedPipedName); +}); + +it('should raise selected event when clicked', () => { + click(heroEl); + // selected hero should be the same data bound hero + expect(testHost.selectedHero).toBe(testHost.hero); +}); +``` Only the selected event test differs. -It confirms that the selected `DashboardHeroComponent` hero really does find its way up through the event binding to the host component. +It confirms that the selected `DashboardHero` hero really does find its way up through the event binding to the host component. ## Routing component A _routing component_ is a component that tells the `Router` to navigate to another component. -The `DashboardComponent` is a _routing component_ because the user can navigate to the `HeroDetailComponent` by clicking on one of the _hero buttons_ on the dashboard. +The `Dashboard` component is a _routing component_ because the user can navigate to the `HeroDetail` component by clicking on one of the _hero buttons_ on the dashboard. Angular provides test helpers to reduce boilerplate and more effectively test code which depends on `HttpClient`. The `provideRouter` function can be used directly in the test module as well. - +```ts +beforeEach(async () => { + TestBed.configureTestingModule({ + providers: [ + provideRouter([{path: '**', component: Dashboard}]), + provideHttpClientTesting(), + HeroService, + ], + }); + harness = await RouterTestingHarness.create(); + comp = await harness.navigateByUrl('/', Dashboard); + TestBed.inject(HttpTestingController).expectOne('api/heroes').flush(getTestHeroes()); +}); +``` The following test clicks the displayed hero and confirms that we navigate to the expected URL. - +```ts +it('should tell navigate when hero clicked', async () => { + // get first DebugElement + const heroDe = harness.routeDebugElement!.query(By.css('dashboard-hero')); + heroDe.triggerEventHandler('selected', comp.heroes[0]); + + // expecting to navigate to id of the component's first hero + const id = comp.heroes[0].id; + expect(TestBed.inject(Router).url, 'should nav to HeroDetail for first hero').toEqual( + `/heroes/${id}`, + ); +}); +``` ## Routed components A _routed component_ is the destination of a `Router` navigation. It can be trickier to test, especially when the route to the component _includes parameters_. -The `HeroDetailComponent` is a _routed component_ that is the destination of such a route. +The `HeroDetail` is a _routed component_ that is the destination of such a route. -When a user clicks a _Dashboard_ hero, the `DashboardComponent` tells the `Router` to navigate to `heroes/:id`. +When a user clicks a _Dashboard_ hero, the `Dashboard` tells the `Router` to navigate to `heroes/:id`. The `:id` is a route parameter whose value is the `id` of the hero to edit. -The `Router` matches that URL to a route to the `HeroDetailComponent`. -It creates an `ActivatedRoute` object with the routing information and injects it into a new instance of the `HeroDetailComponent`. +The `Router` matches that URL to a route to the `HeroDetail`. +It creates an `ActivatedRoute` object with the routing information and injects it into a new instance of the `HeroDetail`. -Here are the services injected into `HeroDetailComponent`: +Here are the services injected into `HeroDetail`: - +```ts +private heroDetailService = inject(HeroDetailService); +private route = inject(ActivatedRoute); +private router = inject(Router); +``` The `HeroDetail` component needs the `id` parameter so it can fetch the corresponding hero using the `HeroDetailService`. The component has to get the `id` from the `ActivatedRoute.paramMap` property which is an `Observable`. @@ -579,9 +780,16 @@ The component has to get the `id` from the `ActivatedRoute.paramMap` property wh It can't just reference the `id` property of the `ActivatedRoute.paramMap`. The component has to _subscribe_ to the `ActivatedRoute.paramMap` observable and be prepared for the `id` to change during its lifetime. - +```ts +constructor() { + // get hero when `id` param changes + this.route.paramMap + .pipe(takeUntilDestroyed()) + .subscribe((pmap) => this.getHero(pmap.get('id'))); +} +``` -Tests can explore how the `HeroDetailComponent` responds to different `id` parameter values by navigating to different routes. +Tests can explore how the `HeroDetail` responds to different `id` parameter values by navigating to different routes. ## Nested component tests @@ -589,17 +797,28 @@ Component templates often have nested components, whose templates might contain The component tree can be very deep and sometimes the nested components play no role in testing the component at the top of the tree. -The `AppComponent`, for example, displays a navigation bar with anchors and their `RouterLink` directives. +The `App` component, for example, displays a navigation bar with anchors and their `RouterLink` directives. - +```angular-html + + + + + + +``` To validate the links but not the navigation, you don't need the `Router` to navigate and you don't need the `` to mark where the `Router` inserts _routed components_. -The `BannerComponent` and `WelcomeComponent` \(indicated by `` and ``\) are also irrelevant. +The `Banner` and `Welcome` components \(indicated by `` and ``\) are also irrelevant. -Yet any test that creates the `AppComponent` in the DOM also creates instances of these three components and, if you let that happen, you'll have to configure the `TestBed` to create them. +Yet any test that creates the `App` component in the DOM also creates instances of these three components and, if you let that happen, you'll have to configure the `TestBed` to create them. -If you neglect to declare them, the Angular compiler won't recognize the ``, ``, and `` tags in the `AppComponent` template and will throw an error. +If you neglect to declare them, the Angular compiler won't recognize the ``, ``, and `` tags in the `App` template and will throw an error. If you declare the real components, you'll also have to declare _their_ nested components and provide for _all_ services injected in _any_ component in the tree. @@ -610,14 +829,39 @@ Use them, alone or in combination, to stay focused on testing the primary compon In the first technique, you create and declare stub versions of the components and directive that play little or no role in the tests. - +```ts +@Component({selector: 'app-banner', template: ''}) +class BannerStub {} + +@Component({selector: 'router-outlet', template: ''}) +class RouterOutletStub {} + +@Component({selector: 'app-welcome', template: ''}) +class WelcomeStub {} +``` The stub selectors match the selectors for the corresponding real components. But their templates and classes are empty. Then declare them by overriding the `imports` of your component using `TestBed.overrideComponent`. - +```ts +let comp: App; +let fixture: ComponentFixture; + +beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideRouter([]), UserAuthentication], + }).overrideComponent(App, { + set: { + imports: [RouterLink, BannerStub, RouterOutletStub, WelcomeStub], + }, + }); + + fixture = TestBed.createComponent(App); + comp = fixture.componentInstance; +}); +``` HELPFUL: The `set` key in this example replaces all the exisiting imports on your component, make sure to imports all dependencies, not only the stubs. Alternatively you can use the `remove`/`add` keys to selectively remove and add imports. @@ -625,11 +869,22 @@ HELPFUL: The `set` key in this example replaces all the exisiting imports on you In the second approach, add `NO_ERRORS_SCHEMA` to the metadata overrides of your component. - +```ts +beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideRouter([]), UserAuthentication], + }).overrideComponent(App, { + set: { + imports: [], // resets all imports + schemas: [NO_ERRORS_SCHEMA], + }, + }); +}); +``` The `NO_ERRORS_SCHEMA` tells the Angular compiler to ignore unrecognized elements and attributes. -The compiler recognizes the `` element and the `routerLink` attribute because you declared a corresponding `AppComponent` and `RouterLink` in the `TestBed` configuration. +The compiler recognizes the `` element and the `routerLink` attribute because you declared a corresponding `App` component and `RouterLink` in the `TestBed` configuration. But the compiler won't throw an error when it encounters ``, ``, or ``. It simply renders them as empty tags and the browser ignores them. @@ -650,15 +905,35 @@ While the stubs in _this_ example were empty, you could give them stripped-down In practice you will combine the two techniques in the same setup, as seen in this example. - +```ts +beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideRouter([]), UserAuthentication], + }).overrideComponent(App, { + remove: {imports: [RouterOutlet, Welcome]}, + set: {schemas: [NO_ERRORS_SCHEMA]}, + }); +}); +``` -The Angular compiler creates the `BannerStubComponent` for the `` element and applies the `RouterLink` to the anchors with the `routerLink` attribute, but it ignores the `` and `` tags. +The Angular compiler creates the `BannerStub` for the `` element and applies the `RouterLink` to the anchors with the `routerLink` attribute, but it ignores the `` and `` tags. ### `By.directive` and injected directives A little more setup triggers the initial data binding and gets references to the navigation links: - +```ts +beforeEach(async () => { + await fixture.whenStable(); + + // find DebugElements with an attached RouterLinkStubDirective + linkDes = fixture.debugElement.queryAll(By.directive(RouterLink)); + + // get attached link directive instances + // using each DebugElement's injector + routerLinks = linkDes.map((de) => de.injector.get(RouterLink)); +}); +``` Three points of special interest: @@ -666,22 +941,60 @@ Three points of special interest: - The query returns `DebugElement` wrappers around the matching elements - Each `DebugElement` exposes a dependency injector with the specific instance of the directive attached to that element -The `AppComponent` links to validate are as follows: +The `App` component links to validate are as follows: - +```angular-html + +``` Here are some tests that confirm those links are wired to the `routerLink` directives as expected: - +```ts +it('can get RouterLinks from template', () => { + expect(routerLinks.length, 'should have 3 routerLinks').toBe(3); + expect(routerLinks[0].href).toBe('/dashboard'); + expect(routerLinks[1].href).toBe('/heroes'); + expect(routerLinks[2].href).toBe('/about'); +}); + +it('can click Heroes link in template', async () => { + const heroesLinkDe = linkDes[1]; // heroes link DebugElement + + TestBed.inject(Router).resetConfig([{path: '**', children: []}]); + heroesLinkDe.triggerEventHandler('click', {button: 0}); + + await fixture.whenStable(); + + expect(TestBed.inject(Router).url).toBe('/heroes'); +}); +``` ## Use a `page` object -The `HeroDetailComponent` is a simple view with a title, two hero fields, and two buttons. +The `HeroDetail` component is a simple view with a title, two hero fields, and two buttons. But there's plenty of template complexity even in this simple form. - +```angular-html +@if (hero) { +
+

+ {{ hero.name | titlecase }} Details +

+
id: {{ hero.id }}
+
+ + +
+ + +
+} +``` Tests that exercise the component need … @@ -696,23 +1009,111 @@ Tame the complexity with a `Page` class that handles access to component propert Here is such a `Page` class for the `hero-detail.component.spec.ts` - +```ts +class Page { + // getter properties wait to query the DOM until called. + get buttons() { + return this.queryAll('button'); + } + get saveBtn() { + return this.buttons[0]; + } + get cancelBtn() { + return this.buttons[1]; + } + get nameDisplay() { + return this.query('span'); + } + get nameInput() { + return this.query('input'); + } + + //// query helpers //// + private query(selector: string): T { + return harness.routeNativeElement!.querySelector(selector)! as T; + } + + private queryAll(selector: string): T[] { + return harness.routeNativeElement!.querySelectorAll(selector) as any as T[]; + } +} +``` Now the important hooks for component manipulation and inspection are neatly organized and accessible from an instance of `Page`. A `createComponent` method creates a `page` object and fills in the blanks once the `hero` arrives. - +```ts +async function createComponent(id: number) { + harness = await RouterTestingHarness.create(); + component = await harness.navigateByUrl(`/heroes/${id}`, HeroDetail); + page = new Page(); + + const request = TestBed.inject(HttpTestingController).expectOne(`api/heroes/?id=${id}`); + const hero = getTestHeroes().find((h) => h.id === Number(id)); + request.flush(hero ? [hero] : []); + await harness.fixture.whenStable(); +} +``` -Here are a few more `HeroDetailComponent` tests to reinforce the point. +Here are a few more `HeroDetail` component tests to reinforce the point. - +```ts +it("should display that hero's name", () => { + expect(page.nameDisplay.textContent).toBe(expectedHero.name); +}); + +it('should navigate when click cancel', () => { + click(page.cancelBtn); + expect(TestBed.inject(Router).url).toEqual(`/heroes/${expectedHero.id}`); +}); + +it('should save when click save but not navigate immediately', () => { + click(page.saveBtn); + expect(TestBed.inject(HttpTestingController).expectOne({method: 'PUT', url: 'api/heroes'})); + expect(TestBed.inject(Router).url).toEqual('/heroes/41'); +}); + +it('should navigate when click save and save resolves', async () => { + click(page.saveBtn); + await harness.fixture.whenStable(); + expect(TestBed.inject(Router).url).toEqual('/heroes/41'); +}); + +it('should convert hero name to Title Case', async () => { + // get the name's input and display elements from the DOM + const hostElement: HTMLElement = harness.routeNativeElement!; + const nameInput: HTMLInputElement = hostElement.querySelector('input')!; + const nameDisplay: HTMLElement = hostElement.querySelector('span')!; + + // simulate user entering a new name into the input box + nameInput.value = 'quick BROWN fOx'; + + // Dispatch a DOM event so that Angular learns of input value change. + nameInput.dispatchEvent(new Event('input')); + + // Wait for Angular to update the display binding through the title pipe + await harness.fixture.whenStable(); + + expect(nameDisplay.textContent).toBe('Quick Brown Fox'); +}); +``` ## Override component providers -The `HeroDetailComponent` provides its own `HeroDetailService`. +The `HeroDetail` provides its own `HeroDetailService`. - +```ts +@Component({ + /* ... */ + providers: [HeroDetailService], +}) +export class HeroDetail { + private heroDetailService = inject(HeroDetailService); + private route = inject(ActivatedRoute); + private router = inject(Router); +} +``` It's not possible to stub the component's `HeroDetailService` in the `providers` of the `TestBed.configureTestingModule`. Those are providers for the _testing module_, not the component. @@ -731,7 +1132,12 @@ There might not be a remote server to call. Fortunately, the `HeroDetailService` delegates responsibility for remote data access to an injected `HeroService`. - +```ts +@Injectable({providedIn: 'root'}) +export class HeroDetailService { + private heroService = inject(HeroService); +} +``` The previous test configuration replaces the real `HeroService` with a `TestHeroService` that intercepts server requests and fakes their responses. @@ -741,7 +1147,22 @@ What if `HeroDetailService` makes its own server requests? The `TestBed.overrideComponent` method can replace the component's `providers` with easy-to-manage _test doubles_ as seen in the following setup variation: - +```ts +beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [ + provideRouter([ + {path: 'heroes', component: HeroList}, + {path: 'heroes/:id', component: HeroDetail}, + ]), + // HeroDetailService at this level is IRRELEVANT! + {provide: HeroDetailService, useValue: {}}, + ], + }).overrideComponent(HeroDetail, { + set: {providers: [{provide: HeroDetailService, useClass: HeroDetailServiceSpy}]}, + }); +}); +``` Notice that `TestBed.configureTestingModule` no longer provides a fake `HeroService` because it's [not needed](#provide-a-spy-stub-herodetailservicespy). @@ -749,35 +1170,35 @@ Notice that `TestBed.configureTestingModule` no longer provides a fake `HeroServ Focus on the `overrideComponent` method. - +```ts +.overrideComponent(HeroDetail, { + set: {providers: [{provide: HeroDetailService, useClass: HeroDetailServiceSpy}]}, +}); +``` -It takes two arguments: the component type to override \(`HeroDetailComponent`\) and an override metadata object. +It takes two arguments: the component type to override (`HeroDetail`) and an override metadata object. The [override metadata object](guide/testing/utility-apis#metadata-override-object) is a generic defined as follows: - - +```ts type MetadataOverride = { -add?: Partial; -remove?: Partial; -set?: Partial; + add?: Partial; + remove?: Partial; + set?: Partial; }; - - +``` A metadata override object can either add-and-remove elements in metadata properties or completely reset those properties. This example resets the component's `providers` metadata. The type parameter, `T`, is the kind of metadata you'd pass to the `@Component` decorator: - - +```ts selector?: string; template?: string; templateUrl?: string; providers?: any[]; … - - +``` ### Provide a _spy stub_ (`HeroDetailServiceSpy`) @@ -786,16 +1207,67 @@ This example completely replaces the component's `providers` array with a new ar The `HeroDetailServiceSpy` is a stubbed version of the real `HeroDetailService` that fakes all necessary features of that service. It neither injects nor delegates to the lower level `HeroService` so there's no need to provide a test double for that. -The related `HeroDetailComponent` tests will assert that methods of the `HeroDetailService` were called by spying on the service methods. +The related `HeroDetail` component tests will assert that methods of the `HeroDetailService` were called by spying on the service methods. Accordingly, the stub implements its methods as spies: - +```ts +import {vi} from 'vitest'; + +class HeroDetailServiceSpy { + testHero: Hero = {...testHero}; + + /* emit cloned test hero */ + getHero = vi.fn(() => asyncData({...this.testHero})); + + /* emit clone of test hero, with changes merged in */ + saveHero = vi.fn((hero: Hero) => asyncData(Object.assign(this.testHero, hero))); +} +``` ### The override tests Now the tests can control the component's hero directly by manipulating the spy-stub's `testHero` and confirm that service methods were called. - +```ts +let hdsSpy: HeroDetailServiceSpy; + +beforeEach(async () => { + harness = await RouterTestingHarness.create(); + component = await harness.navigateByUrl(`/heroes/${testHero.id}`, HeroDetail); + page = new Page(); + // get the component's injected HeroDetailServiceSpy + hdsSpy = harness.routeDebugElement!.injector.get(HeroDetailService) as any; + + harness.detectChanges(); +}); + +it('should have called `getHero`', () => { + expect(hdsSpy.getHero, 'getHero called once').toHaveBeenCalledTimes(1); +}); + +it("should display stub hero's name", () => { + expect(page.nameDisplay.textContent).toBe(hdsSpy.testHero.name); +}); + +it('should save stub hero change', async () => { + const origName = hdsSpy.testHero.name; + const newName = 'New Name'; + + page.nameInput.value = newName; + + page.nameInput.dispatchEvent(new Event('input')); // tell Angular + + expect(component.hero.name, 'component hero has new name').toBe(newName); + expect(hdsSpy.testHero.name, 'service hero unchanged before save').toBe(origName); + + click(page.saveBtn); + expect(hdsSpy.saveHero, 'saveHero called once').toHaveBeenCalledTimes(1); + + await harness.fixture.whenStable(); + expect(hdsSpy.testHero.name, 'service hero has new name after save').toBe(newName); + expect(TestBed.inject(Router).url).toEqual('/heroes'); +}); +``` ### More overrides diff --git a/adev-ja/src/content/guide/testing/components-scenarios.md b/adev-ja/src/content/guide/testing/components-scenarios.md index dd28535e5..2c1571bb2 100644 --- a/adev-ja/src/content/guide/testing/components-scenarios.md +++ b/adev-ja/src/content/guide/testing/components-scenarios.md @@ -1,805 +1,1277 @@ -# コンポーネントテストシナリオ +# コンポーネントテストのシナリオ -このガイドでは、一般的なコンポーネントテストのユースケースについて説明します。 +このガイドでは、一般的なコンポーネントテストのユースケースについて解説します。 -## コンポーネントバインディング {#component-binding} +## コンポーネントのバインディング {#component-binding} -サンプルアプリケーションでは、`BannerComponent`はHTMLテンプレートに静的なタイトルテキストを表示します。 +サンプルアプリケーションでは、`Banner`コンポーネントはHTMLテンプレート内に静的なタイトルテキストを表示します。 -いくつかの変更を加えた後、`BannerComponent`は、次のようにコンポーネントの`title`プロパティにバインドすることで動的なタイトルを表示します。 +いくつかの変更を加えた後、`Banner`コンポーネントは次のようにコンポーネントの`title`プロパティにバインドすることで、動的なタイトルを表示します。 - +```angular-ts {header="banner.ts"} +import {Component, signal} from '@angular/core'; -これは最小限のものですが、コンポーネントが実際に期待どおりに正しいコンテンツを表示していることを確認するためのテストを追加することにします。 +@Component({ + selector: 'app-banner', + template: '

{{ title() }}

', + styles: ['h1 { color: green; font-size: 350%}'], +}) +export class Banner { + title = signal('Test Tour of Heroes'); +} +``` + +これは最小限のものですが、コンポーネントが実際に想定した場所に正しいコンテンツを表示していることを確認するためのテストを追加することにします。 ### `

`のクエリ {#query-for-the-h1} -_title_プロパティの補間バインディングを囲む`

`要素の値を検査する一連のテストを作成します。 +_title_プロパティの補間バインディングをラップする`

`要素の値を検査する一連のテストを作成します。 -`beforeEach`を更新して、標準のHTML`querySelector`でその要素を見つけ、`h1`変数に割り当てます。 +標準的なHTMLの`querySelector`でその要素を見つけ、`h1`変数に割り当てるように`beforeEach`を更新します。 - +```ts {header: "banner.component.spec.ts"} +let component: Banner; +let fixture: ComponentFixture; +let h1: HTMLElement; -### `createComponent()`はデータをバインドしません {#createcomponent-does-not-bind-data} +beforeEach(() => { + fixture = TestBed.createComponent(Banner); + component = fixture.componentInstance; // Banner test instance + h1 = fixture.nativeElement.querySelector('h1'); +}); +``` -最初のテストでは、画面にデフォルトの`title`が表示されることを確認したいと考えています。 -直感的には、次のように`

`をすぐに検査するテストを作成するでしょう。 +### `createComponent()`はデータをバインドしない {#createcomponent-does-not-bind-data} - +最初のテストでは、画面にデフォルトの`title`が表示されることを確認したいと思います。 +直感的には、次のように`

`をすぐに検査するテストを書きたくなるでしょう。 -_このテストは失敗します_、メッセージは次のとおりです。 +```ts +it('should display original title', () => { + expect(h1.textContent).toContain(component.title()); +}); +``` - +_そのテストは失敗し_、次のメッセージが表示されます。 +```shell {hideCopy} expected '' to contain 'Test Tour of Heroes'. - - +``` バインディングは、Angularが**変更検知**を実行したときに発生します。 -本番環境では、Angularがコンポーネントを作成したり、ユーザーがキーストロークを入力したりしたときに、変更検知は自動的に開始されます。 - -`TestBed.createComponent`はデフォルトで変更検知をトリガーしません。これは、修正されたテストで確認できます。 - - - -### `detectChanges()` {#detectchanges} - -`TestBed`に`fixture.detectChanges()`を呼び出してデータバインディングを実行するように指示することができます。 -そうすれば初めて、`

`に期待どおりのタイトルが表示されます。 +本番環境では、例えばAngularがコンポーネントを作成したときや、ユーザーがキー入力したときなどに、変更検知が自動的に開始されます。 - +`TestBed.createComponent`は同期的に変更検知をトリガーしません。この事実は修正されたテストで確認されています。 -変更検知が遅延されるのは意図的なことであり、便利です。 -これにより、テスターはAngularがデータバインディングを開始し、[ライフサイクルフック](guide/components/lifecycle)を呼び出す_前に_、コンポーネントの状態を検査および変更することができます。 - -次に、`fixture.detectChanges()`を呼び出す_前に_、コンポーネントの`title`プロパティを変更するテストを示します。 - - - -### 自動変更検知 {#automatic-change-detection} - -`BannerComponent`のテストでは、頻繁に`detectChanges`を呼び出しています。 -多くのテスターは、Angularのテスト環境が本番環境のように自動的に変更検知を実行することを好みます。 - -これは、`TestBed`を`ComponentFixtureAutoDetect`プロバイダーで設定することで可能です。 -まず、テストユーティリティライブラリからインポートします。 - - - -次に、テストモジュール設定の`providers`配列に追加します。 - - - -HELPFUL: `fixture.autoDetectChanges()`関数を代わりに使用することもできます。 -これは、フィクスチャのコンポーネントの状態を更新した後、自動変更検知を有効にする場合のみです。 -また、自動変更検知は`provideZonelessChangeDetection`を使用する場合、デフォルトで有効になっており、オフにすることは推奨されません。 - -次に、自動変更検知がどのように機能するかを示す3つのテストを示します。 - - +```ts +it('no title in the DOM after createComponent()', () => { + expect(h1.textContent).toEqual(''); +}); +``` -最初のテストは、自動変更検知の利点を示しています。 +### `whenStable()` {#whenstable} -2番目と3番目のテストは、重要な制限を明らかにしています。 -Angularのテスト環境は、コンポーネントの`title`を変更したテストケース内で更新が行われた場合、変更検知を同期的に実行しません。 -テストは、別の変更検知を待つために`await fixture.whenStable`を呼び出す必要があります。 +`await fixture.whenStable()`を使用すると、変更検知が実行されるのを待つように`TestBed`に指示できます。 +そうして初めて、`

`は期待されるタイトルを持つようになります。 -HELPFUL: Angularは、信号ではない値への直接的な更新については知りません。 -変更検知がスケジュールされるようにするための最も簡単な方法は、テンプレートで読み取られる値に信号を使用することです。 +```ts +it('should display original title', async () => { + await fixture.whenStable(); + expect(h1.textContent).toContain(component.title()); +}); +``` -### `dispatchEvent()`を使用して入力値を変更する {#change-an-input-value-with-dispatchevent} +遅延された変更検知は意図的なものであり、有用です。 +これにより、テスターは_Angularがデータバインディングを開始し、[ライフサイクルフック](guide/components/lifecycle)を呼び出す前に_、コンポーネントの状態を検査および変更する機会を得ることができます。 -ユーザー入力をシミュレートするには、入力要素を見つけ、その`value`プロパティを設定します。 +ここに、`fixture.whenStable()`を呼び出す_前に_コンポーネントの`title`プロパティを変更する別のテストがあります。 -しかし、重要な中間ステップがあります。 +```ts +it('should display a different test title', async () => { + component.title.set('Test Title'); + await fixture.whenStable(); + expect(h1.textContent).toContain('Test Title'); +}); +``` -Angularは、入力要素の`value`プロパティを設定したことを知りません。 -`dispatchEvent()`を呼び出して要素の`input`イベントを発生させるまで、そのプロパティを読み込みません。 +### シグナルを入力にバインドする {#binding-signals-to-inputs} -次の例は、正しいシーケンスを示しています。 +入力への変更を反映し、出力をリッスンするために、シグナルを入力に、関数を出力に動的にバインドできます。 - +```ts +import {inputBinding, outputBinding} from '@angular/core'; + +const fixture = TestBed.createComponent(ValueDisplay, { + bindings: [ + inputBinding('value', value), + outputBinding('valueChange', () => (/* ... */) ), + ], +}); +``` -## 外部ファイルを持つコンポーネント {#component-with-external-files} +### `dispatchEvent()`で入力値を変更する {#change-an-input-value-with-dispatchevent} -前の`BannerComponent`は、それぞれ`@Component.template`と`@Component.styles`プロパティで指定された_インラインテンプレート_と_インラインcss_で定義されています。 +ユーザー入力をシミュレートするには、input要素を見つけてその`value`プロパティを設定します。 -多くのコンポーネントは、それぞれ`@Component.templateUrl`と`@Component.styleUrls`プロパティで、_外部テンプレート_と_外部css_を指定します。`BannerComponent`の次のバリアントは、そのようにします。 +しかし、不可欠な中間ステップがあります。 - +Angularは、あなたがinput要素の`value`プロパティを設定したことを知りません。 +`dispatchEvent()`を呼び出して要素の`input`イベントを発生させるまで、Angularはそのプロパティを読み取りません。 -この構文は、Angularコンパイラーに、コンポーネントのコンパイル中に外部ファイルを読み取るように指示します。 +`TitleCasePipe`を使用するコンポーネントの次の例は、適切な手順を示しています。 -これは、CLI`ng test`コマンドを実行するときに問題になりません。なぜなら、CLIは_テストを実行する前にアプリケーションをコンパイルする_からです。 +```ts +it('should convert hero name to Title Case', async () => { + const hostElement = fixture.nativeElement; + const nameInput: HTMLInputElement = hostElement.querySelector('input')!; + const nameDisplay: HTMLElement = hostElement.querySelector('span')!; -しかし、**CLI以外の環境**でテストを実行する場合、このコンポーネントのテストは失敗する可能性があります。 -たとえば、[plunker](https://plnkr.co)などのWebコーディング環境で`BannerComponent`のテストを実行すると、次のようなメッセージが表示されます。 + // simulate user entering a new name into the input box + nameInput.value = 'quick BROWN fOx'; -```shell {hideCopy} + // Dispatch a DOM event so that Angular learns of input value change. + nameInput.dispatchEvent(new Event('input')); -Error: This test module uses the component BannerComponent -which is using a "templateUrl" or "styleUrls", but they were never compiled. -Please call "TestBed.compileComponents" before your test. + // Wait for Angular to update the display binding through the title pipe + await fixture.whenStable(); + expect(nameDisplay.textContent).toBe('Quick Brown Fox'); +}); ``` -このテストエラーメッセージは、ランタイム環境が_テスト自体の実行中に_ソースコードをコンパイルするときに発生します。 - -この問題を解決するには、`compileComponents()`を呼び出します。 - ## 依存関係を持つコンポーネント {#component-with-a-dependency} -コンポーネントは、多くの場合、サービス依存関係を持ちます。 - -`WelcomeComponent`は、ログインしたユーザーに歓迎メッセージを表示します。 -これは、注入された`UserService`のプロパティに基づいて、ユーザーが誰かを知っています。 - - +コンポーネントはしばしばサービスの依存関係を持ちます。 + +`Welcome`コンポーネントは、ログインしているユーザーにウェルカムメッセージを表示します。 +注入された`UserAuthentication`のプロパティに基づいて、ユーザーが誰であるかを認識します。 + +```angular-ts +import {Component, inject, OnInit, signal} from '@angular/core'; +import {UserAuthentication} from '../model/user.authentication'; + +@Component({ + selector: 'app-welcome', + template: '

{{ welcome() }}

', +}) +export class Welcome { + private userAuth = inject(UserAuthentication); + welcome = signal( + this.userAuth.isLoggedIn() ? `Welcome, ${this.userAuth.user().name}` : 'Please log in.', + ); +} +``` -`WelcomeComponent`には、サービスと対話する意思決定ロジックがあり、このコンポーネントのテスト価値を高めています。 +`Welcome`コンポーネントにはサービスと対話する決定ロジックがあり、このロジックがこのコンポーネントをテストする価値のあるものにしています。 -### サービステストダブルを提供する {#provide-service-test-doubles} +### サービスのテストダブルを提供する {#provide-service-test-doubles} -_テスト対象のコンポーネント_は、実際のサービスを提供する必要はありません。 +_テスト対象コンポーネント_(component-under-test)に実際のサービスを提供する必要はありません。 -実際の`UserService`を注入するのは難しい場合があります。 -実際のサービスは、ユーザーにログイン資格情報の入力を求めて、認証サーバーにアクセスしようとするかもしれません。 -これらの動作は、インターセプトするのが難しい場合があります。テストダブルを使用すると、テストが本番環境とは異なる動作をするため、控えめに使用してください。 +実際の`UserAuthentication`を注入するのは難しい場合があります。 +実際のサービスは、ユーザーにログイン資格情報を求め、認証サーバーへの接続を試みるかもしれません。 +これらの動作を傍受するのは困難な場合があります。テストダブルを使用するとテストの動作が本番環境と異なるものになるため、使用は控えめにすることに注意してください。 ### 注入されたサービスを取得する {#get-injected-services} -テストでは、`WelcomeComponent`に注入された`UserService`にアクセスする必要があります。 +テストでは、`Welcome`コンポーネントに注入された`UserAuthentication`にアクセスする必要があります。 -Angularには、階層的な注入システムがあります。 -`TestBed`によって作成されたルートインジェクターから、コンポーネントツリーを下って、複数のレベルにインジェクターが存在する可能性があります。 +Angularには階層的な注入システムがあります。 +`TestBed`によって作成されたルートインジェクターからコンポーネントツリーの下層に至るまで、複数のレベルにインジェクターが存在する可能性があります。 -注入されたサービスを取得する最も安全な方法は、**_常に動作する_**方法であり、 -**_テスト対象のコンポーネント_のインジェクターから取得することです**。 +注入されたサービスを取得する最も安全な方法、つまり**_常に機能する_**方法は、 +**_テスト対象コンポーネント_のインジェクターから取得すること**です。 コンポーネントインジェクターは、フィクスチャの`DebugElement`のプロパティです。 - +```ts +// UserAuthentication actually injected into the component +userAuth = fixture.debugElement.injector.get(UserAuthentication); +``` -HELPFUL: これは_通常_は必要ありません。サービスは、多くの場合、ルートまたは`TestBed`のオーバーライドで提供され、`TestBed.inject()`を使用してより簡単に取得できます(下記参照)。 +HELPFUL: これは_通常_必要ありません。サービスは多くの場合、ルートまたはTestBedのオーバーライドで提供され、`TestBed.inject()`を使用してより簡単に取得できます(下記参照)。 ### `TestBed.inject()` {#testbedinject} -これは、フィクスチャの`DebugElement`を使用してサービスを取得するよりも覚えやすく、冗長性が少なくなります。 - -このテストスイートでは、`UserService`のプロバイダーはルートテストモジュールのみであるため、次のように`TestBed.inject()`を呼び出すのは安全です。 - - - -HELPFUL: `TestBed.inject()`が機能しないユースケースについては、[_コンポーネントプロバイダーのオーバーライド_](#override-component-providers)セクションを参照してください。このセクションでは、いつ、なぜフィクスチャのインジェクターの代わりにコンポーネントのインジェクターからサービスを取得する必要があるのかを説明しています。 - -### 最終的な設定とテスト {#final-setup-and-tests} - -次に、`TestBed.inject()`を使用した完全な`beforeEach()`を示します。 - - - -次に、いくつかのテストを示します。 +これは、フィクスチャの`DebugElement`を使用してサービスを取得するよりも覚えやすく、冗長ではありません。 - +このテストスイートでは、`UserAuthentication`の_唯一の_プロバイダーはルートテストモジュールであるため、次のように`TestBed.inject()`を呼び出しても安全です。 -最初のテストは、健全性テストです。これは、`UserService`が呼び出され、機能していることを確認します。 - -HELPFUL: withContext関数(たとえば、`'expected name'`)は、オプションの失敗ラベルです。 -期待値が失敗した場合、Jasmineは期待値の失敗メッセージにこのラベルを追加します。 -複数の期待値を持つスペックでは、何が間違っていたのか、どの期待値が失敗したのかを明確にするのに役立ちます。 - -残りのテストは、サービスが異なる値を返した場合に、コンポーネントのロジックが正しいことを確認します。 -2番目のテストは、ユーザー名の変更の効果を検証します。 -3番目のテストは、ログインしたユーザーがいない場合に、コンポーネントが適切なメッセージを表示することを確認します。 - -## 非同期サービスを持つコンポーネント {#component-with-async-service} - -このサンプルでは、`AboutComponent`テンプレートは`TwainComponent`をホストしています。 -`TwainComponent`は、マーク・トウェインの引用を表示します。 - - - -HELPFUL: コンポーネントの`quote`プロパティの値は、`AsyncPipe`を通過します。 -つまり、プロパティは`Promise`または`Observable`のいずれかを返します。 - -この例では、`TwainComponent.getQuote()`メソッドは、`quote`プロパティが`Observable`を返すと伝えています。 - - - -`TwainComponent`は、注入された`TwainService`から引用を取得します。 -コンポーネントは、サービスが最初の引用を返す前に、プレースホルダー値(`'...'`)で返された`Observable`を開始します。 +```ts +userAuth = TestBed.inject(UserAuthentication); +``` -`catchError`はサービスエラーをインターセプトし、エラーメッセージを準備し、成功チャネルでプレースホルダー値を返します。 +HELPFUL: `TestBed.inject()`が機能しないユースケースについては、コンポーネントのインジェクターからサービスを取得する必要がある場合とその理由を説明している[_コンポーネントプロバイダーのオーバーライド_](#override-component-providers)セクションを参照してください。 -これらはすべて、テストしたい機能です。 +### 最終的なセットアップとテスト {#final-setup-and-tests} -### スパイによるテスト {#testing-with-a-spy} +`TestBed.inject()`を使用した完全な`beforeEach()`は次のとおりです。 -コンポーネントをテストする場合は、サービスのパブリックAPIのみが問題になります。 -一般的に、テスト自体がリモートサーバーへの呼び出しを行わないようにする必要があります。 -そのような呼び出しをエミュレートする必要があります。 -この`app/twain/twain.component.spec.ts`のセットアップは、その方法の1つを示しています。 +```ts +let fixture: ComponentFixture; +let comp: Welcome; +let userAuth: UserAuthentication; // the TestBed injected service +let el: HTMLElement; // the DOM element with the welcome message - +beforeEach(() => { + fixture = TestBed.createComponent(Welcome); + comp = fixture.componentInstance; -スパイに注目してください。 + // UserAuthentication from the root injector + userAuth = TestBed.inject(UserAuthentication); - + // get the "welcome" element by CSS selector (e.g., by class name) + el = fixture.nativeElement.querySelector('.welcome'); +}); +``` -スパイは、`getQuote`への呼び出しが、テスト引用を含むObservableを受け取るように設計されています。 -実際の`getQuote()`メソッドとは異なり、このスパイはサーバーをバイパスし、値がすぐに利用可能な同期Observableを返します。 +そして、いくつかのテストを以下に示します。 -このスパイを使用すると、`Observable`が同期的なものであっても、多くの役立つテストを作成できます。 +```ts +it('should welcome the user', async () => { + await fixture.whenStable(); + const content = el.textContent; -HELPFUL: スパイの使用は、テストに必要なものに限定するのが最善です。必要なもの以上のモックやスパイを作成すると、壊れやすくなる可能性があります。コンポーネントとインジェクタブルが進化するにつれて、関連のないテストは、それ以外ではテストに影響を与えないのに十分な動作をモックしなくなったために失敗する可能性があります。 + expect(content, '"Welcome ..."').toContain('Welcome'); + expect(content, 'expected name').toContain('Test User'); +}); -### `fakeAsync()`による非同期テスト {#async-test-with-fakeasync} +it('should welcome "Bubba"', async () => { + userAuth.user.set({name: 'Bubba'}); // welcome message hasn't been shown yet + await fixture.whenStable(); -`fakeAsync()`機能を使用するには、テストセットアップファイルに`zone.js/testing`をインポートする必要があります。 -Angular CLIでプロジェクトを作成した場合、`zone-testing`はすでに`src/test.ts`にインポートされています。 + expect(el.textContent).toContain('Bubba'); +}); -次のテストは、サービスが`ErrorObservable`を返した場合に期待される動作を確認します。 +it('should request login if not logged in', async () => { + userAuth.isLoggedIn.set(false); // welcome message hasn't been shown yet + await fixture.whenStable(); + const content = el.textContent; - + expect(content, 'not welcomed').not.toContain('Welcome'); + expect(content, '"log in"').toMatch(/log in/i); +}); +``` -HELPFUL: `it()`関数は、次の形式の引数を受け取ります。 +1つ目はサニティテスト(健全性確認)です。これは`UserAuthentication`が呼び出され、機能していることを確認します。 - +HELPFUL: `expect`の2番目の引数(たとえば`'expected name'`)は、オプションの失敗ラベルです。 +期待値の検証(expectation)が失敗した場合、Vitestはこのラベルを検証失敗メッセージに追加します。 +複数の検証を含むスペックでは、何が間違っていたのか、どの検証が失敗したのかを明確にするのに役立ちます。 -fakeAsync(() => { /_test body_/ }) +残りのテストは、サービスが異なる値を返すときのコンポーネントのロジックを確認します。 +2番目のテストは、ユーザー名を変更した際の影響を検証します。 +3番目のテストは、ログインしているユーザーがいない場合にコンポーネントが適切なメッセージを表示することを確認します。 - +## 非同期サービスを使用するコンポーネント {#component-with-async-service} -`fakeAsync()`関数は、特別な`fakeAsync test zone`でテスト本体を実行することで、線形のコーディングスタイルを可能にします。 -テスト本体は同期的に見えるようになります。 -制御フローを妨げるネストされた構文(`Promise.then()`など)はありません。 +このサンプルでは、`About`コンポーネントテンプレートが`Twain`コンポーネントをホストしています。 +`Twain`コンポーネントはマーク・トウェインの名言を表示します。 -HELPFUL: 制限事項: `fakeAsync()`関数は、テスト本体が`XMLHttpRequest`(XHR)呼び出しをすると機能しません。 -テスト内のXHR呼び出しはまれですが、XHRを呼び出す必要がある場合は、`waitForAsync()`を使用してください。 +```angular-html +

+ {{ quote | async }} +

+ +@if (errorMessage()) { +

{{ errorMessage() }}

+} +``` -IMPORTANT: `fakeAsync`ゾーン内で発生する非同期タスクは、`flush`または`tick`を使用して手動で実行する必要があることに注意してください。 -`fakeAsync`テストヘルパーを使用して時間を進めずに、完了するまで待つと(つまり、`fixture.whenStable`を使用)、テストは失敗する可能性が高いです。 -詳細については、以下を参照してください。 +HELPFUL: コンポーネントの`quote`プロパティの値は`AsyncPipe`を通過します。 +つまり、このプロパティは`Promise`または`Observable`を返します。 -### `tick()`関数 {#the-tick-function} +この例では、`TwainQuotes.getQuote()`メソッドにより、`quote`プロパティが`Observable`を返すことがわかります。 -[tick()](api/core/testing/tick)を呼び出して、仮想クロックを進める必要があります。 +```ts +getQuote() { + this.errorMessage.set(''); + this.quote = this.twainQuotes.getQuote().pipe( + startWith('...'), + catchError((err: any) => { + this.errorMessage.set(err.message || err.toString()); + return of('...'); // reset message to placeholder + }), + ); +} +``` -[tick()](api/core/testing/tick)を呼び出すと、保留中の非同期アクティビティがすべて完了するまで、仮想クロックが前進します。 -この場合、Observableの`setTimeout()`を待ちます。 +`Twain`コンポーネントは、注入された`TwainQuotes`から名言を取得します。 +コンポーネントは、サービスが最初の名言を返す前に、返された`Observable`をプレースホルダー値(`'...'`)で開始します。 -[tick()](api/core/testing/tick)関数は、`millis`と`tickOptions`をパラメーターとして受け取ります。`millis`パラメーターは、仮想クロックがどれだけ進むかを指定し、指定されていない場合はデフォルトで`0`になります。 -たとえば、`fakeAsync()`テストに`setTimeout(fn, 100)`がある場合、`fn`のコールバックをトリガーするには、`tick(100)`を使用する必要があります。 -オプションの`tickOptions`パラメーターには、`processNewMacroTasksSynchronously`という名前のプロパティがあります。`processNewMacroTasksSynchronously`プロパティは、ティック時に新しい生成されたマクロタスクを呼び出すかどうかを表し、デフォルトでは`true`です。 +`catchError`はサービスエラーを傍受し、エラーメッセージを準備して、成功チャネルでプレースホルダー値を返します。 - +これらはすべてテストしたい機能です。 -[tick()](api/core/testing/tick)関数は、`TestBed`でインポートするAngularのテストユーティリティの1つです。 -これは`fakeAsync()`の仲間であり、`fakeAsync()`本体内でのみ呼び出すことができます。 +### `HttpTestingController`を使用したHTTPリクエストのモックによるテスト {#testing-by-mocking-http-requests-with-the-httptestingcontroller} -### tickOptions {#tickoptions} +コンポーネントをテストする場合、サービスのパブリックAPIのみが重要であるべきです。 +一般に、テスト自体がリモートサーバーを呼び出すべきではありません。 +テストはそのような呼び出しをエミュレートする必要があります。 -この例では、ネストされた`setTimeout`関数が新しいマクロタスクです。デフォルトでは、`tick`がsetTimeoutの場合、`outside`と`nested`の両方がトリガーされます。 +非同期サービスがリモートデータをロードするために`HttpClient`に依存している場合、`HttpTestingController`を使用してHTTPレベルでモックレスポンスを返すことを推奨します。 - +`HttpBackend`のモック作成の詳細については、[専用ガイド](guide/http/testing)を参照してください。 -場合によっては、ティック時に新しいマクロタスクをトリガーしたくない場合があります。`tick(millis, {processNewMacroTasksSynchronously: false})`を使用して、新しいマクロタスクを呼び出さないようにすることができます。 +### サービスのスタブ実装を提供することによるテスト {#testing-by-providing-a-stubbed-implementation-of-a-service} - +HTTPレベルでの非同期リクエストのモックが不可能な場合、代替手段としてスパイを活用します。 -### `fakeAsync()`内の日付の比較 {#comparing-dates-inside-fakeasync} +この`app/twain/twain-quotes.spec.ts`の設定は、その方法の1つを示しています。 -`fakeAsync()`は時間の経過をシミュレートするため、`fakeAsync()`内で日付の差を計算することができます。 +```ts {header: "twain.spec.ts"} +class TwainQuotesStub implements TwainQuotes { + private testQuote = 'Test Quote'; - + getQuote() { + return of(this.testQuote); + } -### `jasmine.clock`と`fakeAsync()` {#jasmineclock-with-fakeasync} + // ... Implement everything to conform to the API +} -Jasmineは、日付をモックするための`clock`機能も提供しています。 -Angularは、`jasmine.clock().install()`が`fakeAsync()`メソッド内で呼び出された後、`jasmine.clock().uninstall()`が呼び出されるまで実行されるテストを自動的に実行します。 -`fakeAsync()`は必要なく、ネストされている場合はエラーをスローします。 +beforeEach(async () => { + TestBed.configureTestingModule({ + providers: [{provide: TwainQuotes, useClass: TwainQuotesStub}], + }); -デフォルトでは、この機能はオフになっています。 -有効にするには、`zone-testing`をインポートする前にグローバルフラグを設定します。 + fixture = TestBed.createComponent(Twain); + component = fixture.componentInstance; + await fixture.whenStable(); + quoteEl = fixture.nativeElement.querySelector('.twain'); +}); +``` -Angular CLIを使用している場合は、`src/test.ts`でこのフラグを設定します。 +スタブ実装がどのように元の実装を置き換えるかに注目してください。 ```ts -[window as any]('__zone_symbol__fakeAsyncPatchLock') = true; -import 'zone.js/testing'; +TestBed.configureTestingModule({ + providers: [{provide: TwainQuotes, useClass: TwainQuotesStub}], +}); ``` - - -### `fakeAsync()`内のRxJSスケジューラーの使用 {#using-the-rxjs-scheduler-inside-fakeasync} - -`setTimeout()`や`setInterval()`と同様に、`fakeAsync()`でRxJSスケジューラーを使用することもできますが、RxJSスケジューラーをパッチするために`zone.js/plugins/zone-patch-rxjs-fake-async`をインポートする必要があります。 - - - -### より多くのmacroTasksをサポートする {#support-more-macrotasks} +スタブは、それを注入するコンポーネントやサービスがスタブ化された実装を受け取るように設計されています。 +つまり、`getQuote`への呼び出しはすべて、テスト用の名言を含むObservableを受け取ります。 -デフォルトでは、`fakeAsync()`は次のmacroTasksをサポートしています。 +実際の`getQuote()`メソッドとは異なり、このスパイはサーバーをバイパスし、値が即座に利用可能な同期Observableを返します。 -- `setTimeout` -- `setInterval` -- `requestAnimationFrame` -- `webkitRequestAnimationFrame` -- `mozRequestAnimationFrame` +### Vitestのフェイクタイマーを使用した非同期テスト {#async-test-with-a-vitest-fake-timers} -`HTMLCanvasElement.toBlob()`などの他のmacroTasksを実行すると、_「fake async testで不明なmacroTaskがスケジュールされました」_というエラーがスローされます。 +`setTimeout`や`Promise`のような非同期関数をモックするには、Vitestのフェイクタイマーを活用して発火のタイミングを制御できます。 - - - - - -このような場合をサポートしたい場合は、サポートするmacroTasksを`beforeEach()`で定義する必要があります。 -たとえば、次のようになります。 - - - -HELPFUL: アプリで``要素をZone.js対応にするには、`zone-patch-canvas`パッチをインポートする必要があります(`polyfills.ts`または``を使用する特定のファイルにインポートします)。 - - - -### 非同期Observable {#async-observables} - -これらのテストのテストカバレッジに満足しているかもしれません。 - -しかし、実際のサービスが完全にこのようには動作していないという事実で悩んでいるかもしれません。 -実際のサービスは、リモートサーバーにリクエストを送信します。 -サーバーは応答するまでに時間がかかり、応答は前の2つのテストのようにすぐに利用できるわけではありません。 - -テストでは、次のように`getQuote()`スパイから_非同期_Observableを返すと、現実の世界をより忠実に反映することができます。 +```ts +it('should display error when TwainQuotes service fails', async () => { + class TwainQuotesStub implements TwainQuotes { + getQuote() { + return defer(() => { + return new Promise((_, reject) => { + setTimeout(() => reject('TwainService test failure')); + }); + }); + } + + // ... Implement everything to conform to the API + } + + TestBed.configureTestingModule({ + providers: [{provide: TwainQuotes, useClass: TwainQuotesStub}], + }); + + vi.useFakeTimers(); // setting up the fake timers + const fixture = TestBed.createComponent(TwainComponent); + + // rendering isn't async, we need to flush + await vi.runAllTimersAsync(); + + await expect(fixture.nativeElement.querySelector('.error')!.textContent).toMatch(/test failure/); + expect(fixture.nativeElement.querySelector('.twain')!.textContent).toBe('...'); + + vi.useRealTimers(); // resets to regular async execution +}); +``` - +### その他の非同期テスト {#more-async-tests} -### 非同期Observableのヘルパー {#async-observable-helpers} +スタブサービスが非同期Observableを返す場合、テストのほとんども非同期である必要があります。 -非同期Observableは、`asyncData`ヘルパーによって生成されました。 -`asyncData`ヘルパーは、自分で記述するか、サンプルコードからこのヘルパーをコピーする必要があるユーティリティ関数です。 +ここに、実世界で想定されるデータフローを示すテストがあります。 - +```ts +it('should show quote after getQuote', async () => { + class MockTwainQuotes implements TwainQuotes { + private subject = new Subject(); -このヘルパーのObservableは、JavaScriptエンジンの次のターンで`data`値を発行します。 + getQuote() { + return this.subject.asObservable(); + } -[RxJS `defer()`演算子](http://reactivex.io/documentation/operators/defer.html)は、Observableを返します。 -これは、`Promise`または`Observable`のいずれかを返すファクトリ関数を取得します。 -何かが_defer_のObservableを購読すると、そのファクトリで作成された新しいObservableに購読者を追加します。 + emit(val: string) { + this.subject.next(val); + } + } -`defer()`演算子は、`Promise.resolve()`を、`HttpClient`のように一度発行して完了する新しいObservableに変換します。 -購読者は、`data`値を受け取ると購読解除されます。 + it('should show quote after getQuote (success)', async () => { + vi.useFakeTimers(); -非同期エラーを生成するための同様のヘルパーがあります。 + TestBed.configureTestingModule({ + providers: [{provide: TwainQuotes, useClass: MockTwainQuotes}], + }); - + const fixture = TestBed.createComponent(TwainComponent); + const twainQuotes = TestBed.inject(TwainQuotes) as MockTwainQuotes; + await vi.runAllTimersAsync(); // render before the quote is recieved -### さらなる非同期テスト {#more-async-tests} + const quoteEl = fixture.nativeElement.querySelector('.twain'); + expect(quoteEl.textContent).toBe('...'); -`getQuote()`スパイが非同期Observableを返すようになったので、ほとんどのテストも非同期にする必要があります。 + twainQuotes.emit('Twain Quote'); // emits the quote + await vi.runAllTimersAsync(); // render with the quote received -次に、現実の世界で期待されるデータフローを示す`fakeAsync()`テストを示します。 + expect(quoteEl.textContent).toBe('Twain Quote'); + expect(fixture.nativeElement.querySelector('.error')).toBeNull(); - + vi.useRealTimers(); + }); +}); +``` -引用要素には、`ngOnInit()`の後、プレースホルダー値(`'...'`)が表示されます。 -まだ最初の引用は届いていません。 +最初のレンダリング時に、名言要素がプレースホルダー値(`'...'`)を表示していることに注目してください。 +最初の名言はまだ到着していません。 -Observableから最初の引用をフラッシュするには、[tick()](api/core/testing/tick)を呼び出します。 -次に、`detectChanges()`を呼び出して、Angularに画面を更新するように指示します。 +その後、名言要素が期待されるテキストを表示していることをアサートできます。 -その後、引用要素に期待どおりのテキストが表示されていることをアサートできます。 +### `zone.js`と`fakeAsync`を使用した非同期テスト {#async-tests-with-zonejs-and-fakeasync} -### `fakeAsync()`を使わない非同期テスト {#async-test-without-fakeasync} +`fakeAsync`ヘルパー関数は、`zone.js`で非同期APIにパッチを当てることに依存するもう1つのモッククロックです。これは`zone.js`ベースのアプリケーションのテストで一般的に使用されていました。`fakeAsync`の使用は現在推奨されていません。 -次に、前の`fakeAsync()`テストを`async`で書き直したものを示します。 +TIP: ネイティブの非同期テスト戦略や、VitestやJasmineなどの他のフェイクタイマー(モッククロックとも呼ばれます)を使用することを推奨します。 - +IMPORTANT: `fakeAsync`はVitestテストランナーでは使用できません。このランナーには`zone.js`パッチが適用されないためです。 -### `whenStable` {#whenstable} +## 入力と出力を持つコンポーネント {#component-with-inputs-and-outputs} -テストは、`getQuote()`Observableが次の引用を発行するまで待つ必要があります。 -[tick()](api/core/testing/tick)を呼び出す代わりに、`fixture.whenStable()`を呼び出します。 +入力と出力を持つコンポーネントは通常、ホストコンポーネントのビューテンプレート内に現れます。 +ホストはプロパティバインディングを使用して入力プロパティを設定し、イベントバインディングを使用して出力プロパティによって発生したイベントをリッスンします。 -`fixture.whenStable()`は、JavaScriptエンジンのタスクキューが空になったときに解決されるプロミスを返します。 -この例では、タスクキューはObservableが最初の引用を発行したときに空になります。 +テストの目的は、そのようなバインディングが期待どおりに機能することを確認することです。 +テストでは、入力値を設定し、出力イベントをリッスンする必要があります。 -## 入力と出力を持つコンポーネント {#component-with-inputs-and-outputs} +`DashboardHero`コンポーネントは、この役割を果たすコンポーネントの小さな例です。 +これは、`Dashboard`コンポーネントによって提供される個々のヒーローを表示します。 +そのヒーローをクリックすると、ユーザーがそのヒーローを選択したことが`Dashboard`コンポーネントに通知されます。 -入力と出力を持つコンポーネントは、通常、ホストコンポーネントのビューテンプレート内に表示されます。 -ホストは、プロパティバインディングを使用して入力プロパティを設定し、イベントバインディングを使用して出力プロパティによって発生したイベントをリスンします。 +`DashboardHero`コンポーネントは、次のように`Dashboard`コンポーネントのテンプレートに埋め込まれています。 -テストの目標は、そのようなバインディングが期待どおりに機能することを確認することです。 -テストでは、入力値を設定し、出力イベントをリスンする必要があります。 +```angular-html +@for (hero of heroes; track hero) { + +} +``` -`DashboardHeroComponent`は、この役割を果たすコンポーネントの小さな例です。 -これは、`DashboardComponent`によって提供された個々のヒーローを表示します。 -そのヒーローをクリックすると、`DashboardComponent`にユーザーがヒーローを選択したことを伝えます。 +`DashboardHero`コンポーネントは`@for`ブロック内に現れ、各コンポーネントの`hero`入力プロパティをループ値に設定し、コンポーネントの`selected`イベントをリッスンします。 + +コンポーネントの完全な定義は次のとおりです。 + +```angular-ts +@Component({ + selector: 'dashboard-hero', + imports: [UpperCasePipe], + template: ` + + `, +}) +export class DashboardHero { + readonly hero = input.required(); + readonly selected = output(); + + click() { + this.selected.emit(this.hero()); + } +} +``` -`DashboardHeroComponent`は、次のように`DashboardComponent`テンプレートに埋め込まれています。 +このように単純なコンポーネントをテストすること自体にはあまり価値がありませんが、その方法を知っておくことには価値があります。 +次のアプローチのいずれかを使用します。 - +- `Dashboard`コンポーネントで使用されている状態でテストする +- スタンドアロンコンポーネントとしてテストする +- `Dashboard`コンポーネントの代替で使用されている状態でテストする -`DashboardHeroComponent`は`@for`ブロック内に表示され、各コンポーネントの`hero`入力プロパティはループする値に設定され、コンポーネントの`selected`イベントをリスンします。 +当面の目標は`Dashboard`コンポーネントではなく`DashboardHero`コンポーネントをテストすることなので、2番目と3番目のオプションを試します。 -コンポーネントの完全な定義を次に示します。 +### DashboardHeroコンポーネントを単体でテストする {#test-the-dashboardhero-component-standalone} - +スペックファイルのセットアップの要点は次のとおりです。 -この単純なコンポーネントをテストすることは、ほとんど内在的な価値はありませんが、テスト方法を知る価値はあります。 -次のいずれかの方法を使用します。 +```ts +let fixture: ComponentFixture; +let comp: DashboardHero; +let heroDe: DebugElement; +let heroEl: HTMLElement; +let expectedHero: Hero; -- `DashboardComponent`で使用されているようにテストする -- スタンドアロンコンポーネントとしてテストする -- `DashboardComponent`の代替として使用されているようにテストする +beforeEach(async () => { + fixture = TestBed.createComponent(DashboardHero); + comp = fixture.componentInstance; -当面の目標は、`DashboardComponent`ではなく`DashboardHeroComponent`をテストすることなので、2番目と3番目のオプションを試してみましょう。 + // find the hero's DebugElement and element + heroDe = fixture.debugElement.query(By.css('.hero')); + heroEl = heroDe.nativeElement; -### `DashboardHeroComponent`をスタンドアロンでテストする {#test-dashboardherocomponent-standalone} + // mock the hero supplied by the parent component + expectedHero = {id: 42, name: 'Test Name'}; -スペックファイルの設定の重要な部分を次に示します。 + // simulate the parent setting the input property with that hero + fixture.componentRef.setInput('hero', expectedHero); - + // wait for initial data binding + await fixture.whenStable(); +}); +``` -設定コードは、コンポーネントの`hero`プロパティにテストヒーロー(`expectedHero`)を割り当てています。これは、`DashboardComponent`がリピーターのプロパティバインディングを使用して設定する方法をエミュレートしています。 +セットアップコードがテスト用ヒーロー(`expectedHero`)をコンポーネントの`hero`プロパティに割り当てていることに注目してください。これは、`Dashboard`がリピーター内のプロパティバインディングを使用して設定する方法をエミュレートしています。 次のテストは、ヒーロー名がバインディングを使用してテンプレートに伝播されることを検証します。 - +```ts +it('should display hero name in uppercase', () => { + const expectedPipedName = expectedHero.name.toUpperCase(); + expect(heroEl.textContent).toContain(expectedPipedName); +}); +``` -テンプレートは、ヒーロー名をAngularの`UpperCasePipe`を通して渡すため、テストでは要素値を大文字の名前と照合する必要があります。 +テンプレートはAngularの`UpperCasePipe`を通してヒーロー名を渡すため、テストでは要素の値を大文字の名前に一致させる必要があります。 ### クリック {#clicking} -ヒーローをクリックすると、ホストコンポーネント(`DashboardComponent`と推測される)が聞き取ることができる`selected`イベントが発生するはずです。 +ヒーローをクリックすると、ホストコンポーネント(おそらく`Dashboard`)が検知できる`selected`イベントが発生するはずです。 + +```ts +it('should raise selected event when clicked (triggerEventHandler)', () => { + let selectedHero: Hero | undefined; + comp.selected.subscribe((hero: Hero) => (selectedHero = hero)); - + heroDe.triggerEventHandler('click'); + expect(selectedHero).toBe(expectedHero); +}); +``` -コンポーネントの`selected`プロパティは、`EventEmitter`を返します。これは、消費者にとってはRxJSの同期`Observable`のように見えます。 -テストは、ホストコンポーネントが_暗黙的に_行うように、_明示的に_これを購読します。 +コンポーネントの`selected`プロパティは`EventEmitter`を返します。これはコンシューマーにはRxJSの同期的な`Observable`のように見えます。 +テストは、ホストコンポーネントが_暗黙的に_行うのと同様に、_明示的に_それにサブスクライブします。 -コンポーネントが期待どおりに動作すれば、ヒーローの要素をクリックすると、コンポーネントの`selected`プロパティに`hero`オブジェクトを発行するように指示されます。 +コンポーネントが期待どおりに動作する場合、ヒーローの要素をクリックすると、コンポーネントの`selected`プロパティに`hero`オブジェクトを発行するように指示するはずです。 -テストは、`selected`への購読を通じてそのイベントを検出します。 +テストは、`selected`へのサブスクリプションを通じてそのイベントを検出します。 ### `triggerEventHandler` {#triggereventhandler} -前のテストの`heroDe`は、ヒーロー`
`を表す`DebugElement`です。 +前のテストの`heroDe`は、ヒーローの`
`を表す`DebugElement`です。 -これは、ネイティブ要素との対話を抽象化するAngularのプロパティとメソッドを持っています。 -このテストは、"click"イベント名で`DebugElement.triggerEventHandler`を呼び出します。 -"click"イベントバインディングは、`DashboardHeroComponent.click()`を呼び出すことで応答します。 +これには、ネイティブ要素との対話を抽象化するAngularのプロパティとメソッドがあります。 +このテストは、"click"イベント名を指定して`DebugElement.triggerEventHandler`を呼び出します。 +"click"イベントバインディングは、`DashboardHero.click()`を呼び出すことで応答します。 -Angularの`DebugElement.triggerEventHandler`は、_イベント名_を使用して、_データバインドされたイベント_を発生させることができます。 -2番目のパラメーターは、ハンドラーに渡されるイベントオブジェクトです。 +Angularの`DebugElement.triggerEventHandler`は、_イベント名_によって_任意のデータバインドされたイベント_を発生させることができます。 +2番目のパラメータは、ハンドラーに渡されるイベントオブジェクトです。 -テストでは、"click"イベントをトリガーしました。 +テストは"click"イベントをトリガーしました。 - +```ts +heroDe.triggerEventHandler('click'); +``` -この場合、テストでは、ランタイムイベントハンドラーであるコンポーネントの`click()`メソッドがイベントオブジェクトを気にしないことを正しく想定しています。 +この場合、テストは、ランタイムイベントハンドラーであるコンポーネントの`click()`メソッドがイベントオブジェクトを気にしないと正しく想定しています。 -HELPFUL: 他のハンドラーは、それほど寛容ではありません。 -たとえば、`RouterLink`ディレクティブは、クリック時にどのマウスボタンが押されたかを識別する`button`プロパティを持つオブジェクトを期待しています。 -`RouterLink`ディレクティブは、イベントオブジェクトが不足している場合、エラーをスローします。 +HELPFUL: 他のハンドラーはそれほど寛容ではありません。 +たとえば、`RouterLink`ディレクティブは、クリック中にどのマウスボタン(もしあれば)が押されたかを識別する`button`プロパティを持つオブジェクトを期待します。 +イベントオブジェクトがない場合、`RouterLink`ディレクティブはエラーをスローします。 ### 要素をクリックする {#click-the-element} -次のテストの代替案は、ネイティブ要素自身の`click()`メソッドを呼び出します。これは、_このコンポーネント_には完全に適しています。 +次のテストの代替案は、ネイティブ要素独自の`click()`メソッドを呼び出します。これは_このコンポーネント_にとっては全く問題ありません。 + +```ts +it('should raise selected event when clicked (element.click)', () => { + let selectedHero: Hero | undefined; + comp.selected.subscribe((hero: Hero) => (selectedHero = hero)); - + heroEl.click(); + expect(selectedHero).toBe(expectedHero); +}); +``` ### `click()`ヘルパー {#click-helper} ボタン、アンカー、または任意のHTML要素をクリックすることは、一般的なテストタスクです。 -次の`click()`関数のようなヘルパーに_クリックをトリガーする_プロセスをカプセル化することで、それを一貫性があり、簡単に行うことができます。 +次の`click()`関数のようなヘルパーに_クリックトリガー_プロセスをカプセル化することで、それを一貫性のある簡単なものにします。 - +```ts +/** Button events to pass to `DebugElement.triggerEventHandler` for RouterLink event handler */ +export const ButtonClickEvents = { + left: {button: 0}, + right: {button: 2}, +}; -最初の引数は、_クリックする要素_です。 -必要に応じて、2番目の引数としてカスタムイベントオブジェクトを渡すことができます。 -デフォルトは、`RouterLink`ディレクティブを含む多くのハンドラーで受け入れられる、部分的な[左ボタンのマウスイベントオブジェクト](https://developer.mozilla.org/docs/Web/API/MouseEvent/button)です。 +/** Simulate element click. Defaults to mouse left-button click event. */ +export function click( + el: DebugElement | HTMLElement, + eventObj: any = ButtonClickEvents.left, +): void { + if (el instanceof HTMLElement) { + el.click(); + } else { + el.triggerEventHandler('click', eventObj); + } +} +``` -IMPORTANT: `click()`ヘルパー関数は、Angularのテストユーティリティの**1つではありません**。 -これは、_このガイドのサンプルコード_で定義された関数です。 -サンプルテストはすべてこれを利用しています。 -気に入ったら、自分のヘルパーのコレクションに追加してください。 +最初のパラメータは_クリックする要素_です。 +必要に応じて、2番目のパラメータとしてカスタムイベントオブジェクトを渡します。 +デフォルトは、`RouterLink`ディレクティブを含む多くのハンドラーによって受け入れられる部分的な[左ボタンマウスイベントオブジェクト](https://developer.mozilla.org/docs/Web/API/MouseEvent/button)です。 -次に、クリックヘルパーを使用した前のテストを示します。 +IMPORTANT: `click()`ヘルパー関数は、Angularテストユーティリティの1つでは**ありません**。 +これは、_このガイドのサンプルコード_で定義されている関数です。 +すべてのサンプルテストでこれを使用しています。 +気に入ったら、独自のヘルパーコレクションに追加してください。 - +クリックヘルパーを使用して書き直した前のテストは次のとおりです。 -## テストホスト内のコンポーネント {#component-inside-a-test-host} +```ts +it('should raise selected event when clicked (click helper with DebugElement)', () => { + let selectedHero: Hero | undefined; + comp.selected.subscribe((hero: Hero) => (selectedHero = hero)); -前のテストは、ホスト`DashboardComponent`の役割を自身で演じていました。 -しかし、`DashboardHeroComponent`は、ホストコンポーネントに適切にデータバインドされている場合、正しく動作するでしょうか? + click(heroDe); // click helper with DebugElement - + expect(selectedHero).toBe(expectedHero); +}); +``` + +## テストホスト内のコンポーネント {#component-inside-a-test-host} -テストホストは、コンポーネントの`hero`入力プロパティをテストヒーローで設定します。 -これは、コンポーネントの`selected`イベントを`onSelected`ハンドラーにバインドし、これは`selectedHero`プロパティに発行されたヒーローを記録します。 +以前のテストは、それ自体がホストである`Dashboard`コンポーネントの役割を果たしていました。 +しかし、`DashboardHero`コンポーネントは、ホストコンポーネントに適切にデータバインディングされたときに正しく動作するでしょうか? + +```angular-ts +@Component({ + imports: [DashboardHero], + template: ` `, +}) +class TestHost { + hero: Hero = {id: 42, name: 'Test Name'}; + selectedHero: Hero | undefined; + + onSelected(hero: Hero) { + this.selectedHero = hero; + } +} +``` + +テストホストは、コンポーネントの`hero`入力プロパティにテスト用のヒーローを設定します。 +コンポーネントの`selected`イベントを自身の`onSelected`ハンドラーにバインドし、発行されたヒーローを自身の`selectedHero`プロパティに記録します。 -後で、テストは`selectedHero`をチェックして、`DashboardHeroComponent.selected`イベントが期待どおりのヒーローを発行したことを確認できます。 +その後、テストは`selectedHero`を確認して、`DashboardHero.selected`イベントが期待されるヒーローを発行したことを検証できます。 -`test-host`テストの設定は、スタンドアロンテストの設定に似ています。 +`test-host`テストのセットアップは、スタンドアロンテストのセットアップと似ています。 - +```ts +beforeEach(async () => { + // create TestHost instead of DashboardHero + fixture = TestBed.createComponent(TestHost); + testHost = fixture.componentInstance; + heroEl = fixture.nativeElement.querySelector('.hero'); + + await fixture.whenStable(); +}); +``` -このテストモジュール設定は、2つの重要な違いを示しています。 +このテストモジュールの構成には、2つの重要な違いがあります。 -- `DashboardHeroComponent`ではなく`TestHostComponent`を_作成_します -- `TestHostComponent`は、バインディングで`DashboardHeroComponent.hero`を設定します +- `DashboardHero`の代わりに`TestHost`コンポーネントを*作成*します +- `TestHost`コンポーネントはバインディングを使用して`DashboardHero.hero`を設定します -`createComponent`は、`DashboardHeroComponent`のインスタンスではなく、`TestHostComponent`のインスタンスを保持する`fixture`を返します。 +`createComponent`は、`DashboardHero`のインスタンスではなく、`TestHost`のインスタンスを保持する`fixture`を返します。 -`TestHostComponent`を作成すると、後者が前者のテンプレート内に表示されているため、`DashboardHeroComponent`が作成されます。 -ヒーロー要素(`heroEl`)のクエリは、テストDOM内で見つかりますが、前のテストよりも要素ツリーの深さが大きくなります。 +`TestHost`を作成すると、後者が前者のテンプレート内に現れるため、副作用として`DashboardHero`が作成されます。 +ヒーロー要素(`heroEl`)のクエリは、以前よりも要素ツリーの深い場所にありますが、依然としてテストDOM内でそれを見つけます。 -テスト自体は、スタンドアロンバージョンとほとんど同じです。 +テスト自体は、スタンドアロンバージョンとほぼ同じです。 - +```ts +it('should display hero name', () => { + const expectedPipedName = testHost.hero.name.toUpperCase(); + expect(heroEl.textContent).toContain(expectedPipedName); +}); + +it('should raise selected event when clicked', () => { + click(heroEl); + // selected hero should be the same data bound hero + expect(testHost.selectedHero).toBe(testHost.hero); +}); +``` -選択されたイベントテストのみが異なります。 -これは、選択された`DashboardHeroComponent`のヒーローが、実際にイベントバインディングを通じてホストコンポーネントに到達することを確認します。 +選択イベントのテストのみが異なります。 +これは、選択された`DashboardHero`のヒーローが、イベントバインディングを通じてホストコンポーネントまで実際に到達することを確認します。 ## ルーティングコンポーネント {#routing-component} -_ルーティングコンポーネント_は、`Router`に別のコンポーネントにナビゲートするように指示するコンポーネントです。 -`DashboardComponent`は、ユーザーがダッシュボードの_ヒーローボタン_の1つをクリックすることで`HeroDetailComponent`にナビゲートできるため、_ルーティングコンポーネント_です。 +_ルーティングコンポーネント_とは、`Router`に対して別のコンポーネントへナビゲートするよう指示するコンポーネントです。 +`Dashboard`コンポーネントは、ユーザーがダッシュボード上の_ヒーローボタン_の1つをクリックすることで`HeroDetail`コンポーネントへナビゲートできるため、_ルーティングコンポーネント_です。 -Angularは、`HttpClient`に依存するコードをより効果的にテストするために、テストヘルパーを提供しています。`provideRouter`関数はテストモジュール内でも直接使えます。 +Angularは、ボイラープレートを削減し、`HttpClient`に依存するコードをより効果的にテストするためのテストヘルパーを提供します。`provideRouter`関数は、テストモジュール内で直接使用できます。 - +```ts +beforeEach(async () => { + TestBed.configureTestingModule({ + providers: [ + provideRouter([{path: '**', component: Dashboard}]), + provideHttpClientTesting(), + HeroService, + ], + }); + harness = await RouterTestingHarness.create(); + comp = await harness.navigateByUrl('/', Dashboard); + TestBed.inject(HttpTestingController).expectOne('api/heroes').flush(getTestHeroes()); +}); +``` -次のテストは、表示されているヒーローをクリックし、期待されるURLにナビゲートしたことを確認します。 +次のテストは、表示されたヒーローをクリックし、期待されるURLへナビゲートすることを確認します。 - +```ts +it('should tell navigate when hero clicked', async () => { + // get first DebugElement + const heroDe = harness.routeDebugElement!.query(By.css('dashboard-hero')); + heroDe.triggerEventHandler('selected', comp.heroes[0]); + + // expecting to navigate to id of the component's first hero + const id = comp.heroes[0].id; + expect(TestBed.inject(Router).url, 'should nav to HeroDetail for first hero').toEqual( + `/heroes/${id}`, + ); +}); +``` ## ルーティングされたコンポーネント {#routed-components} _ルーティングされたコンポーネント_は、`Router`ナビゲーションの宛先です。 -特に、コンポーネントへのルートに_パラメーターが含まれている場合_、テストが難しくなる場合があります。 -`HeroDetailComponent`は、このようなルートの宛先である_ルーティングされたコンポーネント_です。 +特にコンポーネントへのルートに_パラメータが含まれている_場合、テストが難しくなることがあります。 +`HeroDetail`は、そのようなルートの宛先となる_ルーティングされたコンポーネント_です。 -ユーザーが_Dashboard_のヒーローをクリックすると、`DashboardComponent`は`Router`に`heroes/:id`にナビゲートするように指示します。 -`:id`は、編集するヒーローの`id`であるルートパラメーターです。 +ユーザーが_ダッシュボード_のヒーローをクリックすると、`Dashboard`は`Router`に`heroes/:id`へナビゲートするように指示します。 +`:id`はルートパラメータであり、その値は編集するヒーローの`id`です。 -`Router`は、そのURLを`HeroDetailComponent`へのルートと照合します。 -これは、ルーティング情報を持つ`ActivatedRoute`オブジェクトを作成し、`HeroDetailComponent`の新しいインスタンスに注入します。 +`Router`はそのURLを`HeroDetail`へのルートと照合します。 +ルーティング情報を持つ`ActivatedRoute`オブジェクトを作成し、それを`HeroDetail`の新しいインスタンスに注入します。 -次に、`HeroDetailComponent`に注入されたサービスを示します。 +`HeroDetail`に注入されるサービスは次のとおりです。 - +```ts +private heroDetailService = inject(HeroDetailService); +private route = inject(ActivatedRoute); +private router = inject(Router); +``` -`HeroDetail`コンポーネントは、`id`パラメーターが必要であり、これにより`HeroDetailService`を使用して対応するヒーローを取得できます。 +`HeroDetail`コンポーネントは、`HeroDetailService`を使用して対応するヒーローを取得するために`id`パラメータを必要とします。 コンポーネントは、`Observable`である`ActivatedRoute.paramMap`プロパティから`id`を取得する必要があります。 -コンポーネントは、`ActivatedRoute.paramMap`の`id`プロパティを参照することはできません。 -コンポーネントは、`ActivatedRoute.paramMap`Observableを_購読_し、ライフタイム中に`id`が変更される場合に備えておく必要があります。 +単に`ActivatedRoute.paramMap`の`id`プロパティを参照できません。 +コンポーネントは`ActivatedRoute.paramMap` observableを_サブスクライブ_し、そのライフサイクル中に`id`が変更されることに備える必要があります。 + +```ts +constructor() { + // get hero when `id` param changes + this.route.paramMap + .pipe(takeUntilDestroyed()) + .subscribe((pmap) => this.getHero(pmap.get('id'))); +} +``` + +テストでは、異なるルートにナビゲートすることで、`HeroDetail`が異なる`id`パラメータ値にどのように応答するかを確認できます。 - +## ネストされたコンポーネントのテスト {#nested-component-tests} -テストでは、異なるルートにナビゲートすることで、`HeroDetailComponent`が異なる`id`パラメーター値にどのように応答するかを調べることができます。 +コンポーネントテンプレートには、ネストされたコンポーネントが含まれることがよくあり、そのテンプレートにもさらにコンポーネントが含まれる場合があります。 -## ネストされたコンポーネントテスト {#nested-component-tests} +コンポーネントツリーは非常に深くなることがあり、ネストされたコンポーネントがツリーの最上位にあるコンポーネントのテストにおいて何の役割も果たさない場合があります。 -コンポーネントテンプレートには、多くの場合、ネストされたコンポーネントが含まれています。そのテンプレートには、さらに多くのコンポーネントが含まれている場合があります。 +たとえば、`App`コンポーネントは、アンカーとそれらの`RouterLink`ディレクティブを持つナビゲーションバーを表示します。 -コンポーネントツリーは非常に深くなる可能性があり、ネストされたコンポーネントはツリーの先頭に配置されたコンポーネントをテストする際に役割を果たさないことがあります。 +```angular-html + + -たとえば、`AppComponent`は、アンカーと`RouterLink`ディレクティブを持つナビゲーションバーを表示します。 + - + +``` -ナビゲーションではなくリンクを検証するために、`Router`が_ルーティングされたコンポーネント_を挿入する場所を示す``も必要ありません。 +ナビゲーションではなくリンクを検証するには、ナビゲートするための`Router`は必要ありませんし、`Router`が*ルーティングされたコンポーネント*を挿入する場所を示すための``も必要ありません。 -`BannerComponent`と`WelcomeComponent`(``と``で示されています)も関係ありません。 +`Banner`および`Welcome`コンポーネント(``および``で示される)も無関係です。 -しかし、DOMに`AppComponent`を作成するテストは、これらの3つのコンポーネントのインスタンスも作成し、それを許可した場合、`TestBed`を設定してそれらを作成する必要があります。 +しかし、DOM内に`App`コンポーネントを作成するテストはすべて、これら3つのコンポーネントのインスタンスも作成します。もしそれを許可する場合、それらを作成するように`TestBed`を設定する必要があります。 -それらを宣言することを怠ると、Angularコンパイラーは`AppComponent`テンプレートの``、``、および``タグを認識せず、エラーをスローします。 +それらを宣言し忘れると、Angularコンパイラは`App`テンプレート内の``、``、および``タグを認識せず、エラーをスローします。 -実際のコンポーネントを宣言すると、_それら_のネストされたコンポーネントも宣言し、ツリー内の_任意の_コンポーネントに注入された_すべての_サービスを提供する必要があります。 +実際のコンポーネントを宣言する場合、*それらの*ネストされたコンポーネントも宣言し、ツリー内の*任意の*コンポーネントに注入される*すべての*サービスを提供する必要があります。 -このセクションでは、セットアップを最小限にするための2つのテクニックについて説明します。 -これらを単独で、または組み合わせて使用して、主要なコンポーネントのテストに集中してください。 +このセクションでは、セットアップを最小限に抑えるための2つのテクニックについて説明します。 +プライマリコンポーネントのテストに集中するために、それらを単独で、または組み合わせて使用してください。 ### 不要なコンポーネントのスタブ化 {#stubbing-unneeded-components} -最初のテクニックでは、テストでほとんど役割を果たさないコンポーネントとディレクティブのスタブバージョンを作成して宣言します。 +最初のテクニックでは、テストにおいてほとんど、あるいはまったく役割を果たさないコンポーネントやディレクティブのスタブバージョンを作成して宣言します。 + +```ts +@Component({selector: 'app-banner', template: ''}) +class BannerStub {} + +@Component({selector: 'router-outlet', template: ''}) +class RouterOutletStub {} - +@Component({selector: 'app-welcome', template: ''}) +class WelcomeStub {} +``` -スタブセレクターは、対応する実際のコンポーネントのセレクターと一致します。 +スタブのセレクターは、対応する実際のコンポーネントのセレクターと一致します。 しかし、それらのテンプレートとクラスは空です。 -次に、`TestBed.overrideComponent`を使用してコンポーネントの`imports`をオーバーライドして宣言します。 +次に、`TestBed.overrideComponent`を使用してコンポーネントの`imports`をオーバーライドすることで、それらを宣言します。 - +```ts +let comp: App; +let fixture: ComponentFixture; + +beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideRouter([]), UserAuthentication], + }).overrideComponent(App, { + set: { + imports: [RouterLink, BannerStub, RouterOutletStub, WelcomeStub], + }, + }); + + fixture = TestBed.createComponent(App); + comp = fixture.componentInstance; +}); +``` -HELPFUL: この例の`set`キーはコンポーネントの既存のimportsをすべて置き換えます。スタブだけでなく、すべての依存関係をimportするようにしてください。または、`remove`/`add`キーを使用してimportsを選択的に削除および追加できます。 +HELPFUL: この例の`set`キーは、コンポーネント上の既存のすべてのインポートを置き換えます。スタブだけでなく、すべての依存関係をインポートするようにしてください。あるいは、`remove`/`add`キーを使用して、インポートを選択的に削除および追加できます。 -### `NO_ERRORS_SCHEMA` {#no_errors_schema} +### `NO_ERRORS_SCHEMA` {#noerrorsschema} -2番目の方法では、コンポーネントのメタデータオーバーライドに`NO_ERRORS_SCHEMA`を追加します。 +2つ目のアプローチでは、コンポーネントのメタデータオーバーライドに`NO_ERRORS_SCHEMA`を追加します。 - +```ts +beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideRouter([]), UserAuthentication], + }).overrideComponent(App, { + set: { + imports: [], // resets all imports + schemas: [NO_ERRORS_SCHEMA], + }, + }); +}); +``` -`NO_ERRORS_SCHEMA`は、Angularコンパイラーに、認識されていない要素と属性を無視するように指示します。 +`NO_ERRORS_SCHEMA`は、認識されない要素や属性を無視するようにAngularコンパイラに指示します。 -コンパイラーは、`TestBed`設定で対応する`AppComponent`と`RouterLink`を宣言したため、``要素と`routerLink`属性を認識します。 +コンパイラは、`TestBed`構成で対応する`App`コンポーネントと`RouterLink`を宣言したため、``要素と`routerLink`属性を認識します。 -しかし、コンパイラーは``、``、または``に遭遇してもエラーをスローしません。 +しかし、コンパイラは``、``、または``に遭遇してもエラーをスローしません。 単にそれらを空のタグとしてレンダリングし、ブラウザはそれらを無視します。 -スタブコンポーネントは不要になりました。 +スタブコンポーネントはもう必要ありません。 -### 2つのテクニックを組み合わせて使用する {#use-both-techniques-together} +### 両方のテクニックを併用する {#use-both-techniques-together} -これらは、_シャローコンポーネントテスト_のためのテクニックであり、コンポーネントの視覚的な表面を、テストにとって重要なコンポーネントのテンプレート内の要素だけに制限するため、そう呼ばれています。 +これらは*浅いコンポーネントテスト(Shallow Component Testing)*のためのテクニックであり、コンポーネントの視覚的な表面を、テストにとって重要なコンポーネントテンプレート内の要素だけに縮小することからそう呼ばれています。 -`NO_ERRORS_SCHEMA`アプローチは2つのうちより簡単ですが、使い過ぎないでください。 +`NO_ERRORS_SCHEMA`アプローチは2つのうち簡単な方ですが、使いすぎないようにしてください。 -`NO_ERRORS_SCHEMA`は、コンパイラーが意図的に省略した、または誤ってスペルミスをした、見逃したコンポーネントと属性について警告するのを防ぎます。 -コンパイラーが瞬時に検出できたはずの幽霊バグを追いかけて何時間も無駄にする可能性があります。 +`NO_ERRORS_SCHEMA`はまた、うっかり省略したりスペルミスしたりした不足しているコンポーネントや属性について、コンパイラが通知するのを防ぎます。 +コンパイラなら一瞬で発見できたはずの幻のバグを追いかけるのに、何時間も無駄にする可能性があります。 -_スタブコンポーネント_アプローチには、もう1つの利点があります。 -_この_例ではスタブは空でしたが、テストでそれらと何らかの形で対話する必要がある場合は、縮小されたテンプレートとクラスを与えることができます。 +*スタブコンポーネント*アプローチには別の利点があります。 +*この*例のスタブは空でしたが、テストにおいて何らかの方法でそれらと対話する必要がある場合は、機能を削ぎ落としたテンプレートやクラスを与えることができます。 -実際には、次の例のように、2つのテクニックを同じセットアップに組み合わせます。 +実際には、この例に見られるように、同じセットアップで2つのテクニックを組み合わせることになります。 - +```ts +beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideRouter([]), UserAuthentication], + }).overrideComponent(App, { + remove: {imports: [RouterOutlet, Welcome]}, + set: {schemas: [NO_ERRORS_SCHEMA]}, + }); +}); +``` -Angularコンパイラーは、``要素に対して`BannerStubComponent`を作成し、`routerLink`属性を持つアンカーに`RouterLink`を適用しますが、``と``タグは無視します。 +Angularコンパイラは、``要素に対して`BannerStub`を作成し、`routerLink`属性を持つアンカーに`RouterLink`を適用しますが、``および``タグは無視します。 ### `By.directive`と注入されたディレクティブ {#bydirective-and-injected-directives} -さらに少しセットアップすると、初期データバインディングがトリガーされ、ナビゲーションリンクへの参照が取得されます。 +もう少しセットアップを行うと、初期データバインディングがトリガーされ、ナビゲーションリンクへの参照が取得されます。 - +```ts +beforeEach(async () => { + await fixture.whenStable(); -3つの重要なポイントを次に示します。 + // find DebugElements with an attached RouterLinkStubDirective + linkDes = fixture.debugElement.queryAll(By.directive(RouterLink)); -- `By.directive`を使用して、アタッチされたディレクティブを持つアンカー要素を見つけます -- クエリは、一致する要素をラップする`DebugElement`ラッパーを返します -- 各`DebugElement`は、その要素にアタッチされたディレクティブの特定のインスタンスを含む依存関係インジェクターを公開します + // get attached link directive instances + // using each DebugElement's injector + routerLinks = linkDes.map((de) => de.injector.get(RouterLink)); +}); +``` -`AppComponent`が検証するリンクは次のとおりです。 +特に興味深い3つのポイント: - +- `By.directive`を使用して、ディレクティブがアタッチされたアンカー要素を見つけます +- クエリは、一致する要素をラップした`DebugElement`を返します +- 各`DebugElement`は、その要素にアタッチされたディレクティブの特定のインスタンスを持つ依存性の注入(DI)を公開します -次に、これらのリンクが期待どおりに`routerLink`ディレクティブに配線されていることを確認するテストを示します。 +検証する`App`コンポーネントのリンクは次のとおりです。 - +```angular-html + +``` -## `page`オブジェクトの使用 {#use-a-page-object} +これらのリンクが期待どおりに`routerLink`ディレクティブに接続されていることを確認するいくつかのテストを以下に示します。 -`HeroDetailComponent`は、タイトル、2つのヒーローフィールド、2つのボタンを持つ単純なビューです。 +```ts +it('can get RouterLinks from template', () => { + expect(routerLinks.length, 'should have 3 routerLinks').toBe(3); + expect(routerLinks[0].href).toBe('/dashboard'); + expect(routerLinks[1].href).toBe('/heroes'); + expect(routerLinks[2].href).toBe('/about'); +}); -しかし、この単純な形式でも、テンプレートの複雑さはたくさんあります。 +it('can click Heroes link in template', async () => { + const heroesLinkDe = linkDes[1]; // heroes link DebugElement - + TestBed.inject(Router).resetConfig([{path: '**', children: []}]); + heroesLinkDe.triggerEventHandler('click', {button: 0}); -コンポーネントをテストするものは、… + await fixture.whenStable(); -- ヒーローが到着するまで待つ必要がある + expect(TestBed.inject(Router).url).toBe('/heroes'); +}); +``` + +## `page`オブジェクトの使用 {#use-a-page-object} + +`HeroDetail`コンポーネントは、タイトル、2つのヒーローフィールド、2つのボタンを持つシンプルなビューです。 + +しかし、この単純なフォームでさえ、テンプレートには多くの複雑さがあります。 + +```angular-html +@if (hero) { +
+

+ {{ hero.name | titlecase }} Details +

+
id: {{ hero.id }}
+
+ + +
+ + +
+} +``` + +コンポーネントを検証するテストには以下が必要です… + +- DOMに要素が表示される前に、ヒーローが到着するのを待つこと - タイトルテキストへの参照 - 検査および設定するための名前入力ボックスへの参照 -- クリックできる2つのボタンへの参照 +- クリックできるようにするための2つのボタンへの参照 -このような小さなフォームでも、むち打ちの条件付きセットアップとCSS要素の選択の混乱を招く可能性があります。 +このような小さなフォームでさえ、条件付きセットアップやCSS要素選択が入り組んで混乱を招く可能性があります。 -コンポーネントのプロパティへのアクセスを処理し、それらを設定するロジックをカプセル化する`Page`クラスを使用して、複雑さを抑制します。 +コンポーネントプロパティへのアクセスを処理し、それらを設定するロジックをカプセル化する`Page`クラスを使用して、複雑さを制御します。 -次に、`hero-detail.component.spec.ts`の`Page`クラスを示します。 +以下は、`hero-detail.component.spec.ts`用のそのような`Page`クラスです。 - +```ts +class Page { + // getter properties wait to query the DOM until called. + get buttons() { + return this.queryAll('button'); + } + get saveBtn() { + return this.buttons[0]; + } + get cancelBtn() { + return this.buttons[1]; + } + get nameDisplay() { + return this.query('span'); + } + get nameInput() { + return this.query('input'); + } + + //// query helpers //// + private query(selector: string): T { + return harness.routeNativeElement!.querySelector(selector)! as T; + } + + private queryAll(selector: string): T[] { + return harness.routeNativeElement!.querySelectorAll(selector) as any as T[]; + } +} +``` -これで、コンポーネントの操作と検査のための重要なフックが、整理され、`Page`のインスタンスからアクセスできるようになりました。 +これで、コンポーネントの操作と検査のための重要なフックが整理され、`Page`のインスタンスからアクセスできるようになりました。 -`createComponent`メソッドは、`page`オブジェクトを作成し、`hero`が到着すると空白を埋めます。 +`createComponent`メソッドは`page`オブジェクトを作成し、`hero`が到着すると空白を埋めます。 - +```ts +async function createComponent(id: number) { + harness = await RouterTestingHarness.create(); + component = await harness.navigateByUrl(`/heroes/${id}`, HeroDetail); + page = new Page(); + + const request = TestBed.inject(HttpTestingController).expectOne(`api/heroes/?id=${id}`); + const hero = getTestHeroes().find((h) => h.id === Number(id)); + request.flush(hero ? [hero] : []); + await harness.fixture.whenStable(); +} +``` -次に、ポイントを強化するための`HeroDetailComponent`のテストをいくつか示します。 +要点を強調するために、さらにいくつかの`HeroDetail`コンポーネントテストを以下に示します。 - +```ts +it("should display that hero's name", () => { + expect(page.nameDisplay.textContent).toBe(expectedHero.name); +}); + +it('should navigate when click cancel', () => { + click(page.cancelBtn); + expect(TestBed.inject(Router).url).toEqual(`/heroes/${expectedHero.id}`); +}); + +it('should save when click save but not navigate immediately', () => { + click(page.saveBtn); + expect(TestBed.inject(HttpTestingController).expectOne({method: 'PUT', url: 'api/heroes'})); + expect(TestBed.inject(Router).url).toEqual('/heroes/41'); +}); + +it('should navigate when click save and save resolves', async () => { + click(page.saveBtn); + await harness.fixture.whenStable(); + expect(TestBed.inject(Router).url).toEqual('/heroes/41'); +}); + +it('should convert hero name to Title Case', async () => { + // get the name's input and display elements from the DOM + const hostElement: HTMLElement = harness.routeNativeElement!; + const nameInput: HTMLInputElement = hostElement.querySelector('input')!; + const nameDisplay: HTMLElement = hostElement.querySelector('span')!; + + // simulate user entering a new name into the input box + nameInput.value = 'quick BROWN fOx'; + + // Dispatch a DOM event so that Angular learns of input value change. + nameInput.dispatchEvent(new Event('input')); + + // Wait for Angular to update the display binding through the title pipe + await harness.fixture.whenStable(); + + expect(nameDisplay.textContent).toBe('Quick Brown Fox'); +}); +``` ## コンポーネントプロバイダーのオーバーライド {#override-component-providers} -`HeroDetailComponent`は独自の`HeroDetailService`を提供します。 +`HeroDetail`は独自の`HeroDetailService`を提供します。 - +```ts +@Component({ + /* ... */ + providers: [HeroDetailService], +}) +export class HeroDetail { + private heroDetailService = inject(HeroDetailService); + private route = inject(ActivatedRoute); + private router = inject(Router); +} +``` -`TestBed.configureTestingModule`の`providers`でコンポーネントの`HeroDetailService`をスタブすることはできません。 -それらは_テストモジュール_のプロバイダーであり、コンポーネントのプロバイダーではありません。 -それらは_フィクスチャレベル_で依存関係インジェクターを準備します。 +`TestBed.configureTestingModule`の`providers`でコンポーネントの`HeroDetailService`をスタブできません。 +これらは*テストモジュール*のプロバイダーであり、コンポーネントのものではありません。 +これらは*フィクスチャレベル*で依存性インジェクターを準備します。 -Angularは、コンポーネントを_独自の_インジェクターで作成します。これは、フィクスチャインジェクターの_子_です。 -これは、コンポーネントのプロバイダー(この場合は`HeroDetailService`)を子インジェクターに登録します。 +Angularは、フィクスチャインジェクターの*子*である*独自の*インジェクターを使用してコンポーネントを作成します。 +コンポーネントのプロバイダー(この場合は`HeroDetailService`)を子インジェクターに登録します。 -テストは、フィクスチャインジェクターから子インジェクターのサービスを取得できません。 -`TestBed.configureTestingModule`もそれらを設定することはできません。 +テストはフィクスチャインジェクターから子インジェクターのサービスにアクセスできません。 +また、`TestBed.configureTestingModule`もそれらを設定できません。 -Angularは、ずっと前から実際の`HeroDetailService`の新しいインスタンスを作成していました! +Angularはずっと本物の`HeroDetailService`の新しいインスタンスを作成していたのです! -HELPFUL: これらのテストは、`HeroDetailService`がリモートサーバーに独自のXHR呼び出しを行う場合、失敗したり、タイムアウトしたりする可能性があります。 -呼び出すリモートサーバーがない可能性があります。 +HELPFUL: `HeroDetailService`がリモートサーバーへの独自のXHR呼び出しをする場合、これらのテストは失敗またはタイムアウトする可能性があります。 +呼び出すリモートサーバーが存在しないかもしれません。 -幸いなことに、`HeroDetailService`は、リモートデータアクセスの責任を注入された`HeroService`に委任しています。 +幸いなことに、`HeroDetailService`はリモートデータアクセスの責任を注入された`HeroService`に委譲しています。 - +```ts +@Injectable({providedIn: 'root'}) +export class HeroDetailService { + private heroService = inject(HeroService); +} +``` -前のテスト設定は、実際の`HeroService`を`TestHeroService`に置き換えます。これは、サーバーリクエストをインターセプトし、その応答を偽造します。 +前のテスト構成では、本物の`HeroService`を、サーバーリクエストを傍受してレスポンスを偽装する`TestHeroService`に置き換えています。 -もし、そんなに恵まれていなかったらどうでしょうか? -`HeroService`を偽造するのが難しい場合はどうでしょうか? -`HeroDetailService`が独自のサーバーリクエストを行う場合はどうでしょうか? +もし運が悪かったらどうでしょう。 +`HeroService`を偽装するのが難しい場合はどうでしょう? +`HeroDetailService`が独自のサーバーリクエストを行う場合はどうでしょう? -`TestBed.overrideComponent`メソッドは、次のようなセットアップのバリエーションのように、コンポーネントの`providers`を管理しやすい_テストダブル_に置き換えることができます。 +`TestBed.overrideComponent`メソッドを使用すると、次のセットアップのバリエーションに見られるように、コンポーネントの`providers`を管理しやすい*テストダブル*に置き換えることができます。 - +```ts +beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [ + provideRouter([ + {path: 'heroes', component: HeroList}, + {path: 'heroes/:id', component: HeroDetail}, + ]), + // HeroDetailService at this level is IRRELEVANT! + {provide: HeroDetailService, useValue: {}}, + ], + }).overrideComponent(HeroDetail, { + set: {providers: [{provide: HeroDetailService, useClass: HeroDetailServiceSpy}]}, + }); +}); +``` -`TestBed.configureTestingModule`は、[不要になったため](#provide-a-spy-stub-herodetailservicespy)、偽の`HeroService`を提供しなくなっていることに注意してください。 +`TestBed.configureTestingModule`はもはや偽の`HeroService`を提供していないことに注意してください。なぜなら、それは[必要ない](#provide-a-spy-stub-herodetailservicespy)からです。 ### `overrideComponent`メソッド {#the-overridecomponent-method} `overrideComponent`メソッドに注目してください。 - - -これは、2つの引数を取ります。オーバーライドするコンポーネントタイプ(`HeroDetailComponent`)と、オーバーライドメタデータオブジェクトです。 -[オーバーライドメタデータオブジェクト](guide/testing/utility-apis#metadata-override-object)は、次のように定義された汎用型です。 +```ts +.overrideComponent(HeroDetail, { + set: {providers: [{provide: HeroDetailService, useClass: HeroDetailServiceSpy}]}, +}); +``` - +これは2つの引数を取ります。オーバーライドするコンポーネント型(`HeroDetail`)と、オーバーライドメタデータオブジェクトです。 +[オーバーライドメタデータオブジェクト](guide/testing/utility-apis#metadata-override-object)は、次のように定義されたジェネリックです。 +```ts type MetadataOverride = { -add?: Partial; -remove?: Partial; -set?: Partial; + add?: Partial; + remove?: Partial; + set?: Partial; }; +``` - - -メタデータオーバーライドオブジェクトは、メタデータプロパティの要素を追加および削除するか、それらのプロパティを完全にリセットできます。 +メタデータオーバーライドオブジェクトは、メタデータプロパティ内の要素を追加および削除するか、それらのプロパティを完全にリセットできます。 この例では、コンポーネントの`providers`メタデータをリセットします。 -型パラメーター`T`は、`@Component`デコレーターに渡すメタデータの種類です。 - - +型パラメータ`T`は、`@Component`デコレーターに渡すメタデータの種類です。 +```ts selector?: string; template?: string; templateUrl?: string; providers?: any[]; … +``` - - -### _スパイスタブ_(`HeroDetailServiceSpy`)を提供する {#provide-a-spy-stub-herodetailservicespy} +### *スパイスタブ*(`HeroDetailServiceSpy`)を提供する {#provide-a-spy-stub-herodetailservicespy} この例では、コンポーネントの`providers`配列を、`HeroDetailServiceSpy`を含む新しい配列に完全に置き換えます。 -`HeroDetailServiceSpy`は、実際の`HeroDetailService`のスタブバージョンであり、そのサービスに必要なすべての機能を偽造します。 -これは、下位の`HeroService`を注入したり、委任したりしないため、そのためのテストダブルを提供する必要はありません。 +`HeroDetailServiceSpy`は、そのサービスの必要なすべての機能を偽装する、本物の`HeroDetailService`のスタブバージョンです。 +これは下位レベルの`HeroService`を注入や委譲をしないため、そのためのテストダブルを提供する必要はありません。 -関連する`HeroDetailComponent`のテストは、サービスメソッドをスパイすることで、`HeroDetailService`のメソッドが呼び出されたことをアサートします。 -それに応じて、スタブはメソッドをスパイとして実装します。 +関連する`HeroDetail`コンポーネントのテストは、サービスメソッドをスパイすることで、`HeroDetailService`のメソッドが呼び出されたことをアサートします。 +したがって、スタブはそのメソッドをスパイとして実装します。 - +```ts +import {vi} from 'vitest'; + +class HeroDetailServiceSpy { + testHero: Hero = {...testHero}; + + /* emit cloned test hero */ + getHero = vi.fn(() => asyncData({...this.testHero})); + + /* emit clone of test hero, with changes merged in */ + saveHero = vi.fn((hero: Hero) => asyncData(Object.assign(this.testHero, hero))); +} +``` ### オーバーライドテスト {#the-override-tests} これでテストは、スパイスタブの`testHero`を操作することでコンポーネントのヒーローを直接制御し、サービスメソッドが呼び出されたことを確認できます。 - +```ts +let hdsSpy: HeroDetailServiceSpy; + +beforeEach(async () => { + harness = await RouterTestingHarness.create(); + component = await harness.navigateByUrl(`/heroes/${testHero.id}`, HeroDetail); + page = new Page(); + // get the component's injected HeroDetailServiceSpy + hdsSpy = harness.routeDebugElement!.injector.get(HeroDetailService) as any; + + harness.detectChanges(); +}); + +it('should have called `getHero`', () => { + expect(hdsSpy.getHero, 'getHero called once').toHaveBeenCalledTimes(1); +}); + +it("should display stub hero's name", () => { + expect(page.nameDisplay.textContent).toBe(hdsSpy.testHero.name); +}); + +it('should save stub hero change', async () => { + const origName = hdsSpy.testHero.name; + const newName = 'New Name'; + + page.nameInput.value = newName; + + page.nameInput.dispatchEvent(new Event('input')); // tell Angular + + expect(component.hero.name, 'component hero has new name').toBe(newName); + expect(hdsSpy.testHero.name, 'service hero unchanged before save').toBe(origName); + + click(page.saveBtn); + expect(hdsSpy.saveHero, 'saveHero called once').toHaveBeenCalledTimes(1); + + await harness.fixture.whenStable(); + expect(hdsSpy.testHero.name, 'service hero has new name after save').toBe(newName); + expect(TestBed.inject(Router).url).toEqual('/heroes'); +}); +``` -### さらなるオーバーライド {#more-overrides} +### その他のオーバーライド {#more-overrides} `TestBed.overrideComponent`メソッドは、同じコンポーネントまたは異なるコンポーネントに対して複数回呼び出すことができます。 -`TestBed`はこれらの他のクラスの一部を掘り下げて置き換えるために、`overrideDirective`や`overrideModule`、`overridePipe`などの類似のメソッドを提供します。 +`TestBed`は、これらの他のクラスの一部を掘り下げて置き換えるための同様の`overrideDirective`、`overrideModule`、および`overridePipe`メソッドを提供します。 -これらのオプションと組み合わせを自分で調べてみてください。 +独自のオプションと組み合わせを試してみてください。 From e8974afb7e9c2dd09a5a21d0046181d120ff1900 Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 17:25:38 +0900 Subject: [PATCH 31/32] fix(docs): fix textlint errors in define-routes and services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-fixed issues: - プロミス → Promise in define-routes.md - Remove spaces between half/full-width characters in services.md --- adev-ja/src/content/guide/routing/define-routes.md | 2 +- adev-ja/src/content/guide/testing/services.md | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/adev-ja/src/content/guide/routing/define-routes.md b/adev-ja/src/content/guide/routing/define-routes.md index 03b8f1201..8ac32f271 100644 --- a/adev-ja/src/content/guide/routing/define-routes.md +++ b/adev-ja/src/content/guide/routing/define-routes.md @@ -224,7 +224,7 @@ export const routes: Routes = [ `loadComponent`と`loadChildren`プロパティは、それぞれAngularコンポーネントまたはルートのセットに解決されるPromiseを返すローダー関数を受け入れます。ほとんどの場合、この関数は標準の[JavaScript動的インポートAPI](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import)を使用します。ただし、任意の非同期ローダー関数も使用できます。 -遅延読み込みされるファイルが`default`エクスポートを使用している場合、エクスポートされたクラスを選択するための追加の`.then`呼び出しなしで、`import()`プロミスを直接返すことができます。 +遅延読み込みされるファイルが`default`エクスポートを使用している場合、エクスポートされたクラスを選択するための追加の`.then`呼び出しなしで、`import()`Promiseを直接返すことができます。 遅延読み込みルートは、初期バンドルからJavaScriptの大部分を削除することで、Angularアプリケーションの読み込み速度を大幅に向上させることができます。これらのコード部分は、ユーザーが対応するルートにアクセスしたときにのみルーターが要求する個別のJavaScript「チャンク」にコンパイルされます。 diff --git a/adev-ja/src/content/guide/testing/services.md b/adev-ja/src/content/guide/testing/services.md index d20db3204..45fcbfe6c 100644 --- a/adev-ja/src/content/guide/testing/services.md +++ b/adev-ja/src/content/guide/testing/services.md @@ -35,19 +35,19 @@ describe('ValueService', () => { ## `TestBed` を使用したサービスのテスト {#testing-services-with-the-testbed} -アプリケーションは、Angular の [依存関係注入 (DI)](guide/di) に依存してサービスを作成します。 -サービスが依存サービスを持っている場合、DI はその依存サービスを見つけたり、作成します。 -さらに、その依存サービスに独自の依存関係がある場合、DI はそれらも探し出して作成します。 +アプリケーションは、Angularの [依存関係注入 (DI)](guide/di) に依存してサービスを作成します。 +サービスが依存サービスを持っている場合、DIはその依存サービスを見つけたり、作成します。 +さらに、その依存サービスに独自の依存関係がある場合、DIはそれらも探し出して作成します。 サービスの _消費者_ として、あなたはこれらについて心配する必要はありません。 コンストラクター引数の順序や、それらがどのように作成されるかについて心配する必要はありません。 -サービスの _テスター_ として、少なくともサービス依存関係の最初のレベルについて考える必要はありますが、`TestBed` テストユーティリティを使用してサービスを提供して作成し、コンストラクター引数の順序を処理するときは、Angular DI にサービスの作成を任せることができます。 +サービスの _テスター_ として、少なくともサービス依存関係の最初のレベルについて考える必要はありますが、`TestBed` テストユーティリティを使用してサービスを提供して作成し、コンストラクター引数の順序を処理するときは、Angular DIにサービスの作成を任せることができます。 ## Angular `TestBed` {#angular-testbed} -`TestBed` は、Angular のテストユーティリティの中で最も重要なものです。 -`TestBed` は、Angular の [@NgModule](guide/ngmodules) をエミュレートする、動的に構築された Angular の _テスト_ モジュールを作成します。 +`TestBed` は、Angularのテストユーティリティの中で最も重要なものです。 +`TestBed` は、Angularの [@NgModule](guide/ngmodules) をエミュレートする、動的に構築されたAngularの _テスト_ モジュールを作成します。 `TestBed.configureTestingModule()` メソッドは、[@NgModule](guide/ngmodules) のほとんどのプロパティを持つことができるメタデータオブジェクトを受け取ります。 From 676db7e01f169b21cfdd0844ee7f1cadf3d1f17b Mon Sep 17 00:00:00 2001 From: Suguru Inatomi Date: Wed, 7 Jan 2026 18:06:54 +0900 Subject: [PATCH 32/32] fix(docs): fix remaining textlint errors Fix redundant expressions and consecutive kanji issues in: - hydration.md - reactive-forms.md - customizing-route-behavior.md - signals/overview.md - components-basics.md --- adev-ja/src/content/guide/forms/reactive-forms.md | 2 +- adev-ja/src/content/guide/hydration.md | 4 ++-- .../src/content/guide/routing/customizing-route-behavior.md | 2 +- adev-ja/src/content/guide/signals/overview.md | 2 +- adev-ja/src/content/guide/testing/components-basics.md | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/adev-ja/src/content/guide/forms/reactive-forms.md b/adev-ja/src/content/guide/forms/reactive-forms.md index e7f2c693f..4e2fcfbde 100644 --- a/adev-ja/src/content/guide/forms/reactive-forms.md +++ b/adev-ja/src/content/guide/forms/reactive-forms.md @@ -525,7 +525,7 @@ NOTE: 値の変更時、このコントロールの値が更新された直後 ## フォームコントロール状態の管理 -リアクティブフォームは、**touched/untouched** と **pristine/dirty** を通じてコントロールの状態を追跡します。Angularは、DOM操作中にこれらを自動的に更新しますが、プログラムで管理することもできます。 +リアクティブフォームは、**touched/untouched** と **pristine/dirty** を通じてコントロールの状態を追跡します。AngularはDOM操作中にこれらを自動的に更新しますが、プログラムで管理もできます。 **[`markAsTouched`](api/forms/FormControl#markAsTouched)** — フォーカスとブラーイベントによってコントロールまたはフォームをtouchedとしてマークし、値は変更されません。デフォルトで親コントロールに伝播します。 diff --git a/adev-ja/src/content/guide/hydration.md b/adev-ja/src/content/guide/hydration.md index 44834ec8a..db78050ae 100644 --- a/adev-ja/src/content/guide/hydration.md +++ b/adev-ja/src/content/guide/hydration.md @@ -176,7 +176,7 @@ HELPFUL: これによりレンダリングの問題は修正されますが、 ### アプリケーションの安定性のデバッグ {#debugging-application-stability} -`provideStabilityDebugging` ユーティリティは、アプリケーションが安定化に失敗する理由を特定するのに役立ちます。このユーティリティは、`provideClientHydration` を使用する場合、devモードではデフォルトで提供されます。また、本番バンドルで使用したり、たとえばハイドレーションなしでSSRを使用する場合に、アプリケーションプロバイダーに手動で追加することもできます。この機能は、予想よりもアプリケーションの安定化に時間がかかる場合、コンソールに情報をログ出力します。 +`provideStabilityDebugging` ユーティリティは、アプリケーションが安定化に失敗する理由を特定するのに役立ちます。このユーティリティは、`provideClientHydration` を使用する場合、devモードではデフォルトで提供されます。また、本番バンドルで使用したり、たとえばハイドレーションなしでSSRを使用する場合に、アプリケーションプロバイダーに手動で追加できます。この機能は、予想よりもアプリケーションの安定化に時間がかかる場合、コンソールに情報をログ出力します。 ```typescript import {provideStabilityDebugging} from '@angular/core'; @@ -188,7 +188,7 @@ bootstrapApplication(AppComponent, { }); ``` -有効にすると、ユーティリティは保留中のタスク(`PendingTasks`)をコンソールにログ出力します。アプリケーションでZone.jsを使用している場合は、`zone.js/plugins/task-tracking` をインポートすることで、AngularゾーンがUnstableになる原因となっているマクロタスクを確認することもできます。このプラグインは、マクロタスク作成のスタックトレースを提供し、遅延の原因を効果的に特定するのに役立ちます。 +有効にすると、ユーティリティは保留中のタスク(`PendingTasks`)をコンソールにログ出力します。アプリケーションでZone.jsを使用している場合は、`zone.js/plugins/task-tracking` をインポートして、AngularゾーンがUnstableになる原因となっているマクロタスクを確認できます。このプラグインは、マクロタスク作成のスタックトレースを提供し、遅延の原因を効果的に特定するのに役立ちます。 IMPORTANT: Angularは、zone.jsタスクトラッキングプラグインやこのユーティリティを本番バンドルから削除しません。これらは、最適化された本番ビルドを含む、開発中の安定性の問題を一時的にデバッグするためにのみ使用してください。 diff --git a/adev-ja/src/content/guide/routing/customizing-route-behavior.md b/adev-ja/src/content/guide/routing/customizing-route-behavior.md index 0d159070e..aae7263fb 100644 --- a/adev-ja/src/content/guide/routing/customizing-route-behavior.md +++ b/adev-ja/src/content/guide/routing/customizing-route-behavior.md @@ -242,7 +242,7 @@ NOTE: `canMatch`ガードが関与している場合、重複したエントリ デフォルトでは、Angularは、`RouteReuseStrategy`によって保存されなくなった場合でも、デタッチされたルートのインジェクターを破棄しません。これは主に、このレベルのメモリ管理がほとんどのアプリケーションで一般的に必要とされていないためです。 -未使用のルートインジェクターの自動クリーンアップを有効にするには、ルーター設定で`withExperimentalAutoCleanupInjectors`機能を使用できます。この機能は、ナビゲーション後に現在ストラテジーによって保存されているルートを確認し、現在再利用戦略によって保存されていないデタッチされたルートのインジェクターを破棄します。 +未使用のルートインジェクターの自動クリーンアップを有効にするには、ルーター設定で`withExperimentalAutoCleanupInjectors`機能を使用できます。この機能は、ナビゲーション後に現在ストラテジーによって保存されているルートを確認し、現在の再利用戦略によって保存されていないデタッチされたルートのインジェクターを破棄します。 ```ts import {provideRouter, withExperimentalAutoCleanupInjectors} from '@angular/router'; diff --git a/adev-ja/src/content/guide/signals/overview.md b/adev-ja/src/content/guide/signals/overview.md index 91a356088..c52065d0e 100644 --- a/adev-ja/src/content/guide/signals/overview.md +++ b/adev-ja/src/content/guide/signals/overview.md @@ -69,7 +69,7 @@ export class AwesomeCounter { } ``` -読み取り専用のシグナルは、元の書き込み可能なシグナルへの変更を反映しますが、`set()`または`update()`メソッドを使用して変更することはできません。 +読み取り専用のシグナルは、元の書き込み可能なシグナルへの変更を反映しますが、`set()`または`update()`メソッドを使用して変更できません。 IMPORTANT: 読み取り専用のシグナルには、その値の深い変更を防ぐ組み込みのメカニズムは**ありません**。 diff --git a/adev-ja/src/content/guide/testing/components-basics.md b/adev-ja/src/content/guide/testing/components-basics.md index c04d93059..550c14cf8 100644 --- a/adev-ja/src/content/guide/testing/components-basics.md +++ b/adev-ja/src/content/guide/testing/components-basics.md @@ -152,7 +152,7 @@ it('should contain "banner works!"', () => { ### `setup` 関数の作成 -`beforeEach` の代わりに、各テストで呼び出すsetup関数を作成することもできます。 +`beforeEach` の代わりに、各テストで呼び出すsetup関数を作成できます。 setup関数は、パラメーターを介してカスタマイズできるという利点があります。 setup関数の例を次に示します。