Skip to content

fix: answer DOM probes on NativeScriptDocument instead of throwing#176

Open
edusperoni wants to merge 1 commit into
mainfrom
fix/document-dom-probes
Open

fix: answer DOM probes on NativeScriptDocument instead of throwing#176
edusperoni wants to merge 1 commit into
mainfrom
fix/document-dom-probes

Conversation

@edusperoni

Copy link
Copy Markdown
Collaborator

What

Angular discovers some optional configuration by reading the DOM. NativeScriptDocument exposed a body that was truthy but hollow ({ isOverride: true }), so those probes did not fall into their "not found" path — they threw.

The clearest case is CSP_NONCE, whose default factory is:

inject(DOCUMENT).body?.querySelector('[ngCspNonce]')?.getAttribute('ngCspNonce') || null

The body?. guard anticipates a missing body, not a body without querySelector, so this fails with inject(...).body?.querySelector is not a function.

Signal forms hit this on the very first [formField] binding: FormField injects the root InputValidityMonitor, whose default implementation (AnimationInputValidityMonitor) injects CSP_NONCE in a field initializer. The result is that every signal-form dialog crashes at construction on Angular 22. Note that inject(CSP_NONCE, { optional: true }) does not help — InjectionToken defaults to providedIn: 'root', so the token always resolves and the DOM-reading factory always runs.

Changes

  • NativeScriptDocument now answers DOM lookups with "nothing found" rather than leaving the methods undefined: querySelector, querySelectorAll, getElementById, getElementsByTagName, getElementsByClassName on the document itself, plus body and head (getAttribute/hasAttribute included there).
  • createElement deliberately keeps throwing. Probing for optional config should degrade quietly; constructing real nodes is a caller bug and should stay loud.
  • CSP_NONCE is provided as null in NATIVESCRIPT_MODULE_STATIC_PROVIDERS. There is no CSP in a NativeScript runtime, and providing it skips the DOM-reading factory entirely.

Why CSP_NONCE must be a root provider

It is deliberately in NATIVESCRIPT_MODULE_STATIC_PROVIDERS (root scope) rather than COMMON_PROVIDERS (platform scope). R3Injector.get() checks its own records, then self-instantiates any providedIn: 'root' token whose scope it matches, and only then delegates to its parent. A platform-level provider is therefore never consulted. Verified empirically — see below.

Verification

Ran Angular 22's real DI against both the old and new document shapes:

Scenario Result
Hollow body, no provider (status quo) throws inject(...).body?.querySelector is not a function
Shim only null
Root CSP_NONCE provider only null
Both (this PR) null
Platform-level CSP_NONCE provider throws — confirms root scope is required

The first row reproduces the reported crash string exactly.

nx build angular passes, and packages/angular/src/lib/platform-nativescript.ts is prettier-clean. nativescript.ts has pre-existing formatting drift that I left alone to keep the diff focused.

Follow-up

This is the robust workaround. The cleaner fix belongs upstream: InputValidityMonitor is an abstract class whose provider is { providedIn: 'root', useClass: forwardRef(() => AnimationInputValidityMonitor) } — an obvious platform seam that Angular's own tests already override — but the symbol is not exported. I have a companion Angular PR adding ɵInputValidityMonitor to @angular/forms/signals. Once that lands, @nativescript/angular can provide an inert monitor and never touch the DOM at all.

* Angular discovers some optional configuration by reading the DOM. The
  CSP_NONCE default factory, for example, evaluates
  `DOCUMENT.body.querySelector('[ngCspNonce]')`. `NativeScriptDocument.body`
  was a hollow object, so the optional chaining in those probes did not help
  and they failed with `body?.querySelector is not a function`. Signal forms
  reach this on the first `[formField]` binding, which crashed every
  signal-form dialog at construction.
* `querySelector`, `querySelectorAll`, `getElementById`,
  `getElementsByTagName` and `getElementsByClassName` now answer "nothing
  found" on the document and on `body`/`head`. `createElement` still throws,
  since constructing real nodes is a caller bug rather than a probe.
* CSP_NONCE is also provided as `null`, as there is no CSP in a NativeScript
  runtime. It has to be a root provider: the root injector resolves
  `providedIn: 'root'` tokens itself before consulting the platform injector,
  so providing it in COMMON_PROVIDERS would have had no effect.
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: eb649bb1-b114-4527-9615-91a420ff4894

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/document-dom-probes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant