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:
| Parameter | Description |
|---|---|
context | The BuildContext for theme and media query access |
control | The PropertyControl<T> for reading/writing the value |
label | Display label from the property |
name | Unique field name for form registration |
help | Optional 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:
- If
customKeyis set, look up_customEditors[customKey] - If not found (or no custom key), look up the editor registered for the property's type
- 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
- JSON Serialization -- Custom converters for JSON
- Integration Guide -- How editors are used in form editor and dashboard editor
- API Reference: Property -- Full Property API