Skip to content

InlineEditableField

Complete form field implementation for inline editing with label, error display, and customization options.

Import

tsx
import { InlineEditableField } from '@/shared/ui/components/editable/InlineEditableField'

Props

InlineEditableFieldProps

PropTypeDefaultDescription
Core
idstringRequiredUnique field identifier
valuestringRequiredCurrent value
onSave(value: string) => Promise<void> | voidRequiredSave function (sync or async)
Display
labelReactNode-Field label
placeholderstring-Input placeholder text
typestring'text'HTML input type
State
disabledbooleanfalseWhether field is disabled
readOnlybooleanfalseWhether field is read-only
isSavingboolean-External saving state (controlled)
Callbacks
onCancel() => void-Called when editing is cancelled
onBlur() => void-Custom blur handler
Customization
renderDisplay(startEdit: () => void) => ReactNode-Custom display render function
renderInput(props: InputRenderProps) => ReactNode-Custom input render function
Styling
classNamestring-Container CSS classes
displayClassNamestring-Display mode CSS classes
inputClassNamestring-Input CSS classes
Features
editedbooleanfalseMark field as edited
showEditedBadgebooleantrueShow "edited" badge when edited is true
editedBadgeContentReactNode'Edited'Content for edited badge
saveOnBlurbooleanfalseAutomatically save when input loses focus
hideActionsbooleanfalseHide save/cancel buttons
errorstring-Error message to display

InputRenderProps

Props passed to renderInput function:

ts
interface InputRenderProps {
  value: string
  onChange: (value: string) => void
  onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void
  onBlur: () => void
  autoFocus: boolean
  isSaving: boolean
}

Examples

Basic Usage

tsx
<InlineEditableField
  id="user-name"
  label="Name"
  value={user.name}
  onSave={async (value) => await updateUser({ name: value })}
  placeholder="Enter name..."
/>

With Edited Badge

tsx
<InlineEditableField
  id="user-email"
  label="Email"
  value={user.email}
  onSave={handleSave}
  edited={user.email !== originalEmail}
  showEditedBadge
  editedBadgeContent="Modified"
/>

Save on Blur (No Buttons)

tsx
<InlineEditableField
  id="quick-edit"
  value={value}
  onSave={handleSave}
  saveOnBlur
  hideActions
  placeholder="Click to edit..."
/>

With Error Handling

tsx
<InlineEditableField
  id="validated-field"
  label="Username"
  value={username}
  onSave={async (value) => {
    if (value.length < 3) {
      throw new Error('Username must be at least 3 characters')
    }
    await api.updateUsername(value)
  }}
  error={validationError}
/>

Custom Display

tsx
<InlineEditableField
  id="custom-display"
  value={user.name}
  onSave={handleSave}
  renderDisplay={(startEdit) => (
    <div className="flex items-center gap-2" onClick={startEdit}>
      <Avatar src={user.avatar} size="sm" />
      <div>
        <p className="font-medium">{user.name}</p>
        <p className="text-sm text-muted-foreground">{user.role}</p>
      </div>
    </div>
  )}
/>

Custom Input (Textarea)

tsx
<InlineEditableField
  id="description"
  label="Description"
  value={description}
  onSave={handleSave}
  renderInput={({ value, onChange, onKeyDown, autoFocus, isSaving }) => (
    <Textarea
      value={value}
      onChange={(e) => onChange(e.target.value)}
      onKeyDown={(e) => {
        // Prevent Enter from saving for multi-line
        if (e.key === 'Enter' && !e.shiftKey) {
          e.stopPropagation()
        }
        onKeyDown(e)
      }}
      autoFocus={autoFocus}
      disabled={isSaving}
      rows={4}
    />
  )}
/>

Custom Input (Select)

tsx
<InlineEditableField
  id="status"
  label="Status"
  value={status}
  onSave={handleSave}
  renderInput={({ value, onChange, autoFocus }) => (
    <Select value={value} onValueChange={onChange}>
      <SelectTrigger autoFocus={autoFocus}>
        <SelectValue />
      </SelectTrigger>
      <SelectContent>
        <SelectItem value="active">Active</SelectItem>
        <SelectItem value="inactive">Inactive</SelectItem>
        <SelectItem value="pending">Pending</SelectItem>
      </SelectContent>
    </Select>
  )}
/>

With Custom Styling

tsx
<InlineEditableField
  id="styled-field"
  value={value}
  onSave={handleSave}
  className="border-b border-border pb-2"
  displayClassName="text-lg font-semibold"
  inputClassName="text-lg font-semibold"
/>

Disabled State

tsx
<InlineEditableField
  id="readonly-field"
  label="ID"
  value={user.id}
  onSave={() => {}}
  disabled
/>

Read-Only State

tsx
<InlineEditableField
  id="readonly-field"
  label="Created At"
  value={formatDate(user.createdAt)}
  onSave={() => {}}
  readOnly
/>

Features

Automatic Keyboard Shortcuts

  • Enter: Save changes
  • Escape: Cancel editing

Loading State

Shows "Saving..." text during async save operations:

tsx
<InlineEditableField
  value={value}
  onSave={async (value) => {
    // "Saving..." automatically shown
    await api.update(value)
    // Loading state automatically cleared
  }}
/>

Error Display

Displays error messages below the input:

tsx
<InlineEditableField
  value={value}
  onSave={async (value) => {
    const result = await api.update(value)
    if (!result.ok) throw new Error('Update failed')
  }}
  error={errorMessage}
/>

Edited Badge

Shows a badge when the field has been modified:

tsx
<InlineEditableField
  value={currentValue}
  onSave={handleSave}
  edited={currentValue !== originalValue}
  showEditedBadge
  editedBadgeContent={<span className="text-orange-500">●</span>}
/>

Common Patterns

Profile Field

tsx
function ProfileField({ label, value, onSave }) {
  return (
    <div className="flex items-center gap-4 py-2">
      <span className="w-32 text-sm text-muted-foreground">{label}</span>
      <InlineEditableField
        id={`profile-${label}`}
        value={value}
        onSave={onSave}
        className="flex-1"
      />
    </div>
  )
}

Quick Edit

tsx
<InlineEditableField
  id="quick-edit"
  value={value}
  onSave={handleSave}
  saveOnBlur
  hideActions
  placeholder="Click to edit..."
/>

Validated Field

tsx
function ValidatedField({ value, onSave, validate }) {
  const [error, setError] = useState('')

  const handleSave = async (newValue: string) => {
    const validationError = validate(newValue)
    if (validationError) {
      setError(validationError)
      throw new Error(validationError)
    }
    setError('')
    await onSave(newValue)
  }

  return (
    <InlineEditableField
      id="validated"
      value={value}
      onSave={handleSave}
      error={error}
    />
  )
}

Accessibility

  • Labels: Proper label association with id
  • Keyboard navigation: Full keyboard support
  • Focus management: Auto-focus on edit mode
  • Error announcements: Errors are properly announced
  • Disabled state: Communicated to screen readers

See Also