Skip to content

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