How to add a field type
Add a new field renderer to a cloned form generator (e.g., `gen-shadcn-form`).
When to use this
A form generator's stock dispatch doesn't cover a schema shape
you need to render. Common cases: a custom format value
mapping to a specific React component (date picker, rich text,
file upload, etc.).
Prerequisites
- The form generator cloned (tutorial: cloning).
- A consumer-side React component for the field type (existing or about to be written).
Steps
Create the Snippet class in src/fields/
Form generators have a src/fields/ subdirectory with one
Snippet per field type:
// src/fields/DatePickerInput.ts — the shape every field Snippet follows
export class DatePickerInput extends TsSnippet {
#fieldName: string
constructor(args: Args) {
super({ context: args.context })
this.#fieldName = args.fieldName
// the Snippet registers its OWN import — presence of the field
// implies presence of the import
this.register({
destinationPath: args.destinationPath,
imports: { '@/components/DatePicker': ['DatePicker'] }
})
}
override toString(): string {
return `<DatePicker {...form.register('${this.#fieldName}')} />`
}
}The complete class — imports, Args type, and the dispatch wiring —
is in the custom form field renderer recipe,
which walks this exact example end to end.
The Snippet's toString() produces just the JSX for one field.
Register the field's import
The Snippet's constructor (or a register call from its parent)
needs to register the import for the consumer-side component:
constructor(...) {
super(args)
this.register({
destinationPath: args.destinationPath,
imports: { '@/components/DatePicker': ['DatePicker'] }
})
}Imports go to the file's destinationPath. See
the Import reference.
Add a dispatch branch in schemaToField
src/schemaToField.ts is where the form generator decides which
field renderer to use per schema property:
// schemaToField.ts (simplified)
import { DatePickerInput } from './fields/DatePickerInput.ts'
export const schemaToField = (args) => {
const { schema, fieldName, format } = args
switch (true) {
case format === 'date': return new DatePickerInput(...)
case schema.type === 'string' && format === 'email': return new EmailInput(...)
case schema.type === 'string': return new StringInput(...)
case schema.type === 'boolean': return new BooleanInput(...)
// ... your new branch
}
}Add a case for your new field type before the more-general
fallbacks. Order matters — first match wins.
Implement the consumer-side field component
The Snippet produces a JSX reference; the actual <DatePicker />
component needs to exist on the consumer side. Either add it to
your component library, or import an existing one (e.g.,
react-day-picker).
The generator doesn't produce this component — it's user code.
Rebundle and regenerate
skmtc bundle my-project
skmtc generate my-projectVerification
Generate against a schema with a format: 'date' field. Inspect
the generated form file — the new field renderer should appear
in place of the stock string input:
// src/generated/forms/CreateEvent.generated.tsx
<DatePicker {...form.register('startDate')} />In your app, the form should now render the date picker component.
Troubleshooting
- Stock renderer still used — Your case statement is
unreachable. Either a more-general case matches first, or your
match condition is wrong. Re-check the order in
schemaToField. - Component import missing in output — The
register({ imports })call didn't run. Confirm the Snippet's constructor actually fires (it does ifschemaToFieldreaches thenew DatePickerInput(...)branch). - Generated form fails to compile — Consumer-side
DatePickercomponent doesn't exist or has a different prop shape. Either build it or adjust the Snippet's output.
Related
- Recipe: Custom form field renderer — end-to-end example with concrete code
- gen-shadcn-form reference — the source layout this how-to operates against
- Projections and Snippets concept