-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
#88 Add the DropdownAction #110
Conversation
Hey @alexandre-castelain. What if we could completely drop the The $builder
->addRowAction('multipleActions', DropdownActionType::class, [
'actions' => [
$builder->createRowAction('update', ButtonActionType::class, [
'href' => function (Product $product) {
return $this->urlGenerator->generate('app_product_update', [
'id' => $product->getId(),
]);
},
])
},
])
->addRowAction('multipleActionsCallback', DropdownActionType::class, [
'actions' => function (mixed $data) { // $data is row data, since its a row action
return [
$builder->createRowAction('update', ButtonActionType::class, [
'href' => function (Product $product) {
return $this->urlGenerator->generate('app_product_update', [
'id' => $product->getId(),
]);
},
])
];
},
])
; It would work the same with
I think, directly in the final class DropdownActionType extends AbstractActionType
{
public function buildView(ActionView $view, ActionInterface $action, array $options): void
{
$children = [];
$actions = $view->vars['actions'];
// Resolve the callable if parent is column value (the action is row action)
if ($view->parent instanceof ColumnValueView) {
$actions = $actions($view->parent->value);
}
foreach ($actions as $child) {
if ($child instanceof ActionBuilderInterface) {
$child = $child->getAction();
}
$children[] = $child;
}
$view->vars = array_replace($view->vars, [
'children' => $children,
]);
}
} And in the Twig, we could iterate through the |
I worked only on the row action at this time, but doing as you propose, we could do this :
Add a batchAction in a rowAction. This doesn't make any sense to me. Using a callback, we would be sure the builder has only the correct method to use for the current action type. For a DX point of view, it's easier if there's less options available.
Yeah, I like this idea. |
I think you're right. That could be handy for autocompletion. I still don't like the idea of requiring a callback for that, but maybe that's just me 😄 Also, remember that this also would be correct: $builder->addRowAction('multipleActions', DropdownActionType::class, [
'actions' => [
$builder
->createAction('update', ButtonActionType::class, [...])
->setContext(ActionContext::Row)
]
]) However, you can still define a child action with any action type. We can validate this in Also, we have to validate the context of each action though, probably in the
That is true, and this will throw an exception, because the option does not allow values of type However, another idea - we can change the definitions of the actions for the key-value map, like so: $builder->addRowAction('multipleActions', DropdownActionType::class, [
'actions' => [
'update' => ['label' => 'Update something'],
'delete' => ['label' => 'Delete something'],
]
]); and then, based on the context of the parent action (global, row, batch) create an action of correct type and context: final class DropdownActionType extends AbstractActionType
{
public function buildAction(ActionBuilderInterface $builder, array $options): void
{
// like in CollectionColumnType, we're saving the factory as attribute,
// so we can access it in buildView() without unnecessary dependency injection
// the symfony form component is also doing this in source in CollectionType
$builder->setAttribute('action_factory', $builder->getActionFactory());
}
public function buildView(ActionView $view, ActionInterface $action, array $options): void
{
/** @var ActionFactoryInterface $actionFactory */
$actionFactory = $action->getConfig()->getAttribute('action_factory');
$children = [];
$actions = $view->vars['actions'];
// Resolve the callable if parent is column value (the action is row action)
if ($view->parent instanceof ColumnValueView) {
$actions = $actions($view->parent->value);
}
foreach ($actions as $name => $options) {
$children[] = $actionFactory
->createNamedBuilder($name, DropdownActionType::class, $options) // here we use the correct action type
->setContext($action->getConfig()->getContext()) // here we use the correct context (global/row/batch)
->getAction()
;
}
$view->vars = array_replace($view->vars, [
'children' => $children,
]);
}
} I feel like we're so close to establish a solid API for the dropdown action, yet so far 😅 |
On the second thought, I think we are slightly... overthinking this. What if we simply create a
This can be handled without touching current builder API, does not have any weird quirks related to action context (is it row? is it batch?) nor action type (dropdown item is dropdown item, simple as that). final class DropdownActionType extends AbstractActionType
{
public function buildView(ActionView $view, ActionInterface $action, array $options): void
{
// resolve items, add to $view->vars
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver
->setDefault('items', function (OptionsResolver $itemResolver): void {
$itemResolver
->setDefaults([
'url' => '#',
'method' => 'GET',
'attr' => [],
])
->setRequired('label')
->setAllowedTypes('label', 'string')
->setAllowedTypes('url', 'string')
->setAllowedTypes('method', ['null', 'string'])
->setAllowedTypes('attr', 'array')
;
})
;
}
} Now, I don't know if this method has any drawbacks, but what if we render each item as a form - so it can do any given method? https://jsfiddle.net/oravm9bf/5/ <div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
Dropdown button
</button>
<ul class="dropdown-menu">
{% for item in items %}
<li>
{# Note: form element method is either GET or POST, other methods belong to hidden _method input #}
<form action="{{ url }}" method="{{ method == 'GET' ? 'GET' : 'POST' }}">
<input type="hidden" name="_method" value="{{ method }}">
{# Here we should merge "attr" with type, class and value attributes and render it #}
{# using "attributes" block, but for the sake of simplicity of this example, I'm skipping it #}
<input type="submit" class="dropdown-item" value="{{ label }}" {{ block('attributes') }} />
</form>
</li>
{% endfor %}
</ul>
</div> This is something I was thinking about to merge built-in <div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
Dropdown button
</button>
<ul class="dropdown-menu">
{% for item in items %}
<li>
{% if method == 'GET' %}
<a class="dropdown-item" href="{{ url }}" {{ block('attributes') }}>{{ label }}</a>
{% else %}
<form action="{{ url }}" method="POST">
<input type="hidden" name="_method" value="{{ method }}">
<input type="submit" class="dropdown-item" value="{{ label }}" {{ block('attributes') }} />
</form>
{% endif %}
</li>
{% endfor %}
</ul>
</div> What do you think? I think we can skip a lot of complexity, while also giving a lot of new functionality. Easier to use (API not changed, options easily validated), and easier to maintain. You can even handle each action with JavaScript, for example, with Stimulus, by simply adding an |
Hey, I'll try to focus on your second proposal, because it looks promising.
Clearly, this approach seems much simpler. On the other hand, I also find that we lose the reuse of other actions. For example:
I think if we go with this "simplified" approach, we might quickly find ourselves limited in more complex cases. However, I think that by slightly modifying your proposal, we could achieve something interesting. For example:
I’m not sure if I am entirely convinced by this proposal. Advantages:
Disadvantages:
Another proposal:
Advantages:
Disadvantages:
It would be ideal if 'actions' could accept either an array or a callback (yeah I know, another callback !) to retrieve the values of the current row:
|
Okay, let's ignore the options and builder API for a moment. Reusing other action types seems cool, and I would be all-in on that idea, but how do we render them? If I use a dropdown action, I would expect them to render as a classic dropdown list: If we want to reuse other actions, this is impossible to render in that way, because each action type HTML may or may not be suitable for a dropdown (take into account custom action types and themes): We cannot modify each child action HTML on render to make it suitable for a dropdown, as each type and theme can render those differently. Because we are not limiting how each action type can be configured and rendered (total freedom!), this also prevents us from reusing them in a dropdown without creating a monstrosity of a HTML 😬 In fact, I would say, that we:
|
Maybe a naive question, but: wouldn't it be possible to create a specific "sub" block? For the ActionLinkType, for example:
This way, depending on the theme, we can "redesign" the way the HTML is displayed in the Dropdown without touching the default HTML. I am not used to working with Twig themes like this, so I might be completely off track! |
Yeah, we could do it in the same way the column is split into two parts - a "header" and a "value". Currently, action Twig blocks are suffixed with Also, we would have to add something like I know it would be possible, but I really don't want to increase complexity like that just for the sake of a single dropdown action type. On the other hand... shouldn't this be a theme-specific feature? For example, we could add a theme that groups actions into one dropdown, and renders them properly (the theme will simply contain proper HTML for each action type). This way, we can use this theme in data tables that require this feature. This also means, that we would no longer mix and match inline actions with dropdown actions, but I think that's how its done in, for example, a EasyAdmin - actions are either inline or not. For now, we could do something like |
Hi Kreyu! Sorry for my very slow response, I've been quite busy these past few months. I'll make some time to work on this bundle until the end of the year. I hope to contribute to the project so it can meet some of my needs. If we make enough progress, I might be able to allocate some work time to it as well. After rereading our conversation several times, I think I initially proposed something too complex. The idea is simply to be able to add multiple actions in a dropdown. I would like it to be relatively simple to add a new type of action to a dropdown, without too many files needing modification. I like the idea of simply integrating a twig file to switch from inline to dropdown. Now, it would be interesting to define how to create these actions, which "builder" to use. And also, where do we use it? In the "DataTableType"? In the "DropdownActionType"? Somewhere else? I really want to work on this point, but I don't know exactly how to start. Do you have any advice? If you'd prefer to discuss it verbally, we can chat elsewhere, whichever you prefer. Have a great day! |
Hey @alexandre-castelain! I've tried doing something related to dropdown actions in one of my projects and now I have some conclusions. I'll collect my thoughts and try to write it down this week. Cheers! |
Sorry for the delay. In one of my projects, I had a data table with 6+ actions (icons only), which started to be... not quite intuitive. Previously, I suggested:
In custom theme, I had changed the {% block column_actions_value %}
<div class="text-end" data-turbo="false">
<div class="dropdown position-static">
<button class="btn dropdown-toggle align-text-top"
data-bs-toggle="dropdown"
data-bs-boundary="window"
aria-expanded="false"
>
{{ 'data_table.actions'|trans }}
</button>
<div class="dropdown-menu dropdown-menu-end">
{% for action in actions %}
{# adding "dropdown" variable to the context #}
{{ data_table_action(action, { dropdown: true }) }}
{% endfor %}
</div>
</div>
</div>
{% endblock %} Then, I had changed the {% block action_button_value %}
{% if dropdown ?? false %}
{% set attr = attr|merge({ class: (attr.class|default('') ~ ' dropdown-item mx-1')|trim }) %}
{% set icon_attr = icon_attr|merge({ class: (icon_attr.class|default('') ~ ' me-1')|trim }) %}
{# ... #}
{% endif %}
{{ parent() }}
{% endblock %} And that worked fine until I tried clicking the confirmable action. The modal doesn't work that way, because it is rendered inside the dropdown. So inside the {% block column_actions_value %}
{# ... render dropdown and its items #}
{# Render confirmation and modal action type modals outside of dropdown, otherwise it won't render #}
{% for action in actions %}
{% if action.vars.confirmation %}
{% with action.vars %}
{{ block('action_confirmation_modal') }}
{% endwith %}
{% endif %}
{% endfor %}
{% endblock %}
{% block action_button_value %}
{# ... configure attributes to render the action as dropdown item #}
{# Set confirmation to false, as we're manually rendering confirmation modal outside of the dropdown #}
{% set confirmation = false %}
{{ parent() }}
{% endblock %} This is... fine, but shows a really noticable issue - if you render an action as dropdown item, you cannot provide additional HTML to render outside the dropdown. For example, if you create So, doing it this way:
I'm open for making this as simple as possible, for example a single |
No worries, we all have a life! :) I find the thought process interesting. A few remarks:
In the end, perhaps splitting the views for greater simplicity is the way to go: one "inline" view and one "list" view. That’s what we use in our project, and it turns out to be quite simple to manage—the code remains clear and easy to maintain. |
I'm not sure whether this bundle should provide its own scripts for handling the modals, especially if Bootstrap provides its own scripts used in most cases (I think, speaking from experience). For me, what seems to be most intuitive, is simply a $builder->addRowAction('advanced', DropdownActionType::class); Now, the dropdown requires items, so we provide them via $builder->addRowAction('advanced', DropdownActionType::class, [
'items' => [
$builder->createRowAction('toggle', DropdownItemActionType::class),
$builder->createRowAction('remove', DropdownItemActionType::class),
],
]); Thanks to that, we can mix and match various action types: $builder
->addRowAction('details', ButtonActionType::class)
->addRowAction('update', ButtonActionType::class)
->addRowAction('advanced', DropdownActionType::class, [
'items' => [
$builder->createRowAction('toggle', DropdownItemActionType::class),
$builder->createRowAction('remove', DropdownItemActionType::class),
],
])
; Rendering those action types seems simple: {# @KreyuDataTable/themes/bootstrap_5.html.twig #}
{% block action_dropdown_value %}
<div class="dropdown">
<button class="btn btn-primary" data-bs-toggle="button">...</button>
<ul class="dropdown-menu">
{% for item in items %}
<li>{{ data_table_action(item) }}</li>
{% endfor %}
</ul>
</div>
{% endblock %}
{% block action_dropdown_item_value %}
<a class="dropdown-item">...</a>
{% endblock %} Now, let's assume that we have a {% block action_modal_value %}
<button data-bs-toggle="modal" data-bs-target="#modal">...</button>
<div id="modal">...</div>
{% endblock %} If we use this action type as a dropdown item, the dropdown will render the button, what most likely will look weird and out of place. I think creating a separate {% block action_modal_dropdown_item_value %}
<a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#modal">...</a>
<div id="modal">...</div>
{% endblock %} As I described before, because the modal is rendered inside the dropdown, it won't show up. It has to be rendered outside the modal. I think this shows that some action types should allow rendering via multiple blocks: {% block action_dropdown_value %}
<div class="dropdown">
<button class="btn btn-primary" data-bs-toggle="button">...</button>
<ul class="dropdown-menu">
{% for item in items %}
{# This will render "action_*_value" #}
<li>{{ data_table_action(item, 'value') }}</li>
{% endfor %}
</ul>
{# This will render "action_*_outer" #}
{{ data_table_action(item, 'outer') }}
</div>
{% endblock %}
{% block action_modal_dropdown_item_value %}
<a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#modal">...</a>
{% endblock %}
{% block action_modal_dropdown_item_outer %}
<div id="modal">...</div>
{% endblock %} This would require extending the Twig functions to accept a block suffix instead of always rendering the Speaking of |
I didn't explain it earlier, but in my mind, the content of the modal is fulfilled with an AJAX request. So the If we don't do this way, for each modal we should create a specific form theme. I don't find this really fancy.
I'll work on that and show you what I manage to do. |
# Conflicts: # assets/package.json
Hi @Kreyu ! With the MR I created, it's possible to have a dropdown, with multiple actions, in the table.
In the end, I just created new ActionType, and it's really easy to add new ones based on other actions : just extend them (via the For the What do you think about this ? I will wait the changes on the |
Great! That's what I had in mind as well. I will review the code and add comments there, thanks! |
I'm not sure we should merge it yet, there's no documentation and no test. I should give it a try ! |
src/Action/Type/DropdownItemActionType/LinkDropdownItemActionType.php
Outdated
Show resolved
Hide resolved
assets/controllers/dropdown.js
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this controller at all? I don't think we really need a custom solution to dropdowns - the Bootstrap provides its own scripts we're using with data params, and base theme doesn't need a functional dropdown, because it is not meant to be used directly. I would assume that themes that extend the base theme should provide their own implementation of dropdown, like we do with Bootstrap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know why, but I didn't manage to make any dropdown from bootstrap properly.
If this works well with bootstrap, I totally agree with you, we can remove this controller.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll have to check the issue with Bootstrap modals in an application and try some things. I'll come back to you.
I think it would be inappropriate to require tests when there's no tests for actions at all 😄. Recently, I've added tests for all column types, and I will do it soon for actions, so no worries - I can add test for dropdown-related types later. Adding documentation would be excellent, even simple reference pages for new action types. I can do it myself later if you prefer, or if you encounter some issues with documentation. Running the docs locally should be easy though: https://data-table-bundle.swroblewski.pl/docs/contributing.html#documentation Overall, great work, thank you! I've added some comments to the review. |
Now that I think about it, what would you expect with Because currently it will, because Recently I've been thinking whether we should ditch the inheritance in core completely to prevent issues with extensions. The same way Symfony Form |
Thanks for the changes after the review, I'll try to test it out as soon as possible in a real application! |
Oops, sorry, ignore that review request. |
* Changed the renamed attribute * #88 Create the DropdownActionType * #88 Add the bootstrap 5 theme * #88 Extract the RowActionBuilder methods in another interface. * #88 Run php-cs-fixer * Add the LinkDropdownItemActionType and handle it in the views * Format code * Remove the RowActionBuilderInterface * Force the ActionBuilderInterface type * Remove useless style * Make the confirmation modal work in the dropdown * Move files to have everything in the same folder * Revert the DataTableBuilderInterface.php * Remove dropdown controller, add Bootstrap data parameters to use its own dropdown implementation, improve dropdown attr * Add docs for dropdown-related action types --------- Co-authored-by: Alexandre Castelain <[email protected]>
Thanks again for all your efforts @alexandre-castelain. I've added small improvements here and there and added the documentation. Unfortunately, I had to (or I think I had to, I don't really know) create a separate branch for that, since I was unable to push to your branch. Merged in #170. I'll draft a new release next week, since there are multiple changes waiting to be released. |
Hey !
This is the first step to create the DropdownAction. Do not merge it as is.
On the theme side :
On the PHP side :
This is the way I want to add row actions :
As you can see, I inject a RowActionBuilderInterface in the callback to add the actions. And the DataTableBuilderInterface extends it.
I like it, because we could add a DropdownAction to a DropdownAction if we want it. It adds a lot of flexibility.
At this point, I'm not sure where I should resolve this callback. Neither if we should inject a DataTableBuilder or another class.
What do you think about it ? Any advice ?
Have a good day !
Alexandre