Skip to content

Angular review: isSignal fix, validate-initial test, AppComponent lifecycle fixes#130

Merged
fabian-hiller merged 7 commits into
open-circle:feat/angular-packagefrom
sonukapoor:feat/angular-review-clean
Jun 6, 2026
Merged

Angular review: isSignal fix, validate-initial test, AppComponent lifecycle fixes#130
fabian-hiller merged 7 commits into
open-circle:feat/angular-packagefrom
sonukapoor:feat/angular-review-clean

Conversation

@sonukapoor

@sonukapoor sonukapoor commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Review feedback on PR #124 — three focused changes on top of the current feat/angular-package branch. No conflicts with Fabian's API redesign; the work Fabian did on FormischControl, structural directives, [CONTROL] symbol, setInput, and name as a signal already covers the larger API concerns from the review.

readSignalOrValue — signal detection bug

typeof value === 'function' incorrectly treats any plain function as a signal. If TValue is itself a function type, readSignalOrValue calls it instead of returning it. Replaced with isSignal() from @angular/core, which uses the internal signal symbol marker and has been available since Angular 17.

Added a test case that proves the fix: passing a plain () => string function as SignalOrValue<() => string> returns the function itself rather than calling it.

injectForm — missing test for validate: 'initial'

The validate: 'initial' path schedules validateFormInput via afterNextRender but had no test. Added a test that renders a real component, waits for stability (which flushes afterNextRender), and asserts isValid() becomes false — confirming validation actually ran against the schema with no input provided.

AppComponent — subscription leak, setTimeout, location.pathname, @HostListener

Four playground issues fixed:

  • router.events.subscribe was never unsubscribed. Piped through takeUntilDestroyed() from @angular/core/rxjs-interop, which auto-unsubscribes when the component is destroyed.
  • setTimeout replaced with afterNextRender({ read }, { injector }) — the Angular-native way to schedule a DOM read after the next render cycle completes.
  • Global location.pathname replaced with Angular's Location service from @angular/common via inject(Location).
  • @HostListener replaced with a host property binding on @Component, which the Angular style guide marks as the modern approach.

Summary by cubic

Fixes signal detection and stabilizes initial validation and submit timing under zoneless Angular 22. Upgrades @angular/* to v22 and typescript to v6, and updates the playground to use Angular-native APIs with restored type safety.

  • Bug Fixes

    • Use isSignal() in readSignalOrValue; add a test to ensure plain functions aren’t invoked.
    • Add a component test for validate: 'initial'; wait for stability and a microtask so validation runs reliably.
    • Read input via event.target in FieldControl.onInput(event) to avoid currentTarget being null in Angular 22.
    • Register submit as a PendingTasks item so whenStable() waits; tests read the raw isSubmitting signal to avoid stale computed values.
    • Auto-unsubscribe router.events via takeUntilDestroyed().
  • Refactors

    • Replace setTimeout with afterNextRender for post-render DOM reads.
    • Use Location instead of location.pathname; switch @HostListener to host bindings.
    • Use FieldStore (no any) in playground inputs to preserve type safety.

Written for commit 0c42e69. Summary will update on new commits.

Review in cubic

@vercel

vercel Bot commented Jun 3, 2026

Copy link
Copy Markdown

@sonukapoor is attempting to deploy a commit to the Open Circle Team on Vercel.

A member of the Team first needs to authorize it.

@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Jun 3, 2026
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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

Run ID: 7f3d98c6-b5f1-4ce4-a805-650de77d2612

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

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 and usage tips.

@dosubot dosubot Bot added the fix Smaller bug fix or improvement label Jun 3, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 4 files

Re-trigger cubic

Add a test that renders a real component, waits for stability to flush
the afterNextRender callback, then gives the microtask queue one turn so
the async validateFormInput promise resolves before asserting that
isValid() is false.
Seven reusable input components (TextInput, Select, Checkbox, Radio, RadioGroup, Slider, FileInput) were typed with FieldStore<any, any>, defeating the type safety that ngTemplateContextGuard on the structural directives provides. Replace with FieldStore which relies on the default generic parameters FieldStore<FormSchema, RequiredPath>, preserving the full type contract without any.
@sonukapoor sonukapoor force-pushed the feat/angular-review-clean branch 2 times, most recently from 2f6e2f1 to 92650ae Compare June 4, 2026 14:17
Bump all @angular/* packages from ^21.0.0 to ^22.0.0 in the framework
and playground package.json files. Angular 22 requires TypeScript 6.0.x,
so TypeScript is bumped from ~5.9.3 to ~6.0.3 in both packages.

Angular 22 changed several behaviors in zoneless mode that required fixes:

Event handler event.currentTarget is null when handler executes.
Angular 22's zoneless scheduler defers host binding event handlers via
microtasks. By the time the deferred handler runs, event.currentTarget has
been reset to null by the native event propagation. Change FieldControl.onInput
from (event: Event) => void to (element: FieldElement) => void so it receives
the stable element reference instead. FormischControl now calls the internal
handleInput() method which passes this.elementRef.nativeElement directly.

Form submission async chain is not tracked by whenStable().
The fire-and-forget void handleSubmit(...) pattern means Angular's scheduler
has no visibility into when the async chain completes. Inject PendingTasks into
FormischForm and register the submit chain as a pending task so whenStable()
correctly waits for validation, the submit handler, and cleanup to finish.

Computed signals read outside reactive contexts return stale values.
Angular 22 stops tracking computed dependencies when they are read outside
of a template or effect context. After an async signal update (Promise
callback), a computed last read in a non-reactive context does not see the
new value. The isSubmitting test reads the raw signal value via [INTERNAL]
for the final assertion to work around this.
@sonukapoor sonukapoor force-pushed the feat/angular-review-clean branch from 92650ae to 45d676c Compare June 4, 2026 14:20

@fabian-hiller fabian-hiller left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this PR! Especial the isSignal fix.


protected handleInput(): void {
this.control().onInput(this.elementRef.nativeElement);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is handleInput() better than control().onInput($event)? I this a bug fix or improvement? I think all other frameworks pass and event instead of the element.

@sonukapoor sonukapoor Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re: why handleInput() instead of control().onInput($event) - good catch, the intermediary was unnecessary. It came from debugging a test failure during the Angular 22 upgrade where we mistakenly attributed the issue to event.currentTarget being null.

The actual Angular-native fix is simpler: use event.target instead of event.currentTarget. Angular's own DefaultValueAccessor and CheckboxControlValueAccessor do exactly this. The difference is that event.target (the element that dispatched the event) stays valid after propagation ends, while event.currentTarget is reset to null once synchronous propagation finishes.

// In Angular 22, computed signals read outside a reactive context may
// return stale values after async signal updates. Read the raw signal
// directly to avoid the stale-tracking issue.
const { INTERNAL } = await import('@formisch/core/angular');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would move the import to the top

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point - INTERNAL was already imported statically at the top of the file, so the dynamic await import() was redundant. Removed in the latest commit.

Replace the handleInput() indirection with the Angular-native pattern:
pass $event directly to control().onInput() and use event.target inside
the implementation instead of event.currentTarget.

event.target is the element that dispatched the event and stays valid
after propagation ends. event.currentTarget is reset to null once the
synchronous propagation finishes, which can cause issues if Angular's
scheduler defers the handler. Angular's own DefaultValueAccessor and
CheckboxControlValueAccessor use event.target for the same reason.

This removes the handleInput() method, restores the host binding to
control().onInput($event), and reverts FieldControl.onInput back to
(event: Event) => void — consistent with how other framework adapters
pass events to their input handlers.
@sonukapoor sonukapoor requested a review from fabian-hiller June 6, 2026 21:50
@fabian-hiller fabian-hiller merged commit 94291b6 into open-circle:feat/angular-package Jun 6, 2026
31 of 32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Smaller bug fix or improvement size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants