Skip to content

fix(validate): keep model values when fields use validate-on - #103

Merged
ErickXavier merged 2 commits into
mainfrom
fix/model-validate-on-clears-value
Oct 14, 2026
Merged

ErickXavier merged 2 commits into
mainfrom
fix/model-validate-on-clears-value

Conversation

@ErickXavier

@ErickXavier ErickXavier commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

When a form uses validate-on, either on a field or on the form, a field with model loses the user's value. The documented validated login posts {"email":"","password":""}. Text inputs, textareas, checkboxes, selects and radios with model are all affected.

Root cause

The problem is in bindFieldEvents() in src/validate/element.js.

  1. When the form or any field has validate-on, the form's validate init attaches value and focusout listeners directly to each field.
  2. The form initialises before its descendants. So these listeners are registered before Core's model attaches its own listener on the same field.
  3. On each value event, the validate listener runs first. checkValidity() then calls ctx.$set("$form", …) and _notifyFormSubtree() synchronously.
  4. That notification makes model re-render the stale context value over the user's input. model's own listener then reads the stale value back.
  5. Select, checkbox and radio have one more problem: model syncs on change, but validate listened on input, which fires earlier.

The legacy path (no validate-on) delegates its listeners on the form and is not affected.

This is not a regression. It reproduces with Core and Elements v1.20.2 and v1.21.1, with current main, and in the cross combinations.

Fix

The fix only changes fields that carry model. With validate-on, those fields never kept their value on v1.21.1:

  • Event. A model field's value listener (the validation trigger, or the silent dirty/re-validate listener) uses the same event as Core's model: change for select, checkbox and radio, and input otherwise.
  • Order. After the subtree is processed, a microtask re-appends that value listener behind model's listener. A dispose guard skips the re-append when the form is gone before the microtask runs.
  • Fields without model. Their listeners, events and order are exactly as on v1.21.1. That covers on:input reading $form, on:focusout reading touched state and errors, and aria-invalid while typing in a blur field.

Known trade-offs, which apply only to model fields:

  • Validation now runs after the field's own handlers. A model field's own on:input handler reads the previous $form state. The aria-invalid timing set by field-level validate follows the form's triggers.
  • A model that is attached after the one-shot microtask still lands after validate. One example is a field that an if adds later. This case was not verified in a browser.

Tests

Rule strings now use the real | separator, so the rules are actually exercised.

Jest, __tests__/validation.test.js tests 36 to 48:

  • model fields:
    • field-level blur and input;
    • form-level validate-on;
    • state on an ancestor;
    • an invalid value kept and reported on blur, with the submit button disabled;
    • a state-to-input update;
    • a checkbox;
    • a select and radios.
  • Fields without model, pinned to their v1.21.1 behavior:
    • on:input reads the current keystroke's state;
    • on:focusout reads true:Required;
    • aria-invalid is set while typing in a blur field;
    • no listener is re-bound.
  • Disposal through Core's disposeTree before the microtask runs.
  • Failure checks:
    • 8 of these tests fail on main.
    • 6 fail against the previous head of this PR: the checkbox and select test, the radio test, and the 4 pins for fields without model.

e2e, e2e/examples/validation-model.html and e2e/tests/validation-model.spec.ts: these use real keyboard and mouse input in Chromium, Firefox and WebKit.

  • With Elements v1.21.1, the four model tests fail and the three tests for fields without model pass. So the pins match v1.21.1.

Verification

  • npx jest: 532 passed, 5 skipped, against Core main src/ and against Core v1.21.1 src/.
  • cd e2e && npx playwright test on port 3953: 456 passed, against the Core main build and against the Core v1.21.1 build.
  • QA regression hunt: 18 scenarios × 4 combinations × 3 browsers. Every scenario without model is identical to Core v1.21.1 + Elements v1.21.1: R01, R02, R03, R05, R11, R12 and R14. R04, R06 and R16 are also identical.
  • Every difference is a model field that now keeps its value, plus the error text that follows from that value. The affected scenarios are B01, B02, R07, R08, R09, R10, R13 and R15. In R08, only the form with model differs.

With validate-on, the form binds per-field input/focusout listeners
while it initialises, before the field's own `model` listener. On each
keystroke checkValidity() notified the context synchronously, so
`model` re-rendered its stale value ('') over the typed text, then read
that '' back. A validated login posted empty credentials.

Re-append the field listeners in a microtask, once the subtree is
processed, so they run after `model`. Long-standing: reproduces with
Core and Elements v1.20.2 and v1.21.1.
Re-appending every field listener changed v1.21.1 behavior for fields
without `model`: on:input read a stale $form, on:focusout saw the
field untouched, and aria-invalid lagged while typing. Only `model`
fields are re-ordered now; other fields keep their v1.21.1 order.

`model` on select/checkbox/radio syncs on "change", so validate's
value listener for those fields uses "change" too. Otherwise the
"input" notify reverted the user's pick before `model` read it.

Tests use the real `|` rule separator and pin the non-model order.
@ErickXavier
ErickXavier merged commit 73fa034 into main Oct 14, 2026
2 checks passed
ErickXavier added a commit that referenced this pull request Oct 15, 2026
#103 moved the validate-on value listener of select/checkbox/radio
fields with `model` from "input" to "change". A script-dispatched
"input" no longer validated those fields, unlike v1.21.1.

- listen on "input" again, in the v1.21.1 position; no "change"
  listener, so a real pick (input then change) validates once and an
  explicit validate-on keeps its v1.21.1 meaning
- before validating, write the field value to its `model` the way
  core's `model` does on "change", so the validation notify no longer
  re-renders the stale value over the pick
- skip that write for an unchecked radio and for an if-gated `model`
ErickXavier added a commit that referenced this pull request Oct 15, 2026
chore: reapply October PRs (#101, #102, #103) onto release-1.22
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