Buy

Dynamically Remove the delete Action Link

Another chapter, another problem to solve! I need to hide the delete button on the list page if an entity is published. So... nope! We can't just go into config.yml and add -delete. We need to override the list.html.twig template and take control of those actions manually.

Copy that file. Then, up inside our views directory, I want to show the other way of overriding templates: by convention. Create a new easy_admin directory, and paste the template there and... that's it! EasyAdminBundle will automatically know to use our list template.

Dumping Variables

The toughest thing about overriding a template is... well... figuring out what variables you can use! In list.html.twig... how about in the content_header block, add {{ dump() }}:

245 lines app/Resources/views/easy_admin/list.html.twig
... lines 1 - 38
{% block content_header %}
{{ dump() }}
... lines 41 - 93
{% endblock content_header %}
... lines 95 - 245

And in _id.html.twig, do the same:

2 lines app/Resources/views/admin/fields/_id.html.twig
{{ dump() }}<i class="fa fa-key"></i> {{ value }}

I want to see what the variables look like in each template.

Ok, refresh the genus list page! Awesome! This first dump is from list.html.twig. It has the same fields configuration we've been looking at in the profiler, a paginator object and a few other things, including configuration for this specific section.

The other dumps come from _id.html.twig. The big difference is that we're rendering one Genus each time this template is called. So it has an item variable set to the Genus object. That will be super handy. If some of the other keys are tough to look at, remember, a lot of this already lives in the EasyAdminBundle profiler area.

Extending the Original Template

Ok, take out those dumps! So, how can we hide the delete button for published genuses? It's actually a bit tricky.

In list.html.twig, if you search, there is a variable called _list_item_actions:

244 lines app/Resources/views/easy_admin/list.html.twig
... lines 1 - 94
{% block main %}
{% set _list_item_actions = easyadmin_get_actions_for_list_item(_entity_config.name) %}
... lines 97 - 189
{% endblock main %}
... lines 191 - 244

This contains information about the actions that should be rendered for each row. It's used further below, in a block called item_actions:

244 lines app/Resources/views/easy_admin/list.html.twig
... lines 1 - 94
{% block main %}
{% set _list_item_actions = easyadmin_get_actions_for_list_item(_entity_config.name) %}
<div class="table-responsive">
<table class="table">
... lines 100 - 129
<tbody>
{% block table_body %}
{% for item in paginator.currentPageResults %}
... line 133
<tr data-id="{{ _item_id }}">
... lines 135 - 143
{% if _list_item_actions|length > 0 %}
... line 145
<td data-label="{{ _column_label }}" class="actions">
{% block item_actions %}
{{ include('@EasyAdmin/default/includes/_actions.html.twig', {
... lines 149 - 153
}, with_context = false) }}
{% endblock item_actions %}
</td>
{% endif %}
</tr>
... lines 159 - 164
{% endfor %}
{% endblock table_body %}
</tbody>
</table>
</div>
... lines 170 - 189
{% endblock main %}
... lines 191 - 244

The template it renders - _actions.html.twig - generates a link at the end of the row for each action.

Let's dump _list_item_actions to see exactly what it looks like.

Ah, ok! It's an array with 3 keys: edit, show and delete. We need to remove that delete key, only if the entity is published. But how?

Here's my idea: if we override the item_actions block, we could remove the delete key from the _list_item_actions array and then call the parent item_actions block. It would use the new, smaller _list_item_actions.

Start by deleting everything and extending the base layout: @EasyAdmin/default/list.html.twig... so that we don't need to duplicate everything:

8 lines app/Resources/views/easy_admin/list.html.twig
{% extends '@EasyAdmin/default/list.html.twig' %}
... lines 2 - 8

Next, add block item_actions and endblock:

8 lines app/Resources/views/easy_admin/list.html.twig
{% extends '@EasyAdmin/default/list.html.twig' %}
{% block item_actions %}
... lines 4 - 6
{% endblock %}

Twig isn't really meant for complex logic like removing keys from an array. But, to accomplish our goal, we don't have any other choice. So, set _list_item_actions = _list_item_actions|filter_admin_actions(item):

8 lines app/Resources/views/easy_admin/list.html.twig
{% extends '@EasyAdmin/default/list.html.twig' %}
{% block item_actions %}
{% set _list_item_actions = _list_item_actions|filter_admin_actions(item) %}
... lines 5 - 6
{% endblock %}

That filter does not exist yet: we're about to create it.

Just to review, open up the original list.html.twig. The _list_item_actions variable is set up here:

244 lines app/Resources/views/easy_admin/list.html.twig
... lines 1 - 94
{% block main %}
{% set _list_item_actions = easyadmin_get_actions_for_list_item(_entity_config.name) %}
... lines 97 - 189
{% endblock main %}
... lines 191 - 244

Later, the for loop creates an item variable...

244 lines app/Resources/views/easy_admin/list.html.twig
... lines 1 - 94
{% block main %}
{% set _list_item_actions = easyadmin_get_actions_for_list_item(_entity_config.name) %}
<div class="table-responsive">
<table class="table">
... lines 100 - 129
<tbody>
{% block table_body %}
{% for item in paginator.currentPageResults %}
... lines 133 - 164
{% endfor %}
{% endblock table_body %}
</tbody>
</table>
</div>
... lines 170 - 189
{% endblock main %}
... lines 191 - 244

which we have access to in the item_actions block:

244 lines app/Resources/views/easy_admin/list.html.twig
... lines 1 - 94
{% block main %}
{% set _list_item_actions = easyadmin_get_actions_for_list_item(_entity_config.name) %}
<div class="table-responsive">
<table class="table">
... lines 100 - 129
<tbody>
{% block table_body %}
{% for item in paginator.currentPageResults %}
... line 133
<tr data-id="{{ _item_id }}">
... lines 135 - 143
{% if _list_item_actions|length > 0 %}
... line 145
<td data-label="{{ _column_label }}" class="actions">
{% block item_actions %}
{{ include('@EasyAdmin/default/includes/_actions.html.twig', {
actions: _list_item_actions,
request_parameters: _request_parameters,
translation_domain: _entity_config.translation_domain,
trans_parameters: _trans_parameters,
item_id: _item_id
}, with_context = false) }}
{% endblock item_actions %}
</td>
{% endif %}
</tr>
... lines 159 - 164
{% endfor %}
{% endblock table_body %}
</tbody>
</table>
</div>
... lines 170 - 189
{% endblock main %}
... lines 191 - 244

Creating the Filter Twig Exension

Phew! All we need to do now is create that filter! In src/AppBundle/Twig, create a new PHP class: EasyAdminExtension. To make this a Twig extension, extend \Twig_Extension:

28 lines src/AppBundle/Twig/EasyAdminExtension.php
... lines 1 - 2
namespace AppBundle\Twig;
... lines 4 - 6
class EasyAdminExtension extends \Twig_Extension
{
... lines 9 - 26
}

Then, go to the Code->Generate menu - or Command+N on a Mac - and override the getFilters() method:

28 lines src/AppBundle/Twig/EasyAdminExtension.php
... lines 1 - 2
namespace AppBundle\Twig;
... lines 4 - 6
class EasyAdminExtension extends \Twig_Extension
{
public function getFilters()
{
... lines 11 - 16
}
... lines 18 - 26
}

Here, return an array with the filter we need: new \Twig_SimpleFilter('filter_admin_actions', [$this, 'filterActions']):

28 lines src/AppBundle/Twig/EasyAdminExtension.php
... lines 1 - 2
namespace AppBundle\Twig;
... lines 4 - 6
class EasyAdminExtension extends \Twig_Extension
{
public function getFilters()
{
return [
new \Twig_SimpleFilter(
'filter_admin_actions',
[$this, 'filterActions']
)
];
}
... lines 18 - 26
}

Down below, create public function filterActions() with two arguments. First, it will be passed an $itemActions array - that's the _list_item_actions variable. And second, $item: whatever entity is being listed at that moment:

28 lines src/AppBundle/Twig/EasyAdminExtension.php
... lines 1 - 6
class EasyAdminExtension extends \Twig_Extension
{
... lines 9 - 18
public function filterActions(array $itemActions, $item)
{
... lines 21 - 25
}
}

Ok, let's fill in the logic: if $item instanceof Genus && $item->getIsPublished(), then unset($itemActions['delete']). At the bottom, return $itemActions:

28 lines src/AppBundle/Twig/EasyAdminExtension.php
... lines 1 - 4
use AppBundle\Entity\Genus;
class EasyAdminExtension extends \Twig_Extension
{
... lines 9 - 18
public function filterActions(array $itemActions, $item)
{
if ($item instanceof Genus && $item->getIsPublished()) {
unset($itemActions['delete']);
}
return $itemActions;
}
}

Phew! That should do it! This project uses the new Symfony 3.3 autowiring, auto-registration and autoconfigure services.yml goodness:

32 lines app/config/services.yml
... lines 1 - 5
services:
# default configuration for services in *this* file
_defaults:
autowire: true
autoconfigure: true
... lines 11 - 12
AppBundle\:
resource: '../../src/AppBundle/*'
exclude: '../../src/AppBundle/{Entity,Repository,Tests}'
... lines 16 - 32

So... we don't need to configure anything: EasyAdminExtension will automatically be registered as a service and tagged with twig.extension. In other words... it should just work.

Let's go. Refresh... and hold your breath.

Haha, it kind of worked! Delete is gone... but so is everything else. And you may have noticed why. We did change the _list_item_actions variable... but we forgot to call the parent block. Add {{ parent() }}:

8 lines app/Resources/views/easy_admin/list.html.twig
... lines 1 - 2
{% block item_actions %}
{% set _list_item_actions = _list_item_actions|filter_admin_actions(item) %}
{{ parent() }}
{% endblock %}

Try it again. Got it! The delete icon is only there when the item is not published. This was a tricky example... which is why we did it! But usually, customizing things is easier. Technically, the user could still go directly to the URL to delete the Genus, but we'll see how to close that down later.

Leave a comment!

  • 2017-10-09 axa

    Hello Victor,
    Yes, it is clear now. Thanks.

  • 2017-10-09 Victor Bocharsky

    Yo Axa!

    Good question! :) You're totally right, filterActions() requires 2 args, but you need to know how Twig filters work. The 1st argument to the Twig filter *is always* a value which is on the left of the pipe "|" char. In your case, the first argument to filterActions() is the value of "_list_item_actions" variable, because it's standing right before | filter_admin_actions(). So the *second* argument to filterActions() is always the *first* argument in Twig filer parentheses, i.e. "item" variable in "|filter_admin_actions(item)". Does it clear for you now? Also, you can read official docs about Twig filter: https://twig.symfony.com/do...

    Cheers!

  • 2017-10-07 axa

    Hello guys,
    You set up in twig "_list_item_actions|filter_admin_actions(item)" with one argument 'item'.
    But in Twig extension we have public function filterActions(array $itemActions, $item) with two arguments. How twig knows about $itemActions?

    Am I missing something?

  • 2017-08-15 weaverryan

    Yo Billy Van!

    This is a special feature of EasyAdminBundle. In the previous chapter, I quickly mention that in their docs, they talk about all the different options for overriding templates: https://symfony.com/doc/cur.... One of them is to create this easy_admin directory: the bundle is programmed to look there.

    Cheers!

  • 2017-08-14 Billy Van

    Hello guys! How to know name of directory (f.e. easy_admin) which EasyAdminBundle will automatically to use instead of default?