# Custom form field renderer



## What you'll build [#what-youll-build]

A cloned form generator that produces `<DatePicker />` instead of
`<Input type="date">` whenever a request-body property has
`format: 'date'` or `format: 'date-time'`. The full flow:
clone → Snippet → dispatch → consumer component → regenerate.

## Stack [#stack]

* React + shadcn/ui (or compatible Tailwind component library)
* `react-day-picker` (or any date-picker component you prefer)
* A SKMTC project with `@skmtc/gen-shadcn-form` cloned

## Setup [#setup]

```bash
skmtc clone my-project -g @skmtc/gen-shadcn-form
```

Verify with `skmtc list my-project --json` — the form generator
should now be listed with `source: "clone"`.

## Step-by-step [#step-by-step]

### Clone gen-shadcn-form [#clone-gen-shadcn-form]

Done in setup. The source layout:

```
.skmtc/my-project/gen-shadcn-form/src/
├── ShadcnForm.ts              # main Projection
├── FormFields.ts              # iterates schema properties
├── schemaToField.ts           # dispatch: schema → field renderer
├── fields/
│   ├── BooleanInput.ts
│   ├── StringInput.ts
│   ├── ReferenceField.ts
│   └── ...
└── ...
```

Read `schemaToField.ts` and `fields/` first. You'll add to both.

### Create `DatePickerInput.ts` [#create-datepickerinputts]

Add a new Snippet under `fields/`:

```ts
// .skmtc/my-project/gen-shadcn-form/src/fields/DatePickerInput.ts
import type { GenerateContextType } from '@skmtc/core'
import { TsSnippet } from '@skmtc/lang-typescript'

type Args = {
  context: GenerateContextType
  destinationPath: string
  fieldName: string
}

export class DatePickerInput extends TsSnippet {
  #fieldName: string

  constructor(args: Args) {
    super({ context: args.context })
    this.#fieldName = args.fieldName

    this.register({
      destinationPath: args.destinationPath,
      imports: { '@/components/DatePicker': ['DatePicker'] }
    })
  }

  override toString(): string {
    return `<DatePicker {...form.register('${this.#fieldName}')} />`
  }
}
```

The Snippet's `register` call adds the `DatePicker` import into
the form file. The `toString()` produces the JSX inline.

### Dispatch in `schemaToField` [#dispatch-in-schematofield]

Add a branch in `schemaToField.ts` **before** the generic
`string` fallback:

```ts
// schemaToField.ts (excerpt)
import { DatePickerInput } from './fields/DatePickerInput.ts'

export const schemaToField = (args) => {
  const { schema, fieldName, destinationPath, context } = args

  // ... existing cases ...

  // Date and date-time formats → DatePicker
  if (!schema.isRef() && schema.type === 'string'
      && (schema.format === 'date' || schema.format === 'date-time')) {
    return new DatePickerInput({ context, destinationPath, fieldName })
  }

  // String fallback (already exists)
  if (!schema.isRef() && schema.type === 'string') {
    return new StringInput({...})
  }

  // ...
}
```

Order matters — first match wins. Put format-specific cases
above the generic ones.

### Implement the consumer-side `DatePicker` component [#implement-the-consumer-side-datepicker-component]

The Snippet produces `<DatePicker />`. That component has to exist
in the consumer app:

```tsx
// app/src/components/DatePicker.tsx (consumer side, hand-written)
import { DayPicker } from 'react-day-picker'
import 'react-day-picker/dist/style.css'

type Props = {
  value?: string
  onChange?: (date: string) => void
}

export const DatePicker = ({ value, onChange }: Props) => {
  const date = value ? new Date(value) : undefined
  return (
    <DayPicker
      mode="single"
      selected={date}
      onSelect={(d) => onChange?.(d?.toISOString() ?? '')}
    />
  )
}
```

The generator doesn't produce this — it's user code that the
generated forms import.

### Test with a representative schema [#test-with-a-representative-schema]

Find an operation in your spec with a `format: 'date'` request-body
property. If none exist, add one to a test spec:

```yaml
# test-spec.yaml
components:
  schemas:
    Event:
      type: object
      properties:
        title: { type: string }
        startDate: { type: string, format: date }
```

Regenerate:

```bash
skmtc bundle my-project
skmtc generate my-project
```

Open the generated form:

```bash
cat src/generated/forms/CreateEvent.generated.tsx
```

You should see `<DatePicker {...form.register('startDate')} />`
in place of the stock string input, plus the `import { DatePicker
} from '@/components/DatePicker'` line near the top.

## Result [#result]

Forms for any operation with a `date` or `date-time` field now
render a date picker. The dispatch is part of the generator
(it'll apply to every applicable field across every operation),
not a per-operation enrichment.

## Variations [#variations]

* **Per-operation override.** Some operations might want a
  different picker (e.g., a date-range picker for analytics
  queries). Add an enrichment field that names a specific Snippet
  class, and check it in `schemaToField` before falling back to
  format-based dispatch.
* **Format-conditional library.** Use one date-picker for `date`
  and a different one for `date-time`. Two branches in
  `schemaToField`, two Snippets.
* **Field-aware default values.** Inspect the schema's `default`
  field and pass it to the consumer-side component.

## Source [#source]

The dispatch pattern is the heart of any field-aware generator.
Same approach works for rich-text editors (custom format), file
upload (format: 'binary'), color pickers (format: 'color'), etc.

## See also [#see-also]

* [How to add a field type](/docs/authoring/how-to/add-a-field-type) — the
  targeted reference for this kind of task
* [gen-shadcn-form reference](/docs/reference/stock-generators/gen-shadcn-form)
* [Projections and Snippets concept](/docs/concepts/projections-and-snippets)
