Skip to content

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

@jonasnobile

Description

@jonasnobile

Problem

bindx-ui ships InputField / TextareaField / SelectEnumField / CheckboxField / RadioEnumField, but no JsonField. Entities backed by c.jsonColumn() have no first-class form component, so applications either:

  1. Reach for TextareaField, which is typed FieldRef<string> and can't round-trip JSONValue.
  2. Build a custom controlled component using useField() + local string state.

We hit this implementing a ImportMappingTemplate entity in contember-external/npi — two c.jsonColumn() fields (mapping and transforms) needed operator-editable forms. We went with option (2) but it's ~150 lines of plumbing that every consumer of jsonb forms will duplicate.

Confirmed against @contember/bindx-ui@0.1.37.

Current workaround (the duplicated plumbing)

// Sketch — full version is ~150 lines including formatOnBlur + error UX
export function JsonTextareaField({ field, label, description, placeholder, required, rows, emptyValue = null }: Props) {
    const accessor = useField(field)
    const [text, setText] = useState(stringifyValue(accessor.value ?? accessor.serverValue))
    const [error, setError] = useState<string | null>(null)

    // Sync local state on first load (server data arrives after first render)
    useEffect(() => { /* … */ }, [accessor.serverValue])

    const commit = useCallback((next: string) => {
        setText(next)
        const trimmed = next.trim()
        if (trimmed === '') { accessor.setValue(emptyValue); return }
        try {
            const parsed = JSON.parse(trimmed) as JSONValue
            setError(null)
            accessor.setValue(parsed)
        } catch (err) {
            setError(`Neplatný JSON: ${err.message}`)
        }
    }, [accessor, emptyValue])

    const handleFormat = useCallback(() => { /* pretty-print if parses */ }, [accessor, text])

    return (
        <div>
            <Label>{label}{required && '*'}</Label>
            <Button onClick={handleFormat}>Format JSON</Button>
            <Textarea value={text} onChange={e => commit(e.target.value)} rows={rows ?? 8} />
            {error && <p className=\"text-destructive\">{error}</p>}
        </div>
    )
}

Three things every consumer ends up reinventing:

  1. JSON ↔ string round-trip. JSON.parse on commit, JSON.stringify for the initial textarea seed.
  2. Server-data seed sync. useEffect to refresh the textarea once bindx loads the entity, but only while the user hasn't typed yet.
  3. Inline parse-error UX. Show the error, keep the previous valid accessor.value until a parse succeeds.

Proposal — JsonField form component

Sketch of the public API (matching the existing field-component conventions):

export interface JsonFieldProps {
    field: FieldRef<JSONValue | null>
    label: string
    description?: string
    placeholder?: string
    required?: boolean
    rows?: number
    /** What to setValue when the textarea is empty. Default `null`. */
    emptyValue?: JSONValue | null
    /** Optional schema-shape validation beyond syntax. Returns an error message or null. */
    validate?: (value: JSONValue) => string | null
    /** Pretty-print on blur if it parses. Default false. */
    formatOnBlur?: boolean
}

export function JsonField(props: JsonFieldProps): ReactElement

Behavior:

  • Initial textarea value is JSON.stringify(field.serverValue ?? field.value, null, 2).
  • On change: JSON.parse the trimmed input; on success call field.setValue(parsed) and (if validate is set) run schema validation; on failure surface the syntax error inline and leave the accessor's prior value intact.
  • Empty string → field.setValue(emptyValue).
  • A "Format JSON" trigger (could be a small button slot or a formatOnBlur prop) pretty-prints the current input if it parses.
  • Same data-invalid attribute as InputField so the existing CSS hooks light up.

This is a strict subset of what a richer "JSON schema-aware editor" would do — that's a separate feature. This component just gives consumers the round-trip + validation primitive every jsonb field needs.

Why not a rich schema-aware editor

Some jsonb fields carry typed shapes (in our case Record<string, string> for column mapping, Record<string, TransformSpec[]> for transforms), and a richer editor with field-aware controls would be nice. But:

  • Typed-shape editors are domain-specific (drag-to-map columns, transform pickers, etc.). They belong in app code, not in the framework.
  • The plain JSON textarea handles every jsonb field shape, even ones the framework can't introspect.
  • A schema-aware editor can be a separate component layered on top (<JsonField field={…} renderEditor={…}> slot pattern) once a couple of real consumers have stabilized.

Shipping the plain JsonField first unblocks the common case.

PR offer

Happy to send a PR with this API + the four convention details (data-invalid hook, server-data seed sync, parse error inline UX, optional format-on-blur). Let me know if the API shape works or if you'd want it sliced differently.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions