How to Render a Dynamic Field in EasyAdmin Based on Another Field

If you’ve used Symfony forms independently, you already know how flexible they can be. Adding a field that changes based on another field’s value is straightforward: you listen to form events, update the form, and Symfony handles the rest.

But once you step into EasyAdmin‘s world, the story changes. EasyAdmin wraps Symfony forms inside its own abstractions. This is great for building back-office interfaces quickly, but it makes common Symfony tricks harder to apply. One of the pain point is creating a field whose options depend on another field (like states depending on the chosen country).


What has been postulated?

This is not a new problem. If you search through GitHub issues and forums, you’ll find different approaches people have proposed. For example:

  • Tweaking the JavaScript behind TomSelect to reload options dynamically.
  • Attaching Symfony form events to hack in conditional fields.
  • Adding custom AJAX endpoints and wiring them into EasyAdmin’s UI.

There’s a good example of this discussion here: EasyAdmin Issue #4716.

In that thread, Javier Eguiluz (the creator of EasyAdmin) gave a very clear answer:

“Thanks for proposing this feature … but I’m afraid we can’t implement it. The reason is the same I said in the past when this was proposed too: this is a feature that Symfony Forms should provide. I know that Forms provide an event system … but we need the whole feature to be provided by Symfony Forms so we can use it.

I know my answer is disappointing, but I hope you understand that we can’t implement all this complex features missing in Symfony Forms. Sadly we don’t have resources to do that. Thanks.”

Well honestly, that’s fair. EasyAdmin is not trying to replace or extend Symfony Forms beyond recognition. If Symfony doesn’t support it directly, EasyAdmin can’t just bolt it on.


My Easy Solution: Service-Driven Dynamic Fields

Instead of fighting EasyAdmin with half-working form events or patches of JavaScript, I went with a service-driven approach. The idea is simple: move the responsibility of deciding what the field looks like into a dedicated service.

In your CRUD controller, your fields might look something like this:

class YourCrudController extends AbstractCrudController
{
  ...
  
  public function configureFields(string $pageName): iterable
  {
      yield ChoiceField::new('country'); // the main field
  
      yield ChoiceField::new('state') // depends on country
  }
}

But since you want state to be dynamic based on the value of country, you can keep things simple by autowiring a service that will help:

class YourCrudController extends AbstractCrudController
{
  ...
  
  public function __construct(private DynamicStateService $dynamicStateService) 
  {}
  
  public function configureFields(string $pageName): iterable
  {
      yield ChoiceField::new('country'); // the main field
  
      yield $this->dynamicStateService->getStateField($pageName); // depends on country
  }
}

Then you let the service figure out how state should look depending on the current context.


Building the service

namespace App\Service\EasyAdmin;

use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContextProvider;
use EasyCorp\Bundle\EasyAdminBundle\Field\ChoiceField;
use EasyCorp\Bundle\EasyAdminBundle\Field\FieldInterface;

class DynamicStateService
{
    public function __construct(protected AdminContextProvider $adminContextProvider) 
    {}

    public function getStateField(string $pageName): FieldInterface
    {
        // Same admin context used in the crud controller
        $context = $this->adminContextProvider->getContext();

        /**
         * Get the current entity instance. 
         *
         * Depending on the page, this will be:
         * 
         * - PAGE_NEW: defined but with null values
         * - PAGE_EDIT: defined and fully populated
         * - PAGE_INDEX: always null
         */
        $instance = $context->getEntity()->getInstance();

        /** 
         * Get the country value
         *
         * When editing or showing details, the entity already has country set
         */
        $country = $instance?->getCountry();

        // For "PAGE_NEW" or "PAGE_EDIT", we need to check submitted form data
        $formPages = [Crud::PAGE_NEW, Crud::PAGE_EDIT];
        
        if (in_array($pageName, $formPages, true)) {
            
            // Check if form data is submitted
            $data = $this->getSubmittedData();
            
            if (!empty($data)) {
                // Get the updated country value
                $country = $data['type'] ?? null;
            }
        }

        // Implement your own logic for returning FieldInterface
        return match($country) {
            'US' => $this->getUnitedStatesField(),
            'NG' => $this->getNigeriaStatesField(),
            default => $this->getDefaultField($pageName), // NEW or NULL
        };
    }

    private function getUnitedStatesField(): FieldInterface
    {
        return ChoiceField::new('state')
            ->setChoices([
                'California' => 'CA',
                'Texas' => 'TX',
                'New York' => 'NY',
            ])
            ->setRequired(false)
            ->setFormTypeOption('constraints', [
                new NotBlank(message: 'Please select a state in USA'),
            ])
        ;
    }

    private function getNigeriaStatesField(): FieldInterface
    {
        return ChoiceField::new('state')
            ->setChoices([
                'Lagos' => 'LA',
                'Abuja' => 'ABJ',
                'Kano' => 'KN',
            ])
            ->setRequired(false)
            ->setFormTypeOption('constraints', [
                new NotBlank(message: 'Please select a state in Nigeria'),
            ])
        ;
    }

    private function getDefaultField(string $pageName): FieldInterface
    {
        if ($pageName == Crud::PAGE_INDEX) {
          return ChoiceField::new('state')
              ->setChoices(function ($entity) {
                  // Implement your logic for index page
                  return match($entity->getCountry()) {
                      // ... similar logics here
                  };
              })
          ;
        }
        
        // Implement your logic for new page (with no form submission)
        return ChoiceField::new('state')
            ->setChoices([])
            ->setRequired(false)
            ->setFormTypeOption('constraints', [
                new NotBlank()
            ])
            ->setHelp('Submit the form the with selected country to derive the states')
        ;
    }

    private function getSubmittedData(): ?array
    {
        $context = $this->adminContextProvider->getContext();
        $request = $context->getRequest();
        $formName = $context->getEntity()->getName();
        
        if ($request->isMethod('POST')) {
            return $request->request->all($formName);
        }
        
        return null;
    }
}

Why this solution works fine:

The trick here is that EasyAdmin always works with an AdminContext. That context contains the entity instance (if it exists), the request, and all the information you need to know whether you’re creating, editing, or just listing entities.

By wrapping your dynamic field logic in a service:

  • You don’t need to hack around with form events.
  • You don’t have to fight EasyAdmin’s JavaScript layer.
  • Your solution remains fully compatible with EasyAdmin’s way of rendering fields.

The Simple Logic

  • On “new” or “edit” pages, you peek into the submitted request data.
  • Otherwise, you just read from the entity.
  • On “index” pages, you provide a fallback renderer that can figure it out per row.
  • Using setRequired(false) allows the country to be changed without forcing a value in the state field when the form is submitted.
  • Adding a new NotBlank() constraint ensures the form cannot be submitted with an empty state field.

Wrapping up

Dynamic fields in EasyAdmin are a real need, but not something the bundle offers out of the box. While form events or JS tweaks can work, they’re often fragile. A service-driven solution keeps the logic centralized, uses EasyAdmin’s own context system, and stays clean.

It’s not the only way, but it’s one that has kept my admin panels simple and predictable.

Leave a Comment