DEV Community

Cover image for Running Angular 22 With CDK / Material 16: Bridging the `ComponentFactoryResolver` Gap
Zahid Hossain
Zahid Hossain

Posted on

Running Angular 22 With CDK / Material 16: Bridging the `ComponentFactoryResolver` Gap

Subtitle: How to keep an older Angular CDK / Material stack alive after Angular 22 removes factory-based component creation — without rewriting your entire UI layer overnight.

Tags: Angular · Angular 22 · CDK · Material · Migration · postinstall · Compatibility Shim

Audience: Frontend engineers upgrading Angular core while temporarily pinning @angular/cdk / @angular/material to an older major.


Table of contents

  1. The problem in one sentence
  2. Why this combination is common
  3. What Angular 22 actually removed
  4. How the crash shows up
  5. Architecture of the fix
  6. Step-by-step implementation guide
  7. What each patch does (deep dive)
  8. How the runtime flow works (visual)
  9. Pros and cons
  10. Best practices when you force this compatibility
  11. Corner cases you must keep in mind
  12. Exit strategy: how to leave the shim behind
  13. Checklist before you ship
  14. Conclusion

1. The problem in one sentence

You upgraded Angular core to 22, but you still need CDK / Material 16. The app builds (or almost builds), then dies at runtime when Overlay, Dialog, Menu, SnackBar, or Tabs try to inject APIs that no longer exist in @angular/core@22.

That mismatch is not a bug in your feature code. It is a version skew between framework internals and an older UI toolkit that still speaks the pre-Ivy factory language.


2. Why this combination is common

Teams often need to split a migration into two tracks:

Track Goal Risk if delayed
Core / build / TypeScript Stay on a supported Angular major, Node, and TS Security, ecosystem lag, CI failures
CDK / Material / UI rewrite Move off legacy Material APIs, theming, and breaking CSS Large visual + a11y + regression surface

Material major upgrades are rarely “bump the package and ship.” They touch:

  • Legacy Material modules (MatLegacy*)
  • Theme / typography / density
  • Overlay-heavy flows (dialogs, menus, snackbars, bottom sheets)
  • Custom CDK portals and tooltips

So a deliberate temporary state looks like:

@angular/core            → 22.x
@angular/cdk             → 16.x   (pinned)
@angular/material        → 16.x   (pinned)
Enter fullscreen mode Exit fullscreen mode

Peer dependency warnings are expected. Runtime breakage is the real blocker.

Important: This article describes a temporary compatibility bridge, not a long-term architecture. Treat the shim as debt with an owner and a removal date.


3. What Angular 22 actually removed

Angular has been steering away from factory-based dynamic components since Ivy. In Angular 22, the cleanup became a hard break:

  • ComponentFactoryResolverremoved from the public API
  • ComponentFactoryremoved
  • The old ViewContainerRef.createComponent(factory, index, …) overload — gone

Official direction (paraphrased from Angular’s breaking-change notes):

Pass the component class directly to ViewContainerRef.createComponent, or use the standalone createComponent() function.

Old pattern (CDK 16 still uses this)

const resolver = this.componentFactoryResolver;
const factory = resolver.resolveComponentFactory(MyComponent);
viewContainerRef.createComponent(
  factory,
  index,
  injector,
  projectableNodes,
);
Enter fullscreen mode Exit fullscreen mode

New pattern (Angular 22+)

viewContainerRef.createComponent(MyComponent, {
  index,
  injector,
  projectableNodes,
});
Enter fullscreen mode Exit fullscreen mode

Or, when there is no ViewContainerRef:

createComponent(MyComponent, {
  environmentInjector: appRef.injector,
  elementInjector: customInjector,
  projectableNodes,
});
Enter fullscreen mode Exit fullscreen mode

CDK / Material 16’s portal and overlay stack still follows the old path. Angular 22 follows only the new path. Something has to translate between them.


4. How the crash shows up

Typical symptoms after a “core-only” upgrade:

Symptom A — DI token is undefined

TypeError: Cannot read properties of undefined (reading 'hasOwnProperty')
Enter fullscreen mode Exit fullscreen mode

Why: Overlay / portal code still declares an injection dependency on ComponentFactoryResolver. In Angular 22 that token is missing, so DI effectively injects undefined, then blows up inside the injector.

Symptom B — Dialog / Menu / Tooltip attach fails later

Even if you somehow get past bootstrap, attaching a component through DomPortalOutlet or CdkPortalOutlet can fail because createComponent is called with a factory, not a Type.

Symptom C — SnackBar content is empty or throws

MatSnackBar often goes through CdkPortalOutlet. That path is easy to miss if you only patched DomPortalOutlet.

Symptom D — “It works until I restart / reinstall”

If you hand-edit node_modules without a postinstall script, the next clean install wipes the fix. Always automate the patch.


5. Architecture of the fix

The workable temporary approach has three layers:

Mental model

Layer Role
Shim in core Makes the token exist again for dependency injection
Portal patches Makes component creation use Angular 22’s Type-based API
postinstall Makes the bridge repeatable on every clean install / CI machine

Without all three, you usually fix one crash and discover the next on the first snackbar or dialog.


6. Step-by-step implementation guide

Step 0 — Decide this is temporary debt

Write down:

  • Why Material cannot move yet
  • Who owns the follow-up Material upgrade
  • Target removal sprint / release

If you cannot answer those, do not ship the shim.

Step 1 — Pin versions intentionally

Keep core and CDK majors explicit in package.json:

{
  "dependencies": {
    "@angular/core": "^22.1.1",
    "@angular/cdk": "16.2.14",
    "@angular/material": "16.2.14"
  }
}
Enter fullscreen mode Exit fullscreen mode

Prefer an exact CDK / Material pin (no ^) while the shim exists, so a minor CDK bump does not silently invalidate your string replacements.

Use whatever peer-deps strategy your team already uses (legacy-peer-deps, overrides, etc.). Peer warnings alone are not the fix — the runtime bridge is.

Step 2 — Confirm the failure is the factory gap

Reproduce with a minimal Overlay / Dialog / SnackBar path. Confirm the stack involves:

  • @angular/cdk/overlay or @angular/cdk/portal
  • Missing / undefined ComponentFactoryResolver
  • Or createComponent receiving a factory

Step 3 — Add an idempotent postinstall patch script

Create something like:

scripts/patch-angular-22-cdk-compat.mjs

Design rules:

  1. Idempotent — running twice must be a no-op (use unique markers).
  2. Fail loud — if Angular / CDK bundle shape changes, throw instead of silently doing nothing.
  3. No secrets — only patch known public package files under node_modules.
  4. Deterministic — same input packages → same output.

Wire it:

{
  "scripts": {
    "postinstall": "node scripts/patch-angular-22-cdk-compat.mjs"
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 4 — Shim ComponentFactoryResolver back onto @angular/core

Conceptually:

  1. Open node_modules/@angular/core/fesm2022/core.mjs
  2. Insert a small class that:
    • Is providedIn: 'root'
    • Implements resolveComponentFactory(componentType) using Angular’s internal component definition helpers still present in the bundle
  3. Re-export ComponentFactoryResolver from the public export list

This restores the DI token. It does not by itself make every portal attach path correct.

Step 5 — Patch CDK portal attach paths

In node_modules/@angular/cdk/fesm2022/portal.mjs, update three call sites:

  1. DomPortalOutlet with ViewContainerRef
  2. DomPortalOutlet without ViewContainerRef
  3. CdkPortalOutlet (SnackBar and template portals)

Replace factory-based creation with Type-based creation.

Step 6 — Clear stale prebundles

Angular’s Vite-based serve cache can keep old prebundled CDK copies. After patching, delete the relevant Vite deps cache under .angular/cache (or restart with a clean cache) so ng serve loads patched files.

Step 7 — Verify the high-risk UI surfaces

At minimum:

  • [ ] App bootstrap
  • [ ] MatDialog open / close
  • [ ] MatMenu / select overlays
  • [ ] MatSnackBar with custom component content
  • [ ] MatBottomSheet / sidenav overlays if used
  • [ ] Custom CDK Overlay + ComponentPortal directives
  • [ ] Tabs that dynamically create content

Step 8 — Document the debt in the repo

In README or an ADR:

  • Why the shim exists
  • Which Angular / CDK versions it targets
  • How to remove it when Material is upgraded
  • Who to ping when postinstall starts failing

7. What each patch does (deep dive)

Patch A — Core shim (DI compatibility)

Problem: CDK Overlay injects ComponentFactoryResolver. Token missing → crash at construction.

Fix: Provide a root injectable with the same name/token and a resolveComponentFactory method.

What it must not do:

  • Invent a second Angular runtime
  • Monkey-patch unrelated DI tokens
  • Hide unrelated upgrade errors

Patch B — DomPortalOutlet + ViewContainerRef

Before (CDK 16):

componentRef = portal.viewContainerRef.createComponent(
  componentFactory,
  portal.viewContainerRef.length,
  portal.injector || portal.viewContainerRef.injector,
  portal.projectableNodes || undefined,
);
Enter fullscreen mode Exit fullscreen mode

After (Angular 22-compatible):

componentRef = portal.viewContainerRef.createComponent(portal.component, {
  index: portal.viewContainerRef.length,
  injector: portal.injector || portal.viewContainerRef.injector,
  projectableNodes: portal.projectableNodes || undefined,
});
Enter fullscreen mode Exit fullscreen mode

Patch C — DomPortalOutlet without ViewContainerRef

Some attaches (including certain snackbar / overlay container paths) call componentFactory.create(...).

After:

componentRef = createComponent(portal.component, {
  environmentInjector: this._appRef.injector,
  elementInjector: portal.injector || this._defaultInjector || Injector.NULL,
  projectableNodes: portal.projectableNodes || undefined,
});
Enter fullscreen mode Exit fullscreen mode

Remember to import createComponent from @angular/core in the portal bundle if it is not already imported.

Patch D — CdkPortalOutlet

This is the path many teams miss. SnackBar custom content often fails here even after DomPortalOutlet looks “fixed.”

Replace:

resolver.resolveComponentFactory(portal.component);
viewContainerRef.createComponent(componentFactory, ...);
Enter fullscreen mode Exit fullscreen mode

With:

viewContainerRef.createComponent(portal.component, { ...options });
Enter fullscreen mode Exit fullscreen mode

8. How the runtime flow works (visual)

Before the bridge (broken)

After the bridge (working)

Install-time flow

Prefer also opening the interactive companion visualization in Cursor if you are reading this from the engineering workspace — it animates the same flows step by step.


9. Pros and cons

Pros

Benefit Why it matters
Unblocks Angular 22 core upgrade Security / ecosystem / TypeScript 6 alignment
Avoids a big-bang Material rewrite UI risk stays in a dedicated follow-up
Localized change Mostly node_modules patching + one script
CI-reproducible postinstall applies on every clean install
Incremental verification You can validate Overlay surfaces before Material migration

Cons

Cost Why it hurts
Unsupported configuration Angular does not promise this pairing
Fragile string patches CDK / core FESM shape can change on patch releases
Hidden debt New hires may not know why postinstall exists
Dual mental models App code on Angular 22 APIs; UI kit on Angular 16 APIs
Cache footguns Stale Vite prebundles look like “random” regressions
Slows Material urgency Temporary fixes become permanent if no removal plan

Bottom line: Great as a bridge. Expensive as a destination.


10. Best practices when you force this compatibility

These practices matter precisely because the solution is forceful.

1. Pin exact CDK / Material versions

Do not float on ^16.2.14 while the patch depends on exact source strings.

2. Keep the patch script boring and strict

  • Unique markers for every edit
  • Throw on unexpected file shape
  • Log success / already-applied / failure clearly
  • Never “best effort” swallow errors in CI

3. Treat postinstall failures as build failures

If the shim cannot apply, fail the pipeline. Shipping an unpatched tree is worse than a red build.

4. Add a smoke suite for Overlay surfaces

Automate dialog, menu, snackbar, and custom portal opens in CI. Unit tests alone rarely catch this.

5. Separate “core upgrade” PRs from “Material upgrade” PRs

Reviewers need a clean story:

  • PR A: Angular 22 + compatibility bridge
  • PR B: Material / CDK upgrade + delete bridge

6. Do not spread shims into application code

Prefer patching the boundary (core export + CDK portal) over sprinkling fake providers across feature modules. One bridge is easier to delete than twenty local workarounds.

7. Record version matrix in an ADR

Example:

Supported bridge matrix:
  @angular/core@22.1.x
  @angular/cdk@16.2.14
  @angular/material@16.2.14
Anything else = re-verify or remove the script.
Enter fullscreen mode Exit fullscreen mode

8. Plan the removal before merge

A shim without a removal ticket is how temporary hacks become folklore.

9. Prefer official upgrades when feasible

If Material can move in the same quarter, do that instead. The bridge is for when UI migration truly cannot keep pace with core.

10. Watch Angular / CDK release notes every bump

Even a core patch release can reshape FESM output enough to break naive replacements.


11. Corner cases you must keep in mind

  1. SnackBar custom component content

    Often uses CdkPortalOutlet, not only DomPortalOutlet. Patch both.

  2. Portals without ViewContainerRef

    The no-VCR branch needs createComponent(...) with an environmentInjector. Easy to miss.

  3. Custom application code still using ComponentFactoryResolver

    Search your src/ too. The shim may hide app debt that should be migrated properly.

  4. Legacy Material modules

    MatLegacy* can surface more Overlay paths. Test legacy and non-legacy entry points.

  5. Multiple Angular copies

    If a dependency bundles another @angular/core, your shim may patch the wrong copy. Deduplicate.

  6. pnpm / Yarn / npm differences

    Patch paths assume classic node_modules/@angular/... layout. Adapt for PnP or isolated linkers.

  7. Docker / CI clean installs

    Locally patched trees lie. Always validate from a fresh npm ci.

  8. Vite / esbuild prebundle cache

    After patching, clear Angular’s Vite deps cache or you will debug ghosts.

  9. SSR / non-browser platforms

    If you use Angular Universal or similar, verify overlay-heavy routes on server and client.

  10. Animations module presence

    Overlay behavior can differ if animations providers are missing or duplicated.

  11. Zone vs zoneless

    Timing bugs in overlays can look like “compat failures” under zoneless experiments. Isolate variables.

  12. Partial upgrades

    Updating only @angular/material but not @angular/cdk (or the reverse) can reintroduce skew inside the UI toolkit itself.

  13. Marker collisions

    If you fork the script across repos, keep marker comments unique and stable.

  14. Security / supply chain reviews

    Some orgs flag postinstall mutation of node_modules. Document intent for AppSec.

  15. Developer onboarding

    “Delete node_modules and reinstall” must remain a safe recovery path — which is why idempotent postinstall matters.


12. Exit strategy: how to leave the shim behind

When you are ready:

  1. Upgrade @angular/cdk and @angular/material to a major aligned with your Angular core.
  2. Run Material migrations / theme updates.
  3. Remove postinstall patch script and markers.
  4. Grep the repo for shim markers and ComponentFactoryResolver leftovers.
  5. Delete ADR “temporary” section or mark it superseded.
  6. Re-run the Overlay smoke suite on a clean install.

Success looks like:

npm ci
# no compatibility patch needed
ng serve / ng build / e2e overlays → green
Enter fullscreen mode Exit fullscreen mode

13. Checklist before you ship

  • [ ] Angular core on 22.x; CDK / Material pinned exactly
  • [ ] Idempotent postinstall patch committed
  • [ ] Core CFR shim applies on clean install
  • [ ] DomPortalOutlet with-VCR path patched
  • [ ] DomPortalOutlet no-VCR path patched
  • [ ] CdkPortalOutlet path patched
  • [ ] Vite / Angular dependency cache strategy documented
  • [ ] Dialog, Menu, SnackBar, custom Overlay smoke-tested
  • [ ] CI fails if patch cannot apply
  • [ ] ADR + removal ticket exist
  • [ ] No confidential internal names required to understand the script

14. Conclusion

Angular 22’s removal of ComponentFactoryResolver is the right long-term cleanup. CDK / Material 16 still living on factory-based portals is the short-term reality for many large apps.

You can bridge that gap by:

  1. Restoring a minimal DI-compatible shim in @angular/core
  2. Rewriting CDK portal attach calls to Angular 22’s Type-based createComponent
  3. Running that bridge from postinstall so every machine gets the same result

Do it deliberately. Pin versions. Fail loud. Test overlays. And schedule the Material upgrade that makes the bridge unnecessary.

Temporary compatibility is engineering. Permanent unsupported skew is risk.


References


This article describes a general compatibility pattern for Angular 22 + CDK/Material 16. Adapt paths and markers to your package manager and bundle layout. Validate on a clean install before production.

Top comments (0)