Dynamic Behavior
Properties can react to each other through conditions (visibility/enabled) and derivations (auto-computed values). This guide covers both mechanisms in depth.
Conditions
Conditions are declarative objects that control visibility and enabled state. They are evaluated automatically when dependent property values change.
Visibility vs. Enabled
| Aspect | visibleCondition | enabledCondition |
|---|---|---|
| When false | Property is hidden from the editor | Property is shown but grayed out / non-editable |
| Mechanism | ValueNotifier<bool> on visibleNotifier | PropertyControl.markAsDisabled() |
| UI update | ValueListenableBuilder triggers rebuild | FormControl.disabled state change |
| Value preserved | Yes, value is retained when hidden | Yes, value is retained when disabled |
Chaining Conditions
Build complex visibility rules by combining conditions:
// Show "Premium Features" group only when:
// - Plan is not "free"
// - AND trial has not expired
// - AND user has accepted terms
b.boolean('acceptedTerms', 'Accept Terms');
b.enumeration<String>('plan', 'Plan', options: [...]);
b.boolean('trialExpired', 'Trial Expired');
b.group('premium', 'Premium Features');
b.integer('maxUsers', 'Max Users',
defaultValue: 10,
visibleCondition: PropertyCollectionBuilder.whenAll([
PropertyCollectionBuilder.whenNot<String>('plan', 'free'),
PropertyCollectionBuilder.whenFalse('trialExpired'),
PropertyCollectionBuilder.whenTrue('acceptedTerms'),
]),
);Mixing Visibility and Enabled
A property can have both conditions:
b.string('apiKey', 'API Key',
// Only visible when external data source is selected
visibleCondition: PropertyCollectionBuilder.when<String>(
'dataSource', 'external',
),
// Only editable when not in read-only mode
enabledCondition: PropertyCollectionBuilder.whenFalse('readOnly'),
);Custom Conditions for Complex Logic
When built-in conditions are insufficient, use CustomCondition:
b.string('discount', 'Discount Code',
visibleCondition: PropertyCollectionBuilder.whenCustom(
dependencies: ['level', 'capacity', 'enrollmentType'],
evaluator: (collection) {
final level = collection.getValue<CourseLevel>('level');
final capacity = collection.getValue<int>('capacity');
final enrollment = collection.getValue<EnrollmentType>('enrollmentType');
// Complex business rule
return level == CourseLevel.advanced &&
capacity > 20 &&
enrollment == EnrollmentType.limited;
},
),
);Derivations
Derivations auto-compute a property value from other properties. Unlike conditions (which control display), derivations modify actual values.
Basic Derivation
b.string('title', 'Title', required: true);
b.string('slug', 'URL Slug', required: true);
// slug = toIdentifier(title)
b.derivedProperty('slug',
from: {'title'},
transformer: (values) =>
PropertyValidators.toIdentifier(values['title'] as String?),
);Multi-Source Derivation
A derivation can depend on multiple source properties:
b.string('firstName', 'First Name');
b.string('lastName', 'Last Name');
b.string('displayName', 'Display Name');
b.derivedProperty('displayName',
from: {'firstName', 'lastName'},
transformer: (values) {
final first = values['firstName'] as String? ?? '';
final last = values['lastName'] as String? ?? '';
return '$first $last'.trim();
},
);Derivation Lifecycle
- Source changes -- When any source property changes, the transformer is called
- Target updates -- The computed value is set on the target property
- User overrides -- If the user manually edits the target, the derivation stops (dirty flag set)
- User clears -- If the user clears the target value, the derivation resumes
This behavior is managed internally via a counter-based mechanism that distinguishes programmatic updates from user edits.
Loading Existing Data
When loading saved data into a form, you typically want to prevent derivations from overwriting stored values:
final collection = buildCourseSettings();
collection.fromJson(savedData);
// Prevent slug from being re-derived from the saved title
collection.markDerivationsAsDirty();Initially Dirty
Alternatively, set initiallyDirty: true on the derivation:
b.derivedProperty('slug',
from: {'title'},
transformer: (values) => ...,
initiallyDirty: true, // skip auto-derivation on init
);Reactive Updates in the UI
The PropertyCollectionEditor handles reactivity automatically. Here is what happens when a value changes:
Manual Reactivity
If you are not using PropertyCollectionEditor, you can react to changes manually:
// Listen to a single property
property.control.valueChanges.listen((value) {
print('Value changed to: $value');
});
// Listen to visibility
property.visibleNotifier.addListener(() {
print('Visibility changed to: ${property.visible}');
});
// Listen to all changes in the collection
collection.valueChanges.listen((values) {
print('Any value changed: $values');
});Combining Conditions and Derivations
Conditions and derivations work independently but can complement each other:
b.group('billing', 'Billing');
b.enumeration<String>('billingType', 'Billing Type',
options: [
EnumOption('free', 'Free'),
EnumOption('paid', 'Paid'),
EnumOption('subscription', 'Subscription'),
],
defaultValue: 'free',
);
// Only visible when paid
b.decimal('price', 'Price',
defaultValue: 0.0,
visibleCondition: PropertyCollectionBuilder.whenIn<String>(
'billingType', {'paid', 'subscription'},
),
);
// Derived: tax = price * 0.1, only visible when price is visible
b.decimal('tax', 'Tax (auto-computed)',
defaultValue: 0.0,
visibleCondition: PropertyCollectionBuilder.whenIn<String>(
'billingType', {'paid', 'subscription'},
),
);
b.derivedProperty('tax',
from: {'price'},
transformer: (values) {
final price = values['price'] as double? ?? 0.0;
return price * 0.1;
},
);Next Steps
- Dependent Properties Pattern -- Advanced chaining patterns
- Computed Properties Pattern -- Derivation patterns
- Conditions Reference -- All 12 condition types