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