Logic

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:

npm
1npm install @uplink-protocol/form-controller

Then install the integration package for your framework:

npm
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

  • 1
    Step-Based Architecture - Forms are organized into one or more steps, each containing fields with their own configuration and validation rules.
  • 2
    Declarative Configuration - Define your entire form structure, field properties, and validation rules in a single configuration object.
  • 3
    Reactive State Management - All form state is reactive, automatically updating the UI when data or validation status changes.
  • 4
    Progressive Validation - Fields can be validated on change, blur, step transition, or only on form submission.

Key State Properties

formDataAll current field values
fieldErrorsValidation error messages
currentStepCurrent active step
isFormValidOverall form validity status
stepsValidityPer-step validation status

Form Configuration Overview

basic-form-config.ts
1import { FormController, FormConfig } from '@uplink-protocol/form-controller';
2
3// Create a form configuration
4const formConfig: FormConfig = {
5 steps: [
6 {
7 id: 'personal', // Unique step identifier
8 title: 'Personal Info', // Display title for the step
9 fields: {
10 name: {
11 id: 'name', // Unique field identifier
12 type: 'text', // Input field type
13 label: 'Full Name',// Display label
14 required: true, // Field is required
15 value: '' // Initial value
16 },
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 needed
30 ]
31};
32
33// Initialize the form controller
34const 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:

react
1import { useUplink } from "@uplink-protocol/react";
2import { FormController } from "@uplink-protocol/form-controller";
3
4function 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 );
19
20 return (
21 <form onSubmit={(e) => { e.preventDefault(); methods.submitForm(); }}>
22 <input
23 type="text"
24 value={state.formData.personal?.name || ""}
25 onChange={(e) => methods.updateField("personal", "name", e.target.value)}
26 />
27 <input
28 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.

react-conditional-fields.tsx
1import React, { useEffect } from 'react';
2import { useUplinkController } from "@uplink-protocol/react";
3import { FormController } from "@uplink-protocol/form-controller";
4
5function 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: false
16 },
17 phoneNumber: {
18 id: 'phoneNumber',
19 type: 'tel',
20 label: 'Phone Number',
21 value: ''
22 }
23 }
24 }
25 ]
26 }), { trackBindings: "all" });
27
28 // Register conditional validator
29 useEffect(() => {
30 methods.registerValidator('requiredIf', (value, context) => {
31 const { dependsOn, dependsOnValue, errorMessage } =
32 context.field.validation.dynamicValidatorParams || {};
33
34 if (!dependsOn) return true;
35
36 const stepId = context.field.stepId;
37 const dependentValue = context.formData[stepId][dependsOn];
38
39 if (dependentValue === dependsOnValue && (!value || value.trim() === '')) {
40 return errorMessage || 'This field is conditionally required';
41 }
42
43 return true;
44 });
45
46 // Apply validator to phone number field
47 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 });
57
58 // Cleanup on unmount
59 return () => {
60 methods.unregisterValidator('requiredIf');
61 };
62 }, [methods]);
63
64 const handleSubmit = (e) => {
65 e.preventDefault();
66 if (state.isFormValid) {
67 console.log('Form submitted:', state.formData);
68 }
69 };
70
71 return (
72 <form onSubmit={handleSubmit}>
73 <div className="form-group">
74 <label>
75 <input
76 type="checkbox"
77 checked={state.values.subscribe || false}
78 onChange={(e) => methods.setValue('subscribe', e.target.checked)}
79 />
80 {' '}Subscribe to newsletter
81 </label>
82 </div>
83
84 {state.values.subscribe && (
85 <div className="form-group">
86 <label>
87 Phone Number:
88 <input
89 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 )}
101
102 <button type="submit" disabled={!state.isFormValid}>
103 Submit
104 </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.

react-cross-field-validation.tsx
1import React, { useEffect } from 'react';
2import { useUplinkController } from "@uplink-protocol/react";
3import { FormController } from "@uplink-protocol/form-controller";
4
5function 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: true
17 },
18 confirmPassword: {
19 id: 'confirmPassword',
20 type: 'password',
21 label: 'Confirm Password',
22 value: '',
23 required: true
24 }
25 }
26 }
27 ]
28 }), { trackBindings: "all" });
29
30 // Register a custom dynamic validator for password matching
31 useEffect(() => {
32 methods.registerValidator('equals', (value, context) => {
33 const { matchField } = context.field.validation.dynamicValidatorParams || {};
34 const stepId = context.field.stepId;
35
36 if (!matchField) return true;
37
38 const targetValue = context.formData[stepId][matchField];
39 return value === targetValue || 'Fields must match';
40 });
41
42 // Apply validator to confirm password field
43 methods.extendField('credentials', 'confirmPassword', {
44 validation: {
45 dynamicValidator: 'equals',
46 dynamicValidatorParams: {
47 matchField: 'password',
48 errorMessage: 'Passwords must match'
49 }
50 }
51 });
52
53 // Cleanup on unmount
54 return () => {
55 methods.unregisterValidator('equals');
56 };
57 }, [methods]);
58
59 const handleSubmit = (e) => {
60 e.preventDefault();
61 if (state.isFormValid) {
62 console.log('Form submitted:', state.formData);
63 }
64 };
65
66 return (
67 <form onSubmit={handleSubmit}>
68 <div className="form-group">
69 <label>
70 Password:
71 <input
72 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>
83
84 <div className="form-group">
85 <label>
86 Confirm Password:
87 <input
88 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>
99
100 <button type="submit" disabled={!state.isFormValid}>
101 Submit
102 </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.

react-form-persistence.tsx
1import { useUplinkController } from "@uplink-protocol/react";
2import { FormController } from "@uplink-protocol/form-controller";
3import { useEffect } from "react";
4
5export 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: true
17 },
18 email: {
19 id: 'email',
20 type: 'email',
21 label: 'Email Address',
22 value: ''
23 }
24 }
25 }
26 ]
27 }), { trackBindings: "all" });
28
29 // Load saved state on component mount
30 useEffect(() => {
31 try {
32 const savedState = localStorage.getItem('myForm');
33 if (savedState) {
34 const { timestamp, fields } = JSON.parse(savedState);
35
36 // 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 values
40 Object.entries(fields).forEach(([fieldId, value]) => {
41 methods.setValue(fieldId, value);
42 });
43
44 // Display notification to user
45 alert('Your previous form data has been restored.');
46 } else {
47 // Clear expired data
48 localStorage.removeItem('myForm');
49 }
50 }
51 } catch (error) {
52 console.error('Error loading saved form state:', error);
53 }
54 }, [methods]);
55
56 // Set up auto-save
57 useEffect(() => {
58 const handleStateChange = () => {
59 localStorage.setItem('myForm', JSON.stringify({
60 timestamp: Date.now(),
61 fields: state.values
62 }));
63 };
64
65 // Save state on changes
66 const unsubscribe = methods.subscribe(handleStateChange);
67
68 return () => {
69 unsubscribe();
70 };
71 }, [state.values, methods]);
72
73 const handleSubmit = (e: React.FormEvent) => {
74 e.preventDefault();
75 if (state.isFormValid) {
76 // Process form submission
77 console.log('Form submitted:', state.formData);
78
79 // Clear saved form data after successful submission
80 localStorage.removeItem('myForm');
81 }
82 };
83
84 return (
85 <form onSubmit={handleSubmit}>
86 <div className="form-group">
87 <label>
88 Full Name:
89 <input
90 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>
100
101 <div className="form-group">
102 <label>
103 Email:
104 <input
105 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>
112
113 <button type="submit" disabled={!state.isFormValid}>
114 Submit
115 </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.

validation-interface.ts
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:

Validator
Type
Description
required
Boolean
Ensures the field has a non-empty value
pattern
RegExp | string
Validates the value against a regular expression pattern
minLength
Number
Ensures the value has at least the specified number of characters
maxLength
Number
Ensures the value doesn't exceed the specified number of characters
min
Number
For numeric fields, ensures the value is at least the specified minimum
max
Number
For numeric fields, ensures the value doesn't exceed the specified maximum
custom
Function
A custom function that returns true for valid input or an error message string
basic-validation-examples.js
1// Required field validation
2const nameField = {
3 id: 'name',
4 type: 'text',
5 label: 'Full Name',
6 required: true, // Shorthand for validation: { required: true }
7 value: ''
8};
9
10// 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};
21
22// 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};
34
35// 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};
47
48// Custom validation function
49const 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 }
59
60 if (value.length < 3) {
61 return 'Username must be at least 3 characters long';
62 }
63
64 // Additional async check could be performed here if needed
65
66 return true; // Valid
67 }
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.

dynamic-validator-example.js
1// First, register a dynamic validator
2methods.registerValidator('requiredIf', (value, context) => {
3 const { dependsOn, dependsOnValue, errorMessage } =
4 context.field.validation.dynamicValidatorParams || {};
5
6 if (!dependsOn) return true;
7
8 const stepId = context.field.stepId;
9 const dependentValue = context.formData[stepId][dependsOn];
10
11 // If the condition is met and field is empty, return error message
12 if (dependentValue === dependsOnValue && (!value || value.trim() === '')) {
13 return errorMessage || 'This field is conditionally required';
14 }
15
16 return true;
17});
18
19// Then use the dynamic validator in a field configuration
20const 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: true
36 },
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.

async-validation.js
1// Register an async validator
2methods.registerValidator('usernameAvailable', async (value, context) => {
3 // Skip validation if empty (let required validator handle it)
4 if (!value) return true;
5
6 try {
7 // Show loading state during validation
8 methods.setFieldValidating(context.field.stepId, context.field.id, true);
9
10 // Make API call to check username availability
11 const response = await fetch(`/api/check-username?username=${encodeURIComponent(value)}`);
12 const data = await response.json();
13
14 // Return validation result
15 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 validation
21 methods.setFieldValidating(context.field.stepId, context.field.id, false);
22 }
23});
24
25// Use the async validator in field configuration
26const 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 calls
35 debounce: 500
36 }
37};
38
39// In your UI, show loading state
40return (
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:

react-validation.tsx
1import React from 'react';
2import { useUplinkController } from "@uplink-protocol/react";
3import { FormController } from "@uplink-protocol/form-controller";
4
5function 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: true
39 }
40 }
41 }
42 ]
43 }), {
44 trackBindings: "all" // For this example, we track all bindings since we use multiple fields
45 });
46
47 // Add custom cross-field validation
48 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 });
55
56 // Apply the validator to the confirm password field
57 methods.extendField('registration', 'confirmPassword', {
58 validation: {
59 dynamicValidator: 'passwordsMatch'
60 }
61 });
62
63 // Cleanup on unmount
64 return () => {
65 methods.unregisterValidator('passwordsMatch');
66 };
67 }, [methods]);
68
69 const handleSubmit = (e) => {
70 e.preventDefault();
71 methods.submit();
72 };
73
74 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 <input
82 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>
93
94 <div className="form-group">
95 <label htmlFor="password">
96 {state.fields.password.label}
97 {state.fields.password.required && ' *'}
98 </label>
99 <input
100 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>
111
112 <div className="form-group">
113 <label htmlFor="confirmPassword">
114 {state.fields.confirmPassword.label}
115 {state.fields.confirmPassword.required && ' *'}
116 </label>
117 <input
118 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>
129
130 <button
131 type="submit"
132 disabled={!state.isFormValid || !state.isFormDirty}
133 className="bg-blue-500 text-white py-2 px-4 rounded"
134 >
135 Register
136 </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:

  • 1
    Type Checking - Catch configuration errors at compile time rather than at runtime
  • 2
    IntelliSense - Get autocompletion and hints as you type, making development faster and reducing errors
  • 3
    Documentation - Access JSDoc comments directly in your IDE for better understanding of the API
  • 4
    Refactoring - Safely refactor your code with reliable type information

Basic TypeScript Setup

typed-form-config.ts
1import { FormController, FormConfig } from '@uplink-protocol/form-controller';
2
3// Define your form configuration with proper types
4const 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: true
15 } 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 Field
25 }
26 }
27 ]
28};
29
30// Initialize the form controller
31const form = FormController(formConfig);
32
33// TypeScript will provide proper typing for all form methods and properties
34const currentStep = form.state.currentStep.value; // string
35const isValid = form.state.isFormValid.value; // boolean
36const formData = form.state.formData.value; // Record<string, Record<string, any>>
37
38// Type checking for methods
39form.methods.updateField('contact', 'name', 'John Doe');
40form.methods.validateField('contact', 'email');
41
42// 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

react-typescript.tsx
1import React from 'react';
2import { useUplinkController } from "@uplink-protocol/react";
3import { FormController, FormConfig, Field } from "@uplink-protocol/form-controller";
4
5// Define your form data type
6interface 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}
17
18// Create a strongly typed form component
19function TypedForm() {
20 // Form configuration with type parameters
21 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: true
32 } 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: 120
50 }
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: true
74 } as Field<boolean>
75 }
76 }
77 ]
78 };
79
80 // Note the generic type parameter for proper type checking
81 const { state, methods } = useUplinkController<UserForm>(
82 () => FormController<UserForm>(formConfig)
83 );
84
85 // TypeScript now provides full type safety
86 const handleSubmit = (e: React.FormEvent) => {
87 e.preventDefault();
88 if (state.isFormValid) {
89 // Access with type safety
90 const name: string = state.values.name;
91 const age: number = state.values.age;
92 const theme: 'light' | 'dark' | 'system' = state.values.theme;
93
94 console.log('Form submitted with:', {
95 name, age, theme,
96 // Full form data is also correctly typed
97 formData: state.formData
98 });
99 }
100 };
101
102 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 <input
112 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>
122
123 {/* More fields... */}
124
125 <button
126 type="button"
127 onClick={() => methods.goToStep('preferences')}
128 disabled={!state.stepsValidity.personal}
129 >
130 Next
131 </button>
132 </div>
133 )}
134
135 {/* 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.

typed-event-handlers.ts
1import { FormController, FormConfig } from '@uplink-protocol/form-controller';
2
3const form = FormController(formConfig);
4
5// Type-safe subscription to form data changes
6form.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 properties
9 console.log('Name updated:', nameValue);
10});
11
12// Type-safe event handler for step changes
13form.state.currentStep.subscribe((stepId) => {
14 // stepId is properly typed as string
15 console.log('Current step:', stepId);
16
17 // You can safely use stepId in other methods
18 const isStepValid = form.state.stepsValidity.value[stepId];
19});
20
21// Type-safe form submission handler
22form.methods.onSubmit(() => {
23 if (form.state.isFormValid.value) {
24 // Type-safe access to all form data
25 const formData = form.state.formData.value;
26
27 // Safely access nested properties
28 const contactData = formData.contact;
29 const name = contactData?.name;
30 const email = contactData?.email;
31
32 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.

custom-form-model.ts
1import { FormController, FormConfig } from '@uplink-protocol/form-controller';
2
3// Define a custom form data model with specific types
4interface 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}
16
17// Create a typed form configuration
18const 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: true
29 } as Field<string>,
30 age: {
31 id: 'age',
32 type: 'number',
33 label: 'Age',
34 required: true,
35 validation: {
36 min: 18,
37 max: 120
38 }
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: true
51 } as Field<string>,
52 city: {
53 id: 'city',
54 type: 'text',
55 label: 'City',
56 required: true
57 } as Field<string>,
58 zipCode: {
59 id: 'zipCode',
60 type: 'text',
61 label: 'ZIP/Postal Code',
62 required: true
63 } 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};
79
80// Initialize with the generic type
81const form = FormController<UserFormData>(formConfig);
82
83// Now all methods and state are strongly typed to your data model
84form.methods.updateField('personal', 'age', 30); // Type safe - age is number
85form.methods.updateField('address', 'zipCode', '10001'); // Type safe - zipCode is string
86
87// This would cause TypeScript errors:
88// form.methods.updateField('personal', 'nonExistentField', 'value'); // Error: 'nonExistentField' doesn't exist
89// form.methods.updateField('personal', 'age', 'thirty'); // Error: 'thirty' is not assignable to type 'number'
90
91// Strongly-typed access to form data
92form.state.formData.subscribe((formData) => {
93 // Fully typed as UserFormData
94 const age: number = formData.personal.age; // Properly typed as number
95 const country: string = formData.address.country; // Properly typed as string
96
97 // TypeScript will catch errors:
98 // const invalid = formData.personal.invalidProperty; // Error: Property 'invalidProperty' does not exist
99});

Advanced TypeScript Tips

  • Create type-safe custom validator functions by defining proper parameter and return types
  • Use TypeScript utility types like Partial, Pick, and Omit 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

  • FC
    FormController

    The main entry point that initializes services and exposes the public API

  • BS
    BaseService

    Foundation for reactive state management across all services

  • CS
    ConfigService

    Manages form configuration and structure

  • FS
    FieldService

    Handles field operations and validation

  • FS
    FormService

    Manages form data and overall form state

  • IS
    InteractionService

    Handles user interactions and events

  • SS
    StepperService

    Manages multi-step navigation

  • VS
    ValidatorService

    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. 1.User action triggers a method call
  2. 2.Method updates service state
  3. 3.Service notifies subscribers about state change
  4. 4.Binding callbacks are triggered with new values
  5. 5.UI components react to binding changes

Architecture Diagram

User Interface Layer

React Components
Vue Components
Svelte Components

Public API Layer

State (Reactive Properties)
Methods (Public Functions)

Form Controller

Orchestrates Services & Manages Form Lifecycle

Service Layer

ConfigService
FormService
FieldService
ValidatorService
StepperService
InteractionService
BaseService

Implementation Example

reactive-updates.js
1// Example of how reactive updates flow through the system
2import { FormController } from '@uplink-protocol/form-controller';
3
4// Initialize form controller with config
5const 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: true
16 },
17 email: {
18 id: 'email',
19 type: 'email',
20 label: 'Email Address',
21 required: true
22 }
23 }
24 }
25 ]
26});
27
28// 1. Subscribe to form data changes in the UI
29form.state.formData.subscribe((formData) => {
30 console.log('Form data updated:', formData);
31 // Update UI components with new data
32});
33
34// 2. User updates a field via the public API
35form.methods.updateField('personal', 'name', 'John Doe');
36
37// Internal flow (simplified):
38// - updateField() method is called on FormController
39// - FormController delegates to FieldService.updateField()
40// - FieldService updates internal state
41// - FieldService notifies FormService of the change
42// - FormService updates its formData state
43// - FormService notifies subscribers (the binding)
44// - The binding callback is triggered with the new value
45// - UI component reacts to the binding change
46
47// Additional automatic steps:
48// - ValidatorService is notified to validate the field
49// - If validation fails, fieldErrors state is updated
50// - 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

Share this page