Fifty Forms

pub package License: MIT

Production-ready form building with validation, multi-step wizards, and draft persistence. Part of Fifty Flutter Kit.

Home Login Form Registration Multi-Step

Features

  • State Management - FiftyFormController for centralized form state
  • Immutable Field State - FieldState tracks value, touched, dirty, error states
  • 25 Built-in Validators - Required, Email, MinLength, Pattern, and more
  • Async Validation - Debounced async validators for server-side checks
  • FDL Components - Form field wrappers for fifty_ui components
  • Multi-Step Forms - Wizard-style forms with step validation
  • Dynamic Arrays - Add/remove repeating field groups
  • Draft Persistence - Auto-save and restore form data

Installation

dependencies:
  fifty_forms: ^0.1.2

For Contributors

dependencies:
  fifty_forms:
    path: ../fifty_forms

Dependencies: fifty_tokens, fifty_theme, fifty_ui, fifty_storage, get_storage


Quick Start

final controller = FiftyFormController(
  initialValues: {'email': '', 'password': ''},
  validators: {
    'email': [Required(), Email()],
    'password': [Required(), MinLength(8)],
  },
);

Column(
  children: [
    FiftyTextFormField(
      name: 'email',
      controller: controller,
      label: 'Email',
      keyboardType: TextInputType.emailAddress,
    ),
    FiftyTextFormField(
      name: 'password',
      controller: controller,
      label: 'Password',
      obscureText: true,
    ),
    FiftySubmitButton(
      controller: controller,
      label: 'LOGIN',
      onPressed: () => controller.submit((values) async {
        await api.login(values['email'], values['password']);
      }),
    ),
  ],
)

Architecture

fifty_forms
+-- core/
|   +-- FiftyFormController   # Central state manager
|   +-- FieldState            # Immutable per-field state
+-- validators/
|   +-- Validator             # Sync validator base
|   +-- AsyncValidator        # Async validator with debounce
|   +-- Built-ins             # Required, Email, MinLength, etc.
+-- fields/
|   +-- FiftyTextFormField    # fifty_ui field wrappers
|   +-- FiftyDropdownFormField
|   +-- FiftyCheckboxFormField (+ others)
+-- widgets/
|   +-- FiftyForm             # Form container
|   +-- FiftySubmitButton     # Submit with loading state
|   +-- FiftyMultiStepForm    # Wizard container
|   +-- FiftyFormArray        # Dynamic repeating fields
|   +-- FiftyValidationSummary
+-- models/
|   +-- FormStep              # Step definition for wizards
|   +-- FormStatus            # Form lifecycle enum
+-- persistence/
    +-- DraftManager          # Auto-save via GetStorage

Core Components

Component Description
FiftyFormController Central state manager: values, validation, submission
FieldState<T> Immutable container for a single field's state
FormStatus Enum: idle, validating, submitting, submitted, error
Validator Composable synchronous validator base class
AsyncValidator Asynchronous validator with debounce support
DraftManager Persists and restores form drafts via GetStorage
FiftyMultiStepForm Wizard-style multi-step form widget
FiftyFormArray Dynamic add/remove repeating field groups

API Reference

FiftyFormController

The central state manager for forms. Handles field registration, value tracking, validation (sync and async), and form submission.

final controller = FiftyFormController(
  initialValues: {'name': '', 'age': 0},
  validators: {
    'name': [Required(), MinLength(2)],
    'age': [Required(), Min(18)],
  },
  onValidationChanged: (isValid) => print('Valid: $isValid'),
);

// Get/set values
controller.setValue('name', 'John');
final name = controller.getValue<String>('name');

// Check state
controller.isValid;      // All fields valid?
controller.isDirty;      // Any field changed?
controller.isValidating; // Async validation running?

// Field operations
controller.registerField('newField', initialValue: '');
controller.unregisterField('fieldToRemove');
controller.markTouched('name');
controller.markAllTouched();

// Actions
await controller.validate();          // Validate all fields
await controller.validateField('email'); // Validate single field
controller.clearErrors();             // Clear all validation errors
controller.reset();                   // Reset to initial values
controller.clear();                   // Clear all values

// Submit
await controller.submit((values) async {
  await api.save(values);
});

// Cleanup
controller.dispose();

Key Properties:

Property Type Description
status FormStatus Current form lifecycle status
isValid bool All fields valid and not validating
isDirty bool Any field changed from initial value
isValidating bool Async validation in progress
values Map<String, dynamic> All current field values
errors Map<String, String> All fields with errors
fieldNames List<String> All registered field names

FieldState

Immutable state container for a form field. Tracks the current value, validation error, and interaction state.

class FieldState<T> {
  final T? value;           // Current value
  final String? error;      // Validation error
  final bool isTouched;     // Field has been focused
  final bool isDirty;       // Value differs from initial
  final bool isValidating;  // Async validation running

  bool get isValid => error == null && !isValidating;
  bool get hasError => error != null;
}

// Usage
final state = controller.getFieldState('email');
if (state.isTouched && state.hasError) {
  print(state.error);
}

FormStatus

Enum representing the form lifecycle status:

Status Description
idle Default state, ready for input
validating Validation in progress
submitting Form submission in progress
submitted Form successfully submitted
error Validation or submission failed

Validators

String Validators:

Validator Description Example
Required() Non-null, non-empty Required(message: 'Required')
MinLength(n) Minimum length MinLength(8)
MaxLength(n) Maximum length MaxLength(100)
Email() Valid email format Email()
Url() Valid URL format Url()
Pattern(regex) Matches pattern Pattern(RegExp(r'^[A-Z]+$'))
AlphaNumeric() Letters and numbers only AlphaNumeric()

Number Validators:

Validator Description Example
Min(n) Minimum value Min(0)
Max(n) Maximum value Max(100)
Range(min, max) Within range (inclusive) Range(1, 10)
Integer() Must be integer Integer()
Positive() Must be positive (> 0) Positive()

Date Validators:

Validator Description Example
MinDate(date) On or after date MinDate(DateTime.now())
MaxDate(date) On or before date MaxDate(DateTime(2030))
MinAge(years) Minimum age MinAge(18)
FutureDate() Must be future FutureDate()
PastDate() Must be past PastDate()

Password Validators:

Validator Description Example
HasUppercase() Contains uppercase HasUppercase()
HasLowercase() Contains lowercase HasLowercase()
HasNumber() Contains digit HasNumber()
HasSpecialChar() Contains special char HasSpecialChar()

Comparison Validators:

Validator Description Example
Equals(field) Equals another field Equals('password')
NotEquals(field) Differs from field NotEquals('oldPassword')

Form Fields

Wrapper components for fifty_ui widgets that integrate with FiftyFormController:

Field Wraps Use Case
FiftyTextFormField FiftyTextField Text input
FiftyDropdownFormField FiftyDropdown Selection from list
FiftyCheckboxFormField FiftyCheckbox Boolean toggle
FiftySwitchFormField FiftySwitch On/off toggle
FiftyRadioFormField FiftyRadioGroup Single selection from options
FiftySliderFormField FiftySlider Numeric range selection
FiftyDateFormField Date picker Date input
FiftyTimeFormField Time picker Time input
FiftyFileFormField File picker File upload

UI Widgets

Widget Description
FiftyForm Form container with controller binding
FiftySubmitButton Submit button with loading state
FiftyFormProgress Step progress indicator
FiftyMultiStepForm Multi-step wizard container
FiftyFormArray Dynamic repeating fields
FiftyFormError Form-level error display
FiftyFieldError Field-level error display
FiftyValidationSummary All errors summary
FiftyFormField Generic field wrapper

Configuration

FiftyFormController

Parameter Type Default Description
initialValues Map<String, dynamic> required Initial field values keyed by field name
validators Map<String, List<Validator>> {} Sync validators per field
asyncValidators Map<String, List<AsyncValidator>> {} Async validators per field
onValidationChanged void Function(bool)? null Callback when overall validity changes
validateOnChange bool true Re-validate fields when values change

FiftyMultiStepForm

Parameter Type Default Description
controller FiftyFormController required Form controller instance
steps List<FormStep> required Step definitions
stepBuilder Widget Function(BuildContext, int, FormStep) required Builder for each step's content
onComplete void Function(Map<String, dynamic>) required Callback on final step completion
onStepChanged void Function(int)? null Callback when active step changes
showProgress bool true Show step progress indicator
validateOnNext bool true Validate current step before advancing
nextLabel String 'NEXT' Label for the next button
previousLabel String 'BACK' Label for the previous button
completeLabel String 'COMPLETE' Label for the final step button

FormStep

Parameter Type Default Description
title String required Step title for progress indicator
description String? null Optional step description
fields List<String> required Field names to validate for this step
isOptional bool false Can skip if all fields empty
validator String? Function(Map<String, dynamic>)? null Custom step-level validation

DraftManager

Parameter Type Default Description
controller FiftyFormController required Form controller to manage
key String required Unique storage key for draft
debounce Duration 2 seconds Delay before auto-save triggers
containerName String? 'fifty_forms_drafts' GetStorage container name

FiftyFormArray

Parameter Type Default Description
controller FiftyFormController required Form controller instance
name String required Base name for array fields
minItems int 0 Minimum items required
maxItems int 10 Maximum items allowed
initialCount int 1 Initial number of items
animate bool true Animate add/remove transitions
itemSpacing double 16 Spacing between items
itemBuilder Widget Function(BuildContext, int, VoidCallback) required Builder for each array item
addButtonBuilder Widget Function(VoidCallback)? null Custom add button builder

Usage Patterns

Strong Password Validation

validators: {
  'password': [
    Required(message: 'Password is required'),
    MinLength(8, message: 'At least 8 characters'),
    HasUppercase(message: 'Must contain uppercase letter'),
    HasLowercase(message: 'Must contain lowercase letter'),
    HasNumber(message: 'Must contain a number'),
    HasSpecialChar(message: 'Must contain special character'),
  ],
}

Password Confirmation

validators: {
  'password': [Required(), MinLength(8)],
  'confirmPassword': [Required(), Equals('password', message: 'Passwords must match')],
}

Custom Validators

// Synchronous
Custom<String>((value) {
  if (value?.contains('banned') == true) {
    return 'Contains banned word';
  }
  return null;
})

// Asynchronous with debounce
AsyncCustom<String>(
  (value) async {
    final exists = await api.checkUsername(value);
    return exists ? 'Username taken' : null;
  },
  debounce: Duration(milliseconds: 500),
)

Composite Validators

// And -- all must pass
And([Required(), MinLength(8), HasUppercase()])

// Or -- at least one must pass
Or([Email(), Pattern(phoneRegex)])

Multi-Step Form

FiftyMultiStepForm(
  controller: controller,
  steps: [
    FormStep(
      title: 'Account',
      description: 'Create your credentials',
      fields: ['email', 'password'],
    ),
    FormStep(
      title: 'Profile',
      fields: ['name', 'bio'],
      isOptional: true,
    ),
    FormStep(
      title: 'Review',
      fields: [],
      validator: (values) {
        if (values['bio']?.isEmpty == true) {
          return 'Consider adding a bio';
        }
        return null;
      },
    ),
  ],
  stepBuilder: (context, index, step) => _buildStep(index),
  onComplete: (values) => api.createUser(values),
  onStepChanged: (step) => print('Now on step $step'),
  showProgress: true,
  validateOnNext: true,
  nextLabel: 'NEXT',
  previousLabel: 'BACK',
  completeLabel: 'COMPLETE',
)

Dynamic Form Arrays

FiftyFormArray(
  controller: controller,
  name: 'addresses',
  minItems: 1,
  maxItems: 5,
  itemBuilder: (context, index, remove) => Column(
    children: [
      FiftyTextFormField(
        name: 'addresses[$index].street',
        controller: controller,
        label: 'Street',
      ),
      FiftyTextFormField(
        name: 'addresses[$index].city',
        controller: controller,
        label: 'City',
      ),
      IconButton(
        icon: Icon(Icons.delete),
        onPressed: remove,
      ),
    ],
  ),
  addButtonBuilder: (add) => FiftyButton(
    label: 'Add Address',
    onPressed: add,
    variant: FiftyButtonVariant.ghost,
    icon: Icons.add,
  ),
)

Accessing Array Values:

// Get single field
final street = controller.getArrayValue<String>('addresses', 0, 'street');

// Set single field
controller.setArrayValue('addresses', 0, 'city', 'New York');

// Get all items as list of maps
final addresses = controller.getArrayValues('addresses');
// Returns: [{'street': '123 Main', 'city': 'NYC'}, ...]

// Get array length
final count = controller.getArrayLength('addresses');

// Remove item (shifts subsequent indices)
controller.removeArrayItem('addresses', 1);

Draft Persistence

// Initialize storage once at app start
await DraftManager.initStorage();

// Create draft manager
final draftManager = DraftManager(
  controller: controller,
  key: 'registration_form',
  debounce: Duration(seconds: 2),
);

// Start auto-save
draftManager.start();

// Check for existing draft
if (await draftManager.hasDraft()) {
  final restored = await draftManager.restoreDraft();
  if (restored != null) {
    showSnackBar('Draft restored');
  }
}

// Manual save
await draftManager.saveDraft();

// Clear after successful submission
await controller.submit((values) async {
  await api.save(values);
  await draftManager.clearDraft();
});

// Stop auto-save (keeps draft)
draftManager.stop();

// Cleanup
draftManager.dispose();

FiftySubmitButton

FiftySubmitButton(
  controller: controller,
  label: 'SUBMIT',
  icon: Icons.send,
  loadingText: 'SAVING...',
  onPressed: () => controller.submit((values) async {
    await api.save(values);
  }),
  disableWhenInvalid: true,
  expanded: true,
  variant: FiftyButtonVariant.primary,
)

Platform Support

Platform Support Notes
Android Yes
iOS Yes
macOS Yes
Linux Yes
Windows Yes
Web Yes

Fifty Design Language Integration

This package is part of Fifty Flutter Kit:

  • fifty_tokens - All spacing, radius, and typography values sourced from design tokens
  • fifty_ui - Form field widgets wrap FDL components (FiftyTextField, FiftyDropdown, FiftyCheckbox, etc.)
  • fifty_theme - Consumes theming from the FDL theme system; no custom theme classes defined
  • fifty_storage - Draft persistence layer integrates with the FDL storage abstraction

Version

Current: 0.1.2


License

MIT License - see LICENSE for details.

Part of Fifty Flutter Kit.

Libraries

fifty_forms
Fifty Forms - Production-ready form building for Flutter.