Skip to content

Form Components Overview

A comprehensive collection of form components built with Pulse Framework for creating reactive, validated, and accessible forms.

Available Components

Text Input Components

Input

Text input fields with labels, hints, validation, and icons.

Code Éditable
Résultat

Textarea

Multi-line text input with auto-resize support.

Code Éditable
Résultat

Selection Components

Select

Dropdown selection with options and groups.

Code Éditable
Résultat

RadioGroup

Grouped radio buttons for single selection.

Code Éditable
Résultat

Boolean Input Components

Checkbox

Checkbox input with label and description.

Code Éditable
Résultat

Toggle

Switch component for on/off states.

Code Éditable
Résultat

Specialized Input Components

FileInput

File upload input with validation.

Code Éditable
Résultat

RangeSlider

Numeric range slider.

Code Éditable
Résultat

ColorPicker

Color selection input.

Code Éditable
Résultat

Layout Components

FormGroup

Container for organizing form fields.

Code Éditable
Résultat

Complete Form Example

Here's a comprehensive example using multiple form components:

tsx
import { 
  Input, 
  Select, 
  Checkbox, 
  Toggle,
  Textarea,
  RadioGroup,
  Button,
  FormGroup,
  Pulse 
} from '@odyssee/components';

const RegistrationForm = () => {
  // Form state
  const firstName = Pulse.signal('');
  const lastName = Pulse.signal('');
  const email = Pulse.signal('');
  const password = Pulse.signal('');
  const country = Pulse.signal('');
  const plan = Pulse.signal('free');
  const bio = Pulse.signal('');
  const acceptTerms = Pulse.signal(false);
  const newsletter = Pulse.signal(true);
  const isLoading = Pulse.signal(false);

  // Validation
  const isValidEmail = Pulse.computed(() => {
    return email().includes('@') && email().includes('.');
  });

  const isValidPassword = Pulse.computed(() => {
    return password().length >= 8;
  });

  const isFormValid = Pulse.computed(() => {
    return firstName().length > 0 &&
           lastName().length > 0 &&
           isValidEmail() &&
           isValidPassword() &&
           country().length > 0 &&
           acceptTerms();
  });

  // Submit handler
  const handleSubmit = async (e: Event) => {
    e.preventDefault();
    
    if (!isFormValid()) return;
    
    isLoading(true);
    
    try {
      const formData = {
        firstName: firstName(),
        lastName: lastName(),
        email: email(),
        password: password(),
        country: country(),
        plan: plan(),
        bio: bio(),
        newsletter: newsletter()
      };
      
      console.log('Submitting:', formData);
      
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 2000));
      
      alert('Registration successful!');
    } catch (error) {
      console.error('Registration failed:', error);
    } finally {
      isLoading(false);
    }
  };

  return (
    <form onsubmit={handleSubmit} class="max-w-2xl mx-auto space-y-6">
      {/* Personal Information */}
      <FormGroup 
        label="Personal Information" 
        description="Tell us about yourself"
        bordered={true}
      >
        <div class="grid grid-cols-2 gap-4">
          <Input
            label="First Name"
            value={firstName}
            onChange={(val) => firstName(val)}
            placeholder="John"
            required={true}
          />
          
          <Input
            label="Last Name"
            value={lastName}
            onChange={(val) => lastName(val)}
            placeholder="Doe"
            required={true}
          />
        </div>
        
        <Input
          type="email"
          label="Email Address"
          value={email}
          onChange={(val) => email(val)}
          placeholder="john@example.com"
          icon="✉️"
          error={Pulse.computed(() => {
            const val = email();
            if (val.length === 0) return '';
            return isValidEmail() ? '' : 'Invalid email format';
          })}
          required={true}
        />
        
        <Input
          type="password"
          label="Password"
          value={password}
          onChange={(val) => password(val)}
          placeholder="••••••••"
          icon="🔒"
          hint="Must be at least 8 characters"
          error={Pulse.computed(() => {
            const val = password();
            if (val.length === 0) return '';
            return isValidPassword() ? '' : 'Password too short';
          })}
          required={true}
        />
        
        <Select
          label="Country"
          value={country}
          onChange={(val) => country(val)}
          options={[
            { value: '', label: 'Select a country' },
            { value: 'us', label: 'United States' },
            { value: 'uk', label: 'United Kingdom' },
            { value: 'fr', label: 'France' },
            { value: 'de', label: 'Germany' },
            { value: 'ca', label: 'Canada' }
          ]}
          required={true}
        />
      </FormGroup>

      {/* Plan Selection */}
      <FormGroup label="Choose Your Plan">
        <RadioGroup
          name="plan"
          value={plan}
          onChange={(val) => plan(val)}
          options={[
            { 
              value: 'free', 
              label: 'Free', 
              description: 'Basic features for personal use' 
            },
            { 
              value: 'pro', 
              label: 'Pro - $29/month', 
              description: 'Advanced features for professionals' 
            },
            { 
              value: 'enterprise', 
              label: 'Enterprise', 
              description: 'Custom pricing and support' 
            }
          ]}
        />
      </FormGroup>

      {/* Bio */}
      <FormGroup label="About You">
        <Textarea
          label="Bio"
          value={bio}
          onChange={(val) => bio(val)}
          placeholder="Tell us about yourself..."
          maxLength={500}
          showCount={true}
          autoResize={true}
        />
      </FormGroup>

      {/* Preferences */}
      <FormGroup label="Preferences">
        <Checkbox
          label="I accept the Terms and Conditions"
          checked={acceptTerms}
          onChange={(val) => acceptTerms(val)}
          required={true}
        />
        
        <Toggle
          label="Subscribe to newsletter"
          description="Get updates about new features and tips"
          checked={newsletter}
          onChange={(val) => newsletter(val)}
        />
      </FormGroup>

      {/* Submit Button */}
      <Button
        type="submit"
        color="primary"
        size="lg"
        fullWidth={true}
        loading={isLoading}
        disabled={Pulse.computed(() => !isFormValid())}
      >
        Create Account
      </Button>
    </form>
  ) as Pulse.JSX.Element;
};

Form Validation

Real-time Validation

Validate fields as users type using Pulse computed values:

tsx
const email = Pulse.signal('');

const emailError = Pulse.computed(() => {
  const value = email();
  if (value.length === 0) return '';
  if (!value.includes('@')) return 'Email must contain @';
  if (!value.includes('.')) return 'Email must contain domain';
  return '';
});

<Input
  label="Email"
  value={email}
  onChange={(val) => email(val)}
  error={emailError}
/>

Form-level Validation

Validate entire form before submission:

tsx
const isFormValid = Pulse.computed(() => {
  return email().includes('@') &&
         password().length >= 8 &&
         acceptTerms();
});

<Button
  type="submit"
  disabled={Pulse.computed(() => !isFormValid())}
>
  Submit
</Button>

Custom Validators

Create reusable validation functions:

tsx
const validators = {
  email: (value: string) => {
    return value.includes('@') && value.includes('.');
  },
  
  password: (value: string) => {
    return value.length >= 8 && /[A-Z]/.test(value);
  },
  
  phone: (value: string) => {
    return /^\+?[\d\s-()]+$/.test(value);
  }
};

const email = Pulse.signal('');
const isValidEmail = Pulse.computed(() => validators.email(email()));

Accessibility Best Practices

Labels

Always provide labels for form fields:

tsx
// ✅ Good
<Input label="Email Address" type="email" />

// ❌ Bad
<Input placeholder="Email" />

Required Fields

Mark required fields explicitly:

tsx
<Input
  label="Email"
  required={true}
  aria-required="true"
/>

Error Messages

Associate error messages with inputs:

tsx
<Input
  label="Email"
  error="Invalid email format"
  aria-invalid="true"
/>

Keyboard Navigation

Ensure proper tab order:

tsx
<form>
  <Input label="First Name" tabindex="1" />
  <Input label="Last Name" tabindex="2" />
  <Button type="submit" tabindex="3">Submit</Button>
</form>

Form Patterns

Pattern 1: Multi-step Form

tsx
const step = Pulse.signal(1);

const MultiStepForm = () => {
  return (
    <div>
      {step() === 1 && <PersonalInfoStep />}
      {step() === 2 && <AddressStep />}
      {step() === 3 && <ReviewStep />}
      
      <div class="flex justify-between mt-6">
        {step() > 1 && (
          <Button onClick={() => step(step() - 1)}>
            Previous
          </Button>
        )}
        {step() < 3 && (
          <Button onClick={() => step(step() + 1)}>
            Next
          </Button>
        )}
        {step() === 3 && (
          <Button type="submit">Submit</Button>
        )}
      </div>
    </div>
  );
};

Pattern 2: Dynamic Fields

tsx
const emails = Pulse.signal(['']);

const addEmail = () => {
  emails([...emails(), '']);
};

const removeEmail = (index: number) => {
  emails(emails().filter((_, i) => i !== index));
};

const DynamicFields = () => {
  return (
    <div class="space-y-2">
      {emails().map((email, index) => (
        <div class="flex gap-2">
          <Input
            value={email}
            onChange={(val) => {
              const newEmails = [...emails()];
              newEmails[index] = val;
              emails(newEmails);
            }}
          />
          <Button
            variant="ghost"
            color="danger"
            onClick={() => removeEmail(index)}
          >
            Remove
          </Button>
        </div>
      ))}
      <Button variant="outline" onClick={addEmail}>
        Add Email
      </Button>
    </div>
  );
};

Pattern 3: Conditional Fields

tsx
const accountType = Pulse.signal('personal');

<RadioGroup
  label="Account Type"
  value={accountType}
  options={[
    { value: 'personal', label: 'Personal' },
    { value: 'business', label: 'Business' }
  ]}
  onChange={(val) => accountType(val)}
/>

{accountType() === 'business' && (
  <Input
    label="Company Name"
    placeholder="Acme Inc."
    required={true}
  />
)}

Styling Forms

Layout with Tailwind

tsx
<form class="space-y-6">
  {/* Two columns on desktop */}
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
    <Input label="First Name" />
    <Input label="Last Name" />
  </div>
  
  {/* Full width */}
  <Input label="Email" />
  
  {/* Inline fields */}
  <div class="flex gap-4">
    <Input label="City" className="flex-1" />
    <Input label="ZIP" className="w-32" />
  </div>
</form>

Responsive Forms

tsx
<form class="max-w-2xl mx-auto px-4">
  {/* Stack on mobile, side-by-side on desktop */}
  <div class="flex flex-col md:flex-row gap-4">
    <Input label="First Name" className="flex-1" />
    <Input label="Last Name" className="flex-1" />
  </div>
</form>

Performance Tips

  1. Debounce expensive validation

    tsx
    let debounceTimer: number;
    const debouncedValidation = (value: string) => {
      clearTimeout(debounceTimer);
      debounceTimer = setTimeout(() => {
        // Perform validation
      }, 300);
    };
  2. Use computed values for derived state

    tsx
    const fullName = Pulse.computed(() => 
      `${firstName()} ${lastName()}`
    );
  3. Avoid unnecessary re-renders

    tsx
    // Good: Only update when needed
    const updateEmail = (val: string) => {
      if (val !== email()) {
        email(val);
      }
    };

Common Mistakes

❌ Don't: Forget onChange handlers

tsx
// This won't work - value is not reactive
<Input value={email} />

✅ Do: Provide onChange handlers

tsx
<Input 
  value={email} 
  onChange={(val) => email(val)} 
/>

❌ Don't: Validate on mount

tsx
// Bad: Shows error before user interaction
const error = Pulse.computed(() => 
  email().length === 0 ? 'Required' : ''
);

✅ Do: Validate after interaction

tsx
const touched = Pulse.signal(false);
const error = Pulse.computed(() => 
  touched() && email().length === 0 ? 'Required' : ''
);

<Input
  value={email}
  onChange={(val) => {
    email(val);
    touched(true);
  }}
  error={error}
/>

Version: 1.0.0
Last Updated: January 2025

Released under the MIT License.