EasyCorp / EasyAdminBundle

EasyAdmin is a fast, beautiful and modern admin generator for Symfony applications.
MIT License
4.02k stars 1.02k forks source link

Turbo & Stimulus Powered JavaScript #6051

Open weaverryan opened 7 months ago

weaverryan commented 7 months ago

Short description of what this feature will allow to do: Hi everyone!

This is not really a request as it would take a bit of work and I am very willing to help with it. But I do think it is important, and people are asking about it more and more (I answered 2 questions about this on Symfonycasts tonight).

In short, it would be wonderful if the JavaScript used by EasyAdmin were powered by Stimulus, This would then allow us to add Turbo to EasyAdmin, giving it the SPA-like experience. Once these are done, it opens up a lot of other possibilities - like allowing any forms to be opened up in a modal natively (there is a simple strategy for this using Turbo Frames that I'll talk about in our LAST stack tutorial https://symfonycasts.com/screencast/last-stack ).

@javiereguiluz WDYT about this in principle? Have you gotten other similar requests or request for an even more modern frontend for EasyAdmin?

Thanks!

b1rdex commented 7 months ago

I have a little experience having these inside EasyAdmin application. The stack feels like magic, it's very impressive!

I have built a form that uses DynamicFormBuilder with dependent fields to implement a "pre creation step" where a user selects a preset for a CRUD form fields.

image

I'll provide here a little scenario, just in case someone would be brave to try this too.

  1. create a custom action inside a CRUD controller that simply renders a template
  2. the template should embed a twig live component plus an importmap() statement to include the magic
    
    {% extends '@EasyAdmin/page/content.html.twig' %}

{% block main %}

{{ importmap() }}
<link rel="stylesheet" href="{{ asset('styles/app.css') }}">

{% endblock %}


3. follow the demo code https://ux.symfony.com/demos/live-component/dependent-form-fields and create your form

As you can see, the scenario is not so obvious. It would be amazing to have a more "native" way to implement this.
weaverryan commented 7 months ago

That's really cool @b1rdex! Inside RouteTemplateSelector, you're rendering a completely custom form type... and so... not using the form builder from EasyAdmin for this form? I know this is a little off topic from my original post, but I'd love to see the form details :). Thanks!

b1rdex commented 7 months ago

@weaverryan yes, the form is completely standalone and is built using ComponentWithFormTrait in the live component. However, I'm using EasyAdmin's AdminUrlGenerator inside component for form actions.

mozkomor05 commented 6 months ago

Thanks @b1rdex, very cool! I tried something similar, but got stuck on not being able to get AdminContext from LiveComponent. Did you manage to do that? Because then it would be possible to render CRUD controller forms as well.

b1rdex commented 6 months ago

@mozkomor05 you can try getting it from request attributes. Or just create it manually from request.

Reference code for more details: \EasyCorp\Bundle\EasyAdminBundle\EventListener\AdminRouterSubscriber::onKernelRequest()

mozkomor05 commented 6 months ago

@b1rdex Thanks for the tips. After many hours of debugging I finally had success implementing LiveComponents for Easy Admin Forms. The trick was to save the AdminContext as a LiveProp, for which I wrote my own hydrator.

    #[LiveProp(hydrateWith: 'hydrateAdminContext', dehydrateWith: 'dehydrateAdminContext')]
    public AdminContext $context;

    public function hydrateAdminContext(array $data): AdminContext
    {
        $request = Request::create($data['requestUri']);
        $action = $data['action'];

        $dashboardController = $this->controllerFactory->getDashboardControllerInstance(
            $data['dashboardControllerFqcn'],
            $request
        );

        $crudController = $this->controllerFactory->getCrudControllerInstance(
            $data['crudControllerFqcn'],
            $action,
            $request
        );

        $context = $this->adminContextFactory->create($request, $dashboardController, $crudController);

        // Necessary as some EasyAdmin methods load context using AdminContextProvider
        $this->requestStack->getCurrentRequest()?->attributes->set(EA::CONTEXT_REQUEST_ATTRIBUTE, $context);

        return $context;
    }

    public function dehydrateAdminContext(AdminContext $context): array
    {
        return [
            'dashboardControllerFqcn' => $context->getDashboardControllerFqcn(),
            'crudControllerFqcn' => $context->getCrud()->getControllerFqcn(),
            'requestUri' => $context->getRequest()->getUri(),
            'action' => $context->getCrud()->getCurrentAction(),
        ];
    }

With AdminContext it is easy to instantiate EasyAdmin form:


    protected function instantiateForm(): FormInterface
    {
        $context = $this->context;
        $action = $context->getCrud()->getCurrentAction();
        $entityDto = $context->getEntity();
        $entityFqcn = $entityDto->getFqcn();

        $crudController = $this->controllerFactory->getCrudControllerInstance(
            $context->getCrud()->getControllerFqcn(),
            $context->getCrud()->getCurrentAction(),
            $context->getRequest()
        );

        switch ($action) {
            case Action::NEW:
                $entityDto->setInstance($crudController->createEntity($entityFqcn));
                $this->preprocessEntity($context, $crudController);

                return $crudController->createNewForm($entityDto, $context->getCrud()->getNewFormOptions(), $context);
            case Action::EDIT:
                $this->preprocessEntity($context, $crudController);

                return $crudController->createEditForm($entityDto, $context->getCrud()->getEditFormOptions(), $context);
            default:
                throw new \RuntimeException('LiveForm only supports new and edit actions.');
        }
    }

    private function preprocessEntity(AdminContext $context, CrudControllerInterface $crudController): void
    {
        $entityDto = $context->getEntity();
        $action = $context->getCrud()->getCurrentAction();

        $this->entityFactory->processFields($entityDto, FieldCollection::new($crudController->configureFields($action)));

        $fieldAssetsDto = new AssetsDto();
        $currentPageName = $context->getCrud()?->getCurrentPage();
        foreach ($entityDto->getFields() as $fieldDto) {
            $fieldAssetsDto = $fieldAssetsDto->mergeWith($fieldDto->getAssets()->loadedOn($currentPageName));
        }

        $context->getCrud()->setFieldAssets($fieldAssetsDto);
        $this->entityFactory->processActions($entityDto, $context->getCrud()->getActionsConfig());
    }

Usage:

{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #}
{% extends '@EasyAdmin/crud/edit.html.twig' %}

{% block edit_form %}
    <twig:LiveForm :form="edit_form" :context="ea"/>
{% endblock %}

This can be easily combined with DynamicFormBuilder (https://ux.symfony.com/demos/live-component/dependent-form-fields) in create<Action>FormBuilder, or you can also implement the dynamic logic manually.