1

I'm currently trying to implement a dynamic survey page containing pages, sections and questions defined by the user (part of a larger application). The user may defined different question types, which will render a different component (radio, textarea, textfield, selectionlist, ...). This app is currently deployed in Wildfly 16 / Java 8 / JSF 2.3 / Servlet 4.

Since the user may define a specific set of values and associated images for the radio button, I need to customize the radio button output. So my option was to use the new group attribute available in selectoneradio. Using this new attribute is causing erratic behaviour when used inside a multi-level ui:repeat.

The following example was simplified and created in order to illustrate the problem.

Class

package test;

import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.annotation.PostConstruct;
import javax.faces.view.ViewScoped;
import javax.inject.Named;

@Named("test")
@ViewScoped
public class Test implements Serializable
{
    private static final long serialVersionUID = 1L;

    private static Map<String, Map<String, String>> questionMap = new TreeMap<>();

    private static Map<String, String> fixedValuesMap = new TreeMap<>();

    private Map<String, String> answerMap = new TreeMap<>();

    private List<String> sections = Arrays.asList("Section_1", "Section_2", "Section_3", "Section_4");

    static
    {
        fixedValuesMap.put("1", "Value 1");
        fixedValuesMap.put("2", "Value 2");
        fixedValuesMap.put("3", "Value 3");
        fixedValuesMap.put("4", "Value 4");
        fixedValuesMap.put("5", "Value 5");

        Map<String, String> sec1questions = new TreeMap<>();
        sec1questions.put("1", "Question 1");
        sec1questions.put("2", "Question 2");
        sec1questions.put("3", "Question 3");
        questionMap.put("Section_1", sec1questions);

        Map<String, String> sec2questions = new TreeMap<>();
        sec2questions.put("4", "Question 4");
        questionMap.put("Section_2", sec2questions);

        Map<String, String> sec3questions = new TreeMap<>();
        sec3questions.put("5", "Question 5");
        questionMap.put("Section_3", sec3questions);

        Map<String, String> sec4questions = new TreeMap<>();
        sec4questions.put("6", "Question 6");
        questionMap.put("Section_4", sec4questions);
    }

    public Test()
    {
    }

    @PostConstruct
    private void init()
    {
        answerMap.put("1", null);
        answerMap.put("2", null);
        answerMap.put("3", null);
        answerMap.put("4", null);
        answerMap.put("5", null);
        answerMap.put("6", null);
    }

    public String getQuestionType(String index)
    {
        switch(index)
        {
            case "1":
                return "RADIO_BUTTON";
            case "2":
                return "RADIO_BUTTON";
            case "3":
                return "RADIO_BUTTON";
            case "4":
                return "TEXT_AREA";
            case "5":
                return "RADIO_BUTTON";
            case "6":
                return "FREE_TEXT";
            default:
                return "FREE_TEXT";
        }
    }

    public Map<String, String> getQuestions(String section)
    {
        return questionMap.get(section);
    }

    public Map<String, String> getAnswerMap()
    {
        return answerMap;
    }

    public Map<String, String> getFixedValues()
    {
        return fixedValuesMap;
    }

    public List<String> getSections()
    {
        return sections;
    }

    public void changeRadio(String questionKey, String questionValue)
    {
        answerMap.put(questionKey, questionValue);
    }

    public String submit()
    {
        for(Map.Entry<String, String> entry : answerMap.entrySet())
            System.out.println("ENTRY: " + entry.getKey() + " - " + entry.getValue());

        return null;
    }
}

XHTML

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
        xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
        xmlns:f="http://xmlns.jcp.org/jsf/core"
        xmlns:h="http://xmlns.jcp.org/jsf/html"
        xmlns:a="http://xmlns.jcp.org/jsf/passthrough">

    <h:head>
        <meta charset="utf-8"/>
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
        <meta name="description" content="My Test"/>

        <link rel="stylesheet" href="#{request.contextPath}/resources/bootstrap/css/bootstrap.min.css" />
        <title>My Test</title>
    </h:head>

    <h:body>

        <div class="container-fluid">

            <div class="row">

                <main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-4">

                    <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
                        <h1 class="h2">UI Repeat Test</h1>
                    </div>

                    <h:form role="form" prependId="false">

                        <div class="row mx-2">

                            <ui:repeat var="sec" value="#{test.sections}">

                                <div class="col-12">

                                    <ui:repeat var="question" value="#{test.getQuestions(sec)}">

                                        <div>
                                            <div class="col-12">
                                                <div class="form-group">
                                                    <label for="answer">#{question.key} - #{question.value}</label>

                                                    <div class="col-12">

                                                        <h:panelGroup rendered="#{test.getQuestionType(question.key) eq 'FREE_TEXT'}" layout="block">
                                                            <h:inputText value="#{test.answerMap[question.key]}" styleClass="form-control"/>
                                                        </h:panelGroup>

                                                        <h:panelGroup rendered="#{test.getQuestionType(question.key) eq 'TEXT_AREA'}"  layout="block">
                                                            <h:inputTextarea value="#{test.answerMap[question.key]}" styleClass="form-control" rows="2" />
                                                        </h:panelGroup>

                                                        <h:panelGroup rendered="#{test.getQuestionType(question.key) eq 'RADIO_BUTTON'}"  layout="block">

                                                            <table class="radio-label checkbox">
                                                                <tbody>
                                                                    <tr>
                                                                        <ui:repeat var="item" value="#{test.fixedValues.entrySet()}">
                                                                            <td class="text-center grid-margin">    
                                                                                <h:selectOneRadio group="#{question.key}" value="#{test.answerMap[question.key]}" layout="lineDirection" styleClass="checkbox"  >
                                                                                        <f:selectItem itemValue="#{item.key}" itemLabel="#{item.value}" />
                                                                                </h:selectOneRadio>
                                                                            </td>   
                                                                        </ui:repeat>
                                                                    </tr>
                                                                </tbody>
                                                            </table>

                                                        </h:panelGroup>

                                                    </div>  
                                            </div>
                                            </div>
                                        </div>

                                    </ui:repeat>

                                </div>

                            </ui:repeat>

                        </div>

                        <div class="row mx-2 my-2">
                            <div class="col-12">
                                <h:commandButton value="Test Me" type="submit" action="#{test.submit}" styleClass="btn btn-primary" />
                            </div>
                        </div>

                    </h:form>

                </main>

            </div>

        </div>

        <script src="#{request.contextPath}/resources/jquery/jquery.slim.min.js"></script>
        <script src="#{request.contextPath}/resources/bootstrap/js/bootstrap.bundle.min.js"></script>

    </h:body>

</html>

When the form is submitted only the last radio button and text field values are stored. If I replace the selectItems ui:repeat

<table class="radio-label checkbox">
  <tbody>
    <tr>
      <ui:repeat var="item" value="#{test.fixedValues.entrySet()}">
        <td class="text-center grid-margin">    
          <h:selectOneRadio group="#{question.key}" value="#{test.answerMap[question.key]}" layout="lineDirection" styleClass="checkbox"  >
            <f:selectItem itemValue="#{item.key}" itemLabel="#{item.value}" />
          </h:selectOneRadio>
        </td>   
      </ui:repeat>
    </tr>
  </tbody>
</table>

with

<h:selectOneRadio value="#{test.answerMap[question.key]}" layout="lineDirection" >
  <f:selectItems value="#{test.fixedValues.entrySet()}" var="item" itemLabel="#{item.key}" itemValue="#{item.value}" />
</h:selectOneRadio>

everything works fine.

I really need to use the first option, so I can add some images to the radio button values.
Am I missing something?

Selaron
  • 6,105
  • 4
  • 31
  • 39
areal
  • 157
  • 5
  • And check https://stackoverflow.com/questions/7415230/uiform-with-prependid-false-breaks-fajax-render And please remove all non-relevant tags... Make a real [mcve]. Way easier to check things then – Kukeltje Oct 23 '19 at 11:19
  • @Kukeltje Just tried without the prependId attribute and it's the same behaviour. – areal Oct 23 '19 at 11:26
  • @Selaron I'm not sure if I understood correctly, but the question.key is a unique value. Also, if I change the selectoneRadio to use the selectItems (the usual way) everything works fine. – areal Oct 23 '19 at 11:28
  • I did not say it would solve it ;-) Just wanted to put your attention to it. And you are grouping on nothing from the repeat and/or items. That does not sound right – Kukeltje Oct 23 '19 at 11:30
  • You are right, I didn't see that only one of these components is rendered per question. ( rendered="#{test.getQuestionType(question.key) eq ... }" ) – Selaron Oct 23 '19 at 11:30
  • @Kukeltje I'm using the new group attribute available in selectOneRadio, and I'm grouping using the question.key. Could you please explain what do you mean by "And you are grouping on nothing from the repeat and/or items"? – areal Oct 23 '19 at 13:08

2 Answers2

0

It seemes like you have found an issue. I tried to simplify the example as follows:

My managed Bean:

package my.pkg;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.PostConstruct;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;

@Named
@RequestScoped
public class MyBean {
    private List<String> items;

    private List<Number> numbers;

    private Map<String, Object> mapVal;

    @PostConstruct
    public void init() {
        mapVal = new LinkedHashMap<>();
        items = new ArrayList<>();
        numbers = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            items.add("group_" + i);
            numbers.add(i);
        }
    }

// getters/setters ...

}

Example form:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
    xmlns:f="http://xmlns.jcp.org/jsf/core"
    xmlns:h="http://xmlns.jcp.org/jsf/html"
    xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">

<h:head />

<h:body>

    <h:form id="frm1">

        <ui:repeat var="grp" value="#{myBean.items}">

            <ui:repeat var="num" value="#{myBean.numbers}" varStatus="stat">

                <h:selectOneRadio id="radioButton" value="#{myBean.mapVal[grp]}"
                    group="myGroup_#{grp}">
                    <f:selectItem itemValue="#{num}" itemLabel="#{grp}: #{num}" />
                </h:selectOneRadio>

                <br />

            </ui:repeat>

            <hr />
        </ui:repeat>

        <h:commandButton value="submit" />

        <h:outputText value="myBean.mapVal: #{myBean.mapVal}" />
    </h:form>

</h:body>
</html>

User interface an example input:

enter image description here

After submission I'd expect output to be:

myBean.mapVal: {group_0=0, group_1=1, group_2=2}

But it is:

myBean.mapVal: {group_2=2}

The radio buttons from group 0 and 1 lose their selection.

For some inputs I get the expected results:

myBean.mapVal: {group_0=0, group_1=0, group_2=0}
myBean.mapVal: {group_0=1, group_1=1, group_2=1}
myBean.mapVal: {group_0=2, group_1=2, group_2=2}

... using Mojarra 2.3.9.


For MyFaces 2.3.4 I get the expected result for the selection in above screenshot:

{group_0=0, group_1=1, group_2=2}

but it fails for other selections:

1)
expected: {group_0=0, group_1=0, group_2=0}
but is: {group_2=0}

2)
expected: {group_0=0, group_1=0, group_2=2}
but is: {group_1=0, group_2=2}

3)
expected: {group_0=1, group_1=1, group_2=0}
but is: {group_1=1, group_2=0}

In my opinion we should reportan issue for Mojarra and ( if not already present) for MyFaces.


Workaround

If I replace

    <ui:repeat var="grp" value="#{myBean.items}">

by

    <c:forEach var="grp" items="#{myBean.items}">

I get the expected results for Mojarra and MyFaces.

Selaron
  • 6,105
  • 4
  • 31
  • 39
  • The 'workaround' working is logical since it creates a completely different component tree. But what if you use the `varStatus` index instead of the `var`? – Kukeltje Oct 23 '19 at 16:33
  • Thank you @Selaron. I tried your solution/workaround and it works. I'll try to report and issue with Mojarra! – areal Oct 23 '19 at 17:27
  • @Kukeltje if I add `varStatus="groupStat"` to the outer `ui:repeat`, and use `group="myGroup_#{groupStat.index}"` it's the same unexpected behavior. – Selaron Oct 24 '19 at 06:15
  • ok, see my comment in https://stackoverflow.com/questions/7435039/hselectoneradio-renders-table-element-how-to-avoid-this, it might 'help'. I unfortunately don't have the time to try myself due to a short one week holiday – Kukeltje Oct 24 '19 at 06:24
  • @Kukeltje I had tried use case (2) from [this](https://download.oracle.com/javaee-archive/javaserverfaces-spec-public.java.net/jsr372-experts/2017/05/1595.html) discussion also explained in the post linked by you, but it did not help either. – Selaron Oct 24 '19 at 07:14
0

Like @Selaron suggested, I created an issue under eclipse-ee4j/mojarra

Thank you for the help!

areal
  • 157
  • 5