Sunday, October 26, 2014

ADF - Displaying Multi-Line Text with outputText Component

With ADF, the inputText component can be used to generate a multi-row text area control in browser, allowing the user to enter multi-line text. To display the multi-line text that the user input, a common practice is using a read-only multi-row inputText component. I bet this approach isn't always what you want, as the inputText component has fixed rows, and shows a scrollbar when necessary. In this post, I'm describing how to display multi-line text with the outputText component. It's about CSS line-wrapping and word-breaking pertaining to ADF faces components.

To display multi-line text, the basic idea is applying the white-space CSS property to the element with one of following values: pre, pre-wrap and pre-line. This property controls the processing of whitespace inside an element. The exact value you need depends on the behavior you want. In this post, I'm using pre-wrap as the example, which means "Sequences of whitespace are preserved. Lines are broken at newline characters, at <br>, and as necessary to fill line boxes.". Here's the code snippet from the skin of the sample application:

.PreWrap {
    white-space: -moz-pre-wrap; /* Mozilla */
    white-space: pre;           /* CSS 2.0 */
    white-space: pre-wrap;      /* CSS 2.1 */
    word-wrap: break-word;      /* IE 5.5-7 */
}

In this rule, the word-wrap CSS property specifies whether the break within a word is allowed to prevent overflow when an otherwise-unbreakable string is too long to fit within line box. The property is applies only when the white-space property allows wrapping. The value of break-word means "Normally unbreakable words may be broken at arbitrary points if there are no otherwise acceptable break points in the line." This property is useful for the cases, such as really long URLs.

For example, the normal word-breaking runs like the following:


As you can see, it tries the best to wrap the text to fit within the line box by breaking the text after the hyphens which are acceptable break points. But it still overflows.

With break-word, it runs like this:


Now, it breaks the text at arbitrary points in addition to those acceptable break points.

The white-space and word-wrap properties are supposed to be what exactly you need to display multi-line text with the outputText ADF Faces component, but it's not the whole story. You have to deal with the layout component to make sure the outputText component inside it works as you expect. Here, I'm talking about the panelFormLayout and then panelGridLayout components - the most common layout components that the multi-line outputText components will work with.

To work with the panelFormLayout component, wrap the outputText component with the PreWrap style class in a panelLabelAndMessage component, and put it into the panelFormLayout component:

<af:panelFormLayout id="pfl1" labelWidth="100" fieldWidth="350">
    <af:inputText id="it1" label="inputText"
                  placeholder="Add some multi-line text and click Update to display it below."
                  contentStyle="width:340px;" rows="12" value="#{pageFlowScope.text}"/>
    <af:button text="Update" id="b_updt"/>
    <af:panelLabelAndMessage id="plam1" label="outputText" labelStyle="vertical-align:top">
        <af:outputText id="ot1" value="#{pageFlowScope.text}" partialTriggers="b_updt"
                       styleClass="PreWrap"/>
    </af:panelLabelAndMessage>
</af:panelFormLayout>

If there is a long unbreakable string in the text, it works like this (See panelFormLayout page in the sample application):



Even with the fieldWidth="350" property, the content cells (with yellow background) of the panelFormLayout still expand to fit the longest unbreakable string.

The panelFormLayout component generates table elements to layout the components inside it. By default, the table element uses the automatic table layout algorithm. In case the content does not fit in the column,  the column's width is set by the preferred minimum width, e.g. by trying all acceptable line breaks.

This is how it works - by default. Fortunately, we can change this behavior by changing the table-layout CSS property of the table from auto to fixed. For example:

.FixedTableLayout af|panelFormLayout::column > table {
    table-layout: fixed;
    width: 100%;
}

This ADF skin rule makes sure that tables generated by the panelFormLayout component for columns use the fixed layout algorithm: "Table and column widths are set by the widths of table and col elements or by the width of the first row of cells. Cells in subsequent rows do not affect column widths".  Apply the rule to the example (panelFormLayout-fixedTableLayout.jsf):

<af:panelFormLayout id="pfl1" labelWidth="100" fieldWidth="350" styleClass="FixedTableLayout">
    <af:inputText id="it1" label="inputText"
                  placeholder="Add some multi-line text and click Update to display it below."
                  contentStyle="width:340px;" rows="12" value="#{pageFlowScope.text}"/>
    <af:button text="Update" id="b_updt"/>
    <af:panelLabelAndMessage id="plam1" label="outputText" labelStyle="vertical-align:top">
        <af:outputText id="ot1" value="#{pageFlowScope.text}" partialTriggers="b_updt"
                       styleClass="PreWrap"/>
    </af:panelLabelAndMessage>
</af:panelFormLayout>


Or, keeping the table-layout: auto property, to achieve the same purpose, we can wrap the outputText component inside a fixed-width containing box (panelFormLayout-fixedWidthContainer.jsf):

<af:panelFormLayout id="pfl1" labelWidth="100" fieldWidth="350">
    <af:inputText id="it1" label="inputText"
                  placeholder="Add some multi-line text and click Update to display it below."
                  contentStyle="width:340px;" rows="12" value="#{pageFlowScope.text}"/>
    <af:button text="Update" id="b_updt"/>
    <af:panelLabelAndMessage id="plam1" label="outputText" labelStyle="vertical-align:top">
        <af:panelGroupLayout id="pgl1" layout="vertical" styleClass="FixedWidthContainer">
            <af:outputText id="ot1" value="#{pageFlowScope.text}" partialTriggers="b_updt"
                           styleClass="PreWrap"/>
        </af:panelGroupLayout>
    </af:panelLabelAndMessage>
</af:panelFormLayout>

Here's the CSS rule:

.FixedWidthContainer {
    display: block;
    width: 340px;
    background-color: #fbf; /* for visual hint only */
}


We are good with the panelFormLayout component now. Let's try out the panelGridLayout Component. If we put the multi-line outputText component directly inside a gridCell component as in the following code snippet (panelGridLayout.jsf):

<af:gridCell id="gc4" marginStart="10px" marginEnd="10px" width="340px"
             inlineStyle="background-color:#ffb;">
    <af:outputText id="ot1" value="#{pageFlowScope.text}" partialTriggers="b_updt"
                   styleClass="PreWrap"/>
</af:gridCell>

It works like this:


Pretty much the same problem as that in the panelFormLayout component. That's because for the geometry management purpose, the gridCell component generates an absolutely positioned div element inside the div element generated by the panelGridLayout component. For an absolutely positioned block, it width is determined by so-called shrink-to-fit width - similar to calculating the width of a table cell using the automatic table layout algorithm.

We don't have a layout property this time, but we can still work around this by wrapping the outputText component inside a fixed-width containing block (panelGridLayout-fixedWidthContainer.jsf):

<af:gridCell id="gc4" marginStart="10px" marginEnd="10px" styleClass="af_gridCell">
    <af:panelGroupLayout id="pgl3" layout="vertical" styleClass="FixedWidthContainer">
        <af:outputText id="ot1" value="#{pageFlowScope.text}" partialTriggers="b_updt"
                       styleClass="PreWrap"/>
    </af:panelGroupLayout>
</af:gridCell>

Now, it works like this:


In summary, we can make use of the white-space and word-wrap CSS properties to display multi-line text with the outputText component. To get the expected width of the outputText component in a panelFormLayout or panelGridLayout component, we can wrap the outputText component in a fixed-width containing box. For the panelFormLayout component particularly, we can also apply table-layout: fixed to the content cells of the panelFormLayout component in the skin file to enforce the field width.

Chinese Summary:

利用 white-spaceword-wrap CSS 属性,我们可以用 ADF 的 outputText 组件显示自动折行的多行文本。将 outputText 组件配合 panelFormLayoutpanelGridLayout 布局组件使用时,为了保证 outputText 输出指定宽度的多行文本,可以将 outputText 组件包裹在一个定宽的 panelGroupLayout 组件中,然后再置于 panelFormLayout 或者 panelGridLayout 组件中。对于 panelFormLayout 组件,也可以在 ADF 皮肤文件中为 panelFormLayout 组件的 column 伪元素中对应的 table 元素设置 table-layout: fixed 属性,以关闭默认的表格自动布局算法,保证表格按照指定的宽度布局。

Resources:
Sample Application:

Environment:

  • Mac OS X Version 10.10
  • Safari Version 8.0
  • JDeveloper 12.1.3.0.0 Build JDEVADF_12.1.3.0.0_GENERIC_140521.1008.S
  • Oracle Alta UI

1 comment: