Skip to content

Building Forms with state()

Learn how to build reactive forms in Flexium using the state() API for form state management and validation.

Basic Form Example

Use state() to manage form values and validation state:

tsx
import { state } from 'flexium';

function LoginForm() {
  const [formData, setFormData] = state({
    email: '',
    password: ''
  });

  const [errors, setErrors] = state({
    email: null as string | null,
    password: null as string | null
  });

  const handleSubmit = async (e: Event) => {
    e.preventDefault();

    // Reset errors
    setErrors({ email: null, password: null });

    // Validate
    const newErrors: any = {};
    if (!formData.email) {
      newErrors.email = 'Email is required';
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = 'Invalid email format';
    }

    if (!formData.password) {
      newErrors.password = 'Password is required';
    } else if (formData.password.length < 6) {
      newErrors.password = 'Password must be at least 6 characters';
    }

    if (Object.keys(newErrors).length > 0) {
      setErrors(newErrors);
      return;
    }

    // Submit form
    console.log('Submitting:', formData);
    await submitLogin(formData);
  };

  return (
    <form onsubmit={handleSubmit}>
      <div>
        <label for="email">Email</label>
        <input
          id="email"
          type="email"
          value={formData.email}
          oninput={(e) => setFormData({ ...formData, email: e.target.value })}
        />
        {errors.email && <div class="error">{errors.email}</div>}
      </div>

      <div>
        <label for="password">Password</label>
        <input
          id="password"
          type="password"
          value={formData.password}
          oninput={(e) => setFormData({ ...formData, password: e.target.value })}
        />
        {errors.password && <div class="error">{errors.password}</div>}
      </div>

      <button type="submit">Login</button>
    </form>
  );
}

Form with Real-time Validation

Add validation as users type:

tsx
import { state, effect } from 'flexium';

function RegistrationForm() {
  const [formData, setFormData] = state({
    username: '',
    email: '',
    password: '',
    confirmPassword: ''
  });

  const [errors, setErrors] = state({
    username: null as string | null,
    email: null as string | null,
    password: null as string | null,
    confirmPassword: null as string | null
  });

  const [touched, setTouched] = state({
    username: false,
    email: false,
    password: false,
    confirmPassword: false
  });

  // Validate on change
  effect(() => {
    const newErrors: any = {};

    if (touched.username && !formData.username) {
      newErrors.username = 'Username is required';
    } else if (touched.username && formData.username.length < 3) {
      newErrors.username = 'Username must be at least 3 characters';
    }

    if (touched.email && !formData.email) {
      newErrors.email = 'Email is required';
    } else if (touched.email && !/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = 'Invalid email format';
    }

    if (touched.password && !formData.password) {
      newErrors.password = 'Password is required';
    } else if (touched.password && formData.password.length < 8) {
      newErrors.password = 'Password must be at least 8 characters';
    }

    if (touched.confirmPassword && formData.password !== formData.confirmPassword) {
      newErrors.confirmPassword = 'Passwords do not match';
    }

    setErrors(newErrors);
  });

  const handleSubmit = async (e: Event) => {
    e.preventDefault();

    // Mark all as touched
    setTouched({
      username: true,
      email: true,
      password: true,
      confirmPassword: true
    });

    // Check if there are any errors
    if (Object.values(errors).some(error => error !== null)) {
      return;
    }

    // Submit form
    console.log('Submitting:', formData);
    await submitRegistration(formData);
  };

  return (
    <form onsubmit={handleSubmit}>
      <div>
        <label for="username">Username</label>
        <input
          id="username"
          type="text"
          value={formData.username}
          oninput={(e) => setFormData({ ...formData, username: e.target.value })}
          onblur={() => setTouched({ ...touched, username: true })}
        />
        {touched.username && errors.username && (
          <div class="error">{errors.username}</div>
        )}
      </div>

      <div>
        <label for="email">Email</label>
        <input
          id="email"
          type="email"
          value={formData.email}
          oninput={(e) => setFormData({ ...formData, email: e.target.value })}
          onblur={() => setTouched({ ...touched, email: true })}
        />
        {touched.email && errors.email && (
          <div class="error">{errors.email}</div>
        )}
      </div>

      <div>
        <label for="password">Password</label>
        <input
          id="password"
          type="password"
          value={formData.password}
          oninput={(e) => setFormData({ ...formData, password: e.target.value })}
          onblur={() => setTouched({ ...touched, password: true })}
        />
        {touched.password && errors.password && (
          <div class="error">{errors.password}</div>
        )}
      </div>

      <div>
        <label for="confirmPassword">Confirm Password</label>
        <input
          id="confirmPassword"
          type="password"
          value={formData.confirmPassword}
          oninput={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
          onblur={() => setTouched({ ...touched, confirmPassword: true })}
        />
        {touched.confirmPassword && errors.confirmPassword && (
          <div class="error">{errors.confirmPassword}</div>
        )}
      </div>

      <button type="submit">Register</button>
    </form>
  );
}

Custom Form Hook

Create a reusable form hook pattern:

tsx
import { state, effect } from 'flexium';

interface ValidationRule<T> {
  validate: (value: T, allValues: any) => string | null;
  message?: string;
}

interface FieldConfig<T = any> {
  initial: T;
  rules?: ValidationRule<T>[];
}

interface FormConfig {
  [key: string]: FieldConfig;
}

function useForm<T extends FormConfig>(config: T) {
  // Initialize form data
  const initialValues = Object.keys(config).reduce((acc, key) => {
    acc[key] = config[key].initial;
    return acc;
  }, {} as any);

  const [formData, setFormData] = state(initialValues);
  const [errors, setErrors] = state<Record<string, string | null>>({});
  const [touched, setTouched] = state<Record<string, boolean>>({});

  // Validate a single field
  const validateField = (fieldName: string) => {
    const field = config[fieldName];
    if (!field || !field.rules) return null;

    for (const rule of field.rules) {
      const error = rule.validate(formData[fieldName], formData);
      if (error) return error;
    }

    return null;
  };

  // Validate all fields
  const validateForm = () => {
    const newErrors: Record<string, string | null> = {};
    let isValid = true;

    for (const fieldName of Object.keys(config)) {
      const error = validateField(fieldName);
      newErrors[fieldName] = error;
      if (error) isValid = false;
    }

    setErrors(newErrors);
    return isValid;
  };

  // Validate on change for touched fields
  effect(() => {
    const newErrors: Record<string, string | null> = {};

    for (const fieldName of Object.keys(config)) {
      if (touched[fieldName]) {
        newErrors[fieldName] = validateField(fieldName);
      }
    }

    setErrors(newErrors);
  });

  const setFieldValue = (fieldName: string, value: any) => {
    setFormData({ ...formData, [fieldName]: value });
  };

  const setFieldTouched = (fieldName: string) => {
    setTouched({ ...touched, [fieldName]: true });
  };

  const reset = () => {
    setFormData(initialValues);
    setErrors({});
    setTouched({});
  };

  return {
    formData,
    errors,
    touched,
    setFieldValue,
    setFieldTouched,
    validateForm,
    reset
  };
}

// Usage
function UserForm() {
  const form = useForm({
    email: {
      initial: '',
      rules: [
        {
          validate: (value) => value ? null : 'Email is required'
        },
        {
          validate: (value) => /\S+@\S+\.\S+/.test(value) ? null : 'Invalid email'
        }
      ]
    },
    age: {
      initial: 0,
      rules: [
        {
          validate: (value) => value >= 18 ? null : 'Must be 18 or older'
        }
      ]
    }
  });

  const handleSubmit = async (e: Event) => {
    e.preventDefault();

    if (form.validateForm()) {
      console.log('Valid form:', form.formData);
      await submitForm(form.formData);
      form.reset();
    }
  };

  return (
    <form onsubmit={handleSubmit}>
      <div>
        <label>Email</label>
        <input
          type="email"
          value={form.formData.email}
          oninput={(e) => form.setFieldValue('email', e.target.value)}
          onblur={() => form.setFieldTouched('email')}
        />
        {form.touched.email && form.errors.email && (
          <div class="error">{form.errors.email}</div>
        )}
      </div>

      <div>
        <label>Age</label>
        <input
          type="number"
          value={form.formData.age}
          oninput={(e) => form.setFieldValue('age', parseInt(e.target.value))}
          onblur={() => form.setFieldTouched('age')}
        />
        {form.touched.age && form.errors.age && (
          <div class="error">{form.errors.age}</div>
        )}
      </div>

      <button type="submit">Submit</button>
    </form>
  );
}

Async Validation

Handle async validation like checking if an email already exists:

tsx
import { state } from 'flexium';

function SignupForm() {
  const [formData, setFormData] = state({
    email: ''
  });

  const [errors, setErrors] = state({
    email: null as string | null
  });

  const [validating, setValidating] = state({
    email: false
  });

  const validateEmail = async (email: string) => {
    if (!email) {
      setErrors({ ...errors, email: 'Email is required' });
      return;
    }

    if (!/\S+@\S+\.\S+/.test(email)) {
      setErrors({ ...errors, email: 'Invalid email format' });
      return;
    }

    setValidating({ ...validating, email: true });

    try {
      const response = await fetch(`/api/check-email?email=${email}`);
      const data = await response.json();

      if (data.exists) {
        setErrors({ ...errors, email: 'Email already registered' });
      } else {
        setErrors({ ...errors, email: null });
      }
    } catch (error) {
      setErrors({ ...errors, email: 'Failed to validate email' });
    } finally {
      setValidating({ ...validating, email: false });
    }
  };

  const handleEmailBlur = () => {
    validateEmail(formData.email);
  };

  const handleSubmit = async (e: Event) => {
    e.preventDefault();

    await validateEmail(formData.email);

    if (!errors.email) {
      console.log('Submitting:', formData);
      await submitSignup(formData);
    }
  };

  return (
    <form onsubmit={handleSubmit}>
      <div>
        <label for="email">Email</label>
        <input
          id="email"
          type="email"
          value={formData.email}
          oninput={(e) => setFormData({ ...formData, email: e.target.value })}
          onblur={handleEmailBlur}
        />
        {validating.email && <div class="validating">Checking email...</div>}
        {errors.email && <div class="error">{errors.email}</div>}
      </div>

      <button type="submit" disabled={validating.email}>
        Sign Up
      </button>
    </form>
  );
}

Form with Loading State

Show loading state during form submission:

tsx
import { state } from 'flexium';

function ContactForm() {
  const [formData, setFormData] = state({
    name: '',
    email: '',
    message: ''
  });

  const [submitting, setSubmitting] = state(false);
  const [submitted, setSubmitted] = state(false);

  const handleSubmit = async (e: Event) => {
    e.preventDefault();

    setSubmitting(true);

    try {
      await fetch('/api/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData)
      });

      setSubmitted(true);
      setFormData({ name: '', email: '', message: '' });
    } catch (error) {
      console.error('Failed to submit form:', error);
    } finally {
      setSubmitting(false);
    }
  };

  if (submitted) {
    return <div class="success">Thank you! Your message has been sent.</div>;
  }

  return (
    <form onsubmit={handleSubmit}>
      <div>
        <label for="name">Name</label>
        <input
          id="name"
          type="text"
          value={formData.name}
          oninput={(e) => setFormData({ ...formData, name: e.target.value })}
          disabled={submitting}
        />
      </div>

      <div>
        <label for="email">Email</label>
        <input
          id="email"
          type="email"
          value={formData.email}
          oninput={(e) => setFormData({ ...formData, email: e.target.value })}
          disabled={submitting}
        />
      </div>

      <div>
        <label for="message">Message</label>
        <textarea
          id="message"
          value={formData.message}
          oninput={(e) => setFormData({ ...formData, message: e.target.value })}
          disabled={submitting}
        />
      </div>

      <button type="submit" disabled={submitting}>
        {submitting ? 'Sending...' : 'Send Message'}
      </button>
    </form>
  );
}

See Also

Released under the MIT License.