orbeon / orbeon-forms

Orbeon Forms is an open source web forms solution. It includes an XForms engine, the Form Builder web-based form editor, and the Form Runner runtime.
http://www.orbeon.com/
GNU Lesser General Public License v2.1
518 stars 220 forks source link

fr:date: control on new row keeps previous row value #3687

Closed avernet closed 6 years ago

avernet commented 6 years ago

To reproduce:

  1. In Form Builder, create a form, add a repeated grid, in it add a date control. This adds the new date control based on bootstrap-datepicker.
  2. Publish, and run the form (or just use Test).
  3. Enter a date in the first row, click on dropdown to the right of the date and select "Insert Above".

The newly added row has been initialized correctly, however the first row incorrectly still shows the same date, while it should be empty. This component implements methods from the JavaScript livecycle instead of reacting to XForms events. On the client:

Doing things this way is OK, but then the controls on the first row should be notified that their value has changed, e.g. was reset in the above example. And maybe this should be consistently done for methods of the JavaScript lifecycle and for XForms events, even if the latter might be more tricky, depending on when the events are dispatched.

ebruchez commented 6 years ago

There are two distinct aspects to controls within repeats:

  1. Controls in the control tree on the server.
    • These can be refreshed multiple times during an Ajax request.
    • The identity of controls can be preserved when nodes are preserved, like when an iteration is added between other iterations. (It depends how fr:grid does the updates, in particular.)
  2. Updates sent by the server to the client.
    • The server never tells the client to insert iterations between others. New iterations are always added and removed at the end.
    • So the representation of controls on the client does not follow that of the server.

So say you have 2 iterations, and you insert a row before the first iteration. The server sees:

A  C → new control with new value
B  A → xxforms-iteration-moved
   B → xxforms-iteration-moved

While the client must see:

1  1 → existing control with updated value
2  2 → existing control with updated value (doesn't work right now)
   3 → new control with new value

We can think of two solutions, as suggested:

  1. Send the appropriate xformsUpdateValue, etc.
    • Probably the easiest solution.
  2. Improve the server → client protocol to support inserting/deleting rows other than at the end.
    • This would also reduce server → client chatter in case of inserts in large grids.
    • We can think of adding a client-side iterationMoved() function in the JavaScript lifecycle API.
ebruchez commented 6 years ago

Some of this is discussed in "Smarter repeat updates between client/server" (#1274) from 5 years ago.

ebruchez commented 6 years ago

Open question, based on #1274: if we choose the second solution, do we need to "assign new unique ids to iterations, so that the client can keep references from id to JavaScript structure"?

ebruchez commented 6 years ago

It is unclear whether #1274 rightly calls for different ids. The client maps instances of XBLCompanion to the component's container DOM element using the data API with key xforms-xbl-object. If, say, the client moves an element in the DOM in response to a server event, the element will be preserved and the associated data as well. We could add a lifecycle call to indicate that the iteration/position has changed, but I am not sure we need a new id scheme.

ebruchez commented 6 years ago

The xformsUpdateValue on the companion is called on the client when receiving a value from the server. So if we want to use this, we need the server to communicate that.

ebruchez commented 6 years ago

The server does send an empty <value/>, but then it also calls a scriptxf_0cfac97c799fcfcae00c2263a166faec9b60753e`.

<xxf:event-response xmlns:xxf="http://orbeon.org/oxf/xml/xforms">
    <xxf:action>
        <xxf:control-values>
            <xxf:copy-repeat-template id="fr-view-component≡my-section-section≡grid-2-grid≡grid-2-grid-repeat" parent-indexes=""
                                      start-suffix="2" end-suffix="2"/>
            <xxf:control class="+can-remove +can-move-down" id="fr-view-component≡my-section-section≡grid-2-grid≡xf-742⊙1"/>
            <xxf:control id="fr-view-component≡my-section-section≡grid-2-grid≡my-date-control⊙1">
                <xxf:value/>
            </xxf:control>
            <xxf:control class="fr-grid-repeat-iteration can-remove can-move-down can-insert-above can-insert-below fr-grid-body"
                         id="fr-view-component≡my-section-section≡grid-2-grid≡xf-742⊙2"/>
            <xxf:control id="fr-view-component≡my-section-section≡grid-2-grid≡xf-762⊙2" class="fr-repeat-column-left"/>
            <xxf:attribute for="fr-view-component≡my-section-section≡grid-2-grid≡xf-763⊙2" name="aria-label">Menu</xxf:attribute>
            <xxf:control id="fr-view-component≡my-section-section≡grid-2-grid≡xf-765⊙2" class="fr-grid-td"/>
            <xxf:control id="fr-view-component≡my-section-section≡grid-2-grid≡my-date-control⊙2" class="fr-grid-1-1" label="Date"
                         init="true">
                <xxf:value>2018-10-02</xxf:value>
            </xxf:control>
            <xxf:attribute for="fr-view-component≡my-section-section≡grid-2-grid≡my-date-control≡xf-778⊙2" name="placeholder">MM/DD/YYYY
            </xxf:attribute>
            <xxf:control id="fr-view-component≡my-section-section≡grid-2-grid≡xf-767⊙2" class="fr-grid-td"/>
        </xxf:control-values>
        <xxf:repeat-indexes>
            <xxf:repeat-index id="xf-276" new-index="0"/>
        </xxf:repeat-indexes>
        <xxf:script name="xf_0cfac97c799fcfcae00c2263a166faec9b60753e"
                    target-id="fr-view-component≡my-section-section≡grid-2-grid≡my-date-control⊙1"
                    observer-id="fr-view-component≡my-section-section≡grid-2-grid≡my-date-control⊙1">
            <xxf:param>[M]/[D]/[Y]</xxf:param>
        </xxf:script>
    </xxf:action>
</xxf:event-response>
ebruchez commented 6 years ago

That's in response to this:

<xbl:handler event="xxforms-visible">
    <xf:action type="javascript">
        <xf:param name="format" value="xxf:property('oxf.xforms.format.input.date')"/>
        <xf:body>ORBEON.xforms.XBL.instanceForControl(this).setFormat(format)</xf:body>
    </xf:action>
</xbl:handler>

The control on the first iteration is newly-visible, runs that script, which sets the format. The implementation of setFormat calls the date picker to set the current date again. On the client, the date picker is that associated with the previously-selected date on the previous first iteration.

def setFormat(format: String): Unit = {
  // On iOS, ignore the format as the native widget uses its own format
  if (! iOS) {
    val date = datePicker.getDate
    datePicker.options.format = format
      .replaceAllLiterally("[D]", "d")
      .replaceAllLiterally("[M]", "m")
      .replaceAllLiterally("[Y]", "yyyy")
    datePicker.setDate(date)
  }
}
ebruchez commented 6 years ago

Verified that the reason it is not working is that when we get a blank value, we try to parse it as an ISO date, and we fail, and the picker stays the same. Instead, we must handle the case of a blank value separately and call datePicker.clearDates().

ebruchez commented 6 years ago

In addition, the format doesn't get set when iterations are moved, and so we must respond to xxforms-iteration-moved.

ebruchez commented 6 years ago

Entered #3765 for a declarative way of propagating XBL parameters to the client.