Interactive demo of input group field
/**
* Input Group Field Demo - Interactive examples of input group fields
*/
import { Form, FormBuilder } from '@saastro/forms';
import { FormProvider } from '@/components/FormProvider';
import { TooltipProvider } from '@/components/ui/tooltip';
const config = FormBuilder.create('input-group-demo')
.layout('manual')
.columns(12)
.addField('price', (f) =>
f
.type('input-group')
.label('Price')
.placeholder('0.00')
.helperText('Enter the price in USD')
.required('Price is required')
.columns({ default: 12, md: 6 }),
)
.addField('weight', (f) =>
f
.type('input-group')
.label('Weight')
.placeholder('0')
.helperText('Enter weight in kilograms')
.required()
.columns({ default: 12, md: 6 }),
)
.addField('website', (f) =>
f
.type('input-group')
.label('Website')
.placeholder('yoursite')
.helperText('Enter your website domain')
.optional()
.columns({ default: 12 }),
)
.addStep('main', ['price', 'weight', 'website'])
.build();
export default function InputGroupDemo() {
const handleSubmit = (data: Record<string, unknown>) => {
console.log('Form submitted:', data);
alert('Form submitted! Check console for data.');
};
return (
<TooltipProvider>
<FormProvider>
<Form config={config} onSubmit={handleSubmit} className="space-y-4" />
</FormProvider>
</TooltipProvider>
);
} Overview
The input group field extends the standard text input with prefix and suffix text addons rendered on either side of the input. Use it for formatted inputs like currency amounts, URLs, or measurements.
Both addons are plain strings set directly on the field config — .prefix(text) / .suffix(text) in the fluent API, or the prefix / suffix keys in JSON. The input itself always renders as a text input, so the field value is a string.
Usage
With Prefix
import { FormBuilder } from '@saastro/forms';
const config = FormBuilder.create('form')
.addField('price', (f) =>
f
.type('input-group')
.label('Price')
.placeholder('0.00')
.prefix('$')
.required('Price is required'),
)
.addStep('main', ['price'])
.build();
With Suffix
.addField('weight', (f) =>
f.type('input-group')
.label('Weight')
.placeholder('0')
.suffix('kg')
.required()
)
With Both
.addField('website', (f) =>
f.type('input-group')
.label('Website')
.placeholder('example')
.prefix('https://')
.suffix('.com')
.optional()
)
JSON Configuration
{
"type": "input-group",
"label": "Price",
"placeholder": "0.00",
"prefix": "$",
"suffix": "USD",
"schema": { "required": true }
}
The currency Alias
currency is an alias of input-group: both types accept the same props and render through the same component. It adds no currency formatting, masking, or locale logic — set the symbol yourself via prefix or suffix:
.addField('amount', (f) =>
f.type('currency')
.label('Amount')
.placeholder('0.00')
.prefix('€')
.required()
)
Props
| Prop | Type | Default | Description |
|---|---|---|---|
type | 'input-group' | 'currency' | - | Field type (required) |
label | string | - | Label text |
placeholder | string | - | Placeholder text |
helperText | string | - | Help text below field |
prefix | string | - | Text addon rendered before the input |
suffix | string | - | Text addon rendered after the input |
schema | z.ZodType | ValidationRules | - | Validation schema (required; fluent validation methods like .required() set it for you) |
size | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Input size variant |
columns | Partial<Record<Breakpoint, number>> | - | Grid columns by breakpoint |
disabled | boolean | function | ConditionGroup | false | Disable the field |
hidden | boolean | function | ConditionGroup | Responsive | false | Hide the field |
Common Patterns
Currency Input
.addField('amount', (f) =>
f.type('input-group')
.label('Amount')
.placeholder('0.00')
.prefix('$')
.required('Amount is required')
)
URL Input
.addField('domain', (f) =>
f.type('input-group')
.label('Domain')
.placeholder('yoursite')
.prefix('https://')
.suffix('.com')
.required()
)
Percentage Input
.addField('discount', (f) =>
f.type('input-group')
.label('Discount')
.placeholder('0')
.suffix('%')
.optional()
)
Validation
The field value is a string, so string-based rules apply: required, minLength, maxLength, pattern, and format. For numeric input, validate the shape with a regex:
.addField('price', (f) =>
f.type('input-group')
.label('Price')
.prefix('$')
.required('Price is required')
.regex('^\\d+(\\.\\d{1,2})?$', 'Enter a valid amount')
)
Styling
Custom Classes
.addField('price', (f) =>
f.type('input-group')
.label('Price')
.placeholder('0.00')
.prefix('$')
.required()
.classNames({
wrapper: 'bg-muted/30 p-4 rounded-lg',
input: 'border-2 border-primary/20 font-mono',
label: 'text-lg font-semibold',
error: 'text-destructive text-sm',
})
)
Size Variants
.addField('amount', (f) =>
f.type('input-group')
.label('Amount')
.prefix('$')
.size('lg')
.optional()
)
Responsive Layout
.addField('price', (f) =>
f.type('input-group')
.label('Price')
.prefix('$')
.columns({ default: 12, md: 4 })
.optional()
)
JSON Styling
{
"type": "input-group",
"label": "Price",
"prefix": "$",
"wrapper_className": "bg-muted/30 p-4 rounded-lg",
"input_className": "border-2 border-primary/20 font-mono",
"label_className": "text-lg font-semibold",
"size": "lg",
"columns": { "default": 12, "md": 4 },
"schema": { "required": true }
}
Related Fields
Accessibility
- Prefix and suffix render as plain text addons next to the input; they are not associated with the input via ARIA, so keep essential context in the label or helper text
- Main input retains full accessibility
- Proper labeling maintained