- Angular
Angularルーティング設定|provideRouter・パラメータ・404
Angularのルーティングとは、URLとコンポーネントの対応表(ルート定義)を作り、URLに応じて画面の一部を差し替える仕組みです。 現在の Angular では、app.config.ts の provideRouter() に対応表を渡すだけで有効になり、NgModule を用意する必要はありません。
この記事では、ng new で作ったプロジェクトをもとに、設定・リンク・子ルート・404・リダイレクトまでを1つの最小アプリとして全ファイル掲載します。さらに、パラメータ取得の3方式を同じ条件で動かして比べ、ルート定義の順番で404に吸われる例や「NG04002: Cannot match any routes」を実際に出して直します。
確認環境:Ubuntu 24.04 / Node.js 24.21.0 / Angular CLI 22.2.1・Angular 22.2.1(@angular/router 22.2.1)/ TypeScript 6.0.3。動作は ng build と、ng test(Vitest 5.0.3 + jsdom)で確かめています。
ルーティング設定の早見表
| やりたいこと | 書く場所 | 書き方 |
|---|---|---|
| ルーティングを有効にする | app.config.ts | provideRouter(routes) |
| URLと画面を対応させる | app.routes.ts | { path: 'items', component: ItemList } |
| 画面を差し込む場所を作る | テンプレート | <router-outlet />(RouterOutlet を imports に入れる) |
| リンクを張る | テンプレート | <a routerLink="/items">(RouterLink を imports に入れる) |
| 今いるリンクに class を付ける | テンプレート | routerLinkActive="active" |
| パラメータ付きのURLにする | app.routes.ts | { path: 'items/:id', component: ItemDetail } |
| パラメータを input で受け取る | app.config.ts + コンポーネント | withComponentInputBinding() + id = input.required<string>() |
| 画面の中にさらに画面を入れる | app.routes.ts | children: [...] + 親のテンプレートに <router-outlet /> |
| 404ページを出す | app.routes.ts の最後 | { path: '**', component: NotFound } |
| 別のURLへ転送する | app.routes.ts | { path: 'catalog', redirectTo: 'items' } |
| タブのタイトルを変える | app.routes.ts | title: '備品一覧' |
| コードから移動する | コンポーネント | inject(Router).navigate(['/items', 3]) |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Angularルーティング設定|provideRouter・パラメータ・404」
最小アプリの全ファイル(コピペで動く)
ng new demo で作ったプロジェクトの src/app を、次の構成にします。備品の一覧・詳細・登録の画面を持つ小さなアプリです。
src/app/
├── app.config.ts … provideRouter を書く
├── app.routes.ts … URLとコンポーネントの対応表
├── app.ts … ナビゲーションと router-outlet
└── pages/
├── home.ts
├── item-list.ts
├── item-new.ts
├── item-detail.ts … 子ルートを持つ
├── item-info.ts … 子ルート(基本情報)
├── item-history.ts … 子ルート(履歴)
└── not-found.ts … 404
src/main.ts は ng new が作ったまま(bootstrapApplication(App, appConfig))で変更しません。app.html と app.css は使わないので削除します。
app.config.ts:provideRouter を登録する
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes, withComponentInputBinding()),
],
};
ng new 直後は provideRouter(routes) だけが書かれています。ここに withComponentInputBinding() を足すと、URLのパラメータをコンポーネントの input で受け取れるようになります。
app.routes.ts:ルート定義
import { Routes } from '@angular/router';
import { Home } from './pages/home';
import { ItemList } from './pages/item-list';
import { ItemNew } from './pages/item-new';
import { ItemDetail } from './pages/item-detail';
import { ItemInfo } from './pages/item-info';
import { ItemHistory } from './pages/item-history';
import { NotFound } from './pages/not-found';
export const routes: Routes = [
{ path: '', component: Home, title: 'ホーム' },
{ path: 'items', component: ItemList, title: '備品一覧' },
{ path: 'items/new', component: ItemNew, title: '備品の登録' },
{
path: 'items/:id',
component: ItemDetail,
title: '備品の詳細',
children: [
{ path: '', component: ItemInfo },
{ path: 'history', component: ItemHistory },
],
},
{ path: 'catalog', redirectTo: 'items' },
{ path: '**', component: NotFound, title: 'ページが見つかりません' },
];
app.ts:ナビゲーションと router-outlet
import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet, RouterLink, RouterLinkActive],
template: `
<nav>
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">ホーム</a>
<a routerLink="/items" routerLinkActive="active">備品一覧</a>
<a routerLink="/items/new" routerLinkActive="active">登録</a>
</nav>
<main>
<router-outlet />
</main>
`,
styles: `
nav a { margin-right: 12px; }
.active { font-weight: bold; }
`,
})
export class App {}
pages/home.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-home',
template: `<h1>ホーム</h1>`,
})
export class Home {}
pages/item-list.ts:一覧とクエリパラメータ
import { Component, input } from '@angular/core';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-item-list',
imports: [RouterLink],
template: `
<h1>備品一覧</h1>
<p class="cond">並び順: {{ sort() ?? '指定なし' }} / ページ: {{ page() ?? 1 }}</p>
<ul>
@for (item of items; track item.id) {
<li><a [routerLink]="['/items', item.id]">{{ item.name }}</a></li>
}
</ul>
<a routerLink="/items" [queryParams]="{ sort: 'name', page: 2 }" fragment="list">名前順の2ページ目</a>
`,
})
export class ItemList {
// クエリパラメータ(?sort=name&page=2)も input で受け取れる
sort = input<string>();
page = input<string>();
items = [
{ id: 1, name: 'ノート' },
{ id: 2, name: 'ペン' },
{ id: 3, name: '付箋' },
];
}
pages/item-new.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-item-new',
template: `<h1>備品の登録</h1>`,
})
export class ItemNew {}
pages/item-detail.ts:パラメータと子ルート
import { Component, input } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
@Component({
selector: 'app-item-detail',
imports: [RouterOutlet, RouterLink, RouterLinkActive],
template: `
<h1>備品 #{{ id() }}</h1>
<nav class="tabs">
<a routerLink="./" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">基本情報</a>
<a routerLink="history" routerLinkActive="active">履歴</a>
<a [routerLink]="['/items', next()]">次の備品</a>
</nav>
<router-outlet />
`,
})
export class ItemDetail {
// withComponentInputBinding() により、:id が input に入る
id = input.required<string>();
next() {
return Number(this.id()) + 1;
}
}
pages/item-info.ts と pages/item-history.ts:子ルートの中身
import { Component } from '@angular/core';
@Component({
selector: 'app-item-info',
template: `<p class="tab">基本情報タブ</p>`,
})
export class ItemInfo {}
import { Component } from '@angular/core';
@Component({
selector: 'app-item-history',
template: `<p class="tab">履歴タブ</p>`,
})
export class ItemHistory {}
pages/not-found.ts:404ページ
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-not-found',
imports: [RouterLink],
template: `
<h1>404 ページが見つかりません</h1>
<a routerLink="/">ホームへ戻る</a>
`,
})
export class NotFound {}
動かした結果
npx ng build が通ることを確かめたうえで、リンクのクリックとURLの直接指定を順に行うテストを書き、npx ng test --watch=false --reporters=verbose で実行しました。各行は「操作 → 移動後のURL・タブのタイトル・表示された見出し・active が付いたリンク」です。
[/] url=/ title=ホーム
表示: ホーム active=["ホーム"]
クリック「備品一覧」 href=/items
→ url=/items title=備品一覧
表示: 備品一覧 | 並び順: 指定なし / ページ: 1 active=["備品一覧"]
クリック「名前順の2ページ目」 href=/items?sort=name&page=2#list
→ url=/items?sort=name&page=2#list title=備品一覧
表示: 備品一覧 | 並び順: name / ページ: 2 active=["備品一覧"]
クリック「ペン」 href=/items/2
→ url=/items/2 title=備品の詳細
表示: 備品 #2 | 基本情報タブ active=["備品一覧","基本情報"]
クリック「履歴」 href=/items/2/history
→ url=/items/2/history title=備品の詳細
表示: 備品 #2 | 履歴タブ active=["備品一覧","履歴"]
クリック「次の備品」 href=/items/3
→ url=/items/3 title=備品の詳細
表示: 備品 #3 | 基本情報タブ active=["備品一覧","基本情報"]
[/items/new] url=/items/new title=備品の登録
表示: 備品の登録 active=["備品一覧","登録"]
[/catalog] url=/items title=備品一覧
表示: 備品一覧 | 並び順: 指定なし / ページ: 1 active=["備品一覧"]
[/no/such/page] url=/no/such/page title=ページが見つかりません
表示: 404 ページが見つかりません active=[]
この結果から、次のことが確かめられます。
| 確かめたこと | 結果 |
|---|---|
routerLink が href に変換される |
/items、/items/2、/items?sort=name&page=2#list など |
title がタブのタイトルになる |
移動のたびに切り替わる |
:id が input に入る |
「備品 #2」「備品 #3」 |
| クエリパラメータも input に入る | 「並び順: name / ページ: 2」 |
| 子ルートが親の中に表示される | 「備品 #2 | 履歴タブ」 |
redirectTo で転送される |
/catalog → /items |
どれにも合わないURLは ** が受ける |
/no/such/page → 404(URLはそのまま) |
provideRouter と router-outlet の役割
ルーティングを動かす最小の部品は3つです。
| 部品 | 役割 | 書き忘れたとき |
|---|---|---|
provideRouter(routes) |
ルーターをアプリに登録し、対応表を渡す | 実行時に NG0201(下記) |
<router-outlet /> |
URLに合ったコンポーネントを差し込む場所 | 画面が切り替わらない |
RouterOutlet / RouterLink の imports |
テンプレートでタグや属性を使えるようにする | NG8001 / NG8002(下記) |
provideRouter の第2引数以降には、withComponentInputBinding() のような追加機能を並べられます。
provideRouter を書き忘れたときのエラー
provideRouter を登録せずに、完成コードの App(routerLink を使っている)を表示しようとした結果です。
NP) NG0201: No provider found for `ActivatedRoute`. Source: DynamicTestModule. Find more at https://v22.angular.dev/errors/NG0201
ActivatedRoute や Router の提供元が見つからない、という NG0201 が出たら、app.config.ts の providers に provideRouter(routes) があるかを確認します(Source: DynamicTestModule はテスト用の環境で動かしたことを示す表示です)。
imports を忘れたときのエラー
app.ts の imports から RouterOutlet を外してビルドした結果です。
✘ [ERROR] NG8001: 'router-outlet' is not a known element:
1. If 'router-outlet' is an Angular component, then verify that it is included in the '@Component.imports' of this component.
2. If 'router-outlet' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@Component.schemas' of this component to suppress this message. Find more at https://next.angular.dev/errors/NG8001 [plugin angular-compiler]
src/app/app.ts:14:6:
14 │ <router-outlet />
╵ ~~~~~~~~~~~~~~~~~
RouterLink と RouterLinkActive を外した場合は、角かっこ付きの属性でエラーになりました。
✘ [ERROR] NG8002: Can't bind to 'routerLinkActiveOptions' since it isn't a known property of 'a'. Find more at https://next.angular.dev/errors/NG8002 [plugin angular-compiler]
気をつけたいのは、角かっこの無い routerLink="/items" だけを書いた場合です。RouterLink を imports に入れていなくてもビルドは通り、次のように href の無いリンクが出力されました。
import なし: <a routerlink="/items">備品一覧</a>
「リンクを押しても何も起きない」ときは、まずそのコンポーネントの imports に RouterLink があるかを確認してください。
routerLink と routerLinkActive の書き方
パス・クエリ・フラグメントの早見表
URLの各部分と、リンクでの書き方・受け取り方の対応です。
| URLの部分 | 例 | リンクでの書き方 | 受け取り方 |
|---|---|---|---|
| パス | /items |
routerLink="/items" |
(ルート定義で対応づけ) |
| パスパラメータ | /items/2 |
[routerLink]="['/items', item.id]" |
id = input.required<string>() または route.snapshot.paramMap.get('id') |
| クエリパラメータ | ?sort=name&page=2 |
[queryParams]="{ sort: 'name', page: 2 }" |
sort = input<string>() または route.snapshot.queryParamMap.get('sort') |
| フラグメント | #list |
fragment="list" |
route.snapshot.fragment |
ActivatedRoute でクエリとフラグメントを読むコンポーネントを、/items?sort=name&page=2#list で表示した結果です。
@Component({ selector: 'app-query', template: `<p>sort={{ sort }} fragment={{ frag }} 全部={{ all }}</p>` })
class QueryReader {
private route = inject(ActivatedRoute);
sort = this.route.snapshot.queryParamMap.get('sort');
frag = this.route.snapshot.fragment;
all = JSON.stringify(this.route.snapshot.queryParams);
}
/items?sort=name&page=2#list → url=/items?sort=name&page=2#list 表示: sort=name fragment=list 全部={"sort":"name","page":"2"}
URLから受け取る値はすべて文字列です(page が "2" になっています)。数値として使うときは Number() などで変換します。
相対リンクと絶対リンク
/items/2/history?sort=name を表示中に、items/:id のコンポーネント(詳細画面)の中へ置いたリンクが、どの href になるかを出力しました。
| 書き方 | 生成された href | 意味 |
|---|---|---|
routerLink="/items" |
/items |
先頭が / なら絶対パス |
routerLink="history" |
/items/2/history |
今のルートの下(子)へ |
routerLink="./" |
/items/2 |
今のルート自身 |
routerLink="../" |
/items |
1つ上へ |
routerLink="../5" |
/items/5 |
1つ上に戻って別のIDへ |
[routerLink]="['/items', 7, 'history']" |
/items/7/history |
配列で区切って組み立てる |
[routerLink]="['/items']" [queryParams]="{ sort: 'name' }" fragment="list" |
/items?sort=name#list |
クエリとフラグメント付き |
routerLink="/items" [queryParams]="{ page: 2 }" queryParamsHandling="merge" |
/items?sort=name&page=2 |
今のクエリを残して足す |
変数を含むパスは、文字列の連結ではなく配列(['/items', item.id])で書くと、区切りの / や記号の扱いを Angular に任せられます。
routerLinkActive:今いる場所を目立たせる
routerLinkActive="active" を付けたリンクは、そのリンク先が今のURLに含まれているとき、class に active が付きます。先ほどの実行結果では、/items/new を開いたとき active=["備品一覧","登録"] と2つに付いていました。/items/new は /items で始まるURLでもあるためです。
| 指定 | 判定 | 向いている使い方 |
|---|---|---|
routerLinkActive="active" |
リンク先が今のURLの先頭部分と一致すれば付く | 「備品」配下にいる間ずっと目立たせるメニュー |
[routerLinkActiveOptions]="{ exact: true }" を追加 |
完全に一致したときだけ付く | 「ホーム(/)」や、タブの「基本情報(./)」 |
ホームのリンク(/)は、すべてのURLの先頭に一致してしまうので、exact: true を付けています。
コードから移動する:Router.navigate
ボタンを押したあとの処理などで移動したいときは、Router を注入して navigate を呼びます。実際に呼んだあとの router.url を並べます。
// コンポーネントでは private router = inject(Router); で受け取る(下はテストでの呼び出し)
const router = TestBed.inject(Router);
await router.navigate(['/items', 3], { queryParams: { tab: 'info' }, fragment: 'top' });
await router.navigate(['/items'], { queryParams: { sort: 'name', q: 'ノート 青' } });
await router.navigate([], { queryParams: { page: 3 }, queryParamsHandling: 'merge' });
navigate(['/items', 3], { queryParams, fragment }) → /items/3?tab=info#top
navigate(['/items'], { queryParams }) → /items?sort=name&q=%E3%83%8E%E3%83%BC%E3%83%88%20%E9%9D%92
navigate([], { queryParamsHandling: merge }) → /items?sort=name&q=%E3%83%8E%E3%83%BC%E3%83%88%20%E9%9D%92&page=3
日本語や空白は自動でエンコードされます。3行目のように配列を空にして queryParamsHandling: 'merge' を指定すると、同じ画面のままクエリだけを足せます(ページ送りや絞り込みに便利です)。
パラメータ取得の3方式を比べる
同じ条件で動かす
items/:id に対して、次の3つのコンポーネントを用意し、/items/1 → /items/2 の順に移動して表示を比べます。
// 方式1: snapshot(その時点の値を1回だけ読む)
@Component({ selector: 'app-by-snapshot', template: `<p>snapshot: {{ id }}</p>` })
class BySnapshot {
private route = inject(ActivatedRoute);
id = this.route.snapshot.paramMap.get('id');
}
// 方式2: paramMap を購読(変わるたびに受け取る)
@Component({ selector: 'app-by-param-map', template: `<p>paramMap: {{ id() }}</p>` })
class ByParamMap implements OnInit {
private route = inject(ActivatedRoute);
id = signal<string | null>(null);
ngOnInit() {
this.route.paramMap.subscribe((params) => this.id.set(params.get('id')));
}
}
// 方式3: withComponentInputBinding + input
@Component({ selector: 'app-by-input', template: `<p>input: {{ id() }}</p>` })
class ByInput {
id = input.required<string>();
}
R1 snapshot
/items/1 → url=/items/1 表示: snapshot: 1
/items/2 → url=/items/2 表示: snapshot: 1
R1 paramMap
/items/1 → url=/items/1 表示: paramMap: 1
/items/2 → url=/items/2 表示: paramMap: 2
R1 input
/items/1 → url=/items/1 表示: input: 1
/items/2 → url=/items/2 表示: input: 2
snapshot だけ、URLが /items/2 になっても表示が「1」のままです。同じルートの中でパラメータだけが変わる場合、Angular はコンポーネントを作り直さずに使い回します。snapshot は作られた時点の値を1回読むだけなので、変化を拾えません。公式ドキュメント(Read route state)にも、スナップショットは静的で、その後の変化を反映しないと書かれています。
比較表
| 方式 | 書く量 | 同じ画面でIDが変わったとき | 必要な設定 | 向いている場面 |
|---|---|---|---|---|
route.snapshot.paramMap.get('id') |
少ない | 追従しない(上の結果) | なし | 別の画面からしか来ない、開いた時点の値だけでよい |
route.paramMap.subscribe(...) |
多い | 追従する | なし | 変化のたびに処理を組み立てたい、RxJS の演算子と組み合わせたい |
id = input.required<string>() |
最も少ない | 追従する | withComponentInputBinding() |
新しく書くコード全般。computed() と相性がよい |
完成コードの「次の備品」リンク(/items/2 → /items/3)で見出しが「備品 #3」に変わったのは、方式3を使っているためです。
withComponentInputBinding を付け忘れたとき
provideRouter(routes) だけの状態で、input.required を使うコンポーネントを表示すると、次のエラーになりました。
/items/1 → エラー: NG0950: Input "id" is required but no value is available yet. Find more at https://v22.angular.dev/errors/NG0950
ルーターが input に値を入れてくれないので、必須入力が空のまま読まれた、という状態です。app.config.ts の provideRouter に withComponentInputBinding() を足すと解消します。
公式ドキュメントでは、この機能でパスパラメータのほか、クエリパラメータ、ルートの data、resolve の結果もコンポーネントの入力に結び付く、と説明されています。完成コードの一覧画面で sort と page を input で受け取れているのは、このためです。input の名前は、ルート定義の :id やクエリのキーと同じ名前にします。
子ルート:画面の中で一部だけ切り替える
子ルートは、親のコンポーネントを表示したまま、その中の一部だけを切り替える仕組みです。必要なのは2つだけです。
- ルート定義に
childrenを書く - 親コンポーネントのテンプレートに、子を差し込む
<router-outlet />を置く
{
path: 'items/:id',
component: ItemDetail,
children: [
{ path: '', component: ItemInfo }, // /items/2
{ path: 'history', component: ItemHistory }, // /items/2/history
],
},
| URL | 外側の outlet(app.ts) | 内側の outlet(item-detail.ts) |
|---|---|---|
/items/2 |
ItemDetail | ItemInfo(path: '' の子) |
/items/2/history |
ItemDetail | ItemHistory |
子の path には親の部分(items/:id)を書かず、続きの部分だけを書きます。path: '' の子は「親のURLそのままのとき」に表示される既定の子です。
実行結果では、/items/2 → /items/2/history と移動したとき、見出しの「備品 #2」はそのままで、下の部分だけが「基本情報タブ」から「履歴タブ」に変わりました。
ワイルドカードの404と、ルート定義の順番
順番で結果が変わる例
公式ドキュメント(Define routes)には、Angular は最初に一致したルートを採用し、それ以降は調べない(first-match wins)と書かれています。順番を入れ替えて確かめました。
// R2-a::id を先に書いた場合
[
{ path: 'items/:id', component: ByInput },
{ path: 'items/new', component: ItemNew },
]
// R2-b:new を先に書いた場合
[
{ path: 'items/new', component: ItemNew },
{ path: 'items/:id', component: ByInput },
]
R2-a :id を先に書いた場合
/items/new → url=/items/new 表示: input: new
R2-b new を先に書いた場合
/items/new → url=/items/new 表示: 備品の登録
:id は「どんな文字でも受け取る」ので、先に書くと new もIDとして受け取ってしまい、登録画面が表示されません。
ワイルドカード ** も同じです。
R2-c ** を先頭に書いた場合
/items → url=/items 表示: 404 ページが見つかりませんホームへ戻る
R2-d ** を最後に書いた場合
/items → url=/items 表示: 備品一覧並び順: 指定なし / ページ: 1ノートペン付箋名前順の2ページ目
** は「どのURLにも一致する」ので、先頭に置くとすべてのURLが404に吸われます。並べ方の決まりは「具体的なものを上、あいまいなものを下、ワイルドカードは最後」です。
| 順番 | 書くもの | 例 |
|---|---|---|
| 1 | 固定のパス | items、items/new |
| 2 | パラメータを含むパス | items/:id |
| 3 | リダイレクト | { path: 'catalog', redirectTo: 'items' } |
| 4(最後) | ワイルドカード | { path: '**', component: NotFound } |
NG04002:Cannot match any routes を出して直す
** を書いていない設定で、対応表に無いURLへ移動すると、NG04002 になります。
provideRouter([{ path: 'items', component: ItemList }])
R3 ** が無い設定
/item → エラー: NG04002: Cannot match any routes. URL Segment: 'item'
/items/1/history → エラー: NG04002: Cannot match any routes. URL Segment: 'items/1/history'
URL Segment: の後ろに、一致するルートが見つからなかったURLが表示されます。原因と直し方は次のとおりです。
| 原因 | 確かめ方 | 直し方 |
|---|---|---|
リンクやURLのつづり違い(item と items) |
URL Segment: の文字と path を見比べる |
リンク側か path をそろえる |
子ルートを定義していない(items/1/history) |
children にその path があるか |
children に追加する |
| 相対リンクの起点が想定と違う | 生成された href を確認する |
先頭に / を付けて絶対パスにする |
| 利用者が存在しないURLを直接開いた | ― | 最後に { path: '**', component: NotFound } を置く |
** のルートを最後に置いておけば、対応表に無いURLは404ページに案内され、NG04002 は出なくなります(最初の実行結果の /no/such/page)。
redirectTo と、設定時に出るエラー(NG04014)
redirectTo の書き方
{ path: 'catalog', redirectTo: 'items' }, // /catalog → /items
{ path: '', redirectTo: 'items', pathMatch: 'full' }, // トップ → /items
空のパス('')からリダイレクトするときは、pathMatch: 'full' が必要です。付けなかった場合と付けた場合の結果です。
R4-a 空パスの redirectTo に pathMatch なし
エラー: NG04014: Invalid configuration of route '{path: "", redirectTo: "items"}': please provide 'pathMatch'. The default value of 'pathMatch' is 'prefix', but often the intent is to use 'full'.
R4-b pathMatch: full を付けた場合
/ → url=/items 表示: 備品一覧並び順: 指定なし / ページ: 1ノートペン付箋名前順の2ページ目
空のパスは、既定の prefix(前方一致)だとすべてのURLの先頭に一致してしまいます。「URL全体が空のときだけ」という意味にするのが pathMatch: 'full' です。
ルート定義の書き方で出る NG04014
ルート定義そのものに不備があると、ルーターが作られる時点で NG04014 が出ます。
R4-c path を / で始めた場合
エラー: NG04014: Invalid configuration of route '/items': path cannot start with a slash
R4-d component も redirectTo も無い場合
エラー: NG04014: Invalid configuration of route 'items'. One of the following must be provided: component, loadComponent, redirectTo, children or loadChildren
| メッセージの要点 | 直し方 |
|---|---|
| please provide 'pathMatch' | 空パスのリダイレクトに pathMatch: 'full' を足す |
| path cannot start with a slash | path: '/items' → path: 'items'(ルート定義の path は / で始めない) |
| One of the following must be provided | component・redirectTo・children などのどれかを書く |
リンク(routerLink="/items")は / で始め、ルート定義(path: 'items')は / を付けない、という違いを覚えておくと迷いません。
エラーと直し方のまとめ
| 症状・コード | 原因 | 直し方 |
|---|---|---|
| NG8001: 'router-outlet' is not a known element | RouterOutlet を imports に入れていない |
コンポーネントの imports に追加 |
| NG8002: Can't bind to 'routerLink...' | RouterLink / RouterLinkActive を imports に入れていない |
同上 |
リンクを押しても移動しない(href が無い) |
角かっこ無しの routerLink で imports 忘れ |
RouterLink を imports に追加 |
NG0201: No provider found for ActivatedRoute |
provideRouter を登録していない |
app.config.ts の providers に追加 |
| NG04002: Cannot match any routes | 対応表に無いURLへ移動した | path を確認、最後に ** を置く |
| どのURLでも404になる | ** が上のほうにある |
** を最後へ移す |
/items/new が詳細画面になる |
items/:id が items/new より上にある |
固定のパスを上にする |
| NG04014: please provide 'pathMatch' | 空パスの redirectTo |
pathMatch: 'full' |
| NG04014: path cannot start with a slash | path: '/items' |
先頭の / を取る |
| NG0950: Input "id" is required… | withComponentInputBinding() が無い |
provideRouter に足す |
| 同じ画面でIDを変えても表示が変わらない | snapshot で読んでいる | input か paramMap の購読にする |
チェックリスト
- [ ]
app.config.tsにprovideRouter(routes)がある(input で受けるならwithComponentInputBinding()も) - [ ] 画面を差し込む場所に
<router-outlet />があり、RouterOutletを imports に入れた - [ ] リンクを書いたコンポーネントの imports に
RouterLink(必要ならRouterLinkActive)がある - [ ] ルート定義の
pathは/で始めていない - [ ] 固定のパス → パラメータ付き →
**の順に並べた - [ ] 空パスの
redirectToにpathMatch: 'full'を付けた - [ ] 子ルートを持つコンポーネントのテンプレートに、もう1つ
<router-outlet />を置いた - [ ] ホームのリンクなどに
[routerLinkActiveOptions]="{ exact: true }"を付けた - [ ] 各ルートに
titleを付けた
よくある質問
Q. RouterModule.forRoot は使わなくてよいのですか?
A. ng new で作ったプロジェクトは、app.config.ts の provideRouter(routes) でルーターを登録します。この記事のアプリも NgModule を1つも書かずに動いています。リンクや outlet は、使うコンポーネントの imports に RouterLink や RouterOutlet を入れます。
Q. パラメータは snapshot・paramMap・input のどれで受け取ればよいですか?
A. 新しく書くなら withComponentInputBinding() と input の組み合わせが最も短く、同じ画面でIDが変わっても追従します。RxJS の演算子で処理を組み立てたいときは paramMap の購読、開いた時点の値だけで足りるときは snapshot、と使い分けます。
Q. 404ページを表示したとき、URLは変わりますか?
A. 変わりません。今回の結果でも、/no/such/page を開くとURLはそのままで、** に指定した NotFound コンポーネントが表示されました。利用者が入力したURLが残るので、どのURLが見つからなかったのかを画面上でも確認できます。
Q. クエリパラメータを残したまま移動するにはどうしますか?
A. リンクなら queryParamsHandling="merge"、コードなら navigate のオプションに queryParamsHandling: 'merge' を指定します。今のクエリに新しい値を足した URL(例:/items?sort=name&page=2)になります。
まとめ
provideRouter(routes)・<router-outlet />・routerLinkの3つで、ルーティングの土台ができる- ルート定義は上から順に調べられ、最初に一致したものが使われる。固定のパスを上、
:idを下、**は最後に置く - パラメータは
withComponentInputBinding()+ input で受け取ると、短く書けて、同じ画面でのID変更にも追従する。snapshot は開いた時点の値のまま - 子ルートは
childrenと、親テンプレートの<router-outlet />の組み合わせ - NG04002 は「対応表に無いURL」。
URL Segment:を読み、pathを直すか**の404で受ける - 空パスの
redirectToにはpathMatch: 'full'、ルート定義のpathは/で始めない routerLinkActiveは前方一致。完全一致にしたいリンクにはexact: trueを付ける
参考・出典
- Angular 公式ドキュメント「Define routes」 https://angular.dev/guide/routing/define-routes
- Angular 公式ドキュメント「Read route state」 https://angular.dev/guide/routing/read-route-state
- Angular 日本語ドキュメント「その他のルーティングタスク」 https://angular.jp/guide/routing/common-router-tasks
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Angularルーティング設定|provideRouter・パラメータ・404」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。