Skip to content

feat: JsonField and a Json column handler for forms - #107

Closed
matej21 wants to merge 2 commits into
mainfrom
feat/json-field
Closed

feat: JsonField and a Json column handler for forms#107
matej21 wants to merge 2 commits into
mainfrom
feat/json-field

Conversation

@matej21

@matej21 matej21 commented Sep 9, 2026

Copy link
Copy Markdown
Member

Adds first-class JSON support to forms, in two layers.

1. createJsonHandler() in bindx-form

'Json' was already a declared ColumnType, but defaultTypeHandlers had no entry for it, so a json column fell back to the string handler: formatValue rendered an object as [object Object] and parseValue wrote a raw string into a jsonb column. <FormInput> over a json column was broken today, independently of the missing component.

The new handler is registered for ColumnType 'Json', so plain <FormInput> over a json column now works with no extra wiring:

  • formatValue pretty-prints (JSON.stringify(value, null, 2)), but returns the user's raw text while that text still corresponds to the current value — the same state pattern createDoubleHandler uses, so half-typed JSON is not reformatted under the cursor.
  • parseValue parses; on failure it keeps the previous value instead of writing garbage.
  • Empty input writes emptyValue (default null).
  • Optional validate for shape checks beyond syntax.
  • Optional formatOnBlur pretty-prints the input when the field loses focus.

2. The parse error is a client error on the field

An unparseable value must not be silently savable, so the handler reports the failure and FormInput adds it via field.addError(). It renders inline through the existing FormError / data-invalid path and BatchPersister refuses to persist an entity with client errors. setValue clears non-sticky client errors, so the error disappears by itself as soon as the text parses again.

Three small additions in bindx-form carry that:

  • FormInputHandlerContext gains currentValue (so a handler can keep the value it cannot parse) and setError (applied by FormInput after the value is written, since setValue clears client errors).
  • FormInputHandler gains an optional onBlur, used for pretty-print-on-blur and to re-report the error after useFormInputValidationHandler clears the field's errors on blur.
  • FormInputProps / useFormInputHandler accept a handler override, so a component can supply a configured handler instead of relying on schema metadata.

JSONValue is defined in bindx-form and is structurally identical to the JSONValue that bindx-generator emits into generated entity types, so a generated json column matches without a cast.

3. JsonField in bindx-ui

A thin wrapper in the style of TextareaField (FormFieldScopeFormContainerFormInputTextareaAutosize), exported from the form barrel and the package root.

<JsonField field={it.mapping} label="Mapping" formatOnBlur validate={requireObject} />
prop type note
field FieldRef<JSONValue | null>
label / description ReactNode from FormContainerProps
required boolean
inputProps ComponentProps<typeof TextareaAutosize> placeholder, minRows/maxRows, … — the convention the other field components use for rows/placeholder
emptyValue JSONValue | null default null
validate (value: JSONValue) => string | null shape checks beyond syntax
formatOnBlur boolean default false

No schema-aware editor — explicitly out of scope, as the issue proposes.

Unlike the workaround in the issue, the component keeps no local mirror of the value, so there is no server-data seed sync to get wrong: the raw text lives in the input handler's state and is only preferred over the formatted value while it still parses to the current value.

Tests

17 new tests (packages/bindx-form/tests/jsonInput.test.tsx, packages/bindx-ui/tests/jsonField.test.tsx): value round-trips through the accessor as a value and not a string, handler resolution from the Json column type, raw text not reformatted mid-typing, invalid JSON shows the error and keeps the previous accessor value, the error survives blur, it clears when the text parses again, persist is blocked while it is present and succeeds after the fix, empty input writes emptyValue (default and custom), validate failures, and formatOnBlur.

Full suite: 2043 pass / 0 fail. tsc --build clean.

The reporter offered a PR; this covers the same API surface plus the missing column-type handler underneath it.

Fixes #31

🤖 Generated with Claude Code

https://claude.ai/code/session_01Euwkf2wtutqvtE4YRutU5R

matej21 and others added 2 commits September 9, 2026 16:08
'Json' was already a declared ColumnType with no handler, so a json column
fell back to the string handler: the formatter rendered [object Object] and
the parser wrote a raw string into a jsonb column.

createJsonHandler() parses the input into a JSON value, keeps the user's raw
text while it still matches the current value, keeps the previous value when
the text does not parse, and reports the failure through the new ctx.setError,
which FormInput turns into a client error on the field. That surfaces inline
and blocks persist until the text parses again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Euwkf2wtutqvtE4YRutU5R
Entities with c.jsonColumn() fields had no first-class form component, so
every consumer rebuilt the JSON round-trip, the seed sync and the inline
parse-error UX by hand.

JsonField mirrors TextareaField and drives the Json handler, with emptyValue,
an optional validate for shape checks, and formatOnBlur.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Euwkf2wtutqvtE4YRutU5R
@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.

bindx-ui: add JsonField form component for c.jsonColumn() entities

1 participant