Dynamic Forms

This is the official documentation of the forestadmin/laravel-forestadmin v2+ and forestadmin/symfony-forestadmin PHP agents.

Business logic often requires your forms to adapt to their context. Forest Admin makes this possible through a powerful way to extend your form's logic.

To make an action form dynamic, simply use functions instead of a static value on the compatible properties.

Note that:

  • Both synchronous and asynchronous functions are supported

  • The functions take the same context object as the execute handler.

    • As such, they have access to the current values of the form.

    • And the records that the action will be applied to.

Form field properties

functions can be used for the following properties which are also available as static values:

PropertyDescription

isRequired

Make the field required.

isReadOnly

Make the field read-only.

defaultValue

Set the default value of the field.

In addition, depending on the field type, you can also use functions for the following properties:

PropertyDescription

enumValues

Change the list of possible values of the field when type == 'Enum'.

collectionName

Change the target collection of the field when type: 'Collection'.

And finally, those two extra properties are available and can only be used as functions:

PropertyDescription

if

Only display the field if the function returns true.

value

Set the current value of the field.

Examples

Example 1: Dynamic fields based on form values

In this example we make a field required only if the user enters a value greater than 1000 in another field.

use ForestAdmin\AgentPHP\DatasourceCustomizer\CollectionCustomizer;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\BaseAction;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\Context\ActionContextSingle;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\DynamicField;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\ResultBuilder;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\Types\ActionScope;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\Types\FieldType;

$forestAgent->customizeCollection(
  'Customer',
  function (CollectionCustomizer $builder) {
    $builder->addAction('Charge credit card', new BaseAction(
      scope: ActionScope::SINGLE,
      form: [
        new DynamicField(
          label: 'amount',
          type: FieldType::NUMBER,
          description: 'The amount (USD) to charge the credit card. Example: 42.50',
          isRequired: true,
        ),
        new DynamicField(
          label: 'description',
          type: FieldType::STRING,

          /**
           * The field will only be required if the function returns true.
           */
          isRequired: fn ($context) => $context->getFormValues()['amount'] > 1000,
        ),
      ],
      execute: fn ($context) => ...,
    ));
  }
);

Example 2: Conditional field display based on record data

Unlike the previous example, this one will only display the field if the record has a specific value.

It is still a dynamic field, but this time, the condition does not depend on the form values but on the record data.

use ForestAdmin\AgentPHP\DatasourceCustomizer\CollectionCustomizer;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\BaseAction;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\Context\ActionContextSingle;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\DynamicField;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\ResultBuilder;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\Types\ActionScope;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\Types\FieldType;

$forestAgent->customizeCollection(
  'Product',
  function (CollectionCustomizer $builder) {
    $builder->addAction('Leave a review', new BaseAction(
      scope: ActionScope::SINGLE,
      execute: fn ($context) => /* ... perform work here ... */,
      form: [
        new DynamicField(
          label: 'Rating',
          type: FieldType::ENUM,
          enumValues: ['1', '2', '3', '4', '5'],
        ),
        new DynamicField(
          label: 'Put a comment',
          type: FieldType::STRING,
          // Only display this field if the rating is 4 or 5
          if: fn ($context) => $context->getFormValue('Rating') >= 4,
        ),
      ]
    ));
  }
);

Example 3: Conditional enum values based on both record data and form values

You can mix and match the previous examples to create complex forms.

In this example, the form will display a different set of enum values depending on both the record data and the value of the form field.

The first field displays different denominations that can be used to address the customer, depending on the full name and gender of the customer.

The second field displays different levels of loudness depending on if the customer is Morgan Freeman, as to ensure that we never speak Very Loudly at him, for the sake of politeness.

use ForestAdmin\AgentPHP\DatasourceCustomizer\CollectionCustomizer;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\BaseAction;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\Context\ActionContextSingle;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\DynamicField;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\ResultBuilder;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\Types\ActionScope;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\Types\FieldType;

$forestAgent->customizeCollection(
  'Customer',
  function (CollectionCustomizer $builder) {
    $builder->addAction('Tell me a greeting', new BaseAction(
      scope: ActionScope::SINGLE,
      execute: function ($context, $resultBuilder) {
        $denomination = $context->getFormValue('How should we refer to you?');
        $loudness = $context.getFormValue('How loud should we say it?');

        $text = "Hello $denomination";
        if ($loudness === 'Whispering') {
          $text = strtolower($text);
        } elseif ($loudness === 'Loudly') {
          $text = strtoupper($text);
        } elseif ($loudness === 'Very Loudly') {
          $text = strtoupper($text) + '!!!';
        }

        return $resultBuilder->success($text);
      },
      form: [
        new DynamicField(
          label: 'How loud should we say it?',
          type: FieldType::ENUM,
          enumValues: function (ActionContextSingle $context) {
            // Enum values are computed based on another form field value
            // (no need to use an async function here, but doing so would not be a problem)
            $user = $context->getRecord(['firstName', 'lastName', 'gender']);
            $base = [
              $user['firstName'], $user['firstName'] . ' ' . $user['lastName']
            ];

            if ($gender === 'Female') {
              $base[] = 'Mrs. ' . $user['lastName'];
              $base[] = 'Miss. ' . $user['lastName'];
            } else {
              $base[] = 'Mr. ' . $user['lastName'];
            }

            return $base;
          }
        ),
        new DynamicField(
          label: 'How should we refer to you?',
          type: FieldType::ENUM,
          enumValues: function (ActionContextSingle $context) {
            // Enum values are computed based on the record data
            // Because we need to fetch the record data, we need to use an async function
            $denomination = $context->getFormValue('How should we refer to you?');

            return $denomination === 'Morgan Freeman'
              ? ['Whispering', 'Softly', 'Loudly']
              : ['Softly', 'Loudly', 'Very Loudly'];
          }
        ),
      ]
    ));
  }
);

Example 4: Using hasFieldChanged to reset value

In this example we reset a field based on change on another one.

use ForestAdmin\AgentPHP\DatasourceCustomizer\CollectionCustomizer;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\BaseAction;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\Context\ActionContextSingle;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\DynamicField;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\ResultBuilder;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\Types\ActionScope;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\Types\FieldType;

$forestAgent->customizeCollection(
  'Customer',
  function (CollectionCustomizer $builder) {
    $builder->addAction(
      'Create banking identity',
      new BaseAction(
        scope: ActionScope::SINGLE,
        execute: function (ActionContextSingle $context, ResultBuilder $resultBuilder) {
          //...
        },
        form: [
          new DynamicField(type: FieldType::ENUM, label: 'Bank Name', isRequired: true, enumValues: ['CE', 'BP']),
          new DynamicField(
            type: FieldType::STRING,
            label: 'BIC',
            value: function (ActionContextSingle $context) {
              if ($context->hasFieldChanged('Bank Name')) {
                return $context->getFormValue('Bank Name') === 'CE'
                  ? 'CEPAFRPPXXX'
                  : 'CCBPFRPPXXX';
              }
            }
          ),
        ],
      )
    );
  }
);

Example 5: Setting up default value based on the record

In this example we setup a the default value of a field based on the current record.

use ForestAdmin\AgentPHP\DatasourceCustomizer\CollectionCustomizer;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\BaseAction;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\Context\ActionContextSingle;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\DynamicField;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\ResultBuilder;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\Types\ActionScope;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\Types\FieldType;

$forestAgent->customizeCollection(
  'Customer',
  function (CollectionCustomizer $builder) {
    $builder->addAction(
      'Change order price',
      new BaseAction(
        scope: ActionScope::SINGLE,
        execute: function (ActionContextSingle $context, ResultBuilder $resultBuilder) {
          //...
        },
        form: [
          new DynamicField(
            type: FieldType::NUMBER,
            label: 'New Price',
            defaultValue: function (ActionContextSingle $context) {
              return $context->getRecord(['amount'])['amount'];
            }
          ),
        ],
      )
    );
  }
);

Example 6: Block field edition

In this example we make a field read only based on a field from the current record.

use ForestAdmin\AgentPHP\DatasourceCustomizer\CollectionCustomizer;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\BaseAction;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\Context\ActionContextSingle;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\DynamicField;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\ResultBuilder;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\Types\ActionScope;
use ForestAdmin\AgentPHP\DatasourceCustomizer\Decorators\Actions\Types\FieldType;

$forestAgent->customizeCollection(
  'Customer',
  function (CollectionCustomizer $builder) {
    $builder->addAction(
      'Change order price',
      new BaseAction(
        scope: ActionScope::SINGLE,
        execute: function (ActionContextSingle $context, ResultBuilder $resultBuilder) {
          //...
        },
        form: [
          new DynamicField(
            type: FieldType::NUMBER,
            label: 'New Price',
            defaultValue: function (ActionContextSingle $context) {
              return $context->getRecord(['amount'])['amount'];
            },
            isReadOnly: function (ActionContextSingle $context) {
              return $context->getRecord(['status'])['status'] === 'paid';
            },
          ),
        ],
      )
    );
  }
);

Last updated