Needs work
Project:
Drupal core
Version:
main
Component:
forms system
Priority:
Normal
Category:
Feature request
Assigned:
Unassigned
Issue tags:
Reporter:
Created:
30 Dec 2012 at 18:46 UTC
Updated:
17 Apr 2025 at 17:30 UTC
Jump to comment: Most recent, Most recent file
Comments
Comment #1
sunComment #2
sunConversion process has started on #1876712: [meta] Convert all tables in core to new #type 'table'
#1814916: Convert menus into entities, or more specifically, EntityListController overrides, could use this facility, because they currently have to override the entire
buildHeader()andbuildRow()methods, even if they just want to inject a single column.There are multiple ways to approach this:
#weighton header/body columns.All implementations would have to define a #weight for each column. And the defined weights have to leave sufficient room for other code/modules to inject additional columns in between existing; i.e.:
The primary problems with that are:
Columns do not have a #weight by default, or to be more concise, #weight defaults to 0. Thus, if you inject a new column with #weight 25, then it comes last, which is not what you intended.
We'd have to automatically inject #weight values for all columns that do not have a #weight yet, and give them sensible #weight values; e.g., starting from 0 or 10, in intervals of +10. We'd probably want to inject the additional #tableselect column with -100, and the additional 'weight' column for #tabledrag with a weight of 990, and we'd have to give the 'operations' column a weight of 1000 so it comes last.
Furthermore, this would have to happen in retroactively, since
hook_form_alter()runs before any kind of processing; i.e., directly after the table has been defined as form/render structure.Overall, this idea/approach doesn't sound very reliable/feasible.
Sort all body columns according to the column definition in #header.
Essentially, we'd introduce a hard binding between the #header definition and the
element_children()of a table.Requirements:
element_children()) have to use the identical keys; i.e.:To inject a new column, you 1) simply add the column as child element to each row and 2) inject the column into the desired position in #header. To reposition a column, you change the #header definition only.
Injecting or repositioning a column within #header will still require futzing with array keys, but it would be a one-time operation for #header only, not for every single table row. (That is, unless we'd mix the above #weight concept into #header, but I'm less convinced of #weight in the first place.)
The #pre_render of #type table would need some more complex processing to sort all body columns by the order in which the keys appear in #header. I don't know what we'd do about keys that don't match, but we could simply add them last.
This approach sounds more feasible. However, it would be an API change (for #header).
Any other ideas?
Comment #3
quicksketchI came up with similar options @sun in the first iterations of #1833716: WYSIWYG: Introduce "Text editors" as part of filter format configuration (when we were manually injecting columns before the #type = table). I went with option #2 in the early versions of that patch.
I prefer the #2 approach for multiple reasons:
- Finding a header key is no longer dependent on a translated string (since it will now have a machine key).
- The header key having a consistent name means it's easy to find and other modules could easily change the translated string for a header column. i.e.
$form['table']['#header']['title'] = t('Some other title');.- The named approach makes it easy to handle situations where you either want your column to always be in a static position (say the second column) or when you want your column to always be after/before another column.
Even though array_slice() is probably the more efficient way to handle slotting in a column, with the second approach developers may be more comfortable with a significantly easier to understand typical foreach() loop.
Comment #4
quicksketchHere's alternative, more-efficient (but more confusing) code for splicing an item into the header. Assuming we went with option 2. @sun might be able to come up with something more readable or further efficient. Given the choice, I'd probably fallback to the "easy" approach using the foreach loop().
Comment #5
sunAttached patch implements option #2.2.
Comment #6
quicksketchThis is an interesting approach that I wouldn't have thought of. My attempt would have been to use the keys from the $element['#header'] array to determine the order, rather than using element_children() to resort the columns. However the downside of that approach (but solved by the one in the patch) is that elements would have required a matching key in the header. This approach doesn't require a matching key, which should be useful in situations where the table doesn't have a header at all.
However if the developer were to make a mistake and skip a key, or a column was added without a header, the new column would end up in an unpredictable location, possibly throwing off the column matching.
Additionally, if the columns have weights already, the order could come out different than intended also.
Both of these seem like minor inconveniences, outweighed by the ability to easily inject a new column. In *every* place I've ever done a table like this, the column order is established by definition order (that is, they always have a weight of 0). Tables are always screwy if you don't have enough headers to match the columns and shouldn't be allowed anyway. Unless there are other objections, this looks good to go.
Comment #7
damien tournoud commentedI am for mandating matching keys, and mandating a #header structure, even if we can give the option not to display it (probably something along the lines of
#title_display).Comment #8
sunWondering what @moshe thinks.
We might want & need to wrap the sorting logic into an
!empty($element['#header'])to avoid PHP notices.@quicksketch:
Regarding existing #weight properties on columns, yeah, those would be unconditionally overwritten, but in the end, that's the entire point of mapping the order from #header into table body row columns, right?
@Damien:
Making keys in #header required is probably a good idea, yeah. Otherwise, the alter-ability of each #type table would depend on whether a module author used an associative array in #header or not; i.e., inconsistency--.
Not sure though whether to require #header for all tables. It sounds "aggressive" to me, but yeah, we probably have to.
Also not sure how making the keys in #header required would change the proposed implementation? I played with a couple of alternate ways (array sorting functions, array_multisort(), etc), but all of them required much more expensive processing logic.
I don't think we really have to care for key/column mismatches. The resulting table output will be broken either way, so why baby-sit?
Comment #9
quicksketchAh, you're correct. I read this line incorrectly:
I read it as:
Which of course doesn't make any sense. I thought the existing #weight property was factoring in somehow, but it's not. Works for me.
One more thing to consider, what effect does this have on use of colspan? Prior to having the Operations column use a dropbutton, using a colspan of 3-4 was common on the operations column. However if you have 3 columns at the end, they obviously can't all have the same parent key in the $element['#table'] array, so the header column won't be able to group all of them together.
Comment #10
sunGood question. The idea of colspans is generally incompatible with the proposed design thus far, since every key has to map to a column.
The typical and strict engineering answer to that would be: #header keys for colspans, sub-keys for columns.
Ugly. :) Although I almost guess that anything else would be horror in terms of declaration/API, as it's sorta guaranteed that it would involve custom + strangely named sub-properties.
Ugly. :)
Or even dynamic/conditional...
...but that's invalid and obsolete already, since it is the prototype incarnation of "unpredictable" in a multidimensional modular system.
Comment #11
moshe weitzman commentedPatch so far looks good to me. I don't have an opinion yet on latest colspan discussion.
Comment #12
tstoecklerI totally like the idea of mapping columns to header keys, because of its intuitiveness in declaring, i.e. I can do
$form['table'][5]['title'] = 'Foo';and have have 'Foo' actually appear in the correct column. I don't understand, though, why we have given up on weights totally. We already need to support to specify header elements as arrays for anything non-trivial, so from the declaration perspective, I would find it weird for the following not to work:Above the argument was made that you cannot possibly find default values for weights that are flexible enough for each possible use-case. But that is already the case with regular form elements, as well. It is the most common thing in the world if module B alters module A's form that it has to adjust the form elements' weights to make everything appear in the correct order. In core we can just go and change the original weight directly, but in contrib this is extremely common. And, especially think for things like 'operations' and tabledrag's 'weight' I do think we can find defaults that satisfy the 80% use-case. I definitely like setting a couple weights a lot more than array_splice()ing, even if it's just the header. And, again, I have never heard anyone argue for the same thing on the top-level $form level. Neither have I ever seen anyone array_splice()ing the whole $form. So I don't understand why here we are settling for something that seems sub-optimal from the start.
For supporting colspans I would suggest #weight as well. For example: (I know we don't do operations like this anymore, but bear with me)
Of course, if you don't specify any weights, the declaration order would simply be taken over.
I think that would be the most intuitive way to declare this. I don't find specifying sub-keys in the header that great, but if we do want to do that, I think it should be explicitly declared instead of implied by additional array nesting. I.e. I would vote for @sun's second option (only that I would leave 'colspan' as is and name this bikeshed('sub-keys')).
Comment #13
quicksketch@tstoeckler: You're second example uses an interesting but non-traditional way of declaring the operations column, because it defines multiple items inside what would normally be considered a single cell. If we adopted that structure officially, you wouldn't really need to specify colspan in the #header at all, because it would be apparent how big it needs to be based on the number of "subcells" that are specified in the matching key. Then we could keep our #header declaration simple to declare and just deduce that information from the matching key. However this would mean the logic for theme_table() would become even *more* messy, retroactively adjusting the header colspan based on the contents of cells.
Comment #14
tstoecklerWell, I think if we do go with the concept of 'named columns' (which, to be clear, I totally think we should), I think the following would be extra weird:
I don't really see how we can reasonably place all elements in the same array level in the case of colspans.
Thinking about that, the example code in #12 might actually turn into a huge DX WTF on its own, because element_children($form['table'][0]) returns things which are maybe table cells, but not necessarily.
Hmm...
Comment #15
sunIdea: #type 'colspan'
The #type 'colspan' doesn't actually have any meaning for the element itself. It would be primarily used by
drupal_pre_render_table()and the table #header sorting algorithm in order to determine which header columns need a colspan.The #header definition would not actually contain a 'colspan' property, nor the amount of columns, or any column key names. Instead, the colspan is fully derived based on the table body row data declaration.
In the above, the 'Operations' header column automatically gets a 'colspan' => 2 assigned to it, since the 'operations' column is of #type 'colspan' and contains two child elements.
If you'd replace/_alter that #type from 'colspan' into 'dropbutton'/'operations', then the 'operations' column turns into a single and there's no more colspan involved.
This approach would make some sense to me. It has a logical/architectural though: It's actually the reverse of what the #header definition is doing for the sorting otherwise; i.e., the table body row columns impact the #header definition, but for the colspan facility only - aside from that, the #header definition's impact is negated: it has an impact on the table body row column order.
Comment #16
quicksketchIs this what we're doing already for operations (a #type = 'dropbutton')? If so, this solution looks ideal, that you could switch between using a dropbutton or multiple columns simply by changing a single word, and the structure of the form continues to stay the same between them. This approach has great advantages from an "alterability" standpoint. It's easy not only to add an additional "normal" column, but it's also easy to add into an operations column and have the header colspan automatically adjust.
Comment #17
sun@Dries complained about temporary usage of
array_slice()in #1855402: Add generic weighting (tabledrag) support for config entities (DraggableListController), so I'm bumping this to major.I'm not 100% sure, but #15 looks like the best solution that's able to cope with colspans thus far. The general idea is that you always need data for a column.
#15 makes both ways explicit through the render array structure.
What #15 does not allow is to remove a colspan entirely. E.g.:
Comment #18
quicksketchThat sounds like a fine trade-off. And it's still possible, just difficult. Considering dropbuttons are already what we're using in most places and we already have this problem, this isn't introducing a new problem (just the same problem in a different format). I expect it to be uncommon enough it doesn't need any further consideration.
So, +1 #15. I'd love to have it.
We got away with array_splice() and manual changes in #1833716: WYSIWYG: Introduce "Text editors" as part of filter format configuration. So editor.module is another place in core that we can "fix" in this issue's patch.
Comment #19
joelpittetThis may have impact on this issue:
#1948374: #type 'table' allow attributes on table cells
Comment #20
jibran#5: drupal8.table-column-sort.5.patch queued for re-testing.
Comment #21
andypostLook really bad that #header depends on data columns.
I'd prefer more strict as #7 suggests! the only questionable part - should we drop data cells that does not have keys in #header?
Comment #22
benjifisher#5: drupal8.table-column-sort.5.patch queued for re-testing.
Comment #23
thedavidmeister commentedLooks like the patch no longer applies:
error: core/modules/filter/filter.admin.inc: No such file or directory
Comment #24
thedavidmeister commentedComment #25
wim leersI was looking through the remaining @todos for
editor.moduleand leveraging the changes of this issue to simplify some code is one of them.Can this still happen in D8? Or is it D9 material?
Comment #26
andypostThis issue is about approach and DX but there's no conclusion, suppose we should fix this in D8
Comment #26.0
andypostUpdated issue summary.
Comment #27
tim.plunkettWe worked around this so far, except editor.module.
Comment #28
sunComment #29
andypostComment #30
rosk0Reroll.
Changes in filter.admin.inc was overridden by #1987124: Convert filter_admin_format_page() and filter_admin_overview() to a Controller in 93ea9a472b988ac8d1aa56f947bc916c24f8ea73, so doesn't included.
Comment #31
akalata commented#30 didn't have a patch
Comment #45
smustgrave commentedThank you for creating this issue to improve Drupal.
We are working to decide if this task is still relevant to a currently supported version of Drupal. There hasn't been any discussion here for over 8 years which suggests that this has either been implemented or is no longer relevant. Your thoughts on this will allow a decision to be made.
Since we need more information to move forward with this issue, the status is now Postponed (maintainer needs more info). If we don't receive additional information to help with the issue, it may be closed after three months.
Thanks!
Comment #46
amateescu commentedThis looks like a valid feature request still.