Bauke Scholtz created MYFACES-4777:
--------------------------------------
Summary: With rowStatePreserved="true", a row without saved state
of its own shows the local value of the row before it
Key: MYFACES-4777
URL: https://issues.apache.org/jira/browse/MYFACES-4777
Project: MyFaces Core
Issue Type: Bug
Affects Versions: 4.1.5
Reporter: Bauke Scholtz
With {{rowStatePreserved="true"}}, an {{<h:dataTable>}} or {{<ui:repeat>}}
restores the initial state of its children when it enters a row which has no
saved state of its own yet, such as a row added to the model after the last
render. When a child had nothing in its state helper initially, that restore
keeps what the row before left in the child. So the new row shows the local
value of the row before it.
h3. Reproducer
{code:xml}
<h:form id="form">
<h:dataTable id="table" value="#{bean.tableRows}" var="row"
rowStatePreserved="true">
<h:column>
<h:panelGroup id="cell">
<f:event type="postAddToView" listener="#{bean.addInput}" />
</h:panelGroup>
</h:column>
</h:dataTable>
<ui:repeat id="repeat" value="#{bean.repeatRows}" var="row"
rowStatePreserved="true">
<h:panelGroup id="cell">
<f:event type="postAddToView" listener="#{bean.addInput}" />
</h:panelGroup>
</ui:repeat>
<h:commandButton id="submit" value="Submit" />
<h:commandButton id="addRow" value="Add row" action="#{bean.addRow}">
<f:ajax execute="@this" render="@form" />
</h:commandButton>
</h:form>
{code}
{code:java}
@Named
@ViewScoped
public class Bean implements Serializable {
private List<String> tableRows = new ArrayList<>(List.of("0", "1"));
private List<String> repeatRows = new ArrayList<>(List.of("0", "1"));
// Adds an input with only an id, so that its initial state has nothing in
its state helper.
public void addInput(ComponentSystemEvent event) {
UIComponent cell = event.getComponent();
if (cell.getChildCount() == 0) {
HtmlInputText input = new HtmlInputText();
input.setId("in");
cell.getChildren().add(input);
}
}
public void addRow() {
tableRows.add(String.valueOf(tableRows.size()));
repeatRows.add(String.valueOf(repeatRows.size()));
}
// Getters.
}
{code}
The input has no value expression, so it keeps its local value after the submit.
# Type {{typed}} in row 1 of the table and of the repeat, and click Submit.
# Click Add row. The ajax request adds row 2 to the model and re-renders the
form.
h3. Expected
Row 1 shows {{typed}}, and the new row 2 is empty.
h3. Actual
Row 2 of the table and of the repeat shows {{typed}}, the local value of row 1.
Reproduced on MyFaces 4.1.5-SNAPSHOT on Tomcat 11, with server side state
saving.
h3. Cause
When {{UIData}} enters a row without saved state of its own,
{{restoreFullDescendantComponentStates()}} restores the initial full state of
each child:
{code:java}
component.clearInitialState();
component.restoreState(facesContext, childState);
component.markInitialState();
{code}
{{UIComponentBase.restoreState()}} passes the state helper slot of that full
state to {{_DeltaStateHelper.restoreState()}}. The full state of a component
which had nothing in its state helper has {{null}} in that slot, and
{{_DeltaStateHelper.restoreState()}} then returns at once:
{code:java}
if (state == null)
{
return;
}
{code}
So the state helper keeps what the row before put in it. For a {{UIInput}},
that includes {{value}} and {{localValueSet}}. {{UIRepeat}} restores the
initial state of its rows in the same way. The reproducer creates the input in
Java with only an id, so that its state helper is empty initially.
h3. Spec
The javadoc of {{UIData.setRowStatePreserved()}}:
{quote}
To accomplish this, {{UIData}} must call {{StateHolder#saveState}} and
{{TransientStateHolder#saveTransientState}} on its children to capture their
state on exiting each row. When re-entering the row,
{{StateHolder#restoreState}} and {{TransientStateHolder#restoreTransientState}}
must be called in order to reinitialize the children to the correct state for
the new row.
{quote}
The local value of row 1 applies to row 2, so the children of row 2 are not in
the correct state for that row.
The specification, section "StateHolder", on {{saveState()}} and
{{restoreState()}}:
{quote}
Gets or restores the state of the instance as a _Serializable_ _Object_.
{quote}
Restoring the full state of a component which had nothing in its state helper
does not restore that state, because the current state helper stays as it is.
h3. Proposed fix
When {{UIComponentBase.restoreState()}} restores a full state ({{values.length
== FULL_STATE_ARRAY_SIZE}}) whose state helper slot is {{null}}, clear the
state helper. A delta keeps its current meaning, where {{null}} means no change.
Mojarra had the same bug, and fixed it the same way in 4.0.30 and 4.1.21
([eclipse-ee4j/mojarra#6128|https://github.com/eclipse-ee4j/mojarra/issues/6128]).
TCK: {{Issue6128IT}} of the Jakarta Faces 5.0 TCK
([jakartaee/faces#2266|https://github.com/jakartaee/faces/pull/2266]). MyFaces
4.1.5-SNAPSHOT fails it on an equivalent page.
🤖 Generated with Claude Opus 5.5
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)