Skip to content

Example: Course Settings

A complete course configuration panel for a Learning Management System, demonstrating groups, conditions, derivations, validation, and serialization.

Domain Model

FieldTypeDescription
titleStringCourse display name
slugStringURL-safe identifier (auto-derived from title)
descriptionStringBrief overview
levelEnumbeginner / intermediate / advanced
durationIntDuration in hours
startDateDateTimeWhen the course begins
enrollmentTypeEnumopen / limited / invitation
capacityIntMax students (only when limited)
enableWaitlistBoolWaitlist enabled (only when limited + capacity > 0)
invitationCodeStringRequired code (only when invitation)
prerequisitesList<String>Required prior courses
schedulingModeUnionfixed / flexible / self-paced (each with sub-properties)
featuredBoolShow on homepage
tagsList<String>Search keywords
createdByReadOnly<String>System field

Enums

dart
enum CourseLevel { beginner, intermediate, advanced }
enum EnrollmentType { open, limited, invitation }

Full Implementation

dart
import 'package:vyuh_property_system/vyuh_property_system.dart';

PropertyCollection buildCourseSettings({String? createdBy}) {
  final b = PropertyCollectionBuilder();

  // ── Basic Information ──────────────────────────────────────
  b.group('basic', 'Basic Information');

  b.string('title', 'Course Title',
    required: true,
    validators: [
      PropertyValidators.minLength(3),
      PropertyValidators.maxLength(200),
    ],
    help: 'The display name shown to students',
  );

  b.string('slug', 'URL Slug',
    required: true,
    validators: [PropertyValidators.identifier()],
    help: 'Auto-generated from title. Edit to customize.',
  );

  b.derivedProperty('slug',
    from: {'title'},
    transformer: (values) =>
      PropertyValidators.toIdentifier(values['title'] as String?),
  );

  b.string('description', 'Description',
    help: 'Brief overview of the course content (shown in listings)',
    validators: [PropertyValidators.maxLength(500)],
  );

  b.enumeration<CourseLevel>('level', 'Course Level',
    options: [
      EnumOption(CourseLevel.beginner, 'Beginner',
        description: 'No prior knowledge required'),
      EnumOption(CourseLevel.intermediate, 'Intermediate',
        description: 'Some experience recommended'),
      EnumOption(CourseLevel.advanced, 'Advanced',
        description: 'Requires solid foundation'),
    ],
    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');

  // Scheduling mode: union of fixed / flexible / self-paced
  // (UnionProperty is not available via builder, so we add it manually)

  // ── Enrollment ─────────────────────────────────────────────
  b.group('enrollment', 'Enrollment Settings');

  b.enumeration<EnrollmentType>('enrollmentType', 'Enrollment Type',
    options: [
      EnumOption(EnrollmentType.open, 'Open Enrollment',
        description: 'Anyone can enroll'),
      EnumOption(EnrollmentType.limited, 'Limited Capacity',
        description: 'First-come, first-served up to capacity'),
      EnumOption(EnrollmentType.invitation, 'Invitation Only',
        description: 'Requires an invitation code'),
    ],
    defaultValue: EnrollmentType.open,
  );

  b.integer('capacity', 'Max Capacity',
    defaultValue: 30,
    validators: [PropertyValidators.min(1), PropertyValidators.max(10000)],
    visibleCondition: PropertyCollectionBuilder.when<EnrollmentType>(
      'enrollmentType', EnrollmentType.limited,
    ),
  );

  b.boolean('enableWaitlist', 'Enable Waitlist',
    defaultValue: false,
    help: 'Allow students to join a waitlist when the course is full',
    visibleCondition: PropertyCollectionBuilder.whenAll([
      PropertyCollectionBuilder.when<EnrollmentType>(
        'enrollmentType', EnrollmentType.limited,
      ),
      PropertyCollectionBuilder.whenGreaterThan<int>('capacity', 0),
    ]),
  );

  b.string('invitationCode', 'Invitation Code',
    required: true,
    validators: [PropertyValidators.minLength(6)],
    help: 'Students must enter this code to enroll',
    visibleCondition: PropertyCollectionBuilder.when<EnrollmentType>(
      'enrollmentType', EnrollmentType.invitation,
    ),
  );

  // ── Content ────────────────────────────────────────────────
  b.group('content', 'Content');

  b.list<String>('prerequisites', 'Prerequisites',
    help: 'Course titles that should be completed before this one',
  );

  b.list<String>('tags', 'Tags',
    help: 'Keywords for search and categorization',
  );

  // ── Display ────────────────────────────────────────────────
  b.group('display', 'Display Options', initiallyCollapsed: true);

  b.boolean('featured', 'Feature on Homepage',
    defaultValue: false,
    help: 'Show this course prominently on the main page',
  );

  // ── System ─────────────────────────────────────────────────
  b.group('system', 'System',
    collapsible: false,
    showHeader: true,
  );

  b.readonly<String>('createdBy', 'Created By',
    defaultValue: createdBy ?? 'system',
  );

  return b.buildCollection();
}

Adding UnionProperty

For the scheduling mode, add a UnionProperty directly to the collection:

dart
PropertyCollection buildCourseSettingsWithScheduling() {
  final collection = buildCourseSettings();

  // Add scheduling mode as a union property
  final scheduling = UnionProperty(
    key: 'schedulingMode',
    label: 'Scheduling Mode',
    group: 'schedule',
    defaultSelectedKey: 'fixed',
    options: [
      UnionOption(
        key: 'fixed',
        title: 'Fixed Schedule',
        description: 'Course runs on specific dates',
        property: StringProperty(
          key: 'fixedDates',
          label: 'Schedule Description',
          help: 'e.g., "Mon/Wed/Fri 10:00-12:00"',
        ),
      ),
      UnionOption(
        key: 'flexible',
        title: 'Flexible',
        description: 'Complete within a time window',
        property: IntProperty(
          key: 'completionDays',
          label: 'Days to Complete',
          defaultValue: 30,
          validators: [PropertyValidators.min(1)],
        ),
      ),
      UnionOption(
        key: 'selfPaced',
        title: 'Self-Paced',
        description: 'No time constraints',
        property: BoolProperty(
          key: 'withMentoring',
          label: 'With Mentoring',
          defaultValue: false,
        ),
      ),
    ],
  );

  collection.addProperty(scheduling);
  return collection;
}

Usage in a Widget

dart
class CourseSettingsScreen extends StatefulWidget {
  final Map<String, dynamic>? existingData;

  const CourseSettingsScreen({super.key, this.existingData});

  @override
  State<CourseSettingsScreen> createState() => _CourseSettingsScreenState();
}

class _CourseSettingsScreenState extends State<CourseSettingsScreen> {
  late final PropertyCollection _collection;

  @override
  void initState() {
    super.initState();
    _collection = buildCourseSettings(createdBy: 'admin');

    if (widget.existingData != null) {
      _collection.fromJson(widget.existingData!);
      _collection.markDerivationsAsDirty();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Course Settings'),
        actions: [
          TextButton(
            onPressed: _save,
            child: const Text('Save'),
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: PropertyCollectionEditor(
          collection: _collection,
          showGroups: true,
          disposeCollection: false,
        ),
      ),
    );
  }

  void _save() {
    final errors = _collection.validateAll();
    if (errors.isNotEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(errors.values.first)),
      );
      return;
    }

    final json = _collection.toJson();
    // POST to API
    print('Saving: $json');
  }

  @override
  void dispose() {
    _collection.dispose();
    super.dispose();
  }
}

Sample JSON Output

json
{
  "title": "Flutter Fundamentals",
  "slug": "flutter_fundamentals",
  "description": "Learn Flutter from the ground up",
  "level": "beginner",
  "duration": 40,
  "startDate": "2026-04-01T09:00:00.000",
  "enrollmentType": "limited",
  "capacity": 50,
  "enableWaitlist": true,
  "invitationCode": "",
  "prerequisites": ["Dart Basics"],
  "tags": ["flutter", "mobile", "dart"],
  "featured": true,
  "createdBy": "admin"
}

Next Steps