Skip to content

fix(forms): signal forms support, CNPJ_ALPHA mask, dependency bumps#1619

Merged
NepipenkoIgor merged 41 commits into
developfrom
fix/21.0.2
Jul 3, 2026
Merged

fix(forms): signal forms support, CNPJ_ALPHA mask, dependency bumps#1619
NepipenkoIgor merged 41 commits into
developfrom
fix/21.0.2

Conversation

@NepipenkoIgor

@NepipenkoIgor NepipenkoIgor commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fixed Angular Signal Forms (FormValueControl) sync bugs: initial value not mask-formatted on render, disabled schema state not propagating, dynamic mask-change not re-syncing the DOM. Root causes were test/DOM-timing related (queueMicrotask deferral) plus a genuine mode-detection fix.
  • Relaxed the FormValueControl interface surface to value/disabled/touched only, resolving host-directive conflicts (see community issue/PR Regression: nxMask conflicts with native required/readonly inputs #1601/fix(ref:1601): Relax FormValueControl implementation #1602).
  • Added CPF_CNPJ_ALPHA alphanumeric mask format (ported from community PR feat(mask): add support for alphanumeric CNPJ format #1592, credit @stLmpp), plus fixed a mask-dispatch routing bug where the HOURS ('H') substring check was accidentally intercepting CPF_CNPJ_ALPHA (since "ALPHA" contains 'H').
  • Added full Signal Forms unit test coverage in forms.spec.ts (initial render, disabled state, dynamic mask change) alongside existing reactive/template-driven coverage.
  • Fixed a tsconfig.spec.json gap (both root and lib) causing test-setup.ts to be excluded from the TypeScript program, which silently broke the test script's exit code.
  • Bumped all @angular/* packages in lockstep to the latest 21.x line (21.2.17) — deliberately not upgrading to Angular 22 — plus various devDependency bumps (vitest, typescript-eslint, postcss, prettier, stylelint, etc.).
  • Repo hygiene: removed stray local debug output files, added .gitignore patterns, fixed a git-worktree portability issue in the commit-msg hook.

⚠️ Breaking Change

NgxMaskDirective's FormValueControl surface no longer exposes errors, dirty, invalid, pending, readonly, required, or name inputs — only value, disabled, and touched remain. Any consumer binding the removed inputs on [mask]/ngxMask in a Signal Forms context must remove those bindings. Recommend a minor or major version bump with an explicit migration note in the changelog, not a silent patch release.

Community PRs considered

Test plan

  • ng test ngx-mask-lib — 573 passed, 1 skipped, 0 failed
  • ng test ngx-mask (demo app) — 97 passed
  • ng build ngx-mask-lib — success
  • ng build (demo app, production) — success
  • ng lint ngx-mask-lib — clean

Follow-up: refactor + CI fix

  • Refactored ngx-mask.directive.ts/ngx-mask.service.ts internals (dead-code cleanup, extracted CPF/CNPJ counting helper) — no behavior change, verified by full test suite staying green throughout.
  • Reduced duplication in the demo's options.component.spec.ts (1370 → ~1094 lines) via shared test fixture/helper functions — same test coverage (97 passed).
  • Fixed a real bug: migrating Cypress component tests to zoneless change detection (cypress/angular-zoneless, since this project no longer depends on zone.js) exposed a genuine dirty-marking bug in writeValue() — internal onChange calls during programmatic value normalization were spuriously marking FormControl dirty, previously masked by zone.js's CD timing. Fixed with a re-entrancy guard (same root cause as community PR fix: don't call onchange from writeValue #1577).
  • This also resolves the CI quality-check failure that was blocking this PR (Can't resolve 'zone.js/testing').

Updated test plan:

  • ng test ngx-mask-lib — 573 passed, 1 skipped, 0 failed
  • ng test ngx-mask (demo app) — 97 passed
  • npx cypress run --component — 192 passed, 0 failed (previously 20 failing before the fix)
  • ng build ngx-mask-lib — success
  • ng lint ngx-mask-lib — clean

Follow-up 2: signal forms initial-value fix + full mask-type coverage

  • Fixed: under [formField] (Signal Forms), the first writeValue() ran before the directive's first ngOnChanges, so initial values rendered unmasked for literal-inserting masks (date/time/separator). Now deferred and replayed after mask configuration applies.
  • Added Signal Forms unit coverage across mask types: separator.2 reflow, schema validation, dynamic || masks, date/time, percent, prefix/suffix (+420 test lines).
  • Verified: lib 587 passed / demo 97 / Cypress 192 — all green.

Follow-up 3: tri-mode parity guarantee

  • New tri-mode test harness runs every mask type through Reactive, Template-driven, AND Signal Forms bindings with identical expectations (~54 parity tests) — all three modes now verified equivalent.
  • The harness immediately caught a real Signal Forms bug: FormField echoes view-originated model updates back through writeValue(), corrupting masks with ambiguous unmasked forms (IP: 192.168.1.78192.168.178). Fixed via a propagation marker that skips exactly that echo.
  • Verified: lib 641 / demo 97 / Cypress 192 — all green.

Follow-up 4: community issue sweep — final waves

Completes the systematic pass over all 74 open issues. This round (local batches, single push):

Sweep totals: 37 bugs fixed, 16 issues closed with answers, 4 awaiting reporter verification, 15 feature requests + Angular 22 deliberately skipped. Suite: 741 unit / 97 demo / 200 Cypress — all green.

Follow-up 5: demo content, ng21 refactor, dependency cleanup

  • Demo: new example cards for IP, CPF_CNPJ, and the new CPF_CNPJ_ALPHA mask (all three form modes, with tests — demo suite now 106).
  • Demo refactored to Angular 21 best practices (OnPush everywhere, computed-derived content; control flow and signal APIs were already modern).
  • Dependency audit: 18 unused devDependencies removed (~200 MB of node_modules, incl. one critical CVE chain via @web/test-runner), 4 phantom deps declared explicitly, dead scripts/targets deleted.

igor.nepipenko and others added 11 commits January 29, 2026 17:46
…bugs

BREAKING CHANGE: NgxMaskDirective's FormValueControl no longer exposes
errors, dirty, invalid, pending, readonly, required, or name inputs.
Only value, disabled, and touched remain. Consumers binding the removed
inputs in a Signal Forms context must remove those bindings.

Also fixes signal-forms mode detection and value/property sync bugs
in ngx-mask.service.ts (unrelated CNPJ_ALPHA routing fix in the same
file is covered by this commit as well, since the hunks are
interleaved).
Ports the alphanumeric CNPJ mask format (AA.AAA.AAA/AAAA-00) from
community PR #1592, including the applier service pattern handling
and placeholder-character offset logic.
Both the ngx-mask-lib and root demo app tsconfig.spec.json were
missing src/test-setup.ts in their include array, causing the
esbuild-based test builder to fail its first build attempt and
silently retry, which sometimes propagated a nonzero exit code
even when the retried test run passed.
Bump all @angular/* packages and related tooling in lockstep to
21.2.17 (deliberately staying on Angular 21, not jumping to 22).
Also bumps typescript-eslint (8.54.0 -> 8.62.1), vitest
(4.0.18 -> 4.1.9), postcss (8.5.6 -> 8.5.16), prettier
(3.8.1 -> 3.9.4), stylelint (17.0.0 -> 17.14.0), ng-packagr
(21.1.0 -> 21.2.5), and other devDependencies.
Fix commit-msg hook to resolve COMMIT_EDITMSG via git rev-parse
--git-dir for worktree portability, and ignore stray root debug
.txt files produced by local test runs.
Migrating Cypress component tests to zoneless change detection exposed
a real bug: writeValue()'s internal logic could invoke onChange as a
side effect of normalizing a programmatically-written value (e.g.
leadZero-padding), which Angular interprets as a user edit and
spuriously marks the FormControl dirty. Zone.js's CD timing had been
masking this.

Fixed with a re-entrancy guard, using NgControl (lazily resolved via
Injector to avoid a circular-DI cycle with NG_VALUE_ACCESSOR) to
detect and restore pristine/untouched state after a writeValue-driven
emission.

Same root cause as community PR #1577 ("don't call onChange from
writeValue").
The project no longer depends on zone.js (it bootstraps zoneless via
provideZonelessChangeDetection()), but cypress/angular's mount helper
unconditionally imports zone.js, which was failing CI's quality-check
job with "Can't resolve 'zone.js/testing'". Switch to
cypress/angular-zoneless.
Verifies community PR #1533's reported scenario (separator mask without
explicit precision + leadZero=true causing a RangeError from
toFixed(Infinity)) does not reproduce on this branch — the guard already
exists in ngx-mask.service.ts via Number.isFinite(separatorPrecision).
Signal Forms' FormField prefers a host-provided NG_VALUE_ACCESSOR and
runs its control-sync before sibling directives' first ngOnChanges, so
the first writeValue() arrived with an unconfigured mask service (empty
maskExpression) and rendered the raw value for masks that insert literal
characters (date, time, separator). Defer the pre-configuration
writeValue and replay it after the first ngOnChanges pass applies the
mask config. Also correct the stale _isCvaMode doc comment.
Cover signal forms coverage gaps: separator.2 decimal reflow (echo
guard under active reformatting), schema-based validation, dynamic
OR-masks, date/time masks with mid-typing separator auto-insertion,
percent, and prefix/suffix.
Signal Forms' FormField echoes every model update back through
writeValue(); re-masking the unmasked echo is destructive for masks
whose unmasked form is ambiguous (IP: typed 192.168.1.78 -> unmasked
192168178 -> re-masked 192.168.178, dots lost). Guard with a
_lastPropagatedValue marker: writeValue skips exactly the
view-originated echo (consumed on entry; empty-string writes are never
skipped so reset('') still clears; sits after the pending-initial-value
defer; returns before pristine/dirty handling). One forms.spec
expectation asserted the echo artifact itself and is corrected to the
parity-correct display/model values, identical to reactive mode.
Shared harness runs every mask type (basic, pattern literals,
separator, percent, IP, CPF/CNPJ, date, time, email, repeat, OR-masks,
prefix/suffix) through Reactive, Template-driven, and Signal Forms
bindings with identical expectations; it discovered the FormField
writeValue echo bug fixed in the previous commit.
The directive's constructor disabled-effect fired its first run with the
default false value after Forms' setUpControl had already applied
disabled=true — both are queueMicrotask-deferred and run FIFO, so the
effect's initial write re-enabled initially-disabled controls. Guard with
a _disabledEverSet flag so the initial default false is never forwarded;
only explicit disabled input changes reach the control.

Adds tri-mode initially-disabled coverage (new initialDisabled harness
param) and reactive disable()/enable() scenarios.

Closes #1607
Closes #1614
Backspacing the leading digit of a separator value collapsed the
remaining zeros in two stages: the backspaced-zero guard disjunct
treated any all-zero remainder as removable and collapsed it entirely,
and the separatorPrecision=0 leading-zero stripper then ate one more
zero per keystroke. Give all-zero remainders passthrough semantics so
1,000,000 -> 000,000, 100,000 -> 00,000 and 500 -> 00 behave
consistently.

Closes #1355
Closes #1578
- scan to first pattern position in single-char rejection guard so
  masks with two leading literals like +( accept input (#1498)
- expand exponential-notation numeric FormControl values to plain
  decimals at applyMask entry for separator masks; remove dead no-op
  replace from numberToString (#1492)
- use BigInt-based _stringToFixed beyond double precision in
  _checkPrecision to stop corruption of >15-significant-digit values,
  preserving exact toFixed semantics in the safe range (#1567)

Closes #1498
Closes #1492
Closes #1567
- drop the stale hiddenInput shadow when values contain a literal *
  in the non-hiddenInput selection-edit case so selection edits are
  not discarded (#1504)
- make select-all + Backspace deterministic: preventDefault, single
  emission and synchronous view write instead of the two-event flow
  Firefox dropped (#1350)
- validate || masks with a boundary-aware rule: a short value is
  valid if it is a pattern-valid prefix ending at a special-char
  boundary and meets some alternative's length (#1583)

Closes #1504
Closes #1350
Closes #1583
- applier: guard backward-looking month/day heuristics when digit
  tokens abut M0/d0 without separators (00M0d0) so year digits are
  not misread; #1611 flow-gate untouched
- service: strip placeHolderCharacter from formControlResult emitted
  model values when showMaskTyped is on and placeholder is not a
  special char, so placeholders like X no longer leak into the model
- applier: anchor checkAndRemoveSuffix to end of value with an
  edit-direction discriminator via actualValue, so initial '00' no
  longer collides with suffix ':00'

Closes #1523
Closes #1519
Closes #1495
…render

- #1573: remove all locale-API calls from the service; replace
  toLocaleString('fullwide') (silent fallback to runtime default locale)
  with pure-string number conversion and make
  currentLocaleDecimalMarker() return a constant '.' instead of
  consulting the runtime locale.
- #1379: onInput rewrites el.value only when the mask transform actually
  changed the value; an empty mask is now a true passthrough, preserving
  the browser's user-edit flag and native constraint validation.
- #1305/#1264: mirror the initial writeValue render synchronously via
  Renderer2 so MatInput's shouldLabelFloat sees the value in the same CD
  pass; the deferred FIFO write still re-applies the same value.
- basic-logic.spec.ts: 4 vacuous legacy expectations corrected to assert
  the settled render (documented in-code).

Closes #1573
Closes #1379
Closes #1305
Closes #1264
- #1293/#1497: composition gate now waits only for letter-accepting
  masks (numeric masks process insertCompositionText live, keeping the
  model in sync before blur); deletions detected via
  InputEvent.inputType (deleteContentBackward/Forward) since Android
  keydown reports Unidentified/229; stale-code reset for IME inserts.
  Device-unverifiable remainder for Android is documented in the issues.
- #1515: trailing-optional masks keep the early-return validation;
  leading-optional plain-token masks (e.g. 999SSS) use a position-aware
  backtracking matcher (_matchesMaskWithOptionalSkip); exotic masks
  fall through to the legacy length check.

Closes #1293
Closes #1497
Closes #1515
Three new Common-cases cards with per-mode tests (reactive/template/signal);
CPF_CNPJ_ALPHA showcases the new 21.1.0 mask.
OnPush on all components; imperative card-switch replaced with signal +
computed-derived content; readonly/protected hygiene; audit confirmed
control flow, signal inputs and inject already modern.
Audit-driven cleanup: dead types packages, eslint meta-package duplicates,
snyk/markdownlint/ncu/ghpages/http-server with orphaned scripts, dead @nrwl
ct target. 4 phantom deps declared explicitly (globals, @eslint/js,
stylelint-scss, postcss-scss); node_modules 1.3G -> 1.1G. @types/jest
retained: it currently supplies test-global types (migration to
vitest/globals tracked in notes).
@NepipenkoIgor
NepipenkoIgor merged commit 802f424 into develop Jul 3, 2026
8 checks passed
@NepipenkoIgor
NepipenkoIgor deleted the fix/21.0.2 branch July 3, 2026 10:11
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