Form Controller
A flexible, reactive form management system for both multi-step and single-step forms with advanced validation capabilities.
Form Controller: The Complete Solution for Form Management
Form Controller is a powerful, reactive form management system designed to handle all aspects of form interaction in modern web applications. From simple contact forms to complex multi-page wizards with conditional logic, Form Controller provides a comprehensive solution with minimal boilerplate code.
It handles state management, validation, data collection, error handling, and UI interactions through a well-structured, predictable system that integrates with all major frontend frameworks.
Form Controller takes a declarative approach to form management, allowing developers to define form structure and behavior through configuration rather than imperative code. This results in cleaner codebases, improved maintainability, and faster development cycles.
Flexible Form Structure
Unified API for both single-step and multi-step forms with progressive form building capabilities.
Advanced Validation
Comprehensive validation rules including pattern matching, dynamic validators, and cross-field validation.
Reactive Architecture
Real-time state updates with reactive state properties that automatically sync UI with form state.
Framework Agnostic
Integrates with React, Vue, and Svelte through dedicated binding packages.
Type Safety
Full TypeScript support with strong typing for form configurations and state management.
Progressive Disclosure
Step-by-step form progression with conditional logic and dependency tracking.
Installation
Install the form controller package:
1npm install @uplink-protocol/form-controller
Then install the integration package for your framework:
1npm install @uplink-protocol/react
TypeScript Support
Form Controller comes with full TypeScript support out of the box. No additional type packages are required.
This enables code completion, type checking, and better developer experience when working with the library.
Form Controller Basics
At its core, Form Controller is built on a reactive architecture that provides real-time state management, field validation, step navigation, and comprehensive event handling. It follows the principle of "configuration over code" while offering powerful extension capabilities.
Core Concepts
- 1Step-Based Architecture - Forms are organized into one or more steps, each containing fields with their own configuration and validation rules.
- 2Declarative Configuration - Define your entire form structure, field properties, and validation rules in a single configuration object.
- 3Reactive State Management - All form state is reactive, automatically updating the UI when data or validation status changes.
- 4Progressive Validation - Fields can be validated on change, blur, step transition, or only on form submission.
Key State Properties
formData
All current field valuesfieldErrors
Validation error messagescurrentStep
Current active stepisFormValid
Overall form validity statusstepsValidity
Per-step validation statusForm Configuration Overview
1import { FormController, FormConfig } from '@uplink-protocol/form-controller';23// Create a form configuration4const formConfig: FormConfig = {5 steps: [6 {7 id: 'personal', // Unique step identifier8 title: 'Personal Info', // Display title for the step9 fields: {10 name: {11 id: 'name', // Unique field identifier12 type: 'text', // Input field type13 label: 'Full Name',// Display label14 required: true, // Field is required15 value: '' // Initial value16 },17 email: {18 id: 'email',19 type: 'email',20 label: 'Email Address',21 required: true,22 validation: {23 pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,24 errorMessage: 'Please enter a valid email address'25 }26 }27 }28 },29 // Additional steps as needed30 ]31};3233// Initialize the form controller34const form = FormController(formConfig);
Reactive State Architecture
Form Controller uses a reactive subscriber pattern to automatically update UI components when form state changes. This includes field values, errors, validation status, and step navigation state.
Comprehensive Validation
From simple required fields to complex pattern matching, cross-field validation, and custom asynchronous validators, Form Controller provides a complete validation system with granular control.
Multi-Step Capabilities
Built from the ground up for both simple and multi-step forms, with step navigation, conditional progression, step validation, and persistent state between steps.
Framework Integrations
Form Controller is designed to be framework-agnostic, with dedicated binding packages for popular frameworks:
1import { useUplink } from "@uplink-protocol/react";2import { FormController } from "@uplink-protocol/form-controller";34function MyForm() {5 const { state, methods } = useUplink(6 () => FormController({7 steps: [8 {9 id: 'personal',10 fields: {11 name: { id: 'name', type: 'text', required: true },12 email: { id: 'email', type: 'email', required: true }13 }14 }15 ]16 }),17 { trackBindings: "all" }18 );1920 return (21 <form onSubmit={(e) => { e.preventDefault(); methods.submitForm(); }}>22 <input23 type="text"24 value={state.formData.personal?.name || ""}25 onChange={(e) => methods.updateField("personal", "name", e.target.value)}26 />27 <input28 type="email"29 value={state.formData.personal?.email || ""}30 onChange={(e) => methods.updateField("personal", "email", e.target.value)}31 />32 />33 <button type="submit">Submit</button>34 </form>35 );
Advanced Capabilities
Form Controller goes beyond basic form management, offering sophisticated capabilities to handle complex real-world scenarios. These advanced features enable you to build enterprise-grade forms with dynamic behavior, complex validation logic, and seamless integration with your data pipeline.
Key Advanced Features
- Conditional Logic - Show, hide, or validate fields based on other field values, creating dynamic form experiences.
- Cross-Field Validation - Compare values between fields to ensure data integrity and consistency.
- State Persistence - Save and restore form state across sessions, enabling draft functionality and resumable forms.
When to Use Advanced Features
Consider implementing these advanced capabilities when your form:
- ✓Has complex dependencies between fields
- ✓Needs to collect structured data like nested objects or arrays
- ✓Supports saving drafts or recovering form state
- ✓Requires complex business rules for validation
Conditional Logic & Dynamic Fields
Form Controller offers a powerful system for creating forms that adapt to user input. You can show or hide fields, make them required, or apply specific validation rules based on the values of other fields. This enables you to build intuitive, responsive forms that guide users through complex processes.
1import React, { useEffect } from 'react';2import { useUplinkController } from "@uplink-protocol/react";3import { FormController } from "@uplink-protocol/form-controller";45function ConditionalForm() {6 const { state, methods } = useUplinkController(FormController({7 steps: [8 {9 id: 'contact',10 fields: {11 subscribe: {12 id: 'subscribe',13 type: 'checkbox',14 label: 'Subscribe to newsletter',15 value: false16 },17 phoneNumber: {18 id: 'phoneNumber',19 type: 'tel',20 label: 'Phone Number',21 value: ''22 }23 }24 }25 ]26 }), { trackBindings: "all" });2728 // Register conditional validator29 useEffect(() => {30 methods.registerValidator('requiredIf', (value, context) => {31 const { dependsOn, dependsOnValue, errorMessage } =32 context.field.validation.dynamicValidatorParams || {};3334 if (!dependsOn) return true;3536 const stepId = context.field.stepId;37 const dependentValue = context.formData[stepId][dependsOn];3839 if (dependentValue === dependsOnValue && (!value || value.trim() === '')) {40 return errorMessage || 'This field is conditionally required';41 }4243 return true;44 });4546 // Apply validator to phone number field47 methods.extendField('contact', 'phoneNumber', {48 validation: {49 dynamicValidator: 'requiredIf',50 dynamicValidatorParams: {51 dependsOn: 'subscribe',52 dependsOnValue: true,53 errorMessage: 'Phone number is required when subscribing'54 }55 }56 });5758 // Cleanup on unmount59 return () => {60 methods.unregisterValidator('requiredIf');61 };62 }, [methods]);6364 const handleSubmit = (e) => {65 e.preventDefault();66 if (state.isFormValid) {67 console.log('Form submitted:', state.formData);68 }69 };7071 return (72 <form onSubmit={handleSubmit}>73 <div className="form-group">74 <label>75 <input76 type="checkbox"77 checked={state.values.subscribe || false}78 onChange={(e) => methods.setValue('subscribe', e.target.checked)}79 />80 {' '}Subscribe to newsletter81 </label>82 </div>8384 {state.values.subscribe && (85 <div className="form-group">86 <label>87 Phone Number:88 <input89 type="tel"90 value={state.values.phoneNumber || ''}91 onChange={(e) => methods.setValue('phoneNumber', e.target.value)}92 onBlur={() => methods.touchField('phoneNumber')}93 className={state.fieldErrors.phoneNumber ? 'border-red-500' : ''}94 />95 </label>96 {state.fieldErrors.phoneNumber && (97 <div className="text-red-500">{state.fieldErrors.phoneNumber}</div>98 )}99 </div>100 )}101102 <button type="submit" disabled={!state.isFormValid}>103 Submit104 </button>105 </form>106 );107}
Implementation Tips
- •Always handle both UI rendering validation logic for conditional fields
- •Consider creating reusable dynamic validators for common conditional patterns
- •For complex conditions, you can use custom functions that take multiple dependencies into account
- •Store conditional state in the form data to maintain consistency across component re-renders
Cross-Field Validation
Validating fields by comparing their values to other fields is essential for ensuring data integrity in many forms. Form Controller provides comprehensive tools for implementing cross-field validation, from simple equality checks to complex data relationship validations.
1import React, { useEffect } from 'react';2import { useUplinkController } from "@uplink-protocol/react";3import { FormController } from "@uplink-protocol/form-controller";45function PasswordForm() {6 const { state, methods } = useUplinkController(FormController({7 steps: [8 {9 id: 'credentials',10 fields: {11 password: {12 id: 'password',13 type: 'password',14 label: 'Password',15 value: '',16 required: true17 },18 confirmPassword: {19 id: 'confirmPassword',20 type: 'password',21 label: 'Confirm Password',22 value: '',23 required: true24 }25 }26 }27 ]28 }), { trackBindings: "all" });2930 // Register a custom dynamic validator for password matching31 useEffect(() => {32 methods.registerValidator('equals', (value, context) => {33 const { matchField } = context.field.validation.dynamicValidatorParams || {};34 const stepId = context.field.stepId;3536 if (!matchField) return true;3738 const targetValue = context.formData[stepId][matchField];39 return value === targetValue || 'Fields must match';40 });4142 // Apply validator to confirm password field43 methods.extendField('credentials', 'confirmPassword', {44 validation: {45 dynamicValidator: 'equals',46 dynamicValidatorParams: {47 matchField: 'password',48 errorMessage: 'Passwords must match'49 }50 }51 });5253 // Cleanup on unmount54 return () => {55 methods.unregisterValidator('equals');56 };57 }, [methods]);5859 const handleSubmit = (e) => {60 e.preventDefault();61 if (state.isFormValid) {62 console.log('Form submitted:', state.formData);63 }64 };6566 return (67 <form onSubmit={handleSubmit}>68 <div className="form-group">69 <label>70 Password:71 <input72 type="password"73 value={state.values.password || ''}74 onChange={(e) => methods.setValue('password', e.target.value)}75 onBlur={() => methods.touchField('password')}76 className={state.fieldErrors.password ? 'border-red-500' : ''}77 />78 </label>79 {state.fieldErrors.password && (80 <div className="text-red-500">{state.fieldErrors.password}</div>81 )}82 </div>8384 <div className="form-group">85 <label>86 Confirm Password:87 <input88 type="password"89 value={state.values.confirmPassword || ''}90 onChange={(e) => methods.setValue('confirmPassword', e.target.value)}91 onBlur={() => methods.touchField('confirmPassword')}92 className={state.fieldErrors.confirmPassword ? 'border-red-500' : ''}93 />94 </label>95 {state.fieldErrors.confirmPassword && (96 <div className="text-red-500">{state.fieldErrors.confirmPassword}</div>97 )}98 </div>99100 <button type="submit" disabled={!state.isFormValid}>101 Submit102 </button>103 </form>104 );105}
Best Practices
- ✓Always validate both fields in a relationship to ensure consistent error states
- ✓Use type coercion when comparing values of different types
- ✓Consider the timing of validation (onChange, onBlur, or onSubmit)
Common Pitfalls
- ✗Forgetting to handle undefined values in comparisons
- ✗Not updating related field error states when one field changes
- ✗Creating circular validation dependencies
Form State Persistence
Form Controller provides a robust foundation for implementing state persistence across sessions, page refreshes, and navigation. This is crucial for complex forms where users might need to save their progress and return later, or for recovering form data in case of unexpected errors.
1import { useUplinkController } from "@uplink-protocol/react";2import { FormController } from "@uplink-protocol/form-controller";3import { useEffect } from "react";45export function FormWithPersistence() {6 const { state, methods } = useUplinkController(FormController({7 steps: [8 {9 id: 'userInfo',10 fields: {11 name: {12 id: 'name',13 type: 'text',14 label: 'Full Name',15 value: '',16 required: true17 },18 email: {19 id: 'email',20 type: 'email',21 label: 'Email Address',22 value: ''23 }24 }25 }26 ]27 }), { trackBindings: "all" });2829 // Load saved state on component mount30 useEffect(() => {31 try {32 const savedState = localStorage.getItem('myForm');33 if (savedState) {34 const { timestamp, fields } = JSON.parse(savedState);3536 // Check if saved data is still valid (e.g., not older than 24 hours)37 const ONE_DAY = 24 * 60 * 60 * 1000;38 if (Date.now() - timestamp < ONE_DAY) {39 // Restore field values40 Object.entries(fields).forEach(([fieldId, value]) => {41 methods.setValue(fieldId, value);42 });4344 // Display notification to user45 alert('Your previous form data has been restored.');46 } else {47 // Clear expired data48 localStorage.removeItem('myForm');49 }50 }51 } catch (error) {52 console.error('Error loading saved form state:', error);53 }54 }, [methods]);5556 // Set up auto-save57 useEffect(() => {58 const handleStateChange = () => {59 localStorage.setItem('myForm', JSON.stringify({60 timestamp: Date.now(),61 fields: state.values62 }));63 };6465 // Save state on changes66 const unsubscribe = methods.subscribe(handleStateChange);6768 return () => {69 unsubscribe();70 };71 }, [state.values, methods]);7273 const handleSubmit = (e: React.FormEvent) => {74 e.preventDefault();75 if (state.isFormValid) {76 // Process form submission77 console.log('Form submitted:', state.formData);7879 // Clear saved form data after successful submission80 localStorage.removeItem('myForm');81 }82 };8384 return (85 <form onSubmit={handleSubmit}>86 <div className="form-group">87 <label>88 Full Name:89 <input90 type="text"91 value={state.values.name || ''}92 onChange={(e) => methods.setValue('name', e.target.value)}93 onBlur={() => methods.touchField('name')}94 />95 </label>96 {state.fieldErrors.name && (97 <div className="text-red-500">{state.fieldErrors.name}</div>98 )}99 </div>100101 <div className="form-group">102 <label>103 Email:104 <input105 type="email"106 value={state.values.email || ''}107 onChange={(e) => methods.setValue('email', e.target.value)}108 onBlur={() => methods.touchField('email')}109 />110 </label>111 </div>112113 <button type="submit" disabled={!state.isFormValid}>114 Submit115 </button>116 </form>117 );118}
Implementation Considerations
- ⚠️Consider storage limitations when deciding what and how frequently to save
- ⚠️Implement expiration logic for saved form data to prevent stale data issues
- ⚠️Provide clear UI indicators when form data is being restored from a saved state
- ⚠️Consider server-side storage for larger forms or when persistence across devices is needed
Validation System
Form Controller offers a comprehensive validation system to ensure data integrity and provide helpful feedback to users. From simple required field checks to complex cross-field and asynchronous validation, the form controller handles all validation scenarios.
Validation Overview
The validation system in Form Controller works on multiple levels:
Field Validation
Applied to individual fields to validate their content according to specific rules. This is the most common form of validation and happens automatically.
Step Validation
Validates all fields within a step, ensuring the entire step is valid before allowing navigation to the next step. Useful for multi-step forms.
Form Validation
Validates the entire form state, ensuring all fields across all steps are valid before submission. This prevents submitting invalid data.
1interface Validation {2 required?: boolean;3 pattern?: RegExp | string;4 minLength?: number;5 maxLength?: number;6 min?: number;7 max?: number;8 custom?: (value: any) => boolean | string;9 dynamicValidator?: string;10 dynamicValidatorParams?: Record<string, any>;11 errorMessage?: string;12}
Built-in Validators
Form Controller provides several built-in validators that cover the most common validation needs:
required
pattern
minLength
maxLength
min
max
custom
1// Required field validation2const nameField = {3 id: 'name',4 type: 'text',5 label: 'Full Name',6 required: true, // Shorthand for validation: { required: true }7 value: ''8};910// Pattern validation (email)11const emailField = {12 id: 'email',13 type: 'email',14 label: 'Email Address',15 validation: {16 required: true,17 pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,18 errorMessage: 'Please enter a valid email address'19 }20};2122// Min/max length validation (password)23const passwordField = {24 id: 'password',25 type: 'password',26 label: 'Password',27 validation: {28 required: true,29 minLength: 8,30 maxLength: 50,31 errorMessage: 'Password must be between 8 and 50 characters'32 }33};3435// Min/max numeric validation (age)36const ageField = {37 id: 'age',38 type: 'number',39 label: 'Age',40 validation: {41 required: true,42 min: 18,43 max: 120,44 errorMessage: 'Age must be between 18 and 120'45 }46};4748// Custom validation function49const usernameField = {50 id: 'username',51 type: 'text',52 label: 'Username',53 validation: {54 required: true,55 custom: (value) => {56 if (!/^[a-zA-Z0-9_]+$/.test(value)) {57 return 'Username can only contain letters, numbers, and underscores';58 }5960 if (value.length < 3) {61 return 'Username must be at least 3 characters long';62 }6364 // Additional async check could be performed here if needed6566 return true; // Valid67 }68 }69};
Dynamic Validators
Dynamic validators enable more complex validation scenarios by taking into account the context of the form, including other field values. This is particularly useful for conditional validation and cross-field validation scenarios.
1// First, register a dynamic validator2methods.registerValidator('requiredIf', (value, context) => {3 const { dependsOn, dependsOnValue, errorMessage } =4 context.field.validation.dynamicValidatorParams || {};56 if (!dependsOn) return true;78 const stepId = context.field.stepId;9 const dependentValue = context.formData[stepId][dependsOn];1011 // If the condition is met and field is empty, return error message12 if (dependentValue === dependsOnValue && (!value || value.trim() === '')) {13 return errorMessage || 'This field is conditionally required';14 }1516 return true;17});1819// Then use the dynamic validator in a field configuration20const formConfig = {21 steps: [22 {23 id: 'shipping',24 title: 'Shipping Information',25 fields: {26 shippingMethod: {27 id: 'shippingMethod',28 type: 'select',29 label: 'Shipping Method',30 options: [31 { value: 'standard', label: 'Standard Shipping' },32 { value: 'express', label: 'Express Shipping' },33 { value: 'pickup', label: 'Local Pickup' }34 ],35 required: true36 },37 pickupLocation: {38 id: 'pickupLocation',39 type: 'select',40 label: 'Pickup Location',41 options: [42 { value: 'store1', label: 'Downtown Store' },43 { value: 'store2', label: 'Mall Location' }44 ],45 validation: {46 dynamicValidator: 'requiredIf',47 dynamicValidatorParams: {48 dependsOn: 'shippingMethod',49 dependsOnValue: 'pickup',50 errorMessage: 'Please select a pickup location'51 }52 }53 }54 }55 }56 ]57};
Dynamic Validator Context
Every dynamic validator receives a context object that provides access to:
- •
field
- The field configuration including all validation settings - •
formData
- The current values of all fields in the form - •
fieldErrors
- The current validation errors for all fields - •
methods
- Access to form controller methods for complex validation scenarios
Asynchronous Validation
Form Controller supports asynchronous validation for scenarios where validation requires API calls or other asynchronous operations. This is useful for checking username availability, email uniqueness, or other server-side validations.
1// Register an async validator2methods.registerValidator('usernameAvailable', async (value, context) => {3 // Skip validation if empty (let required validator handle it)4 if (!value) return true;56 try {7 // Show loading state during validation8 methods.setFieldValidating(context.field.stepId, context.field.id, true);910 // Make API call to check username availability11 const response = await fetch(`/api/check-username?username=${encodeURIComponent(value)}`);12 const data = await response.json();1314 // Return validation result15 return data.available ? true : 'This username is already taken';16 } catch (error) {17 console.error('Username validation error:', error);18 return 'Could not verify username availability';19 } finally {20 // Hide loading state after validation21 methods.setFieldValidating(context.field.stepId, context.field.id, false);22 }23});2425// Use the async validator in field configuration26const usernameField = {27 id: 'username',28 type: 'text',29 label: 'Username',30 validation: {31 required: true,32 minLength: 3,33 dynamicValidator: 'usernameAvailable',34 // Debounce to prevent excessive API calls35 debounce: 50036 }37};3839// In your UI, show loading state40return (41 <div>42 <input type="text" {...methods.bindInput('username')} />43 {state.fieldsValidating.registration?.username && (44 <span className="text-blue-500">Checking availability...</span>45 )}46 {state.fieldErrors.registration?.username && (47 <span className="text-red-500">{state.fieldErrors.registration.username}</span>48 )}49 </div>50);
Async Validation Tips
- •Always implement debouncing to prevent excessive API calls
- •Show clear loading indicators during validation
- •Handle network errors gracefully with meaningful messages
When to Use Async Validation
- •Checking for duplicates in a database (usernames, emails)
- •Validating addresses or other complex data against external services
- •Verifying coupon codes or other user-entered references
Framework-Specific Validation Examples
Here's how to implement form validation using Form Controller in different frontend frameworks:
1import React from 'react';2import { useUplinkController } from "@uplink-protocol/react";3import { FormController } from "@uplink-protocol/form-controller";45function RegistrationForm() {6 const { state, methods } = useUplinkController(FormController({7 steps: [8 {9 id: 'registration',10 fields: {11 username: {12 id: 'username',13 type: 'text',14 label: 'Username',15 required: true,16 validation: {17 minLength: 3,18 maxLength: 20,19 pattern: /^[a-zA-Z0-9_]+$/,20 errorMessage: 'Username must be 3-20 characters and contain only letters, numbers, and underscores'21 }22 },23 password: {24 id: 'password',25 type: 'password',26 label: 'Password',27 required: true,28 validation: {29 minLength: 8,30 pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/,31 errorMessage: 'Password must be at least 8 characters and include uppercase, lowercase, number and special character'32 }33 },34 confirmPassword: {35 id: 'confirmPassword',36 type: 'password',37 label: 'Confirm Password',38 required: true39 }40 }41 }42 ]43 }), {44 trackBindings: "all" // For this example, we track all bindings since we use multiple fields45 });4647 // Add custom cross-field validation48 React.useEffect(() => {49 methods.registerValidator('passwordsMatch', (value, context) => {50 if (context.field.id === 'confirmPassword' && value !== context.formData.password) {51 return 'Passwords do not match';52 }53 return true;54 });5556 // Apply the validator to the confirm password field57 methods.extendField('registration', 'confirmPassword', {58 validation: {59 dynamicValidator: 'passwordsMatch'60 }61 });6263 // Cleanup on unmount64 return () => {65 methods.unregisterValidator('passwordsMatch');66 };67 }, [methods]);6869 const handleSubmit = (e) => {70 e.preventDefault();71 methods.submit();72 };7374 return (75 <form onSubmit={handleSubmit}>76 <div className="form-group">77 <label htmlFor="username">78 {state.fields.username.label}79 {state.fields.username.required && ' *'}80 </label>81 <input82 id="username"83 type="text"84 value={state.values.username || ''}85 onChange={(e) => methods.setValue('username', e.target.value)}86 onBlur={() => methods.touchField('username')}87 className={state.fieldErrors.username ? 'border-red-500' : ''}88 />89 {state.fieldErrors.username && (90 <div className="text-red-500">{state.fieldErrors.username}</div>91 )}92 </div>9394 <div className="form-group">95 <label htmlFor="password">96 {state.fields.password.label}97 {state.fields.password.required && ' *'}98 </label>99 <input100 id="password"101 type="password"102 value={state.values.password || ''}103 onChange={(e) => methods.setValue('password', e.target.value)}104 onBlur={() => methods.touchField('password')}105 className={state.fieldErrors.password ? 'border-red-500' : ''}106 />107 {state.fieldErrors.password && (108 <div className="text-red-500">{state.fieldErrors.password}</div>109 )}110 </div>111112 <div className="form-group">113 <label htmlFor="confirmPassword">114 {state.fields.confirmPassword.label}115 {state.fields.confirmPassword.required && ' *'}116 </label>117 <input118 id="confirmPassword"119 type="password"120 value={state.values.confirmPassword || ''}121 onChange={(e) => methods.setValue('confirmPassword', e.target.value)}122 onBlur={() => methods.touchField('confirmPassword')}123 className={state.fieldErrors.confirmPassword ? 'border-red-500' : ''}124 />125 {state.fieldErrors.confirmPassword && (126 <div className="text-red-500">{state.fieldErrors.confirmPassword}</div>127 )}128 </div>129130 <button131 type="submit"132 disabled={!state.isFormValid || !state.isFormDirty}133 className="bg-blue-500 text-white py-2 px-4 rounded"134 >135 Register136 </button>137 </form>138 );139}
TypeScript Support
Form Controller is built from the ground up with full TypeScript support, providing type safety, code completion, and improved developer experience throughout your application.
TypeScript Benefits
Using TypeScript with Form Controller provides several key benefits:
- 1Type Checking - Catch configuration errors at compile time rather than at runtime
- 2IntelliSense - Get autocompletion and hints as you type, making development faster and reducing errors
- 3Documentation - Access JSDoc comments directly in your IDE for better understanding of the API
- 4Refactoring - Safely refactor your code with reliable type information
Basic TypeScript Setup
1import { FormController, FormConfig } from '@uplink-protocol/form-controller';23// Define your form configuration with proper types4const formConfig: FormConfig = {5 steps: [6 {7 id: 'contact',8 title: 'Contact Information',9 fields: {10 name: {11 id: 'name',12 type: 'text',13 label: 'Full Name',14 required: true15 } as Field,16 email: {17 id: 'email',18 type: 'email',19 label: 'Email Address',20 required: true,21 validation: {22 pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/23 }24 } as Field25 }26 }27 ]28};2930// Initialize the form controller31const form = FormController(formConfig);3233// TypeScript will provide proper typing for all form methods and properties34const currentStep = form.state.currentStep.value; // string35const isValid = form.state.isFormValid.value; // boolean36const formData = form.state.formData.value; // Record<string, Record<string, any>>3738// Type checking for methods39form.methods.updateField('contact', 'name', 'John Doe');40form.methods.validateField('contact', 'email');4142// This would cause a TypeScript error:43// form.methods.goToStep(123); // Error: Argument of type 'number' is not assignable to parameter of type 'string'
Framework-Specific TypeScript Integration
1import React from 'react';2import { useUplinkController } from "@uplink-protocol/react";3import { FormController, FormConfig, Field } from "@uplink-protocol/form-controller";45// Define your form data type6interface UserForm {7 personal: {8 name: string;9 email: string;10 age: number;11 };12 preferences: {13 theme: 'light' | 'dark' | 'system';14 notifications: boolean;15 };16}1718// Create a strongly typed form component19function TypedForm() {20 // Form configuration with type parameters21 const formConfig: FormConfig<UserForm> = {22 steps: [23 {24 id: 'personal',25 title: 'Personal Information',26 fields: {27 name: {28 id: 'name',29 type: 'text',30 label: 'Full Name',31 required: true32 } as Field<string>,33 email: {34 id: 'email',35 type: 'email',36 label: 'Email Address',37 required: true,38 validation: {39 pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/40 }41 } as Field<string>,42 age: {43 id: 'age',44 type: 'number',45 label: 'Age',46 required: true,47 validation: {48 min: 18,49 max: 12050 }51 } as Field<number>52 }53 },54 {55 id: 'preferences',56 title: 'Preferences',57 fields: {58 theme: {59 id: 'theme',60 type: 'select',61 label: 'Theme',62 options: [63 { value: 'light', label: 'Light' },64 { value: 'dark', label: 'Dark' },65 { value: 'system', label: 'System' }66 ],67 defaultValue: 'system'68 } as Field<'light' | 'dark' | 'system'>,69 notifications: {70 id: 'notifications',71 type: 'checkbox',72 label: 'Enable Notifications',73 defaultValue: true74 } as Field<boolean>75 }76 }77 ]78 };7980 // Note the generic type parameter for proper type checking81 const { state, methods } = useUplinkController<UserForm>(82 () => FormController<UserForm>(formConfig)83 );8485 // TypeScript now provides full type safety86 const handleSubmit = (e: React.FormEvent) => {87 e.preventDefault();88 if (state.isFormValid) {89 // Access with type safety90 const name: string = state.values.name;91 const age: number = state.values.age;92 const theme: 'light' | 'dark' | 'system' = state.values.theme;9394 console.log('Form submitted with:', {95 name, age, theme,96 // Full form data is also correctly typed97 formData: state.formData98 });99 }100 };101102 return (103 <form onSubmit={handleSubmit}>104 {state.currentStep === 'personal' && (105 <div>106 <div className="form-group">107 <label htmlFor="name">108 {state.fields.personal.name.label}109 {state.fields.personal.name.required && ' *'}110 </label>111 <input112 id="name"113 type="text"114 value={state.values.name || ''}115 onChange={(e) => methods.setValue('name', e.target.value)}116 onBlur={() => methods.touchField('name')}117 />118 {state.fieldErrors.name && (119 <div className="text-red-500">{state.fieldErrors.name}</div>120 )}121 </div>122123 {/* More fields... */}124125 <button126 type="button"127 onClick={() => methods.goToStep('preferences')}128 disabled={!state.stepsValidity.personal}129 >130 Next131 </button>132 </div>133 )}134135 {/* More steps... */}136 </form>137 );138}
Type-Safe Event Handlers
TypeScript provides complete type safety for event handlers and subscriptions, ensuring you're always working with the correct data types.
1import { FormController, FormConfig } from '@uplink-protocol/form-controller';23const form = FormController(formConfig);45// Type-safe subscription to form data changes6form.state.formData.subscribe((formData) => {7 // formData is properly typed as Record<string, Record<string, any>>8 const nameValue = formData.contact?.name; // Type safety for accessing properties9 console.log('Name updated:', nameValue);10});1112// Type-safe event handler for step changes13form.state.currentStep.subscribe((stepId) => {14 // stepId is properly typed as string15 console.log('Current step:', stepId);1617 // You can safely use stepId in other methods18 const isStepValid = form.state.stepsValidity.value[stepId];19});2021// Type-safe form submission handler22form.methods.onSubmit(() => {23 if (form.state.isFormValid.value) {24 // Type-safe access to all form data25 const formData = form.state.formData.value;2627 // Safely access nested properties28 const contactData = formData.contact;29 const name = contactData?.name;30 const email = contactData?.email;3132 submitToApi({ name, email });33 }34});
Custom Form Models
For more complex applications, you can create custom form data models with specific types for each field, providing additional type safety and better documentation.
1import { FormController, FormConfig } from '@uplink-protocol/form-controller';23// Define a custom form data model with specific types4interface UserFormData {5 personal: {6 name: string;7 age: number;8 };9 address: {10 street: string;11 city: string;12 zipCode: string;13 country: string;14 };15}1617// Create a typed form configuration18const formConfig: FormConfig<UserFormData> = {19 steps: [20 {21 id: 'personal',22 title: 'Personal Information',23 fields: {24 name: {25 id: 'name',26 type: 'text',27 label: 'Full Name',28 required: true29 } as Field<string>,30 age: {31 id: 'age',32 type: 'number',33 label: 'Age',34 required: true,35 validation: {36 min: 18,37 max: 12038 }39 } as Field<number>40 }41 },42 {43 id: 'address',44 title: 'Address Information',45 fields: {46 street: {47 id: 'street',48 type: 'text',49 label: 'Street Address',50 required: true51 } as Field<string>,52 city: {53 id: 'city',54 type: 'text',55 label: 'City',56 required: true57 } as Field<string>,58 zipCode: {59 id: 'zipCode',60 type: 'text',61 label: 'ZIP/Postal Code',62 required: true63 } as Field<string>,64 country: {65 id: 'country',66 type: 'select',67 label: 'Country',68 required: true,69 options: [70 { value: 'us', label: 'United States' },71 { value: 'ca', label: 'Canada' },72 { value: 'uk', label: 'United Kingdom' }73 ]74 } as Field<string>75 }76 }77 ]78};7980// Initialize with the generic type81const form = FormController<UserFormData>(formConfig);8283// Now all methods and state are strongly typed to your data model84form.methods.updateField('personal', 'age', 30); // Type safe - age is number85form.methods.updateField('address', 'zipCode', '10001'); // Type safe - zipCode is string8687// This would cause TypeScript errors:88// form.methods.updateField('personal', 'nonExistentField', 'value'); // Error: 'nonExistentField' doesn't exist89// form.methods.updateField('personal', 'age', 'thirty'); // Error: 'thirty' is not assignable to type 'number'9091// Strongly-typed access to form data92form.state.formData.subscribe((formData) => {93 // Fully typed as UserFormData94 const age: number = formData.personal.age; // Properly typed as number95 const country: string = formData.address.country; // Properly typed as string9697 // TypeScript will catch errors:98 // const invalid = formData.personal.invalidProperty; // Error: Property 'invalidProperty' does not exist99});
Advanced TypeScript Tips
- •Create type-safe custom validator functions by defining proper parameter and return types
- •Use TypeScript utility types like
Partial
,Pick
, andOmit
to create variations of your form data types - •Create type assertions for dynamic validators to ensure proper usage
- •Use IDE plugins that enhance TypeScript experience (like VS Code's TypeScript language features)
Architecture
Form Controller is built using a service-oriented architecture with a reactive state management system, providing a robust foundation for complex form handling.
Core Components
- FCFormController
The main entry point that initializes services and exposes the public API
- BSBaseService
Foundation for reactive state management across all services
- CSConfigService
Manages form configuration and structure
- FSFieldService
Handles field operations and validation
- FSFormService
Manages form data and overall form state
- ISInteractionService
Handles user interactions and events
- SSStepperService
Manages multi-step navigation
- VSValidatorService
Handles validation rules and processing
State Management Concepts
Services
Internal state containers with getters/setters and subscription capabilities that manage different aspects of the form
State
Public reactive properties exposed to consumers that automatically update when the underlying state changes
Methods
Public functions for interacting with the form that provide a clean API for form operations
Reactive Update Flow
- 1.User action triggers a method call
- 2.Method updates service state
- 3.Service notifies subscribers about state change
- 4.Binding callbacks are triggered with new values
- 5.UI components react to binding changes
Architecture Diagram
User Interface Layer
Public API Layer
Form Controller
Service Layer
Implementation Example
1// Example of how reactive updates flow through the system2import { FormController } from '@uplink-protocol/form-controller';34// Initialize form controller with config5const form = FormController({6 steps: [7 {8 id: 'personal',9 title: 'Personal Information',10 fields: {11 name: {12 id: 'name',13 type: 'text',14 label: 'Full Name',15 required: true16 },17 email: {18 id: 'email',19 type: 'email',20 label: 'Email Address',21 required: true22 }23 }24 }25 ]26});2728// 1. Subscribe to form data changes in the UI29form.state.formData.subscribe((formData) => {30 console.log('Form data updated:', formData);31 // Update UI components with new data32});3334// 2. User updates a field via the public API35form.methods.updateField('personal', 'name', 'John Doe');3637// Internal flow (simplified):38// - updateField() method is called on FormController39// - FormController delegates to FieldService.updateField()40// - FieldService updates internal state41// - FieldService notifies FormService of the change42// - FormService updates its formData state43// - FormService notifies subscribers (the binding)44// - The binding callback is triggered with the new value45// - UI component reacts to the binding change4647// Additional automatic steps:48// - ValidatorService is notified to validate the field49// - If validation fails, fieldErrors state is updated50// - UI components subscribed to fieldErrors update accordingly
Architecture Benefits
- ✓Separation of Concerns - Each service handles a specific aspect of form management
- ✓Testability - Services can be tested in isolation with minimal mocking
- ✓Extensibility - New capabilities can be added by extending existing services or adding new ones
- ✓Framework Agnostic - The core architecture isn't tied to any UI framework
Advanced Usage Notes
- •Services maintain immutable state internally to prevent side effects
- •State provides a read-only view of state to prevent direct mutations
- •Method calls are the only way to update state, ensuring all updates go through proper validation
- •Event-based communication between services reduces tight coupling