Problem/Motivation
On #2900409: [Meta] Improve UI of Reference field settings form (postponed on this issue), we realized that resolving that issue would be much simpler if the Select element had the ability to sort its options by their labels (after translation). This would presumably be useful for many other use cases.
Proposed resolution
Add options to the Select form element to sort by label. The sorting should allow skipping the first N elements (so that things like "- NONE -" stay at the top). Turned off by default (for backwards compatibility).
Note that option labels will need to be cast to string in order to sort them. However, the labels passed into the Twig templates should be left as TranslatableMarkup objects (assuming they were translatable objects before).
Completed tasks
- Make a patch.
- Add a test to \Drupal\Tests\system\Functional\Form\FormTest::testSelect() to test the new behavior.
Remaining tasks
- Credit alexpott and benjifisher from the other issue, who worked on, reviewed, and consulted on this idea.
User interface changes
Select elements will have optional sorting by label (turned off by default).
API changes
Select elements will have new optional properties to facilitate sorting.
Data model changes
None.
Release notes snippet
The #type => 'select' form element (Select form element class) has new options to allow the options in the list to be sorted by their labels:
- #sort_options: Set to TRUE (default is FALSE) to sort the options by their (translated) labels.
- #sort_start: Set to an integer to start the sorting at that index in the labels. For instance, if your first option is "- none -", start the sorting at index 1 to make sure that first option stays at the top of the list of options. If the empty option is being automatically added, this defaults to 1; otherwise, it defaults to 0.
Either of these can be used within option groups, to sort that group of options. See the Select form element class documentation for more information on Select elements.
Usage example, if you made a list of $options elsewhere in the code:
$form['example_select'] = [
'#type' => 'select',
'#title' => $this->t('Select element'),
'#options' => [
'first' => $this->t('First option'),
] + $options,
'#ajax' => TRUE,
'#sort_options' => TRUE,
'#sort_start' => 1,
];
Comment | File | Size | Author |
---|---|---|---|
#35 | 3065903-35.patch | 15.46 KB | jhodgdon |
#35 | interdiff.txt | 2.23 KB | jhodgdon |
Comments
Comment #2
jhodgdonHere's a patch. It's slightly modified from the patch as it was in #2900409-59: [Meta] Improve UI of Reference field settings form:
- Allows sorting within option groups [documentation modified accordingly]
- Includes a test. Slightly crude (since it just uses strpos to verify that the elements are in order, but it's quick/easy/accurate).
The test passes locally, and I also manually tested it on the EntitySelect (parent issue) with various values of the sort_start property, and it seems to work fine.
Comment #3
jhodgdonFixing 2 coding standards messages.
Comment #4
benjifisherI have a few nit-picks and one more substantive suggestion at the end:
Too bad: PHP 7 is required for new Drupal 8 sites, but not for existing ones, so we still cannot use the null coalescing operator. This makes me sad. I would leave off the optional parentheses when setting
$sort_start
, but I understand that there are arguments on both sides. Personally, I assume people know that=
has lower precedence than?:
, but I put in the optional parentheses in! ($object instanceof 'class')
.Again, it is a matter of preference. I would use
return strcmp((string) $a['label'], (string) $b['label']);
. Also, is there any reason for indenting four spaces inside the anonymous function?Generally, we try to avoid any sort of logic in automated tests. The
foreach
loop is only 6 lines shorter than the definition of the$unsorted_options
array. I suggest making a similar explicit definition of$sortable_options
. Of course, this code is in the supporting module rather than the test iteself, so perhaps we should leave it as is.I fixed the indentation, but did not change the example. Looking at the existing documentation in Select.php, I notice the
#empty_option
and#empty_value
options. I think we should encourage people to use these options rather than handle the empty option as in this example. (I will add a similar suggestion on #2900409: [Meta] Improve UI of Reference field settings form.) So I suggest changing this to something like'first' => $this->t('First option'),
.I feel a little foolish, since I was the first to suggest (on Slack) adding the
#sort_start
option, and it was explicitly to handle the "- None -" option. I now wonder if there are any uses for this option. Are we adding an option that no one will ever use?Looking at
form_select_options()
after applying the patch from #3, I see that the code for handling the empty option comes after the code for the sort options. It should still work, but I think it would be a good idea to check this in the test, to make sure that future changes do not break it.Comment #5
jhodgdonRegarding the nitpicks...
- The 4-space indentation in the inline function is a mistake, due to my editor settings. I'll fix that. Surprised Coder didn't flag it... I will make the change and then we can see if Coder flags it. ?!?
- I like having extra parens. My philosophy on code is to make it easier to read, and the parens do that for me (I don't have to think about operator precedence). OK to leave them?
- The reason I used the foreach in the FormTestSelectForm was that during development, it saved me a lot of copy/pasting, and also that way I was sure the two arrays were the same except the keys. But I can take that out.
Regarding the question of sort_start, I do think there is a use case for it, because sometimes there is more than one "stay at top" element in a select list. For instance, in a list of all the countries in the world, a web site in the US might want to have "Select one" and then "United States" and then "Canada" at the top (their most likely customers), and then all the other options (sorted in the user's current language).
So, I think sort_start makes sense to leave in. But we should define better what it means with the empty option. I actually think the sorting should come after empty option, and we should document and test that behavior. So, setting this to Needs Work to do that.
So if I make a patch that fixes the 4-space indentation, takes out the foreach in FormTestSelectForm, changes the order of the empty/sort operations, and documents/tests that... does that sound like a good plan?
Comment #6
jhodgdonSo... Regarding that last point in comment #4, adding the empty option to the options list actually happens in Select::processSelect(), which is before it gets to the theme layer, and hence before everything in form.inc. There is some *testing* in that form.inc code to see if there is an empty option and either select it or not, but the empty element is already there if it's coming.
Given all of that, I made a new patch:
- Fixes the nitpicks (I hope). :) I made a method on the form test class that creates options with a prefix, because (see below) I now needed 3 different prefixes. I think it's much easier to maintain in one place rather than having 3 copies of the same complicated select list.
- Updates documentation to define how sort_start works with empty_value [as noted in #5, I'm pretty sure there's a use case for the flexible sorting, and given that the empty value is added in the process step, it happens before sorting, so we'll need this internally if nothing else]
- Adds a test for that behavior
Comment #7
benjifisherLooking more closely at
form_select_options()
, I see I was very wrong in my earlier comments. Thanks for getting it straight! On the plus side, I no longer feel foolish for suggesting the#sort_start
option.Thanks also for expanding the test coverage.
I have just two tiny complaints, both about consistency:
I am not sure why we have essentially the same documentation in two different places, but the small differences between the two bother me. (Only the second mentions rendering and translation, and only the second mentions the empty option.) It is too easy to imagine myself staring at the documentation in form.inc, reading it over and over, and thinking, "I am sure there is something about the empty option here, but I cannot find it."
I find it easier to read, in this case, without the optional parentheses, but my intention in #4.1 ("I would ...") was to mention that preference, not to insist. It is OK to keep the optional parentheses, but please be consistent in the two parts of the
if
block.I tested by applying the patch from #2900409-63: [Meta] Improve UI of Reference field settings form and installing the Umami demo profile. I checked in English and Spanish, and I also checked using the "empty option" settings. To confirm that sorting took place after translation, I paid attention to "Language [langcode]" (en) and "Idioma [langcode]" (es). In all cases, the patch worked as expected. (I also hacked "- None -" to "None -" to make sure that it would not stay at the top if it were part of the sorting.)
This needs a change record, so I am adding the corresponding tag. The release notes snippet is a good start on that.
Speaking of the release notes snippet, I am updating it as I suggested in #4.
Comment #8
jhodgdonThanks again for your thoughtful and careful reviews!
I think the right answer on your first point is to remove the docs in form.inc in favor of the ones on Select, because Select should be the canonical source. I've compared all the property docs in form.inc to the ones on Select, and added docs so that everything is documented in the one place.
Second -- I agreed in the end about removing the parens but forgot one set. Fixed.
So, here's a new patch.
Also created a change record:
https://www.drupal.org/node/3068163
Comment #9
benjifisher@jhodgdon:
Thanks for the updates. Having the documentation in a single place is even better than having it consistent in two places. I am not sure how these docblocks are processed, nor what the standards are, so I will defer to you on which one to keep.
I did not re-test since these changes (except for one line) "only" affect the documentation blocks.
I reviewed the CR, and it looks good.
Comment #10
jhodgdonAdding tag, as this is blocking fixing a usability bug (parent issue).
Comment #11
alexpottAs far as I can see there is no test coverage of using #sort_start in an option group.
This bit of the test is a bit confusing because we're lumping all of the selects in one go. I think this might be better written as:
That way we'll get better error messages and it's a bit easier to see what's wrong because each select element is tested individually.
These should be using the correct assertion.
$this->assertSession()->responseNotContains()
Comment #12
jhodgdonGood points! Here's a new patch and interdiff. Only tests and test code were changed; the code in Select and form.inc has not changed. Test still passes locally.
I did make an interdiff, but I think it is actually less confusing to review the two test files in the patch instead of the interdiff (which I think is confusing).
Comment #13
benjifisherThe
$content
variable is not used anywhere. At least eliminate the variable. Can we eliminate the entire line, or does it have side effects that we need?The patch in #12 addresses the feedback in #11. I am happy to mark the issue RTBC if you do not like the following suggestions, but I think they will make the helper function a little easier to read.
Kudos for using
string[]
in the@param
comment instead of the less specificarray
. Let's add a type hint to the function declaration:validateSelectSorting($select, array $order)
.The variable name
$option
does not suggest a NodeElement object: I suggest$node
(as below) or$element
. I know I am in the minority, but I think the ternary operator is clearer in these situations:The variable name
$field
does not suggest a NodeElement of typeselect
. I also think it is clearer to eliminate that intermediate variable:You can also consider combining the last two lines (eliminating the
$options
variable) and/or using an anonymous function insidearray_map()
(eliminating the$option_map_function
variable).Comment #14
jhodgdonThose look like good suggestions to me! That first one -- line left over from the old design of the test.
Here's a new patch and interdiff. Only the test class changed... we will need to verify the test still passes on the test bot (and no coding standards problems), as I am not on my working machine and did not run the test locally, but it should be OK I think.
Comment #15
benjifisher@jhodgdon:
I am glad you liked my suggestions.
As I said in #13, I think that all the points in #11 have been addressed, so back to RTBC.
We have not updated the "Remaining tasks" in the issue description for a while. I just took care of that.
Comment #16
goodboy1. This works for alphabetical sorting only and list [1,2,11] will be sorted as [1,11,2] . Using strnatcmp() instead strcmp() will allow natural sorting (https://en.wikipedia.org/wiki/Natural_sort_order)
2. What about sort_direction - 'ASC' (default) and 'DESC' ?
Comment #17
catchBack to needs review for #16.
Comment #18
jhodgdonHm. The point of this issue is that sorting alphabetically by label needs to be done after translation, and these new properties allow you to do that without translating earlier than necessary and introducing string safety issues. If your labels are numeric, then they don't depend on translation, and you can sort pre-translation (or by array key) and not use the new sort properties. I also think that DESC sort order would be a very rare use case for post-translation alphabetic sorting.
I guess mixed letters and numbers, such as file names, would be another case for strnatcmp, but again there, it wouldn't be translation-dependent and could be done before translation by the function that is setting up the Select element.
So... My inclination is that these properties should be sufficient to cover the intended use case, which is providing string-safe sorting, post-translation. Any other thoughts? Any actual use cases where strnatcmp or DESC makes sense, for post-translation alphabetic sorting by label?
Comment #19
goodboyList [step1,step2,step11] will be sorted incorrectly, for example. I suggest to make two types of sorting: alphabetic (by default) and natural. And yet, for order, two kinds of sorting. If my amendments are adopted, it makes sense to think about the sorting options in the form of an array.
Comment #20
jhodgdon(Assuming the status change was a mistake.)
It seems to me that if labels are something like "Step 1", "Step 2", etc., then they probably don't need to be sorted by label, because the function that creates the Select knows what the order is (they have numbers behind the scenes, and can be added to the options array in the correct order rather than sorting after the fact). That is why I don't think the "natural order after translation" use case is probably necessary.
Comment #21
jhodgdonReally, not only would you not need to sort by the labels for "Step 1" etc., I think it would be better *not* to. What if in some language, it got translated as the equivalent of "First step", "Second step", etc.? Also word order is funny in some languages... and words are not necessarily consistent for all numbers... For instance if the choices were something like "1 apple", "2 apples", in some other language it might be that the numbers come afterwards, and the plural form for apples comes before alphabetically the singular, so you'd end up with "2 apples" then "1 apple" when it was sorted alphabetically.
So I think if you really had a series of numbered steps or counted items, you should order them yourself and not rely on alphabetical or natural ordering coming out right.
Comment #22
jhodgdonSo... Someone needs to make a decision on this idea. It would not be difficult to:
- Replace strcmp with natural-order comparison so that numbers are ordered better
- Add an option to use strcmp or strnatcmp
- Add an option to sort in reverse
However, the question is whether any of those are a good idea and/or a common use case, given that this is for sorting labels alphabetically post-translation.
My feeling is that they are not a common use case, and that if numbers or dates are involved (which is I think the only likely reason to want to sort DESC -- alphabetical reverse is an unlikely UI), you should be figuring out the order when you create the options and not relying on alphabetical or "natural" ordering to do the right thing in all languages.
Any other thoughts?
Comment #23
benjifisherWe discussed this issue at the Drupal usability meeting 2019-07-23.
We agreed that the additional options suggested in #16 should be postponed to a follow-up issue, for many of the reasons @jhodgdon has given:
strnatcmp()
is useful here, then we should consider offering it as an option in other places in core.Based on that, I am moving this issue back to RTBC. I will also add a follow-up issue to discuss the additional options.
I am also un-assigning this issue. (Better late than never.)
Comment #24
benjifisherI added a follow-up issue: #3069844: Provide additional options for sorting the options in Select form elements.
I tend to agree that many cases where descending order and/or "natural" comparison would be useful are cases where sorting could be done when creating the Select element. The best way to argue against that opinion would be to find a use case in core where we would want to use one of the proposed new options. If there are a lot of such examples, then we might even decide to add the options for convenience even if the sorting could be done "early".
Comment #25
jibranDo we have a follow-up issue to update https://api.drupal.org/api/drupal/developer%21topics%21forms_api_referen...?
Comment #26
benjifisher@jibran:
Unless I am confused, that page is a reference for Drupal 7, despite the "8.x" in the URL. I am not aware of any similar reference for Drupal 8.
If I am confused, or if there is a similar page for D8, can you let me know how to update it? I do not think we have any follow-up issues to update the documentation after this issue.
Comment #27
jhodgdon@benjifisher is correct -- that page is 7.x, as you will see if you scroll to the top. No idea about the suffix on the URL. How did you get to that page?
In 8.x, the reference is generated from the API docs headers in the individual form/render elements.... So if you start from
https://api.drupal.org/api/drupal
you can see on the sidebar a link to Elements, which takes you to
https://api.drupal.org/api/drupal/elements/8.7.x
It is not the same page as the old (totally unmaintainable and always out of date) Form API reference, but at least the information is accurate. To get detailed information on an element, you would click through to its class, and see the docs that will be updated in this patch on
https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21...
Comment #28
larowlanComment #29
larowlanwe can use the null coalesce operator here now as we have access to php7 features on 8.8.x
i was kind of hoping we could get the first use of the spaceship operator in core here, but alas, its not suitable :)
should we automatically do this if #empty_option/#empty_value are provided?
I can't parse this sentence, could we rewrite it - I think the grammar is slightly disjointed
I thought we were trying to avoid adding new functional tests for forms, we can use the kernel test is a form trick (https://www.previousnext.com.au/blog/drupal-8-ftw-it-test-or-it-form-act...) - should we be doing that here?
Comment #30
jhodgdonThanks for taking a look at this! I think I've addressed the review comments in #29:
- 1 -- Converted those to ?? operator. Those are the only 2 spots in the patched code area.
- 2 -- Seems to be just an observation, so did nothing.
- 3 -- Actually that sounds like a good idea. Rewrote the docs & code for that, and added to the test.
- 4 -- Rewrote that comment. Hopefully clearer.
- 5 -- Hm.... All of the existing tests for Select are all in this same class, and all I've done is added a test method and a helper method to the existing test class. How about making that a follow-up issue? I mean, I don't think it would be good to have part of the Select test where it has been, and part of it in a completely new place. Nor do I think converting the existing Select tests to a totally new way of doing things is in scope for this issue. Thoughts? For now I left it as is.
Comment #31
jhodgdonUpdating release notes in issue summary, with new default for #sort_start, and I'll also update the change record.
Comment #32
larowlanAgree, making it a follow up issue to convert to a kernel test makes sense
Thanks for all the other changes
Comment #33
jhodgdonI created a follow-up issue to address the question about kernel vs. functional tests:
#3077193: FormTest should be converted (as much as possible) to a kernel test
Comment #34
benjifisherI reviewed the updated patch. Three of my points below are just comments. Point (3) is a suggestion, and (4) is a strong suggestion. I will put off re-testing until next time.
I think I reviewed PHP requirements and the related change record on ending support for PHP 5: "PHP 5.5 only supported for existing Drupal 8 sites, new Drupal 8 sites require PHP 7.0.8". It could be clearer that the new site/old site distinction only matters for Drupal 8.7, and that Drupal 8.8 will require PHP 7.
#empty_value
property. Then I got a good night's sleep. It is safe to set#sort_start
to 1 based on the#empty_value
property since existing code will not set the#sort_options
property.I would not put parentheses around
isset($element['#empty_value'])
.I guess I missed a chance to object in an earlier patch, since only the first snippet is new. I think "#sort_start" is clearer than "sort start" or "a sort start".
Comment #35
jhodgdonThanks for the review! in comment #34.
Responses:
item 1 -- yes, larowlan also said to use the ?? operator in comment #29, so that was done
item 2 -- yes, we decided it wasn't a BC problem because, as you said, the sort_options property will not be set
item 3 -- fixed, good idea!
item 4 -- it's never too late to improve the code/comments. I agree with your suggestion. Fixed.
item 5 -- ok, good!
So, here's a new patch with the changes noted above.
Comment #36
benjifisherI reviewed an interdiff and verified the small changes since #30. Everything looks good.
The test coverage is much more thorough than my manual test, but I still think manual testing is worthwhile. As before, I installed the Umami demo profile and the latest patch from #2900409: [Meta] Improve UI of Reference field settings form. Then I added a reference field and looked at the options for the sort order. It worked in both English and Spanish. (The Spanish label for langcode is Idioma, which makes it easy to check that we are actually sorting the labels after translation and not the machine names.)
Both reviewed and tested, so I am happy to mark this issue RTBC.
Comment #37
jrockowitz CreditAttribution: jrockowitz as a volunteer and at The Big Blue House commentedI found a potential issue with this improvement. The Webform module provides a 'Select other' element where the 'Other...' option should always be last. Below is the HTML markup for a select menu with an 'Other...' option.
This is an edge case but...
Comment #38
jhodgdonThat is an interesting use case....
Since this issue is blocking progress on another usability issue, I would strongly prefer to get this in, and open up a follow-up for the #sort_end property. It would need code, tests, reviews, etc. and would further delay the other issue.
Setting back to RTBC for now, but if you feel strongly enough about it, you can set it back to Needs Work.
Comment #39
jrockowitz CreditAttribution: jrockowitz as a volunteer and at The Big Blue House commentedI am fine with RTBC.
Comment #40
larowlanComment #41
larowlanCommitted 7430b7a and pushed to 8.8.x. Thanks!
Published change record
Comment #43
jhodgdonBy the way, we never made a sort_end issue (see comment #37), but I think I'll add it to #3069844: Provide additional options for sorting the options in Select form elements.