Skip to content

Custom Functions

Overview

Custom functions are user-defined functions that extend the expression language with your own logic. There are two ways to add custom functions, each with different scope and lifetime. Beyond functions, resolvers extend method and property access to your own types.

All custom functions can only accept positional arguments — named parameters are not supported.

Registered Functions

Use registerFunction to add functions that are globally available in every EL expression, just like built-in functions. Register them once at startup and they're accessible everywhere without passing them through Dependencies.

import 'package:xwidget_el/xwidget_el.dart';

void main() {
  // Register global custom functions
  registerFunction("formatCurrency", formatCurrency);
  registerFunction("pluralize", pluralize);
}

String formatCurrency(dynamic value) {
  final amount = (value as num).toStringAsFixed(2);
  return '\$$amount';
}

String pluralize(dynamic count, String singular, String plural) {
  return (count as num) == 1 ? singular : plural;
}
<Text data="${formatCurrency(price)}"/>
<!-- 19.99 -> "$19.99" -->

<Text data="${pluralize(itemCount, 'item', 'items')}"/>
<!-- 1 -> "item", 5 -> "items" -->

Resolution Order

When an EL expression calls a function, it is resolved in this order:

  1. Built-in functions — the predefined set (e.g. abs, length, isEmpty)
  2. Registered functions — added via registerFunction
  3. Dependency functions — stored in the current Dependencies instance

Built-in functions cannot be overridden by registered functions. Registered functions take priority over dependency functions of the same name.

Dependency Functions

Add functions to a Dependencies instance for scoped access. These are available only where that Dependencies instance is in scope, making them ideal for controller-specific logic.

final dependencies = Dependencies();
dependencies.setValue("greet", (String name) => 'Hello, $name!');
<Text data="${greet('Sally')}"/>
<!-- "Hello, Sally!" -->

Dependency functions are commonly used in controllers to expose methods to fragments:

class ProductController extends Controller {
  @override
  void bindDependencies() {
    dependencies.setValue("formatSku", formatSku);
  }

  String formatSku(dynamic sku) => 'SKU-${sku.toString().padLeft(6, '0')}';
}
<Text data="${formatSku(product.id)}"/>
<!-- 42 -> "SKU-000042" -->

When to Use Which

Registered Dependency
Scope Global — available everywhere Scoped — only where Dependencies is accessible
Lifetime App lifetime Tied to the Dependencies instance
Registration Once at startup Each time Dependencies is created
Use case Utility functions, formatters, shared logic Controller-specific methods, context-dependent logic

Instance Method Resolvers

Use registerMethodResolver to make methods on your own types callable from expressions. Resolvers run after the built-in instance functions fall through, in registration order; the first resolver that returns a non-null Function wins. Built-in instance functions cannot be overridden. Return null to defer to the next resolver.

import 'package:xwidget_el/xwidget_el.dart';

void main() {
  registerMethodResolver((name, target) {
    if (target is Money) {
      switch (name) {
        case "format": return target.format;
      }
    }
    return null;
  });
}
<Text data="${price.format()}"/>
<!-- Money(19.99, 'USD') -> "$19.99" -->

Property Resolvers

Use registerPropertyResolver to give your own types properties. Return a PropertyResolution wrapping the value, or null to defer — the wrapper is what distinguishes "resolved to null" from "not resolved." Resolvers run after the built-in core properties, in registration order; built-in properties cannot be overridden.

import 'package:xwidget_el/xwidget_el.dart';

void main() {
  registerPropertyResolver((name, target) {
    if (target is Money) {
      switch (name) {
        case "amount": return PropertyResolution(target.amount);
        case "currency": return PropertyResolution(target.currency);
      }
    }
    return null;
  });
}
<Text data="${price.amount} ${price.currency}"/>
<!-- Money(19.99, 'USD') -> "19.99 USD" -->