Skip to content

Computed Properties ​

This pattern covers using PropertyDerivation to create properties whose values are automatically computed from other properties.

Basic Auto-Derivation ​

The most common use case: derive a URL slug from a title.

dart
final b = PropertyCollectionBuilder();

b.string('title', 'Title', required: true);
b.string('slug', 'URL Slug', required: true);

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

final collection = b.buildCollection();

When the user types "My Course Title", the slug auto-fills as my_course_title.

Multi-Source Derivation ​

Compute from multiple properties:

dart
b.string('firstName', 'First Name');
b.string('lastName', 'Last Name');
b.string('fullName', 'Full Name');
b.string('email', 'Email');

// Full name from first + last
b.derivedProperty('fullName',
  from: {'firstName', 'lastName'},
  transformer: (values) {
    final first = values['firstName'] as String? ?? '';
    final last = values['lastName'] as String? ?? '';
    return '$first $last'.trim();
  },
);

// Email suggestion from first.last@company.com
b.derivedProperty('email',
  from: {'firstName', 'lastName'},
  transformer: (values) {
    final first = (values['firstName'] as String? ?? '').toLowerCase();
    final last = (values['lastName'] as String? ?? '').toLowerCase();
    if (first.isEmpty && last.isEmpty) return '';
    return '$first.$last@company.com';
  },
);

Dirty Tracking ​

Derivations track whether the user has manually edited the target property:

  1. Auto mode -- When the user has not touched the target, changes to sources auto-update it
  2. User edited -- When the user types into the target, the dirty flag is set and auto-derivation stops
  3. User clears -- When the user clears the target (empty string), the dirty flag resets and auto-derivation resumes

This ensures user intent is always preserved.

Loading Existing Data ​

When loading saved data, you want to preserve the stored value instead of re-deriving:

dart
final collection = buildSettings();

// Load saved data
collection.fromJson({
  'title': 'Custom Course',
  'slug': 'custom_slug_override',  // user had manually set this
});

// Mark all derivations dirty to prevent overwrite
collection.markDerivationsAsDirty();

Initially Dirty ​

Alternatively, create the derivation as initially dirty:

dart
b.derivedProperty('slug',
  from: {'title'},
  transformer: (values) => ...,
  initiallyDirty: true,  // do not auto-derive on first load
);

Computed Read-Only Values ​

Combine derivations with ReadOnlyProperty for display-only computed values:

dart
b.decimal('price', 'Price', defaultValue: 0.0);
b.decimal('quantity', 'Quantity', defaultValue: 1.0);
b.readonly<String>('total', 'Total', defaultValue: '\$0.00');

b.derivedProperty('total',
  from: {'price', 'quantity'},
  transformer: (values) {
    final price = values['price'] as double? ?? 0.0;
    final qty = values['quantity'] as double? ?? 1.0;
    return '\$${(price * qty).toStringAsFixed(2)}';
  },
);

Note: ReadOnlyProperty displays the value but does not allow editing, so the dirty tracking mechanism does not interfere.

Chained Derivations ​

You can chain derivations where one derived property feeds into another. Ensure source properties are declared before their dependents:

dart
b.string('title', 'Title', required: true);
b.string('slug', 'Slug');
b.string('url', 'Preview URL');

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

// url derived from slug
b.derivedProperty('url',
  from: {'slug'},
  transformer: (values) {
    final slug = values['slug'] as String? ?? '';
    return slug.isEmpty ? '' : 'https://example.com/courses/$slug';
  },
);

When the user types a title, the slug auto-fills, and the URL auto-fills from the slug.

Conditional Derivation Logic ​

The transformer has access to all property values, so it can implement conditional logic:

dart
b.enumeration<String>('plan', 'Plan',
  options: [
    EnumOption('basic', 'Basic'),
    EnumOption('pro', 'Pro'),
    EnumOption('enterprise', 'Enterprise'),
  ],
  defaultValue: 'basic',
);
b.integer('seats', 'Seats', defaultValue: 1);
b.readonly<String>('priceEstimate', 'Price Estimate', defaultValue: '');

b.derivedProperty('priceEstimate',
  from: {'plan', 'seats'},
  transformer: (values) {
    final plan = values['plan'] as String? ?? 'basic';
    final seats = values['seats'] as int? ?? 1;

    final pricePerSeat = switch (plan) {
      'basic' => 10,
      'pro' => 25,
      'enterprise' => 50,
      _ => 0,
    };

    return '\$${pricePerSeat * seats}/month';
  },
);

PropertyDerivation API ​

FieldTypeDescription
targetPropertyStringKey of the property to auto-compute
sourcePropertiesSet<String>Keys of source properties to watch
transformerFunction(Map<String, dynamic>)Computes the target value
initiallyDirtyboolIf true, skip derivation on initialization

Add derivations to an existing collection:

dart
collection.addDerivations([
  PropertyDerivation(
    targetProperty: 'slug',
    sourceProperties: {'title'},
    transformer: (values) => ...,
  ),
]);

Next Steps ​