Files
arcum42-ComfyUI_SageUtils/docs/FORM_ELEMENTS_GUIDE.md
T
2026-07-01 13:51:56 -07:00

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 ui subbundle is the primary source for UI documentation. Legacy docs under docs/ 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 text
  • options.value (string|number) - Initial value
  • options.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 classes
  • options.onInput (Function) - Input event handler
  • options.onChange (Function) - Change event handler
  • options.onKeydown (Function) - Keydown event handler
  • options.style (Object) - Custom style overrides
  • options.disabled (boolean) - Disabled state
  • options.id (string) - Element ID
  • options.name (string) - Element name attribute
  • options.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} objects
  • options.value (string|number) - Initially selected value
  • options.className (string) - Additional CSS classes
  • options.onChange (Function) - Change event handler
  • options.style (Object) - Custom style overrides
  • options.disabled (boolean) - Disabled state
  • options.id (string) - Element ID
  • options.name (string) - Element name attribute
  • options.ariaLabel (string) - ARIA label
  • options.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 text
  • options.value (string) - Initial value
  • options.rows (number) - Number of rows. Default: 6
  • options.cols (number) - Number of columns
  • options.monospace (boolean) - Use monospace font. Default: true
  • options.className (string) - Additional CSS classes
  • options.onInput (Function) - Input event handler
  • options.onChange (Function) - Change event handler
  • options.onKeydown (Function) - Keydown event handler
  • options.style (Object) - Custom style overrides
  • options.disabled (boolean) - Disabled state
  • options.id (string) - Element ID
  • options.name (string) - Element name attribute
  • options.ariaLabel (string) - ARIA label
  • options.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 text
  • options.checked (boolean) - Initial checked state. Default: false
  • options.className (string) - Additional CSS classes for container
  • options.checkboxClass (string) - Additional CSS classes for checkbox
  • options.labelClass (string) - Additional CSS classes for label
  • options.onChange (Function) - Change event handler
  • options.style (Object) - Custom style overrides for container
  • options.disabled (boolean) - Disabled state
  • options.id (string) - Element ID for checkbox
  • options.name (string) - Element name attribute
  • options.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 text
  • options.min (number) - Minimum value. Default: 0
  • options.max (number) - Maximum value. Default: 100
  • options.step (number) - Step increment. Default: 1
  • options.value (number) - Initial value. Default: 50
  • options.className (string) - Additional CSS classes for container
  • options.sliderClass (string) - Additional CSS classes for slider
  • options.labelClass (string) - Additional CSS classes for label
  • options.valueClass (string) - Additional CSS classes for value display
  • options.onInput (Function) - Input event handler (fires during dragging)
  • options.onChange (Function) - Change event handler (fires on release)
  • options.formatValue (Function) - Function to format displayed value
  • options.style (Object) - Custom style overrides for container
  • options.disabled (boolean) - Disabled state
  • options.id (string) - Element ID
  • options.name (string) - Element name attribute
  • options.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} objects
  • options.selectedValue (string) - Initially selected value
  • options.className (string) - Additional CSS classes for container
  • options.radioClass (string) - Additional CSS classes for radio buttons
  • options.labelClass (string) - Additional CSS classes for labels
  • options.onChange (Function) - Change event handler
  • options.style (Object) - Custom style overrides
  • options.disabled (boolean) - Disabled state for all radios
  • options.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 text
  • element (HTMLElement) - Form element to include
  • options.className (string) - Additional CSS classes for container
  • options.labelClass (string) - Additional CSS classes for label
  • options.style (Object) - Custom style overrides
  • options.id (string) - ID for the element (will set for attribute on label)
  • options.required (boolean) - Whether field is required (adds asterisk)
  • options.helpText (string) - Optional help text below the element
  • options.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 rows
  • options.className (string) - Additional CSS classes
  • options.title (string) - Optional group title
  • options.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' });
// 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 ariaLabel option 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

  1. Reuse components - Create once, append multiple times if needed
  2. Batch updates - Group multiple form changes together
  3. Event delegation - Use single handler for multiple similar elements
  4. 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

See Also