Best Practices
Guidelines for designing effective, maintainable forms with the Vyuh form system.
Form Design
Keep Forms Focused
Each form should have a single purpose. If a form grows beyond 15-20 fields, consider splitting it into a StepForm with logical steps.
// Instead of one massive form, use steps
StepForm(
title: 'Student Registration',
steps: [
FormBuilder(title: 'Personal Info').text(...).build(),
FormBuilder(title: 'Academic Background').select(...).build(),
FormBuilder(title: 'Course Selection').reference(...).build(),
],
)Use Sections for Visual Grouping
Group related fields into sections with descriptive titles:
.section('Contact Information', (s) => s
.text('email', title: 'Email')
.phone('phone', title: 'Phone'),
description: 'How we can reach you',
)Leverage the 12-Column Grid
Use column spans intentionally. Related fields of similar importance should share a row:
// Good: related fields side-by-side
.text('first_name', title: 'First Name')
.text('last_name', title: 'Last Name')
// Good: address in a natural layout
.text('street', title: 'Street Address') // full width
.text('city', title: 'City')
.select('state', title: 'State')
.text('zip', title: 'ZIP Code')Naming Conventions
Field Names
Use snake_case for field names. Names must be unique across the entire form (including inside sections):
// Good
.text('student_name', title: 'Student Name')
.text('emergency_contact_phone', title: 'Emergency Phone')
// Avoid: camelCase (inconsistent with JSON conventions)
.text('studentName', title: 'Student Name')Consistent Titles
Titles should be concise, user-facing labels:
// Good
.text('email', title: 'Email Address')
.dateTime('start_date', title: 'Start Date')
// Avoid: developer jargon
.text('email', title: 'Email String Value')Validation
Order Matters
Validations run in order. Put the most common failure first:
// Good: required check first, then format
.text('email', title: 'Email')
.required('Email is required')
.email('Enter a valid email')
// Less good: format check on empty field wastes time
.text('email', title: 'Email')
.email('Enter a valid email')
.required('Email is required')Helpful Error Messages
Write error messages that tell the user what to do, not just what went wrong:
// Good: actionable
.required('Please enter your email address')
.email('Please enter a valid email (e.g., name@example.com)')
.minLength(8, message: 'Password must be at least 8 characters')
// Less helpful: just states the rule
.required('Required')
.email('Invalid email')
.minLength(8)Use Soft Validation for Recommendations
When a value is unusual but not invalid, use soft validation:
NumberField(
name: 'class_size',
title: 'Class Size',
validations: [
NumberRangeValidation(min: 1, max: 500, errorMessage: 'Must be 1-500'),
SoftRangeValidation(softMin: 10, softMax: 40,
warningMessage: 'Recommended range is 10-40'),
],
)Conditional Logic
Prefer Rules over Manual State Management
Use the rules system instead of managing visibility/enabled state manually:
// Good: declarative rule
.text('payment_info', title: 'Payment Info')
.showWhen(When.fieldEquals('type', 'paid'))
// Avoid: imperative state management
// if (typeController.value == 'paid') showPaymentField();Keep Conditions Simple
Complex conditions should be broken into named intermediate steps when possible. Use CompoundCondition sparingly:
// Acceptable: clear AND condition
.showWhen(When.allOf([
When.fieldEquals('type', 'paid'),
When.fieldTrue('is_international'),
]))
// Consider simplifying if logic gets deeper than 2 levelsPerformance
Dispose Forms
Always dispose forms when the widget is removed:
@override
void dispose() {
form.dispose();
super.dispose();
}Avoid Unnecessary Rebuilds
Use MobX Observer widgets to isolate rebuilds to the parts of the UI that actually change:
Observer(
builder: (_) => ElevatedButton(
onPressed: form.valid.value ? _submit : null,
child: Text('Submit'),
),
)Pre-populate at Construction Time
Set initial values during build() rather than after construction to avoid unnecessary re-validation:
// Good: values applied before listeners are wired
final form = FormBuilder(title: 'Edit').text(...).build(
initialValues: {'name': 'Alice'},
);
// Less efficient: triggers re-validation after construction
final form = FormBuilder(title: 'Edit').text(...).build();
form.initialValues = {'name': 'Alice'};Form Editor
Validate Before Export
Always validate the form definition before exporting:
final errors = store.validate();
if (errors.isNotEmpty) {
// Show validation errors to the designer
return;
}
final json = store.toJson();Use Descriptors for Configuration
Prefer FormEditorDescriptor.withDefaults() over manual registry construction:
// Good: declarative, composable
final store = FormEditorStore.fromDescriptors([
FormEditorDescriptor.withDefaults(
fieldTypeDescriptors: [MyCustomFieldDescriptor()],
),
]);
// More verbose: manual registry
final store = FormEditorStore.withRegistry(
registry: FormEditorRegistry(
fieldTypes: [...allDefaults, ...custom],
// Must remember all defaults
),
);Next Steps
- Enrollment Form -- Complete example
- Architecture -- System design
- FormBuilder DSL -- DSL reference