Building Forms
This guide walks through building a complete course settings form with groups, validation, conditions, derivations, and serialization.
Goal
Create a settings panel for an LMS course with:
- Basic info (title, description, slug auto-derived from title)
- Schedule settings (duration, start date)
- Enrollment settings (type, capacity, waitlist -- conditionally visible)
- Display options (featured, tags)
Step 1: Define Enums and Options
dart
import 'package:vyuh_property_system/vyuh_property_system.dart';
enum CourseLevel { beginner, intermediate, advanced }
enum EnrollmentType { open, limited, invitation }Step 2: Build the Property Collection
dart
PropertyCollection buildCourseSettings() {
final b = PropertyCollectionBuilder();
// ── Basic Info ──────────────────────────────────────────
b.group('basic', 'Basic Information');
b.string('title', 'Course Title',
required: true,
validators: [
PropertyValidators.minLength(3),
PropertyValidators.maxLength(200),
],
help: 'The display title for this course',
);
b.string('slug', 'URL Slug',
required: true,
validators: [PropertyValidators.identifier()],
help: 'Auto-generated from title. Edit to override.',
);
// Auto-derive slug from title
b.derivedProperty('slug',
from: {'title'},
transformer: (values) =>
PropertyValidators.toIdentifier(values['title'] as String?),
);
b.string('description', 'Description',
help: 'Brief overview of the course content',
);
b.enumeration<CourseLevel>('level', 'Course Level',
options: [
EnumOption(CourseLevel.beginner, 'Beginner',
description: 'No prerequisites required'),
EnumOption(CourseLevel.intermediate, 'Intermediate'),
EnumOption(CourseLevel.advanced, 'Advanced',
description: 'Requires prior experience'),
],
defaultValue: CourseLevel.beginner,
);
// ── Schedule ────────────────────────────────────────────
b.group('schedule', 'Schedule');
b.integer('duration', 'Duration (hours)',
defaultValue: 1,
validators: [
PropertyValidators.min(1),
PropertyValidators.max(500),
],
);
b.dateTime('startDate', 'Start Date');
// ── Enrollment ──────────────────────────────────────────
b.group('enrollment', 'Enrollment Settings');
b.enumeration<EnrollmentType>('enrollmentType', 'Enrollment Type',
options: [
EnumOption(EnrollmentType.open, 'Open Enrollment'),
EnumOption(EnrollmentType.limited, 'Limited Capacity'),
EnumOption(EnrollmentType.invitation, 'Invitation Only'),
],
defaultValue: EnrollmentType.open,
);
// Capacity: only visible when enrollment is limited
b.integer('capacity', 'Max Capacity',
defaultValue: 30,
validators: [PropertyValidators.min(1)],
visibleCondition: PropertyCollectionBuilder.when<EnrollmentType>(
'enrollmentType', EnrollmentType.limited,
),
);
// Waitlist: only visible when limited AND capacity > 0
b.boolean('enableWaitlist', 'Enable Waitlist',
defaultValue: false,
visibleCondition: PropertyCollectionBuilder.whenAll([
PropertyCollectionBuilder.when<EnrollmentType>(
'enrollmentType', EnrollmentType.limited,
),
PropertyCollectionBuilder.whenGreaterThan<int>('capacity', 0),
]),
);
// Invitation code: only visible when invitation type
b.string('invitationCode', 'Invitation Code',
required: true,
visibleCondition: PropertyCollectionBuilder.when<EnrollmentType>(
'enrollmentType', EnrollmentType.invitation,
),
);
// ── Display ─────────────────────────────────────────────
b.group('display', 'Display Options', initiallyCollapsed: true);
b.boolean('featured', 'Feature on Homepage', defaultValue: false);
b.list<String>('tags', 'Tags',
help: 'Keywords for search and categorization',
);
return b.buildCollection();
}Step 3: Render the Form
dart
class CourseSettingsForm extends StatefulWidget {
const CourseSettingsForm({super.key});
@override
State<CourseSettingsForm> createState() => _CourseSettingsFormState();
}
class _CourseSettingsFormState extends State<CourseSettingsForm> {
late final PropertyCollection _collection;
@override
void initState() {
super.initState();
_collection = buildCourseSettings();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: PropertyCollectionEditor(
collection: _collection,
showGroups: true,
disposeCollection: false, // we manage disposal
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
spacing: 8,
children: [
OutlinedButton(
onPressed: _reset,
child: const Text('Reset'),
),
FilledButton(
onPressed: _save,
child: const Text('Save'),
),
],
),
),
],
);
}
void _reset() {
_collection.resetAll();
setState(() {}); // rebuild to reflect reset
}
void _save() {
final errors = _collection.validateAll();
if (errors.isNotEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Please fix: ${errors.values.join(", ")}'),
),
);
return;
}
final json = _collection.toJson();
// Send to API, save to storage, etc.
print(json);
}
@override
void dispose() {
_collection.dispose();
super.dispose();
}
}Step 4: Load Existing Data
To edit an existing course, restore values from JSON after building the collection:
dart
final collection = buildCourseSettings();
// Restore from saved data
collection.fromJson({
'title': 'Flutter Fundamentals',
'slug': 'flutter_fundamentals',
'description': 'Learn Flutter from scratch',
'level': 'beginner',
'duration': 40,
'enrollmentType': 'limited',
'capacity': 50,
'enableWaitlist': true,
'featured': true,
'tags': ['flutter', 'dart', 'mobile'],
});
// Prevent auto-derivation from overwriting the loaded slug
collection.markDerivationsAsDirty();Step 5: Listen for Changes
React to property changes in real time:
dart
// Stream of all value changes
collection.valueChanges.listen((values) {
print('Form changed: $values');
});
// Check if form is dirty
if (collection.hasChanges) {
// Show "unsaved changes" warning
}Form Architecture Diagram
Next Steps
- Custom Editors -- Create custom property editors
- Dynamic Behavior -- Conditions and derivations in depth
- Course Settings Example -- Full reference implementation