24 KiB
Form Elements Component Guide
The Sage Utils form component documentation has been moved to the OKF bundle.
See okf/ui/form_guide.md for the current canonical form guide and API reference.
This file now serves as a backward-compatible pointer to the structured OKF documentation.
Note: The OKF
uisubbundle is the primary source for UI documentation. Legacy docs underdocs/are retained only as redirects and quick references.
Quick Start
import {
createInput,
createSelect,
createTextarea,
createCheckbox,
createSlider,
createRadioGroup,
createFormRow,
createFormGroup
} from '../components/formElements.js';
API Reference
createInput(options)
Create a styled text input element.
Parameters:
options.type(string) - Input type: 'text', 'number', 'email', 'password', etc. Default: 'text'options.placeholder(string) - Placeholder textoptions.value(string|number) - Initial valueoptions.min(string) - Minimum value (for number type)options.max(string) - Maximum value (for number type)options.step(string) - Step value (for number type)options.className(string) - Additional CSS classesoptions.onInput(Function) - Input event handleroptions.onChange(Function) - Change event handleroptions.onKeydown(Function) - Keydown event handleroptions.style(Object) - Custom style overridesoptions.disabled(boolean) - Disabled stateoptions.id(string) - Element IDoptions.name(string) - Element name attributeoptions.ariaLabel(string) - ARIA label for accessibility
Returns: HTMLInputElement
Examples:
// Basic text input
const nameInput = createInput({
placeholder: 'Enter your name',
value: 'John Doe'
});
// Number input with range
const ageInput = createInput({
type: 'number',
min: '0',
max: '120',
step: '1',
value: '25',
placeholder: 'Age'
});
// Email input with validation
const emailInput = createInput({
type: 'email',
placeholder: 'email@example.com',
onInput: (e) => {
console.log('Valid:', e.target.checkValidity());
}
});
// Password input
const passwordInput = createInput({
type: 'password',
placeholder: 'Enter password',
onKeydown: (e) => {
if (e.key === 'Enter') {
// Submit form
}
}
});
// Custom styled input
const customInput = createInput({
placeholder: 'Custom',
style: {
background: '#222',
border: '2px solid #4CAF50'
}
});
createSelect(options)
Create a styled select dropdown.
Parameters:
options.items(Array) - Array of strings or{value, text, disabled}objectsoptions.value(string|number) - Initially selected valueoptions.className(string) - Additional CSS classesoptions.onChange(Function) - Change event handleroptions.style(Object) - Custom style overridesoptions.disabled(boolean) - Disabled stateoptions.id(string) - Element IDoptions.name(string) - Element name attributeoptions.ariaLabel(string) - ARIA labeloptions.placeholder(string) - Placeholder option text
Returns: HTMLSelectElement
Examples:
// Simple string array
const modelSelect = createSelect({
items: ['GPT-4', 'Claude', 'Llama'],
value: 'GPT-4',
onChange: (e) => console.log('Selected:', e.target.value)
});
// Object array with values
const themeSelect = createSelect({
items: [
{ value: 'dark', text: 'Dark Theme' },
{ value: 'light', text: 'Light Theme' },
{ value: 'auto', text: 'Auto' }
],
value: 'dark'
});
// With placeholder
const providerSelect = createSelect({
items: ['Ollama', 'LM Studio', 'OpenAI'],
placeholder: 'Select provider...',
onChange: (e) => {
if (e.target.value) {
loadModels(e.target.value);
}
}
});
// Disabled options
const tierSelect = createSelect({
items: [
{ value: 'free', text: 'Free' },
{ value: 'pro', text: 'Pro' },
{ value: 'enterprise', text: 'Enterprise', disabled: true }
]
});
createTextarea(options)
Create a styled textarea element.
Parameters:
options.placeholder(string) - Placeholder textoptions.value(string) - Initial valueoptions.rows(number) - Number of rows. Default: 6options.cols(number) - Number of columnsoptions.monospace(boolean) - Use monospace font. Default: trueoptions.className(string) - Additional CSS classesoptions.onInput(Function) - Input event handleroptions.onChange(Function) - Change event handleroptions.onKeydown(Function) - Keydown event handleroptions.style(Object) - Custom style overridesoptions.disabled(boolean) - Disabled stateoptions.id(string) - Element IDoptions.name(string) - Element name attributeoptions.ariaLabel(string) - ARIA labeloptions.resize(string) - CSS resize property: 'vertical', 'horizontal', 'both', 'none'. Default: 'vertical'
Returns: HTMLTextAreaElement
Examples:
// Basic textarea
const promptArea = createTextarea({
placeholder: 'Enter your prompt...',
rows: 8,
onInput: (e) => {
console.log(`${e.target.value.length} characters`);
}
});
// Code editor style
const codeArea = createTextarea({
placeholder: '// Enter code here',
rows: 15,
monospace: true,
resize: 'both',
style: {
fontFamily: 'Consolas, Monaco, monospace',
fontSize: '14px'
}
});
// Non-monospace textarea
const descriptionArea = createTextarea({
placeholder: 'Description...',
rows: 4,
monospace: false,
resize: 'none'
});
// With keyboard shortcuts
const editorArea = createTextarea({
rows: 10,
onKeydown: (e) => {
if (e.ctrlKey && e.key === 'Enter') {
e.preventDefault();
submitForm();
}
if (e.key === 'Tab') {
e.preventDefault();
// Insert tab
const start = e.target.selectionStart;
const end = e.target.selectionEnd;
e.target.value = e.target.value.substring(0, start) + '\t' +
e.target.value.substring(end);
e.target.selectionStart = e.target.selectionEnd = start + 1;
}
}
});
createCheckbox(labelText, options)
Create a checkbox with label.
Parameters:
labelText(string) - Label textoptions.checked(boolean) - Initial checked state. Default: falseoptions.className(string) - Additional CSS classes for containeroptions.checkboxClass(string) - Additional CSS classes for checkboxoptions.labelClass(string) - Additional CSS classes for labeloptions.onChange(Function) - Change event handleroptions.style(Object) - Custom style overrides for containeroptions.disabled(boolean) - Disabled stateoptions.id(string) - Element ID for checkboxoptions.name(string) - Element name attributeoptions.value(string) - Checkbox value attribute
Returns: {container, checkbox, label}
Examples:
// Simple checkbox
const { container, checkbox } = createCheckbox('Enable feature', {
checked: true,
onChange: (e) => {
console.log('Enabled:', e.target.checked);
}
});
// Multiple checkboxes
const features = ['Auto-save', 'Notifications', 'Dark mode'];
const checkboxes = features.map(feature => {
return createCheckbox(feature, {
name: 'features',
value: feature.toLowerCase().replace(' ', '-'),
onChange: (e) => {
updateSettings(e.target.value, e.target.checked);
}
});
});
// Disabled checkbox
const { container: disabledBox } = createCheckbox('Premium feature', {
checked: false,
disabled: true
});
// With ID for external control
const { container, checkbox } = createCheckbox('Remember me', {
id: 'remember-checkbox',
checked: localStorage.getItem('remember') === 'true',
onChange: (e) => {
localStorage.setItem('remember', e.target.checked);
}
});
createSlider(labelText, options)
Create a slider with label and value display.
Parameters:
labelText(string) - Label textoptions.min(number) - Minimum value. Default: 0options.max(number) - Maximum value. Default: 100options.step(number) - Step increment. Default: 1options.value(number) - Initial value. Default: 50options.className(string) - Additional CSS classes for containeroptions.sliderClass(string) - Additional CSS classes for slideroptions.labelClass(string) - Additional CSS classes for labeloptions.valueClass(string) - Additional CSS classes for value displayoptions.onInput(Function) - Input event handler (fires during dragging)options.onChange(Function) - Change event handler (fires on release)options.formatValue(Function) - Function to format displayed valueoptions.style(Object) - Custom style overrides for containeroptions.disabled(boolean) - Disabled stateoptions.id(string) - Element IDoptions.name(string) - Element name attributeoptions.showValue(boolean) - Whether to show value display. Default: true
Returns: {container, slider, valueDisplay, label}
Examples:
// Temperature slider
const { container, slider } = createSlider('Temperature', {
min: 0,
max: 2,
step: 0.1,
value: 0.7,
formatValue: (v) => parseFloat(v).toFixed(1),
onInput: (e) => {
updateTemperature(e.target.value);
}
});
// Percentage slider
const { container, slider } = createSlider('Opacity', {
min: 0,
max: 100,
step: 5,
value: 80,
formatValue: (v) => `${v}%`,
onChange: (e) => {
element.style.opacity = e.target.value / 100;
}
});
// Integer slider
const { container, slider } = createSlider('Count', {
min: 1,
max: 10,
step: 1,
value: 5,
formatValue: (v) => `${v} items`
});
// Without value display
const { container, slider } = createSlider('Volume', {
min: 0,
max: 100,
value: 50,
showValue: false,
onInput: (e) => {
setVolume(e.target.value);
}
});
// Custom styling
const { container, slider } = createSlider('Brightness', {
min: 0,
max: 100,
value: 75,
style: {
marginBottom: '20px'
},
sliderClass: 'custom-slider'
});
createRadioGroup(name, items, options)
Create a radio button group.
Parameters:
name(string) - Group name (same name for all radios in group)items(Array) - Array of strings or{value, label, checked, disabled}objectsoptions.selectedValue(string) - Initially selected valueoptions.className(string) - Additional CSS classes for containeroptions.radioClass(string) - Additional CSS classes for radio buttonsoptions.labelClass(string) - Additional CSS classes for labelsoptions.onChange(Function) - Change event handleroptions.style(Object) - Custom style overridesoptions.disabled(boolean) - Disabled state for all radiosoptions.layout(string) - Layout direction: 'vertical' or 'horizontal'. Default: 'vertical'
Returns: {container, radios} where radios is an array of {radio, label, container} objects
Examples:
// Simple radio group
const { container, radios } = createRadioGroup('theme',
['Dark', 'Light', 'Auto'],
{
selectedValue: 'Dark',
onChange: (e) => {
setTheme(e.target.value);
}
}
);
// Object-based items
const { container } = createRadioGroup('plan', [
{ value: 'free', label: 'Free Plan' },
{ value: 'pro', label: 'Pro Plan ($10/mo)' },
{ value: 'enterprise', label: 'Enterprise Plan' }
], {
selectedValue: 'free'
});
// Horizontal layout
const { container } = createRadioGroup('size',
['Small', 'Medium', 'Large'],
{
selectedValue: 'Medium',
layout: 'horizontal'
}
);
// With disabled option
const { container } = createRadioGroup('tier', [
{ value: 'basic', label: 'Basic' },
{ value: 'premium', label: 'Premium', disabled: true },
{ value: 'unlimited', label: 'Unlimited' }
], {
selectedValue: 'basic',
onChange: (e) => {
console.log('Selected:', e.target.value);
}
});
// Access individual radios
const { container, radios } = createRadioGroup('option', ['A', 'B', 'C']);
radios.forEach((item, index) => {
item.radio.addEventListener('focus', () => {
console.log(`Option ${index} focused`);
});
});
createFormRow(labelText, element, options)
Create a form row with label and element.
Parameters:
labelText(string) - Label textelement(HTMLElement) - Form element to includeoptions.className(string) - Additional CSS classes for containeroptions.labelClass(string) - Additional CSS classes for labeloptions.style(Object) - Custom style overridesoptions.id(string) - ID for the element (will setforattribute on label)options.required(boolean) - Whether field is required (adds asterisk)options.helpText(string) - Optional help text below the elementoptions.layout(string) - Layout: 'vertical' or 'horizontal'. Default: 'vertical'
Returns: HTMLElement (container with label and element)
Examples:
// Basic form row
const nameInput = createInput({ placeholder: 'Enter name' });
const nameRow = createFormRow('Name', nameInput);
// Required field
const emailInput = createInput({ type: 'email' });
const emailRow = createFormRow('Email', emailInput, {
required: true,
helpText: 'We will never share your email'
});
// Horizontal layout
const themeSelect = createSelect({ items: ['Dark', 'Light'] });
const themeRow = createFormRow('Theme', themeSelect, {
layout: 'horizontal'
});
// With ID for linking
const passwordInput = createInput({ type: 'password', id: 'pwd' });
const passwordRow = createFormRow('Password', passwordInput, {
id: 'pwd',
required: true,
helpText: 'At least 8 characters'
});
createFormGroup(elements, options)
Create a form group container with optional title.
Parameters:
elements(Array) - Array of form elements or rowsoptions.className(string) - Additional CSS classesoptions.title(string) - Optional group titleoptions.style(Object) - Custom style overrides
Returns: HTMLElement (form group container)
Examples:
// Basic form group
const group = createFormGroup([
createFormRow('First Name', createInput()),
createFormRow('Last Name', createInput()),
createFormRow('Email', createInput({ type: 'email' }))
]);
// With title
const loginGroup = createFormGroup([
createFormRow('Username', createInput()),
createFormRow('Password', createInput({ type: 'password' }))
], {
title: 'Login Credentials'
});
// Multiple groups
const personalInfo = createFormGroup([
createFormRow('Name', createInput()),
createFormRow('Age', createInput({ type: 'number' }))
], { title: 'Personal Information' });
const contactInfo = createFormGroup([
createFormRow('Email', createInput({ type: 'email' })),
createFormRow('Phone', createInput({ type: 'tel' }))
], { title: 'Contact Information' });
const form = document.createElement('form');
form.appendChild(personalInfo);
form.appendChild(contactInfo);
Complete Form Example
import {
createInput,
createSelect,
createTextarea,
createCheckbox,
createSlider,
createRadioGroup,
createFormRow,
createFormGroup
} from '../components/formElements.js';
// Create form elements
const nameInput = createInput({
placeholder: 'Enter your name',
id: 'name'
});
const emailInput = createInput({
type: 'email',
placeholder: 'email@example.com',
id: 'email'
});
const providerSelect = createSelect({
items: ['Ollama', 'LM Studio', 'OpenAI'],
placeholder: 'Select provider...',
id: 'provider'
});
const promptArea = createTextarea({
placeholder: 'Enter your prompt...',
rows: 8,
id: 'prompt'
});
const { container: tempSlider } = createSlider('Temperature', {
min: 0,
max: 2,
step: 0.1,
value: 0.7,
formatValue: (v) => parseFloat(v).toFixed(1)
});
const { container: streamCheckbox } = createCheckbox('Enable streaming', {
checked: true,
id: 'streaming'
});
const { container: modelRadios } = createRadioGroup('modelType', [
{ value: 'small', label: 'Small (Fast)' },
{ value: 'medium', label: 'Medium (Balanced)' },
{ value: 'large', label: 'Large (Accurate)' }
], {
selectedValue: 'medium',
layout: 'horizontal'
});
// Create form groups
const basicInfo = createFormGroup([
createFormRow('Name', nameInput, { required: true, id: 'name' }),
createFormRow('Email', emailInput, { required: true, id: 'email' })
], {
title: 'Basic Information'
});
const llmSettings = createFormGroup([
createFormRow('Provider', providerSelect, { id: 'provider' }),
createFormRow('Model Type', modelRadios),
tempSlider,
streamCheckbox
], {
title: 'LLM Settings'
});
const promptSection = createFormGroup([
createFormRow('Prompt', promptArea, { id: 'prompt' })
], {
title: 'Prompt'
});
// Build complete form
const form = document.createElement('form');
form.appendChild(basicInfo);
form.appendChild(llmSettings);
form.appendChild(promptSection);
// Add submit button
const submitBtn = document.createElement('button');
submitBtn.textContent = 'Submit';
submitBtn.type = 'submit';
form.appendChild(submitBtn);
// Handle form submission
form.addEventListener('submit', (e) => {
e.preventDefault();
const formData = {
name: nameInput.value,
email: emailInput.value,
provider: providerSelect.value,
prompt: promptArea.value
};
console.log('Form data:', formData);
});
Migration Guide
Before (Manual Creation)
// Old way - manual creation
const slider = document.createElement('input');
slider.type = 'range';
slider.min = '0';
slider.max = '2';
slider.step = '0.1';
slider.value = '0.7';
slider.style.cssText = `width: 100%; margin: 5px 0;`;
const valueDisplay = document.createElement('span');
valueDisplay.textContent = '0.7';
valueDisplay.style.cssText = `color: #fff; font-size: 12px;`;
slider.addEventListener('input', () => {
valueDisplay.textContent = slider.value;
});
const container = document.createElement('div');
container.appendChild(slider);
container.appendChild(valueDisplay);
After (Using Component)
// New way - using component
import { createSlider } from '../components/formElements.js';
const { container, slider, valueDisplay } = createSlider('Temperature', {
min: 0,
max: 2,
step: 0.1,
value: 0.7
});
Benefits:
- 15+ lines → 5 lines (70% reduction)
- Consistent styling
- Automatic value display
- Better accessibility
- Easier to maintain
Best Practices
1. Use Form Rows for Better Structure
// Good - structured with form rows
const emailInput = createInput({ type: 'email', id: 'email' });
const emailRow = createFormRow('Email', emailInput, {
required: true,
helpText: 'Your email address'
});
// Less ideal - bare input
const emailInput = createInput({ type: 'email' });
2. Group Related Fields
// Good - grouped related fields
const accountInfo = createFormGroup([
createFormRow('Username', createInput()),
createFormRow('Password', createInput({ type: 'password' }))
], { title: 'Account' });
const personalInfo = createFormGroup([
createFormRow('Name', createInput()),
createFormRow('Age', createInput({ type: 'number' }))
], { title: 'Personal' });
3. Use Appropriate Input Types
// Good - semantic types
const emailInput = createInput({ type: 'email' });
const urlInput = createInput({ type: 'url' });
const telInput = createInput({ type: 'tel' });
const dateInput = createInput({ type: 'date' });
// Less ideal - all text
const emailInput = createInput({ type: 'text' });
4. Provide Helpful Labels and Help Text
// Good - clear labels and help text
const apiKeyRow = createFormRow('API Key', createInput({ type: 'password' }), {
required: true,
helpText: 'Your OpenAI API key (starts with sk-)'
});
// Less ideal - minimal context
const apiKeyInput = createInput({ type: 'password' });
5. Use Format Functions for Sliders
// Good - formatted display
const { container } = createSlider('Opacity', {
min: 0,
max: 100,
value: 80,
formatValue: (v) => `${v}%`
});
// Good - precision control
const { container } = createSlider('Temperature', {
min: 0,
max: 2,
step: 0.1,
value: 0.7,
formatValue: (v) => parseFloat(v).toFixed(1)
});
Styling Customization
All components accept a style parameter for customization:
// Custom input styling
const input = createInput({
placeholder: 'Custom',
style: {
background: '#222',
border: '2px solid #4CAF50',
borderRadius: '8px',
fontSize: '14px',
padding: '12px'
}
});
// Custom slider styling
const { container, slider } = createSlider('Custom Slider', {
min: 0,
max: 100,
value: 50,
style: {
marginBottom: '24px',
padding: '12px',
background: '#1a1a1a',
borderRadius: '8px'
}
});
Accessibility Features
All components include accessibility features:
- ARIA labels - Use
ariaLabeloption for screen readers - Label associations - Labels linked to inputs via
for/id - Keyboard navigation - Full keyboard support
- Required indicators - Visual indicators for required fields
- Help text - Descriptive help text for complex fields
// Accessible form example
const emailInput = createInput({
type: 'email',
id: 'user-email',
ariaLabel: 'User email address'
});
const emailRow = createFormRow('Email', emailInput, {
id: 'user-email',
required: true,
helpText: 'We will send a confirmation to this address'
});
Performance Tips
- Reuse components - Create once, append multiple times if needed
- Batch updates - Group multiple form changes together
- Event delegation - Use single handler for multiple similar elements
- Lazy loading - Create forms only when needed
// Good - event delegation
const { container, radios } = createRadioGroup('option', ['A', 'B', 'C']);
container.addEventListener('change', (e) => {
if (e.target.type === 'radio') {
handleChange(e.target.value);
}
});
// Less efficient - individual handlers
radios.forEach(({ radio }) => {
radio.addEventListener('change', (e) => handleChange(e.target.value));
});
Common Patterns
Dynamic Option Lists
// Load options dynamically
async function createModelSelect() {
const models = await fetchModels();
return createSelect({
items: models.map(m => ({ value: m.id, text: m.name })),
placeholder: 'Select a model...',
onChange: (e) => loadModel(e.target.value)
});
}
Conditional Fields
// Show/hide based on selection
const typeSelect = createSelect({
items: ['Simple', 'Advanced'],
onChange: (e) => {
advancedOptions.style.display =
e.target.value === 'Advanced' ? 'block' : 'none';
}
});
const advancedOptions = createFormGroup([
createFormRow('Option 1', createInput()),
createFormRow('Option 2', createInput())
]);
advancedOptions.style.display = 'none';
Form Validation
// Validate on input
const emailInput = createInput({
type: 'email',
onInput: (e) => {
const isValid = e.target.checkValidity();
e.target.style.borderColor = isValid ? '#555' : '#f44336';
}
});
// Validate on submit
form.addEventListener('submit', (e) => {
e.preventDefault();
const inputs = form.querySelectorAll('input[required]');
const allValid = Array.from(inputs).every(input => input.checkValidity());
if (allValid) {
submitForm();
} else {
showToast('Please fill in all required fields', NOTIFICATION_TYPES.ERROR);
}
});
Troubleshooting
Issue: Styles not applying
Solution: Check that custom styles don't conflict with default styles. Use !important if necessary or override the entire style object.
const input = createInput({
style: {
background: '#000 !important'
}
});
Issue: Event handlers not firing
Solution: Ensure you're using the correct event type (onInput vs onChange). onInput fires during typing, onChange fires on blur.
// Use onInput for live updates
const input = createInput({
onInput: (e) => console.log('Typing:', e.target.value)
});
// Use onChange for final value
const input = createInput({
onChange: (e) => console.log('Final:', e.target.value)
});
Issue: Form elements not aligned
Solution: Use createFormRow with consistent layout option, or use createFormGroup for better structure.
Version History
- 1.0 (October 12, 2025) - Initial release
- All core form element creators
- Form row and group utilities
- Comprehensive documentation