Skip to content

fix(bindx-form): stop a blur from wiping every error on the field - #108

Closed
matej21 wants to merge 2 commits into
mainfrom
fix/blur-clears-all-field-errors
Closed

fix(bindx-form): stop a blur from wiping every error on the field#108
matej21 wants to merge 2 commits into
mainfrom
fix/blur-clears-all-field-errors

Conversation

@matej21

@matej21 matej21 commented Sep 9, 2026

Copy link
Copy Markdown
Member

Symptom

A user saves, the server returns a validation error on a field, the user tabs through that field — and the message silently disappears while the value is still invalid. The same happens to any client-side error some other code put on the field (a custom validation, a parse error): one blur wipes it.

Cause

useFormInputValidationHandler calls field.clearErrors() unconditionally in two places:

  • in onBlur, right before re-adding the HTML5 validation message
  • in the value-sync useEffect below it, whenever the validity message changes

FieldHandle.clearErrors() dispatched clearFieldErrors(...) with no narrowing, so the whole field was reset. A blur was a global error reset for that field.

The machinery for a narrower clear already existed and was simply never used: ErrorStore.clearFieldErrors(fieldKey, source?) filtered by source, and the CLEAR_FIELD_ERRORS action already carried the source field — no caller ever passed it. setValue already distinguishes "mine to clear" from "not mine" via clearNonStickyFieldErrors.

Mechanism

The handler tags the error it creates with a code (html5-validity) and clears exactly that error. Field-error clearing takes a filter instead of a bare source:

interface FieldErrorFilter {
	readonly source?: FieldError['source']
	readonly code?: string
}

An omitted key matches anything, so clearErrors() with no argument still clears everything. The filter is threaded through FieldRef.clearErrorsclearFieldErrors action → ActionDispatcherSnapshotStoreErrorStore, replacing the source?: 'client' | 'server' parameter that was there before. HTML5_VALIDATION_ERROR_CODE is exported from @contember/bindx-form so consumers can recognise the hook's own error.

Rejected: clearErrors('client') — clearing by source only. It fixes the server-error case but not the second requirement: a JSON parse error, or any other client-side validation raised by other code, is also source: 'client' and would still be wiped on blur. Ownership is not the same question as source.

Also rejected: identity-based removal (removeError(error) keeping a reference to the instance it added). It needs the hook to hold a live reference across renders and rekeys, and adds a second removal API for one caller. The code field already exists on BindxError, is free-form, and was unused for this.

Scope kept deliberately narrow: only the field error path takes a filter. clearEntityErrors / clearRelationErrors keep their source? parameter — nothing needs code-level narrowing there, and widening them would be an unused API.

Other clearErrors() callers

Nothing else in the repo relied on the clear-everything behaviour, and no call site changes meaning:

  • EntityHandle.$clearErrors(), HasOneHandle.$clearErrors(), HasManyListHandle.clearErrors() — untouched, still clear all errors on their target.
  • ErrorStore.clearAllServerErrors — internal call updated from 'server' to { source: 'server' }, same behaviour.
  • PlaceholderHandle and the collector proxies implement clearErrors() with no parameters — still valid against the widened signature.
  • Test callers passing no filter are unaffected; one store test that passed 'client' now passes { source: 'client' }.

One behavioural nuance worth knowing: a filtered clear that matches nothing no longer bumps the error-state version. Subscriber notification is unchanged (it happens in SnapshotStore either way).

Tests

packages/bindx-form/tests/formInputValidation.test.tsx drives the real FormFieldScope / FormInput / FormError path:

  • a server error survives a blur
  • a server error survives the sync effect when the validity changes after a blur
  • a client error raised by other code survives a blur
  • the HTML5 validation message still appears on blur and still clears once the field is valid (this one passed before the fix — it is the regression guard)

The first three failed before the change. happy-dom computes validity but leaves validationMessage empty, so the tests set the message with setCustomValidity. tests/unit/store/snapshotStore.test.ts gains a case for clearing by code.

bun run typecheck clean, bun run test 2031 pass / 0 fail (browser tests not run), bun run lint 0 errors.

Interaction with #107

#107 (feat/json-field, still open at the time of writing) hit exactly this bug: its JSON parse error was wiped on blur, so it works around it by adding an onBlur hook to FormInputHandler and re-reporting the error after the blur. This PR branches off main and does not depend on it. Once both land, that workaround can be revisited — with this fix the parse error survives the blur on its own, so the re-report may no longer be needed (the onBlur handler hook may still be wanted for other reasons; that call is best made on #107).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Euwkf2wtutqvtE4YRutU5R

matej21 and others added 2 commits September 9, 2026 16:39
Blurring an input clears all errors on the field, so a server validation
error and a client error raised elsewhere both vanish while the value is
still invalid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Euwkf2wtutqvtE4YRutU5R
The handler tags the HTML5 validation message it adds with a code and clears
exactly that error, instead of resetting the whole field. Field-error clearing
now takes a FieldErrorFilter (source and/or code) in place of the source-only
argument the store already accepted but nothing passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Euwkf2wtutqvtE4YRutU5R
@matej21
matej21 force-pushed the fix/blur-clears-all-field-errors branch from 2505631 to 2408c98 Compare September 9, 2026 14:41
matej21 added a commit that referenced this pull request Sep 10, 2026
@matej21

matej21 commented Sep 10, 2026

Copy link
Copy Markdown
Member Author

Consolidated into #109, merged there.

@matej21 matej21 closed this Sep 10, 2026
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