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

  1. Make a patch.
  2. Add a test to \Drupal\Tests\system\Functional\Form\FormTest::testSelect() to test the new behavior.

Remaining tasks

  1. 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,
];
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

jhodgdon created an issue. See original summary.

jhodgdon’s picture

Status: Active » Needs review
FileSize
9.53 KB

Here'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.

jhodgdon’s picture

FileSize
9.45 KB
659 bytes

Fixing 2 coding standards messages.

benjifisher’s picture

Issue summary: View changes
Status: Needs review » Needs work

I have a few nit-picks and one more substantive suggestion at the end:

  1. +++ b/core/includes/form.inc
    @@ -90,7 +97,17 @@ function form_select_options($element, $choices = NULL) {
           return [];
         }
         $choices = $element['#options'];
    +    $sort_options = isset($element['#sort_options']) && $element['#sort_options'];
    +    $sort_start = (isset($element['#sort_start']) ? $element['#sort_start'] : 0);
       }
    +  else {
    +    // We are within an option group.
    +    $sort_options = isset($choices['#sort_options']) && $choices['#sort_options'];
    +    $sort_start = (isset($choices['#sort_start']) ? $choices['#sort_start'] : 0);
    +    unset($choices['#sort_options']);
    +    unset($choices['#sort_start']);
    +  }
        

    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').

  2. @@ -125,6 +142,16 @@ function form_select_options($element, $choices = NULL) {
    +  if ($sort_options) {
    +    $unsorted = array_slice($options, 0, $sort_start);
    +    $sorted = array_slice($options, $sort_start);
    +    uasort($sorted, function ($a, $b) {
    +        $label1 = (string) $a['label'];
    +        $label2 = (string) $b['label'];
    +        return strcmp($label1, $label2);
    +    });
    +    $options = array_merge($unsorted, $sorted);
    +  }
        

    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?

  3. +++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestSelectForm.php
    @@ -125,6 +126,61 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    ...
    +    $sortable_options = [];
    +    foreach ($unsorted_options as $key => $item) {
    ...
    +    }
    ...
    +    $sortable_options['sso_zzgroup']['#sort_options'] = TRUE;
        

    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.

  4. From the issue description:
    $form['example_select'] = [
      '#type' => 'select',
      '#title' => $this->t('Select element'),
             '#options' => [
              '_none' => $this->t('- None -'),
            ] + $options,
            '#ajax' => TRUE,
            '#sort_options' => TRUE,
            '#sort_start' => 1,
    ];
        

    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.

jhodgdon’s picture

Regarding 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?

jhodgdon’s picture

Status: Needs work » Needs review
FileSize
7.75 KB
10.26 KB

So... 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

benjifisher’s picture

Issue summary: View changes
Status: Needs review » Needs work
Issue tags: +Needs change record

Looking 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:

  1. +++ b/core/includes/form.inc
    @@ -63,6 +64,12 @@ function template_preprocess_select(&$variables) {
    + *   - #sort_options: (optional) If set to TRUE (default is FALSE), sort the
    + *     options by their labels. Can be set within an option group to sort that
    + *     group.
    + *   - #sort_start: (optional) Option index to start sorting at, where 0 is the
    + *     first option (default is 0, and this only applies if #sort_options is
    + *     TRUE). Can be used within an option group.
    +++ b/core/lib/Drupal/Core/Render/Element/Select.php
    @@ -14,6 +14,14 @@
    + * - #sort_options: (optional) If set to TRUE (default is FALSE), sort the
    + *   options by their labels, after rendering and translation is complete.
    + *   Can be set within an option group to sort that group.
    + * - #sort_start: (optional) Option index to start sorting at, where 0 is the
    + *   first option (default is 0, and this only applies if #sort_options is
    + *   TRUE). Can be used within an option group. If an empty option is being
    + *   added automatically (see #empty_option and #empty_value properties), and
    + *   you want it to stay at the top of the list, be sure to start sorting at 1.
       

    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."

  2. +++ b/core/includes/form.inc
    @@ -90,7 +97,17 @@ function form_select_options($element, $choices = NULL) {
    +    $sort_options = isset($element['#sort_options']) && $element['#sort_options'];
    +    $sort_start = (isset($element['#sort_start']) ? $element['#sort_start'] : 0);
       }
    +  else {
    +    // We are within an option group.
    +    $sort_options = isset($choices['#sort_options']) && $choices['#sort_options'];
    +    $sort_start = isset($choices['#sort_start']) ? $choices['#sort_start'] : 0;
    +    unset($choices['#sort_options']);
    +    unset($choices['#sort_start']);
    +  }
       

    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.

jhodgdon’s picture

Status: Needs work » Needs review
Issue tags: -Needs change record
FileSize
5.31 KB
13.02 KB

Thanks 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

benjifisher’s picture

Status: Needs review » Reviewed & tested by the community

@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.

jhodgdon’s picture

Issue tags: +blocker

Adding tag, as this is blocking fixing a usability bug (parent issue).

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/includes/form.inc
    @@ -90,7 +72,17 @@ function form_select_options($element, $choices = NULL) {
    +    // We are within an option group.
    +    $sort_options = isset($choices['#sort_options']) && $choices['#sort_options'];
    +    $sort_start = isset($choices['#sort_start']) ? $choices['#sort_start'] : 0;
    

    As far as I can see there is no test coverage of using #sort_start in an option group.

  2. +++ b/core/modules/system/tests/src/Functional/Form/FormTest.php
    @@ -464,6 +464,73 @@ public function testEmptySelect() {
    +    // Verify the expected order of the options in the unsorted and
    +    // sorted elements.
    +    $expected_order = [
    +      'uso_first',
    +      'uso_second',
    +      'uso_zzgroup',
    +      'uso_gc',
    +      'uso_ga',
    +      'uso_gb',
    +      'uso_yygroup',
    +      'uso_ge',
    +      'uso_gd',
    +      'uso_gf',
    +      'uso_d',
    +      'uso_c',
    +      'uso_b',
    +      'uso_a',
    +      'sso_first',
    +      'sso_second',
    +      'sso_zzgroup',
    +      'sso_ga',
    +      'sso_gb',
    +      'sso_gc',
    +      'sso_a',
    +      'sso_d',
    +      'sso_b',
    +      'sso_c',
    +      'sso_yygroup',
    +      'sso_ge',
    +      'sso_gd',
    +      'sso_gf',
    +      'sno_empty',
    +      'sno_first',
    +      'sno_second',
    +      'sno_zzgroup',
    +      'sno_ga',
    +      'sno_gb',
    +      'sno_gc',
    +      'sno_a',
    +      'sno_d',
    +      'sno_b',
    +      'sno_c',
    +      'sno_yygroup',
    +      'sno_ge',
    +      'sno_gd',
    +      'sno_gf',
    +    ];
    +
    +    $prev_position = 0;
    +    foreach ($expected_order as $string) {
    +      $position = strpos($content, $string);
    +      $this->assertTrue($position > $prev_position, 'Item ' . $string . ' is in correct order');
    +      $prev_position = $position;
    +    }
    

    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:

        $option_map_function = function (NodeElement $option) {
          if ($option->getTagName() === 'optgroup') {
            return $option->getAttribute('label');
          }
          return $option->getValue();
        };
    
        $field = $this->getSession()->getPage()->findField('sorted');
        $options = array_map($option_map_function, $field->findAll('css', 'option,optgroup'));
        $this->assertIdentical([
          'sso_first_element',
          'sso_second',
          'sso_zzgroup',
          'sso_ga',
          'sso_gb',
          'sso_gc',
          'sso_a',
          'sso_d',
          'sso_b',
          'sso_c',
          'sso_yygroup',
          'sso_ge',
          'sso_gd',
          'sso_gf',
        ], $options);
    

    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.

  3. +++ b/core/modules/system/tests/src/Functional/Form/FormTest.php
    @@ -464,6 +464,73 @@ public function testEmptySelect() {
    +    $this->assertFalse(strpos($content, '#sort_order'), 'Sort order property has been removed');
    +    $this->assertFalse(strpos($content, '#sort_start'), 'Sort start property has been removed');
    

    These should be using the correct assertion. $this->assertSession()->responseNotContains()

jhodgdon’s picture

Status: Needs work » Needs review
FileSize
14.36 KB
5.36 KB

Good 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).

benjifisher’s picture

Status: Needs review » Needs work
  public function testSelectSorting() {
    $this->drupalGet('form-test/select');
    $content = $this->getSession()->getPage()->getContent();

The $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.

  /**
   * Validates that the options are in the right order in a select.
   *
   * @param string $select
   *   Name of the select to verify.
   * @param string[] $order
   *   Expected order of its options.
   */
  protected function validateSelectSorting($select, $order) {
    $option_map_function = function (NodeElement $option) {
      if ($option->getTagName() === 'optgroup') {
        return $option->getAttribute('label');
      }
      return $option->getValue();
    };

    $field = $this->getSession()->getPage()->findField($select);
    $options = array_map($option_map_function,
      $field->findAll('css', 'option, optgroup'));
    $this->assertIdentical($order, $options);
  }

Kudos for using string[] in the @param comment instead of the less specific array. 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:

    $option_map_function = function (NodeElement $node) {
      return ($node->getTagName() === 'optgroup')
        ? $node->getAttribute('label')
        : $node->getValue();
    };

The variable name $field does not suggest a NodeElement of type select. I also think it is clearer to eliminate that intermediate variable:

    $option_nodes = $this->getSession()
      ->getPage()
      ->findField($select)
      ->findAll('css', 'option, optgroup');
    $options = array_map($option_map_function, $option_nodes);
    $this->assertIdentical($order, $options);

You can also consider combining the last two lines (eliminating the $options variable) and/or using an anonymous function inside array_map() (eliminating the $option_map_function variable).

jhodgdon’s picture

Status: Needs work » Needs review
FileSize
14.3 KB
1.63 KB

Those 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.

benjifisher’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community

@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.

goodboy’s picture

1. 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' ?

catch’s picture

Status: Reviewed & tested by the community » Needs review

Back to needs review for #16.

jhodgdon’s picture

Hm. 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?

goodboy’s picture

Status: Needs review » Reviewed & tested by the community

List [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.

jhodgdon’s picture

Status: Reviewed & tested by the community » Needs review

(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.

jhodgdon’s picture

Really, 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.

jhodgdon’s picture

So... 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?

benjifisher’s picture

Assigned: jhodgdon » Unassigned
Status: Needs review » Reviewed & tested by the community

We 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:

  1. We do not want to delay this issue and #2900409: [Meta] Improve UI of Reference field settings form with further discussion.
  2. The additional options may be an edge case. We can continue to discuss whether to add them in the follow-up issue.
  3. If strnatcmp() is useful here, then we should consider offering it as an option in other places in core.
  4. As usual, we want to avoid adding complexity to core unless a feature will be widely useful.
  5. The two options added by the current patch will actually be used in core, assuming that #2900409: [Meta] Improve UI of Reference field settings form is adopted.

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.)

benjifisher’s picture

I 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".

jibran’s picture

benjifisher’s picture

@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.

jhodgdon’s picture

@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...

larowlan’s picture

larowlan’s picture

Status: Reviewed & tested by the community » Needs review
  1. +++ b/core/includes/form.inc
    @@ -90,7 +72,17 @@ function form_select_options($element, $choices = NULL) {
    +    $sort_start = isset($element['#sort_start']) ? $element['#sort_start'] : 0;
    ...
    +    $sort_start = isset($choices['#sort_start']) ? $choices['#sort_start'] : 0;
    

    we can use the null coalesce operator here now as we have access to php7 features on 8.8.x

  2. +++ b/core/includes/form.inc
    @@ -125,6 +117,14 @@ function form_select_options($element, $choices = NULL) {
    +    uasort($sorted, function ($a, $b) {
    +      return strcmp((string) $a['label'], (string) $b['label']);
    +    });
    

    i was kind of hoping we could get the first use of the spaceship operator in core here, but alas, its not suitable :)

  3. +++ b/core/lib/Drupal/Core/Render/Element/Select.php
    @@ -9,11 +9,32 @@
    + *   you want it to stay at the top of the list, be sure to start sorting at 1.
    
    @@ -68,6 +89,8 @@ public function getInfo() {
    +      '#sort_start' => 0,
    

    should we automatically do this if #empty_option/#empty_value are provided?

  4. +++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestSelectForm.php
    @@ -125,11 +126,80 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    +      // Don't use $this->t() here, because we really don't want these
    +      // to be translated or added to localize.d.o. Using TranslatableMarkup
    +      // in places tests that casting to string is working, however.
    

    I can't parse this sentence, could we rewrite it - I think the grammar is slightly disjointed

  5. +++ b/core/modules/system/tests/src/Functional/Form/FormTest.php
    @@ -464,6 +465,104 @@ public function testEmptySelect() {
    +  public function testSelectSorting() {
    

    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?

jhodgdon’s picture

FileSize
15.46 KB
6 KB

Thanks 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.

jhodgdon’s picture

Issue summary: View changes

Updating release notes in issue summary, with new default for #sort_start, and I'll also update the change record.

larowlan’s picture

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

Agree, making it a follow up issue to convert to a kernel test makes sense

Thanks for all the other changes

jhodgdon’s picture

I 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

benjifisher’s picture

Status: Needs review » Needs work

I 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.

  1. In #4, I wrote,

    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.

    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.

  2. When I first read Comment #29.3, I was afraid that it would be a BC problem since existing code uses the #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.
  3. +++ b/core/lib/Drupal/Core/Render/Element/Select.php
    @@ -129,6 +152,9 @@ public static function processSelect(&$element, FormStateInterface $form_state,
    +    // Provide the correct default value for #sort_start.
    +    $element['#sort_start'] = $element['#sort_start'] ??
    +      ((isset($element['#empty_value'])) ? 1 : 0);
        

    I would not put parentheses around isset($element['#empty_value']).

  4. +++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestSelectForm.php
    @@ -4,6 +4,7 @@
    ...
    +    // Add a select to test sorting at the top level, and with some of the
    +    // option groups sorted, some left alone, and at least one with sort start
    +    // set to a non-default value.
    ...
    +    // Add a select to test sorting with a -NONE- option included,
    +    // and a sort start set.
    ...
    +    // Add a select to test sorting with a -NONE- option included,
    +    // and sort start not set.
        

    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".

  5. I reviewed the updates to both the release notes snippet in the issue summary and the change record. I think the changes are clear.
jhodgdon’s picture

Status: Needs work » Needs review
FileSize
2.23 KB
15.46 KB

Thanks 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.

benjifisher’s picture

Status: Needs review » Reviewed & tested by the community

I 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.

jrockowitz’s picture

Status: Reviewed & tested by the community » Needs work

I 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.

<select class="js-webform-choices webform-choices form-select" data-drupal-selector="edit-select-other-select" id="edit-select-other-select" name="select_other[select]">
<option value="" selected="selected">- None -</option>
<option value="Caucasian">Caucasian</option>
<option value="Latino/Hispanic">Latino/Hispanic</option>
<option value="Middle Eastern">Middle Eastern</option>
<option value="African">African</option>
<option value="Caribbean">Caribbean</option>
<option value="South Asian">South Asian</option>
<option value="East Asian">East Asian</option>
<option value="Mixed">Mixed</option>
<option value="_other_">Other…</option>
</select>

This is an edge case but...

Should there be an '#sort_end' property?

jhodgdon’s picture

Status: Needs work » Reviewed & tested by the community

That 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.

jrockowitz’s picture

I am fine with RTBC.

larowlan’s picture

larowlan’s picture

Status: Reviewed & tested by the community » Fixed
Issue tags: +8.8.0 highlights

Committed 7430b7a and pushed to 8.8.x. Thanks!

Published change record

  • larowlan committed 7430b7a on 8.8.x
    Issue #3065903 by jhodgdon, benjifisher, larowlan, alexpott: Add label...
jhodgdon’s picture

By 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.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.