Repository navigation
refactor: Angular v22 へアップデート + city-search を Signal Forms 統合に再整理 - #50
Merged
Merged
Conversation
…r to v22 ng update が生成した自動 migration: - app.config.ts: provideClientHydration に withNoIncrementalHydration() を追加し v22 既定の incremental hydration を opt out して従来挙動を維持 - tsconfig.app.json: extended diagnostics の nullishCoalescingNotNullable と optionalChainNotNullable を suppress(v22 で警告に昇格) v22 対応のため依存追加: - @angular/cdk: combobox harness で利用する CDK testing インフラに必要 - @angular-eslint/builder: angular-eslint v21 の builder が v22 CLI で 解決されないため明示依存にして lint を復活 - playwright: @vitest/browser-playwright の peer dep が --legacy-peer-deps で 脱落するのを防ぐため明示
…undle v22 の @angular/build:unit-test は build の externalPackages: true から得た implicitBrowser 一覧をそのまま vite の optimizeDeps.include に投入する。同時に 同パッケージを esbuild の external に並べるため、msw のように両側に載るものは prebundle で「entry point "msw" cannot be marked as external」を吐く。 - angular.json: runnerConfig: true で vitest-base.config.ts を読み込ませる - vitest-base.config.ts: optimizeDeps.exclude に msw を入れつつ、Angular が 注入する optimizeDeps.include 配列からも configResolved で剥がす 参考: angular/angular-cli#32523
… API v22 で @angular/aria の Combobox 構成が再設計された: - ComboboxInput / ComboboxPopupContainer が廃止され、ngCombobox を input に 直接適用する形に統一 - ComboboxPopup(ng-template)+ ComboboxWidget(popup 中身)でポップアップを 組み立てる二段構成に分離 - close() が削除され、expanded model 経由で閉じる - Listbox の二方向バインディングが [(values)] (複数形) から [(value)] へ統一 (値は依然 V[] だがプロパティ名が変更) - Listbox.selectionMode の既定が 'follow' になり、focus が動くだけで選択も 追従するようになったため、明示的に selectionMode="explicit" を指定して click/Enter で確定する従来挙動を維持 加えて v22 Combobox は popup 展開中の Enter で preventDefault しないため、 form submit が暴発する。(keydown.enter) で展開中のみ抑止する。 テスト: - Keyboard navigation 2 件は userEvent.keyboard 経由だと Combobox.keyboardEventRelayEffect が afterRenderEffect 内で同期 dispatchEvent し、@testing-library/angular の safeDetectChanges と衝突して NG0101: ApplicationRef.tick is called recursively を出す。 公式の ComboboxHarness(@angular/aria/combobox/testing)は CDK の TestbedHarnessEnvironment 経由で sendKeys するため testing-library の ラッパーを通らず、CD 衝突を回避できる。 - sendKeys に複数キーを一度に渡すと keyboardEventRelay signal が同一 tick で 上書きされ最後のキーしか listbox に届かないため、1 key ずつ送る。 - v22 では popup 展開時に先頭 option が自動 active になるため、テスト名と キー回数を v22 挙動に合わせて調整(Enter 単独で先頭選択、Arrow Down 1 回で 2 番目に進む)
… v21 comparison comments
@angular/aria の KeyboardEventManager は default options で
preventDefault: true / stopPropagation: true を持つ。Combobox host の
.on('Enter', ...) は個別 options を省略しているのでデフォルトが適用され、
Enter は Combobox 内部で既に preventDefault される。よって追加の
(keydown.enter) ハンドラは不要。
併せて v21 挙動との差分に言及していたコメントを除去する。
v22 で安定版になった現行 API の挙動として読めば十分。
Combobox/Listbox の状態テストに ComboboxHarness を使うのは正規の選択肢で、 non-obvious な workaround ではない。経緯コメントは不要。
ng update が pre-v22 挙動を保つために自動投入していた opt-out 設定を撤去: - withNoIncrementalHydration(): プロジェクトに @defer (hydrate ...) は無いため v22 デフォルトのままで影響なし。シムは単なる挙動凍結。 - tsconfig.app.json の nullishCoalescingNotNullable / optionalChainNotNullable suppress: 現コードに該当パターンは無く、 警告が出ない以上 suppress も不要。 build / 107 tests / lint いずれも pass。
…f imperative set selectionMode が 'follow' のままだった頃の暫定回避として viewChild + 手続き的 expanded.set(false) を使っていたが、selectionMode="explicit" を入れた現在は auto-select に伴う暴れも無くなった。"isExpanded" signal と [(expanded)] の 二方向バインディングに戻す。
…Field] [formField] ディレクティブで input を searchForm.city フィールドに直接バインド する。これに伴い手続き的なコードが大きく整理された: - cityInputValue signal を削除(searchForm.city().value() が source of truth) - debounce 用の effect + setTimeout を削除 - onComboboxEnter / onInputEnter / commit ハンドラを削除 - 選択値を反映する effect は selectedCities → searchModel.city への単純な 同期 1 つだけに [formField] 経由なら他の examples(location-select 等)と同じ Signal Forms integration パターンに揃う。
選択後 popup が消えるのは suggestionItems computed が選択値に一致する候補を 空配列で返し @if が描画を落とすため。combobox の expanded 状態をユーザー側で 管理する必要はなく、Escape / blur での自動クローズも combobox 内部の closePopupOnBlurEffect / Escape ハンドラが担う。
… propagation
listbox の選択 → form model 反映 と popup の自動クローズを effect + untracked
で書いていたのを、linkedSignal の source-based 派生に置き換える。"selection
を source にしつつ、他経路([formField] や combobox の two-way)からの
書き込みも受け付ける" という意図がそのまま型で表現でき、effect / untracked /
イベントハンドラなしで宣言的に同期できる。
- searchModel: selectedCities[0] を source、選択時は { city } で上書き、それ
以外は previous.value 保持([formField] の書き込みを尊重)
- isExpanded: 同じ source、選択時は false にリセット、それ以外は previous.value
保持(combobox の two-way 書き込みを尊重)
suggestionItems の "完全一致なら隠す" ワークアラウンドも不要に
(isExpanded が選択時に false になり popup の ng-template ごと描画されない)。
無効入力時のフォーカス移動を viewChild(Combobox).element.focus() ではなく searchForm.city().focusBoundControl() に置き換える。Signal Forms 経由で [formField] でバインドされた control が自動で focus される。viewChild ごと 撤去できた(location-select.ts と同じパターン)。 linkedSignal の型は computation の戻り値型注釈で十分推論できるので、 ジェネリクスパラメータも省略。
httpResource / onSubmit ともに searchForm.city().value() で field 経由の 読み出しに揃える。searchModel を直接読むのと結果は同じだが、Signal Forms の field アクセス API を示すサンプルとして読みやすい。あわせて suggestions を suggestionItems より前に宣言して top-down に読めるよう並び替え。
searchModel / isExpanded の linkedSignal で同じ selectedCities[0] を読んで いたのを共通の selectedCity computed に抽出。"両者は同じ source に対する 別の派生" という関係性が型と宣言から読める。
"isExpanded" だけだと述語の対象(何が expanded か)が読めない。combobox の expanded model に対応する signal なので isComboboxExpanded にする。
CI の `npm ci` で @angular-eslint/[email protected] が @angular/[email protected] と peer dep 競合して install 失敗するため、.npmrc で legacy-peer-deps を有効化して local / CI を統一する。angular-eslint が v22 対応リリースを出したら撤去できる。
@angular/build:unit-test の Vitest runner は isolate: false が既定で、複数 test ファイルが同じ browser context を共有する。ファイルごとの afterAll で worker.stop() を呼ぶと、他ファイルの test 実行中に MSW が無効化されて "Could not verify ..." などの onError 経路に落ちる(CI で再現)。 ブラウザコンテキスト自体が test run 終了時に破棄されるので明示停止は不要。
lacolaco
added a commit
that referenced
this pull request
Jun 6, 2026
PR #50 で Angular v22 にアップデート済みなので README 冒頭の "Angular 21" / "experimental" の記述を v22 として書き直す。
2 tasks done
lacolaco
added a commit
that referenced
this pull request
Jun 6, 2026
PR #50 で Angular v22 にアップデート済みなので README 冒頭の "Angular 21" / "experimental" の記述を v22 として書き直す。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
@angular/cli@angular/core@angular/aria@angular/ssr@angular/cdkを v21.1 → v22.0 へng updateで lockstep アップデート@angular/ariaCombobox 破壊的変更(developer preview → stable)に追随してcity-searchサンプルを移行し、Signal Forms と組み合わせる実装を整理@angular/build:unit-testの msw prebundle 衝突 (angular-cli#32523) をrunnerConfig+ 独自vitest-base.config.tsで回避ComboboxHarness+TestbedHarnessEnvironment経由に書き直し(v22 のkeyboardEventRelayEffectが@testing-library/angularのsafeDetectChangesと衝突してNG0101を出すため)city-search の整理(v22 stable API へ)
ComboboxInput/ComboboxPopupContainerの 3 段構成ngComboboxを<input>に直接、ngComboboxPopup+ngComboboxWidgetの 2 段構成combobox.close()メソッドexpanded: ModelSignal<boolean>を直接書き換え[(values)](複数形)[(value)](Listbox 値モデルは依然V[])cityInputValuesignal + setTimeout debounce + 手書き sync effect[formField]="searchForm.city"一本で Signal Forms に統合selectedCities→searchModel反映を effect +untrackedで同期linkedSignal({ source, computation })で宣言的に伝播combobox.expanded.set(false)を effect で手続き的にクローズisComboboxExpandedも同じ source のlinkedSignalで選択時に自動 falseviewChild(Combobox).element.focus()searchForm.city().focusBoundControl()最終的に effect / untracked / イベントハンドラ全廃で、コード量は半分以下に縮減。
linkedSignalのsourceは両者で共通のselectedCitycomputed に集約し、「Listbox 選択を source にした派生」という関係性が宣言から読める形になっています。ng update 自動投入の互換シム撤去
ng updatemigration が v21 挙動維持のために自動投入した opt-out も削除しました(CLAUDE.md "no backwards-compat shims" 規約):withNoIncrementalHydration(): 当 repo に@defer (hydrate ...)は無く影響なしtsconfig.app.jsonのnullishCoalescingNotNullable/optionalChainNotNullablesuppress: 該当パターンが無く警告も出ないv22 互換のための追加 devDep
@angular/cdk@angular-eslint/builderplaywright@vitest/browser-playwrightの peer dep が--legacy-peer-depsで脱落するのを防止動作確認
npm test— 107/107 passednpm run build— bundle 生成成功(ng update の自動 migration /withNoIncrementalHydration削除後も SSR prerender 11 routes OK)npm run lint— All files pass lintingcode-criticApproved(初回時点。以降の linkedSignal/[formField] refactor は本 PR で完結)npm start+ 実 Chrome (chrome-devtools MCP) で動作確認:[MSW] GET /api/cities 200 OK)🤖 Generated with Claude Code