Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Form Panels

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Last topic! We made it! And our admin is getting super customized. For this final trick, I want to look closer at the form. Almost all of this is controlled by the Field configuration. Each field corresponds to a Symfony form type... and then EasyAdmin renders those fields through the form system. It really is that simple.

Custom Form Theme

EasyAdmin comes with a custom form theme. So if you wanted to, for example, make a text type field look different in EasyAdmin, you could create a custom form theme template. This theme can be added to the $crud object in configureCrud(). Down here, for example, we could say ->addFormTheme() to add our form theme template to just one CRUD controller... or you could put this in the dashboard to apply everywhere.

Form Panel

But, apart from a custom form theme, there are a few other ways that EasyAdmin allows us to control what this page looks like... which, right now, is just a long list of fields.

Over in QuestionCrudController, up in configureFields()... here we go... right before the askedBy field, add yield FormField::. So we're starting like normal, but instead of saying new, say addPanel('Details').

... lines 1 - 28
class QuestionCrudController extends AbstractCrudController
{
... lines 31 - 112
public function configureFields(string $pageName): iterable
{
... lines 115 - 140
yield FormField::addPanel('Details');
... lines 142 - 161
}
... lines 163 - 225
}

Watch what this does! Refresh and... cool! "Asked By" and "Answers" appear under this "Details" header. That's because, as you can see, askedBy and answers are the two fields that appear after the addPanel() call. And because the rest of these fields are not under a panel, they just... kind of appear at the bottom, which works, but doesn't look the greatest.

So, when I use addPanel(), I put everything under a panel. Right after IdField, which isn't going to appear on the form, say FormField::addPanel('Basic Data'). Oh! And let me make sure I don't forget to yield that.

... lines 1 - 112
public function configureFields(string $pageName): iterable
{
... lines 115 - 116
yield FormField::addPanel('Basic Data');
... lines 118 - 162
}
... lines 164 - 228

Thanks to this... awesome! We have a "Basic Data" panel, all of the fields below that, then the second panel down here.

Customizing the Panels

These panels have a few methods on them. One of the most useful is ->collapsible(). Make this panel collapsible... and the other as well.

... lines 1 - 112
public function configureFields(string $pageName): iterable
{
... lines 115 - 116
yield FormField::addPanel('Basic Data')
->collapsible();
... lines 119 - 142
yield FormField::addPanel('Details')
->collapsible();
... lines 145 - 164
}
... lines 166 - 230

I bet you can guess what this does. Yep! We get a nice way to collapse each section.

What else can we tweak? How about ->setIcon('fa fa-info')... or ->setHelp('Additional Details)?

Oh, I actually meant to put this down on the other panel, so let me grab this... find that other panel... here we go... and paste.

... lines 1 - 112
public function configureFields(string $pageName): iterable
{
... lines 115 - 142
yield FormField::addPanel('Details')
->collapsible()
->setIcon('fa fa-info')
->setHelp('Additional Details');
... lines 147 - 166
}
... lines 168 - 232

Let's check it out! Nice! The second panel has an icon and some sub-text.

By the way, the changes we're making not only affect the form page, but also the Detail page. Go check out the Detail page for one of these. Yup! The same organization is happening here, which is nice.

Form Tabs

If you want to organize things even a bit more, instead of panels, you can use tabs. Change addPanel() to addTab(). And... repeat that below: addTab().

... lines 1 - 112
public function configureFields(string $pageName): iterable
{
... lines 115 - 116
yield FormField::addTab('Basic Data')
... lines 118 - 142
yield FormField::addTab('Details')
... lines 144 - 166
}
... lines 168 - 232

When we refresh now... yup! Each shows up as a separate tab. But the ->collapsible() doesn't really make sense anymore. It is still being called, but it doesn't do anything. So, remove that.

Fixing the Icon on the Tab

Oh, and we also lost our icon! We added an fa fa-info icon... but it's not showing! Or is it? If you look closely, there's some extra space. Inspect element on that. There is an icon! But... it looks... weird. It has an extra fa-fa for some reason.

We can fix this by changing the icon to, simply, info. This is... sort of a bug. Or, it's at least inconsistent. When we use tabs, EasyAdmin adds the fa- for us. So all we need is info. Watch: when I refresh... there! fa-info... and now the icon shows up!

... lines 1 - 112
public function configureFields(string $pageName): iterable
{
... lines 115 - 142
yield FormField::addTab('Details')
->setIcon('info')
... lines 145 - 165
}
... lines 167 - 231

Form Columns

The last thing we can do, instead of having this long list of fields, is to put the fields next to each other. We do this by controlling the columns on this page.

To show this off, move the name field above slug. Yup, got it. And now let's see if we can put these fields next to each other. We're using bootstrap, which means there are 12 invisible columns on each page. So, on name, say ->setColumns(5)... and on slug, do the same thing: ->setColumns(5).

... lines 1 - 112
public function configureFields(string $pageName): iterable
{
... lines 115 - 119
yield Field::new('name')
... line 121
->setColumns(5);
yield Field::new('slug')
... lines 124 - 128
->setColumns(5);
... lines 130 - 167
}
... lines 169 - 233

We could use 6 to take up all of the space, but I'll stick with 5 and give it some room. Refresh now and... very nice! The fields float next to each other. This is a great way to help this page... make a bit more sense.

And... that's it, friends! We are done! This was fun! We should do it again sometime. I love EasyAdmin, and we here at SymfonyCasts are super proud of the admin section we built with it... which includes a lot of custom stuff. Let us know what you're building! And as always, we're here for you down in the Comments section with any questions, ideas, or delicious recipes that you might have.

All right friends, see you next time!

Leave a comment!

18
Login or Register to join the conversation

Finally! the last tutorial of EasyAdmin Course! Many thanks for your great efforts!

2 Reply

Hey Lubna,

Yes, you made it! Congratulations! 🎉 I really happy to hear you liked the course ;)

Cheers!

Reply
Nuno F. Avatar
Nuno F. Avatar Nuno F. | posted 1 month ago

Hello and Thank you for that tutorial... Very useful to take advantage of it.

Let me ask you something you didn't talk about.

Let me assume that we have a Crud for Courses and Some Participants (based on a OneToMany Relation).
I want to build a fully operational CourseParticipants Crud that is called by an Action 'participants' on Courses Crud (With index, show , edit, create and delete features)
but maintaining the parent course id on every pages inside it.?

So I don't want to open index of all participants but only those belonging to that course. And when I create a new one, i want to set internaly the parent course.

How do you think it is the best way to solve that?

Cheers!!!!!!

1 Reply

Hey Nuno Luz!

That's a really interesting situation! Hmm. The use-case makes sense... and fun! Here's what I'm thinking:

A) So we will have CourseParticipantCrudController... and it will be a normal controller except that we want to read a "parent course" so that we can do things like (i) only show participants for that course and (ii) auto-set the course when a new CourseParticipant is created. To do this, when the user arrives at the CRUD, there will need to be an extra ?course=# in the URL where # is the id of the course. When you link to the CourseParticipantCrudController, you should be able to add this when using the AdminUrlGenerator:


$targetUrl = $adminUrlGenerator
->setController(CourseParticipantCrudController::class)
->setAction(Crud::PAGE_INDEX)
// this is the key: it should add the ?course=# to the URL
->set('course', $course->getId())
->generateUrl();

B) Ok, now we have (hopefully) arrived at the INDEX action for CourseParticipantCrudController. Our first job is to read that and filter the CourseParticipant. You should be able to do that by overriding createIndexQueryBuilder() in your CRUD controller. Autowire the RequestStack into a __construct() method of your CourseParticipantCrudController, then use that to read the ?course=# from the URL and modify the query. Yay! Our list should now filter by the correct Course

C) But what happens when the user clicks a link inside of CourseParticipantCrudController - like the "New" link? We need that URL to keep the ?course=# on it. And... I think that this will happen automatically. The AdminUrlGenerator reads all of the current query parameters and keeps them when generating URLs. If I'm wrong about this, let me know. There is a solution, but it's more complex.

D) We have now arrived on the "new" action and we STILL have the ?course=# on the URL. We re doing GREAT. To automatically set the course on the CourseParticipant when it saves, I would override persistEntity() in your controller. Once again, use the RequestStack you autowired in (B) to read the ?course=# from the URL, query for that Course, then set it onto your CourseParticipant. Call parent::persistEntity($entityManager, $entityInstance) so that it can finish saving.

There is a decent chance that I forgot a detail - but let me know! This is a very cool use-case for EasyAdmin - I'd love to know if it works.

Cheers!

Reply
Nuno F. Avatar

Hello Ryan,

And thanks for your fast answer
I have chosen quite the same process.
but i were right about C) the url looses all the parameters. I could get it from 'referrer' param but I prefer update the actions :


public function configureActions(Actions $actions): Actions
{
$menuIdx = $this->requestStack->getCurrentRequest()->get('menuIdx');
return $actions
->update(Crud::PAGE_INDEX, 'edit', function(Action $e) use ($menuIdx) {
return $e->linkToUrl(
$this->adminUrlGenerator->setAction('edit')
->setController(self::class)
->set('menuIdx', $menuIdx)->generateUrl()
);
})
->update(Crud::PAGE_INDEX, 'new', function(Action $e) use ($menuIdx) {
return $e->linkToUrl(
$this->adminUrlGenerator->setAction('new')
->setController(self::class)
->set('menuIdx', $menuIdx)->generateUrl()
);
});
}

I have tested on Create and Update everything works nice.

By the way, I have also made some changes on the configCrud to have a nice custom title made from the current course.

Nothing can stop us now.
Again thanks a lot for your help and keep going.

Nuno

1 Reply

Hey Nuno!

Thanks for letting me know that the parameters were in fact NOT sticky. And what a great solution you found to re-add it - thanks for sharing that!

> Nothing can stop us now.

That's right 😎

Cheers!

Reply

GG

EasyAdmin has come a long way (glad to see the yaml config gone)

1 Reply

Hey Edin,

Yeah, agree, now it's much flexible with PHP config. And thanks to IDE we have autocomplete now :)

Cheers!

Reply

Hello! A little bit question please!
To create "Edit Profile" page for my users (no Admin role permission). Are there any way to use EasyAdmin or it works only for Admin role?

Many thanks in advance!

Reply

Hey Lubna!

Hmm. This might technically be possible... but in practice, no, EasyAdmin isn't really meant for this. If you did do this, your users would suddenly be in the admin "layout" when going to their edit profile page - so it'll be a weird experience. Better to just build a normal page for this using a Symfony form :).

Cheers!

1 Reply

Thank you very much for the clarification!

Reply
Sergey-P Avatar
Sergey-P Avatar Sergey-P | posted 28 days ago

Hello! Is it possible to put two entities on one page? For example, in the first tab, I want to add one entity, and in the second tab another one

Reply

Hey Sergey,

It should be possible if you create your own templates and Crud controller, but I don't think it will just work out of the box

Cheers!

Reply
Sergey-P Avatar

MolloKhan, thanks :)

Reply
JoshuaGugun Avatar
JoshuaGugun Avatar JoshuaGugun | posted 1 month ago

Finally! Many thanks for your great efforts!
I really enjoy learning the EasyAdmin 4 course.

Now, I will continue learning the other course of Symfony 6 track.

Reply
VickaB Avatar

Thanks a lot ! What a great Job!

Reply

Thanks for this GREAT tutorial!

Two things I found out digging in the code that I think will make the admin looks even better. Thought I would share it here in case it could help others.

1. I really don't like the fact that the admin content section is not taking the whole window this can be fixed easily in the DashboardController like this.

public function configureDashboard(): Dashboard
{
$dashboard = Dashboard::new()
->setTitle('Cauldron Overflow Admin')
;
$dashboard->getAsDto()->setContentWidth(Crud::LAYOUT_CONTENT_FULL);

return $dashboard;
}

2. In case you want to have panel split into columns (kinda like in the Wordpress admin), you can't use FormField::addPanel('Basic Data')->setColumns($int) on panels but you can do FormField::addPanel('Basic Data')->addCssClass('col-md-9')

Reply

Love these tips - thanks for sharing them Julien Bonnier!!

2 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.0.2",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/doctrine-bundle": "^2.1", // 2.5.5
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.2.1
        "doctrine/orm": "^2.7", // 2.10.4
        "easycorp/easyadmin-bundle": "^4.0", // v4.0.2
        "handcraftedinthealps/goodby-csv": "^1.4", // 1.4.0
        "knplabs/knp-markdown-bundle": "dev-symfony6", // dev-symfony6
        "knplabs/knp-time-bundle": "^1.11", // 1.17.0
        "sensio/framework-extra-bundle": "^6.0", // v6.2.5
        "stof/doctrine-extensions-bundle": "^1.4", // v1.7.0
        "symfony/asset": "6.0.*", // v6.0.1
        "symfony/console": "6.0.*", // v6.0.2
        "symfony/dotenv": "6.0.*", // v6.0.2
        "symfony/flex": "^2.0.0", // v2.0.1
        "symfony/framework-bundle": "6.0.*", // v6.0.2
        "symfony/mime": "6.0.*", // v6.0.2
        "symfony/monolog-bundle": "^3.0", // v3.7.1
        "symfony/runtime": "6.0.*", // v6.0.0
        "symfony/security-bundle": "6.0.*", // v6.0.2
        "symfony/stopwatch": "6.0.*", // v6.0.0
        "symfony/twig-bundle": "6.0.*", // v6.0.1
        "symfony/ux-chartjs": "^2.0", // v2.0.1
        "symfony/webpack-encore-bundle": "^1.7", // v1.13.2
        "symfony/yaml": "6.0.*", // v6.0.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.7
        "twig/twig": "^2.12|^3.0" // v3.3.7
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.1
        "symfony/debug-bundle": "6.0.*", // v6.0.2
        "symfony/maker-bundle": "^1.15", // v1.36.4
        "symfony/var-dumper": "6.0.*", // v6.0.2
        "symfony/web-profiler-bundle": "6.0.*", // v6.0.2
        "zenstruck/foundry": "^1.1" // v1.16.0
    }
}