4

In my application I have a list of keys (strings), where the user can select one of them. In the user-interface, the keys will be output according to the current locale:

<h:selectOneMenu value="#{bean.selectedKey}">
  <f:selectItems value="#{bean.allKeys}" var="_key" itemLabel="#{msgs[_key]}" />
</h:selectOneMenu>

My setup uses a standard resource-bundle configured in faces-config.xml as explained in this answer by BalusC. msgs in the example above is the name of the resource-bundle variable.

What I want now, is the items from the selectOneMenu to be sorted in alphabetic order. Of course the order depends on the used locale. The problem is, I can't/won't do the sorting in the backing-bean, as I don't know how the JSF-page will output the keys.

This problem seems quite generic to me, so I'm wondering what the best practice is to solve this kind of problem.

(Of course the problem is not only applicable to selectOneMenu. Any list/collection that will be output in the user-interface suffers from the same problem.)

Community
  • 1
  • 1
Martin Höller
  • 2,714
  • 26
  • 44

1 Answers1

10

You've basically 2 options.

  1. Sort in client side with a little help of JS. I'll for simplicity assume that you're using jQuery.

    <h:selectOneMenu ... styleClass="ordered">
    
    $("select.ordered").each(function(index, select) {
        var $select = $(select);
        $select.find("option").sort(function(left, right) {
            return left.text == right.text ? 0 : left.text < right.text ? -1 : 1;
        }).appendTo($select);
        if (!$select.find("option[selected]").length) {
            select.options.selectedIndex = 0;
        }
    });
    

  2. Sort in server side wherein you create List<SelectItem> and grab #{msgs} via injection. Assuming that you're using CDI and thus can't use @ManagedProperty("#{msgs}"), you'd need to create a producer for that first. I'll for simplicity assume that you're using OmniFaces (which is also confirmed based on your question history).

    public class BundleProducer {
    
        @Produces
        public PropertyResourceBundle getBundle() {
            return Faces.evaluateExpressionGet("#{msgs}");
        }
    
    }
    

    Then you can make use of it as follows in the backing bean associated with <f:selectItems value>:

    private static final Comparator<SelectItem> SORT_SELECTITEM_BY_LABEL = new Comparator<SelectItem>() {
        @Override
        public int compare(SelectItem left, SelectItem right) {
            return left.getLabel().compareTo(right.getLabel());
        }
    };
    
    private List<SelectItem> allKeys;
    
    @Inject
    private PropertyResourceBundle msgs;
    
    @PostConstruct
    public void init() {
        allKeys = new ArrayList<>();
    
        for (String key : keys) {
           allKeys.add(new SelectItem(key, msgs.getString(key)));
        }
    
        Collections.sort(allKeys, SORT_SELECTITEM_BY_LABEL);
    }
    

    And reference it directly without var as follows:

    <f:selectItems value="#{bean.allKeys}" />
    
BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • Thanks @BalusC, all your assumptions were correct :) Especially option one (the JavaScript solution) is new to me and I'll have a closer look. As always, very good and detailed explanations! – Martin Höller Dec 12 '14 at 13:31
  • The injection doesn't work if the bean is ViewScoped : serialization problem. – grigouille Nov 05 '21 at 10:35
  • Resource bundles are indeed request scoped. You can safely use a different bean for `` than for ``. – BalusC Nov 07 '21 at 16:01