Skip to content

Custom Editors

When the built-in editors are insufficient, you can create custom PropertyEditor implementations for your own types and register them with PropertySystem.

The PropertyEditor Interface

dart
abstract interface class PropertyEditor<T> {
  Widget createEditor({
    required BuildContext context,
    required PropertyControl<T> control,
    required String label,
    required String name,
    String? help,
  });
}

The editor receives:

ParameterDescription
contextThe BuildContext for theme and media query access
controlThe PropertyControl<T> for reading/writing the value
labelDisplay label from the property
nameUnique field name for form registration
helpOptional help text

Creating a Custom Editor

Example: Color Picker Editor

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

class ColorPropertyEditor implements PropertyEditor<String> {
  const ColorPropertyEditor();

  @override
  Widget createEditor({
    required BuildContext context,
    required PropertyControl<String> control,
    required String label,
    required String name,
    String? help,
  }) {
    final colors = {
      'red': Colors.red,
      'blue': Colors.blue,
      'green': Colors.green,
      'orange': Colors.orange,
      'purple': Colors.purple,
    };

    return StreamBuilder<String?>(
      stream: control.valueChanges,
      builder: (context, snapshot) {
        final current = control.value ?? 'blue';

        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(label, style: Theme.of(context).textTheme.bodySmall),
            if (help != null)
              Text(help!, style: Theme.of(context).textTheme.bodySmall),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              children: colors.entries.map((entry) {
                final isSelected = entry.key == current;
                return GestureDetector(
                  onTap: control.disabled
                      ? null
                      : () => control.value = entry.key,
                  child: Container(
                    width: 36,
                    height: 36,
                    decoration: BoxDecoration(
                      color: entry.value,
                      shape: BoxShape.circle,
                      border: isSelected
                          ? Border.all(color: Colors.black, width: 3)
                          : null,
                    ),
                  ),
                );
              }).toList(),
            ),
          ],
        );
      },
    );
  }
}

Example: Slider Editor for Integers

dart
class SliderPropertyEditor implements PropertyEditor<int> {
  final int min;
  final int max;

  const SliderPropertyEditor({this.min = 0, this.max = 100});

  @override
  Widget createEditor({
    required BuildContext context,
    required PropertyControl<int> control,
    required String label,
    required String name,
    String? help,
  }) {
    return StreamBuilder<int?>(
      stream: control.valueChanges,
      builder: (context, snapshot) {
        final current = control.value ?? min;

        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Text(label),
                const Spacer(),
                Text('$current',
                  style: Theme.of(context).textTheme.bodyLarge),
              ],
            ),
            Slider(
              value: current.toDouble(),
              min: min.toDouble(),
              max: max.toDouble(),
              divisions: max - min,
              onChanged: control.disabled
                  ? null
                  : (v) => control.value = v.round(),
            ),
            if (help != null)
              Text(help!, style: Theme.of(context).textTheme.bodySmall),
          ],
        );
      },
    );
  }
}

Registering Custom Editors

Type-Based Registration

Register as the default editor for a type:

dart
PropertySystem.register<Color>(
  editor: const ColorPropertyEditor(),
  converter: ColorJsonConverter(),
);

Now every Color property will use ColorPropertyEditor automatically.

Custom Key Registration

Register with a custom key for specialized variants:

dart
PropertySystem.register<int>(
  key: 'slider:int',
  editor: const SliderPropertyEditor(min: 0, max: 100),
);

Use the custom key on a property:

dart
IntProperty(
  key: 'volume',
  label: 'Volume',
  defaultValue: 50,
  customKey: 'slider:int',  // uses SliderPropertyEditor
);

Or via the builder:

dart
b.integer('volume', 'Volume',
  defaultValue: 50,
  customKey: 'slider:int',
);

Using reactive_forms Widgets

For editors that need direct access to reactive_forms widgets (like ReactiveTextField), import the interop layer:

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

class RichTextPropertyEditor implements PropertyEditor<String> {
  const RichTextPropertyEditor();

  @override
  Widget createEditor({
    required BuildContext context,
    required PropertyControl<String> control,
    required String label,
    required String name,
    String? help,
  }) {
    return ReactiveTextField<String>(
      formControl: control.formControl,  // extension from interop
      decoration: InputDecoration(
        labelText: label,
        helperText: help,
      ),
      maxLines: 5,
      validationMessages: defaultValidationMessages,
    );
  }
}

Editor Lookup Chain

When property.createEditor() is called, the lookup follows this chain:

  1. If customKey is set, look up _customEditors[customKey]
  2. If not found (or no custom key), look up the editor registered for the property's type
  3. If neither exists, throw a StateError

Some property types (EnumProperty, UnionProperty, ListProperty) override createEditor() to use their own specialized editors before falling back to the registry.

Verifying Registration

dart
// Check if fully registered (both editor and converter)
PropertySystem.isRegistered<Color>();

// Check individual registrations
PropertySystem.hasEditor<Color>();
PropertySystem.hasConverter<Color>();

// Check custom key
PropertySystem.hasExactCustomEditor('slider:int');

// Validate for warnings
final warnings = PropertySystem.validate();
// ['Type Color has converter but no editor']

Next Steps