fix(forms): signal forms support, CNPJ_ALPHA mask, dependency bumps#1619
Merged
Conversation
…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.
This was referenced Jul 1, 2026
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).
13 tasks
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.
This was referenced Jul 2, 2026
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
This was referenced Jul 2, 2026
- 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
This was referenced Jul 2, 2026
Closed
Closed
- 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
This was referenced Jul 3, 2026
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).
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
FormValueControl) sync bugs: initial value not mask-formatted on render,disabledschema state not propagating, dynamic mask-change not re-syncing the DOM. Root causes were test/DOM-timing related (queueMicrotaskdeferral) plus a genuine mode-detection fix.FormValueControlinterface surface tovalue/disabled/touchedonly, resolving host-directive conflicts (see community issue/PR Regression: nxMask conflicts with native required/readonly inputs #1601/fix(ref:1601): Relax FormValueControl implementation #1602).CPF_CNPJ_ALPHAalphanumeric 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 theHOURS('H') substring check was accidentally interceptingCPF_CNPJ_ALPHA(since "ALPHA" contains 'H').forms.spec.ts(initial render, disabled state, dynamic mask change) alongside existing reactive/template-driven coverage.tsconfig.spec.jsongap (both root and lib) causingtest-setup.tsto be excluded from the TypeScript program, which silently broke the test script's exit code.@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.)..gitignorepatterns, fixed a git-worktree portability issue in the commit-msg hook.NgxMaskDirective'sFormValueControlsurface no longer exposeserrors,dirty,invalid,pending,readonly,required, ornameinputs — onlyvalue,disabled, andtouchedremain. Any consumer binding the removed inputs on[mask]/ngxMaskin 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
ref:1560fix.Test plan
ng test ngx-mask-lib— 573 passed, 1 skipped, 0 failedng test ngx-mask(demo app) — 97 passedng build ngx-mask-lib— successng build(demo app, production) — successng lint ngx-mask-lib— cleanFollow-up: refactor + CI fix
ngx-mask.directive.ts/ngx-mask.service.tsinternals (dead-code cleanup, extracted CPF/CNPJ counting helper) — no behavior change, verified by full test suite staying green throughout.options.component.spec.ts(1370 → ~1094 lines) via shared test fixture/helper functions — same test coverage (97 passed).cypress/angular-zoneless, since this project no longer depends onzone.js) exposed a genuine dirty-marking bug inwriteValue()— internalonChangecalls during programmatic value normalization were spuriously markingFormControldirty, 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).quality-checkfailure that was blocking this PR (Can't resolve 'zone.js/testing').Updated test plan:
ng test ngx-mask-lib— 573 passed, 1 skipped, 0 failedng test ngx-mask(demo app) — 97 passednpx cypress run --component— 192 passed, 0 failed (previously 20 failing before the fix)ng build ngx-mask-lib— successng lint ngx-mask-lib— cleanFollow-up 2: signal forms initial-value fix + full mask-type coverage
[formField](Signal Forms), the firstwriteValue()ran before the directive's firstngOnChanges, so initial values rendered unmasked for literal-inserting masks (date/time/separator). Now deferred and replayed after mask configuration applies.||masks, date/time, percent, prefix/suffix (+420 test lines).Follow-up 3: tri-mode parity guarantee
FormFieldechoes view-originated model updates back throughwriteValue(), corrupting masks with ambiguous unmasked forms (IP:192.168.1.78→192.168.178). Fixed via a propagation marker that skips exactly that echo.Follow-up 4: community issue sweep — final waves
Completes the systematic pass over all 74 open issues. This round (local batches, single push):
M0/d0(00M0d0) no longer misread year digits as day/monthplaceHolderCharacterno longer leaks into the model'00'+suffix=":00") render instead of being strippedInputEvent.inputType999SSS) get position-aware validationSweep 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
IP,CPF_CNPJ, and the newCPF_CNPJ_ALPHAmask (all three form modes, with tests — demo suite now 106).